paavohtl Claude Opus 4.6 (1M context) commited on
Commit
7f3ff37
Β·
1 Parent(s): 30d0cba

feat: port DisplayGfx::LoadFont (slot 34) to pure Rust

Browse files

Adds FontObject (0x1C), GlyphEntry (0xC) and FntHeader (0xC) structs and
ports the .fnt loader chain (FUN_004f9940 + FUN_004f99d0) and the slot 34
wrapper. Replaces the last bridged font-loading path so DisplayGfx vtable
patches now cover 30 of 38 slots.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/render/display/vtable.rs CHANGED
@@ -2024,3 +2024,318 @@ pub unsafe fn load_sprite_by_layer(
2024
  // LoadSpriteComplex (vtable slot 33, 0x5237C0) is NOT ported β€” the internal
2025
  // dispatch functions (0x4FAD30, 0x4F9710) use ESI for sprite/bank pointers
2026
  // in complex usercall conventions that are impractical to bridge.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2024
  // LoadSpriteComplex (vtable slot 33, 0x5237C0) is NOT ported β€” the internal
2025
  // dispatch functions (0x4FAD30, 0x4F9710) use ESI for sprite/bank pointers
2026
  // in complex usercall conventions that are impractical to bridge.
2027
+
2028
+ // =========================================================================
2029
+ // Font loading
2030
+ // =========================================================================
2031
+
2032
+ /// Font object β€” 0x1C bytes.
2033
+ ///
2034
+ /// Allocated by `load_font` (vtable slot 34) from a `.fnt` file loaded via
2035
+ /// GfxDir. Referenced from `DisplayBase::font_table` (offset 0x309C). Read by
2036
+ /// `Font__GetInfo` / `Font__GetMetric` / `Font__DrawText` / `Font__SetPalette`
2037
+ /// to render bitmap text.
2038
+ ///
2039
+ /// Field semantics derived from `FUN_004f99d0` (the parser) and `Font__GetInfo`
2040
+ /// (0x4fa7d0, which confirms width at +0, width_div_5 at +2, glyph count at +4,
2041
+ /// and glyph table at +8 with 12-byte entries whose byte +4 is a max metric).
2042
+ #[repr(C)]
2043
+ pub struct FontObject {
2044
+ /// 0x00: Font max width in pixels (read from .fnt metadata).
2045
+ pub width: u16,
2046
+ /// 0x02: `width / 5 + 1` β€” initial max metric seed in `Font__GetInfo`.
2047
+ pub width_div_5: u16,
2048
+ /// 0x04: Glyph count / font height.
2049
+ pub height: u16,
2050
+ /// 0x06: Duplicate of height.
2051
+ pub _height2: u16,
2052
+ /// 0x08: Pointer to glyph table (height entries of 12 bytes each).
2053
+ pub glyph_table: *mut GlyphEntry,
2054
+ /// 0x0C: Pointer to end-of-palette position in the source buffer.
2055
+ pub palette_end: *mut u8,
2056
+ /// 0x10: Pointer to remapped pixel data in the source buffer.
2057
+ pub pixel_data: *mut u8,
2058
+ /// 0x14: Zeroed by `load_font`; purpose unknown.
2059
+ pub _unknown_14: u32,
2060
+ /// 0x18: Zeroed by `load_font`; purpose unknown.
2061
+ pub _unknown_18: u32,
2062
+ }
2063
+
2064
+ const _: () = assert!(core::mem::size_of::<FontObject>() == 0x1C);
2065
+
2066
+ /// Per-glyph metadata in a `.fnt` file (12 bytes).
2067
+ ///
2068
+ /// Indexed by `FontObject::glyph_table`. `Font__GetInfo` (0x4fa7d0) reads
2069
+ /// `max_metric` at +0x4 to compute the maximum advance; the other fields are
2070
+ /// not yet exercised from Rust. Layout matches the loop stride of 0xC used by
2071
+ /// both the parser and `Font__GetInfo`.
2072
+ #[repr(C)]
2073
+ pub struct GlyphEntry {
2074
+ /// 0x00: Unknown / start coordinates.
2075
+ pub _unknown_00: u32,
2076
+ /// 0x04: Max metric seed read by `Font__GetInfo` (used as `max(current, val + 1)`).
2077
+ pub max_metric: u16,
2078
+ /// 0x06: Remaining per-glyph fields (bearing / advance / pixel offset).
2079
+ pub _unknown_06: [u8; 6],
2080
+ }
2081
+
2082
+ const _: () = assert!(core::mem::size_of::<GlyphEntry>() == 0xC);
2083
+
2084
+ /// Fixed prefix of a `.fnt` binary blob (0xC bytes).
2085
+ ///
2086
+ /// Followed in memory by: packed RGB triplets (`palette_count * 3` bytes),
2087
+ /// a 0x100-byte character-width table, `max_width: i16`, `glyph_count: u16`,
2088
+ /// 4-byte alignment padding, `GlyphEntry[glyph_count]`, then the packed
2089
+ /// glyph pixel data. `font_parse_data` walks this variable-length tail using
2090
+ /// byte pointers rather than a single `#[repr(C)]` struct.
2091
+ #[repr(C)]
2092
+ pub struct FntHeader {
2093
+ /// 0x00: Unknown / version.
2094
+ pub _unknown_00: u32,
2095
+ /// 0x04: Total size of this record in bytes, including this field.
2096
+ pub data_size: i32,
2097
+ /// 0x08: Unknown u16 (skipped by the parser).
2098
+ pub _unknown_08: u16,
2099
+ /// 0x0A: Number of palette entries that follow this header.
2100
+ pub palette_count: u16,
2101
+ // +0x0C: packed RGB triplets (variable length)
2102
+ }
2103
+
2104
+ const _: () = assert!(core::mem::size_of::<FntHeader>() == 0xC);
2105
+
2106
+ /// Port of `FUN_004f99d0` β€” parses `.fnt` binary data into a `FontObject`.
2107
+ ///
2108
+ /// Original convention: cdecl with 1 stack arg (buffer), `unaff_EDI = font_obj`.
2109
+ /// Ported as a plain Rust function.
2110
+ ///
2111
+ /// Buffer layout:
2112
+ /// ```text
2113
+ /// +0x00 u32 (unknown / version)
2114
+ /// +0x04 i32 data_size (total bytes in this record, including this field)
2115
+ /// +0x08 u16 (unknown)
2116
+ /// +0x0A u16 palette_count
2117
+ /// +0x0C RGB[palette_count * 3] packed RGB triplets
2118
+ /// +... u8[0x100] character-width table (1 byte per codepoint)
2119
+ /// +0x100 u16 max_width
2120
+ /// +0x102 u16 glyph_count / height
2121
+ /// +... align to 4 from buffer start
2122
+ /// +... GlyphEntry[glyph_count] 12 bytes each (start_x/start_y/end_x/end_y/...)
2123
+ /// +... u8[] packed 4bpp glyph pixels (remapped in place via palette LUT)
2124
+ /// ```
2125
+ ///
2126
+ /// # Safety
2127
+ /// `font_obj` must be a valid writable `FontObject`. `header` must point to a
2128
+ /// freshly-loaded `.fnt` blob at least `header.data_size` bytes long and
2129
+ /// followed in memory by the variable-length tail described above.
2130
+ /// `palette_ctx` must be a valid `PaletteContext`.
2131
+ pub unsafe fn font_parse_data(
2132
+ font_obj: *mut FontObject,
2133
+ header: *mut FntHeader,
2134
+ palette_ctx: *mut PaletteContext,
2135
+ ) -> u32 {
2136
+ use crate::render::palette::{palette_map_color, remap_pixels_through_lut};
2137
+
2138
+ let data_size = (*header).data_size;
2139
+ let palette_count = (*header).palette_count as i32;
2140
+ let base_bytes = header as *mut u8;
2141
+
2142
+ // Cursor starts at the first byte after the fixed header (the RGB triplets).
2143
+ let mut cursor = base_bytes.add(core::mem::size_of::<FntHeader>());
2144
+
2145
+ // Build 256-byte palette LUT. Entry 0 = map_color(0); entries 1..=palette_count
2146
+ // come from the RGB triplets. The original reads 4 bytes per triplet but
2147
+ // advances by 3 β€” only the low 24 bits matter.
2148
+ let mut palette_lut = [0u8; 256];
2149
+ palette_lut[0] = palette_map_color(palette_ctx, 0) as u8;
2150
+ for i in 0..palette_count {
2151
+ let rgb = *(cursor as *const u32);
2152
+ palette_lut[(1 + i) as usize] = palette_map_color(palette_ctx, rgb) as u8;
2153
+ cursor = cursor.add(3);
2154
+ }
2155
+
2156
+ // palette_end = cursor after palette triplets.
2157
+ (*font_obj).palette_end = cursor;
2158
+
2159
+ // Read max_width (i16) at cursor + 0x100 β€” the character-width table sits
2160
+ // between the palette and the header.
2161
+ let max_width = *(cursor.add(0x100) as *const i16) as i32;
2162
+ cursor = cursor.add(0x100);
2163
+ (*font_obj).width = max_width as u16;
2164
+
2165
+ // Seed value for Font__GetInfo's max metric: signed (width / 5) + 1,
2166
+ // with a +1 correction when the quotient is negative (matches the
2167
+ // IMUL+SAR+SHR+LEA sequence in the original).
2168
+ let w_div_5 = max_width / 5;
2169
+ let seed = w_div_5.wrapping_add((w_div_5 >> 31) & 1).wrapping_add(1);
2170
+ (*font_obj).width_div_5 = seed as u16;
2171
+
2172
+ // Skip max_width, read glyph_count (u16).
2173
+ cursor = cursor.add(2);
2174
+ let glyph_count = *(cursor as *const u16);
2175
+ cursor = cursor.add(2);
2176
+ (*font_obj).height = glyph_count;
2177
+ (*font_obj)._height2 = glyph_count;
2178
+
2179
+ // Align cursor to 4 bytes relative to the buffer start.
2180
+ let base_addr = base_bytes as usize;
2181
+ while (cursor as usize - base_addr) & 3 != 0 {
2182
+ cursor = cursor.add(1);
2183
+ }
2184
+
2185
+ // Glyph table: glyph_count entries of 12 bytes each.
2186
+ let glyph_table = cursor as *mut GlyphEntry;
2187
+ (*font_obj).glyph_table = glyph_table;
2188
+ let pixel_data = glyph_table.add(glyph_count as usize) as *mut u8;
2189
+ (*font_obj).pixel_data = pixel_data;
2190
+
2191
+ // Pixel byte count: data_size - (pixel_data - buffer), rounded down to dwords.
2192
+ // Original uses CDQ+AND 3+ADD+SAR 2 which rounds toward zero.
2193
+ let remaining = data_size - (pixel_data as usize - base_addr) as i32;
2194
+ let dword_count = remaining
2195
+ .wrapping_add((remaining >> 31) & 3)
2196
+ .wrapping_shr(2) as u32;
2197
+
2198
+ remap_pixels_through_lut(pixel_data, 0, palette_lut.as_ptr(), dword_count, 1);
2199
+
2200
+ 1
2201
+ }
2202
+
2203
+ /// Port of `FUN_004f9940` β€” loads a font resource by name from a GfxDir and
2204
+ /// feeds it into `font_parse_data`.
2205
+ ///
2206
+ /// Original convention: usercall with `EAX = gfx_dir`, `ECX = font_obj`,
2207
+ /// `stack[0] = layer_ctx`, `stack[1] = filename`, `RET 0x8`. The function
2208
+ /// first tries `GfxDir__FindEntry` + cached-load via `gfx_dir->vtable[2]`;
2209
+ /// on miss it falls back to `GfxDir__LoadImage` + `image->vtable[4]` (get_size)
2210
+ /// + `image->vtable[5]` (read into caller-allocated buffer) + `image->vtable[0](1)`
2211
+ /// (destroy), then tail-calls the parser.
2212
+ ///
2213
+ /// Note: `layer_ctx` is only used by `font_parse_data` (as the PaletteContext).
2214
+ /// The original never touches it in this function β€” it just forwards it via
2215
+ /// ECX to the tail call.
2216
+ ///
2217
+ /// # Safety
2218
+ /// `font_obj` must be a valid writable `FontObject`. `gfx_dir` must be a valid
2219
+ /// GfxDir. `palette_ctx` must be a valid PaletteContext. `name` must be a
2220
+ /// valid null-terminated C string.
2221
+ pub unsafe fn font_load_from_gfx(
2222
+ font_obj: *mut FontObject,
2223
+ gfx_dir: *mut u8,
2224
+ palette_ctx: *mut PaletteContext,
2225
+ name: *const core::ffi::c_char,
2226
+ ) -> u32 {
2227
+ use crate::render::sprite::gfx_dir::{call_gfx_load_image, gfx_dir_find_entry, GfxDirEntry};
2228
+ use crate::wa_alloc::wa_malloc;
2229
+
2230
+ // 1. Try FindEntry β†’ gfx_dir->vtable[2](entry->value) for cached load.
2231
+ let entry = gfx_dir_find_entry(name, gfx_dir);
2232
+ if !entry.is_null() {
2233
+ let vt = *(gfx_dir as *const *const u32);
2234
+ let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut FntHeader =
2235
+ core::mem::transmute(*vt.add(2));
2236
+ let entry_val = (*(entry as *const GfxDirEntry)).value;
2237
+ let cached = load_cached(gfx_dir, entry_val);
2238
+ if !cached.is_null() {
2239
+ return font_parse_data(font_obj, cached, palette_ctx);
2240
+ }
2241
+ }
2242
+
2243
+ // 2. Fallback: LoadImage β†’ vtable[4] (size) β†’ wa_malloc β†’ vtable[5] (read) β†’ vtable[0] (destroy)
2244
+ let image = call_gfx_load_image(gfx_dir, name);
2245
+ if image.is_null() {
2246
+ return 0;
2247
+ }
2248
+
2249
+ let image_vt = *(image as *const *const u32);
2250
+
2251
+ // vtable[4] β€” get size (thiscall, no args)
2252
+ let get_size: unsafe extern "thiscall" fn(*mut u8) -> u32 =
2253
+ core::mem::transmute(*image_vt.add(4));
2254
+ let size = get_size(image);
2255
+
2256
+ // Match the original's allocation: round size up to 4-byte multiple, add 0x20 guard.
2257
+ let alloc_size = ((size + 3) & !3u32).wrapping_add(0x20);
2258
+ let buffer = wa_malloc(alloc_size);
2259
+ // Original memsets only `size` bytes, not the full allocation.
2260
+ if !buffer.is_null() {
2261
+ core::ptr::write_bytes(buffer, 0, size as usize);
2262
+ }
2263
+
2264
+ // vtable[5] β€” read into buffer (thiscall with 2 stack args: buffer, size)
2265
+ let read_into: unsafe extern "thiscall" fn(*mut u8, *mut u8, u32) =
2266
+ core::mem::transmute(*image_vt.add(5));
2267
+ read_into(image, buffer, size);
2268
+
2269
+ // vtable[0] β€” destroy image (thiscall with 1 arg: flags = 1)
2270
+ let destroy: unsafe extern "thiscall" fn(*mut u8, u32) = core::mem::transmute(*image_vt);
2271
+ destroy(image, 1);
2272
+
2273
+ font_parse_data(font_obj, buffer as *mut FntHeader, palette_ctx)
2274
+ }
2275
+
2276
+ /// Port of `DisplayGfx::LoadFont` (vtable slot 34, 0x523560).
2277
+ ///
2278
+ /// Validates `mode` (1..=3) and `font_id` (1..=31), then allocates a
2279
+ /// zero-initialized 0x1C-byte `FontObject`, loads the named resource via
2280
+ /// `font_load_from_gfx`, and stores the object in `DisplayBase::font_table`.
2281
+ ///
2282
+ /// On load failure, the partially-initialized font object is leaked to match
2283
+ /// the original's behavior (it calls a sprite-bank-style cleanup helper
2284
+ /// `FUN_005230c0` which is not exercised here).
2285
+ ///
2286
+ /// # Safety
2287
+ /// `this` must be a valid `*mut DisplayGfx`. `_gfx` must be a GfxDir pointer
2288
+ /// (or null if only the cached path is needed). `filename` must be a valid
2289
+ /// null-terminated C string.
2290
+ pub unsafe fn load_font(
2291
+ this: *mut DisplayGfx,
2292
+ mode: i32,
2293
+ font_id: i32,
2294
+ _gfx: *mut u8,
2295
+ filename: *const core::ffi::c_char,
2296
+ ) -> u32 {
2297
+ use crate::wa_alloc::wa_malloc_struct_zeroed;
2298
+
2299
+ // Validate mode (1..=3) and that the layer context exists.
2300
+ if !(1..=3).contains(&mode) {
2301
+ return 0;
2302
+ }
2303
+ let base = &mut (*this).base;
2304
+ let layer_ctx = base.layer_contexts[mode as usize];
2305
+ if layer_ctx == 0 {
2306
+ return 0;
2307
+ }
2308
+
2309
+ // Validate font_id (1..=31) and that the slot is empty.
2310
+ if !(1..=31).contains(&font_id) {
2311
+ return 0;
2312
+ }
2313
+ if base.font_table[font_id as usize] != 0 {
2314
+ return 0;
2315
+ }
2316
+
2317
+ // Allocate zeroed FontObject. The original uses WA_MallocMemset(0x1C)
2318
+ // which only memsets the requested size; wa_malloc_struct_zeroed matches.
2319
+ let font_obj = wa_malloc_struct_zeroed::<FontObject>();
2320
+ if font_obj.is_null() {
2321
+ return 0;
2322
+ }
2323
+
2324
+ // Load and parse the font data.
2325
+ let result = font_load_from_gfx(font_obj, _gfx, layer_ctx as *mut PaletteContext, filename);
2326
+ if result == 0 {
2327
+ // Original leaks here on failure (via an unported cleanup helper).
2328
+ // We match that behavior rather than introducing a free path that
2329
+ // might differ from WA's.
2330
+ return 0;
2331
+ }
2332
+
2333
+ // Install into font_table. The original also records which mode owns this
2334
+ // font slot at DisplayBase + 0x301C + font_id*4 (the gap region next to
2335
+ // font_table), then bumps the layer visibility counter.
2336
+ base.font_table[font_id as usize] = font_obj as u32;
2337
+ base._gap_301c[font_id as usize] = mode as u32;
2338
+ base.layer_visibility[mode as usize] += 1;
2339
+
2340
+ 1
2341
+ }
crates/openwa-dll/src/replacements/render.rs CHANGED
@@ -1617,6 +1617,16 @@ unsafe extern "thiscall" fn load_sprite_by_layer(
1617
  display_vtable::load_sprite_by_layer(this, layer, id, gfx, name)
1618
  }
1619
 
 
 
 
 
 
 
 
 
 
 
1620
  // ---------------------------------------------------------------------------
1621
  // Display installation
1622
  // ---------------------------------------------------------------------------
@@ -1685,8 +1695,9 @@ fn install_display() -> Result<(), String> {
1685
  set_font_palette => set_font_palette,
1686
  slot 19 => blit_sprite,
1687
  load_sprite_by_layer => load_sprite_by_layer,
 
1688
  })?;
1689
- let _ = log_line("[Display] DisplayGfx: patched 29 methods -> Rust");
1690
  }
1691
 
1692
  Ok(())
 
1617
  display_vtable::load_sprite_by_layer(this, layer, id, gfx, name)
1618
  }
1619
 
1620
+ unsafe extern "thiscall" fn load_font(
1621
+ this: *mut DisplayGfx,
1622
+ mode: i32,
1623
+ font_id: i32,
1624
+ gfx: *mut u8,
1625
+ filename: *const core::ffi::c_char,
1626
+ ) -> u32 {
1627
+ display_vtable::load_font(this, mode, font_id, gfx, filename)
1628
+ }
1629
+
1630
  // ---------------------------------------------------------------------------
1631
  // Display installation
1632
  // ---------------------------------------------------------------------------
 
1695
  set_font_palette => set_font_palette,
1696
  slot 19 => blit_sprite,
1697
  load_sprite_by_layer => load_sprite_by_layer,
1698
+ load_font => load_font,
1699
  })?;
1700
+ let _ = log_line("[Display] DisplayGfx: patched 30 methods -> Rust");
1701
  }
1702
 
1703
  Ok(())