1 | //============================================================================ |
2 | // |
3 | // SSSS tt lll lll |
4 | // SS SS tt ll ll |
5 | // SS tttttt eeee ll ll aaaa |
6 | // SSSS tt ee ee ll ll aa |
7 | // SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" |
8 | // SS SS tt ee ll ll aa aa |
9 | // SSSS ttt eeeee llll llll aaaaa |
10 | // |
11 | // Copyright (c) 1995-2019 by Bradford W. Mott, Stephen Anthony |
12 | // and the Stella Team |
13 | // |
14 | // See the file "License.txt" for information on usage and redistribution of |
15 | // this file, and for a DISCLAIMER OF ALL WARRANTIES. |
16 | //============================================================================ |
17 | |
18 | #include <cmath> |
19 | |
20 | #include "FBSurface.hxx" |
21 | #include "Settings.hxx" |
22 | #include "OSystem.hxx" |
23 | #include "Console.hxx" |
24 | #include "TIA.hxx" |
25 | #include "PNGLibrary.hxx" |
26 | #include "TIASurface.hxx" |
27 | |
28 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
29 | TIASurface::TIASurface(OSystem& system) |
30 | : myOSystem(system), |
31 | myFB(system.frameBuffer()), |
32 | myTIA(nullptr), |
33 | myFilter(Filter::Normal), |
34 | myUsePhosphor(false), |
35 | myPhosphorPercent(0.60f), |
36 | myScanlinesEnabled(false), |
37 | myPalette(nullptr), |
38 | mySaveSnapFlag(false) |
39 | { |
40 | // Load NTSC filter settings |
41 | myNTSCFilter.loadConfig(myOSystem.settings()); |
42 | |
43 | // Create a surface for the TIA image and scanlines; we'll need them eventually |
44 | myTiaSurface = myFB.allocateSurface(AtariNTSC::outWidth(TIAConstants::frameBufferWidth), |
45 | TIAConstants::frameBufferHeight); |
46 | |
47 | // Generate scanline data, and a pre-defined scanline surface |
48 | constexpr uInt32 scanHeight = TIAConstants::frameBufferHeight * 2; |
49 | uInt32 scanData[scanHeight]; |
50 | for(uInt32 i = 0; i < scanHeight; i += 2) |
51 | { |
52 | scanData[i] = 0x00000000; |
53 | scanData[i+1] = 0xff000000; |
54 | } |
55 | mySLineSurface = myFB.allocateSurface(1, scanHeight, scanData); |
56 | |
57 | // Base TIA surface for use in taking snapshots in 1x mode |
58 | myBaseTiaSurface = myFB.allocateSurface(TIAConstants::frameBufferWidth*2, |
59 | TIAConstants::frameBufferHeight); |
60 | |
61 | myRGBFramebuffer.fill(0); |
62 | |
63 | // Enable/disable threading in the NTSC TV effects renderer |
64 | myNTSCFilter.enableThreading(myOSystem.settings().getBool("threads" )); |
65 | } |
66 | |
67 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
68 | void TIASurface::initialize(const Console& console, |
69 | const FrameBuffer::VideoMode& mode) |
70 | { |
71 | myTIA = &(console.tia()); |
72 | |
73 | myTiaSurface->setDstPos(mode.image.x(), mode.image.y()); |
74 | myTiaSurface->setDstSize(mode.image.w(), mode.image.h()); |
75 | mySLineSurface->setDstPos(mode.image.x(), mode.image.y()); |
76 | mySLineSurface->setDstSize(mode.image.w(), mode.image.h()); |
77 | |
78 | // Phosphor mode can be enabled either globally or per-ROM |
79 | int p_blend = 0; |
80 | bool enable = false; |
81 | |
82 | if(myOSystem.settings().getString("tv.phosphor" ) == "always" ) |
83 | { |
84 | p_blend = myOSystem.settings().getInt("tv.phosblend" ); |
85 | enable = true; |
86 | } |
87 | else |
88 | { |
89 | p_blend = atoi(console.properties().get(PropType::Display_PPBlend).c_str()); |
90 | enable = console.properties().get(PropType::Display_Phosphor) == "YES" ; |
91 | } |
92 | enablePhosphor(enable, p_blend); |
93 | |
94 | setNTSC(NTSCFilter::Preset(myOSystem.settings().getInt("tv.filter" )), false); |
95 | |
96 | // Scanline repeating is sensitive to non-integral vertical resolution, |
97 | // so rounding is performed to eliminate it |
98 | // This won't be 100% accurate, but non-integral scaling isn't 100% |
99 | // accurate anyway |
100 | mySLineSurface->setSrcSize(1, 2 * int(float(mode.image.h()) / |
101 | floorf((float(mode.image.h()) / myTIA->height()) + 0.5f))); |
102 | |
103 | #if 0 |
104 | cerr << "INITIALIZE:\n" |
105 | << "TIA:\n" |
106 | << "src: " << myTiaSurface->srcRect() << endl |
107 | << "dst: " << myTiaSurface->dstRect() << endl |
108 | << endl; |
109 | cerr << "SLine:\n" |
110 | << "src: " << mySLineSurface->srcRect() << endl |
111 | << "dst: " << mySLineSurface->dstRect() << endl |
112 | << endl; |
113 | #endif |
114 | } |
115 | |
116 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
117 | void TIASurface::setPalette(const uInt32* tia_palette, const uInt32* rgb_palette) |
118 | { |
119 | myPalette = tia_palette; |
120 | |
121 | // The NTSC filtering needs access to the raw RGB data, since it calculates |
122 | // its own internal palette |
123 | myNTSCFilter.setTIAPalette(rgb_palette); |
124 | } |
125 | |
126 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
127 | const FBSurface& TIASurface::baseSurface(Common::Rect& rect) const |
128 | { |
129 | uInt32 tiaw = myTIA->width(), width = tiaw * 2, height = myTIA->height(); |
130 | rect.setBounds(0, 0, width, height); |
131 | |
132 | // Get Blargg buffer and width |
133 | uInt32 *blarggBuf, blarggPitch; |
134 | myTiaSurface->basePtr(blarggBuf, blarggPitch); |
135 | double blarggXFactor = double(blarggPitch) / width; |
136 | bool useBlargg = ntscEnabled(); |
137 | |
138 | // Fill the surface with pixels from the TIA, scaled 2x horizontally |
139 | uInt32 *buf_ptr, pitch; |
140 | myBaseTiaSurface->basePtr(buf_ptr, pitch); |
141 | |
142 | for(uInt32 y = 0; y < height; ++y) |
143 | { |
144 | for(uInt32 x = 0; x < width; ++x) |
145 | { |
146 | if (useBlargg) |
147 | *buf_ptr++ = blarggBuf[y * blarggPitch + uInt32(nearbyint(x * blarggXFactor))]; |
148 | else |
149 | *buf_ptr++ = myPalette[*(myTIA->frameBuffer() + y * tiaw + x / 2)]; |
150 | } |
151 | } |
152 | |
153 | return *myBaseTiaSurface; |
154 | } |
155 | |
156 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
157 | uInt32 TIASurface::mapIndexedPixel(uInt8 indexedColor, uInt8 shift) |
158 | { |
159 | return myPalette[indexedColor | shift]; |
160 | } |
161 | |
162 | |
163 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
164 | void TIASurface::setNTSC(NTSCFilter::Preset preset, bool show) |
165 | { |
166 | ostringstream buf; |
167 | if(preset == NTSCFilter::Preset::OFF) |
168 | { |
169 | enableNTSC(false); |
170 | buf << "TV filtering disabled" ; |
171 | } |
172 | else |
173 | { |
174 | enableNTSC(true); |
175 | const string& mode = myNTSCFilter.setPreset(preset); |
176 | buf << "TV filtering (" << mode << " mode)" ; |
177 | } |
178 | myOSystem.settings().setValue("tv.filter" , int(preset)); |
179 | |
180 | if(show) myFB.showMessage(buf.str()); |
181 | } |
182 | |
183 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
184 | void TIASurface::setScanlineIntensity(int amount) |
185 | { |
186 | ostringstream buf; |
187 | uInt32 intensity = enableScanlines(amount); |
188 | buf << "Scanline intensity at " << intensity << "%" ; |
189 | myOSystem.settings().setValue("tv.scanlines" , intensity); |
190 | |
191 | myFB.showMessage(buf.str()); |
192 | } |
193 | |
194 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
195 | uInt32 TIASurface::enableScanlines(int relative, int absolute) |
196 | { |
197 | FBSurface::Attributes& attr = mySLineSurface->attributes(); |
198 | if(relative == 0) attr.blendalpha = absolute; |
199 | else attr.blendalpha += relative; |
200 | attr.blendalpha = std::max(0, Int32(attr.blendalpha)); |
201 | attr.blendalpha = std::min(100u, attr.blendalpha); |
202 | |
203 | mySLineSurface->applyAttributes(); |
204 | |
205 | return attr.blendalpha; |
206 | } |
207 | |
208 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
209 | void TIASurface::enablePhosphor(bool enable, int blend) |
210 | { |
211 | if(myUsePhosphor == enable && myPhosphorPercent == blend / 100.0f) |
212 | return; |
213 | |
214 | myUsePhosphor = enable; |
215 | if(blend >= 0) |
216 | myPhosphorPercent = blend / 100.0f; |
217 | myFilter = Filter(enable ? uInt8(myFilter) | 0x01 : uInt8(myFilter) & 0x10); |
218 | |
219 | myRGBFramebuffer.fill(0); |
220 | |
221 | // Precalculate the average colors for the 'phosphor' effect |
222 | if(myUsePhosphor) |
223 | { |
224 | for(int c = 255; c >= 0; c--) |
225 | for(int p = 255; p >= 0; p--) |
226 | myPhosphorPalette[c][p] = getPhosphor(uInt8(c), uInt8(p)); |
227 | |
228 | myNTSCFilter.setPhosphorPalette(myPhosphorPalette); |
229 | } |
230 | } |
231 | |
232 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
233 | inline uInt32 TIASurface::getRGBPhosphor(const uInt32 c, const uInt32 p) const |
234 | { |
235 | #define TO_RGB(color, red, green, blue) \ |
236 | const uInt8 red = color >> 16; const uInt8 green = color >> 8; const uInt8 blue = color; |
237 | |
238 | TO_RGB(c, rc, gc, bc) |
239 | TO_RGB(p, rp, gp, bp) |
240 | |
241 | // Mix current calculated frame with previous displayed frame |
242 | const uInt8 rn = myPhosphorPalette[rc][rp]; |
243 | const uInt8 gn = myPhosphorPalette[gc][gp]; |
244 | const uInt8 bn = myPhosphorPalette[bc][bp]; |
245 | |
246 | return (rn << 16) | (gn << 8) | bn; |
247 | } |
248 | |
249 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
250 | void TIASurface::enableNTSC(bool enable) |
251 | { |
252 | myFilter = Filter(enable ? uInt8(myFilter) | 0x10 : uInt8(myFilter) & 0x01); |
253 | |
254 | // Normal vs NTSC mode uses different source widths |
255 | myTiaSurface->setSrcSize(enable ? AtariNTSC::outWidth(TIAConstants::frameBufferWidth) |
256 | : TIAConstants::frameBufferWidth, myTIA->height()); |
257 | |
258 | FBSurface::Attributes& tia_attr = myTiaSurface->attributes(); |
259 | tia_attr.smoothing = myOSystem.settings().getBool("tia.inter" ); |
260 | myTiaSurface->applyAttributes(); |
261 | |
262 | myScanlinesEnabled = myOSystem.settings().getInt("tv.scanlines" ) > 0; |
263 | FBSurface::Attributes& sl_attr = mySLineSurface->attributes(); |
264 | sl_attr.smoothing = true; |
265 | sl_attr.blending = myScanlinesEnabled; |
266 | sl_attr.blendalpha = myOSystem.settings().getInt("tv.scanlines" ); |
267 | mySLineSurface->applyAttributes(); |
268 | |
269 | myRGBFramebuffer.fill(0); |
270 | } |
271 | |
272 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
273 | string TIASurface::effectsInfo() const |
274 | { |
275 | const FBSurface::Attributes& attr = mySLineSurface->attributes(); |
276 | |
277 | ostringstream buf; |
278 | switch(myFilter) |
279 | { |
280 | case Filter::Normal: |
281 | buf << "Disabled, normal mode" ; |
282 | break; |
283 | case Filter::Phosphor: |
284 | buf << "Disabled, phosphor mode" ; |
285 | break; |
286 | case Filter::BlarggNormal: |
287 | buf << myNTSCFilter.getPreset() << ", scanlines=" << attr.blendalpha << "/" |
288 | << (attr.smoothing ? "inter" : "nointer" ); |
289 | break; |
290 | case Filter::BlarggPhosphor: |
291 | buf << myNTSCFilter.getPreset() << ", phosphor, scanlines=" |
292 | << attr.blendalpha << "/" << (attr.smoothing ? "inter" : "nointer" ); |
293 | break; |
294 | } |
295 | return buf.str(); |
296 | } |
297 | |
298 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
299 | inline uInt32 TIASurface::averageBuffers(uInt32 bufOfs) |
300 | { |
301 | uInt32 c = myRGBFramebuffer[bufOfs]; |
302 | uInt32 p = myPrevRGBFramebuffer[bufOfs]; |
303 | |
304 | // Split into RGB values |
305 | TO_RGB(c, rc, gc, bc) |
306 | TO_RGB(p, rp, gp, bp) |
307 | |
308 | // Mix current calculated buffer with previous calculated buffer (50:50) |
309 | const uInt8 rn = (rc + rp) / 2; |
310 | const uInt8 gn = (gc + gp) / 2; |
311 | const uInt8 bn = (bc + bp) / 2; |
312 | |
313 | // return averaged value |
314 | return (rn << 16) | (gn << 8) | bn; |
315 | } |
316 | |
317 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
318 | void TIASurface::render() |
319 | { |
320 | uInt32 width = myTIA->width(), height = myTIA->height(); |
321 | |
322 | uInt32 *out, outPitch; |
323 | myTiaSurface->basePtr(out, outPitch); |
324 | |
325 | switch(myFilter) |
326 | { |
327 | case Filter::Normal: |
328 | { |
329 | uInt8* tiaIn = myTIA->frameBuffer(); |
330 | |
331 | uInt32 bufofs = 0, screenofsY = 0, pos; |
332 | for(uInt32 y = 0; y < height; ++y) |
333 | { |
334 | pos = screenofsY; |
335 | for (uInt32 x = width / 2; x; --x) |
336 | { |
337 | out[pos++] = myPalette[tiaIn[bufofs++]]; |
338 | out[pos++] = myPalette[tiaIn[bufofs++]]; |
339 | } |
340 | screenofsY += outPitch; |
341 | } |
342 | break; |
343 | } |
344 | |
345 | case Filter::Phosphor: |
346 | { |
347 | uInt8* tiaIn = myTIA->frameBuffer(); |
348 | uInt32* rgbIn = myRGBFramebuffer.data(); |
349 | |
350 | if (mySaveSnapFlag) |
351 | std::copy_n(myRGBFramebuffer.begin(), width * height, |
352 | myPrevRGBFramebuffer.begin()); |
353 | |
354 | uInt32 bufofs = 0, screenofsY = 0, pos; |
355 | for(uInt32 y = height; y ; --y) |
356 | { |
357 | pos = screenofsY; |
358 | for(uInt32 x = width / 2; x ; --x) |
359 | { |
360 | // Store back into displayed frame buffer (for next frame) |
361 | rgbIn[bufofs] = out[pos++] = getRGBPhosphor(myPalette[tiaIn[bufofs]], rgbIn[bufofs]); |
362 | ++bufofs; |
363 | rgbIn[bufofs] = out[pos++] = getRGBPhosphor(myPalette[tiaIn[bufofs]], rgbIn[bufofs]); |
364 | ++bufofs; |
365 | } |
366 | screenofsY += outPitch; |
367 | } |
368 | break; |
369 | } |
370 | |
371 | case Filter::BlarggNormal: |
372 | { |
373 | myNTSCFilter.render(myTIA->frameBuffer(), width, height, out, outPitch << 2); |
374 | break; |
375 | } |
376 | |
377 | case Filter::BlarggPhosphor: |
378 | { |
379 | if(mySaveSnapFlag) |
380 | std::copy_n(myRGBFramebuffer.begin(), height * outPitch, |
381 | myPrevRGBFramebuffer.begin()); |
382 | |
383 | myNTSCFilter.render(myTIA->frameBuffer(), width, height, out, outPitch << 2, myRGBFramebuffer.data()); |
384 | break; |
385 | } |
386 | } |
387 | |
388 | // Draw TIA image |
389 | myTiaSurface->render(); |
390 | |
391 | // Draw overlaying scanlines |
392 | if(myScanlinesEnabled) |
393 | mySLineSurface->render(); |
394 | |
395 | if(mySaveSnapFlag) |
396 | { |
397 | mySaveSnapFlag = false; |
398 | #ifdef PNG_SUPPORT |
399 | myOSystem.png().takeSnapshot(); |
400 | #endif |
401 | } |
402 | } |
403 | |
404 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
405 | void TIASurface::renderForSnapshot() |
406 | { |
407 | // TODO: This is currently called from PNGLibrary::takeSnapshot() only |
408 | // Therefore the code could be simplified. |
409 | // At some point, we will probably merge some of the functionality. |
410 | // Furthermore, toggling the variable 'mySaveSnapFlag' in different places |
411 | // is brittle, especially since rendering can happen in a different thread. |
412 | |
413 | uInt32 width = myTIA->width(); |
414 | uInt32 height = myTIA->height(); |
415 | uInt32 pos = 0; |
416 | uInt32 *outPtr, outPitch; |
417 | |
418 | myTiaSurface->basePtr(outPtr, outPitch); |
419 | |
420 | mySaveSnapFlag = false; |
421 | switch (myFilter) |
422 | { |
423 | // For non-phosphor modes, render the frame again |
424 | case Filter::Normal: |
425 | case Filter::BlarggNormal: |
426 | render(); |
427 | break; |
428 | |
429 | // For phosphor modes, copy the phosphor framebuffer |
430 | case Filter::Phosphor: |
431 | { |
432 | uInt32 bufofs = 0, screenofsY = 0; |
433 | for(uInt32 y = height; y; --y) |
434 | { |
435 | pos = screenofsY; |
436 | for(uInt32 x = width / 2; x; --x) |
437 | { |
438 | outPtr[pos++] = averageBuffers(bufofs++); |
439 | outPtr[pos++] = averageBuffers(bufofs++); |
440 | } |
441 | screenofsY += outPitch; |
442 | } |
443 | break; |
444 | } |
445 | |
446 | case Filter::BlarggPhosphor: |
447 | uInt32 bufofs = 0; |
448 | for(uInt32 y = height; y; --y) |
449 | for(uInt32 x = outPitch; x; --x) |
450 | outPtr[pos++] = averageBuffers(bufofs++); |
451 | break; |
452 | } |
453 | |
454 | if(myUsePhosphor) |
455 | { |
456 | // Draw TIA image |
457 | myTiaSurface->render(); |
458 | |
459 | // Draw overlaying scanlines |
460 | if(myScanlinesEnabled) |
461 | mySLineSurface->render(); |
462 | } |
463 | } |
464 | |