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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
29TIASurface::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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
68void 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
104cerr << "INITIALIZE:\n"
105 << "TIA:\n"
106 << "src: " << myTiaSurface->srcRect() << endl
107 << "dst: " << myTiaSurface->dstRect() << endl
108 << endl;
109cerr << "SLine:\n"
110 << "src: " << mySLineSurface->srcRect() << endl
111 << "dst: " << mySLineSurface->dstRect() << endl
112 << endl;
113#endif
114}
115
116// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
117void 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
127const 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
157uInt32 TIASurface::mapIndexedPixel(uInt8 indexedColor, uInt8 shift)
158{
159 return myPalette[indexedColor | shift];
160}
161
162
163// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
164void 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
184void 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
195uInt32 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
209void 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
233inline 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
250void 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
273string 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
299inline 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
318void 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
405void 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