Add OAM sampling, affine canvases, tile mapping, priority metadata, OBJ windows, and mosaic composition.
213 lines
8.4 KiB
Rust
213 lines
8.4 KiB
Rust
//! Regular and affine OBJ sampling.
|
|
//! Individual fetch timing and OAM contention are not modeled.
|
|
|
|
use super::{halfword, Pixel, VideoError, WIDTH};
|
|
|
|
const SIZES: [[(usize, usize); 4]; 3] = [
|
|
[(8, 8), (16, 16), (32, 32), (64, 64)],
|
|
[(16, 8), (32, 8), (32, 16), (64, 32)],
|
|
[(8, 16), (8, 32), (16, 32), (32, 64)],
|
|
];
|
|
|
|
pub(super) struct Objects {
|
|
pub pixels: Vec<Option<Pixel>>,
|
|
pub window: Vec<bool>,
|
|
}
|
|
|
|
pub(crate) struct PreparedObjects {
|
|
samples: Vec<Sample>,
|
|
window: Vec<bool>,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct ObjectPixel {
|
|
palette_index: u16,
|
|
semi_transparent: bool,
|
|
}
|
|
|
|
// Transparent texels can change priority and mosaic without replacing color.
|
|
#[derive(Clone, Copy, Default)]
|
|
struct Sample {
|
|
pixel: Option<ObjectPixel>,
|
|
priority: u8,
|
|
mosaic: bool,
|
|
}
|
|
|
|
pub(crate) fn prepare(
|
|
control: u16,
|
|
mosaic: u16,
|
|
vertical_counter: Option<u8>,
|
|
oam: &[u8],
|
|
vram: &[u8],
|
|
rows: std::ops::Range<usize>,
|
|
) -> Result<PreparedObjects, VideoError> {
|
|
// Lower priority numbers win; equal priorities retain the earlier OAM entry.
|
|
// Color, priority, and mosaic metadata are tracked separately.
|
|
let mut output = PreparedObjects {
|
|
samples: vec![Sample::default(); WIDTH * rows.len()],
|
|
window: vec![false; WIDTH * rows.len()],
|
|
};
|
|
if control & 0x1000 == 0 {
|
|
return Ok(output);
|
|
}
|
|
let samples = &mut output.samples;
|
|
let mosaic_height = usize::from(mosaic >> 12) + 1;
|
|
let one_dimensional = control & 0x40 != 0;
|
|
for index in 0..128 {
|
|
let a = usize::from(halfword(oam, index * 8));
|
|
let b = usize::from(halfword(oam, index * 8 + 2));
|
|
let c = usize::from(halfword(oam, index * 8 + 4));
|
|
if a & 0x300 == 0x200 {
|
|
continue; // Disabled regular OBJ.
|
|
}
|
|
let reason = if a & 0xc00 == 0xc00 {
|
|
Some("prohibited OBJ mode")
|
|
} else if a >> 14 == 3 {
|
|
Some("prohibited OBJ shape")
|
|
} else {
|
|
None
|
|
};
|
|
if let Some(reason) = reason {
|
|
return Err(VideoError::UnsupportedObject { index, reason });
|
|
}
|
|
let window = a & 0xc00 == 0x800;
|
|
// Window coverage is prepared independently of its later display mask.
|
|
// OBJ-window coverage does not use either mosaic dimension.
|
|
let mosaic_enabled = a & 0x1000 != 0 && !window;
|
|
let (width, height) = SIZES[a >> 14][b >> 14];
|
|
let affine = a & 0x100 != 0;
|
|
let doubled = affine && a & 0x200 != 0;
|
|
let canvas_width = width << usize::from(doubled);
|
|
let canvas_height = height << usize::from(doubled);
|
|
let matrix = if affine {
|
|
let base = ((b >> 9) & 31) * 32 + 6;
|
|
[0, 8, 16, 24].map(|offset| i32::from(halfword(oam, base + offset) as i16))
|
|
} else {
|
|
[0; 4]
|
|
};
|
|
let eight_bit = a & 0x2000 != 0;
|
|
let mut tile = c & 0x3ff;
|
|
if control & 7 >= 3 && tile < 512 {
|
|
continue; // Bitmap modes reserve the first half of OBJ tile memory.
|
|
}
|
|
if eight_bit && !one_dimensional {
|
|
tile &= !1; // 2D 8bpp tiles are aligned to pairs of 32-byte slots.
|
|
}
|
|
let slot_width = if eight_bit { 2 } else { 1 };
|
|
let priority = ((c >> 10) & 3) as u8;
|
|
for y in rows.clone() {
|
|
let sy = (y + 256 - (a & 255)) & 255;
|
|
if sy >= canvas_height {
|
|
continue;
|
|
}
|
|
// Preparation latches the ahead-of-display OBJ counter; snapshots
|
|
// use the screen grid.
|
|
// Clamp partial first blocks to the object's first row, without
|
|
// extending its canvas. OBJ-window sampling bypasses this counter.
|
|
let sample_y = if mosaic_enabled {
|
|
sy.saturating_sub(vertical_counter.map_or(y % mosaic_height, usize::from))
|
|
} else {
|
|
sy
|
|
};
|
|
for sx in 0..canvas_width {
|
|
let x = ((b & 511) + sx) & 511;
|
|
if x >= WIDTH {
|
|
continue;
|
|
}
|
|
let offset = (y - rows.start) * WIDTH + x;
|
|
let (tx, ty) = if affine {
|
|
// Signed 8.8 inverse mapping from canvas center to texture center.
|
|
// Keep fractional products until the final arithmetic shift. Negative
|
|
// coordinates round down, not toward zero. Outside texels are transparent.
|
|
let dx = sx as i32 - canvas_width as i32 / 2;
|
|
let dy = sample_y as i32 - canvas_height as i32 / 2;
|
|
let tx = (matrix[0] * dx + matrix[1] * dy + ((width as i32) << 7)) >> 8;
|
|
let ty = (matrix[2] * dx + matrix[3] * dy + ((height as i32) << 7)) >> 8;
|
|
if tx < 0 || ty < 0 || tx >= width as i32 || ty >= height as i32 {
|
|
continue;
|
|
}
|
|
(tx as usize, ty as usize)
|
|
} else {
|
|
(
|
|
if b & 0x1000 != 0 { width - 1 - sx } else { sx },
|
|
if b & 0x2000 != 0 {
|
|
height - 1 - sample_y
|
|
} else {
|
|
sample_y
|
|
},
|
|
)
|
|
};
|
|
let slot = if one_dimensional {
|
|
tile + (ty / 8 * (width / 8) + tx / 8) * slot_width
|
|
} else {
|
|
// A 2D row is 32 slots wide. Horizontal overflow wraps within
|
|
// that row; vertical overflow wraps within 32 KiB OBJ VRAM.
|
|
(tile & !31) + (ty / 8) * 32 + ((tile + tx / 8 * slot_width) & 31)
|
|
};
|
|
let pixel = (ty % 8) * 8 + tx % 8;
|
|
let address =
|
|
0x10000 + ((slot * 32 + if eight_bit { pixel } else { pixel / 2 }) & 0x7fff);
|
|
let color_index = if eight_bit {
|
|
usize::from(vram[address])
|
|
} else {
|
|
usize::from(vram[address] >> ((pixel & 1) * 4) & 15)
|
|
};
|
|
if window && color_index != 0 {
|
|
output.window[offset] = true;
|
|
} else {
|
|
let sample = &mut samples[offset];
|
|
if sample.pixel.is_none() || priority < sample.priority {
|
|
if color_index != 0 {
|
|
let bank = if eight_bit { 0 } else { (c >> 12) * 16 };
|
|
sample.pixel = Some(ObjectPixel {
|
|
palette_index: (bank + color_index) as u16,
|
|
semi_transparent: a & 0xc00 == 0x400,
|
|
});
|
|
}
|
|
sample.priority = priority;
|
|
sample.mosaic = mosaic_enabled;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(output)
|
|
}
|
|
|
|
impl PreparedObjects {
|
|
/// Resolve buffered indices through the current palette. Horizontal mosaic
|
|
/// belongs to composition, not the previous line's OAM/VRAM preparation.
|
|
pub(super) fn compose(&self, mosaic: u16, palette: &[u8]) -> Objects {
|
|
let mut output = Objects {
|
|
pixels: vec![None; self.samples.len()],
|
|
window: self.window.clone(),
|
|
};
|
|
let mosaic_width = usize::from((mosaic >> 8) & 15) + 1;
|
|
// Horizontal OBJ mosaic latches the selected object sample, not each
|
|
// object's texture coordinate. Reset at each scanline and at transitions
|
|
// to/from non-mosaic samples or a numerically lower priority.
|
|
for (source, destination) in self
|
|
.samples
|
|
.chunks_exact(WIDTH)
|
|
.zip(output.pixels.chunks_exact_mut(WIDTH))
|
|
{
|
|
let mut latched = Sample::default();
|
|
for (x, (¤t, pixel)) in source.iter().zip(destination).enumerate() {
|
|
if x % mosaic_width == 0
|
|
|| !current.mosaic
|
|
|| !latched.mosaic
|
|
|| current.priority < latched.priority
|
|
{
|
|
latched = current;
|
|
}
|
|
*pixel = latched.pixel.map(|p| Pixel {
|
|
color: halfword(palette, 0x200 + usize::from(p.palette_index) * 2),
|
|
layer: 4,
|
|
priority: latched.priority,
|
|
semi_transparent: p.semi_transparent,
|
|
});
|
|
}
|
|
}
|
|
output
|
|
}
|
|
}
|