From a66900f34b6fe222da4a9fb3b963668868eeaddb Mon Sep 17 00:00:00 2001 From: ISSOtm Date: Sun, 21 Sep 2025 18:31:24 +0200 Subject: [PATCH 1/4] Create a new chapter for the Window --- src/LCDC.md | 7 ++----- src/SUMMARY.md | 1 + src/Scrolling.md | 41 +---------------------------------------- src/Window.md | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 45 deletions(-) create mode 100644 src/Window.md diff --git a/src/LCDC.md b/src/LCDC.md index 400180d6..1cc4e915 100644 --- a/src/LCDC.md +++ b/src/LCDC.md @@ -57,11 +57,8 @@ This bit controls whether the window shall be displayed or not. This bit is overridden on DMG by [bit 0](<#LCDC.0 — BG and Window enable/priority>) if that bit is clear. -Changing the value of this register mid-frame triggers a more complex behaviour: -[see further below](<#FF4A–FF4B — WY, WX: Window Y position, X position plus 7>). - -Note that on CGB models, setting this bit to 0 then back to 1 mid-frame -may cause the second write to be ignored. (TODO: test this.) +Changing the value of this register mid-frame triggers several more complex behaviours: +[see the corresponding chapter](<#Window mid-frame behavior>). ### LCDC.4 — BG and Window tile data area diff --git a/src/SUMMARY.md b/src/SUMMARY.md index e2eb5caa..790838a5 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -18,6 +18,7 @@ - [Tile Maps](./Tile_Maps.md) - [OAM](./OAM.md) - [OAM DMA Transfer](./OAM_DMA_Transfer.md) + - [Window](./Window.md) - [LCD Control](./LCDC.md) - [LCD Status Registers](./STAT.md) - [Scrolling](./Scrolling.md) diff --git a/src/Scrolling.md b/src/Scrolling.md index 098edeae..10437133 100644 --- a/src/Scrolling.md +++ b/src/Scrolling.md @@ -1,5 +1,4 @@ - -# LCD Position and Scrolling +# Viewport Position (Scrolling) These registers can be accessed even during Mode 3, but modifications may not take effect immediately (see further below). @@ -22,46 +21,8 @@ Example from the homebrew game *Mindy's Hike*: -## FF4A–FF4B — WY, WX: Window Y position, X position plus 7 - -These two registers specify the on-screen coordinates of [the Window](#Window)'s top-left pixel. - -The Window is visible (if enabled) when both coordinates are in the ranges -WX=0..166, WY=0..143 respectively. Values WX=7, WY=0 place the Window at the -top left of the screen, completely covering the background. - -:::warning Warning - -WX values 0 and 166 are unreliable due to hardware bugs. - -If WX is set to 0, the window will "stutter" horizontally when SCX changes -(depending on SCX % 8). - -If WX is set to 166, the window will span the entirety of the following -scanline. - -::: - ## Mid-frame behavior -### Scrolling - The scroll registers are re-read on each [tile fetch](<#Get Tile>), except for the low 3 bits of SCX, which are only read at the beginning of the scanline (for the initial shifting of pixels). All models before the CGB-D read the Y coordinate once for each bitplane (so a very precisely timed SCY write allows "desyncing" them), but CGB-D and later use the same Y coordinate for both no matter what. - -### Window - -While the Window should work as just mentioned, writing to WX, WY etc. mid-frame shows a more articulated behavior. - -For the window to be displayed on a scanline, the following conditions must be met: - -- **WY condition was triggered**: i.e. at some point in this frame the value of WY was equal to LY (checked at the start of Mode 2 only) -- **WX condition was triggered**: i.e. the current X coordinate being rendered + 7 was equal to WX -- Window enable bit in LCDC is set - -If the WY condition has already been triggered and at the start of a row the window enable bit was set, -then resetting that bit before the WX condition gets triggered on that row yields a nice window glitch pixel where the window would have been activated. - -The way the Window selects which line of its tilemap to render may be surprising: the Y position is selected by an internal counter, which is reset to 0 during VBlank and **only** incremented when the Window starts being rendered on a given scanline. -In particular, this means that hiding the Window mid-frame in any way (via either `WX` or `LCDC`, usually to display a status bar at the top *and* bottom of the screen) will also inhibit incrementing that Y-position counter. diff --git a/src/Window.md b/src/Window.md new file mode 100644 index 00000000..98386267 --- /dev/null +++ b/src/Window.md @@ -0,0 +1,37 @@ +# Window + +## FF4A–FF4B — WY, WX: Window Y position, X position plus 7 + +These two registers specify the on-screen coordinates of [the Window](#Window)'s top-left pixel. + +The Window is visible (if enabled) when both coordinates are in the ranges +WX=0..166, WY=0..143 respectively. Values WX=7, WY=0 place the Window at the +top left of the screen, completely covering the background. + +:::warning Warning + +WX values 0 and 166 are unreliable due to hardware bugs. + +If WX is set to 0, the window will "stutter" horizontally when SCX changes +(depending on SCX % 8). + +If WX is set to 166, the window will span the entirety of the following +scanline. + +::: + +## Window mid-frame behavior + +While the Window should work as just mentioned, writing to WX, WY etc. mid-frame shows a more articulated behavior. + +For the window to be displayed on a scanline, the following conditions must be met: + +- **WY condition was triggered**: i.e. at some point in this frame the value of WY was equal to LY (checked at the start of Mode 2 only) +- **WX condition was triggered**: i.e. the current X coordinate being rendered + 7 was equal to WX +- Window enable bit in LCDC is set + +If the WY condition has already been triggered and at the start of a row the window enable bit was set, +then resetting that bit before the WX condition gets triggered on that row yields a nice window glitch pixel where the window would have been activated. + +The way the Window selects which line of its tilemap to render may be surprising: the Y position is selected by an internal counter, which is reset to 0 during VBlank and **only** incremented when the Window starts being rendered on a given scanline. +In particular, this means that hiding the Window mid-frame in any way (via either `WX` or `LCDC`, usually to display a status bar at the top *and* bottom of the screen) will also inhibit incrementing that Y-position counter. From 396277fc89d9920f6a3c2f511768b94d23da840a Mon Sep 17 00:00:00 2001 From: ISSOtm Date: Sun, 21 Sep 2025 19:43:45 +0200 Subject: [PATCH 2/4] Document more of the Window --- src/Interrupts.md | 8 +++---- src/Memory_Map.md | 2 +- src/Rendering.md | 2 +- src/Scrolling.md | 6 ++--- src/Tile_Maps.md | 2 +- src/Window.md | 59 ++++++++++++++++++++++++++++++----------------- src/halt.md | 2 +- src/othermbc.md | 2 +- 8 files changed, 50 insertions(+), 33 deletions(-) diff --git a/src/Interrupts.md b/src/Interrupts.md index 07119967..a7ebebc6 100644 --- a/src/Interrupts.md +++ b/src/Interrupts.md @@ -8,7 +8,7 @@ - **`ei`**: Enables interrupt handling (that is, `IME := 1`) - **`di`**: Disables interrupt handling (that is, `IME := 0`) - **`reti`**: Enables interrupts and returns (same as `ei` immediately followed by `ret`) -- **When an [interrupt handler](<#Interrupt Handling>) is executed**: Disables interrupts before `call`ing the interrupt handler +- **When an [interrupt handler](<#Interrupt handling>) is executed**: Disables interrupts before `call`ing the interrupt handler `IME` is unset (interrupts are disabled) [when the game starts running](<#0100-0103 — Entry point>). @@ -53,7 +53,7 @@ may still do that in order to manually request (or discard) interrupts. Just like real interrupts, a manually requested interrupt isn't serviced unless/until `IME` and `IE` allow it. -## Interrupt Handling +## Interrupt handling 1. The `IF` bit corresponding to this interrupt and the `IME` flag are reset by the CPU. The former "acknowledges" the interrupt, while the latter prevents any further interrupts @@ -71,7 +71,7 @@ This consumes one last M-cycle. The entire process [lasts 5 M-cycles](https://gist.github.com/SonoSooS/c0055300670d678b5ae8433e20bea595#user-content-isr-and-nmi). -## Interrupt Priorities +## Interrupt priorities In the following circumstances it is possible that more than one bit in the IF register is set, requesting more than one interrupt at once: @@ -85,7 +85,7 @@ is serviced first. The priorities follow the order of the bits in the IE and IF registers: Bit 0 (VBlank) has the highest priority, and Bit 4 (Joypad) has the lowest priority. -## Nested Interrupts +## Nested interrupt handling The CPU automatically disables all the other interrupts by setting IME=0 when it services an interrupt. Usually IME remains zero until the diff --git a/src/Memory_Map.md b/src/Memory_Map.md index 546cb08e..430213e1 100644 --- a/src/Memory_Map.md +++ b/src/Memory_Map.md @@ -29,7 +29,7 @@ $FF04 | $FF07 | DMG | [Timer and divider](<#Timer and Divider Reg $FF0F | | DMG | [Interrupts](<#FF0F — IF: Interrupt flag>) $FF10 | $FF26 | DMG | [Audio](<#Audio Registers>) $FF30 | $FF3F | DMG | [Wave pattern](<#FF30–FF3F — Wave pattern RAM>) -$FF40 | $FF4B | DMG | LCD [Control](<#FF40 — LCDC: LCD control>), [Status](<#FF41 — STAT: LCD status>), [Position, Scrolling](<#LCD Position and Scrolling>), and [Palettes](<#Palettes>) +$FF40 | $FF4B | DMG | LCD [Control](<#FF40 — LCDC: LCD control>), [Status](<#FF41 — STAT: LCD status>), [Position, Scrolling](<#Viewport position (Scrolling)>), and [Palettes](<#Palettes>) $FF4C | $FF4D | CGB | [KEY0](<#FF4C — KEY0/SYS (CGB Mode only): CPU mode select>) and [KEY1](<#FF4D — KEY1/SPD (CGB Mode only): Prepare speed switch>) $FF4F | | CGB | [VRAM Bank Select](<#FF4F — VBK (CGB Mode only): VRAM bank>) $FF50 | | DMG | [Boot ROM mapping control](<#Power-Up Sequence>) diff --git a/src/Rendering.md b/src/Rendering.md index 4f2baa6b..f5748b09 100644 --- a/src/Rendering.md +++ b/src/Rendering.md @@ -6,7 +6,7 @@ The entire frame is not drawn atomically; instead, the image is drawn by the **< A frame consists of 154 **scanlines**; during the first 144, the screen is drawn top to bottom, left to right. The main implication of this rendering process is the existence of **raster effects**: modifying some rendering parameters in the middle of rendering. -The most famous raster effect is modifying the [scrolling registers](<#LCD Position and Scrolling>) between scanlines to create a ["wavy" effect](https://gbdev.io/guides/deadcscroll#effects). +The most famous raster effect is modifying the [scrolling registers](<#Viewport position (Scrolling)>) between scanlines to create a ["wavy" effect](https://gbdev.io/guides/deadcscroll#effects). A "**dot**" = one 222 Hz (≅ 4.194 MHz) time unit. Dots remain the same regardless of whether the CPU is in [Double Speed mode](<#FF4D — KEY1/SPD (CGB Mode only): Prepare speed switch>), so there are 4 dots per Normal Speed M-cycle, and 2 per Double Speed M-cycle. diff --git a/src/Scrolling.md b/src/Scrolling.md index 10437133..5979b3f8 100644 --- a/src/Scrolling.md +++ b/src/Scrolling.md @@ -1,4 +1,4 @@ -# Viewport Position (Scrolling) +# Viewport position (Scrolling) These registers can be accessed even during Mode 3, but modifications may not take effect immediately (see further below). @@ -23,6 +23,6 @@ Example from the homebrew game *Mindy's Hike*: ## Mid-frame behavior -The scroll registers are re-read on each [tile fetch](<#Get Tile>), except for the low 3 bits of SCX, which are only read at the beginning of the scanline (for the initial shifting of pixels). +The scroll registers are re-read on each [tile fetch](<#Get Tile>), except for the low 3 bits of `SCX`, which are only read at the beginning of the scanline (for the initial shifting of pixels). -All models before the CGB-D read the Y coordinate once for each bitplane (so a very precisely timed SCY write allows "desyncing" them), but CGB-D and later use the same Y coordinate for both no matter what. +All models before the CGB-D read the Y coordinate once for each bitplane (so a very precisely timed `SCY` write allows "desyncing" them), but CGB-D and later use the same Y coordinate for both no matter what. diff --git a/src/Tile_Maps.md b/src/Tile_Maps.md index b72a4435..3dd57373 100644 --- a/src/Tile_Maps.md +++ b/src/Tile_Maps.md @@ -106,7 +106,7 @@ for the definition of "Window visibility".) :::tip Window Internal Line Counter -The window keeps an internal line counter that's functionally similar to `LY`, and increments alongside it. However, it only gets incremented when the window is visible, as described [here](<#FF4A–FF4B — WY, WX: Window Y position, X position plus 7>). +The window keeps an internal line counter that's functionally similar to `LY`, and increments alongside it. However, it only gets incremented when the window is visible, as described [here](<#Window rendering criteria>). This line counter determines what window line is to be rendered on the current scanline. diff --git a/src/Window.md b/src/Window.md index 98386267..21454d09 100644 --- a/src/Window.md +++ b/src/Window.md @@ -1,37 +1,54 @@ -# Window +# Window behavior ## FF4A–FF4B — WY, WX: Window Y position, X position plus 7 -These two registers specify the on-screen coordinates of [the Window](#Window)'s top-left pixel. +These two registers specify the on-screen coordinates of [the Window]'s top-left pixel. -The Window is visible (if enabled) when both coordinates are in the ranges -WX=0..166, WY=0..143 respectively. Values WX=7, WY=0 place the Window at the -top left of the screen, completely covering the background. +The Window is visible (if enabled) when `WX` and `WY` are in the range \[0; 166\] and \[0; 143\] respectively. +Values `WX`=7, `WY`=0 place the Window at the top left of the screen, completely covering the background. -:::warning Warning +## Window mid-frame behavior + +While the Window should work as just mentioned, writing to `WX`, `WY` etc. mid-frame displays more articulated behavior. +There are several aspects of the window that respond differently to various mid-frame interactions; the **tl;dr** is this: + +- For the least glitchy results, only write to `WX`, `WY`, and `LCDC` during VBlank (possibly in your [VBlank interrupt handler]); if mid-frame writes are required, prefer writing during HBlank. +- If intending to hide the Window for part of the screen (e.g. to have a status bar at the *top* of the screen instead of the bottom), hide it by setting `WX` to a high value rather than writing to `LCDC`. + +### Window rendering criteria -WX values 0 and 166 are unreliable due to hardware bugs. +The PPU keeps track of a “**Y condition**” throughout a frame. -If WX is set to 0, the window will "stutter" horizontally when SCX changes -(depending on SCX % 8). +- On each VBlank, the *Y condition* is cleared (becomes false). +- At the beginning of each scanline, if the value of `WY` is equal to [`LY`], the *Y condition* becomes true (and remains so for subsequent scanlines). -If WX is set to 166, the window will span the entirety of the following -scanline. +:::tip Erratum + +On GBC, clearing the [Window enable bit] in `LCDC` resets the *Y condition*; `WY` must be set to `LY` or greater for the Window to display again in the current frame. ::: -## Window mid-frame behavior +Additionally, the PPU maintains a counter, initialized to 0 at the beginning of each scanline. +The counter is incremented for each pixel rendered; however, it also increments 7 times before the first pixel is actually rendered (this covers pixels discarded during the initial “fine scroll” adjustment). + +When this counter is equal to `WX`, if the *Y condition* is true and the [Window enable bit] is set in `LCDC`, background rendering is reset, beginning anew from the active row of the Window's tilemap. +The coordinate of the active Window row is then incremented. -While the Window should work as just mentioned, writing to WX, WY etc. mid-frame shows a more articulated behavior. +- This process can happen more than once per scanline, making the Window's “tilemap Y coordinate” increase more than once in the scanline. + (This is demonstrated by the TODO test ROM.) -For the window to be displayed on a scanline, the following conditions must be met: + However, this requires “disabling” the Window by briefly clearing its enable bit from `LCDC` first. +- If this process doesn't happen, the Window's “tilemap Y coordinate” does not increase; so, if the Window is hidden (by any means) on a given scanline, the row of pixels rendered the next time it's shown will be the same as if it had not been hidden in the first place, producing a sort of vertical striped stretching: -- **WY condition was triggered**: i.e. at some point in this frame the value of WY was equal to LY (checked at the start of Mode 2 only) -- **WX condition was triggered**: i.e. the current X coordinate being rendered + 7 was equal to WX -- Window enable bit in LCDC is set + ![Visual demonstration](https://github.com/mattcurrie/mealybug-tearoom-tests/raw/master/expected/DMG-blob/m2_win_en_toggle.png?raw=true) +- If `WX` is equal to 0, the Window is switched to before the initial “fine scroll” adjustment, causing it to be shifted left by SCX % 8 pixels. +- On monochrome systems, `WX` = 166 (which would normally show a single Window pixel, along the right edge of the screen) exhibits a bug: the Window spans the entire screen, but offset vertically by one scanline. +- On monochrome systems, if the Window is disabled via `LCDC`, but the other conditions are met *and* it would have started rendering exactly on a BG tile boundary, then where it would have started rendering, a single pixel with ID 0 (i.e. drawn as the first entry in [the BG palette]) is inserted; this offsets the remainder of the scanline.[^star_trek] -If the WY condition has already been triggered and at the start of a row the window enable bit was set, -then resetting that bit before the WX condition gets triggered on that row yields a nice window glitch pixel where the window would have been activated. +[^star_trek]: This was discovered as affecting the game *Star Trek 25th anniversary*; more information and a test ROM are available [in this thread](https://github.com/LIJI32/SameBoy/issues/278#issuecomment-1189712129). -The way the Window selects which line of its tilemap to render may be surprising: the Y position is selected by an internal counter, which is reset to 0 during VBlank and **only** incremented when the Window starts being rendered on a given scanline. -In particular, this means that hiding the Window mid-frame in any way (via either `WX` or `LCDC`, usually to display a status bar at the top *and* bottom of the screen) will also inhibit incrementing that Y-position counter. +[the Window]: #Window +[VBlank interrupt handler]: <#INT $40 — VBlank interrupt> +[Window enable bit]: <#LCDC.5 — Window enable> +[`LY`]: <#FF44 — LY: LCD Y coordinate \[read-only\]> +[the BG palette]: <#FF47 — BGP (Non-CGB Mode only): BG palette data> diff --git a/src/halt.md b/src/halt.md index 7e330b4b..cb93daa0 100644 --- a/src/halt.md +++ b/src/halt.md @@ -7,7 +7,7 @@ and [`IF`](<#FF0F — IF: Interrupt flag>) is non-zero. Most commonly, [`IME`](<#IME: Interrupt master enable flag \[write only\]>) is set. In this case, the CPU simply wakes up, and before executing the instruction -after the `halt`, the [interrupt handler is called](<#Interrupt Handling>) +after the `halt`, the [interrupt handler is called](<#Interrupt handling>) normally. If `IME` is *not* set, there are two distinct cases, depending on whether an diff --git a/src/othermbc.md b/src/othermbc.md index 57df0bf6..33604baa 100644 --- a/src/othermbc.md +++ b/src/othermbc.md @@ -55,7 +55,7 @@ the mapper consists of a single standard 74 series logic chip, it has two unusual properties: First, unlike a usual MBC, it switches the whole 32 KiB ROM area instead -of just the $4000-$7FFF area. Therefore, if you want to use [the interrupt vectors](<#Interrupt Handling>) +of just the $4000-$7FFF area. Therefore, if you want to use [the interrupt vectors](<#Interrupt handling>) with this cart, you should duplicate them across all banks. Additionally, since the 74LS377's contents can't be guaranteed when powering on, the ROM header and some code for switching to a known bank should also From e5577a680817b15b62313de47ed16660809ca3e1 Mon Sep 17 00:00:00 2001 From: Eldred Habert Date: Sat, 4 Oct 2025 06:18:00 +0200 Subject: [PATCH 3/4] =?UTF-8?q?Remove=20=E2=80=9CErratum=E2=80=9D=20wordin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Antonio Vivace --- src/Window.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Window.md b/src/Window.md index 21454d09..42347535 100644 --- a/src/Window.md +++ b/src/Window.md @@ -22,7 +22,7 @@ The PPU keeps track of a “**Y condition**” throughout a frame. - On each VBlank, the *Y condition* is cleared (becomes false). - At the beginning of each scanline, if the value of `WY` is equal to [`LY`], the *Y condition* becomes true (and remains so for subsequent scanlines). -:::tip Erratum +:::tip Note On GBC, clearing the [Window enable bit] in `LCDC` resets the *Y condition*; `WY` must be set to `LY` or greater for the Window to display again in the current frame. From 2398033dd224a9ad0d774a33dc5263bc1f1c68fd Mon Sep 17 00:00:00 2001 From: Eldred Habert Date: Sat, 4 Oct 2025 06:19:50 +0200 Subject: [PATCH 4/4] Use ASCII quotes Co-authored-by: Antonio Vivace --- src/Window.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Window.md b/src/Window.md index 42347535..9c12d9a9 100644 --- a/src/Window.md +++ b/src/Window.md @@ -29,19 +29,19 @@ On GBC, clearing the [Window enable bit] in `LCDC` resets the *Y condition*; `WY ::: Additionally, the PPU maintains a counter, initialized to 0 at the beginning of each scanline. -The counter is incremented for each pixel rendered; however, it also increments 7 times before the first pixel is actually rendered (this covers pixels discarded during the initial “fine scroll” adjustment). +The counter is incremented for each pixel rendered; however, it also increments 7 times before the first pixel is actually rendered (this covers pixels discarded during the initial "fine scroll" adjustment). When this counter is equal to `WX`, if the *Y condition* is true and the [Window enable bit] is set in `LCDC`, background rendering is reset, beginning anew from the active row of the Window's tilemap. The coordinate of the active Window row is then incremented. -- This process can happen more than once per scanline, making the Window's “tilemap Y coordinate” increase more than once in the scanline. +- This process can happen more than once per scanline, making the Window's "tilemap Y coordinate" increase more than once in the scanline. (This is demonstrated by the TODO test ROM.) - However, this requires “disabling” the Window by briefly clearing its enable bit from `LCDC` first. -- If this process doesn't happen, the Window's “tilemap Y coordinate” does not increase; so, if the Window is hidden (by any means) on a given scanline, the row of pixels rendered the next time it's shown will be the same as if it had not been hidden in the first place, producing a sort of vertical striped stretching: + However, this requires "disabling" the Window by briefly clearing its enable bit from `LCDC` first. +- If this process doesn't happen, the Window's "tilemap Y coordinate" does not increase; so, if the Window is hidden (by any means) on a given scanline, the row of pixels rendered the next time it's shown will be the same as if it had not been hidden in the first place, producing a sort of vertical striped stretching: ![Visual demonstration](https://github.com/mattcurrie/mealybug-tearoom-tests/raw/master/expected/DMG-blob/m2_win_en_toggle.png?raw=true) -- If `WX` is equal to 0, the Window is switched to before the initial “fine scroll” adjustment, causing it to be shifted left by SCX % 8 pixels. +- If `WX` is equal to 0, the Window is switched to before the initial "fine scroll" adjustment, causing it to be shifted left by SCX % 8 pixels. - On monochrome systems, `WX` = 166 (which would normally show a single Window pixel, along the right edge of the screen) exhibits a bug: the Window spans the entire screen, but offset vertically by one scanline. - On monochrome systems, if the Window is disabled via `LCDC`, but the other conditions are met *and* it would have started rendering exactly on a BG tile boundary, then where it would have started rendering, a single pixel with ID 0 (i.e. drawn as the first entry in [the BG palette]) is inserted; this offsets the remainder of the scanline.[^star_trek]