paavohtl's picture
Claude Opus 4.6 (1M context)
refactor: port GfxDir resource loading system to Rust
3f42fbe
Raw History Blame Contribute Delete
95.8 kB
use core::ffi::c_char;
use openwa_core::vtable;
use crate::asset::gfx_dir::GfxDir;
use crate::fixed::Fixed;
use crate::render::display::font::{
font_extend, font_get_info_impl, font_get_metric_impl, font_load_from_gfx,
font_set_palette_impl, font_set_param_impl, Font,
};
use crate::render::display::layer::Layer;
use crate::render::display::line_draw::Vertex;
use crate::render::sprite::sprite::{LayerSprite, LayerSpriteFrame};
use crate::render::sprite::sprite_op::SpriteOp;
use crate::render::SpriteCache;
use crate::wa_alloc::wa_malloc_struct_zeroed;
/// DisplayVtable β€” vtable for the display/rendering subsystem (DisplayGfx).
///
/// Constructor: DisplayGfx__Init (0x569D00).
/// Vtable: 0x66A218 (38 slots).
/// Destructor: 0x569CE0.
///
/// DisplayGfx (0x24E28 bytes) extends DisplayBase.
/// Manages layers, sprites, fonts, palettes, and delegates rendering through
/// RenderContext (g_RenderContext at 0x79D6D4), which in turn dispatches
/// to a renderer backend (CompatRenderer for D3D/DDraw, OpenGLCPU for OpenGL).
///
/// ## Dispatch chain
///
/// ```text
/// DisplayGfx β†’ RenderContext β†’ RendererBackend
/// vtable (CWormsApp sub- (CompatRenderer/
/// 0x66A218 object, vtable OpenGLCPU/DDraw)
/// 0x662EC8)
/// ```
///
/// DisplayGfx methods apply camera offset and clipping, then call through
/// `g_RenderContext->vtable[N]` for the actual rendering operation.
/// Display vtable (0x66A218, 38 slots).
///
/// Slots 2, 3, 25, 29 are stubs (CGameTask no-ops).
/// All other slots are standard thiscall.
///
/// Coordinate conventions:
/// - Methods that add `camera * 0x10000` take Fixed (16.16) coordinates.
/// - Methods that add `camera` directly take pixel-integer coordinates.
#[vtable(size = 38, va = 0x0066_A218, class = "DisplayGfx")]
pub struct DisplayGfxVtable {
/// scalar deleting destructor (original at 0x569CE0, RET 0x4).
///
/// **Ported.** Native Rust impl is
/// [`crate::render::display::destructor::display_gfx_destructor`].
/// The thunk runs the cleanup body (`DestructorImpl`, originally
/// 0x56A010) and then `_free(this)` if `flags & 1` is set, returning
/// `this` per the MSVC ABI. Wired via `vtable_replace!` in
/// `install_display`. The three WA-side helpers
/// (`DestructorImpl`, `FreeLayerSpriteTable`,
/// `TileBitmapSet::Destructor`) are trapped β€” see commit 5 of the
/// destructor port for the dead-slot analysis behind trapping the
/// `TileBitmapSet` helper.
#[slot(0)]
pub destructor: fn(this: *mut DisplayGfx, flags: u32) -> *mut DisplayGfx,
/// get display dimensions in pixels (0x56A460, RET 0x8)
#[slot(1)]
pub get_dimensions: fn(this: *mut DisplayGfx, out_w: *mut u32, out_h: *mut u32),
/// set layer color (0x5231E0, RET 0x8)
#[slot(4)]
pub set_layer_color: fn(this: *mut DisplayGfx, layer: i32, color: i32),
/// set active layer, returns layer context ptr (0x523270, RET 0x4).
///
/// Returns the `PaletteContext*` for the requested layer (1-3),
/// or null if the index is out of range. Callers feed the result to
/// `update_palette` (slot 24) and to GfxDir loaders that need a
/// palette context for color remapping.
#[slot(5)]
pub set_active_layer: fn(this: *mut DisplayGfx, layer: i32) -> *mut PaletteContext,
/// get sprite info by layer and id (0x523500, RET 0x10)
///
/// Looks up sprite metadata for the given layer ID. Checks sprite_ptrs
/// (Sprite* pointers) first, then sprite_banks. On success, writes
/// sprite data, flags, and width to the output pointers.
/// Returns a pointer to the string "sprite" on success, or 0 on failure.
#[slot(6)]
pub get_sprite_info: fn(
this: *mut DisplayGfx,
layer: i32,
out_data: *mut u32,
out_flags: *mut u32,
out_width: *mut u32,
) -> u32,
/// draw bitmap text onto a `BitGrid` surface (0x5236B0, RET 0x1C).
///
/// `font_id` low 16 bits = font slot (1-based, validated against
/// `font_table[1..=31]`); `font_id` high 16 bits = flags. Bit 1 of the
/// high half flips the rasterizer to right-aligned mode (draws the
/// string right-to-left starting from `pen_x` as the right anchor).
///
/// `bitmap` is the destination `BitGrid` (caller passes a layer surface
/// or sprite-bitmap pointer). `pen_x`/`pen_y` are the top-left corner
/// of the text area (or right edge in right-aligned mode).
///
/// `out_pen_x` receives the running advance after drawing (the X-pixel
/// distance covered by the rendered text). `out_width` always receives
/// the font's max-glyph-width unconditionally.
///
/// Returns the count of characters successfully drawn (or the index of
/// the first character that didn't fit if truncated, or `-1` for the
/// right-aligned path completing the full string, or 0 on early
/// validation failure).
#[slot(7)]
pub draw_text_on_bitmap: fn(
this: *mut DisplayGfx,
font_id: i32,
bitmap: *mut BitGrid,
pen_x: i32,
pen_y: i32,
msg: *const c_char,
out_pen_x: *mut i32,
out_width: *mut i32,
) -> i32,
/// get font info for a font slot (0x523790, RET 0xC)
///
/// Reads two signed shorts from the font object and writes them to output pointers.
/// Returns 1 on success, 0 if font_id is out of range or font slot is empty.
#[slot(8)]
pub get_font_info:
fn(this: *mut DisplayGfx, font_id: i32, out_1: *mut u32, out_2: *mut u32) -> u32,
/// get font metric for a character (0x523750, RET 0x10)
///
/// Queries character metrics from the font object. `char_code` is passed
/// as a byte (low 8 bits). Returns result via two output pointers.
/// Returns 1 on success, 0 if font_id is out of range or font slot is empty.
#[slot(9)]
pub get_font_metric: fn(
this: *mut DisplayGfx,
font_id: i32,
char_code: u32,
out_1: *mut u32,
out_2: *mut u32,
) -> u32,
/// set font rendering parameter (0x523710, RET 0x10)
#[slot(10)]
pub set_font_param: fn(this: *mut DisplayGfx, font_id: i32, p3: u32, p4: u32, p5: u32) -> u32,
/// `DisplayGfx::DrawTiledBitmap` (0x56B8C0, RET 0xC).
///
/// Three-phase tile-cached landscape blit (allocate / populate /
/// display). `source` is a [`TiledBitmapSource`] descriptor. Reachable
/// at runtime via `RenderDrawingQueue` case 0xD; only known producer is
/// `CTaskLand::RenderLandscape`. Ported impl at [`draw_tiled_bitmap_impl`].
#[slot(11)]
pub draw_tiled_bitmap:
fn(this: *mut DisplayGfx, dest_x: i32, dest_y: i32, source: *const TiledBitmapSource),
/// draw polyline with camera offset (0x56BCC0, RET 0xC)
///
/// Transforms point array by camera offset, then draws connected line segments.
/// Points are pixel-integer pairs.
#[slot(12)]
pub draw_polyline: fn(this: *mut DisplayGfx, points: *mut i32, count: i32, color: u32),
/// draw line with camera offset (0x56BDB0, RET 0x18)
///
/// Coordinates are fixed-point; camera is applied as `camera * 0x10000 + coord`.
#[slot(13)]
pub draw_line: fn(
this: *mut DisplayGfx,
x1: Fixed,
y1: Fixed,
x2: Fixed,
y2: Fixed,
color1: u32,
color2: u32,
),
/// draw line with camera offset and clip (0x56BD50, RET 0x14)
#[slot(14)]
pub draw_line_clipped:
fn(this: *mut DisplayGfx, x1: Fixed, y1: Fixed, x2: Fixed, y2: Fixed, color: u32),
/// draw repeated pixels along a vector (0x56BE10, RET 0x18)
///
/// Draws count+1 pixels starting at (x,y), stepping by (dx,dy) each iteration.
/// All coordinates are fixed-point.
#[slot(15)]
pub draw_pixel_strip:
fn(this: *mut DisplayGfx, x: Fixed, y: Fixed, dx: Fixed, dy: Fixed, count: i32, color: u32),
/// draw crosshair pattern β€” 9 pixels in a cross (0x56BE80, RET 0x10)
///
/// Pixel-integer coordinates (adds camera directly).
#[slot(16)]
pub draw_crosshair: fn(this: *mut DisplayGfx, x: i32, y: i32, color_fg: u32, color_bg: u32),
/// draw outlined pixel β€” center + 4 cardinal neighbors (0x56BFD0, RET 0x10)
///
/// Pixel-integer coordinates.
#[slot(17)]
pub draw_outlined_pixel:
fn(this: *mut DisplayGfx, x: i32, y: i32, color_fg: u32, color_bg: i32),
/// fill rectangle with camera offset and clipping (0x56B810, RET 0x14)
///
/// Pixel-integer coordinates (adds camera directly, then clips).
#[slot(18)]
pub fill_rect: fn(this: *mut DisplayGfx, x1: i32, y1: i32, x2: i32, y2: i32, color: u32),
/// blit sprite to display (0x56B080, RET 0x10)
///
/// Complex sprite blitting with orientation, palette, and blend flags.
/// x/y are fixed-point world coordinates (`>> 0x10` internally).
/// param_4 low 16 bits = sprite ID, high bits = orientation/blend flags.
/// param_5 = palette/opacity value.
///
/// Note: the original code also reads EBX, ESI, EDI set by the caller
/// (sprite width, sprite height, extra flags) via callee-saved registers.
/// These are not part of the thiscall ABI and cannot be expressed here.
#[slot(19)]
pub blit_sprite: fn(this: *mut DisplayGfx, x: Fixed, y: Fixed, sprite: SpriteOp, palette: u32),
/// draw scaled/rotated sprite (0x56B660, RET 0x20)
///
/// x/y are fixed-point world coordinates.
/// `sprite` is a source DisplayBitGrid pointer.
/// Dispatches by flags:
/// - bit 20: blend mode toggle (0 = ColorTable/transparency, 1 = Copy/opaque)
/// - bit 21 (0x200000): additive blend (uses color_add_table LUT)
/// - bit 26 (0x4000000): color blend (uses color_blend_table LUT)
/// - bit 27 (0x8000000): stippled mode 0
/// - bit 28 (0x10000000): stippled mode 1
#[slot(20)]
pub draw_scaled_sprite: fn(
this: *mut DisplayGfx,
x: Fixed,
y: Fixed,
sprite: *mut DisplayBitGrid,
src_x: i32,
src_y: i32,
src_w: i32,
src_h: i32,
flags: u32,
),
/// draw via object callback (0x56B7C0, RET 0x14)
///
/// Calls vtable[2] on the object pointer with camera-adjusted coordinates.
/// x/y are fixed-point.
#[slot(21)]
pub draw_via_callback:
fn(this: *mut DisplayGfx, x: Fixed, y: Fixed, obj: *mut u8, p5: u32, p6: u32),
/// draw tiled terrain (0x56C5A0, RET 0x10)
///
/// Tiles bitmaps from a grid config in a row-major pattern with camera offset.
/// `count` limits the number of pixel-rows rendered. `flags` low 16 bits
/// selects mode (only 1 supported), bit 19 controls a blit transparency flag.
#[slot(22)]
pub draw_tiled_terrain: fn(this: *mut DisplayGfx, x: Fixed, y: Fixed, count: i32, flags: u32),
/// set layer visibility (0x56A5D0, RET 0x8)
#[slot(23)]
pub set_layer_visibility: fn(this: *mut DisplayGfx, layer: i32, visible: i32),
/// update palette from PaletteContext (0x56A610, RET 0x8)
#[slot(24)]
pub update_palette: fn(this: *mut DisplayGfx, palette_ctx: *mut PaletteContext, commit: i32),
// Slot 25: stub (CGameTask__vt19)
/// flush pending render state (0x56A580, plain RET)
///
/// No stack params. Releases renderer lock and
/// calls through RenderContext vtable to finalize.
#[slot(26)]
pub flush_render: fn(this: *mut DisplayGfx),
/// set camera offset (0x56CC40, RET 0x8)
///
/// Fixed-point input; internally `>> 16` to pixel integers stored
/// in `DisplayGfx::camera_x` / `camera_y`.
#[slot(27)]
pub set_camera_offset: fn(this: *mut DisplayGfx, x: Fixed, y: Fixed),
/// set clip rectangle (0x56CC60, RET 0x10)
///
/// Fixed-point input; internally `>> 16` to pixel integers, clamped to display dimensions.
#[slot(28)]
pub set_clip_rect: fn(this: *mut DisplayGfx, x1: Fixed, y1: Fixed, x2: Fixed, y2: Fixed),
// Slot 29: stub (CGameTask__vt18)
/// load sprite with extended params (0x523310, RET 0x18)
///
/// Allocates 0x17C sprite object, calls DisplayGfx constructor.
#[slot(30)]
pub load_sprite_ex:
fn(this: *mut DisplayGfx, mode: i32, id: i32, p4: u32, count: i32, p6: u32, p7: u32) -> i32,
/// load sprite into layer (0x523400, RET 0x14)
#[slot(31)]
pub load_sprite: fn(
this: *mut DisplayGfx,
layer: u32,
id: u32,
flag: u32,
gfx_dir: *mut GfxDir,
name: *const c_char,
) -> i32,
/// check if a sprite ID is loaded (0x56A480, RET 0x4)
///
/// Returns 1 if the sprite exists in any of the three sprite arrays.
#[slot(32)]
pub is_sprite_loaded: fn(this: *mut DisplayGfx, id: i32) -> u32,
/// look up a sprite frame's surface and metadata for blitting
/// (0x5237C0, RET 0x24) β€” `DisplayGfx::GetSpriteFrameForBlit`.
#[slot(33)]
pub get_sprite_frame_for_blit: fn(
this: *mut DisplayGfx,
sprite_id: u32,
anim_value: u32,
out_w: *mut i32,
out_h: *mut i32,
out_left: *mut i32,
out_top: *mut i32,
out_right: *mut i32,
out_bottom: *mut i32,
out_anim_frac: *mut u32,
) -> *mut DisplayBitGrid,
/// load .fnt bitmap font into a font slot (0x523560, RET 0x10)
#[slot(34)]
pub load_font: fn(
this: *mut DisplayGfx,
mode: i32,
font_id: i32,
gfx_dir: *mut GfxDir,
filename: *const c_char,
) -> u32,
/// load .fex font extension for a font slot (0x523620, RET 0x14)
#[slot(35)]
pub load_font_extension: fn(
this: *mut DisplayGfx,
font_id: i32,
path: *const c_char,
char_map: *const c_char,
palette_value: u32,
flag: i32,
) -> u32,
/// set font palette for all loaded fonts (0x523690, RET 0x8)
#[slot(36)]
pub set_font_palette: fn(this: *mut DisplayGfx, font_count: u32, palette_value: u32),
/// load sprite by layer with fallback allocation (0x56A4C0, RET 0x10)
#[slot(37)]
pub load_sprite_by_layer: fn(
this: *mut DisplayGfx,
layer: u32,
id: u32,
gfx: *mut GfxDir,
name: *const c_char,
) -> i32,
}
// Generate calling wrappers: DisplayGfx::set_layer_color(), etc.
bind_DisplayGfxVtable!(DisplayGfx, base.vtable);
// =========================================================================
// Ported DisplayGfx vtable methods
// =========================================================================
use super::base::DisplayBase;
use super::gfx::DisplayGfx;
use super::line_draw;
use crate::bitgrid::{BitGrid, DisplayBitGrid};
use crate::render::palette::PaletteContext;
use crate::render::sprite::{
frame_cache::frame_cache_allocate, lzss::sprite_lzss_decode, Sprite, SpriteBank, SpriteVtable,
};
/// Port of DisplayGfx::GetDimensions (slot 1, 0x56A460).
pub unsafe extern "thiscall" fn get_dimensions(
this: *mut DisplayGfx,
out_w: *mut u32,
out_h: *mut u32,
) {
*out_w = (*this).base.display_width;
*out_h = (*this).base.display_height;
}
use super::context::{FastcallResult, RenderContext};
use crate::address::va;
use crate::rebase::rb;
/// Port of DisplayGfx::FlushRender (slot 26, 0x56A580).
///
/// The original also calls `unlock_surface_write` when the lock is held,
/// but that's a no-op whose result is discarded β€” we just clear the flag.
pub unsafe extern "thiscall" fn flush_render(this: *mut DisplayGfx) {
let wrapper = *(rb(va::G_RENDER_CONTEXT) as *const *mut RenderContext);
if (*this).render_lock != 0 {
(*this).render_lock = 0;
}
let mut buf = FastcallResult::default();
RenderContext::get_renderer_surface_raw(wrapper, &mut buf);
}
/// Port of DisplayGfx::SetCameraOffset (slot 27, 0x56CC40).
pub unsafe extern "thiscall" fn set_camera_offset(this: *mut DisplayGfx, x: Fixed, y: Fixed) {
(*this).camera_x = x.to_int();
(*this).camera_y = y.to_int();
}
/// Port of DisplayGfx::SetClipRect (slot 28, 0x56CC60).
pub unsafe extern "thiscall" fn set_clip_rect(
this: *mut DisplayGfx,
x1: Fixed,
y1: Fixed,
x2: Fixed,
y2: Fixed,
) {
let base = &mut (*this).base;
// Convert Fixed to pixel integers
let mut cx1 = x1.to_int();
let mut cy1 = y1.to_int();
let mut cx2 = x2.to_int();
let mut cy2 = y2.to_int();
// Store and clamp to display dimensions
base.clip_x1 = cx1;
base.clip_y1 = cy1;
base.clip_x2 = cx2;
base.clip_y2 = cy2;
if cx1 < 0 {
base.clip_x1 = 0;
cx1 = 0;
}
if cy1 < 0 {
base.clip_y1 = 0;
cy1 = 0;
}
if cx2 > base.display_width as i32 {
base.clip_x2 = base.display_width as i32;
cx2 = base.display_width as i32;
}
if cy2 > base.display_height as i32 {
base.clip_y2 = base.display_height as i32;
cy2 = base.display_height as i32;
}
// Mirror clip rect to the layer_0 BitGrid.
let layer = (*this).layer_0;
(*layer).clip_left = cx1 as u32;
(*layer).clip_top = cy1 as u32;
(*layer).clip_right = cx2 as u32;
(*layer).clip_bottom = cy2 as u32;
if cx1 < 0 {
(*layer).clip_left = 0;
}
if cy1 < 0 {
(*layer).clip_top = 0;
}
if cx2 > (*layer).width as i32 {
(*layer).clip_right = (*layer).width;
}
if cy2 > (*layer).height as i32 {
(*layer).clip_bottom = (*layer).height;
}
}
/// Port of the render-lock flush helper at 0x56A330 (usercall on ESI).
unsafe fn flush_render_lock(gfx: *mut DisplayGfx) {
if (*gfx).render_lock != 0 {
let wrapper = *(rb(va::G_RENDER_CONTEXT) as *const *mut RenderContext);
let data = (*(*gfx).layer_0).data;
let mut buf = FastcallResult::default();
RenderContext::unlock_surface_write_raw(wrapper, &mut buf, data);
(*gfx).render_lock = 0;
}
}
/// Port of the render-lock acquire helper at 0x56A370 (usercall on ESI).
pub unsafe fn acquire_render_lock(gfx: *mut DisplayGfx) {
if (*gfx).render_lock != 0 {
return; // already locked
}
let wrapper = *(rb(va::G_RENDER_CONTEXT) as *const *mut RenderContext);
let mut buf = FastcallResult::default();
let mut dims: [u32; 2] = [0; 2];
RenderContext::get_framebuffer_dims_raw(wrapper, &mut buf, dims.as_mut_ptr());
let fb_width = dims[0];
let fb_height = dims[1];
let mut data_ptr: *mut u8 = core::ptr::null_mut();
let mut stride: u32 = 0;
RenderContext::lock_surface_write_raw(wrapper, &mut buf, &mut data_ptr, &mut stride);
let layer = (*gfx).layer_0;
if (*layer).external_buffer != 0 {
(*layer).width = fb_width;
(*layer).height = fb_height;
(*layer).data = data_ptr;
(*layer).row_stride = stride;
(*layer).clip_left = 0;
(*layer).clip_top = 0;
(*layer).clip_right = fb_width;
(*layer).clip_bottom = fb_height;
}
let base = &(*gfx).base;
(*layer).clip_left = base.clip_x1 as u32;
(*layer).clip_top = base.clip_y1 as u32;
(*layer).clip_right = base.clip_x2 as u32;
(*layer).clip_bottom = base.clip_y2 as u32;
if base.clip_x1 < 0 {
(*layer).clip_left = 0;
}
if base.clip_y1 < 0 {
(*layer).clip_top = 0;
}
if base.clip_x2 > (*layer).width as i32 {
(*layer).clip_right = (*layer).width;
}
if base.clip_y2 > (*layer).height as i32 {
(*layer).clip_bottom = (*layer).height;
}
(*gfx).render_lock = 1;
}
/// Port of DisplayGfx::FillRect (slot 18, 0x56B810).
pub unsafe extern "thiscall" fn fill_rect(
this: *mut DisplayGfx,
x1: i32,
y1: i32,
x2: i32,
y2: i32,
color: u32,
) {
let base = &(*this).base;
let mut left = x1 + (*this).camera_x;
let mut top = y1 + (*this).camera_y;
let mut right = x2 + (*this).camera_x;
let mut bottom = y2 + (*this).camera_y;
if right <= base.clip_x1
|| bottom <= base.clip_y1
|| left >= base.clip_x2
|| top >= base.clip_y2
{
return;
}
if left < base.clip_x1 {
left = base.clip_x1;
}
if top < base.clip_y1 {
top = base.clip_y1;
}
if right > base.clip_x2 {
right = base.clip_x2;
}
if bottom > base.clip_y2 {
bottom = base.clip_y2;
}
flush_render_lock(this);
let wrapper = *(rb(va::G_RENDER_CONTEXT) as *const *mut RenderContext);
let mut buf = FastcallResult::default();
RenderContext::fill_rect_raw(
wrapper,
&mut buf,
left,
top,
right - left,
bottom - top,
color,
);
}
/// Port of DisplayGfx::DrawOutlinedPixel (slot 17, 0x56BFD0).
///
/// Center pixel in `color_fg` with 4 cardinal neighbors in `color_bg`
/// (if non-zero).
pub unsafe extern "thiscall" fn draw_outlined_pixel(
this: *mut DisplayGfx,
x: i32,
y: i32,
color_fg: u32,
color_bg: i32,
) {
let cx = x + (*this).camera_x;
let cy = y + (*this).camera_y;
acquire_render_lock(this);
let layer = (*this).layer_0;
if color_bg != 0 {
let bg = color_bg as u8;
DisplayBitGrid::put_pixel_clipped_raw(layer, cx - 1, cy, bg);
DisplayBitGrid::put_pixel_clipped_raw(layer, cx + 1, cy, bg);
DisplayBitGrid::put_pixel_clipped_raw(layer, cx, cy - 1, bg);
DisplayBitGrid::put_pixel_clipped_raw(layer, cx, cy + 1, bg);
}
DisplayBitGrid::put_pixel_clipped_raw(layer, cx, cy, color_fg as u8);
}
/// Port of DisplayGfx::DrawCrosshair (slot 16, 0x56BE80).
///
/// 2Γ—2 foreground block with an 8-pixel `color_bg` outline:
///
/// ```text
/// bg bg
/// bg FG FG bg
/// bg FG FG bg
/// bg bg
/// ```
pub unsafe extern "thiscall" fn draw_crosshair(
this: *mut DisplayGfx,
x: i32,
y: i32,
color_fg: u32,
color_bg: u32,
) {
let cx = x + (*this).camera_x;
let cy = y + (*this).camera_y;
acquire_render_lock(this);
let layer = (*this).layer_0;
let bg = color_bg as u8;
let fg = color_fg as u8;
DisplayBitGrid::put_pixel_clipped_raw(layer, cx, cy - 1, bg);
DisplayBitGrid::put_pixel_clipped_raw(layer, cx + 1, cy - 1, bg);
DisplayBitGrid::put_pixel_clipped_raw(layer, cx - 1, cy, bg);
DisplayBitGrid::put_pixel_clipped_raw(layer, cx + 2, cy, bg);
DisplayBitGrid::put_pixel_clipped_raw(layer, cx - 1, cy + 1, bg);
DisplayBitGrid::put_pixel_clipped_raw(layer, cx + 2, cy + 1, bg);
DisplayBitGrid::put_pixel_clipped_raw(layer, cx, cy + 2, bg);
DisplayBitGrid::put_pixel_clipped_raw(layer, cx + 1, cy + 2, bg);
DisplayBitGrid::put_pixel_clipped_raw(layer, cx, cy, fg);
DisplayBitGrid::put_pixel_clipped_raw(layer, cx + 1, cy, fg);
DisplayBitGrid::put_pixel_clipped_raw(layer, cx, cy + 1, fg);
DisplayBitGrid::put_pixel_clipped_raw(layer, cx + 1, cy + 1, fg);
}
/// `PixelWriter` adapter over a raw `*mut DisplayBitGrid`.
struct BitGridWriter(*mut DisplayBitGrid);
impl line_draw::PixelWriter for BitGridWriter {
#[inline]
fn put_pixel_clipped(&mut self, x: i32, y: i32, color: u8) {
unsafe { DisplayBitGrid::put_pixel_clipped_raw(self.0, x, y, color) }
}
#[inline]
fn fill_hline(&mut self, x1: i32, x2: i32, y: i32, color: u8) {
unsafe { DisplayBitGrid::fill_hline_raw(self.0, x1, x2, y, color) }
}
#[inline]
fn clip_left(&self) -> i32 {
unsafe { (*self.0).clip_left as i32 }
}
#[inline]
fn clip_top(&self) -> i32 {
unsafe { (*self.0).clip_top as i32 }
}
#[inline]
fn clip_right(&self) -> i32 {
unsafe { (*self.0).clip_right as i32 }
}
#[inline]
fn clip_bottom(&self) -> i32 {
unsafe { (*self.0).clip_bottom as i32 }
}
}
/// Port of DisplayGfx::DrawLine (slot 13, 0x56BDB0).
pub unsafe extern "thiscall" fn draw_line(
this: *mut DisplayGfx,
x1: Fixed,
y1: Fixed,
x2: Fixed,
y2: Fixed,
color1: u32,
color2: u32,
) {
let cam_x = Fixed::from_int((*this).camera_x);
let cam_y = Fixed::from_int((*this).camera_y);
acquire_render_lock(this);
let mut writer = BitGridWriter((*this).layer_0);
line_draw::draw_line_two_color(
&mut writer,
x1 + cam_x,
y1 + cam_y,
x2 + cam_x,
y2 + cam_y,
color1 as u8,
color2 as u8,
);
}
/// Port of DisplayGfx::DrawLineClipped (slot 14, 0x56BD50).
pub unsafe extern "thiscall" fn draw_line_clipped(
this: *mut DisplayGfx,
x1: Fixed,
y1: Fixed,
x2: Fixed,
y2: Fixed,
color: u32,
) {
let cam_x = Fixed::from_int((*this).camera_x);
let cam_y = Fixed::from_int((*this).camera_y);
acquire_render_lock(this);
let mut writer = BitGridWriter((*this).layer_0);
line_draw::draw_line_clipped(
&mut writer,
x1 + cam_x,
y1 + cam_y,
x2 + cam_x,
y2 + cam_y,
color as u8,
);
}
/// Port of DisplayGfx::DrawPolyline (slot 12, 0x56BCC0).
///
/// Points are Fixed-point (x, y) pairs.
pub unsafe extern "thiscall" fn draw_polyline(
this: *mut DisplayGfx,
points: *mut i32,
count: i32,
color: u32,
) {
let cam_x = Fixed::from_int((*this).camera_x);
let cam_y = Fixed::from_int((*this).camera_y);
let n = count as usize;
if n == 0 || n > 256 {
return;
}
let mut verts = [Vertex::new(Fixed::ZERO, Fixed::ZERO); 256];
for (i, vert) in verts.iter_mut().enumerate().take(n) {
*vert = Vertex::new(
Fixed::from_raw(*points.add(i * 2)) + cam_x,
Fixed::from_raw(*points.add(i * 2 + 1)) + cam_y,
);
}
acquire_render_lock(this);
let mut writer = BitGridWriter((*this).layer_0);
line_draw::draw_polygon_filled(&mut writer, &verts[..n], color as u8);
}
/// Port of DisplayGfx::IsSpriteLoaded (slot 32, 0x56A480).
///
/// Checks all three sprite arrays (DisplayBase `sprite_ptrs`/`sprite_banks`
/// and DisplayGfx `sprite_table`).
pub unsafe extern "thiscall" fn is_sprite_loaded(this: *mut DisplayGfx, id: i32) -> u32 {
if !is_valid_sprite_id(id) {
return 0;
}
let base = &(*this).base;
if !base.sprite_ptrs[id as usize].is_null()
|| !base.sprite_banks[id as usize].is_null()
|| !(*this).sprite_table[id as usize].is_null()
{
1
} else {
0
}
}
/// Maximum valid font slot index β€” `font_table` has 32 entries, slot 0
/// is reserved (the original validates `1..=31` everywhere).
pub const MAX_FONT_ID: i32 = 31;
/// Sentinel bit on the `id` parameter of `load_sprite` / `load_sprite_by_layer`
/// meaning "already loaded β€” return success without doing any work".
const SPRITE_LOAD_ALREADY_DONE: u32 = 0x0080_0000;
/// Check if a sprite ID is in the valid range `[1, 0x3FF]`. Note that
/// the slot 33 dispatcher uses a slightly tighter bound (`1..=0x3FE`)
/// matching the original β€” see `get_sprite_frame_for_blit`.
#[inline]
fn is_valid_sprite_id(id: i32) -> bool {
(1..=0x3FF).contains(&id)
}
/// Check if `font_id` is a valid 1-based index into `font_table`.
#[inline]
fn is_valid_font_id(id: i32) -> bool {
(1..=MAX_FONT_ID).contains(&id)
}
/// Port of DisplayGfx::GetSpriteInfo (slot 6, 0x523500).
///
/// Returns the static "sprite" string (0x664170) on success, or 0 if
/// `layer` is out of range or no entry exists in `sprite_ptrs` /
/// `sprite_banks`.
pub unsafe extern "thiscall" fn get_sprite_info(
this: *mut DisplayGfx,
layer: i32,
out_data: *mut u32,
out_flags: *mut u32,
out_width: *mut u32,
) -> u32 {
if (layer as u32).wrapping_sub(1) > 0x3FE {
return 0;
}
let base = &(*this).base;
// Path 1: Sprite* in sprite_ptrs
let sprite = base.sprite_ptrs[layer as usize];
if !sprite.is_null() {
return sprite_info_from_sprite(sprite, out_data, out_flags, out_width);
}
// Path 2: SpriteBank* in sprite_banks
let bank = base.sprite_banks[layer as usize];
if !bank.is_null() {
return sprite_info_from_bank(bank, layer, out_data, out_flags, out_width);
}
0
}
/// Address of the static "sprite" string in WA.exe .rdata, returned as
/// a type-tag by `get_sprite_info` and friends.
const SPRITE_STRING: u32 = va::STR_SPRITE;
/// Port of Sprite__GetInfo (0x4FAEC0; usercall EAX=this, ESI=out_data,
/// ECX=out_width, stack=out_flags).
unsafe fn sprite_info_from_sprite(
sprite: *const Sprite,
out_data: *mut u32,
out_flags: *mut u32,
out_width: *mut u32,
) -> u32 {
let s = &*sprite;
if s.frame_meta_ptr.is_null() {
return 0;
}
*out_data = (s._unknown_08 as u32) | ((s.fps as u32) << 16);
// Ping-pong sprites (flags bit 1) report a doubled-minus-one width.
let mut width = s.max_frames as u32;
if s.flags & 2 != 0 {
width = width * 2 - 1;
}
*out_width = width;
*out_flags = (s.flags & 1) as u32;
crate::rebase::rb(SPRITE_STRING)
}
/// Port of SpriteBank__GetInfo (0x4F98C0; usercall EAX=layer, ECX=this,
/// ESI=out_width, stack=out_data+out_flags).
unsafe fn sprite_info_from_bank(
bank: *const SpriteBank,
layer: i32,
out_data: *mut u32,
out_flags: *mut u32,
out_width: *mut u32,
) -> u32 {
let b = &*bank;
if b.frame_table.is_null() {
return 0;
}
let entry_idx = *b.index_table.offset((layer - b.base_id) as isize);
if entry_idx < 0 || entry_idx >= b.frame_count {
return 0;
}
let frame = &*b.frame_table.add(entry_idx as usize);
*out_data = (frame.data_value as u32) << 8;
*out_flags = (frame.flags & 1) as u32;
// GetInfo's "API width" is read from `scale_or_count` (the +8 field),
// not the per-frame source width at +2 β€” see the original disassembly
// at 0x4F98C0 which reads `*(ushort *)(entry + 8)`.
if frame.scale_or_count & 0x8000 != 0 {
*out_width = 1;
} else if frame.flags & 2 != 0 {
*out_width = (frame.scale_or_count as u32) * 2 - 1;
} else {
*out_width = frame.scale_or_count as u32;
}
crate::rebase::rb(SPRITE_STRING)
}
/// Pure-Rust port of `Sprite__GetFrameForBlit` (0x4FAD30).
///
/// Original is usercall (`ESI=this, EAX=out_anim_frac, EDX=anim_value,
/// stack=(out_w, out_h, out_left, out_top, out_right, out_bottom)`); the
/// slot 33 dispatcher shuffles its args into this regular signature.
unsafe fn sprite_get_frame_for_blit(
sprite: *mut Sprite,
mut anim_value: u32,
out_anim_frac: *mut u32,
out_w: *mut i32,
out_h: *mut i32,
out_left: *mut i32,
out_top: *mut i32,
out_right: *mut i32,
out_bottom: *mut i32,
) -> *mut DisplayBitGrid {
let flags = (*sprite).flags;
// Clamp anim_value: bit 0 = use-as-is (truncate), else signed clamp.
if flags & 1 != 0 {
anim_value &= 0xFFFF;
} else {
let signed = anim_value as i32;
if signed < 0 {
anim_value = 0;
} else if signed >= 0x10000 {
anim_value = 0xFFFF;
}
}
// Ping-pong (bounce) iteration.
if flags & 2 != 0 {
anim_value &= 0xFFFF;
anim_value = anim_value.wrapping_mul(2);
if anim_value >= 0x10000 {
anim_value = 0x1FFFE - anim_value;
}
}
let frame_idx: u32 = if (*sprite).is_scaled != 0 {
// Scaled mode: Fixed16 lerp between scale_x/scale_y by anim_value,
// result written to *out_anim_frac. Original asm:
// `IMUL EDX; SHRD EAX, EDX, 0x10` (32Γ—32 β†’ 64 signed mul, >> 16).
let scale_x = (*sprite).scale_x as i32;
let scale_y = (*sprite).scale_y as i32;
let diff = scale_y.wrapping_sub(scale_x);
let prod = (diff as i64).wrapping_mul((anim_value as i32) as i64);
let interp = ((prod >> 16) as i32).wrapping_add(scale_x);
*out_anim_frac = interp as u32;
0
} else if (*sprite).frame_round_mode & 1 != 0 {
// Round-to-nearest, with `frame_idx == max_frames` wrapping to 0
// to avoid reading past the table.
let max_frames = (*sprite).max_frames as i32;
let prod = max_frames.wrapping_mul(anim_value as i32);
let f = (prod.wrapping_add(0x8000) >> 16) as u32;
*out_anim_frac = 0;
if f == max_frames as u32 {
0
} else {
f
}
} else {
let max_frames = (*sprite).max_frames as i32;
let prod = max_frames.wrapping_mul(anim_value as i32);
*out_anim_frac = 0;
(prod >> 16) as u32
};
let frame_meta = (*sprite).frame_meta_ptr.add(frame_idx as usize);
let start_x = (*frame_meta).start_x as i16 as i32;
let start_y = (*frame_meta).start_y as i16 as i32;
let end_x = (*frame_meta).end_x as i16 as i32;
let end_y = (*frame_meta).end_y as i16 as i32;
*out_left = start_x;
*out_top = start_y;
*out_right = end_x;
*out_bottom = end_y;
*out_w = (*sprite).width as i32;
*out_h = (*sprite).height as i32;
let frame_w = end_x - start_x;
let frame_h = end_y - start_y;
// Resolve surface address: flat (already-decoded pixels in the load
// buffer) or cached/decompressed (lazy via FrameCache + LZSS).
let surface_addr: *mut u8 = if (*sprite).header_flags & 0x4000 == 0 {
let bitmap_offset = (*frame_meta).bitmap_offset;
(*sprite).bitmap_data_ptr.add(bitmap_offset as usize)
} else {
// Cached path: bitmap_offset is split into a signed-byte
// subframe index (high byte) and a pixel offset (low 24 bits).
// The original uses `MOVSX byte ptr [EAX+EDI+3]` + a *12 lea β€”
// negative subframe indices index *backward* from the table base.
let bitmap_offset = (*frame_meta).bitmap_offset;
let subframe_idx_signed = ((bitmap_offset >> 24) as i8) as i32;
let entry = (*sprite)
.subframe_cache_table
.offset(subframe_idx_signed as isize);
if (*entry).decoded_ptr.is_null() {
let context_ptr = (*sprite).context_ptr;
let decoded_size = (*entry).decoded_size;
let decoded = frame_cache_allocate(
decoded_size,
context_ptr,
sprite as *mut core::ffi::c_void,
subframe_idx_signed as u32,
);
(*entry).decoded_ptr = decoded;
let src = (*sprite)
.bitmap_data_ptr
.add((*entry).compressed_offset as usize);
sprite_lzss_decode(decoded, src, (*sprite).palette_data_ptr);
}
let pixel_offset = (bitmap_offset & 0xFF_FFFF) as usize;
(*entry).decoded_ptr.add(pixel_offset)
};
// Update embedded bitgrid only if it owns an external buffer slot.
// Field write order copied from 0x4FAEA2..0x4FAEB7; equivalent to
// the shared `DisplayBitGrid::SetExternalBuffer` helper that the
// SpriteBank path calls.
let bitgrid = &raw mut (*sprite).bitgrid;
if (*bitgrid).external_buffer != 0 {
(*bitgrid).clip_bottom = frame_h as u32;
(*bitgrid).clip_right = frame_w as u32;
(*bitgrid).clip_top = 0;
(*bitgrid).clip_left = 0;
(*bitgrid).row_stride = frame_w as u32;
(*bitgrid).data = surface_addr;
(*bitgrid).height = frame_h as u32;
(*bitgrid).width = frame_w as u32;
}
bitgrid
}
/// Panic stub for `SpriteBank__GetFrameForBlit` (0x4F9710).
///
/// Structurally unreachable in shipping WA: the only `SpriteBank`
/// constructor is `SpriteBank__Constructor` (0x4F9450), reached only
/// via `LoadSpriteEx` (slot 30, 0x523310), which is itself trapped in
/// `install_display`. So `sprite_banks[id]` is always null and the bank
/// branch of slot 33 is dead code β€” confirmed by playing several turns
/// without firing this panic. A disassembly-derived port lived in
/// commit `973f234`; per the no-unverified-code rule it was deleted.
/// Revive from history if banks ever become live.
#[allow(clippy::too_many_arguments)]
unsafe fn sprite_bank_get_frame_for_blit(
_bank: *mut SpriteBank,
sprite_id: u32,
_anim_value: u32,
_out_anim_frac: *mut u32,
_out_w: *mut i32,
_out_h: *mut i32,
_out_left: *mut i32,
_out_top: *mut i32,
_out_right: *mut i32,
_out_bottom: *mut i32,
) -> *mut DisplayBitGrid {
panic!(
"SpriteBank::GetFrameForBlit reached for sprite_id={sprite_id} β€” \
banks were supposed to be unreachable. Revive the port from \
commit 973f234 and validate against 0x4F9710."
);
}
/// Port of `DisplayGfx::GetSpriteFrameForBlit` (slot 33, 0x5237C0).
///
/// Thin dispatcher: forwards `sprite_ptrs`-backed IDs to
/// [`sprite_get_frame_for_blit`] and `sprite_banks`-backed IDs to
/// [`sprite_bank_get_frame_for_blit`] (a panic stub).
#[allow(clippy::too_many_arguments)]
pub unsafe extern "thiscall" fn get_sprite_frame_for_blit(
this: *mut DisplayGfx,
sprite_id: u32,
anim_value: u32,
out_w: *mut i32,
out_h: *mut i32,
out_left: *mut i32,
out_top: *mut i32,
out_right: *mut i32,
out_bottom: *mut i32,
out_anim_frac: *mut u32,
) -> *mut DisplayBitGrid {
if sprite_id.wrapping_sub(1) >= 0x3FE {
return core::ptr::null_mut();
}
let base = &mut (*this).base;
let sprite = base.sprite_ptrs[sprite_id as usize];
if !sprite.is_null() {
return sprite_get_frame_for_blit(
sprite,
anim_value,
out_anim_frac,
out_w,
out_h,
out_left,
out_top,
out_right,
out_bottom,
);
}
let bank = base.sprite_banks[sprite_id as usize];
if !bank.is_null() {
return sprite_bank_get_frame_for_blit(
bank,
sprite_id,
anim_value,
out_anim_frac,
out_w,
out_h,
out_left,
out_top,
out_right,
out_bottom,
);
}
core::ptr::null_mut()
}
/// Port of DisplayGfx::DrawViaCallback (slot 21, 0x56B7C0).
///
/// Calls `obj->vtable[2](layer_0, pixel_x, pixel_y, p5, p6)` with
/// camera-adjusted coordinates.
pub unsafe extern "thiscall" fn draw_via_callback(
this: *mut DisplayGfx,
x: Fixed,
y: Fixed,
obj: *mut u8,
p5: u32,
p6: u32,
) {
acquire_render_lock(this);
let pixel_x = (Fixed::from_int((*this).camera_x) + x).to_int();
let pixel_y = (Fixed::from_int((*this).camera_y) + y).to_int();
let layer_0 = (*this).layer_0;
let vtable = *(obj as *const *const u32);
let callback: unsafe extern "thiscall" fn(*mut u8, *mut DisplayBitGrid, i32, i32, u32, u32) =
core::mem::transmute(*vtable.add(2));
callback(obj, layer_0, pixel_x, pixel_y, p5, p6);
}
/// Port of DisplayGfx::DrawTiledTerrain (slot 22, 0x56C5A0).
///
/// Tiles `tile_bitmap_sets[1]`'s bitmaps in a row-major grid. `count`
/// limits how many pixel-rows are rendered. `flags` low 16 bits must be 1
/// (only supported mode); bit 19 controls a blit transparency flag.
pub unsafe extern "thiscall" fn draw_tiled_terrain(
this: *mut DisplayGfx,
x: Fixed,
y: Fixed,
mut count: i32,
flags: u32,
) {
if count <= 0 {
return;
}
let total_height = (*this).tile_total_height;
if count > total_height {
count = total_height;
}
if (flags & 0xFFFF) != 1 {
return;
}
let tile_set = (*this).tile_bitmap_sets[1];
if tile_set.is_null() {
return;
}
let bitmap_array = (*tile_set).bitmap_ptrs;
let pixel_x = (*this).camera_x + x.to_int();
let pixel_y = (*this).camera_y + y.to_int();
let blit_flags = (!flags >> 19) & 2;
let total_width = (*this).tile_total_width;
let col_width = (*this).tile_col_width;
let row_height = (*this).tile_row_height;
let mut bitmap_idx = 0u32;
let mut y_offset = 0i32;
if total_height <= 0 {
return;
}
while y_offset < total_height {
if y_offset >= count {
return;
}
let mut row_h = row_height;
if count - y_offset < row_height {
row_h = count - y_offset;
}
let mut x_offset = 0i32;
while x_offset < total_width {
let col_w = col_width.min(total_width - x_offset);
let bitmap_ptr = *bitmap_array.add(bitmap_idx as usize);
blit_bitmap_clipped_native(
this,
pixel_x + x_offset,
pixel_y + y_offset,
col_w,
row_h,
bitmap_ptr,
blit_flags,
);
bitmap_idx += 1;
x_offset += col_width;
}
y_offset += row_height;
}
}
/// Port of DisplayGfx::DrawScaledSprite (slot 20, 0x56B660).
///
/// The source rect is `(src_x, src_y)..(src_w, src_h)`. Flags select
/// the blit mode:
/// - bit 20: 0 = ColorTable (transparency), 1 = Copy (opaque)
/// - 0x200000: additive blend via `color_add_table` LUT
/// - 0x4000000: color blend via `color_blend_table` LUT
/// - 0x8000000 / 0x10000000: stippled (checkerboard) blit
pub unsafe fn draw_scaled_sprite(
this: *mut DisplayGfx,
x: Fixed,
y: Fixed,
sprite: *mut DisplayBitGrid,
src_x: i32,
src_y: i32,
src_w: i32,
src_h: i32,
flags: u32,
) -> DrawScaledSpriteResult {
let width = src_w - src_x;
let height = src_h - src_y;
// Signed division rounding toward zero.
let half_w = if width < 0 {
(width + 1) / 2
} else {
width / 2
};
let half_h = if height < 0 {
(height + 1) / 2
} else {
height / 2
};
let dst_x = (*this).camera_x - half_w + (x.0 >> 16);
let dst_y = (*this).camera_y - half_h + (y.0 >> 16);
let blend_mode = (!(flags >> 20)) & 1;
if (flags & 0x8000000) != 0 || (flags & 0x10000000) != 0 {
let stipple_mode: u32 = if (flags & 0x10000000) != 0 { 1 } else { 0 };
return DrawScaledSpriteResult::Stippled {
layer: (*this).layer_0,
dst_x,
dst_y,
width,
height,
sprite,
src_x,
src_y,
stipple_mode,
};
}
let color_table: *const u8 = if (flags & 0x200000) != 0 {
(*this).color_add_table.as_ptr()
} else if (flags & 0x4000000) != 0 {
(*this).color_blend_table.as_ptr()
} else {
core::ptr::null()
};
if width <= 0 || height <= 0 {
return DrawScaledSpriteResult::Handled;
}
acquire_render_lock(this);
let layer = (*this).layer_0;
// Core blit flags: low 16 bits = blend mode (0 = Copy, 1 = ColorTable).
let blit_flags = blend_mode;
DrawScaledSpriteResult::Blit {
layer,
dst_x,
dst_y,
width,
height,
sprite,
src_x,
src_y,
color_table,
blit_flags,
}
}
/// Result of `draw_scaled_sprite`'s coordinate / mode resolution. The
/// actual blit is performed by the DLL hook layer, which has access to
/// `blit_impl` (the bridge from `DisplayBitGrid` to `PixelGrid`).
pub enum DrawScaledSpriteResult {
/// Blit should be performed with these parameters.
Blit {
layer: *mut DisplayBitGrid,
dst_x: i32,
dst_y: i32,
width: i32,
height: i32,
sprite: *mut DisplayBitGrid,
src_x: i32,
src_y: i32,
color_table: *const u8,
blit_flags: u32,
},
/// Stippled (checkerboard) blit β€” caller should use blit_stippled_raw.
Stippled {
layer: *mut DisplayBitGrid,
dst_x: i32,
dst_y: i32,
width: i32,
height: i32,
sprite: *mut DisplayBitGrid,
src_x: i32,
src_y: i32,
stipple_mode: u32,
},
/// Already handled (e.g. zero-size, early out).
Handled,
}
/// Port of DisplayGfx::DrawPixelStrip (slot 15, 0x56BE10).
pub unsafe extern "thiscall" fn draw_pixel_strip(
this: *mut DisplayGfx,
x: Fixed,
y: Fixed,
dx: Fixed,
dy: Fixed,
count: i32,
color: u32,
) {
let mut cx = Fixed::from_int((*this).camera_x) + x;
let mut cy = Fixed::from_int((*this).camera_y) + y;
acquire_render_lock(this);
let layer = (*this).layer_0;
if count >= 0 {
for _ in 0..=count {
DisplayBitGrid::put_pixel_clipped_raw(layer, cx.to_int(), cy.to_int(), color as u8);
cx += dx;
cy += dy;
}
}
}
/// Port of DisplayGfx::SetLayerColor (slot 4, 0x5231E0).
///
/// Allocates a `PaletteContext` for `layer` (1-3) if one doesn't exist,
/// claiming `color` consecutive entries in the slot table.
pub unsafe extern "thiscall" fn set_layer_color(this: *mut DisplayGfx, layer: i32, color: i32) {
let Some(layer) = Layer::try_from_i32(layer) else {
return;
};
if !(*this).base.layer_contexts[layer.idx()].is_null() {
return;
}
let start = palette_slot_alloc(&mut (*this).base, color);
// PaletteContext is 0x72C bytes; only the first 0x70C is zeroed.
let ctx = crate::wa_alloc::wa_malloc(0x72C);
core::ptr::write_bytes(ctx, 0, 0x70C);
let result = if ctx.is_null() {
core::ptr::null_mut()
} else {
let ctx = ctx as *mut PaletteContext;
palette_context_init(ctx, start as i16, (start as i32 + color - 1) as i16);
ctx
};
(*this).base.layer_contexts[layer.idx()] = result;
(*this).base.layer_visibility[layer.idx()] = 0;
}
/// Palette slot allocator β€” port of FUN_00523190.
///
/// Scans the 257-entry table `slot_table_guard(0) + slot_table[1..=255] +
/// slot_table_sentinel(-1)` for `count` consecutive available (non-zero)
/// entries, zeroes them, and returns the start index. Returns -1 on
/// failure (the sentinel is hit before `count` consecutive slots).
unsafe fn palette_slot_alloc(base: &mut DisplayBase<*const DisplayGfxVtable>, count: i32) -> i32 {
let count = count as usize;
let table = &base.slot_table_guard as *const u32;
let total = 1 + 0xFF + 1;
let mut consecutive = 0usize;
let mut scan = 0usize;
loop {
if consecutive == count {
let start = scan - count;
for i in start..scan {
*(table.add(i) as *mut u32) = 0;
}
return start as i32;
}
if scan >= total {
return -1;
}
let val = *table.add(scan) as i32;
scan += 1;
if val == 0 {
consecutive = 0;
} else if val < 0 {
return -1;
} else {
consecutive += 1;
}
}
}
/// Initialize a PaletteContext with a palette index range β€”
/// port of FUN_00541170 + FUN_005411A0.
unsafe fn palette_context_init(ctx: *mut PaletteContext, range_min: i16, range_max: i16) {
(*ctx).dirty_range_min = range_min;
(*ctx).dirty_range_max = range_max;
let range_size = range_max - range_min + 1;
(*ctx).cache_count = 0;
(*ctx).free_count = range_size;
// Fill free_stack with [range_max, range_max-1, ..., range_min].
if range_size > 0 {
for i in 0..range_size as usize {
(*ctx).free_stack[i] = (range_max as u8).wrapping_sub(i as u8);
}
}
(*ctx).cache_iter = 0;
core::ptr::write_bytes((*ctx).in_use.as_mut_ptr(), 0, 256);
(*ctx).dirty = 0;
}
/// Port of DisplayGfx::SetActiveLayer (slot 5, 0x523270).
///
/// Returns the `PaletteContext*` for `layer` (1-3), or null if out of
/// range. Used as palette data input for `update_palette`.
pub unsafe extern "thiscall" fn set_active_layer(
this: *mut DisplayGfx,
layer: i32,
) -> *mut PaletteContext {
match Layer::try_from_i32(layer) {
Some(layer) => (*this).base.layer_contexts[layer.idx()],
None => core::ptr::null_mut(),
}
}
/// Port of DisplayGfx::UpdatePalette (slot 24, 0x56A610).
///
/// Copies the indices listed in `palette_ctx.cache` from its `rgb_table`
/// into DisplayGfx's `palette_entries`. Pushes the result to the DDraw
/// surface palette via [`palette_commit`] if `commit != 0`.
pub unsafe extern "thiscall" fn update_palette(
this: *mut DisplayGfx,
palette_ctx: *mut PaletteContext,
commit: i32,
) {
let ctx = &mut *palette_ctx;
ctx.cache_iter = 0;
if ctx.cache_count <= 0 {
return;
}
let dirty_min = ctx.dirty_range_min as i32;
let dirty_max = ctx.dirty_range_max as i32;
ctx.cache_iter = 1;
let mut idx = ctx.cache[0] as usize;
loop {
// rgb_table[idx] is a packed u32: low 3 bytes = R, G, B.
let rgb = ctx.rgb_table[idx].to_le_bytes();
(*this).palette_entries[idx * 4] = rgb[0];
(*this).palette_entries[idx * 4 + 1] = rgb[1];
(*this).palette_entries[idx * 4 + 2] = rgb[2];
(*this).palette_entries[idx * 4 + 3] = 0;
if ctx.cache_iter >= ctx.cache_count {
break;
}
idx = ctx.cache[ctx.cache_iter as usize] as usize;
ctx.cache_iter += 1;
}
// Expand the dirty palette range to cover this update.
if ((*this).palette_dirty_min as i32) > dirty_min {
(*this).palette_dirty_min = dirty_min as u32;
}
if ((*this).palette_dirty_max as i32) < dirty_max {
(*this).palette_dirty_max = dirty_max as u32;
}
if commit != 0 {
palette_commit(this);
(*this).palette_dirty_min = 0x100;
(*this).palette_dirty_max = 0xFFFF_FFFF;
}
}
/// Call WA's palette commit function (0x56CD20). Usercall:
/// `EAX=dirty_min, EDX=dirty_max, stack=this (DisplayGfx*)`.
unsafe fn palette_commit(gfx: *mut DisplayGfx) {
let dirty_min = (*gfx).palette_dirty_min;
let dirty_max = (*gfx).palette_dirty_max;
palette_commit_bridge(gfx, dirty_min, dirty_max, crate::rebase::rb(0x0056_CD20));
}
#[unsafe(naked)]
unsafe extern "cdecl" fn palette_commit_bridge(
_gfx: *mut DisplayGfx,
_dirty_min: u32,
_dirty_max: u32,
_target: u32,
) {
core::arch::naked_asm!(
"mov eax, [esp + 8]", // dirty_min
"mov edx, [esp + 12]", // dirty_max
"push dword ptr [esp + 4]", // gfx (this)
"call dword ptr [esp + 20]", // target (+4 from our push, +16 from original)
"ret",
);
}
/// Port of DisplayGfx::SetLayerVisibility (slot 23, 0x56A5D0).
///
/// Updates the palette from the layer's context (if it exists) and
/// clears the layer's visibility flag when `visible < 0`.
pub unsafe extern "thiscall" fn set_layer_visibility(
this: *mut DisplayGfx,
layer: i32,
visible: i32,
) {
let Some(layer) = Layer::try_from_i32(layer) else {
return;
};
let layer_ctx = (*this).base.layer_contexts[layer.idx()];
if !layer_ctx.is_null() {
update_palette(this, layer_ctx, visible);
}
if visible < 0 {
(*this).base.layer_visibility[layer.idx()] = 0;
}
}
// =========================================================================
// Sprite loading methods
// =========================================================================
/// Pure-Rust port of `ConstructSprite` (0x4FAA30). The caller must
/// pre-zero the rest of the `Sprite` allocation.
pub unsafe fn construct_sprite(sprite: *mut Sprite, sprite_cache: *mut SpriteCache) {
use crate::bitgrid::{BitGridDisplayVtable, BIT_GRID_DISPLAY_VTABLE};
use crate::rebase::rb;
(*sprite).vtable = rb(va::SPRITE_VTABLE) as *const SpriteVtable;
(*sprite).context_ptr = sprite_cache;
(*sprite).bitgrid.vtable = rb(BIT_GRID_DISPLAY_VTABLE) as *const BitGridDisplayVtable;
(*sprite).bitgrid.external_buffer = 1;
(*sprite).bitgrid.cells_per_unit = 8;
}
/// Port of DisplayGfx::LoadSprite (slot 31, 0x523400).
///
/// `flag != 0` clamps `max_frames` to the low 16 bits of `id` and uses
/// the high 16 bits to seed the per-sprite frame round mode bytes.
pub unsafe fn load_sprite(
this: *mut DisplayGfx,
layer: u32,
id: u32,
flag: u32,
gfx_dir: *mut GfxDir,
_name: *const c_char,
load_sprite_from_vfs: unsafe extern "cdecl" fn(
sprite: *mut Sprite,
gfx_dir: *mut GfxDir,
name: *const c_char,
layer_ctx: *mut PaletteContext,
) -> i32,
) -> i32 {
if id & SPRITE_LOAD_ALREADY_DONE != 0 {
return 1;
}
let Some(layer) = Layer::try_from_u32(layer) else {
return 0;
};
let base = &mut (*this).base;
let layer_ctx = base.layer_contexts[layer.idx()];
if layer_ctx.is_null() {
return 0;
}
if !is_valid_sprite_id(id as i32) {
return 0;
}
if is_sprite_loaded(this, id as i32) != 0 {
return 0;
}
let sprite = wa_malloc_struct_zeroed::<Sprite>();
if sprite.is_null() {
return 0;
}
construct_sprite(sprite, base.sprite_cache);
let result = load_sprite_from_vfs(sprite, gfx_dir, _name, layer_ctx);
if result == 0 {
if !sprite.is_null() {
let dtor = (*(*sprite).vtable).destructor;
dtor(sprite, 1);
}
return 0;
}
let base = &mut (*this).base;
base.sprite_ptrs[id as usize] = sprite;
base.sprite_layers[id as usize] = layer.as_u32();
base.layer_visibility[layer.idx()] += 1;
if flag != 0 {
let sprite = base.sprite_ptrs[id as usize];
let id_u16 = id as u16;
if id_u16 != 0 && id_u16 < (*sprite).max_frames {
(*sprite).max_frames = id_u16;
}
// Original splits the high word of `id` into the rounding-mode
// byte (bit 0 picks the round-to-nearest path in
// `Sprite__GetFrameForBlit`) and an adjacent unknown byte.
(*sprite).frame_round_mode = (id >> 16) as u8;
(*sprite)._unknown_19 = (id >> 24) as u8;
}
1
}
/// Port of FUN_005733B0 (`LoadSpriteByName`; original is usercall
/// `EDI=sprite, ECX=gfx_dir, stack=(palette_ctx, name), RET 0x8`).
///
/// Reads the sprite header, palette, and frame pixel data from a `.dir`
/// archive stream. In headless mode (`g_DisplayModeFlag != 0`) skips all
/// surface creation.
pub unsafe fn load_sprite_by_name(
sprite: *mut LayerSprite,
gfx_dir: *mut GfxDir,
palette_ctx: *mut PaletteContext,
name: *const c_char,
) -> i32 {
use crate::address::va;
use crate::asset::gfx_dir::{gfx_dir_load_image, GfxDirStream};
use crate::rebase::rb;
use crate::render::display::context::{FastcallResult, RenderContext, Surface};
use crate::render::palette::{palette_map_color, remap_pixels_through_lut};
use crate::wa_alloc::wa_malloc;
// Copy name into sprite.name (max 0x4F chars + null terminator).
let name_dest = (*sprite).name.as_mut_ptr();
let mut i = 0usize;
while i < 0x4F {
let ch = *name.add(i);
*name_dest.add(i) = ch as u8;
if ch == 0 {
break;
}
i += 1;
}
*name_dest.add(i.min(0x4F)) = 0;
(*sprite).gfx_dir = gfx_dir;
(*sprite).palette_ctx = palette_ctx;
let stream = gfx_dir_load_image(gfx_dir, name);
if stream.is_null() {
return 0;
}
let display_mode_flag = *(rb(va::G_DISPLAY_MODE_FLAG) as *const u8);
if display_mode_flag == 0 {
// The original calls remaining() and discards the result.
GfxDirStream::bytes_consumed_raw(stream);
// Read the .spr header as 4+4+2+2 separate calls β€” matching the
// original's read granularity may matter for internal stream state.
let mut hdr4 = [0u8; 4];
GfxDirStream::read_raw(stream, hdr4.as_mut_ptr(), 4); // unused/version
GfxDirStream::read_raw(stream, hdr4.as_mut_ptr(), 4); // data_size
let mut header_flags: u16 = 0;
GfxDirStream::read_raw(stream, &mut header_flags as *mut u16 as *mut u8, 2);
let mut palette_count: u32 = 0;
GfxDirStream::read_raw(stream, &mut palette_count as *mut u32 as *mut u8, 2);
// Build palette LUT: bulk-read all RGB triplets then iterate.
// Palette entry 0 is always transparent β€” the file's RGB data
// defines entries 1..=palette_count, not entry 0. So
// palette_data[0..3] maps to lut[1], not lut[0].
let mut palette_lut = [0u8; 256];
let lut_count = (palette_count as usize).min(256);
let bulk_size = palette_count as usize * 3;
let mut palette_data = [0u8; 768];
GfxDirStream::read_raw(stream, palette_data.as_mut_ptr(), bulk_size as u32);
palette_lut[0] = 0;
for idx in 0..lut_count {
let r = palette_data[idx * 3];
let g = palette_data[idx * 3 + 1];
let b = palette_data[idx * 3 + 2];
if r == 0 && g == 0 && b == 0 {
palette_lut[idx + 1] = 0;
} else {
let rgb = (r as u32) | ((g as u32) << 8) | ((b as u32) << 16);
let mapped = palette_map_color(palette_ctx, rgb);
palette_lut[idx + 1] = mapped as u8;
}
}
// Sprite metadata fields, in the original's read order
// (field_60 before flags/cell_width/cell_height).
GfxDirStream::read_raw(stream, &raw mut (*sprite).field_60 as *mut u8, 4);
GfxDirStream::read_raw(stream, &raw mut (*sprite).flags as *mut u8, 2);
GfxDirStream::read_raw(stream, &raw mut (*sprite).cell_width as *mut u8, 2);
GfxDirStream::read_raw(stream, &raw mut (*sprite).cell_height as *mut u8, 2);
(*sprite).frame_count = 0;
GfxDirStream::read_raw(stream, &raw mut (*sprite).frame_count as *mut u8, 2);
let frame_count = (*sprite).frame_count as usize;
// Counted `LayerSpriteFrame` array: `count * 0x14` bytes plus a
// 4-byte count prefix at `[-4]`. Saturate the allocation size on
// overflow to match the original's checked-mul behavior.
const LSF_SIZE: u32 = core::mem::size_of::<LayerSpriteFrame>() as u32;
let checked_count = frame_count as u32;
let checked_size = checked_count.checked_mul(LSF_SIZE).unwrap_or(u32::MAX);
let checked_alloc = checked_size.checked_add(4).unwrap_or(u32::MAX);
let array_base = wa_malloc(checked_alloc);
let frame_array: *mut LayerSpriteFrame = if !array_base.is_null() {
*(array_base as *mut u32) = checked_count;
let arr = array_base.add(4) as *mut LayerSpriteFrame;
let cbitmap_vt = rb(va::CBITMAP_VTABLE_MAYBE) as *const core::ffi::c_void;
for j in 0..frame_count {
let elem = arr.add(j);
(*elem).bitmap_vtable = cbitmap_vt;
(*elem).surface = core::ptr::null_mut();
}
arr
} else {
core::ptr::null_mut()
};
(*sprite).frame_array = frame_array;
// Skip alignment padding: while (remaining() & 3) != 0, read 1 byte.
loop {
let remaining = GfxDirStream::bytes_consumed_raw(stream);
if remaining & 3 == 0 {
break;
}
let mut dummy = 0u8;
GfxDirStream::read_raw(stream, &mut dummy, 1);
}
// Frame headers: 4-byte discarded prefix, then start_x/y, end_x/y.
if frame_count > 0 && !frame_array.is_null() {
for j in 0..frame_count {
let frame = frame_array.add(j);
let mut frame_hdr = [0u8; 4];
GfxDirStream::read_raw(stream, frame_hdr.as_mut_ptr(), 4);
GfxDirStream::read_raw(stream, &raw mut (*frame).start_x as *mut u8, 2);
GfxDirStream::read_raw(stream, &raw mut (*frame).start_y as *mut u8, 2);
GfxDirStream::read_raw(stream, &raw mut (*frame).end_x as *mut u8, 2);
GfxDirStream::read_raw(stream, &raw mut (*frame).end_y as *mut u8, 2);
}
}
// Per-frame surface creation + pixel data load.
if frame_count > 0 && !frame_array.is_null() {
let render_ctx = *(rb(va::G_RENDER_CONTEXT) as *const *mut RenderContext);
for j in 0..frame_count {
let frame = frame_array.add(j);
let width = ((*frame).end_x - (*frame).start_x) as i32;
let height = ((*frame).end_y - (*frame).start_y) as i32;
if width * height == 0 {
continue;
}
// alloc_surface returns the surface pointer in EAX, NOT via
// the FastcallResult buffer β€” see feedback_alloc_surface_return.md.
if (*frame).surface.is_null() {
let mut buf = FastcallResult::default();
let ret = RenderContext::alloc_surface_raw(render_ctx, &mut buf);
(*frame).surface = ret as *mut Surface;
}
let surface = (*frame).surface;
if surface.is_null() {
continue;
}
let mut buf = FastcallResult::default();
Surface::init_surface_raw(surface, &mut buf, width, height, 0);
Surface::set_color_key_raw(surface, &mut buf, 0, 0x10);
let mut data_ptr: *mut u8 = core::ptr::null_mut();
let mut pitch: i32 = 0;
Surface::lock_surface_raw(surface, &mut buf, &mut data_ptr, &mut pitch);
if !data_ptr.is_null() && pitch != 0 {
for row in 0..height {
let row_dest = data_ptr.add((row * pitch) as usize);
GfxDirStream::read_raw(stream, row_dest, width as u32);
}
let width_dwords = ((width as u32) + 3) / 4;
remap_pixels_through_lut(
data_ptr,
pitch as u32,
palette_lut.as_ptr(),
width_dwords,
height as u32,
);
}
Surface::unlock_surface_raw(surface, &mut buf, data_ptr);
}
}
}
GfxDirStream::destroy_raw(stream);
1
}
/// Port of FUN_0056A2F0 (`FreeLayerSprite`; usercall EDI=sprite).
pub unsafe fn free_layer_sprite(sprite: *mut LayerSprite) {
use crate::wa_alloc::wa_free;
let frame_array = (*sprite).frame_array;
if !frame_array.is_null() {
// Counted array: count lives at `frame_array[-4]`.
let count_ptr = (frame_array as *mut u32).sub(1);
let count = *count_ptr as usize;
// Reverse-order destruction matches eh_vector_destructor_iterator.
for i in (0..count).rev() {
let frame = frame_array.add(i);
let surface = (*frame).surface;
if !surface.is_null() {
// Surface destructor is vtable[0]; not yet a typed slot.
let vt = (*surface).vtable as *const usize;
let dtor: unsafe extern "thiscall" fn(*mut Surface, u32) =
core::mem::transmute(*vt);
dtor(surface, 1);
}
}
wa_free(count_ptr);
}
wa_free(sprite);
}
/// Port of DisplayGfx::LoadSpriteByLayer (slot 37, 0x56A4C0).
///
/// Simplified sprite loading that stores into `DisplayGfx::sprite_table`
/// (+0x3DD4) instead of `DisplayBase::sprite_ptrs`. Unlike `load_sprite`
/// it does NOT call `construct_sprite`.
pub unsafe fn load_sprite_by_layer(
this: *mut DisplayGfx,
layer: u32,
id: u32,
gfx_dir: *mut GfxDir,
name: *const c_char,
) -> i32 {
use crate::wa_alloc::wa_malloc_zeroed;
if id & SPRITE_LOAD_ALREADY_DONE != 0 {
return 1;
}
let palette_ctx = set_active_layer(this, layer as i32);
if !is_valid_sprite_id(id as i32) {
return 0;
}
if is_sprite_loaded(this, id as i32) != 0 {
return 1;
}
// 0x70 bytes + 0x20 trailing guard, matching WA_MallocMemset.
let sprite = wa_malloc_zeroed(0x90) as *mut LayerSprite;
if sprite.is_null() {
return 0;
}
(*sprite).display_gfx = this;
(*sprite).frame_count = 0;
(*sprite).frame_array = core::ptr::null_mut();
(*sprite).gfx_dir = core::ptr::null_mut();
let result = load_sprite_by_name(sprite, gfx_dir, palette_ctx, name);
if result == 0 {
free_layer_sprite(sprite);
return 0;
}
(*this).sprite_table[id as usize] = sprite;
1
}
/// Port of `DisplayGfx::LoadFont` (slot 34, 0x523560).
///
/// `layer` is the WA "mode" parameter, but it's the same value space as
/// the layer index everywhere else (indexes `layer_contexts[1..=3]` and
/// `layer_visibility[1..=3]`). Shipping WA only ever passes `1`.
///
/// On load failure the partially-initialized font object is leaked, to
/// match the original's call to an unported sprite-bank-style cleanup
/// helper (`FUN_005230C0`).
pub unsafe fn load_font(
this: *mut DisplayGfx,
layer: u32,
font_id: i32,
gfx_dir: *mut GfxDir,
filename: *const c_char,
) -> u32 {
use crate::wa_alloc::wa_malloc_struct_zeroed;
let Some(layer) = Layer::try_from_u32(layer) else {
return 0;
};
let base = &mut (*this).base;
let layer_ctx = base.layer_contexts[layer.idx()];
if layer_ctx.is_null() {
return 0;
}
if !is_valid_font_id(font_id) {
return 0;
}
if !base.font_table[font_id as usize].is_null() {
return 0;
}
let font_obj = wa_malloc_struct_zeroed::<Font>();
if font_obj.is_null() {
return 0;
}
let result = font_load_from_gfx(font_obj, gfx_dir, layer_ctx, filename);
if result == 0 {
// Leak on failure to match the original's behavior.
return 0;
}
base.font_table[font_id as usize] = font_obj;
base.font_layers[font_id as usize] = layer.as_u32();
base.layer_visibility[layer.idx()] += 1;
1
}
/// Port of `DisplayGfx::LoadFontExtension` (slot 35, 0x523620).
pub unsafe fn load_font_extension(
this: *mut DisplayGfx,
font_id: i32,
path: *const c_char,
char_map: *const c_char,
palette_value: u32,
_flag: i32,
) -> u32 {
use crate::render::palette::palette_context_lookup_entry;
if !is_valid_font_id(font_id) {
return 0;
}
let base = &mut (*this).base;
let font_obj_addr = base.font_table[font_id as usize];
if font_obj_addr.is_null() {
return 0;
}
let font_obj = font_obj_addr as *mut Font;
// The original ALWAYS resolves the RGB through layer_contexts[1],
// regardless of which layer owns the font. (Disasm at 0x52364D:
// `MOV ECX, [EDI+0x3120]` = `layer_contexts[1]`.)
let layer1_ctx = base.layer_contexts[Layer::ONE.idx()];
let mut resolved_rgb: u32 = 0;
let _ = palette_context_lookup_entry(layer1_ctx, palette_value as i32, &mut resolved_rgb);
// For the extension call we use the font's *owning* layer's palette
// context. The original reads `layer_contexts[font_layers[font_id]]`
// without validation, so a zero font_layers entry indexes
// `layer_contexts[0]` (always null). We preserve that exact lookup.
let layer_idx = base.font_layers[font_id as usize] as usize;
let layer_ctx = base.layer_contexts[layer_idx];
font_extend(font_obj, layer_ctx, path, char_map, resolved_rgb);
1
}
/// Port of `DisplayGfx::GetFontInfo` (slot 8, 0x523790).
///
/// `out_1` = max metric, `out_2` = font max width β€” the original passes
/// them via EDI/EDX respectively.
pub unsafe extern "thiscall" fn get_font_info(
this: *mut DisplayGfx,
font_id: i32,
out_1: *mut u32,
out_2: *mut u32,
) -> u32 {
if !is_valid_font_id(font_id) {
return 0;
}
let font_obj = (*this).base.font_table[font_id as usize] as *const Font;
if font_obj.is_null() {
return 0;
}
font_get_info_impl(font_obj, out_1 as *mut i32, out_2 as *mut i32)
}
/// Port of `DisplayGfx::GetFontMetric` (slot 9, 0x523750).
///
/// `out_1` = per-character metric (via EDX), `out_2` = font max width
/// (via EDI). `char_code` is truncated to 8 bits to match `MOV AL, ...`.
pub unsafe extern "thiscall" fn get_font_metric(
this: *mut DisplayGfx,
font_id: i32,
char_code: u32,
out_1: *mut u32,
out_2: *mut u32,
) -> u32 {
if !is_valid_font_id(font_id) {
return 0;
}
let font_obj = (*this).base.font_table[font_id as usize] as *const Font;
if font_obj.is_null() {
return 0;
}
font_get_metric_impl(
font_obj,
char_code as u8,
out_1 as *mut i32,
out_2 as *mut i32,
)
}
/// Port of `DisplayGfx::SetFontParam` (slot 10, 0x523710).
///
/// Per the original's register shuffle: `p3` = input string, `p4` =
/// output total advance, `p5` = output font max width.
pub unsafe extern "thiscall" fn set_font_param(
this: *mut DisplayGfx,
font_id: i32,
p3: u32,
p4: u32,
p5: u32,
) -> u32 {
if !is_valid_font_id(font_id) {
return 0;
}
let font_obj = (*this).base.font_table[font_id as usize] as *const Font;
if font_obj.is_null() {
return 0;
}
font_set_param_impl(font_obj, p3 as *const u8, p4 as *mut i32, p5 as *mut i32);
1
}
/// Port of `DisplayGfx::SetFontPalette` (slot 36, 0x523690).
///
/// Despite the name this is the entry point for `font_set_palette_impl`,
/// which extends the digital font with derived `'.'` and `';'` glyphs.
/// The original has no bounds or null check on `font_index`; we mirror that.
pub unsafe extern "thiscall" fn set_font_palette(
this: *mut DisplayGfx,
font_index: u32,
palette_value: u32,
) {
let font_obj = (*this).base.font_table[font_index as usize] as *mut Font;
let _ = font_set_palette_impl(font_obj, palette_value);
}
/// Pure-Rust replacement for `FUN_004FA490` plus the 9 unrolled blit
/// helpers at `0x4FA1E0..0x4FA470` (dispatch table at `0x6A9594`).
///
/// The original splits a glyph into `min(remaining_width, 8)`-pixel
/// chunks and dispatches to one of 9 hand-unrolled helpers. We collapse
/// all of that into one nested loop. No transparency: every source byte
/// (palette index) is copied verbatim, including 0.
#[inline]
pub unsafe fn font_blit_glyph(
dst: *mut u8,
dst_stride: i32,
src: *const u8,
src_stride: i32,
width: i32,
height: i32,
) {
for row in 0..height {
let dst_row = dst.offset((row * dst_stride) as isize);
let src_row = src.offset((row * src_stride) as isize);
core::ptr::copy_nonoverlapping(src_row, dst_row, width as usize);
}
}
/// Pure-Rust port of `Font__DrawText` (0x4FA4E0).
///
/// Rasterizes `msg` into `bitmap`. Glyph rows are copied verbatim β€” the
/// "background" of each glyph carries whatever palette index the .fnt
/// file baked in, including 0.
///
/// Subtle behaviors that match the original exactly:
///
/// - `*out_width = font.width` is written unconditionally up front,
/// even on the early validation-failure paths.
/// - On success: returns the number of chars drawn (forward path) or
/// `-1` (right-aligned, full string drawn).
/// - On truncation: returns the index of the first un-drawn char.
/// - On validation failure (negative pen, vertical overflow): returns 0
/// but `*out_width` has already been written.
/// - `out_pen_x` is initialized to 0 and updated to the running advance.
///
/// **Glyph source row stride** is `glyph.width` for base-font glyphs but
/// `font.width` for extension glyphs (index `>= font._height2`, added by
/// `font_extend` / `font_set_palette_impl`). Extension glyphs live in a
/// separate uniform-stride buffer.
///
/// `font_id_high` is the sign-extended high half of slot 7's `font_id`
/// (the wrapper does `SAR EAX, 0x10`). Bit 1 selects right-aligned mode.
pub unsafe fn font_draw_text_impl(
font_obj: *const Font,
bitmap: *const BitGrid,
pen_x: i32,
pen_y: i32,
msg: *const c_char,
out_pen_x: *mut i32,
out_width: *mut i32,
font_id_high: i32,
) -> i32 {
let msg = msg as *const u8;
let font = &*font_obj;
let font_width = font.width as i16 as i32;
// Written unconditionally β€” even on validation failure (matches original).
*out_width = font_width;
let bm = &*bitmap;
let bitmap_width = bm.width as i32;
let bitmap_height = bm.height as i32;
let stride = bm.row_stride as i32;
if pen_x < 0 || pen_y < 0 || font_width + pen_y > bitmap_height {
return 0;
}
// Pre-offset data pointer to (pen_x, pen_y) β€” glyph dst calc only
// needs the per-glyph delta after this.
let data_origin = bm.data.offset((pen_y * stride + pen_x) as isize);
let height2 = font._height2 as i16 as i32;
let char_to_glyph = font.char_to_glyph_idx;
let glyph_table = font.glyph_table;
let pixel_data = font.pixel_data;
let width_div_5 = font.width_div_5 as i16 as i32;
*out_pen_x = 0;
if (font_id_high >> 1) & 1 != 0 {
// Right-aligned path: walk msg right-to-left, advance leftward.
let mut len: i32 = 0;
while *msg.offset(len as isize) != 0 {
len += 1;
}
let mut idx = len - 1;
if idx < 0 {
return idx;
}
loop {
let ch = *msg.offset(idx as isize) as usize;
let glyph_idx_1based = *char_to_glyph.add(ch);
if glyph_idx_1based == 0 {
*out_pen_x += width_div_5;
} else {
let glyph_idx = glyph_idx_1based as i32 - 1;
let glyph = &*glyph_table.add(glyph_idx as usize);
let glyph_width = glyph.width as i32;
let glyph_height = glyph.height as i32;
let cur_advance = *out_pen_x;
// Right-align fit check: leftmost edge must be β‰₯ 0.
let leftmost = pen_x - cur_advance - glyph_width;
if leftmost < 0 {
return idx;
}
let src_stride = if glyph_idx >= height2 {
font_width
} else {
glyph_width
};
let dst = data_origin
.offset(((glyph.start_y as i32) * stride - glyph_width - cur_advance) as isize);
let src = pixel_data.add(glyph.pixel_offset as usize);
font_blit_glyph(dst, stride, src, src_stride, glyph_width, glyph_height);
*out_pen_x += glyph_width + 1;
}
idx -= 1;
if idx < 0 {
return idx;
}
}
} else {
// Forward path: walk msg left-to-right, advance rightward.
if *msg == 0 {
return 0;
}
let mut idx: i32 = 0;
loop {
let cur_advance = *out_pen_x;
if cur_advance + pen_x >= bitmap_width {
return idx;
}
let ch = *msg.offset(idx as isize) as usize;
let glyph_idx_1based = *char_to_glyph.add(ch);
if glyph_idx_1based == 0 {
*out_pen_x = cur_advance + width_div_5;
} else {
let glyph_idx = glyph_idx_1based as i32 - 1;
let glyph = &*glyph_table.add(glyph_idx as usize);
let glyph_width = glyph.width as i32;
let glyph_height = glyph.height as i32;
// Glyph must end at least 2 px before the bitmap edge.
if glyph_width + cur_advance + pen_x + 2 > bitmap_width {
return idx;
}
let src_stride = if glyph_idx >= height2 {
font_width
} else {
glyph_width
};
let dst =
data_origin.offset(((glyph.start_y as i32) * stride + cur_advance) as isize);
let src = pixel_data.add(glyph.pixel_offset as usize);
font_blit_glyph(dst, stride, src, src_stride, glyph_width, glyph_height);
*out_pen_x += glyph_width + 1;
}
idx += 1;
if *msg.offset(idx as isize) == 0 {
return idx;
}
}
}
}
/// Port of `DisplayGfx::DrawTextOnBitmap` (slot 7, 0x5236B0).
///
/// `font_id` low half = 1-based slot index (1..=31), high half = flags
/// (sign-extended via `SAR EAX, 0x10`). On validation failure (bad slot
/// or null entry) the original returns 0 *without* writing `*out_width`;
/// we mirror that.
pub unsafe extern "thiscall" fn draw_text_on_bitmap(
this: *mut DisplayGfx,
font_id: i32,
bitmap: *mut BitGrid,
pen_x: i32,
pen_y: i32,
msg: *const c_char,
out_pen_x: *mut i32,
out_width: *mut i32,
) -> i32 {
let font_id_low = font_id & 0xFFFF;
if !is_valid_font_id(font_id_low) {
return 0;
}
let font_obj = (*this).base.font_table[font_id_low as usize] as *const Font;
if font_obj.is_null() {
return 0;
}
let font_id_high = (font_id as i32) >> 16;
font_draw_text_impl(
font_obj,
bitmap as *const BitGrid,
pen_x,
pen_y,
msg,
out_pen_x,
out_width,
font_id_high,
)
}
// =============================================================================
// DrawTiledBitmap (slot 11) and its leaf primitives
// =============================================================================
use crate::render::display::context::Surface;
use crate::render::sprite::sprite::CBitmap;
use crate::wa_alloc::wa_malloc;
/// Source descriptor passed to `DisplayGfx::DrawTiledBitmap` (slot 11) as
/// the third stack arg. Field offsets verified from the slot 11 disasm
/// (`0x56B8C0`): only `+0x08`, `+0x10`, `+0x14`, `+0x18` are read.
#[repr(C)]
pub struct TiledBitmapSource {
/// 0x00..0x07: unknown header bytes
pub _unknown_00: [u8; 8],
/// 0x08: source pixel data base pointer. Used as the start of the
/// blit source; per-strip offset is `data + current_y * row_stride`.
pub data: *const u8,
/// 0x0C: unknown
pub _unknown_0c: u32,
/// 0x10: source row stride in bytes (passed as `src_stride` to the
/// `0x40`-bpp blit primitive; for the 8bpp path the inner loop ignores
/// it and advances 8 bytes per row internally).
pub row_stride: i32,
/// 0x14: bpp dispatch value. `8` selects the 8-byte-pattern replicator
/// ([`blit_64byte_row_pattern`]); `0x40` selects the transparent CLUT
/// blit ([`blit_color_table_forward`]); any other value skips the blit.
pub bpp: u32,
/// 0x18: total source height in rows. The cache is allocated as
/// `ceil(source_height / 0x400)` strips of `min(remaining, 0x400)` rows.
pub source_height: i32,
}
const _: () = assert!(core::mem::offset_of!(TiledBitmapSource, data) == 0x08);
const _: () = assert!(core::mem::offset_of!(TiledBitmapSource, row_stride) == 0x10);
const _: () = assert!(core::mem::offset_of!(TiledBitmapSource, bpp) == 0x14);
const _: () = assert!(core::mem::offset_of!(TiledBitmapSource, source_height) == 0x18);
/// Pure-Rust port of `FUN_005B2A5E` β€” 8bpp 64-byte row replicator.
///
/// For each of `row_count` rows: read 8 bytes from `src`, write them 8
/// times consecutively to `dst` (= 64 bytes per row), advance `dst` by
/// `dst_stride` and `src` by exactly 8 bytes. The original is asm
/// hand-unrolled to 16 dword writes per row.
///
/// Only caller (verified via xrefs) is `DrawTiledBitmap` slot 11's 8bpp
/// populate phase.
unsafe fn blit_64byte_row_pattern(
mut dst: *mut u8,
dst_stride: i32,
mut src: *const u8,
row_count: i32,
) {
let mut remaining = row_count;
while remaining > 0 {
let pattern_lo = (src as *const u32).read_unaligned();
let pattern_hi = (src.add(4) as *const u32).read_unaligned();
// Replicate (lo, hi) 8 times = 16 dwords = 64 bytes per row.
let dst_dw = dst as *mut u32;
let mut i = 0;
while i < 8 {
dst_dw.add(i * 2).write_unaligned(pattern_lo);
dst_dw.add(i * 2 + 1).write_unaligned(pattern_hi);
i += 1;
}
src = src.add(8);
dst = dst.offset(dst_stride as isize);
remaining -= 1;
}
}
/// Pure-Rust port of `BlitColorTable_Forward` (0x5B2B5D) β€” transparent
/// byte-level blit (skip source bytes that are 0).
///
/// In our build only `DrawTiledBitmap` (slot 11) reaches this β€” the
/// other WA caller `BitGrid::BlitSpriteRect` (0x4F6C93) is replaced by
/// our `blit_sprite_rect` in `sprite_blit.rs`.
unsafe fn blit_color_table_forward(
mut dst: *mut u8,
dst_stride: i32,
mut src: *const u8,
src_stride: i32,
width: i32,
height: i32,
) {
let mut rows_left = height;
while rows_left > 0 {
let mut col = 0;
while col < width as isize {
let s = *src.offset(col);
if s != 0 {
*dst.offset(col) = s;
}
col += 1;
}
dst = dst.offset(dst_stride as isize);
src = src.offset(src_stride as isize);
rows_left -= 1;
}
}
/// Pure-Rust port of `FUN_00403C60` β€” the `CBitmap` blit-via-wrapper.
///
/// Lazy-allocs `cbm.surface` via `alloc_surface` (slot 22) on first
/// call, then dispatches the blit through `draw_landscape` (slot 23).
/// Note: `alloc_surface` returns its result in EAX, not via the
/// `FastcallResult` buffer β€” see `feedback_alloc_surface_return.md`.
unsafe fn cbitmap_blit_via_wrapper(
cbm: *mut CBitmap,
dst_x: i32,
dst_y: i32,
src_x: i32,
src_y: i32,
width: i32,
height: i32,
flags: u32,
) {
let wrapper = *(rb(va::G_RENDER_CONTEXT) as *const *mut RenderContext);
if (*cbm).surface.is_null() {
let mut buf = FastcallResult::default();
let ret = RenderContext::alloc_surface_raw(wrapper, &mut buf);
(*cbm).surface = ret as *mut Surface;
}
let mut buf = FastcallResult::default();
RenderContext::draw_landscape_raw(
wrapper,
&mut buf,
(*cbm).surface as *mut u8,
dst_x,
dst_y,
src_x,
src_y,
width,
height,
flags,
);
}
/// Pure-Rust port of `DisplayGfx::BlitBitmapClipped` (0x56A700).
///
/// Used by slots 11 (`DrawTiledBitmap`), 22 (`DrawTiledTerrain`), and
/// the bitmap-sprite branch of slot 19 (`BlitSprite`).
pub unsafe fn blit_bitmap_clipped_native(
this: *mut DisplayGfx,
dst_x: i32,
dst_y: i32,
width: i32,
height: i32,
surface: *mut CBitmap,
flags: u32,
) {
let base = &(*this).base;
let cx1 = base.clip_x1;
let cy1 = base.clip_y1;
let cx2 = base.clip_x2;
let cy2 = base.clip_y2;
let dst_x2 = dst_x + width;
let dst_y2 = dst_y + height;
if dst_x >= cx2 || dst_x2 <= cx1 || dst_y >= cy2 || dst_y2 <= cy1 {
return;
}
let new_left = cx1.max(dst_x);
let new_right = cx2.min(dst_x2);
let new_top = cy1.max(dst_y);
let new_bottom = cy2.min(dst_y2);
// Degenerate clipped rect β€” matches the original's
// `local_28 != iVar2 && local_24 != iVar1` check.
if new_left == new_right || new_top == new_bottom {
return;
}
flush_render_lock(this);
cbitmap_blit_via_wrapper(
surface,
new_left,
new_top,
new_left - dst_x,
new_top - dst_y,
new_right - new_left,
new_bottom - new_top,
flags | 1,
);
}
/// Pure-Rust port of `DisplayGfx::BlitBitmapTiled` (0x56A7D0; usercall
/// `EAX=initial_x, EDI=tile_width`).
///
/// Tiles `surface` horizontally across `[clip_x1, clip_x2)`. Used by
/// slot 19's bitmap-sprite branch when the tiled mode bit is set.
pub unsafe fn blit_bitmap_tiled_native(
this: *mut DisplayGfx,
initial_x: i32,
tile_width: i32,
dst_y: i32,
height: i32,
surface: *mut CBitmap,
) {
let clip_x1 = (*this).base.clip_x1;
let clip_x2 = (*this).base.clip_x2;
// Two-loop walk to the largest `x ≀ clip_x1` in the sequence
// {initial_x Β± k*tile_width}, matching 0x56A7DD..0x56A7F4.
let mut x = initial_x;
while x < clip_x1 {
x += tile_width;
}
while x > clip_x1 {
x -= tile_width;
}
while x < clip_x2 {
blit_bitmap_clipped_native(this, x, dst_y, tile_width, height, surface, 2);
x += tile_width;
}
}
/// Pure-Rust port of `DisplayGfx::GetBitmapSpriteInfo` (0x573C50;
/// usercall `EAX=bitmap_obj, EDX=palette_or_anim`).
///
/// `bitmap_obj.flags` interprets `palette_or_anim`:
/// - bit 0: 0 = signed clamp to `[0, 0xFFFF]`, 1 = use low 16 bits as-is
/// - bit 1: 0 = forward iter, 1 = ping-pong over `[0, frame_count)`
///
/// Returns a pointer to the selected `LayerSpriteFrame`'s embedded
/// `CBitmap` (the trailing 12 bytes). Used by slot 19's bitmap-sprite branch.
pub unsafe fn get_bitmap_sprite_info(
bitmap_obj: *mut LayerSprite,
palette_or_anim: u32,
out_w: *mut i32,
out_h: *mut i32,
out_left: *mut i32,
out_top: *mut i32,
out_right: *mut i32,
out_bottom: *mut i32,
) -> *mut CBitmap {
let flags = (*bitmap_obj).flags as i32;
let pal: i32 = if flags & 1 != 0 {
(palette_or_anim & 0xFFFF) as i32
} else {
let p = palette_or_anim as i32;
p.max(0).min(0xFFFF)
};
let frame_count = (*bitmap_obj).frame_count as i16 as i32;
let frame_idx = if flags & 2 != 0 {
// Ping-pong: scaled = ((2*frame_count - 1) * pal) >> 16, fold
// back to `(2*frame_count - scaled) - 1` when past the midpoint.
let scaled = ((frame_count * 2 - 1) * pal) >> 16;
if scaled >= frame_count {
(frame_count * 2 - scaled) - 1
} else {
scaled
}
} else {
(frame_count * pal) >> 16
};
let frame = (*bitmap_obj).frame_array.offset(frame_idx as isize);
*out_left = (*frame).start_x as i32;
*out_top = (*frame).start_y as i32;
*out_right = (*frame).end_x as i32;
*out_bottom = (*frame).end_y as i32;
*out_w = (*bitmap_obj).cell_width as i32;
*out_h = (*bitmap_obj).cell_height as i32;
LayerSpriteFrame::bitmap_ptr(frame)
}
/// Pure-Rust port of `DisplayGfx::DrawTiledBitmap` (vtable slot 11,
/// `0x56B8C0`).
///
/// Three-phase tile-cached landscape blit. See the docstring on
/// [`DisplayVtable::draw_tiled_bitmap`] for the high-level semantics.
///
/// **Tile-cache vector pre-reservation.** The original allocates and
/// `vector::push_back`s each strip's `CBitmap*` into `bitmap_vec`
/// (`+0x3580`) one at a time, growing the vector as needed. We sidestep
/// porting `std::vector::push_back` (`FUN_00402e90`) by computing the
/// final entry count up front (`ceil(source_height / 0x400)`), allocating
/// the entire backing buffer in one shot, then writing entries directly
/// and bumping `bitmap_end`. This changes WA's heap allocation pattern
/// slightly (one bigger allocation vs many growing reallocs) but is
/// behaviorally equivalent: each `DisplayGfx` only ever populates the
/// vector once.
///
/// # Safety
/// `this` must be a valid `*mut DisplayGfx`. `source` must be a valid
/// `*const TiledBitmapSource` whose fields describe the source landscape
/// data. `g_RenderContext` must be initialized.
pub unsafe fn draw_tiled_bitmap_impl(
this: *mut DisplayGfx,
dest_x: i32,
dest_y: i32,
source: *const TiledBitmapSource,
) {
let total_height = (*source).source_height;
let row_stride = (*source).row_stride;
let bpp = (*source).bpp;
let source_data = (*source).data;
// -------------------------------------------------------------------
// Phase 1 β€” Allocate (only if bitmap_vec is empty)
// -------------------------------------------------------------------
let vec_empty = (*this).bitmap_ptr.is_null()
|| ((*this).bitmap_end as usize - (*this).bitmap_ptr as usize) >> 2 == 0;
if vec_empty {
if total_height > 0 {
// Pre-reserve in one allocation; see the docstring above.
let max_tiles = ((total_height + 0x3FF) >> 10) as usize;
let vec_buf = wa_malloc((max_tiles * core::mem::size_of::<*mut CBitmap>()) as u32)
as *mut *mut CBitmap;
if vec_buf.is_null() {
return;
}
(*this).bitmap_ptr = vec_buf;
(*this).bitmap_end = vec_buf;
(*this).bitmap_capacity = vec_buf.add(max_tiles);
let cbitmap_vt = rb(va::CBITMAP_VTABLE_MAYBE) as *const core::ffi::c_void;
let wrapper = *(rb(va::G_RENDER_CONTEXT) as *const *mut RenderContext);
let mut accum = 0i32;
let mut remaining = total_height;
while accum < total_height {
// The original doesn't null-check the malloc; mirror that.
let cbm = wa_malloc_struct_zeroed::<CBitmap>();
if !cbm.is_null() {
(*cbm).vtable = cbitmap_vt;
(*cbm).surface = core::ptr::null_mut();
(*cbm)._pad = 0;
}
let strip_h = remaining.min(0x400);
if (*cbm).surface.is_null() {
let mut buf = FastcallResult::default();
let s = RenderContext::alloc_surface_raw(wrapper, &mut buf);
(*cbm).surface = s as *mut Surface;
}
// Init at 0x40 Γ— strip_h Γ— 8bpp; retry with 4bpp on failure.
let mut init_buf = FastcallResult::default();
Surface::init_surface_raw((*cbm).surface, &mut init_buf, 0x40, strip_h, 8);
if init_buf.value != 0 {
if (*cbm).surface.is_null() {
let mut buf = FastcallResult::default();
let s = RenderContext::alloc_surface_raw(wrapper, &mut buf);
(*cbm).surface = s as *mut Surface;
}
let mut init_buf2 = FastcallResult::default();
Surface::init_surface_raw((*cbm).surface, &mut init_buf2, 0x40, strip_h, 4);
if init_buf2.value != 0 {
return;
}
}
*(*this).bitmap_end = cbm;
(*this).bitmap_end = (*this).bitmap_end.add(1);
accum += 0x400;
remaining -= 0x400;
}
}
(*this).tile_cache_populated = 0;
}
// -------------------------------------------------------------------
// Phase 2 β€” Populate
// -------------------------------------------------------------------
if (*this).tile_cache_populated == 0 {
let mut tile_idx: usize = 0;
let mut current_y: i32 = 0;
loop {
let vec_size = if (*this).bitmap_ptr.is_null() {
0
} else {
((*this).bitmap_end as usize - (*this).bitmap_ptr as usize) >> 2
};
if tile_idx >= vec_size {
break;
}
let strip_end = total_height.min(current_y + 0x400);
let strip_h = strip_end - current_y;
let cbm = *(*this).bitmap_ptr.add(tile_idx);
// Paranoid lazy-alloc β€” should already be non-null from
// phase 1, but the original repeats the check.
if (*cbm).surface.is_null() {
let wrapper = *(rb(va::G_RENDER_CONTEXT) as *const *mut RenderContext);
let mut buf = FastcallResult::default();
let s = RenderContext::alloc_surface_raw(wrapper, &mut buf);
(*cbm).surface = s as *mut Surface;
}
let mut surf_data: *mut u8 = core::ptr::null_mut();
let mut surf_stride: i32 = 0;
{
let mut buf = FastcallResult::default();
Surface::lock_surface_raw(
(*cbm).surface,
&mut buf,
&mut surf_data,
&mut surf_stride,
);
}
let src_row = source_data.offset((current_y as isize) * (row_stride as isize));
if bpp == 8 {
blit_64byte_row_pattern(surf_data, surf_stride, src_row, strip_h);
} else if bpp == 0x40 {
blit_color_table_forward(
surf_data,
surf_stride,
src_row,
row_stride,
0x40,
strip_h,
);
}
// (other bpp values: no blit, just unlock β€” matches original)
{
let mut buf = FastcallResult::default();
Surface::unlock_surface_raw((*cbm).surface, &mut buf, surf_data);
}
tile_idx += 1;
current_y += 0x400;
}
(*this).tile_cache_populated = 1;
}
// -------------------------------------------------------------------
// Phase 3 β€” Display
// -------------------------------------------------------------------
// Snap dest_x to a 0x40-aligned column ≀ dest_x: result is in
// [-0x3f, 0], so stepping by 0x40 covers the visible area starting
// off the left edge. Reproduces the original's
// `AND EAX, 0x8000003f` + sign-extend dance for signed mod 0x40.
let col_x: i32 = {
let dest_x_u = dest_x as u32;
let masked = dest_x_u & 0x8000_003f;
let mut v = if (masked as i32) < 0 {
(((masked.wrapping_sub(1)) | 0xffff_ffc0).wrapping_add(1)) as i32
} else {
masked as i32
};
if v > 0 {
v -= 0x40;
}
v
};
// First/last visible Y-tile indices, using the original's signed
// `(v + ((v >> 31) & 0x3FF)) >> 10` rounding-toward-zero idiom.
let camera_y = (*this).camera_y;
let neg = -(camera_y + dest_y);
let display_height = (*this).base.display_height as i32;
let first_v = neg + 0x20000;
let last_v = display_height + neg + 0x20000;
let mut y_first = (((first_v + ((first_v >> 31) & 0x3FF)) >> 10) - 0x80) as i32;
let mut y_last = (((last_v + ((last_v >> 31) & 0x3FF)) >> 10) - 0x80) as i32;
if y_first < 0 {
y_first = 0;
}
let vec_size = if (*this).bitmap_ptr.is_null() {
0i32
} else {
(((*this).bitmap_end as usize - (*this).bitmap_ptr as usize) >> 2) as i32
};
let max_idx = vec_size - 1;
if max_idx <= y_last {
y_last = vec_size - 1;
}
if y_first > y_last {
return;
}
let display_width = (*this).base.display_width as i32;
let mut tile_idx_y = y_first;
let mut current_strip_y = y_first << 10;
while tile_idx_y <= y_last {
let strip_end = total_height.min(current_strip_y + 0x400);
let strip_h = strip_end - current_strip_y;
if col_x < display_width {
let cbm = *(*this).bitmap_ptr.add(tile_idx_y as usize);
let dst_y = camera_y + current_strip_y + dest_y;
let mut x = col_x;
while x < display_width {
blit_bitmap_clipped_native(this, x, dst_y, 0x40, strip_h, cbm, 0);
x += 0x40;
}
}
tile_idx_y += 1;
current_strip_y += 0x400;
}
}
/// Thiscall entry point for `DisplayGfx::DrawTiledBitmap` (slot 11).
pub unsafe extern "thiscall" fn draw_tiled_bitmap(
this: *mut DisplayGfx,
dest_x: i32,
dest_y: i32,
source: *const TiledBitmapSource,
) {
draw_tiled_bitmap_impl(this, dest_x, dest_y, source);
}