Skip to content
Merged
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
11 changes: 11 additions & 0 deletions docs/internals/chipset.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ Alice adds the FMODE wide-fetch latch, which scales the bitplane and
sprite fetch quanta (FMODE=0 stays byte-identical to the OCS/ECS slot
timing).

A sprite line's two DMA slots ($15+4N and $17+4N) are evaluated at their
own colour clocks: the first slot runs the line's descriptor/arming logic
and samples the DATA word(s) at that slot's beam time, the second samples
DATB at its own beam time and assembles the display line, so chip RAM
rewritten between the two slots is seen by DATB but not DATA. DMACON's
SPREN is sampled by each slot individually, so a mid-line edge fetches
exactly one word of the pair; the skipped word's slot leaves SPRxPT one
fetch behind and the data stream shifts accordingly, while the display
line reuses the stale latch on that side (a missed DATA slot never arms
the sprite).

Sprite DMA retains its latched POS/CTL descriptor independently from the
SPRxPT registers while a sprite data stream is active or waiting for VSTART.
Standard PAL/NTSC hard vertical blank inhibits sprite DMA at the top of the
Expand Down
38 changes: 37 additions & 1 deletion src/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ pub struct HeldSpriteLine {
pub vstop: i32,
}

#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
struct DisplaySpriteDmaState {
control: Option<DisplaySpriteControl>,
#[serde(skip, default = "unset_sprite_control_loaded_vpos")]
Expand All @@ -316,12 +316,48 @@ struct DisplaySpriteDmaState {
terminated: bool,
data_dma_active: bool,
last_line: Option<DisplaySpriteLineData>,
/// Words the hardware pointer lags behind the line-derived stream
/// position: slots skipped by mid-line SPREN edges never fetch, so
/// every later fetch of the stream reads that many words earlier.
data_word_skew: u32,
/// DATA word(s) fetched by the sprite's first DMA slot, sampled at that
/// slot's beam time, awaiting the second slot to assemble the line.
/// None when the first slot was skipped (or fetched nothing).
pending_data: Option<(u16, [u16; 3])>,
/// Line the first slot's evaluation ran for (entry logic + DATA fetch),
/// so the second slot completes rather than re-runs it.
#[serde(skip, default = "unset_sprite_control_loaded_vpos")]
pending_line_vpos: i32,
/// Line the per-line entry logic (vstop comparator, descriptor chain)
/// already ran for, keeping it idempotent across the two slots.
#[serde(skip, default = "unset_sprite_control_loaded_vpos")]
entry_line_vpos: i32,
}

fn unset_sprite_control_loaded_vpos() -> i32 {
i32::MIN
}

impl Default for DisplaySpriteDmaState {
fn default() -> Self {
// The line-marker fields must start unset, not at line 0: a derived
// zero default would make a fresh state claim its per-line work
// already ran when the beam is on line 0.
Self {
control: None,
control_loaded_vpos: unset_sprite_control_loaded_vpos(),
next_ptr: None,
terminated: false,
data_dma_active: false,
last_line: None,
data_word_skew: 0,
pending_data: None,
pending_line_vpos: unset_sprite_control_loaded_vpos(),
entry_line_vpos: unset_sprite_control_loaded_vpos(),
}
}
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SpriteControlRegisterWrite {
Pos,
Expand Down
Loading
Loading