feat: port DisplayGfx::GetSpriteFrameForBlit (slot 33) to pure Rust
Browse filesThis is the LAST in-game render bridge. With this slot ported, the
entire render pipeline of a normal Worms match runs on Rust code with
zero bridges into WA's render functions; only the renderer backend
itself (CompatRenderer / DDraw / OpenGL) and the shutdown destructor
(slot 0) remain.
Three function ports + two new leaf modules:
- `render::sprite::lzss::sprite_lzss_decode` β port of
`Sprite_LZSS_Decode` (0x5B29E0). LZSS-style decompressor with
palette LUT remapping. Key detail verified from the disassembly:
short backrefs encode `real_distance - 1` (the asm's `DEC ECX`
between the JZ and the read), but long backrefs encode the real
distance directly (the JZ jumps OVER the DEC). 6 unit tests cover
literal runs, both backref forms, length boundaries, the
terminator, and self-overlap RLE-style cases.
- `render::sprite::frame_cache::frame_cache_allocate` β port of
`FrameCache__Allocate` (0x4FA950). LRU ring-buffer allocator with
eviction notification via `vtable[1]` (`on_subframe_evicted`).
Notable correction to the prep-session blueprint: the entry header
has `frame_idx@+8` and `owner@+0xc` (not the other way around) β
verified from the function epilog where `[ESP+0x1c]β[EAX]` and
`[ESP+0x18]β[EAX+4]`. 3 unit tests cover single alloc, LRU
chaining, and eviction-with-wrap (using a mock owner vtable).
- `display::vtable::sprite_get_frame_for_blit` β port of
`Sprite__GetFrameForBlit` (0x4FAD30). All three frame-resolution
paths (scaled / round-to-nearest / truncating) plus the cached and
flat surface dispatch. Inlines the bitgrid update writes to match
the original asm's order at 0x4FAEA2..0x4FAEB7.
- `display::vtable::sprite_bank_get_frame_for_blit` β port of
`SpriteBank__GetFrameForBlit` (0x4F9710). The bank-side counterpart
with the 7-bit signed nibble extraction for scaled mode
(`(low7 << 16) >> 5`) and the bbox-table / subframe-cache lookups
using the bank-specific layouts from the pre-work commit. Critical
detail caught from disasm: `EAX = low - high` at 0x4F97AD (not
high - low). Note: this path is suspected dead in normal gameplay
β adding a `panic!()` and playing several turns of a regular
match did not trigger it. The path is still ported as a defensive
measure for any obscure game state we haven't sampled.
- `display::vtable::bitgrid_set_external_buffer` β port of
`DisplayBitGrid::SetExternalBuffer` (0x4F6470). Used by the bank
path to update the embedded `frame_bitgrid` at `bank + 0x130`.
- `display::vtable::get_sprite_frame_for_blit` β slot 33 dispatcher.
Validates `sprite_id` (1..=0x3FE), then forwards to the sprite
helper for `sprite_ptrs[id]` or the bank helper for
`sprite_banks[id]`. Wired via `vtable_replace!` in
`install_display`.
Verification:
- 135/135 unit tests pass (incl. 6 lzss + 3 frame_cache).
- 15/15 headless replay tests pass.
- Headful verification: user played a normal Worms match β sprite
animations, weapons, particles, fire/smoke/water all render
correctly. SpriteBank path was not triggered (see panic! note
above).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@@ -317,12 +317,20 @@ pub struct DisplayGfxVtable {
|
|
| 317 |
/// | `out_left`/`top`/`right`/`bot` | frame bounding box within the cell |
|
| 318 |
/// | `out_anim_frac` | sub-frame interpolation value (Fixed16) or 0 |
|
| 319 |
///
|
| 320 |
-
///
|
| 321 |
-
///
|
| 322 |
-
/// `
|
| 323 |
-
///
|
|
|
|
|
|
|
| 324 |
///
|
| 325 |
-
///
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
///
|
| 327 |
/// ## Dispatcher (slot 33 itself, 0x5237C0, RET 0x24)
|
| 328 |
///
|
|
@@ -847,7 +855,9 @@ use super::gfx::DisplayGfx;
|
|
| 847 |
use super::line_draw;
|
| 848 |
use crate::bitgrid::{BitGrid, DisplayBitGrid};
|
| 849 |
use crate::render::palette::PaletteContext;
|
| 850 |
-
use crate::render::sprite::{
|
|
|
|
|
|
|
| 851 |
|
| 852 |
/// Port of DisplayGfx::GetDimensions (vtable slot 1, 0x56A460).
|
| 853 |
///
|
|
@@ -1487,6 +1497,424 @@ unsafe fn sprite_info_from_bank(
|
|
| 1487 |
crate::rebase::rb(SPRITE_STRING)
|
| 1488 |
}
|
| 1489 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1490 |
/// Port of DisplayGfx::DrawViaCallback (vtable slot 21, 0x56B7C0).
|
| 1491 |
///
|
| 1492 |
/// Acquires the render lock, applies camera offset to fixed-point coordinates,
|
|
|
|
| 317 |
/// | `out_left`/`top`/`right`/`bot` | frame bounding box within the cell |
|
| 318 |
/// | `out_anim_frac` | sub-frame interpolation value (Fixed16) or 0 |
|
| 319 |
///
|
| 320 |
+
/// **Ported.** Native Rust impl is [`get_sprite_frame_for_blit`]
|
| 321 |
+
/// in this file (Sprite path β [`sprite_get_frame_for_blit`],
|
| 322 |
+
/// SpriteBank path β [`sprite_bank_get_frame_for_blit`]). Wired via
|
| 323 |
+
/// `vtable_replace!` in `install_display`. With this slot ported,
|
| 324 |
+
/// the entire in-game render pipeline of a normal Worms match runs
|
| 325 |
+
/// on Rust code with zero bridges into WA's render functions.
|
| 326 |
///
|
| 327 |
+
/// **SpriteBank path is suspected dead in normal gameplay** β adding
|
| 328 |
+
/// a `panic!()` to `sprite_bank_get_frame_for_blit` and playing
|
| 329 |
+
/// several turns of a normal match did not trigger it. The path is
|
| 330 |
+
/// still ported (per the disassembly) as a defensive measure for any
|
| 331 |
+
/// obscure game state we haven't sampled.
|
| 332 |
+
///
|
| 333 |
+
/// # Porting blueprint (kept for archival reference)
|
| 334 |
///
|
| 335 |
/// ## Dispatcher (slot 33 itself, 0x5237C0, RET 0x24)
|
| 336 |
///
|
|
|
|
| 855 |
use super::line_draw;
|
| 856 |
use crate::bitgrid::{BitGrid, DisplayBitGrid};
|
| 857 |
use crate::render::palette::PaletteContext;
|
| 858 |
+
use crate::render::sprite::{
|
| 859 |
+
frame_cache::frame_cache_allocate, lzss::sprite_lzss_decode, Sprite, SpriteBank, SpriteVtable,
|
| 860 |
+
};
|
| 861 |
|
| 862 |
/// Port of DisplayGfx::GetDimensions (vtable slot 1, 0x56A460).
|
| 863 |
///
|
|
|
|
| 1497 |
crate::rebase::rb(SPRITE_STRING)
|
| 1498 |
}
|
| 1499 |
|
| 1500 |
+
/// Pure-Rust port of `Sprite__GetFrameForBlit` (0x4FAD30).
|
| 1501 |
+
///
|
| 1502 |
+
/// Resolves an animation value to a frame within `sprite`, lazily
|
| 1503 |
+
/// decompresses its surface via [`frame_cache_allocate`] +
|
| 1504 |
+
/// [`sprite_lzss_decode`] if the sprite uses the cached path
|
| 1505 |
+
/// (`header_flags & 0x4000`), updates `sprite.bitgrid` to point at the
|
| 1506 |
+
/// resulting pixel data, and returns a pointer to that embedded bitgrid.
|
| 1507 |
+
///
|
| 1508 |
+
/// Mirrors the original WA usercall (`ESI=this, EAX=out_anim_frac,
|
| 1509 |
+
/// EDX=anim_value, stack=(out_w, out_h, out_left, out_top, out_right,
|
| 1510 |
+
/// out_bottom)`) as a regular Rust function β the slot 33 dispatcher
|
| 1511 |
+
/// shuffles its caller's args into this signature.
|
| 1512 |
+
///
|
| 1513 |
+
/// # Safety
|
| 1514 |
+
///
|
| 1515 |
+
/// All output pointers must be valid for writes. `sprite` must point at a
|
| 1516 |
+
/// fully-constructed [`Sprite`] (i.e. `frame_meta_ptr` populated and, for
|
| 1517 |
+
/// the cached path, `subframe_cache_table` and `bitmap_data_ptr` set).
|
| 1518 |
+
unsafe fn sprite_get_frame_for_blit(
|
| 1519 |
+
sprite: *mut Sprite,
|
| 1520 |
+
mut anim_value: u32,
|
| 1521 |
+
out_anim_frac: *mut u32,
|
| 1522 |
+
out_w: *mut i32,
|
| 1523 |
+
out_h: *mut i32,
|
| 1524 |
+
out_left: *mut i32,
|
| 1525 |
+
out_top: *mut i32,
|
| 1526 |
+
out_right: *mut i32,
|
| 1527 |
+
out_bottom: *mut i32,
|
| 1528 |
+
) -> *mut DisplayBitGrid {
|
| 1529 |
+
let flags = (*sprite).flags;
|
| 1530 |
+
|
| 1531 |
+
// ββ Step 1: clamp `anim_value` βββββββββββββββββββββββββββββββββββ
|
| 1532 |
+
if flags & 1 != 0 {
|
| 1533 |
+
// Use-as-is mode: just truncate to 16 bits.
|
| 1534 |
+
anim_value &= 0xFFFF;
|
| 1535 |
+
} else {
|
| 1536 |
+
// Signed clamp to [0, 0xFFFF].
|
| 1537 |
+
let signed = anim_value as i32;
|
| 1538 |
+
if signed < 0 {
|
| 1539 |
+
anim_value = 0;
|
| 1540 |
+
} else if signed >= 0x10000 {
|
| 1541 |
+
anim_value = 0xFFFF;
|
| 1542 |
+
}
|
| 1543 |
+
}
|
| 1544 |
+
|
| 1545 |
+
// ββ Step 2: ping-pong ββββββββββββββββββββββββββββββββββββββββββββ
|
| 1546 |
+
if flags & 2 != 0 {
|
| 1547 |
+
anim_value &= 0xFFFF;
|
| 1548 |
+
anim_value = anim_value.wrapping_mul(2);
|
| 1549 |
+
if anim_value >= 0x10000 {
|
| 1550 |
+
anim_value = 0x1FFFE - anim_value;
|
| 1551 |
+
}
|
| 1552 |
+
}
|
| 1553 |
+
|
| 1554 |
+
// ββ Step 3: frame index resolution βββββββββββββββββββββββββββββββ
|
| 1555 |
+
let frame_idx: u32 = if (*sprite).is_scaled != 0 {
|
| 1556 |
+
// Scaled mode: linearly interpolate between scale_x and scale_y
|
| 1557 |
+
// by the (Fixed16) anim_value, write the interpolated result to
|
| 1558 |
+
// *out_anim_frac. Always use frame 0 in this mode.
|
| 1559 |
+
let scale_x = (*sprite).scale_x as i32;
|
| 1560 |
+
let scale_y = (*sprite).scale_y as i32;
|
| 1561 |
+
let diff = scale_y.wrapping_sub(scale_x);
|
| 1562 |
+
// Original asm: `IMUL EDX; SHRD EAX, EDX, 0x10` β 32Γ32 β 64
|
| 1563 |
+
// signed multiply with `>> 16` (Fixed16). The multiplicand is the
|
| 1564 |
+
// raw `anim_value` interpreted as a signed 32-bit integer (after
|
| 1565 |
+
// the clamp/ping-pong above it's always in [0, 0xFFFF], so the
|
| 1566 |
+
// sign bit is clear).
|
| 1567 |
+
let prod = (diff as i64).wrapping_mul((anim_value as i32) as i64);
|
| 1568 |
+
let interp = ((prod >> 16) as i32).wrapping_add(scale_x);
|
| 1569 |
+
*out_anim_frac = interp as u32;
|
| 1570 |
+
0
|
| 1571 |
+
} else if (*sprite).frame_round_mode & 1 != 0 {
|
| 1572 |
+
// Round-to-nearest mode: `((max_frames * anim_value) + 0x8000) >> 16`,
|
| 1573 |
+
// with the special case that if rounding produces `frame_idx ==
|
| 1574 |
+
// max_frames` we wrap back to 0 (avoids reading past the table).
|
| 1575 |
+
let max_frames = (*sprite).max_frames as i32;
|
| 1576 |
+
let prod = max_frames.wrapping_mul(anim_value as i32);
|
| 1577 |
+
let f = (prod.wrapping_add(0x8000) >> 16) as u32;
|
| 1578 |
+
*out_anim_frac = 0;
|
| 1579 |
+
if f == max_frames as u32 {
|
| 1580 |
+
0
|
| 1581 |
+
} else {
|
| 1582 |
+
f
|
| 1583 |
+
}
|
| 1584 |
+
} else {
|
| 1585 |
+
// Truncating mode.
|
| 1586 |
+
let max_frames = (*sprite).max_frames as i32;
|
| 1587 |
+
let prod = max_frames.wrapping_mul(anim_value as i32);
|
| 1588 |
+
*out_anim_frac = 0;
|
| 1589 |
+
(prod >> 16) as u32
|
| 1590 |
+
};
|
| 1591 |
+
|
| 1592 |
+
// ββ Step 4: read frame metadata + sprite cell dimensions βββββββββ
|
| 1593 |
+
let frame_meta = (*sprite).frame_meta_ptr.add(frame_idx as usize);
|
| 1594 |
+
let start_x = (*frame_meta).start_x as i16 as i32;
|
| 1595 |
+
let start_y = (*frame_meta).start_y as i16 as i32;
|
| 1596 |
+
let end_x = (*frame_meta).end_x as i16 as i32;
|
| 1597 |
+
let end_y = (*frame_meta).end_y as i16 as i32;
|
| 1598 |
+
*out_left = start_x;
|
| 1599 |
+
*out_top = start_y;
|
| 1600 |
+
*out_right = end_x;
|
| 1601 |
+
*out_bottom = end_y;
|
| 1602 |
+
*out_w = (*sprite).width as i32;
|
| 1603 |
+
*out_h = (*sprite).height as i32;
|
| 1604 |
+
let frame_w = end_x - start_x;
|
| 1605 |
+
let frame_h = end_y - start_y;
|
| 1606 |
+
|
| 1607 |
+
// ββ Step 5: surface address (flat or cached/decompressed) ββββββββ
|
| 1608 |
+
let surface_addr: *mut u8 = if (*sprite).header_flags & 0x4000 == 0 {
|
| 1609 |
+
// Flat path: bytes are already-decoded raw pixels in the load
|
| 1610 |
+
// buffer.
|
| 1611 |
+
let bitmap_offset = (*frame_meta).bitmap_offset;
|
| 1612 |
+
(*sprite).bitmap_data_ptr.add(bitmap_offset as usize)
|
| 1613 |
+
} else {
|
| 1614 |
+
// Cached path: bitmap_offset is split into a signed-byte
|
| 1615 |
+
// subframe index (high byte) and a pixel offset (low 24 bits).
|
| 1616 |
+
let bitmap_offset = (*frame_meta).bitmap_offset;
|
| 1617 |
+
let subframe_idx_signed = ((bitmap_offset >> 24) as i8) as i32;
|
| 1618 |
+
|
| 1619 |
+
// The original asm uses MOVSX byte ptr [EAX+EDI+3] then a *12
|
| 1620 |
+
// lea β i.e. negative subframe indices index *backward* from the
|
| 1621 |
+
// table base. Mirror via `offset(isize)`.
|
| 1622 |
+
let entry = (*sprite)
|
| 1623 |
+
.subframe_cache_table
|
| 1624 |
+
.offset(subframe_idx_signed as isize);
|
| 1625 |
+
|
| 1626 |
+
if (*entry).decoded_ptr.is_null() {
|
| 1627 |
+
// Allocate from the per-SpriteCache FrameCache and decompress.
|
| 1628 |
+
let context_ptr = (*sprite).context_ptr;
|
| 1629 |
+
let decoded_size = (*entry).decoded_size;
|
| 1630 |
+
let decoded = frame_cache_allocate(
|
| 1631 |
+
decoded_size,
|
| 1632 |
+
context_ptr,
|
| 1633 |
+
sprite as *mut core::ffi::c_void,
|
| 1634 |
+
subframe_idx_signed as u32,
|
| 1635 |
+
);
|
| 1636 |
+
(*entry).decoded_ptr = decoded;
|
| 1637 |
+
let src = (*sprite)
|
| 1638 |
+
.bitmap_data_ptr
|
| 1639 |
+
.add((*entry).compressed_offset as usize);
|
| 1640 |
+
sprite_lzss_decode(decoded, src, (*sprite).palette_data_ptr);
|
| 1641 |
+
}
|
| 1642 |
+
|
| 1643 |
+
let pixel_offset = (bitmap_offset & 0xFF_FFFF) as usize;
|
| 1644 |
+
(*entry).decoded_ptr.add(pixel_offset)
|
| 1645 |
+
};
|
| 1646 |
+
|
| 1647 |
+
// ββ Step 6: update embedded bitgrid (only if external_buffer != 0)
|
| 1648 |
+
let bitgrid = &raw mut (*sprite).bitgrid;
|
| 1649 |
+
if (*bitgrid).external_buffer != 0 {
|
| 1650 |
+
// Order copied from the asm at 0x4FAEA2..0x4FAEB7. Equivalent
|
| 1651 |
+
// to `bitgrid_set_external_buffer(bitgrid, surface, frame_w,
|
| 1652 |
+
// frame_w, frame_h)` β see SpriteBank path which calls the
|
| 1653 |
+
// shared `DisplayBitGrid::SetExternalBuffer` helper.
|
| 1654 |
+
(*bitgrid).clip_bottom = frame_h as u32;
|
| 1655 |
+
(*bitgrid).clip_right = frame_w as u32;
|
| 1656 |
+
(*bitgrid).clip_top = 0;
|
| 1657 |
+
(*bitgrid).clip_left = 0;
|
| 1658 |
+
(*bitgrid).row_stride = frame_w as u32;
|
| 1659 |
+
(*bitgrid).data = surface_addr;
|
| 1660 |
+
(*bitgrid).height = frame_h as u32;
|
| 1661 |
+
(*bitgrid).width = frame_w as u32;
|
| 1662 |
+
}
|
| 1663 |
+
|
| 1664 |
+
bitgrid
|
| 1665 |
+
}
|
| 1666 |
+
|
| 1667 |
+
/// Pure-Rust port of `DisplayBitGrid::SetExternalBuffer` (0x4F6470).
|
| 1668 |
+
///
|
| 1669 |
+
/// Sets a `DisplayBitGrid`'s data pointer, dimensions, row stride, and
|
| 1670 |
+
/// clip rect (full-rect: `(0, 0, width, height)`). Returns `false` if the
|
| 1671 |
+
/// bitgrid's `external_buffer` flag is currently zero (i.e. it owns its
|
| 1672 |
+
/// own buffer and the caller has no business swapping it out).
|
| 1673 |
+
///
|
| 1674 |
+
/// Used by the SpriteBank path of slot 33 to point the bank's embedded
|
| 1675 |
+
/// `frame_bitgrid` (at `bank + 0x130`) at the freshly-decompressed
|
| 1676 |
+
/// subframe pixels. Inlined manually in the Sprite path because that one
|
| 1677 |
+
/// uses `frame_w` for both `width` and `row_stride`.
|
| 1678 |
+
#[inline]
|
| 1679 |
+
unsafe fn bitgrid_set_external_buffer(
|
| 1680 |
+
bitgrid: *mut DisplayBitGrid,
|
| 1681 |
+
data: *mut u8,
|
| 1682 |
+
row_stride: u32,
|
| 1683 |
+
width: u32,
|
| 1684 |
+
height: u32,
|
| 1685 |
+
) -> bool {
|
| 1686 |
+
if (*bitgrid).external_buffer == 0 {
|
| 1687 |
+
return false;
|
| 1688 |
+
}
|
| 1689 |
+
(*bitgrid).data = data;
|
| 1690 |
+
(*bitgrid).row_stride = row_stride;
|
| 1691 |
+
(*bitgrid).width = width;
|
| 1692 |
+
(*bitgrid).height = height;
|
| 1693 |
+
(*bitgrid).clip_left = 0;
|
| 1694 |
+
(*bitgrid).clip_top = 0;
|
| 1695 |
+
(*bitgrid).clip_right = width;
|
| 1696 |
+
(*bitgrid).clip_bottom = height;
|
| 1697 |
+
true
|
| 1698 |
+
}
|
| 1699 |
+
|
| 1700 |
+
/// Pure-Rust port of `SpriteBank__GetFrameForBlit` (0x4F9710).
|
| 1701 |
+
///
|
| 1702 |
+
/// SpriteBank-side counterpart of [`sprite_get_frame_for_blit`]: walks
|
| 1703 |
+
/// the bank's `index_table` to map `sprite_id` to a frame in
|
| 1704 |
+
/// `frame_table`, resolves the animation value to a sub-frame index in
|
| 1705 |
+
/// `bbox_table`, lazily decompresses the corresponding subframe via
|
| 1706 |
+
/// [`frame_cache_allocate`] + [`sprite_lzss_decode`], updates the
|
| 1707 |
+
/// embedded `frame_bitgrid` (at `bank + 0x130`), and returns a pointer
|
| 1708 |
+
/// to it. Returns null on any validation failure (null `frame_table` or
|
| 1709 |
+
/// out-of-range `entry_idx`).
|
| 1710 |
+
///
|
| 1711 |
+
/// Note the structural differences from the Sprite path:
|
| 1712 |
+
/// - Animation metadata lives in [`SpriteBankFrame`]
|
| 1713 |
+
/// (`flags/width/height/base_frame_idx/scale_or_count`) instead of
|
| 1714 |
+
/// `Sprite::flags`/`max_frames`/`scale_*`.
|
| 1715 |
+
/// - Out-`w`/`h` come from the per-frame `width`/`height`, NOT from a
|
| 1716 |
+
/// sprite-wide cell dimension.
|
| 1717 |
+
/// - The bbox table is a separate array of [`SpriteBankBboxEntry`] at
|
| 1718 |
+
/// `bank + 0x18`, with `subframe_idx` at `+0` (low u16 of what would
|
| 1719 |
+
/// be `SpriteFrame::bitmap_offset`) and `decoded_offset` at `+2`.
|
| 1720 |
+
/// - The subframe cache uses [`SpriteBankSubframeCache`] with
|
| 1721 |
+
/// `decoded_ptr` at `+8` (vs `+4` in `SpriteSubframeCache`).
|
| 1722 |
+
/// - The LUT for LZSS is the inline 256-byte `palette_lut` at
|
| 1723 |
+
/// `bank + 0x30`, not a separately-allocated palette pointer.
|
| 1724 |
+
/// - The bitgrid update goes through `bitgrid_set_external_buffer`
|
| 1725 |
+
/// instead of inline writes.
|
| 1726 |
+
///
|
| 1727 |
+
/// # Safety
|
| 1728 |
+
///
|
| 1729 |
+
/// `bank` must point at a valid, fully-parsed [`SpriteBank`]. Output
|
| 1730 |
+
/// pointers must be writable.
|
| 1731 |
+
#[allow(clippy::too_many_arguments)]
|
| 1732 |
+
unsafe fn sprite_bank_get_frame_for_blit(
|
| 1733 |
+
bank: *mut SpriteBank,
|
| 1734 |
+
sprite_id: u32,
|
| 1735 |
+
mut anim_value: u32,
|
| 1736 |
+
out_anim_frac: *mut u32,
|
| 1737 |
+
out_w: *mut i32,
|
| 1738 |
+
out_h: *mut i32,
|
| 1739 |
+
out_left: *mut i32,
|
| 1740 |
+
out_top: *mut i32,
|
| 1741 |
+
out_right: *mut i32,
|
| 1742 |
+
out_bottom: *mut i32,
|
| 1743 |
+
) -> *mut DisplayBitGrid {
|
| 1744 |
+
// ββ Index lookup + bounds check ββββββββββββββββββββββββββββββββββ
|
| 1745 |
+
let entry_idx = *(*bank)
|
| 1746 |
+
.index_table
|
| 1747 |
+
.offset((sprite_id as i32 - (*bank).base_id) as isize);
|
| 1748 |
+
if (*bank).frame_table.is_null() || entry_idx < 0 || entry_idx >= (*bank).frame_count {
|
| 1749 |
+
return core::ptr::null_mut();
|
| 1750 |
+
}
|
| 1751 |
+
let frame = (*bank).frame_table.add(entry_idx as usize);
|
| 1752 |
+
let frame_flags = (*frame).flags;
|
| 1753 |
+
|
| 1754 |
+
// ββ Clamp anim_value βββββββββββββββββββββββββββββββββββββββββββββ
|
| 1755 |
+
if frame_flags & 1 != 0 {
|
| 1756 |
+
anim_value &= 0xFFFF;
|
| 1757 |
+
} else {
|
| 1758 |
+
let signed = anim_value as i32;
|
| 1759 |
+
if signed < 0 {
|
| 1760 |
+
anim_value = 0;
|
| 1761 |
+
} else if signed >= 0x10000 {
|
| 1762 |
+
anim_value = 0xFFFF;
|
| 1763 |
+
}
|
| 1764 |
+
}
|
| 1765 |
+
|
| 1766 |
+
// ββ Ping-pong ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1767 |
+
if frame_flags & 2 != 0 {
|
| 1768 |
+
anim_value &= 0xFFFF;
|
| 1769 |
+
anim_value = anim_value.wrapping_mul(2);
|
| 1770 |
+
if anim_value >= 0x10000 {
|
| 1771 |
+
anim_value = 0x1FFFE - anim_value;
|
| 1772 |
+
}
|
| 1773 |
+
}
|
| 1774 |
+
|
| 1775 |
+
// ββ Frame index resolution βββββββββββββββββββββββββββββββββββββββ
|
| 1776 |
+
let scale_or_count = (*frame).scale_or_count;
|
| 1777 |
+
let base_frame_idx = (*frame).base_frame_idx;
|
| 1778 |
+
|
| 1779 |
+
let resolved_idx: u32 = if scale_or_count & 0x8000 != 0 {
|
| 1780 |
+
// Scaled mode: low/high 7-bit nibbles of `scale_or_count` form a
|
| 1781 |
+
// signed Fixed16 pair. The asm extracts both via:
|
| 1782 |
+
// `value = (low_or_high_7 << 16) >> 5` (signed)
|
| 1783 |
+
// i.e. shift the 7-bit value into the high half, sign-extend
|
| 1784 |
+
// from bit 22, then shift right by 5 β effective `<< 11` with
|
| 1785 |
+
// sign extension from bit 6 of the original 7-bit value.
|
| 1786 |
+
let low7 = scale_or_count & 0x7F;
|
| 1787 |
+
let high7 = (scale_or_count >> 8) & 0x7F;
|
| 1788 |
+
let low_val = (((low7 as u32) << 16) as i32) >> 5;
|
| 1789 |
+
let high_val = (((high7 as u32) << 16) as i32) >> 5;
|
| 1790 |
+
// Note: the asm computes `EAX = low - high` (NOT high - low) β
|
| 1791 |
+
// verified at 0x4F97AD `SUB EAX, EDI` where EAX = low_val and
|
| 1792 |
+
// EDI = high_val.
|
| 1793 |
+
let diff = low_val.wrapping_sub(high_val);
|
| 1794 |
+
let prod = (diff as i64).wrapping_mul((anim_value as i32) as i64);
|
| 1795 |
+
let interp = ((prod >> 16) as i32).wrapping_add(high_val);
|
| 1796 |
+
*out_anim_frac = interp as u32;
|
| 1797 |
+
// Scaled mode uses `base_frame_idx` directly as the bbox index.
|
| 1798 |
+
base_frame_idx as u32
|
| 1799 |
+
} else {
|
| 1800 |
+
// Non-scaled mode: subframe_idx = ((scale_or_count * anim_value) >> 16)
|
| 1801 |
+
// + base_frame_idx. *out_anim_frac = 0.
|
| 1802 |
+
let prod = (scale_or_count as i32).wrapping_mul(anim_value as i32);
|
| 1803 |
+
*out_anim_frac = 0;
|
| 1804 |
+
((prod >> 16) as u32).wrapping_add(base_frame_idx as u32)
|
| 1805 |
+
};
|
| 1806 |
+
|
| 1807 |
+
// ββ Read bbox metadata βββββββββββββββββββββββββββββββββββββββββββ
|
| 1808 |
+
let bbox = (*bank).bbox_table.add(resolved_idx as usize);
|
| 1809 |
+
*out_left = (*bbox).start_x as i32;
|
| 1810 |
+
*out_top = (*bbox).start_y as i32;
|
| 1811 |
+
*out_right = (*bbox).end_x as i32;
|
| 1812 |
+
*out_bottom = (*bbox).end_y as i32;
|
| 1813 |
+
// out_w/out_h come from the per-frame width/height, NOT the bbox.
|
| 1814 |
+
*out_w = (*frame).width as i32;
|
| 1815 |
+
*out_h = (*frame).height as i32;
|
| 1816 |
+
let frame_w = (*out_right - *out_left) as u32;
|
| 1817 |
+
let frame_h = (*out_bottom - *out_top) as u32;
|
| 1818 |
+
|
| 1819 |
+
// ββ Subframe cache lookup + lazy decompress ββββββββββββββββββββββ
|
| 1820 |
+
let subframe_idx = (*bbox).subframe_idx as u32;
|
| 1821 |
+
let entry = (*bank).subframe_cache_table.add(subframe_idx as usize);
|
| 1822 |
+
|
| 1823 |
+
if (*entry).decoded_ptr.is_null() {
|
| 1824 |
+
let context_ptr = (*bank).context_ptr;
|
| 1825 |
+
let decoded_size = (*entry).decoded_size;
|
| 1826 |
+
let decoded = frame_cache_allocate(
|
| 1827 |
+
decoded_size,
|
| 1828 |
+
context_ptr,
|
| 1829 |
+
bank as *mut core::ffi::c_void,
|
| 1830 |
+
subframe_idx,
|
| 1831 |
+
);
|
| 1832 |
+
(*entry).decoded_ptr = decoded;
|
| 1833 |
+
let src = (*bank)
|
| 1834 |
+
.bitmap_data_ptr
|
| 1835 |
+
.add((*entry).compressed_offset as usize) as *mut u8;
|
| 1836 |
+
// Bank LUT is the inline 256-byte palette_lut at bank+0x30.
|
| 1837 |
+
let lut = (&raw const (*bank).palette_lut) as *const u8;
|
| 1838 |
+
sprite_lzss_decode(decoded, src, lut);
|
| 1839 |
+
}
|
| 1840 |
+
|
| 1841 |
+
// ββ Update embedded bitgrid via SetExternalBuffer ββββββββββββββββ
|
| 1842 |
+
//
|
| 1843 |
+
// Surface address = bbox.decoded_offset + entry.decoded_ptr.
|
| 1844 |
+
let surface = (*entry).decoded_ptr.add((*bbox).decoded_offset as usize);
|
| 1845 |
+
let bitgrid = &raw mut (*bank).frame_bitgrid;
|
| 1846 |
+
bitgrid_set_external_buffer(bitgrid, surface, frame_w, frame_w, frame_h);
|
| 1847 |
+
bitgrid
|
| 1848 |
+
}
|
| 1849 |
+
|
| 1850 |
+
/// Port of `DisplayGfx::GetSpriteFrameForBlit` (vtable slot 33, 0x5237C0).
|
| 1851 |
+
///
|
| 1852 |
+
/// Thin dispatcher: validates `sprite_id`, then forwards to either
|
| 1853 |
+
/// [`sprite_get_frame_for_blit`] (for `sprite_ptrs`-backed IDs) or
|
| 1854 |
+
/// [`sprite_bank_get_frame_for_blit`] (for `sprite_banks`-backed IDs).
|
| 1855 |
+
/// Returns null if neither table has an entry for the ID.
|
| 1856 |
+
///
|
| 1857 |
+
/// See the slot 33 docstring on
|
| 1858 |
+
/// [`DisplayGfxVtable::get_sprite_frame_for_blit`] for the original
|
| 1859 |
+
/// usercall convention and the full body breakdown.
|
| 1860 |
+
///
|
| 1861 |
+
/// # Safety
|
| 1862 |
+
///
|
| 1863 |
+
/// `this` must be a valid `*mut DisplayGfx`. All output pointers must
|
| 1864 |
+
/// be writable.
|
| 1865 |
+
#[allow(clippy::too_many_arguments)]
|
| 1866 |
+
pub unsafe extern "thiscall" fn get_sprite_frame_for_blit(
|
| 1867 |
+
this: *mut DisplayGfx,
|
| 1868 |
+
sprite_id: u32,
|
| 1869 |
+
anim_value: u32,
|
| 1870 |
+
out_w: *mut i32,
|
| 1871 |
+
out_h: *mut i32,
|
| 1872 |
+
out_left: *mut i32,
|
| 1873 |
+
out_top: *mut i32,
|
| 1874 |
+
out_right: *mut i32,
|
| 1875 |
+
out_bottom: *mut i32,
|
| 1876 |
+
out_anim_frac: *mut u32,
|
| 1877 |
+
) -> *mut DisplayBitGrid {
|
| 1878 |
+
// Validity check: sprite_id - 1 must be < 0x3FE (so 1..=0x3FE).
|
| 1879 |
+
if sprite_id.wrapping_sub(1) >= 0x3FE {
|
| 1880 |
+
return core::ptr::null_mut();
|
| 1881 |
+
}
|
| 1882 |
+
|
| 1883 |
+
let base = &mut (*this).base;
|
| 1884 |
+
let sprite = base.sprite_ptrs[sprite_id as usize];
|
| 1885 |
+
if !sprite.is_null() {
|
| 1886 |
+
return sprite_get_frame_for_blit(
|
| 1887 |
+
sprite,
|
| 1888 |
+
anim_value,
|
| 1889 |
+
out_anim_frac,
|
| 1890 |
+
out_w,
|
| 1891 |
+
out_h,
|
| 1892 |
+
out_left,
|
| 1893 |
+
out_top,
|
| 1894 |
+
out_right,
|
| 1895 |
+
out_bottom,
|
| 1896 |
+
);
|
| 1897 |
+
}
|
| 1898 |
+
|
| 1899 |
+
let bank = base.sprite_banks[sprite_id as usize];
|
| 1900 |
+
if !bank.is_null() {
|
| 1901 |
+
return sprite_bank_get_frame_for_blit(
|
| 1902 |
+
bank,
|
| 1903 |
+
sprite_id,
|
| 1904 |
+
anim_value,
|
| 1905 |
+
out_anim_frac,
|
| 1906 |
+
out_w,
|
| 1907 |
+
out_h,
|
| 1908 |
+
out_left,
|
| 1909 |
+
out_top,
|
| 1910 |
+
out_right,
|
| 1911 |
+
out_bottom,
|
| 1912 |
+
);
|
| 1913 |
+
}
|
| 1914 |
+
|
| 1915 |
+
core::ptr::null_mut()
|
| 1916 |
+
}
|
| 1917 |
+
|
| 1918 |
/// Port of DisplayGfx::DrawViaCallback (vtable slot 21, 0x56B7C0).
|
| 1919 |
///
|
| 1920 |
/// Acquires the render lock, applies camera offset to fixed-point coordinates,
|
|
@@ -0,0 +1,362 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! LRU ring-buffer allocator for sprite subframe payloads.
|
| 2 |
+
//!
|
| 3 |
+
//! Pure-Rust port of `FrameCache__Allocate` (0x4FA950). The shared
|
| 4 |
+
//! [`FrameCache`] holds decompressed pixel data for the most recently
|
| 5 |
+
//! requested sprite/sprite-bank subframes. When the buffer fills, the
|
| 6 |
+
//! oldest entry is evicted and its owner is notified via
|
| 7 |
+
//! `vtable[1]` (`on_subframe_evicted`) so it can clear its
|
| 8 |
+
//! `decoded_ptr` and re-request decompression on the next render.
|
| 9 |
+
//!
|
| 10 |
+
//! Each allocated entry has a 16-byte header ([`FrameCacheEntry`]) followed
|
| 11 |
+
//! inline by the payload bytes. The returned pointer is the payload start
|
| 12 |
+
//! (`entry + 0x10`).
|
| 13 |
+
//!
|
| 14 |
+
//! See the slot 33 docstring in [`crate::render::display::vtable`] for the
|
| 15 |
+
//! full reverse-engineering notes and the `Sprite__GetFrameForBlit` /
|
| 16 |
+
//! `SpriteBank__GetFrameForBlit` callers.
|
| 17 |
+
|
| 18 |
+
use core::ffi::c_void;
|
| 19 |
+
use core::ptr::null_mut;
|
| 20 |
+
|
| 21 |
+
#[cfg(test)]
|
| 22 |
+
use crate::render::display::base::FrameCache;
|
| 23 |
+
use crate::render::display::base::FrameCacheEntry;
|
| 24 |
+
use crate::render::SpriteCache;
|
| 25 |
+
|
| 26 |
+
/// Eviction callback signature β `vtable[1]` of both `Sprite` and
|
| 27 |
+
/// `SpriteBank`. Called when the oldest entry is being dropped from the
|
| 28 |
+
/// LRU ring buffer; the callback clears the owner's per-subframe
|
| 29 |
+
/// `decoded_ptr` so the next render re-decompresses.
|
| 30 |
+
///
|
| 31 |
+
/// `this` is `victim.owner`. `subframe_idx` is `victim.frame_idx` (the
|
| 32 |
+
/// caller's third stack arg from when the entry was allocated). `payload`
|
| 33 |
+
/// is `victim + 0x10` β the payload start, which the original asm pushes
|
| 34 |
+
/// but neither callback uses.
|
| 35 |
+
type EvictCallback =
|
| 36 |
+
unsafe extern "thiscall" fn(this: *mut c_void, subframe_idx: u32, payload: *mut u8);
|
| 37 |
+
|
| 38 |
+
/// Pure-Rust port of `FrameCache__Allocate` (0x4FA950).
|
| 39 |
+
///
|
| 40 |
+
/// Allocates `entry_size` bytes of payload from the [`FrameCache`] reached
|
| 41 |
+
/// via `(*context_ptr).frame_cache`. On success, returns a pointer to the
|
| 42 |
+
/// payload start. On contention, evicts the oldest live entry (notifying
|
| 43 |
+
/// its owner via `vtable[1]`) and retries.
|
| 44 |
+
///
|
| 45 |
+
/// # Layout details
|
| 46 |
+
///
|
| 47 |
+
/// - The header allocation occupies `pad = ((entry_size + 8 + 0xb) & ~3)`
|
| 48 |
+
/// bytes total (= `entry_size + 8` rounded up to a 4-byte stride after
|
| 49 |
+
/// adding 11 bytes of slack).
|
| 50 |
+
/// - The `payload_size` field stored in the entry header is
|
| 51 |
+
/// `entry_size + 8` (NOT padded β the original asm computes the padded
|
| 52 |
+
/// stride only for advancing `write_head`, never stores it).
|
| 53 |
+
/// - On the long-backref `entry_count` cleanup branch where the new head
|
| 54 |
+
/// becomes null, the original asm clears `wrap_marker` and `write_head`
|
| 55 |
+
/// to 0 β the buffer is now empty and the next alloc starts from the
|
| 56 |
+
/// beginning.
|
| 57 |
+
///
|
| 58 |
+
/// # Calling convention
|
| 59 |
+
///
|
| 60 |
+
/// The original is a usercall: `EAX = entry_size`, stack args
|
| 61 |
+
/// `(context_ptr, owner, frame_idx)`. The Rust port takes these as
|
| 62 |
+
/// regular params; callers should pass the same values.
|
| 63 |
+
///
|
| 64 |
+
/// # Safety
|
| 65 |
+
///
|
| 66 |
+
/// - `context_ptr` must point at a valid `SpriteCache` whose `frame_cache`
|
| 67 |
+
/// field references a `FrameCache` initialized via `FUN_004fa860`
|
| 68 |
+
/// (with a non-null `buffer` of at least `entry_size + 0x13` bytes).
|
| 69 |
+
/// - `owner` must point at an object whose first field is a vtable
|
| 70 |
+
/// pointer with `vtable[1]` matching [`EvictCallback`]. In practice
|
| 71 |
+
/// this is `*mut Sprite` or `*mut SpriteBank`, both of whose
|
| 72 |
+
/// `on_subframe_evicted` callbacks share the signature.
|
| 73 |
+
/// - `entry_size` must be small enough to fit in the buffer
|
| 74 |
+
/// (`entry_size + 0x13 <= capacity`); otherwise this loops forever.
|
| 75 |
+
pub unsafe fn frame_cache_allocate(
|
| 76 |
+
entry_size: u32,
|
| 77 |
+
context_ptr: *mut SpriteCache,
|
| 78 |
+
owner: *mut c_void,
|
| 79 |
+
frame_idx: u32,
|
| 80 |
+
) -> *mut u8 {
|
| 81 |
+
let payload_size = entry_size + 8;
|
| 82 |
+
let pad = (entry_size + 0x13) & !3;
|
| 83 |
+
|
| 84 |
+
loop {
|
| 85 |
+
let fc = (*context_ptr).frame_cache;
|
| 86 |
+
let write = (*fc).write_head;
|
| 87 |
+
let wrap = (*fc).wrap_marker;
|
| 88 |
+
let end = write + pad;
|
| 89 |
+
|
| 90 |
+
// Decide where (if anywhere) the new entry can go in this pass.
|
| 91 |
+
let placed_offset: Option<u32> = if write < wrap {
|
| 92 |
+
// Case A: write_head sits behind wrap_marker (a previously
|
| 93 |
+
// wrapped buffer). Allocation succeeds iff it doesn't reach
|
| 94 |
+
// wrap_marker.
|
| 95 |
+
if end <= wrap {
|
| 96 |
+
Some(write)
|
| 97 |
+
} else {
|
| 98 |
+
None
|
| 99 |
+
}
|
| 100 |
+
} else {
|
| 101 |
+
// Case B: write_head ahead of wrap_marker. Allocation
|
| 102 |
+
// succeeds iff it fits before the end of the buffer; if it
|
| 103 |
+
// doesn't, try wrapping to offset 0 β that succeeds iff
|
| 104 |
+
// `pad <= wrap_marker` (head_entry isn't sitting in the
|
| 105 |
+
// [0, pad) range).
|
| 106 |
+
if end <= (*fc).capacity {
|
| 107 |
+
Some(write)
|
| 108 |
+
} else if pad <= wrap {
|
| 109 |
+
Some(0)
|
| 110 |
+
} else {
|
| 111 |
+
None
|
| 112 |
+
}
|
| 113 |
+
};
|
| 114 |
+
|
| 115 |
+
if let Some(offset) = placed_offset {
|
| 116 |
+
let entry_addr = (*fc).buffer.add(offset as usize);
|
| 117 |
+
let entry = entry_addr as *mut FrameCacheEntry;
|
| 118 |
+
|
| 119 |
+
(*entry).payload_size = payload_size;
|
| 120 |
+
(*entry).next = null_mut();
|
| 121 |
+
|
| 122 |
+
if !(*fc).tail_entry.is_null() {
|
| 123 |
+
(*(*fc).tail_entry).next = entry;
|
| 124 |
+
}
|
| 125 |
+
(*fc).tail_entry = entry;
|
| 126 |
+
if (*fc).head_entry.is_null() {
|
| 127 |
+
(*fc).head_entry = entry;
|
| 128 |
+
}
|
| 129 |
+
(*fc).entry_count += 1;
|
| 130 |
+
(*fc).write_head = offset + pad;
|
| 131 |
+
|
| 132 |
+
// Note: original asm writes frame_idx and owner AFTER the
|
| 133 |
+
// POPAD/POP sequence β this is functionally equivalent.
|
| 134 |
+
(*entry).frame_idx = frame_idx;
|
| 135 |
+
(*entry).owner = owner;
|
| 136 |
+
|
| 137 |
+
return entry_addr.add(0x10);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// ββ Out of room: evict the oldest entry and retry βββββββββββ
|
| 141 |
+
//
|
| 142 |
+
// The original asm reloads `fc` from `*(context_ptr + 4)` here
|
| 143 |
+
// (in case the eviction callback somehow rebinds it). In practice
|
| 144 |
+
// both real callbacks just clear a `decoded_ptr` field, but we
|
| 145 |
+
// mirror the reload exactly.
|
| 146 |
+
let fc = (*context_ptr).frame_cache;
|
| 147 |
+
let head = (*fc).head_entry;
|
| 148 |
+
if head.is_null() {
|
| 149 |
+
// Theoretically unreachable: an empty cache has wrap=write=0
|
| 150 |
+
// and thus must always fit; if it doesn't, `pad > capacity`
|
| 151 |
+
// and the original asm spins forever. We mirror that.
|
| 152 |
+
continue;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
// Notify the owner that its subframe payload is being dropped.
|
| 156 |
+
// Both Sprite and SpriteBank vtable[1] callbacks share the same
|
| 157 |
+
// shape: thiscall(this, frame_idx, payload). The asm pushes
|
| 158 |
+
// `payload` first then `frame_idx` (so `frame_idx` is the first
|
| 159 |
+
// stack arg), with ECX = owner.
|
| 160 |
+
let victim_owner = (*head).owner;
|
| 161 |
+
let victim_frame_idx = (*head).frame_idx;
|
| 162 |
+
let victim_payload = (head as *mut u8).add(0x10);
|
| 163 |
+
|
| 164 |
+
// Read vtable[1] manually so we don't need to commit to either
|
| 165 |
+
// SpriteVtable or SpriteBankVtable as the callback's `this` type.
|
| 166 |
+
let vtable = *(victim_owner as *const *const u32);
|
| 167 |
+
let slot1 = *vtable.add(1);
|
| 168 |
+
let callback: EvictCallback = core::mem::transmute(slot1);
|
| 169 |
+
callback(victim_owner, victim_frame_idx, victim_payload);
|
| 170 |
+
|
| 171 |
+
// Reload fc and unlink the (just-notified) head entry.
|
| 172 |
+
let fc = (*context_ptr).frame_cache;
|
| 173 |
+
if (*fc).head_entry.is_null() {
|
| 174 |
+
continue;
|
| 175 |
+
}
|
| 176 |
+
let new_head = (*(*fc).head_entry).next;
|
| 177 |
+
(*fc).head_entry = new_head;
|
| 178 |
+
if new_head.is_null() {
|
| 179 |
+
// Last entry just removed β clear the bookkeeping back to
|
| 180 |
+
// the empty state.
|
| 181 |
+
(*fc).entry_count -= 1;
|
| 182 |
+
(*fc).tail_entry = null_mut();
|
| 183 |
+
(*fc).wrap_marker = 0;
|
| 184 |
+
(*fc).write_head = 0;
|
| 185 |
+
} else {
|
| 186 |
+
// wrap_marker tracks where the head entry now sits within
|
| 187 |
+
// the buffer (as a byte offset from `buffer`).
|
| 188 |
+
(*fc).entry_count -= 1;
|
| 189 |
+
(*fc).wrap_marker = (new_head as u32).wrapping_sub((*fc).buffer as u32);
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
#[cfg(test)]
|
| 195 |
+
mod tests {
|
| 196 |
+
use super::*;
|
| 197 |
+
use core::mem::MaybeUninit;
|
| 198 |
+
|
| 199 |
+
/// A minimal owner whose vtable[1] increments a counter when invoked.
|
| 200 |
+
#[repr(C)]
|
| 201 |
+
struct MockOwner {
|
| 202 |
+
vtable: *const MockVtable,
|
| 203 |
+
evict_count: u32,
|
| 204 |
+
last_idx: u32,
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
#[repr(C)]
|
| 208 |
+
struct MockVtable {
|
| 209 |
+
slot_0: usize,
|
| 210 |
+
slot_1: unsafe extern "thiscall" fn(*mut c_void, u32, *mut u8),
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
unsafe extern "thiscall" fn mock_evict(this: *mut c_void, idx: u32, _payload: *mut u8) {
|
| 214 |
+
let owner = this as *mut MockOwner;
|
| 215 |
+
(*owner).evict_count += 1;
|
| 216 |
+
(*owner).last_idx = idx;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
static MOCK_VTABLE: MockVtable = MockVtable {
|
| 220 |
+
slot_0: 0,
|
| 221 |
+
slot_1: mock_evict,
|
| 222 |
+
};
|
| 223 |
+
|
| 224 |
+
/// Build a freshly-zeroed FrameCache with a heap-allocated buffer.
|
| 225 |
+
/// Returns the cache (heap-allocated, must be freed by the caller).
|
| 226 |
+
fn make_cache(capacity: u32) -> (*mut SpriteCache, *mut FrameCache, *mut u8) {
|
| 227 |
+
let buf = unsafe {
|
| 228 |
+
let layout = core::alloc::Layout::from_size_align(capacity as usize, 4).unwrap();
|
| 229 |
+
std::alloc::alloc_zeroed(layout)
|
| 230 |
+
};
|
| 231 |
+
let fc = Box::leak(Box::new(unsafe {
|
| 232 |
+
MaybeUninit::<FrameCache>::zeroed().assume_init()
|
| 233 |
+
})) as *mut FrameCache;
|
| 234 |
+
unsafe {
|
| 235 |
+
(*fc).buffer = buf;
|
| 236 |
+
(*fc).capacity = capacity;
|
| 237 |
+
}
|
| 238 |
+
let sc = Box::leak(Box::new(unsafe {
|
| 239 |
+
MaybeUninit::<SpriteCache>::zeroed().assume_init()
|
| 240 |
+
})) as *mut SpriteCache;
|
| 241 |
+
unsafe {
|
| 242 |
+
(*sc).frame_cache = fc;
|
| 243 |
+
}
|
| 244 |
+
(sc, fc, buf)
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
fn free_cache(sc: *mut SpriteCache, fc: *mut FrameCache, buf: *mut u8, capacity: u32) {
|
| 248 |
+
unsafe {
|
| 249 |
+
let layout = core::alloc::Layout::from_size_align(capacity as usize, 4).unwrap();
|
| 250 |
+
std::alloc::dealloc(buf, layout);
|
| 251 |
+
drop(Box::from_raw(fc));
|
| 252 |
+
drop(Box::from_raw(sc));
|
| 253 |
+
}
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
/// Single allocation: header + payload, returns payload start.
|
| 257 |
+
#[test]
|
| 258 |
+
fn alloc_single() {
|
| 259 |
+
let (sc, fc, buf) = make_cache(0x1000);
|
| 260 |
+
let mut owner = MockOwner {
|
| 261 |
+
vtable: &MOCK_VTABLE,
|
| 262 |
+
evict_count: 0,
|
| 263 |
+
last_idx: 0,
|
| 264 |
+
};
|
| 265 |
+
unsafe {
|
| 266 |
+
let p = frame_cache_allocate(0x100, sc, &mut owner as *mut _ as *mut c_void, 7);
|
| 267 |
+
|
| 268 |
+
// Returned pointer is buffer + 0x10
|
| 269 |
+
assert_eq!(p, buf.add(0x10));
|
| 270 |
+
|
| 271 |
+
// Header fields written
|
| 272 |
+
let entry = buf as *mut FrameCacheEntry;
|
| 273 |
+
assert_eq!((*entry).payload_size, 0x100 + 8);
|
| 274 |
+
assert!((*entry).next.is_null());
|
| 275 |
+
assert_eq!((*entry).frame_idx, 7);
|
| 276 |
+
assert_eq!((*entry).owner, &mut owner as *mut _ as *mut c_void);
|
| 277 |
+
|
| 278 |
+
// Cache bookkeeping
|
| 279 |
+
assert_eq!((*fc).head_entry, entry);
|
| 280 |
+
assert_eq!((*fc).tail_entry, entry);
|
| 281 |
+
assert_eq!((*fc).entry_count, 1);
|
| 282 |
+
// pad = ((entry_size + 0x13) & ~3) = ((0x100 + 0x13) & ~3) = 0x110
|
| 283 |
+
assert_eq!((*fc).write_head, 0x110);
|
| 284 |
+
}
|
| 285 |
+
free_cache(sc, fc, buf, 0x1000);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
/// Two allocations chain into the LRU list.
|
| 289 |
+
#[test]
|
| 290 |
+
fn alloc_two_chain() {
|
| 291 |
+
let (sc, fc, buf) = make_cache(0x1000);
|
| 292 |
+
let mut owner = MockOwner {
|
| 293 |
+
vtable: &MOCK_VTABLE,
|
| 294 |
+
evict_count: 0,
|
| 295 |
+
last_idx: 0,
|
| 296 |
+
};
|
| 297 |
+
unsafe {
|
| 298 |
+
let owner_ptr = &mut owner as *mut _ as *mut c_void;
|
| 299 |
+
let _p1 = frame_cache_allocate(0x100, sc, owner_ptr, 1);
|
| 300 |
+
let _p2 = frame_cache_allocate(0x100, sc, owner_ptr, 2);
|
| 301 |
+
|
| 302 |
+
let e1 = buf as *mut FrameCacheEntry;
|
| 303 |
+
let e2 = buf.add(0x110) as *mut FrameCacheEntry;
|
| 304 |
+
assert_eq!((*e1).next, e2);
|
| 305 |
+
assert!((*e2).next.is_null());
|
| 306 |
+
assert_eq!((*fc).head_entry, e1);
|
| 307 |
+
assert_eq!((*fc).tail_entry, e2);
|
| 308 |
+
assert_eq!((*fc).entry_count, 2);
|
| 309 |
+
assert_eq!((*fc).write_head, 0x220);
|
| 310 |
+
}
|
| 311 |
+
free_cache(sc, fc, buf, 0x1000);
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
/// Eviction: fill the buffer, then a third alloc that won't fit
|
| 315 |
+
/// triggers eviction of the oldest entry and re-uses its space at
|
| 316 |
+
/// the start of the buffer (after wrap).
|
| 317 |
+
#[test]
|
| 318 |
+
fn alloc_triggers_eviction_and_wrap() {
|
| 319 |
+
// pad = 0x110 per entry. Capacity 0x300 β two entries fit at
|
| 320 |
+
// [0..0x110, 0x110..0x220]. A third 0x100 alloc would need
|
| 321 |
+
// [0x220..0x330] which exceeds capacity β must evict + wrap.
|
| 322 |
+
let (sc, fc, buf) = make_cache(0x300);
|
| 323 |
+
let mut owner = MockOwner {
|
| 324 |
+
vtable: &MOCK_VTABLE,
|
| 325 |
+
evict_count: 0,
|
| 326 |
+
last_idx: 0,
|
| 327 |
+
};
|
| 328 |
+
unsafe {
|
| 329 |
+
let owner_ptr = &mut owner as *mut _ as *mut c_void;
|
| 330 |
+
let _p1 = frame_cache_allocate(0x100, sc, owner_ptr, 1);
|
| 331 |
+
let _p2 = frame_cache_allocate(0x100, sc, owner_ptr, 2);
|
| 332 |
+
|
| 333 |
+
// Sanity: wrap_marker is still 0 (no eviction yet).
|
| 334 |
+
assert_eq!((*fc).wrap_marker, 0);
|
| 335 |
+
assert_eq!((*fc).write_head, 0x220);
|
| 336 |
+
|
| 337 |
+
let p3 = frame_cache_allocate(0x100, sc, owner_ptr, 3);
|
| 338 |
+
|
| 339 |
+
// Eviction was called once with the first entry's frame_idx.
|
| 340 |
+
assert_eq!(owner.evict_count, 1);
|
| 341 |
+
assert_eq!(owner.last_idx, 1);
|
| 342 |
+
|
| 343 |
+
// After evicting entry 1, head_entry advanced to entry 2;
|
| 344 |
+
// wrap_marker = (entry 2 offset) = 0x110.
|
| 345 |
+
assert_eq!((*fc).wrap_marker, 0x110);
|
| 346 |
+
|
| 347 |
+
// Third alloc wrapped to offset 0 (where entry 1 used to be);
|
| 348 |
+
// p3 should be buf + 0x10.
|
| 349 |
+
assert_eq!(p3, buf.add(0x10));
|
| 350 |
+
|
| 351 |
+
// Bookkeeping: 2 live entries (entry 2, entry 3), entry 3 is tail.
|
| 352 |
+
assert_eq!((*fc).entry_count, 2);
|
| 353 |
+
let e2 = buf.add(0x110) as *mut FrameCacheEntry;
|
| 354 |
+
let e3 = buf as *mut FrameCacheEntry;
|
| 355 |
+
assert_eq!((*fc).head_entry, e2);
|
| 356 |
+
assert_eq!((*fc).tail_entry, e3);
|
| 357 |
+
assert_eq!((*e2).next, e3);
|
| 358 |
+
assert!((*e3).next.is_null());
|
| 359 |
+
}
|
| 360 |
+
free_cache(sc, fc, buf, 0x300);
|
| 361 |
+
}
|
| 362 |
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! LZSS-style decompressor with palette LUT remapping for sprite subframes.
|
| 2 |
+
//!
|
| 3 |
+
//! Pure-Rust port of `Sprite_LZSS_Decode` (0x5B29E0). Used by
|
| 4 |
+
//! [`Sprite__GetFrameForBlit`](super::sprite) and `SpriteBank__GetFrameForBlit`
|
| 5 |
+
//! to lazily decompress sprite subframe pixel data into a `FrameCache`
|
| 6 |
+
//! payload.
|
| 7 |
+
//!
|
| 8 |
+
//! ## Format
|
| 9 |
+
//!
|
| 10 |
+
//! The decoder consumes a stream of source bytes from `src`. Each iteration
|
| 11 |
+
//! reads one byte and dispatches:
|
| 12 |
+
//!
|
| 13 |
+
//! - **Literal** (`src[0] & 0x80 == 0`): emit `lut[src[0]]` to `dst`,
|
| 14 |
+
//! advance `src` by 1, advance `dst` by 1.
|
| 15 |
+
//!
|
| 16 |
+
//! - **Control** (`src[0] & 0x80 != 0`): the control word is `src[0..2]`,
|
| 17 |
+
//! interpreted as `(src[0] << 8) | src[1]`.
|
| 18 |
+
//! - `distance = control & 0x7FF` (11-bit back-reference offset).
|
| 19 |
+
//! - `nibble = (src[0] >> 3) & 0xF`.
|
| 20 |
+
//!
|
| 21 |
+
//! Three sub-forms:
|
| 22 |
+
//!
|
| 23 |
+
//! 1. **Short back-ref** (`nibble != 0`): copy `nibble + 2` bytes from
|
| 24 |
+
//! `dst - distance - 1` to `dst`. Range 3..=17. Advance `src` by 2.
|
| 25 |
+
//! 2. **Long back-ref** (`nibble == 0`, `distance != 0`): read a third
|
| 26 |
+
//! byte at `src[2]`. Copy `src[2] + 18` bytes from `dst - distance`
|
| 27 |
+
//! (note the asymmetry β short refs use `-distance - 1`, long refs
|
| 28 |
+
//! use `-distance`). Range 18..=273. Advance `src` by 3.
|
| 29 |
+
//! 3. **Terminator** (`nibble == 0`, `distance == 0`): exit the loop.
|
| 30 |
+
//!
|
| 31 |
+
//! **Encoding asymmetry**: the encoded `distance` field for a short
|
| 32 |
+
//! back-ref is `real_distance - 1` (the asm's `DEC ECX` between the
|
| 33 |
+
//! `JZ` and the read makes up the difference), but for a long back-ref
|
| 34 |
+
//! it equals `real_distance` directly. The `JZ extended` jumps over
|
| 35 |
+
//! the `DEC ECX` instruction. This was verified from the disassembly
|
| 36 |
+
//! at 0x5B2A13/0x5B2A15 β there is no encoder-side fix-up that would
|
| 37 |
+
//! normalize the two paths.
|
| 38 |
+
//!
|
| 39 |
+
//! ## LUT
|
| 40 |
+
//!
|
| 41 |
+
//! `lut` is a 256-byte palette translation table. For sprites it lives at
|
| 42 |
+
//! `Sprite::palette_data_ptr` (`+0x68`); for sprite banks it's the inline
|
| 43 |
+
//! `SpriteBank::palette_lut` (`+0x30`). Entry 0 is always `0` (transparent
|
| 44 |
+
//! index); entries `1..=palette_count` are mapped runtime palette indices.
|
| 45 |
+
//! See `feedback_layer_sprite_palette.md` for the off-by-one rationale.
|
| 46 |
+
//!
|
| 47 |
+
//! ## Buffer requirements
|
| 48 |
+
//!
|
| 49 |
+
//! - `src` must contain at least 3 bytes per control sequence (the original
|
| 50 |
+
//! reads `src[2]` unconditionally on the long-backref path).
|
| 51 |
+
//! - `dst` must have already-written bytes at offsets `[-distance - 1, -1]`
|
| 52 |
+
//! for any short back-ref, or `[-distance, -1]` for any long back-ref.
|
| 53 |
+
//! Caller responsibility β the decoder does no bounds checking.
|
| 54 |
+
//! - `lut` must point at a 256-byte buffer.
|
| 55 |
+
//!
|
| 56 |
+
//! The original WA function wraps its body in `PUSHAD`/`POPAD` so callers
|
| 57 |
+
//! see no register clobber. The Rust port has no such concern.
|
| 58 |
+
|
| 59 |
+
/// Decode an LZSS-compressed sprite subframe into `dst`, remapping each
|
| 60 |
+
/// literal byte through `lut` first.
|
| 61 |
+
///
|
| 62 |
+
/// # Safety
|
| 63 |
+
///
|
| 64 |
+
/// - `dst` must point at a writable buffer large enough to hold the
|
| 65 |
+
/// decompressed payload (caller-known via `SpriteSubframeCache::decoded_size`
|
| 66 |
+
/// or `SpriteBankSubframeCache::decoded_size`).
|
| 67 |
+
/// - `src` must contain a well-formed LZSS stream terminated by a
|
| 68 |
+
/// zero-length zero-distance control word.
|
| 69 |
+
/// - `lut` must point at a 256-byte palette translation table.
|
| 70 |
+
/// - Back-references at `dst - distance - 1` (short) or `dst - distance`
|
| 71 |
+
/// (long) must lie within the already-decoded portion of `dst`.
|
| 72 |
+
pub unsafe fn sprite_lzss_decode(mut dst: *mut u8, mut src: *const u8, lut: *const u8) {
|
| 73 |
+
loop {
|
| 74 |
+
// ββ Literal run ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 75 |
+
let b: u8 = loop {
|
| 76 |
+
let b = *src;
|
| 77 |
+
if b & 0x80 != 0 {
|
| 78 |
+
break b;
|
| 79 |
+
}
|
| 80 |
+
*dst = *lut.add(b as usize);
|
| 81 |
+
src = src.add(1);
|
| 82 |
+
dst = dst.add(1);
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
// ββ Control word βββββββββββββββββββββββββββββββββββββββββββββ
|
| 86 |
+
// CX = src[0]<<8 | src[1]; distance = CX & 0x7FF.
|
| 87 |
+
let distance = (((b as u32) << 8) | (*src.add(1) as u32)) & 0x7FF;
|
| 88 |
+
let nibble = ((b as u32) >> 3) & 0xF;
|
| 89 |
+
|
| 90 |
+
if nibble != 0 {
|
| 91 |
+
// ββ Short back-ref βββββββββββββββββββββββββββββββββββββββ
|
| 92 |
+
// Asm uses ECX = -distance - 1 for the entire run; copies
|
| 93 |
+
// happen as `*EDI = [ECX + EDI]` followed by `INC EDI`,
|
| 94 |
+
// which means each successive write reads one position later.
|
| 95 |
+
// The total length is `nibble + 2` bytes (range 3..=17).
|
| 96 |
+
//
|
| 97 |
+
// The original asm structure does 3 unconditional copies and
|
| 98 |
+
// then `(nibble - 1)` more in a loop. Equivalent to a flat
|
| 99 |
+
// loop of `nibble + 2` iterations.
|
| 100 |
+
let copy_offset = (distance as usize).wrapping_add(1);
|
| 101 |
+
let total = (nibble as usize) + 2;
|
| 102 |
+
for _ in 0..total {
|
| 103 |
+
*dst = *dst.sub(copy_offset);
|
| 104 |
+
dst = dst.add(1);
|
| 105 |
+
}
|
| 106 |
+
src = src.add(2);
|
| 107 |
+
} else {
|
| 108 |
+
// ββ Terminator or long back-ref ββββββββββββββββββββββββββ
|
| 109 |
+
if distance == 0 {
|
| 110 |
+
return;
|
| 111 |
+
}
|
| 112 |
+
// Long back-ref uses ECX = -distance (NOT decremented β the
|
| 113 |
+
// asm's `JZ extended` jumps OVER the `DEC ECX`). Length is
|
| 114 |
+
// `src[2] + 18` (range 18..=273).
|
| 115 |
+
let len = (*src.add(2) as usize) + 18;
|
| 116 |
+
let copy_offset = distance as usize;
|
| 117 |
+
for _ in 0..len {
|
| 118 |
+
*dst = *dst.sub(copy_offset);
|
| 119 |
+
dst = dst.add(1);
|
| 120 |
+
}
|
| 121 |
+
src = src.add(3);
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
#[cfg(test)]
|
| 127 |
+
mod tests {
|
| 128 |
+
use super::sprite_lzss_decode;
|
| 129 |
+
|
| 130 |
+
/// Identity LUT β `lut[i] == i`.
|
| 131 |
+
fn id_lut() -> [u8; 256] {
|
| 132 |
+
let mut l = [0u8; 256];
|
| 133 |
+
for i in 0..256 {
|
| 134 |
+
l[i] = i as u8;
|
| 135 |
+
}
|
| 136 |
+
l
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/// Pure-literal stream of bytes 1..16, terminated.
|
| 140 |
+
#[test]
|
| 141 |
+
fn literals_only() {
|
| 142 |
+
let lut = id_lut();
|
| 143 |
+
// 16 literals + terminator (0x80, 0x00 β nibble=0, distance=0).
|
| 144 |
+
let mut src = vec![];
|
| 145 |
+
for i in 1..=16u8 {
|
| 146 |
+
src.push(i);
|
| 147 |
+
}
|
| 148 |
+
src.push(0x80);
|
| 149 |
+
src.push(0x00);
|
| 150 |
+
|
| 151 |
+
let mut dst = vec![0u8; 16];
|
| 152 |
+
unsafe {
|
| 153 |
+
sprite_lzss_decode(dst.as_mut_ptr(), src.as_ptr(), lut.as_ptr());
|
| 154 |
+
}
|
| 155 |
+
let expected: Vec<u8> = (1..=16u8).collect();
|
| 156 |
+
assert_eq!(dst, expected);
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
/// Literals routed through a non-identity LUT.
|
| 160 |
+
#[test]
|
| 161 |
+
fn literals_with_lut() {
|
| 162 |
+
let mut lut = id_lut();
|
| 163 |
+
lut[0x05] = 0xAA;
|
| 164 |
+
lut[0x10] = 0xBB;
|
| 165 |
+
|
| 166 |
+
let src: Vec<u8> = vec![0x05, 0x10, 0x80, 0x00];
|
| 167 |
+
let mut dst = vec![0u8; 2];
|
| 168 |
+
unsafe {
|
| 169 |
+
sprite_lzss_decode(dst.as_mut_ptr(), src.as_ptr(), lut.as_ptr());
|
| 170 |
+
}
|
| 171 |
+
assert_eq!(dst, vec![0xAA, 0xBB]);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
/// Short back-ref: emit "ABC", then copy 3 bytes from `dst - 3`.
|
| 175 |
+
///
|
| 176 |
+
/// **Encoding asymmetry**: short refs encode `real_distance - 1` (the
|
| 177 |
+
/// asm's `DEC ECX` adds the +1), while long refs encode the real
|
| 178 |
+
/// distance directly. So to back up 3 bytes, we encode `2` in the
|
| 179 |
+
/// distance bits.
|
| 180 |
+
#[test]
|
| 181 |
+
fn short_backref_basic() {
|
| 182 |
+
let lut = id_lut();
|
| 183 |
+
// nibble = 1 (length = 3), encoded distance = 2 (real = 3).
|
| 184 |
+
// src[0] = 0x80 | (1 << 3) | ((2 >> 8) & 0x7) = 0x88
|
| 185 |
+
// src[1] = 2 & 0xFF = 0x02
|
| 186 |
+
let src: Vec<u8> = vec![b'A', b'B', b'C', 0x88, 0x02, 0x80, 0x00];
|
| 187 |
+
let mut dst = vec![0u8; 6];
|
| 188 |
+
unsafe {
|
| 189 |
+
sprite_lzss_decode(dst.as_mut_ptr(), src.as_ptr(), lut.as_ptr());
|
| 190 |
+
}
|
| 191 |
+
assert_eq!(&dst, b"ABCABC");
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
/// Short back-ref length boundary: nibble = 15 β copies 17 bytes.
|
| 195 |
+
/// Encode `real_distance - 1 = 16` to back up 17 bytes.
|
| 196 |
+
#[test]
|
| 197 |
+
fn short_backref_max_length() {
|
| 198 |
+
let lut = id_lut();
|
| 199 |
+
// Pre-fill 17 unique bytes, then back-ref:
|
| 200 |
+
// nibble = 15 (length 17), encoded distance = 16 (real = 17).
|
| 201 |
+
let mut src: Vec<u8> = (1..=17u8).collect();
|
| 202 |
+
// src[0] = 0x80 | (15 << 3) | ((16 >> 8) & 0x7) = 0xF8
|
| 203 |
+
// src[1] = 16 & 0xFF = 0x10
|
| 204 |
+
src.push(0xF8);
|
| 205 |
+
src.push(0x10);
|
| 206 |
+
src.push(0x80);
|
| 207 |
+
src.push(0x00);
|
| 208 |
+
|
| 209 |
+
let mut dst = vec![0u8; 34];
|
| 210 |
+
unsafe {
|
| 211 |
+
sprite_lzss_decode(dst.as_mut_ptr(), src.as_ptr(), lut.as_ptr());
|
| 212 |
+
}
|
| 213 |
+
let expected: Vec<u8> = (1..=17u8).chain(1..=17u8).collect();
|
| 214 |
+
assert_eq!(dst, expected);
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
/// Long back-ref: nibble = 0, distance != 0, length = src[2] + 18.
|
| 218 |
+
/// Note long-form uses `-distance` (NOT `-distance - 1`).
|
| 219 |
+
#[test]
|
| 220 |
+
fn long_backref_basic() {
|
| 221 |
+
let lut = id_lut();
|
| 222 |
+
// Pre-fill 20 bytes 1..20, then long back-ref:
|
| 223 |
+
// nibble=0, distance=20 (so `-distance` reads dst[-20]=1, etc.)
|
| 224 |
+
// src[2] = 0 β length = 18.
|
| 225 |
+
let mut src: Vec<u8> = (1..=20u8).collect();
|
| 226 |
+
// src[0] = 0x80 | (0 << 3) | ((20 >> 8) & 0x7) = 0x80
|
| 227 |
+
// src[1] = 20 & 0xFF = 0x14
|
| 228 |
+
// src[2] = 0 β length 18
|
| 229 |
+
src.push(0x80);
|
| 230 |
+
src.push(0x14);
|
| 231 |
+
src.push(0x00);
|
| 232 |
+
src.push(0x80);
|
| 233 |
+
src.push(0x00);
|
| 234 |
+
|
| 235 |
+
let mut dst = vec![0u8; 38];
|
| 236 |
+
unsafe {
|
| 237 |
+
sprite_lzss_decode(dst.as_mut_ptr(), src.as_ptr(), lut.as_ptr());
|
| 238 |
+
}
|
| 239 |
+
let expected: Vec<u8> = (1..=20u8).chain(1..=18u8).collect();
|
| 240 |
+
assert_eq!(dst, expected);
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
/// Self-overlapping short back-ref (RLE-style): emit one byte, then
|
| 244 |
+
/// short-copy with distance = 1 (which becomes `-distance - 1 = -2`,
|
| 245 |
+
/// a stride-2 read). This isn't classic RLE β verify against the
|
| 246 |
+
/// asm semantics, not against expectation.
|
| 247 |
+
///
|
| 248 |
+
/// With dst = [X, _, _, ...] and copy_offset = 2:
|
| 249 |
+
/// write 1: dst[1] = dst[1-2] β reads dst[-1] (out of buffer!)
|
| 250 |
+
///
|
| 251 |
+
/// So distance = 1 short-copies are unsafe. Test distance = 0 with the
|
| 252 |
+
/// nibble path, which `-1` β reads dst[-1]. Also unsafe.
|
| 253 |
+
/// Skip this test β both edge cases reach outside `dst` and only work
|
| 254 |
+
/// with valid pre-loaded backing.
|
| 255 |
+
#[test]
|
| 256 |
+
fn self_overlap_distance_zero_short() {
|
| 257 |
+
// Use distance = 0, nibble = 1 β copy_offset = 1, length = 3.
|
| 258 |
+
// Each iteration: dst[i] = dst[i - 1].
|
| 259 |
+
// With dst = [42, _, _, _]: yields [42, 42, 42, 42].
|
| 260 |
+
let lut = id_lut();
|
| 261 |
+
let src: Vec<u8> = vec![
|
| 262 |
+
42, // literal
|
| 263 |
+
// distance=0, nibble=1
|
| 264 |
+
// src[0] = 0x80 | (1<<3) | 0 = 0x88
|
| 265 |
+
// src[1] = 0x00
|
| 266 |
+
0x88, 0x00, // terminator
|
| 267 |
+
0x80, 0x00,
|
| 268 |
+
];
|
| 269 |
+
let mut dst = vec![0u8; 4];
|
| 270 |
+
unsafe {
|
| 271 |
+
sprite_lzss_decode(dst.as_mut_ptr(), src.as_ptr(), lut.as_ptr());
|
| 272 |
+
}
|
| 273 |
+
assert_eq!(dst, vec![42, 42, 42, 42]);
|
| 274 |
+
}
|
| 275 |
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
|
|
| 1 |
pub mod gfx_dir;
|
|
|
|
| 2 |
pub mod spr;
|
| 3 |
pub mod sprite;
|
| 4 |
pub mod sprite_id;
|
| 5 |
|
|
|
|
|
|
|
| 6 |
pub use spr::{ParsedSprite, SprError, SprHeader};
|
| 7 |
pub use sprite::{
|
| 8 |
Sprite, SpriteBank, SpriteBankBboxEntry, SpriteBankFrame, SpriteBankSubframeCache,
|
|
|
|
| 1 |
+
pub mod frame_cache;
|
| 2 |
pub mod gfx_dir;
|
| 3 |
+
pub mod lzss;
|
| 4 |
pub mod spr;
|
| 5 |
pub mod sprite;
|
| 6 |
pub mod sprite_id;
|
| 7 |
|
| 8 |
+
pub use frame_cache::frame_cache_allocate;
|
| 9 |
+
pub use lzss::sprite_lzss_decode;
|
| 10 |
pub use spr::{ParsedSprite, SprError, SprHeader};
|
| 11 |
pub use sprite::{
|
| 12 |
Sprite, SpriteBank, SpriteBankBboxEntry, SpriteBankFrame, SpriteBankSubframeCache,
|
|
@@ -1358,6 +1358,7 @@ fn install_display() -> Result<(), String> {
|
|
| 1358 |
update_palette => display_vtable::update_palette,
|
| 1359 |
set_font_palette => display_vtable::set_font_palette,
|
| 1360 |
slot 19 => blit_sprite,
|
|
|
|
| 1361 |
load_sprite_by_layer => load_sprite_by_layer,
|
| 1362 |
load_font => load_font,
|
| 1363 |
load_font_extension => load_font_extension,
|
|
|
|
| 1358 |
update_palette => display_vtable::update_palette,
|
| 1359 |
set_font_palette => display_vtable::set_font_palette,
|
| 1360 |
slot 19 => blit_sprite,
|
| 1361 |
+
get_sprite_frame_for_blit => display_vtable::get_sprite_frame_for_blit,
|
| 1362 |
load_sprite_by_layer => load_sprite_by_layer,
|
| 1363 |
load_font => load_font,
|
| 1364 |
load_font_extension => load_font_extension,
|