Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/Interrupts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>).

Expand Down Expand Up @@ -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
Expand All @@ -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:

Expand All @@ -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
Expand Down
7 changes: 2 additions & 5 deletions src/LCDC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/Memory_Map.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>)
Expand Down
2 changes: 1 addition & 1 deletion src/Rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 2<sup>22</sup> 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.
Expand Down
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
45 changes: 3 additions & 42 deletions src/Scrolling.md
Original file line number Diff line number Diff line change
@@ -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).
Expand All @@ -22,46 +21,8 @@ Example from the homebrew game *Mindy's Hike*:

</figure>

## 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 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).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd think the low bits of SCX would only be read once per line, because that's the logical way to do so, but unfortunately this isn't true. The fetched tile's X position is calculated (roughly) by adding SCX to LX and taking the 5 topmost bits, rather than adding the 5 topmost bits of SCX and LX.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Do you know of a test ROM that checks this?

Also, it seems like it'd be a better fit for a separate PR, so I'll open one when I have some info to pen down.


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.
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.
2 changes: 1 addition & 1 deletion src/Tile_Maps.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
54 changes: 54 additions & 0 deletions src/Window.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Window behavior

## FF4A–FF4B — WY, WX: Window Y position, X position plus 7

These two registers specify the on-screen coordinates of [the Window]'s top-left pixel.

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.

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

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

:::

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.

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

![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 <math><mi>SCX</mi> <mo>%</mo> <mn>8</mn></math> 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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then where it would have started rendering sounds a bit convoluted here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Describing this accurately is really difficult, so if you can think of a better way..?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- 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]
- On monochrome systems, if the Window is disabled via `LCDC` but the other conditions are met, and rendering would have begun exactly on a BG tile boundary, then a single pixel with ID 0 (i.e. the first entry in [the BG palette]) is inserted at that point; this offsets the remainder of the scanline.[^star_trek]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this can occur anywhere within the scanline, not just at pixel 0. Perhaps “its rendering would have begun”? But that sounds even more confusing than my original phrasing...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ISSOtm where did I imply it's "at pixel 0"? The phrasing says A single pixel with ID 0 is inserted at that point (where "that point" is where rendering would have begun.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I interpreted “rendering would have begun” to mean the scanline's overall rendering, since it's unqualified.


[^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 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>
2 changes: 1 addition & 1 deletion src/halt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/othermbc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down