fix(video): read BG maps and tiles through the 128 KiB VRAM mirror window
This commit is contained in:
parent
8a69e837cf
commit
d7de5f7121
6 changed files with 60 additions and 50 deletions
|
|
@ -43,11 +43,11 @@ impl fmt::Display for VideoError {
|
||||||
),
|
),
|
||||||
Self::UnsupportedTileAddress(address) => write!(
|
Self::UnsupportedTileAddress(address) => write!(
|
||||||
f,
|
f,
|
||||||
"text background tile address {address:#x} exceeds the supported 64 KiB BG area"
|
"background tile address {address:#x} exceeds the supported 128 KiB BG VRAM window"
|
||||||
),
|
),
|
||||||
Self::UnsupportedMapAddress(address) => write!(
|
Self::UnsupportedMapAddress(address) => write!(
|
||||||
f,
|
f,
|
||||||
"background map address {address:#x} exceeds the supported 64 KiB BG area"
|
"background map address {address:#x} exceeds the supported 128 KiB BG VRAM window"
|
||||||
),
|
),
|
||||||
Self::UnsupportedObject { index, reason } => write!(f, "OBJ{index}: {reason}"),
|
Self::UnsupportedObject { index, reason } => write!(f, "OBJ{index}: {reason}"),
|
||||||
Self::UnsupportedLayers(control) => {
|
Self::UnsupportedLayers(control) => {
|
||||||
|
|
@ -250,6 +250,16 @@ fn halfword(bytes: &[u8], offset: usize) -> u16 {
|
||||||
u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
|
u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Translate a BG VRAM offset through the bus's 128 KiB mirror window.
|
||||||
|
fn vram_index(offset: usize) -> usize {
|
||||||
|
let offset = offset & 0x1ffff;
|
||||||
|
if offset >= 0x18000 {
|
||||||
|
offset - 0x8000
|
||||||
|
} else {
|
||||||
|
offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn text_pixel(
|
fn text_pixel(
|
||||||
bg: Background,
|
bg: Background,
|
||||||
vram: &[u8],
|
vram: &[u8],
|
||||||
|
|
@ -265,10 +275,10 @@ fn text_pixel(
|
||||||
let y = (y + usize::from(bg.y)) % height;
|
let y = (y + usize::from(bg.y)) % height;
|
||||||
let block = x / 256 + (y / 256) * (width / 256);
|
let block = x / 256 + (y / 256) * (width / 256);
|
||||||
let map = ((control >> 8) & 31) * 0x800 + block * 0x800 + ((y / 8 % 32) * 32 + x / 8 % 32) * 2;
|
let map = ((control >> 8) & 31) * 0x800 + block * 0x800 + ((y / 8 % 32) * 32 + x / 8 % 32) * 2;
|
||||||
if map >= 0x10000 {
|
if map >= 0x20000 {
|
||||||
return Err(VideoError::UnsupportedMapAddress(map));
|
return Err(VideoError::UnsupportedMapAddress(map));
|
||||||
}
|
}
|
||||||
let entry = usize::from(halfword(vram, map));
|
let entry = usize::from(halfword(vram, vram_index(map)));
|
||||||
let tx = (x & 7) ^ if entry & 0x400 != 0 { 7 } else { 0 };
|
let tx = (x & 7) ^ if entry & 0x400 != 0 { 7 } else { 0 };
|
||||||
let ty = (y & 7) ^ if entry & 0x800 != 0 { 7 } else { 0 };
|
let ty = (y & 7) ^ if entry & 0x800 != 0 { 7 } else { 0 };
|
||||||
let eight_bit = control & 0x80 != 0;
|
let eight_bit = control & 0x80 != 0;
|
||||||
|
|
@ -276,11 +286,10 @@ fn text_pixel(
|
||||||
let address = ((control >> 2) & 3) * 0x4000
|
let address = ((control >> 2) & 3) * 0x4000
|
||||||
+ (entry & 0x3ff) * if eight_bit { 64 } else { 32 }
|
+ (entry & 0x3ff) * if eight_bit { 64 } else { 32 }
|
||||||
+ if eight_bit { pixel } else { pixel / 2 };
|
+ if eight_bit { pixel } else { pixel / 2 };
|
||||||
// The real PPU has special behavior outside BG VRAM. Do not read OBJ tiles
|
if address >= 0x20000 {
|
||||||
// as ordinary BG data or silently wrap this unsupported case.
|
|
||||||
if address >= 0x10000 {
|
|
||||||
return Err(VideoError::UnsupportedTileAddress(address));
|
return Err(VideoError::UnsupportedTileAddress(address));
|
||||||
}
|
}
|
||||||
|
let address = vram_index(address);
|
||||||
let index = if eight_bit {
|
let index = if eight_bit {
|
||||||
usize::from(vram[address])
|
usize::from(vram[address])
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//! Internal origins advance at line boundaries. Pixel-fetch and per-access timing
|
//! Internal origins advance at line boundaries. Pixel-fetch and per-access timing
|
||||||
//! remain outside this row-level model.
|
//! remain outside this row-level model.
|
||||||
|
|
||||||
use super::{halfword, Background, VideoError, HEIGHT, WIDTH};
|
use super::{halfword, vram_index, Background, VideoError, HEIGHT, WIDTH};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use crate::display::{CYCLES_PER_FRAME, CYCLES_PER_LINE};
|
use crate::display::{CYCLES_PER_FRAME, CYCLES_PER_LINE};
|
||||||
|
|
||||||
|
|
@ -136,14 +136,18 @@ pub(super) fn pixel(
|
||||||
}
|
}
|
||||||
let (tx, ty) = (tx as usize, ty as usize);
|
let (tx, ty) = (tx as usize, ty as usize);
|
||||||
let map = ((control >> 8) & 31) * 0x800 + (ty / 8) * (size as usize / 8) + tx / 8;
|
let map = ((control >> 8) & 31) * 0x800 + (ty / 8) * (size as usize / 8) + tx / 8;
|
||||||
if map >= 0x10000 {
|
if map >= 0x20000 {
|
||||||
return Err(VideoError::UnsupportedMapAddress(map));
|
return Err(VideoError::UnsupportedMapAddress(map));
|
||||||
}
|
}
|
||||||
// Affine maps have byte-sized indices and no flip/palette-bank attributes.
|
// Affine maps have byte-sized indices and no flip/palette-bank attributes.
|
||||||
// Every tile is 8bpp, regardless of BGCNT bit 7.
|
// Every tile is 8bpp, regardless of BGCNT bit 7.
|
||||||
|
let address = vram_index(map);
|
||||||
let address =
|
let address =
|
||||||
((control >> 2) & 3) * 0x4000 + usize::from(vram[map]) * 64 + (ty % 8) * 8 + tx % 8;
|
((control >> 2) & 3) * 0x4000 + usize::from(vram[address]) * 64 + (ty % 8) * 8 + tx % 8;
|
||||||
let index = usize::from(vram[address]); // Maximum address is 0xffff.
|
if address >= 0x20000 {
|
||||||
|
return Err(VideoError::UnsupportedTileAddress(address));
|
||||||
|
}
|
||||||
|
let index = usize::from(vram[vram_index(address)]);
|
||||||
Ok(if index == 0 {
|
Ok(if index == 0 {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,7 @@ fn transformed_backgrounds_composite_with_regular_and_affine_sprites() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn diagnostics_leave_output_unchanged_and_disabled_layers_skip_validation() {
|
fn unsupported_layers_leave_output_unchanged_and_disabled_layers_skip_validation() {
|
||||||
let mut m = setup(2, 3, false);
|
let mut m = setup(2, 3, false);
|
||||||
let mut f = Framebuffer::default();
|
let mut f = Framebuffer::default();
|
||||||
f.clear(0x001f);
|
f.clear(0x001f);
|
||||||
|
|
@ -262,15 +262,8 @@ fn diagnostics_leave_output_unchanged_and_disabled_layers_skip_validation() {
|
||||||
m.render_frame(&mut f),
|
m.render_frame(&mut f),
|
||||||
Err(VideoError::UnsupportedLayers(control))
|
Err(VideoError::UnsupportedLayers(control))
|
||||||
);
|
);
|
||||||
}
|
|
||||||
m.write16(DISPCNT, 0x402).unwrap();
|
|
||||||
m.write16(BG2CNT, 0xdf00).unwrap(); // Map31, 1024 pixels.
|
|
||||||
m.write32(BG2Y, 128 * 256).unwrap(); // Flat row starts outside first64K.
|
|
||||||
assert_eq!(
|
|
||||||
m.render_frame(&mut f),
|
|
||||||
Err(VideoError::UnsupportedMapAddress(0x10000))
|
|
||||||
);
|
|
||||||
assert!(f.pixels().iter().all(|&p| p == 0xff0000));
|
assert!(f.pixels().iter().all(|&p| p == 0xff0000));
|
||||||
|
}
|
||||||
m.write16(DISPCNT, 2).unwrap();
|
m.write16(DISPCNT, 2).unwrap();
|
||||||
m.render_frame(&mut f).unwrap();
|
m.render_frame(&mut f).unwrap();
|
||||||
assert!(f.pixels().iter().all(|&p| p == rgb555_to_rgb888(0x4000)));
|
assert!(f.pixels().iter().all(|&p| p == rgb555_to_rgb888(0x4000)));
|
||||||
|
|
@ -279,6 +272,17 @@ fn diagnostics_leave_output_unchanged_and_disabled_layers_skip_validation() {
|
||||||
assert!(f.pixels().iter().all(|&p| p == 0xffffff));
|
assert!(f.pixels().iter().all(|&p| p == 0xffffff));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn affine_map_at_map_base_31_reads_high_vram_through_the_bus_mirror() {
|
||||||
|
let mut m = setup(2, 3, false);
|
||||||
|
m.write16(BG2CNT, 0xdf00).unwrap(); // Map31, 1024 pixels.
|
||||||
|
m.write32(BG2Y, 128 * 256).unwrap(); // Map row16 starts at offset0x10000.
|
||||||
|
m.write16(VRAM + 0x18000, 1).unwrap(); // Halfword bus mirror of the selected map byte.
|
||||||
|
let mut f = Framebuffer::default();
|
||||||
|
m.render_frame(&mut f).unwrap();
|
||||||
|
assert_eq!(f.pixels()[0], rgb555_to_rgb888(31));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cpu_demo_pans_rotates_zooms_clips_cancels_opposites_and_resets() {
|
fn cpu_demo_pans_rotates_zooms_clips_cancels_opposites_and_resets() {
|
||||||
use gba_core::input::{Button, Buttons};
|
use gba_core::input::{Button, Buttons};
|
||||||
|
|
|
||||||
|
|
@ -184,48 +184,40 @@ fn priorities_ties_transparency_and_opaque_black_composite_all_four_layers() {
|
||||||
fn character_base_and_high_tile_numbers_cross_character_blocks() {
|
fn character_base_and_high_tile_numbers_cross_character_blocks() {
|
||||||
for eight in [false, true] {
|
for eight in [false, true] {
|
||||||
let mut m = setup();
|
let mut m = setup();
|
||||||
m.write16(BG0CNT, 0x1004 | if eight { 0x80 } else { 0 })
|
m.write16(BG0CNT, 0x100c | if eight { 0x80 } else { 0 })
|
||||||
.unwrap();
|
.unwrap();
|
||||||
m.write16(VRAM + 0x8000, 1023).unwrap();
|
m.write16(VRAM + 0x8000, 1023).unwrap();
|
||||||
let address = 0x4000 + 1023 * if eight { 64 } else { 32 };
|
let address = 0xc000 + 1023 * if eight { 64 } else { 32 };
|
||||||
if eight {
|
tile(
|
||||||
assert!(matches!(
|
&mut m,
|
||||||
m.render_frame(&mut Framebuffer::default()),
|
address,
|
||||||
Err(VideoError::UnsupportedTileAddress(_))
|
if eight { 0x0101 } else { 0x1111 },
|
||||||
));
|
if eight { 64 } else { 32 },
|
||||||
} else {
|
);
|
||||||
tile(&mut m, address, 0x1111, 32);
|
|
||||||
assert_eq!(pixel(&m, 0, 0), 0xff0000);
|
assert_eq!(pixel(&m, 0, 0), 0xff0000);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn screen_map_outside_supported_bg_area_reports_error_without_wrapping() {
|
fn wide_screen_block_at_map_base_31_reads_beyond_64k() {
|
||||||
let mut m = setup();
|
let mut m = setup();
|
||||||
m.write16(BG0CNT, 0x5f04).unwrap(); // Map block31, wide; tile base1.
|
m.write16(BG0CNT, 0x5f04).unwrap(); // Map block31, wide; tile base1.
|
||||||
m.write16(BG0HOFS, 256).unwrap(); // Second block lies outside the BG area.
|
m.write16(BG0HOFS, 256).unwrap(); // First pixel selects screen block1 at 0x10000.
|
||||||
let mut frame = Framebuffer::default();
|
m.write16(VRAM + 0x18000, 1).unwrap(); // Bus mirror of the selected map entry.
|
||||||
frame.clear(0x001f);
|
tile(&mut m, 0x4020, 0x1111, 32);
|
||||||
assert_eq!(
|
assert_eq!(pixel(&m, 0, 0), 0xff0000);
|
||||||
m.render_frame(&mut frame),
|
|
||||||
Err(VideoError::UnsupportedMapAddress(0x10000))
|
|
||||||
);
|
|
||||||
assert!(frame.pixels().iter().all(|&p| p == 0xff0000));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unsupported_features_leave_output_unchanged_and_forced_blank_bypasses_them() {
|
fn unsupported_modes_leave_output_unchanged_and_forced_blank_bypasses_them() {
|
||||||
let mut m = setup();
|
let mut m = setup();
|
||||||
let mut frame = Framebuffer::default();
|
let mut frame = Framebuffer::default();
|
||||||
frame.clear(0x001f);
|
frame.clear(0x001f);
|
||||||
m.write16(DISPCNT, 0x100).unwrap();
|
m.write16(DISPCNT, 0x106).unwrap();
|
||||||
m.write16(BG0CNT, 0x100c).unwrap();
|
assert_eq!(
|
||||||
m.write16(VRAM + 0x8002, 1023).unwrap(); // Error after some valid pixels.
|
|
||||||
assert!(matches!(
|
|
||||||
m.render_frame(&mut frame),
|
m.render_frame(&mut frame),
|
||||||
Err(VideoError::UnsupportedTileAddress(_))
|
Err(VideoError::UnsupportedMode(6))
|
||||||
));
|
);
|
||||||
assert!(frame.pixels().iter().all(|&p| p == 0xff0000));
|
assert!(frame.pixels().iter().all(|&p| p == 0xff0000));
|
||||||
m.write16(DISPCNT, 0xf180).unwrap();
|
m.write16(DISPCNT, 0xf180).unwrap();
|
||||||
m.render_frame(&mut frame).unwrap();
|
m.render_frame(&mut frame).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,8 @@ Mode 0 supports all four text backgrounds, with these features:
|
||||||
- Uncovered pixels use palette entry zero. Green swap runs after composition.
|
- Uncovered pixels use palette entry zero. Green swap runs after composition.
|
||||||
|
|
||||||
Mode 0 supports mosaic independently on each background. Rendering errors leave the output buffer unchanged.
|
Mode 0 supports mosaic independently on each background. Rendering errors leave the output buffer unchanged.
|
||||||
Background tile or map fetches outside the first 64 KiB return diagnostics.
|
Background tile or map fetches use the 128 KiB VRAM window; offsets `0x18000..0x1ffff`
|
||||||
|
mirror `0x10000..0x17fff`. Addresses at or beyond `0x20000` return diagnostics.
|
||||||
This is not a complete picture processing unit (PPU).
|
This is not a complete picture processing unit (PPU).
|
||||||
|
|
||||||
Affine background snapshots support these configurations:
|
Affine background snapshots support these configurations:
|
||||||
|
|
@ -94,7 +95,7 @@ Map sizes are 128×128, 256×256, 512×512, or 1024×1024 pixels.
|
||||||
BGCNT bit 13 selects wrapping or transparent edges in Modes 1 and 2. All bitmap modes ignore this bit.
|
BGCNT bit 13 selects wrapping or transparent edges in Modes 1 and 2. All bitmap modes ignore this bit.
|
||||||
Palette index zero is transparent in tiled backgrounds and Mode 4. Mode 3/5 black bitmap pixels remain opaque.
|
Palette index zero is transparent in tiled backgrounds and Mode 4. Mode 3/5 black bitmap pixels remain opaque.
|
||||||
Background priorities, sprite composition, and green swap apply after sampling.
|
Background priorities, sprite composition, and green swap apply after sampling.
|
||||||
Map fetches beyond the supported 64 KiB background area still return diagnostics.
|
Map fetches use the same 128 KiB VRAM mirror. Addresses at or beyond `0x20000` return diagnostics.
|
||||||
|
|
||||||
**Timing limit:** debug snapshots use one set of programmed registers for the whole image.
|
**Timing limit:** debug snapshots use one set of programmed registers for the whole image.
|
||||||
Row capture instead samples from separate internal X/Y origins.
|
Row capture instead samples from separate internal X/Y origins.
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,7 @@ The replacement BIOS now returns for that empty stream. Full starter-selection p
|
||||||
A later [visible-progress probe](research/emerald-visible-progress.md) renders the title screen, logo, NEW GAME/OPTION menu, Prof. Birch scene, and legible dialogue in attract mode.
|
A later [visible-progress probe](research/emerald-visible-progress.md) renders the title screen, logo, NEW GAME/OPTION menu, Prof. Birch scene, and legible dialogue in attract mode.
|
||||||
Sparse A/Start taps reach the gender prompt by 4,000 frames and the in-game start menu over the overworld by 12,000 frames without a diagnostic.
|
Sparse A/Start taps reach the gender prompt by 4,000 frames and the in-game start menu over the overworld by 12,000 frames without a diagnostic.
|
||||||
A 40,000-frame tapped run passed the reported 37,101-frame trap point without a diagnostic; blind taps take a different menu path, so the identical scene is not confirmed visited.
|
A 40,000-frame tapped run passed the reported 37,101-frame trap point without a diagnostic; blind taps take a different menu path, so the identical scene is not confirmed visited.
|
||||||
A local second-game ROM faults in boot init on a halfword store to ROM mirror `0x09fe2ffe` past its 16 MiB EOF; without save hardware it faults earlier on a Flash unlock write. Both are recorded, not fixed.
|
Text/affine background map and tile fetches now follow the 128 KiB VRAM mirror window instead of faulting past 64 KiB; this fixes a user-reported window diagnostic at map address `0x10d00` after 23,024 captured frames.
|
||||||
See [the startup result](research/emerald-reset.md) for the bounded run and next investigation.
|
See [the startup result](research/emerald-reset.md) for the bounded run and next investigation.
|
||||||
See [cartridge loading](hardware/cartridge.md) for file limits, step budgets, and exit status.
|
See [cartridge loading](hardware/cartridge.md) for file limits, step budgets, and exit status.
|
||||||
The [ROM suite runner](rom-tests.md) has original regressions and a [pinned public ARM baseline](public-arm-tests.md).
|
The [ROM suite runner](rom-tests.md) has original regressions and a [pinned public ARM baseline](public-arm-tests.md).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue