1 module gbaid.gba.display; 2 3 import core.sync.mutex : Mutex; 4 import core.sync.condition : Condition; 5 6 import std.meta : Alias, AliasSeq; 7 import std.conv : to; 8 import std.algorithm.comparison : min; 9 import std.algorithm.mutation : swap; 10 11 import gbaid.util; 12 13 import gbaid.gba.io; 14 import gbaid.gba.memory; 15 import gbaid.gba.dma; 16 import gbaid.gba.interrupt; 17 import gbaid.gba.assembly; 18 19 public enum uint DISPLAY_WIDTH = 240; 20 public enum uint DISPLAY_HEIGHT = 160; 21 22 public enum uint BLANK_LENGTH = 68; 23 public enum uint TIMING_WIDTH = DISPLAY_WIDTH + BLANK_LENGTH; 24 public enum uint TIMING_HEIGTH = DISPLAY_HEIGHT + BLANK_LENGTH; 25 26 public enum uint CYCLES_PER_DOT = 4; 27 public enum size_t CYCLES_PER_FRAME = TIMING_WIDTH * TIMING_HEIGTH * CYCLES_PER_DOT; 28 29 private struct DisplayControl { 30 private byte bgMode = 0; 31 private bool frameIndex = 0; 32 private bool objMap1D = false; 33 private bool forceBlank = false; 34 private byte layerEnableFlags = 0; 35 private byte windowEnableFlags = 0; 36 } 37 38 private struct DisplayStatus { 39 private bool inVBlank = false; 40 private bool inHBlank = false; 41 private bool vCountMatch = false; 42 private bool intVBlankEnabled = false; 43 private bool intHBlankEnabled = false; 44 private bool intVCounterEnabled = false; 45 private ubyte vCountTarget = 0; 46 private ubyte vCounter = 0; 47 } 48 49 private struct BackgroundControl { 50 private byte priority = 0; 51 private byte tileBase = 0; 52 private bool mosaicEnabled = false; 53 private bool singlePalette = false; 54 private byte mapBase = 0; 55 private bool overflowWrapAround = false; 56 private byte size = 0; 57 } 58 59 private struct BackgroundOffset { 60 private short x = 0; 61 private short y = 0; 62 } 63 64 private struct BackgroundTransform { 65 private short a = 0; 66 private short b = 0; 67 private short c = 0; 68 private short d = 0; 69 private int x = 0; 70 private int y = 0; 71 private int cx = 0; 72 private int cy = 0; 73 } 74 75 private struct WindowSize { 76 private ubyte endX = 0; 77 private ubyte startX = 0; 78 private ubyte endY = 0; 79 private ubyte startY = 0; 80 } 81 82 private struct WindowControl { 83 private byte layerEnableFlags = 0; 84 private bool specialEffectEnabled = false; 85 } 86 87 private struct MosaicSize { 88 private byte x = 0; 89 private byte y = 0; 90 } 91 92 private struct SpecialEffect { 93 private byte firstTargetFlags = 0; 94 private byte effectType = 0; 95 private byte secondTargetFlags = 0; 96 } 97 98 private struct BlendCoefficients { 99 private byte eva = 0; 100 private byte evb = 0; 101 private byte evy = 0; 102 } 103 104 public class Display { 105 private static enum short TRANSPARENT = cast(short) 0x8000; 106 private Palette* palette; 107 private Vram* vram; 108 private Oam* oam; 109 private InterruptHandler interruptHandler; 110 private DMAs dmas; 111 private FrameSwapper _frameSwapper; 112 mixin declareFields!(short[DISPLAY_WIDTH], true, "linePixels", 0, 6); 113 private alias objectLinePixels = linePixels!4; 114 private alias infoLinePixels = linePixels!5; 115 private DisplayControl control; 116 private DisplayStatus status; 117 mixin declareFields!(BackgroundControl, true, "bgControl", BackgroundControl.init, 4); 118 mixin declareFields!(BackgroundOffset, true, "bgOffset", BackgroundOffset.init, 4); 119 mixin declareFields!(BackgroundTransform, true, "bgTransform", BackgroundTransform.init, 2); 120 mixin declareFields!(WindowSize, true, "windowSize", WindowSize.init, 2); 121 mixin declareFields!(WindowControl, true, "windowControl", WindowControl.init, 4); 122 private alias outWindowCtrl = windowControl!2; 123 private alias objWindowCtrl = windowControl!3; 124 private MosaicSize bgMosaicSize; 125 private MosaicSize objMosaicSize; 126 private SpecialEffect specialEffect; 127 private BlendCoefficients blendCoefficients; 128 private int line = 0; 129 private int dot = 0; 130 131 public this(IoRegisters* ioRegisters, Palette* palette, Vram* vram, Oam* oam, 132 InterruptHandler interruptHandler, DMAs dmas) { 133 this.palette = palette; 134 this.vram = vram; 135 this.oam = oam; 136 this.interruptHandler = interruptHandler; 137 this.dmas = dmas; 138 139 _frameSwapper = new FrameSwapper(); 140 141 ioRegisters.mapAddress(0x0, &control.bgMode, 0b111, 0); 142 ioRegisters.mapAddress(0x0, &control.frameIndex, 0b1, 4); 143 ioRegisters.mapAddress(0x0, &control.objMap1D, 0b1, 6); 144 ioRegisters.mapAddress(0x0, &control.forceBlank, 0b1, 7); 145 ioRegisters.mapAddress(0x0, &control.layerEnableFlags, 0x1F, 8); 146 ioRegisters.mapAddress(0x0, &control.windowEnableFlags, 0b111, 13); 147 148 ioRegisters.mapAddress(0x4, &status.inVBlank, 0b1, 0, true, false); 149 ioRegisters.mapAddress(0x4, &status.inHBlank, 0b1, 1, true, false); 150 ioRegisters.mapAddress(0x4, &status.vCountMatch, 0b1, 2, true, false); 151 ioRegisters.mapAddress(0x4, &status.intVBlankEnabled, 0b1, 3); 152 ioRegisters.mapAddress(0x4, &status.intHBlankEnabled, 0b1, 4); 153 ioRegisters.mapAddress(0x4, &status.intVCounterEnabled, 0b1, 5); 154 ioRegisters.mapAddress(0x4, &status.vCountTarget, 0xFF, 8); 155 ioRegisters.mapAddress(0x4, &status.vCounter, 0xFF, 16, true, false); 156 157 foreach (i; AliasSeq!(0, 1, 2, 3)) { 158 enum address = (i / 2) * 4 + 0x8; 159 enum shift = (i % 2) * 16; 160 ioRegisters.mapAddress(address, &bgControl!i.priority, 0b11, shift); 161 ioRegisters.mapAddress(address, &bgControl!i.tileBase, 0b11, shift + 2); 162 ioRegisters.mapAddress(address, &bgControl!i.mosaicEnabled, 0b1, shift + 6); 163 ioRegisters.mapAddress(address, &bgControl!i.singlePalette, 0b1, shift + 7); 164 ioRegisters.mapAddress(address, &bgControl!i.mapBase, 0x1F, shift + 8); 165 ioRegisters.mapAddress(address, &bgControl!i.overflowWrapAround, 0b1, shift + 13); 166 ioRegisters.mapAddress(address, &bgControl!i.size, 0b11, shift + 14); 167 } 168 169 foreach (i; AliasSeq!(0, 1, 2, 3)) { 170 enum address = i * 4 + 0x10; 171 ioRegisters.mapAddress(address, &bgOffset!i.x, 0x1FF, 0, false, true); 172 ioRegisters.mapAddress(address, &bgOffset!i.y, 0x1FF, 16, false, true); 173 } 174 175 foreach (i; AliasSeq!(0, 1)) { 176 enum address = i * 16 + 0x20; 177 ioRegisters.mapAddress(address, &bgTransform!i.a, 0xFFFF, 0, false, true); 178 ioRegisters.mapAddress(address, &bgTransform!i.b, 0xFFFF, 16, false, true); 179 ioRegisters.mapAddress(address + 0x4, &bgTransform!i.c, 0xFFFF, 0, false, true); 180 ioRegisters.mapAddress(address + 0x4, &bgTransform!i.d, 0xFFFF, 16, false, true); 181 ioRegisters.mapAddress(address + 0x8, &bgTransform!i.x, 0xFFFFFFF, 0, false, true) 182 .preWriteMonitor(&onAffineReferencePointPreWrite!(i, false)); 183 ioRegisters.mapAddress(address + 0xC, &bgTransform!i.y, 0xFFFFFFF, 0, false, true) 184 .preWriteMonitor(&onAffineReferencePointPreWrite!(i, true)); 185 } 186 187 ioRegisters.mapAddress(0x40, &windowSize!0.endX, 0xFF, 0, false, true); 188 ioRegisters.mapAddress(0x40, &windowSize!0.startX, 0xFF, 8, false, true); 189 ioRegisters.mapAddress(0x40, &windowSize!1.endX, 0xFF, 16, false, true); 190 ioRegisters.mapAddress(0x40, &windowSize!1.startX, 0xFF, 24, false, true); 191 ioRegisters.mapAddress(0x44, &windowSize!0.endY, 0xFF, 0, false, true); 192 ioRegisters.mapAddress(0x44, &windowSize!0.startY, 0xFF, 8, false, true); 193 ioRegisters.mapAddress(0x44, &windowSize!1.endY, 0xFF, 16, false, true); 194 ioRegisters.mapAddress(0x44, &windowSize!1.startY, 0xFF, 24, false, true); 195 196 foreach (i; AliasSeq!(0, 1, 2, 3)) { 197 enum shift = i * 8; 198 ioRegisters.mapAddress(0x48, &windowControl!i.layerEnableFlags, 0x1F, shift); 199 ioRegisters.mapAddress(0x48, &windowControl!i.specialEffectEnabled, 0b1, shift + 5); 200 } 201 202 ioRegisters.mapAddress(0x4C, &bgMosaicSize.x, 0xF, 0, false, true); 203 ioRegisters.mapAddress(0x4C, &bgMosaicSize.y, 0xF, 4, false, true); 204 ioRegisters.mapAddress(0x4C, &objMosaicSize.x, 0xF, 8, false, true); 205 ioRegisters.mapAddress(0x4C, &objMosaicSize.y, 0xF, 12, false, true); 206 207 ioRegisters.mapAddress(0x50, &specialEffect.firstTargetFlags, 0x3F, 0); 208 ioRegisters.mapAddress(0x50, &specialEffect.effectType, 0b11, 6); 209 ioRegisters.mapAddress(0x50, &specialEffect.secondTargetFlags, 0x3F, 8); 210 ioRegisters.mapAddress(0x50, &blendCoefficients.eva, 0x1F, 16, false, true); 211 ioRegisters.mapAddress(0x50, &blendCoefficients.evb, 0x1F, 24, false, true); 212 ioRegisters.mapAddress(0x54, &blendCoefficients.evy, 0x1F, 0, false, true); 213 } 214 215 private bool onAffineReferencePointPreWrite(int affineLayer, bool y)(int mask, ref int value) { 216 alias referencePoint(bool y) = Alias!("bgTransform!affineLayer.c" ~ (y ? "y" : "x")); 217 // Update the internal reference point 218 mixin(referencePoint!y) = mixin(referencePoint!y) & ~mask | value; 219 // Sign extend it 220 mixin(referencePoint!y) = (mixin(referencePoint!y) << 4) >> 4; 221 return true; 222 } 223 224 @property public FrameSwapper frameSwapper() { 225 return _frameSwapper; 226 } 227 228 public size_t emulate(size_t cycles) { 229 // Use up 4 cycles per dot 230 while (cycles >= CYCLES_PER_DOT) { 231 // Take the cycles 232 cycles -= CYCLES_PER_DOT; 233 // Do stuff for the first visible dot and first blanked dot 234 if (dot == 0) { 235 // Run the events for a line starting to be drawn 236 startLineDrawEvents(line); 237 // Draw the line if it is visible 238 if (line < DISPLAY_HEIGHT) { 239 drawLine(line); 240 } 241 } else if (dot == DISPLAY_WIDTH) { 242 // Swap out the frame if we are done drawing it 243 if (line == DISPLAY_HEIGHT - 1) { 244 _frameSwapper.swapFrame(); 245 } 246 // Run the events for a line drawing ending 247 endLineDrawEvents(line); 248 } 249 // Increment the dot and line counts 250 if (dot == TIMING_WIDTH - 1) { 251 // Reset the dot count if it is the last one 252 dot = 0; 253 // Increment the line count 254 if (line == TIMING_HEIGTH - 1) { 255 // Reset the line count back to zero if we reach the end 256 line = 0; 257 } else { 258 // Else just increment the line count 259 line++; 260 } 261 } else { 262 // If not the last, just increment it 263 dot++; 264 } 265 } 266 return cycles; 267 } 268 269 private void drawLine(int line) { 270 // If blanking is forced then we only draw a white line 271 if (control.forceBlank) { 272 lineBlank(line); 273 return; 274 } 275 // Otherwise we start by drawing the background layers, which depend on the mode 276 switch (control.bgMode) { 277 case 0: 278 layerBackgroundText!0(line); 279 layerBackgroundText!1(line); 280 layerBackgroundText!2(line); 281 layerBackgroundText!3(line); 282 break; 283 case 1: 284 layerBackgroundText!0(line); 285 layerBackgroundText!1(line); 286 layerBackgroundAffine!2(line); 287 layerTransparent!3(); 288 break; 289 case 2: 290 layerTransparent!0(); 291 layerTransparent!1(); 292 layerBackgroundAffine!2(line); 293 layerBackgroundAffine!3(line); 294 break; 295 case 3: 296 layerTransparent!0(); 297 layerTransparent!1(); 298 lineBackgroundBitmap!("16Single", 2)(line); 299 layerTransparent!3(); 300 break; 301 case 4: 302 layerTransparent!0(); 303 layerTransparent!1(); 304 lineBackgroundBitmap!("8Double", 2)(line); 305 layerTransparent!3(); 306 break; 307 case 5: 308 layerTransparent!0(); 309 layerTransparent!1(); 310 lineBackgroundBitmap!("16Double", 2)(line); 311 layerTransparent!3(); 312 break; 313 default: 314 break; 315 } 316 // We always draw the object layer 317 layerObjects(line); 318 // Finally we compose all the layers into the drawn line 319 layerCompose(line); 320 } 321 322 private void lineBlank(int line) { 323 // When blanking we just fill the line with white 324 auto frame = _frameSwapper.workFrame; 325 auto p = line * DISPLAY_WIDTH; 326 frame[p .. p + DISPLAY_WIDTH] = cast(short) 0xFFFF; 327 } 328 329 private void layerTransparent(int layer)() { 330 // Bit 16 of a dots's color data is unused in the GBA, but we'll use it for transparency 331 linePixels!layer[] = TRANSPARENT; 332 } 333 334 private void layerBackgroundText(int layer)(int line) { 335 // Draw a transparent line if the layer is not enabled 336 if (!control.layerEnableFlags.checkBit(layer)) { 337 layerTransparent!layer(); 338 return; 339 } 340 // Tile palette data is 4 bit when using 16 palettes, or 8 bit when just using 1 341 // We also calculate a shift so that: 1 << tileSizeShift = sizeOfTile = (8 * 8 * paletteDataSize) 342 int tile4Bit = bgControl!layer.singlePalette ? 0 : 1; 343 int tileSizeShift = 6 - tile4Bit; 344 // In text mode, the screen size is 256 or 512 in each dimension (1 or 2 maps in each dimension) 345 // Here we get this size as a bit mask (writable as 2^n - 1) 346 int totalWidth = (256 << (bgControl!layer.size & 0b1)) - 1; 347 int totalHeight = (256 << ((bgControl!layer.size & 0b10) >> 1)) - 1; 348 // To get the tile y coordinate, we add the offet and apply the height mask (to wrap around) 349 int y = (line + bgOffset!layer.y) & totalHeight; 350 // If y is outside the first vertical tile map, we address into the second one instead 351 int mapBase = bgControl!layer.mapBase << 11; 352 if (y & ~255) { 353 // Restrict y to the map size 354 y &= 255; 355 // if the width is also of two maps, then we address past the second horizontal map too 356 mapBase += BYTES_PER_KIB << (totalWidth & ~255 ? 2 : 1); 357 } 358 // If the mosaic is enabled, then we round down to the next mosaic multiple 359 if (bgControl!layer.mosaicEnabled) { 360 y -= y % (bgMosaicSize.y + 1); 361 } 362 // Now we calculate the map line (row of tiles in a map), and the tile line (row of dots in a tile) 363 int mapLine = y >> 3; 364 int tileLine = y & 7; 365 // Every row of tiles in a map has 32 of them, so we get the linear offset into the map by doing mapLine * 32 366 int lineMapOffset = mapLine << 5; 367 // The tile base is the start address for the tile data, it's in increments of 16KB 368 int tileBase = bgControl!layer.tileBase << 14; 369 // Use the optimized ASM implementation of the line drawing code if available 370 static if (__traits(compiles, LINE_BACKGROUND_TEXT_ASM)) { 371 // Place data used by the ASM on the stack 372 int xOffset = bgOffset!layer.x; 373 int singlePalette = bgControl!layer.singlePalette; 374 int mosaicEnabled = bgControl!layer.mosaicEnabled; 375 int mosaicSizeX = bgMosaicSize.x; 376 // Also place the addresses for the memory 377 auto lineAddress = cast(size_t) linePixels!layer.ptr; 378 auto vramAddress = cast(size_t) vram.getPointer!byte(0x0); 379 auto paletteAddress = cast(size_t) palette.getPointer!byte(0x0); 380 mixin (LINE_BACKGROUND_TEXT_ASM); 381 } else { 382 foreach (column; 0 .. DISPLAY_WIDTH) { 383 // For every column, we get the base x coordinate like we did for the y 384 int x = (column + bgOffset!layer.x) & totalWidth; 385 // Again, we address into the second horizontal map the if x is outside the first 386 int map = mapBase; 387 if (x & ~255) { 388 x &= 255; 389 map += BYTES_PER_KIB << 1; 390 } 391 // If the mosaic is enabled, then we round down to the next mosaic multiple 392 if (bgControl!layer.mosaicEnabled) { 393 x -= x % (bgMosaicSize.x + 1); 394 } 395 // We calculate the map and tile columns just like we did for the y 396 int mapColumn = x >> 3; 397 int tileColumn = x & 7; 398 // Now we can calculate address into the map: we add the line offset to the column, 399 // multiply them by two because each tile is 2 bytes, then add the map base address 400 int mapAddress = map + (lineMapOffset + mapColumn << 1); 401 // Then we fetch the tile data from the map 402 int tile = vram.get!short(mapAddress); 403 // The tile number is taken from the lower bits 404 int tileNumber = tile & 0x3FF; 405 // The two middle bits are used to flip horizontally and vertically, respectively 406 int sampleColumn = void, sampleLine = void; 407 if (tile & 0x400) { 408 sampleColumn = ~tileColumn & 7; 409 } else { 410 sampleColumn = tileColumn; 411 } 412 if (tile & 0x800) { 413 sampleLine = ~tileLine & 7; 414 } else { 415 sampleLine = tileLine; 416 } 417 // Now we calculate the address into the tile data: we add the base tile address, tile number * tile size, 418 // line into the tile * 8 dots, and the column into the tile (both divided by 2 if 4 bits per dots) 419 int tileAddress = tileBase + (tileNumber << tileSizeShift) + ((sampleLine << 3) + sampleColumn >> tile4Bit); 420 // By addressing into the tile, we get the palette index, but this depends on the palette mode: 1 or 16 421 int paletteAddress = void; 422 if (bgControl!layer.singlePalette) { 423 // For a single palette we address directly 424 int paletteIndex = vram.get!byte(tileAddress) & 0xFF; 425 // The first color of the palette is transparent 426 if (paletteIndex == 0) { 427 linePixels!layer[column] = TRANSPARENT; 428 continue; 429 } 430 // Every color is 2 bytes, so me multiply the index by 2 431 paletteAddress = paletteIndex << 1; 432 } else { 433 // For multiple palettes we address the byte, then address the low or high nibble (4 bit index) 434 int paletteIndex = vram.get!byte(tileAddress) >> ((sampleColumn & 0b1) << 2) & 0xF; 435 // The first color of the palette is also transparent 436 if (paletteIndex == 0) { 437 linePixels!layer[column] = TRANSPARENT; 438 continue; 439 } 440 // The tile upper bits are the palette number. We multiply by 16 (colors per palette), 441 // then add the index into the palette, and also multiply by 2 because each color takes 2 bytes 442 paletteAddress = (tile >> 8 & 0xF0) + paletteIndex << 1; 443 } 444 // Finally we have the address into the palette, which yields the color for the layer dot 445 short color = palette.get!short(paletteAddress) & 0x7FFF; 446 linePixels!layer[column] = color; 447 } 448 } 449 } 450 451 private void layerBackgroundAffine(int layer)(int line) { 452 // There are two affine layers, with indices 2 or 3 453 enum affineLayer = layer - 2; 454 // If the layer isn't enabled, we make it transparent 455 if (!control.layerEnableFlags.checkBit(layer)) { 456 layerTransparent!layer(); 457 // We also need to increment the current coordinates by the vertical transformation coefficients 458 bgTransform!affineLayer.cx += bgTransform!affineLayer.b; 459 bgTransform!affineLayer.cy += bgTransform!affineLayer.d; 460 return; 461 } 462 // We calculate the size of the layer (square) and represent it as a bit mask (2^n -1) 463 int bgSize = (128 << bgControl!layer.size) - 1; 464 int bgSizeInv = ~bgSize; 465 // This shift is an equivalent multiplier for the tile map size (1 << n = tilesPerLine) 466 int mapLineShift = bgControl!layer.size + 4; 467 // These are the current coordinates of the dots to be sampled in the layer, in fixed 20.8 format 468 int cx = bgTransform!affineLayer.cx; 469 int cy = bgTransform!affineLayer.cy; 470 // We increment the stored values by the vertical coefficients 471 bgTransform!affineLayer.cx += bgTransform!affineLayer.b; 472 bgTransform!affineLayer.cy += bgTransform!affineLayer.d; 473 // These are the horizontal transformation coefficients 474 int pa = bgTransform!affineLayer.a; 475 int pc = bgTransform!affineLayer.c; 476 // The tile base is the start address for the background tile map, it's in increments of 2KB 477 int mapBase = bgControl!layer.mapBase << 11; 478 // The tile base is the start address for the tile data, it's in increments of 16KB 479 int tileBase = bgControl!layer.tileBase << 14; 480 // Use the optimized ASM implementation of the line drawing code if available 481 static if (__traits(compiles, LINE_BACKGROUND_AFFINE_ASM)) { 482 // Place data used by the ASM on the stack 483 int overflowWrapAround = bgControl!layer.overflowWrapAround; 484 int mosaicEnabled = bgControl!layer.mosaicEnabled; 485 int mosaicSizeX = bgMosaicSize.x; 486 int mosaicSizeY = bgMosaicSize.y; 487 // Also place the addresses for the memory 488 auto lineAddress = cast(size_t) linePixels!layer.ptr; 489 auto vramAddress = cast(size_t) vram.getPointer!byte(0x0); 490 auto paletteAddress = cast(size_t) palette.getPointer!byte(0x0); 491 mixin (LINE_BACKGROUND_AFFINE_ASM); 492 } else { 493 // On every iteration we also increment the coordinates by the transform coefficients 494 for (int column = 0; column < DISPLAY_WIDTH; column++, cx += pa, cy += pc) { 495 // The coordinates have 8 fractional bits, so we get the integer part by shifting 496 int x = cx >> 8; 497 int y = cy >> 8; 498 // Now we check if the coordinates are outside the layer by using the inverse mask 499 if (x & bgSizeInv) { 500 // There are two modes for overflow: wrap around (apply mask) or make it transparent 501 if (bgControl!layer.overflowWrapAround) { 502 x &= bgSize; 503 } else { 504 linePixels!layer[column] = TRANSPARENT; 505 continue; 506 } 507 } 508 if (y & bgSizeInv) { 509 if (bgControl!layer.overflowWrapAround) { 510 y &= bgSize; 511 } else { 512 linePixels!layer[column] = TRANSPARENT; 513 continue; 514 } 515 } 516 // If the mosaic mode is enabled, we round the coordinates down to the nearest multiple 517 if (bgControl!layer.mosaicEnabled) { 518 x -= x % (bgMosaicSize.x + 1); 519 y -= y % (bgMosaicSize.y + 1); 520 } 521 // Tiles are 8x8, so dividing the x and y dots coordinates by 8 gives us their coordinates in the map 522 int mapColumn = x >> 3; 523 int mapLine = y >> 3; 524 // Similar idea here, but we use the modulo operation instead to get the coordinates in the tile 525 int tileColumn = x & 7; 526 int tileLine = y & 7; 527 // To calculate the address in the map, we add the base address to line and column offsets 528 // The line offset is multiplied by the number of tiles in a map line 529 int mapAddress = mapBase + (mapLine << mapLineShift) + mapColumn; 530 // Now we can fetch the tile number 531 int tileNumber = vram.get!byte(mapAddress) & 0xFF; 532 // To calculate the address in the tile data, we add the tile base to the number, line and column offsets 533 // The tile number is multiplied by the tile size (64), and the line offset by the tile line size (8) 534 int tileAddress = tileBase + (tileNumber << 6) + (tileLine << 3) + tileColumn; 535 // By addressing into the tile, we get the palette index, which we multiply by 2 to get the address 536 int paletteAddress = (vram.get!byte(tileAddress) & 0xFF) << 1; 537 // The first color of the palette is transparent 538 if (paletteAddress == 0) { 539 linePixels!layer[column] = TRANSPARENT; 540 continue; 541 } 542 // Finally we can fetch the dot color from the palette 543 short color = palette.get!short(paletteAddress) & 0x7FFF; 544 linePixels!layer[column] = color; 545 } 546 } 547 } 548 549 private void lineBackgroundBitmap(string mode, int layer)(int line) 550 if (mode == "16Single" || mode == "8Double" || mode == "16Double") { 551 // Bitmaps always use the first affine layer properties 552 enum affineLayer = 0; 553 // If the layer isn't enabled, we make it transparent 554 if (!control.layerEnableFlags.checkBit(2)) { 555 layerTransparent!layer(); 556 // We also need to increment the current coordinates by the vertical transformation coefficients 557 bgTransform!affineLayer.cx += bgTransform!affineLayer.b; 558 bgTransform!affineLayer.cy += bgTransform!affineLayer.d; 559 return; 560 } 561 // These are the current coordinates of the dots to be sampled in the layer, in fixed 20.8 format 562 int cx = bgTransform!affineLayer.cx; 563 int cy = bgTransform!affineLayer.cy; 564 // We increment the stored values by the vertical coefficients 565 bgTransform!affineLayer.cx += bgTransform!affineLayer.b; 566 bgTransform!affineLayer.cy += bgTransform!affineLayer.d; 567 // These are the horizontal transformation coefficients 568 int pa = bgTransform!affineLayer.a; 569 int pc = bgTransform!affineLayer.c; 570 // Calculate the frame base address from the index (not used for 16Single mode) 571 int addressBase = control.frameIndex ? 0xA000 : 0x0; 572 // On every iteration we also increment the coordinates by the transform coefficients 573 for (int column = 0; column < DISPLAY_WIDTH; column++, cx += pa, cy += pc) { 574 int x = cx >> 8; 575 int y = cy >> 8; 576 // The 16Double mode has a smaller layer, others use the display size 577 static if (mode == "16Double") { 578 enum layerWidth = 160; 579 enum layerHeight = 128; 580 } else { 581 enum layerWidth = DISPLAY_WIDTH; 582 enum layerHeight = DISPLAY_HEIGHT; 583 } 584 // Use transparent on overflow 585 if (x < 0 || x >= layerWidth || y < 0 || y >= layerHeight) { 586 linePixels!layer[column] = TRANSPARENT; 587 continue; 588 } 589 // If the mosaic mode is enabled, we round the coordinates down to the nearest multiple 590 if (bgControl!layer.mosaicEnabled) { 591 x -= x % (bgMosaicSize.x + 1); 592 y -= y % (bgMosaicSize.y + 1); 593 } 594 // The dot offet is just the x offset added to y multiplied by the number of dots in a layer line 595 int dotOffset = x + y * layerWidth; 596 // Both 16 bits modes are direct, but the 8 bit mode indexes the palette 597 static if (mode == "16Single" || mode == "16Double") { 598 // The dots are 2 bytes wide, so we multiply by 2 to get the color address, then add the frame base 599 short color = vram.get!short((dotOffset << 1) + addressBase); 600 } else { 601 // The dots are only 1 byte wide, so we get the palette index directly 602 int paletteIndex = vram.get!byte(dotOffset + addressBase) & 0xFF; 603 // The first color of the palette is transparent 604 if (paletteIndex == 0) { 605 linePixels!layer[column] = TRANSPARENT; 606 continue; 607 } 608 // The colors take 2 bytes, so we multiply by 2 to get the palette address 609 short color = palette.get!short(paletteIndex << 1); 610 } 611 // Finally we set the color bits in the layer 612 linePixels!layer[column] = color & 0x7FFF; 613 } 614 } 615 616 private void layerObjects(int line) { 617 // Sprites only covert part of the line, so we start by clearing the layer with transparency 618 objectLinePixels[] = TRANSPARENT; 619 // The info line is the top object priority (0 and 1) and mode bits (2 and 3). Fill with the lowest priority 620 infoLinePixels[] = 0b11; 621 // Skip if objects aren't enabled 622 if (!control.layerEnableFlags.checkBit(4)) { 623 return; 624 } 625 // Objects start a higher address with bitmaped display modes 626 int tileBase = 0x10000; 627 if (control.bgMode >= 3) { 628 tileBase += 0x4000; 629 } 630 // Higher index objects have lower priority, and we traverse in increasing priority 631 foreach_reverse (i; 0 .. 128) { 632 // Attributes are 8 bytes long (6 used, 2 for padding), and consecutive in memory 633 int attributeAddress = i << 3; 634 int attribute0 = oam.get!short(attributeAddress); 635 // Get the flag that controls if rotation and scale is enabled 636 int rotAndScale = attribute0.getBit(8); 637 // The function of this bit depends on the previous one 638 int doubleSize = attribute0.getBit(9); 639 // If rotation and scale is not enabled, it is used to disable the object 640 if (!rotAndScale) { 641 if (doubleSize) { 642 continue; 643 } 644 } 645 // The shape bits decide if the object is square or rectangular (vertical or horizontal) 646 int shape = attribute0.getBits(14, 15); 647 // Next we get the second attribute and the size parameter 648 int attribute1 = oam.get!short(attributeAddress + 2); 649 int size = attribute1.getBits(14, 15); 650 // We'll calculate the final dimensions, and a multiplier shift: horizontalSize = 2 ^^ (mapYShift + 3) 651 int horizontalSize = void, verticalSize = void, mapYShift = void; 652 if (shape == 0) { 653 // For a square it simple: sizes grow by a factor of 2 on all dimensions 654 horizontalSize = 8 << size; 655 verticalSize = horizontalSize; 656 mapYShift = size; 657 } else { 658 // For a rectangle, we assume it is a horizontal one 659 int mapXShift = void; 660 final switch (size) { 661 case 0: 662 horizontalSize = 16; 663 verticalSize = 8; 664 mapXShift = 0; 665 mapYShift = 1; 666 break; 667 case 1: 668 horizontalSize = 32; 669 verticalSize = 8; 670 mapXShift = 0; 671 mapYShift = 2; 672 break; 673 case 2: 674 horizontalSize = 32; 675 verticalSize = 16; 676 mapXShift = 1; 677 mapYShift = 2; 678 break; 679 case 3: 680 horizontalSize = 64; 681 verticalSize = 32; 682 mapXShift = 2; 683 mapYShift = 3; 684 break; 685 } 686 // If it is actually vertical, we just have to swap the dimensions 687 if (shape == 2) { 688 swap!int(horizontalSize, verticalSize); 689 swap!int(mapXShift, mapYShift); 690 } 691 } 692 // We have two different sizes: the size of the original object, and the one after transformation 693 // The sample one is the original size (in memory), the other is the area in which we draw the object 694 int sampleHorizontalSize = horizontalSize; 695 int sampleVerticalSize = verticalSize; 696 // If double size is enabled, we must double the drawn area 697 if (doubleSize) { 698 horizontalSize <<= 1; 699 verticalSize <<= 1; 700 } 701 // Now we fetch the y coordinate. If it is too large, we subtract the arbitrary 256 value 702 int y = attribute0 & 0xFF; 703 if (y >= DISPLAY_HEIGHT) { 704 y -= 256; 705 } 706 // We subtract the y coordinate from the line to get the y relative to the object 707 int objectY = line - y; 708 // If we are outside the area to draw, then the object isn't in this line (skip it) 709 if (objectY < 0 || objectY >= verticalSize) { 710 continue; 711 } 712 // Now we calculate masks for the size, which depends on the kind of drawing 713 int horizontalSizeMask = void; 714 int verticalSizeMask = void; 715 if (rotAndScale) { 716 // When transformation is enabled, we calculate inverse masks for the original object 717 horizontalSizeMask = ~(sampleHorizontalSize - 1); 718 verticalSizeMask = ~(sampleVerticalSize - 1); 719 } else { 720 // When transformation is disabled, we calculate ordinary masks 721 horizontalSizeMask = horizontalSize - 1; 722 verticalSizeMask = verticalSize - 1; 723 } 724 // Now we fetch the x coordinate. If it is too large, we subtract the arbitrary 512 value 725 int x = attribute1 & 0x1FF; 726 if (x >= DISPLAY_WIDTH) { 727 x -= 512; 728 } 729 // If the object is has rotation and scale we fetch the matrix, otherwise it's just a few flip parameters 730 int horizontalFlip = void, verticalFlip = void; 731 int pa = void, pb = void, pc = void, pd = void; 732 if (rotAndScale) { 733 horizontalFlip = 0; 734 verticalFlip = 0; 735 int rotAndScaleParameters = attribute1.getBits(9, 13); 736 int parametersAddress = (rotAndScaleParameters << 5) + 0x6; 737 pa = oam.get!short(parametersAddress); 738 pb = oam.get!short(parametersAddress + 8); 739 pc = oam.get!short(parametersAddress + 16); 740 pd = oam.get!short(parametersAddress + 24); 741 } else { 742 horizontalFlip = attribute1.getBit(12); 743 verticalFlip = attribute1.getBit(13); 744 pa = 0; 745 pb = 0; 746 pc = 0; 747 pd = 0; 748 } 749 // Finally we get the rest of the attribute data 750 int mode = attribute0.getBits(10, 11); 751 int mosaic = attribute0.getBit(12); 752 int singlePalette = attribute0.getBit(13); 753 int attribute2 = oam.get!short(attributeAddress + 4); 754 int tileNumber = attribute2 & 0x3FF; 755 int priority = attribute2.getBits(10, 11); 756 int paletteNumber = attribute2.getBits(12, 15); 757 // We're ready to draw the object, one dot at a time 758 foreach (objectX; 0 .. horizontalSize) { 759 // We calculate the column in the line from the x cooordinate, and skip if outside the line 760 int column = objectX + x; 761 if (column >= DISPLAY_WIDTH) { 762 continue; 763 } 764 // We fetch the priority of the previous object, and skip if the current one is lower 765 int previousInfo = infoLinePixels[column]; 766 int previousPriority = previousInfo & 0b11; 767 // Lower priority numbers are actually higher priority 768 if (priority > previousPriority) { 769 continue; 770 } 771 // Next we transform the draw coordinates into the sampling coordinates 772 int sampleX = objectX, sampleY = objectY; 773 if (rotAndScale) { 774 // We offset the draw area to center it, apply the transformation, then offset back 775 int tmpX = sampleX - (horizontalSize >> 1); 776 int tmpY = sampleY - (verticalSize >> 1); 777 sampleX = pa * tmpX + pb * tmpY >> 8; 778 sampleY = pc * tmpX + pd * tmpY >> 8; 779 sampleX += sampleHorizontalSize >> 1; 780 sampleY += sampleVerticalSize >> 1; 781 // We check against the inverted mask for out-of-bounds (skip in that case) 782 if ((sampleX & horizontalSizeMask) || (sampleY & verticalSizeMask)) { 783 continue; 784 } 785 } else { 786 // When not using rotation and scale, we only apply flips 787 if (horizontalFlip) { 788 sampleX = ~sampleX & horizontalSizeMask; 789 } 790 if (verticalFlip) { 791 sampleY = ~sampleY & verticalSizeMask; 792 } 793 } 794 // Now that we have the coordinates to sample in memory, we can apply the mosaic effect 795 if (mosaic) { 796 sampleX -= sampleX % (objMosaicSize.x + 1); 797 sampleY -= sampleY % (objMosaicSize.y + 1); 798 } 799 // We divide the coordinates by 8 to get coordinates of the tile to draw 800 int mapX = sampleX >> 3; 801 int mapY = sampleY >> 3; 802 // We get the divide by 8 remainder to get coordinates of the dots in the tile to draw 803 int tileX = sampleX & 7; 804 int tileY = sampleY & 7; 805 // Now we calculate the tile address, which starts with the number 806 int tileAddress = tileNumber; 807 // To which we add the tile coordinate offsets, depending on the layout 808 if (control.objMap1D) { 809 // For a 1D layout we add: the y offset * the number of tiles in an object line, and the x offset 810 // We then multiply by 2 if we're using a single palette, since that means the tile are 2x size 811 tileAddress += mapX + (mapY << mapYShift) << singlePalette; 812 } else { 813 // A 2D layout is similar, but we always have 32 tiles horizontally, regardless of the tile size 814 tileAddress += (mapX << singlePalette) + (mapY << 5); 815 } 816 // Tiles are at least 32B, so that the base multiplier. For 64B, we multiplied by two earlier 817 tileAddress <<= 5; 818 // Now we add the offsets into the tile, starting with the base address 819 tileAddress += tileBase; 820 // Tiles are always 8 dots wide, so that's the y offet multiplier 821 // Since multiple palettes use half a byte per dot, we must divide by 2 when in that mode 822 tileAddress += tileX + (tileY << 3) >> (1 - singlePalette); 823 // Now we can calculate the palette address to get the final dot color 824 int paletteAddress = void; 825 if (singlePalette) { 826 // For a single palette, we address directly to get the palette index 827 int paletteIndex = vram.get!byte(tileAddress) & 0xFF; 828 // The first palette color is transparent 829 if (paletteIndex == 0) { 830 continue; 831 } 832 // Colors are 2 bytes wide, so we multiply the index by 2 833 paletteAddress = paletteIndex << 1; 834 } else { 835 // For multiple palettes we address the byte, then address the low or high nibble (4 bit index) 836 int paletteIndex = vram.get!byte(tileAddress) >> ((tileX & 1) << 2) & 0xF; 837 // The first palette color is transparent 838 if (paletteIndex == 0) { 839 continue; 840 } 841 // We multiply the palette number by 16 (colors per palette), then add the index into the palette, 842 // and also multiply by 2 because each color takes 2 bytes 843 paletteAddress = (paletteNumber << 4) + paletteIndex << 1; 844 } 845 // We get the color from the palette, which is a different one to the backgrounds, hence the offset 846 short color = palette.get!short(0x200 + paletteAddress) & 0x7FFF; 847 // The mode for the info flags is the current mode, but we keep the window flag from the object below 848 int modeFlags = mode << 2 | previousInfo & 0b1000; 849 if (mode == 2) { 850 // In windows mode nothing is drawn, but we must keep the window flag since that will be used later 851 infoLinePixels[column] = cast(short) (modeFlags | previousPriority); 852 } else { 853 // Otherwise we update the color, and write the current priority as the top one 854 objectLinePixels[column] = color; 855 infoLinePixels[column] = cast(short) (modeFlags | priority); 856 } 857 } 858 } 859 } 860 861 private void layerCompose(int line) { 862 short backColor = palette.get!short(0x0) & 0x7FFF; 863 // We fill the line in the frame with the composed layer 864 auto frame = _frameSwapper.workFrame; 865 // Layers are composed into the final line by resolving priorities and applying special effects 866 for (int column = 0, p = line * DISPLAY_WIDTH; column < DISPLAY_WIDTH; column++, p++) { 867 // From the object info line, we get the object priority and mode 868 int objInfo = infoLinePixels[column]; 869 int objPriority = objInfo & 0b11; 870 int objMode = objInfo >> 2; 871 // Now we find in which window we are (if any; they could be disabled) 872 WindowControl window = void; 873 getWindow(objMode, line, column, window); 874 // Now we do the actual composition: we find the top most dot color, and the one just below 875 // We also need to save the dots' layers and priorities to apply special effects later 876 // We start on the backdrop: layer 5, with priority 3, and a constant color 877 short firstColor = backColor; 878 short secondColor = backColor; 879 int firstLayer = 5; 880 int secondLayer = 5; 881 int firstPriority = 3; 882 int secondPriority = 3; 883 // Every layer has an assigned priority. Ties for backgrounds are broken by the layer number 884 // (lower is higher priority). A tied object layer is higher priority then all backgrounds 885 // Now we traverse the layers in the natural order of priorities (the tie breaking order) 886 foreach (layer; AliasSeq!(3, 2, 1, 0, 4)) { 887 // We skip disabled layers 888 if (!window.layerEnableFlags.checkBit(layer)) { 889 continue; 890 } 891 // We skip transparent colors 892 short layerColor = linePixels!layer[column]; 893 if (layerColor & TRANSPARENT) { 894 continue; 895 } 896 // We check if this layer has a higher priority (smaller value) than the current one 897 // We use <= so that ties will result in the naturally higher priority layer being used 898 static if (layer == 4) { 899 int layerPriority = objPriority; 900 } else { 901 int layerPriority = bgControl!layer.priority; 902 } 903 if (layerPriority <= firstPriority) { 904 // The first layer is now the second 905 secondColor = firstColor; 906 secondLayer = firstLayer; 907 secondPriority = firstPriority; 908 // Update the first layer data 909 firstColor = layerColor; 910 firstLayer = layer; 911 firstPriority = layerPriority; 912 } else if (layerPriority <= secondPriority) { 913 // If it's not higher than the first, we check the second, and update it in the same way 914 secondColor = layerColor; 915 secondLayer = layer; 916 secondPriority = layerPriority; 917 } 918 } 919 // Now that we have the data for the top two layers, we combine them in to the final dot 920 if ((objMode & 0b1) && firstLayer == 4 && specialEffect.secondTargetFlags.checkBit(secondLayer)) { 921 // If the object is in alpha-blend mode and on the top layer, and blending is enabled 922 // for the second layer, then we must blend the two, regardless of the special effects mode 923 firstColor = applyBlendEffect(firstColor, secondColor); 924 } else if (window.specialEffectEnabled) { 925 // Othwerwise we might apply a special effect 926 final switch (specialEffect.effectType) { 927 case 0: 928 // No effect, just use the top color as is 929 break; 930 case 1: 931 // If both layers have blending enabled, then we blend the two 932 if (specialEffect.firstTargetFlags.checkBit(firstLayer) 933 && specialEffect.secondTargetFlags.checkBit(secondLayer)) { 934 firstColor = applyBlendEffect(firstColor, secondColor); 935 } 936 break; 937 case 2: 938 // If the first layer has blending enabled, then we increase its brightness 939 if (specialEffect.firstTargetFlags.checkBit(firstLayer)) { 940 firstColor = applyBrightnessEffect!false(firstColor); 941 } 942 break; 943 case 3: 944 // If the second layer has blending enabled, then we decrease its brightness 945 if (specialEffect.firstTargetFlags.checkBit(firstLayer)) { 946 firstColor = applyBrightnessEffect!true(firstColor); 947 } 948 break; 949 } 950 } 951 // The final color is that of the first layer 952 frame[p] = firstColor; 953 } 954 } 955 956 private void getWindow(int objectMode, int line, int column, ref WindowControl window) { 957 // Enable everything if windows are not in use 958 if (control.windowEnableFlags == 0) { 959 window.layerEnableFlags = 0b11111; 960 window.specialEffectEnabled = true; 961 return; 962 } 963 // If any window is enabled, then we check that the dot is inside, using the priority order 964 foreach (i; AliasSeq!(0, 1)) { 965 if (control.windowEnableFlags.checkBit(i) && insideWindow!i(line, column)) { 966 window = windowControl!i; 967 return; 968 } 969 } 970 if (control.windowEnableFlags.checkBit(2) && objectMode.checkBit(1)) { 971 window = objWindowCtrl; 972 return; 973 } 974 window = outWindowCtrl; 975 } 976 977 private bool insideWindow(int i)(int line, int column) { 978 // When the bounds are max < min, the window is in [0, max) and [min, size) 979 // Start by checking the horizontal bounds 980 if (windowSize!i.startX <= windowSize!i.endX) { 981 if (column < windowSize!i.startX || column >= windowSize!i.endX) { 982 return false; 983 } 984 } else { 985 if (column >= windowSize!i.endX && column < windowSize!i.startX) { 986 return false; 987 } 988 } 989 // Then check the vertical bounds 990 if (windowSize!i.startY <= windowSize!i.endY) { 991 if (line < windowSize!i.startY || line >= windowSize!i.endY) { 992 return false; 993 } 994 } else { 995 if (line >= windowSize!i.endY && line < windowSize!i.startY) { 996 return false; 997 } 998 } 999 return true; 1000 } 1001 1002 private short applyBrightnessEffect(bool decrease)(short color) { 1003 // Get the individual colour components 1004 int red = color & 0b11111; 1005 int green = color.getBits(5, 9); 1006 int blue = color.getBits(10, 14); 1007 // Get the scaling factor, which is in 0.4 fixed format 1008 int evy = min(blendCoefficients.evy, 16); 1009 // Apply the effect 1010 static if (decrease) { 1011 // For decrease, we subtract the rounded percentage from each component 1012 red -= red * evy + 8 >> 4; 1013 green -= green * evy + 8 >> 4; 1014 blue -= blue * evy + 8 >> 4; 1015 } else { 1016 // For increase, we add the rounded percentage from the inverse of each component 1017 red += (31 - red) * evy + 8 >> 4; 1018 green += (31 - green) * evy + 8 >> 4; 1019 blue += (31 - blue) * evy + 8 >> 4; 1020 } 1021 // Recombine the components into the colour data 1022 return (blue & 0x1F) << 10 | (green & 0x1F) << 5 | red & 0x1F; 1023 } 1024 1025 private short applyBlendEffect(short first, short second) { 1026 // Get the individual colour components of both colors 1027 int firstRed = first & 0b11111; 1028 int firstGreen = first.getBits(5, 9); 1029 int firstBlue = first.getBits(10, 14); 1030 int secondRed = second & 0b11111; 1031 int secondGreen = second.getBits(5, 9); 1032 int secondBlue = second.getBits(10, 14); 1033 // Get the blending coefficients for both colors, which are in 0.4 fixed format 1034 int eva = min(blendCoefficients.eva, 16); 1035 int evb = min(blendCoefficients.evb, 16); 1036 // Get the fraction from each component of each colour 1037 firstRed = firstRed * eva + 8 >> 4; 1038 firstGreen = firstGreen * eva + 8 >> 4; 1039 firstBlue = firstBlue * eva + 8 >> 4; 1040 secondRed = secondRed * evb + 8 >> 4; 1041 secondGreen = secondGreen * evb + 8 >> 4; 1042 secondBlue = secondBlue * evb + 8 >> 4; 1043 // Add the fractions and clamp to 31 (max component value) 1044 int blendRed = min(31, firstRed + secondRed); 1045 int blendGreen = min(31, firstGreen + secondGreen); 1046 int blendBlue = min(31, firstBlue + secondBlue); 1047 // Recombine the components into the colour data 1048 return (blendBlue & 0x1F) << 10 | (blendGreen & 0x1F) << 5 | blendRed & 0x1F; 1049 } 1050 1051 private void endLineDrawEvents(int line) { 1052 // Set the HBLANK flag in the display status 1053 status.inHBlank = true; 1054 // Run the DMAs if within the visible vertical lines 1055 if (line < DISPLAY_HEIGHT) { 1056 dmas.signalHBLANK(); 1057 } 1058 // Trigger the HBLANK interrupt if enabled 1059 if (status.intHBlankEnabled) { 1060 interruptHandler.requestInterrupt(InterruptSource.LCD_HBLANK); 1061 } 1062 } 1063 1064 private void startLineDrawEvents(int line) { 1065 // Clear the HBLANK bit in the display status 1066 status.inHBlank = false; 1067 // Update the VCOUNT register 1068 status.vCounter = cast(ubyte) line; 1069 // Update the VMATCH bit in the display status 1070 status.vCountMatch = status.vCounter == status.vCountTarget; 1071 // Trigger the VMATCH interrupt if enabled 1072 if (status.vCountMatch && status.intVCounterEnabled) { 1073 interruptHandler.requestInterrupt(InterruptSource.LCD_VCOUNTER_MATCH); 1074 } 1075 // Check for VBLANK start or end 1076 switch (line) { 1077 case DISPLAY_HEIGHT: { 1078 // Set the VBLANK bit in the display status 1079 status.inVBlank = true; 1080 // Signal VBLANK to the DMAs 1081 dmas.signalVBLANK(); 1082 // Trigger the VBLANK interrupt if enabled 1083 if (status.intVBlankEnabled) { 1084 interruptHandler.requestInterrupt(InterruptSource.LCD_VBLANK); 1085 } 1086 break; 1087 } 1088 case TIMING_HEIGTH - 1: { 1089 // Clear the VBLANK bit 1090 status.inVBlank = false; 1091 // Reload the transformation data 1092 foreach (i; AliasSeq!(0, 1)) { 1093 bgTransform!i.cx = (bgTransform!i.x << 4) >> 4; 1094 bgTransform!i.cy = (bgTransform!i.y << 4) >> 4; 1095 } 1096 break; 1097 } 1098 default: { 1099 break; 1100 } 1101 } 1102 } 1103 } 1104 1105 public class FrameSwapper { 1106 private enum FRAME_SIZE = DISPLAY_WIDTH * DISPLAY_HEIGHT; 1107 private short[FRAME_SIZE] frame0; 1108 private short[FRAME_SIZE] frame1; 1109 private bool workFrame1 = false; 1110 private bool newFrameReady = false; 1111 private Condition frameReadySignal; 1112 1113 private this() { 1114 frameReadySignal = new Condition(new Mutex()); 1115 } 1116 1117 @property private short[] workFrame() { 1118 return workFrame1 ? frame1 : frame0; 1119 } 1120 1121 public void swapFrame() { 1122 synchronized (frameReadySignal.mutex) { 1123 workFrame1 = !workFrame1; 1124 newFrameReady = true; 1125 frameReadySignal.notify(); 1126 } 1127 } 1128 1129 public short[] nextFrame() { 1130 synchronized (frameReadySignal.mutex) { 1131 while (!newFrameReady) { 1132 frameReadySignal.wait(); 1133 } 1134 newFrameReady = false; 1135 return workFrame1 ? frame0 : frame1; 1136 } 1137 } 1138 1139 public short[] currentFrame() { 1140 synchronized (frameReadySignal.mutex) { 1141 return workFrame1 ? frame0 : frame1; 1142 } 1143 } 1144 }