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 }