| 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 | |