paavohtl Claude Opus 4.6 (1M context) commited on
Commit
b6987a4
·
1 Parent(s): c38e471

feat: port DisplayGfx::SetFontPalette (slot 36) to pure Rust

Browse files

Slot 36 was bridging via a naked-asm trampoline to Font__SetPalette
(0x4F9F20). Replace it with a native Rust port and delete the trampoline.

The original function name is wildly misleading. Despite "SetPalette",
it actually extends the font with two new derived glyphs: it generates
'.' (0x2E) and ';' (0x3B) at runtime by copying the existing ':' (0x3A)
glyph, fills the top half of '.' with `palette_value as u8`, and copies
':' into ';' substituting any pixel matching the '8' glyph's foreground
color with the '-' glyph's foreground color. Called exactly once during
DDGame::LoadFonts for `digiwht.fnt` (slot 28, the digital seven-segment
font), which doesn't ship with those two codepoints. The full step list
is in the new docstring on `font_set_palette_impl`.

While in the area, rename `FontObject::_unknown_18` to `aux_alloc` —
both `font_extend` (the .fex loader) and the new `font_set_palette_impl`
write the address of an auxiliary glyph+pixel buffer there for the font
destructor to free, so the field has a clear semantic role now.

All 15 headless replay tests pass. Headless can't see whether the new
glyphs render correctly though — slot 36 only writes data that text
rendering reads back, so wrong byte offsets in the new buffer would
silently corrupt the digital-font glyph table without crashing the sim.
The digital font shows up in HUD elements like the turn timer; needs
visual verification that '.' and ';' look right before trusting the
port.

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

crates/openwa-core/src/address.rs CHANGED
@@ -261,7 +261,10 @@ pub mod va {
261
  /// Font object: get info — usercall(EAX=font_obj, EDX=out2, EDI=out1), plain RET.
262
  /// Ported as `display::vtable::font_get_info_impl`; address kept for registry.
263
  fn/Usercall FONT_OBJ_GET_INFO = 0x004F_A7D0;
264
- /// Font object: set palette — usercall(ESI=font_obj) + 1 stack(palette_value), RET 0x4
 
 
 
265
  fn/Usercall FONT_OBJ_SET_PALETTE = 0x004F_9F20;
266
  }
267
 
 
261
  /// Font object: get info — usercall(EAX=font_obj, EDX=out2, EDI=out1), plain RET.
262
  /// Ported as `display::vtable::font_get_info_impl`; address kept for registry.
263
  fn/Usercall FONT_OBJ_GET_INFO = 0x004F_A7D0;
264
+ /// Font object: "set palette" — usercall(ESI=font_obj) + 1 stack(palette_value), RET 0x4.
265
+ /// Misnamed in the original — actually extends `digiwht.fnt` with
266
+ /// derived `'.'` and `';'` glyphs at runtime. Ported as
267
+ /// `display::vtable::font_set_palette_impl`; address kept for registry.
268
  fn/Usercall FONT_OBJ_SET_PALETTE = 0x004F_9F20;
269
  }
270
 
crates/openwa-core/src/render/display/vtable.rs CHANGED
@@ -2119,8 +2119,14 @@ pub struct FontObject {
2119
  pub pixel_data: *mut u8,
2120
  /// 0x14: Zeroed by `load_font`; purpose unknown.
2121
  pub _unknown_14: u32,
2122
- /// 0x18: Zeroed by `load_font`; purpose unknown.
2123
- pub _unknown_18: u32,
 
 
 
 
 
 
2124
  }
2125
 
2126
  const _: () = assert!(core::mem::size_of::<FontObject>() == 0x1C);
@@ -2434,7 +2440,7 @@ pub unsafe fn load_font(
2434
  /// - computes a tight bounding box per new glyph and writes it as a new entry
2435
  /// - remaps every new pixel byte through the LUT
2436
  /// - swaps `font_obj.glyph_table` to the new buffer and bumps `font_obj.height`
2437
- /// - records the allocation in `font_obj._unknown_18`
2438
  ///
2439
  /// The `pixel_offset` field in each new glyph entry stores
2440
  /// `(absolute_pixel_addr - font_obj.pixel_data)` so the existing
@@ -2507,7 +2513,7 @@ pub unsafe fn font_extend(
2507
  let new_pixels = alloc.add(glyph_entries_bytes as usize);
2508
 
2509
  // Record the allocation so font cleanup paths can free it.
2510
- (*font_obj)._unknown_18 = alloc as u32;
2511
 
2512
  // ── Copy existing glyph entries verbatim into the new buffer ──
2513
  let old_glyphs = (*font_obj).glyph_table as *const u8;
@@ -2928,3 +2934,217 @@ pub unsafe extern "thiscall" fn set_font_param(
2928
  font_set_param_impl(font_obj, p3 as *const u8, p4 as *mut i32, p5 as *mut i32);
2929
  1
2930
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2119
  pub pixel_data: *mut u8,
2120
  /// 0x14: Zeroed by `load_font`; purpose unknown.
2121
  pub _unknown_14: u32,
2122
+ /// 0x18: When non-null, holds the address of an auxiliary allocation
2123
+ /// owned by this font (a separate buffer of glyph entries + pixel data
2124
+ /// pointed at by individual `GlyphEntry::pixel_offset` deltas). Zeroed
2125
+ /// by `load_font`; written by `font_extend` (which appends new glyphs
2126
+ /// from a `.fex` file) and `font_set_palette_impl` (which derives the
2127
+ /// `.`/`;` glyphs for the digital font). The cleanup path frees this
2128
+ /// when the FontObject is destroyed.
2129
+ pub aux_alloc: u32,
2130
  }
2131
 
2132
  const _: () = assert!(core::mem::size_of::<FontObject>() == 0x1C);
 
2440
  /// - computes a tight bounding box per new glyph and writes it as a new entry
2441
  /// - remaps every new pixel byte through the LUT
2442
  /// - swaps `font_obj.glyph_table` to the new buffer and bumps `font_obj.height`
2443
+ /// - records the allocation in `font_obj.aux_alloc`
2444
  ///
2445
  /// The `pixel_offset` field in each new glyph entry stores
2446
  /// `(absolute_pixel_addr - font_obj.pixel_data)` so the existing
 
2513
  let new_pixels = alloc.add(glyph_entries_bytes as usize);
2514
 
2515
  // Record the allocation so font cleanup paths can free it.
2516
+ (*font_obj).aux_alloc = alloc as u32;
2517
 
2518
  // ── Copy existing glyph entries verbatim into the new buffer ──
2519
  let old_glyphs = (*font_obj).glyph_table as *const u8;
 
2934
  font_set_param_impl(font_obj, p3 as *const u8, p4 as *mut i32, p5 as *mut i32);
2935
  1
2936
  }
2937
+
2938
+ /// Pure-Rust port of `Font__SetPalette` (0x4F9F20).
2939
+ ///
2940
+ /// **The function name is misleading** — despite "SetPalette", this routine
2941
+ /// extends the font with two new derived glyphs rather than recoloring an
2942
+ /// existing palette. It is called once during `DDGame__LoadFonts` for the
2943
+ /// digital seven-segment font (`digiwht.fnt`, slot 28), which doesn't ship
2944
+ /// with `'.'` or `';'` glyphs and needs them generated at runtime.
2945
+ ///
2946
+ /// What it does, in order:
2947
+ ///
2948
+ /// 1. Look up the **`'-'`** (0x2D), **`'8'`** (0x38) and **`':'`** (0x3A)
2949
+ /// glyph indices via `char_to_glyph_idx`. If any is missing, return 0.
2950
+ /// 2. Sample the foreground-color palette indices of `'-'` and `'8'` by
2951
+ /// reading byte +5 within each glyph's pixel data. Save them as
2952
+ /// `minus_fg` and `eight_fg`.
2953
+ /// 3. Allocate a new buffer sized for `(old_height + 2)` glyph entries
2954
+ /// plus enough pixel area for **two** glyphs the size of `':'`.
2955
+ /// 4. Copy all existing glyph entries verbatim into the start of the
2956
+ /// buffer.
2957
+ /// 5. Add two new glyph entries:
2958
+ /// - `char_to_glyph_idx[0x2E]` (= `'.'`) → 1-based index `old_height + 1`
2959
+ /// - `char_to_glyph_idx[0x3B]` (= `';'`) → 1-based index `old_height + 2`
2960
+ ///
2961
+ /// Both new entries copy `start_x`/`start_y`/`width`/`height` from the
2962
+ /// `':'` glyph. Their `pixel_offset` fields point at consecutive
2963
+ /// `colon_height * font_width`-byte regions inside the new buffer.
2964
+ /// 6. Initialize the **first** new glyph (`'.'`)'s pixel area:
2965
+ /// - `memset(area, palette_value as u8, colon_height * font_width / 2)`
2966
+ /// — fills approximately the top half with a uniform palette index.
2967
+ /// - For rows `colon_height/2 .. colon_height-1`, copy `colon.width`
2968
+ /// bytes per row from `':'`'s pixel data into the new buffer at
2969
+ /// `font_width`-strided rows.
2970
+ /// 7. Initialize the **second** new glyph (`';'`)'s pixel area: for every
2971
+ /// `(row, col)` in `(0..colon.height, 0..colon.width)`, copy the
2972
+ /// `':'` source pixel; if it equals `eight_fg`, replace it with
2973
+ /// `minus_fg` instead.
2974
+ /// 8. Update `font_obj.glyph_table` to point at the new buffer, store the
2975
+ /// new buffer in `aux_alloc` (so destructors can free it), and bump
2976
+ /// `font_obj.height` by 2.
2977
+ ///
2978
+ /// The original uses `wa_malloc(round_up_to_4(total_size) + 0x20)` and
2979
+ /// zero-initializes the body up to `total_size` (the trailing 0x20 padding
2980
+ /// is left uninitialized).
2981
+ ///
2982
+ /// # Safety
2983
+ /// `font_obj` must be a valid mutable `FontObject`. `palette_value`'s low
2984
+ /// byte is used as a single-byte memset filler. The original's
2985
+ /// allocate/copy/update sequence is preserved verbatim.
2986
+ pub unsafe fn font_set_palette_impl(font_obj: *mut FontObject, palette_value: u32) -> u32 {
2987
+ use crate::wa_alloc::wa_malloc;
2988
+
2989
+ let char_to_glyph = (*font_obj).char_to_glyph_idx;
2990
+ if char_to_glyph.is_null() {
2991
+ return 0;
2992
+ }
2993
+
2994
+ // Step 1: '-' (0x2D) glyph index.
2995
+ let minus_idx = *char_to_glyph.add(0x2D);
2996
+ if minus_idx == 0 {
2997
+ return 0;
2998
+ }
2999
+
3000
+ // Step 2a: read byte +5 of '-' glyph's pixel data.
3001
+ let glyph_table = (*font_obj).glyph_table;
3002
+ let pixel_data = (*font_obj).pixel_data;
3003
+ let minus_glyph = glyph_table.add(minus_idx as usize - 1);
3004
+ let minus_fg = *pixel_data.add((*minus_glyph).pixel_offset as usize + 5);
3005
+
3006
+ // Step 2b: '8' (0x38) glyph index + byte +5.
3007
+ let eight_idx = *char_to_glyph.add(0x38);
3008
+ if eight_idx == 0 {
3009
+ return 0;
3010
+ }
3011
+ let eight_glyph = glyph_table.add(eight_idx as usize - 1);
3012
+ let eight_fg = *pixel_data.add((*eight_glyph).pixel_offset as usize + 5);
3013
+
3014
+ // Step 1c: ':' (0x3A) — required for the template.
3015
+ let colon_idx = *char_to_glyph.add(0x3A);
3016
+ if colon_idx == 0 {
3017
+ return 0;
3018
+ }
3019
+
3020
+ let old_height = (*font_obj).height as i32; // signed via MOVSX in the original
3021
+ let new_height = old_height + 2;
3022
+ let font_width = (*font_obj).width as i16 as i32;
3023
+
3024
+ // Step 3: compute total size and allocate.
3025
+ // total_size = (colon.height * font_width + new_height * 6) * 2
3026
+ // = colon_height * font_width * 2 + new_height * 12
3027
+ // alloc = round_up_to_dword(total_size) + 0x20
3028
+ let colon_glyph = glyph_table.add(colon_idx as usize - 1);
3029
+ let colon_height = (*colon_glyph).height as i32;
3030
+ let colon_width = (*colon_glyph).width as i32;
3031
+ let colon_pixel_offset = (*colon_glyph).pixel_offset as usize;
3032
+
3033
+ let total_size = (colon_height * font_width + new_height * 6) * 2;
3034
+ let alloc_size = ((total_size as u32 + 3) & !3u32) + 0x20;
3035
+ let new_buffer = wa_malloc(alloc_size);
3036
+ if new_buffer.is_null() {
3037
+ return 0;
3038
+ }
3039
+ // Zero-init the body (not the trailing 0x20 padding).
3040
+ core::ptr::write_bytes(new_buffer, 0, total_size as usize);
3041
+
3042
+ // _Dst = new_buffer + new_height*12 = where pixel data starts in the new buffer.
3043
+ let new_glyph_table = new_buffer as *mut GlyphEntry;
3044
+ let new_pixel_area = new_buffer.add((new_height as usize) * 12);
3045
+
3046
+ // Save the allocation in aux_alloc so cleanup paths can free it
3047
+ // (the original writes this *before* the copy loop).
3048
+ (*font_obj).aux_alloc = new_buffer as u32;
3049
+
3050
+ // Step 4: copy existing glyph entries.
3051
+ if old_height > 0 {
3052
+ core::ptr::copy_nonoverlapping(
3053
+ glyph_table as *const u8,
3054
+ new_buffer,
3055
+ (old_height as usize) * 12,
3056
+ );
3057
+ }
3058
+
3059
+ // Step 5: write new char_to_glyph_idx entries for '.' and ';'.
3060
+ *char_to_glyph.add(0x2E) = (old_height as u8).wrapping_add(1);
3061
+ *char_to_glyph.add(0x3B) = (old_height as u8).wrapping_add(2);
3062
+
3063
+ // Step 5b: first new glyph (= colon copy with adjusted pixel_offset).
3064
+ // pixel_offset = (_Dst - pixel_data)
3065
+ let new_first = new_glyph_table.add(old_height as usize);
3066
+ (*new_first).start_x = (*colon_glyph).start_x;
3067
+ (*new_first).start_y = (*colon_glyph).start_y;
3068
+ (*new_first).width = (*colon_glyph).width;
3069
+ (*new_first).height = (*colon_glyph).height;
3070
+ (*new_first).pixel_offset = (new_pixel_area as u32).wrapping_sub(pixel_data as u32);
3071
+
3072
+ // Step 5c: second new glyph (= duplicate of first new glyph, but its
3073
+ // pixel_offset is bumped by colon_height * font_width so it points to
3074
+ // the *next* chunk of new pixel data).
3075
+ let new_second = new_first.add(1);
3076
+ (*new_second).start_x = (*new_first).start_x;
3077
+ (*new_second).start_y = (*new_first).start_y;
3078
+ (*new_second).width = (*new_first).width;
3079
+ (*new_second).height = (*new_first).height;
3080
+ (*new_second).pixel_offset = (*new_first)
3081
+ .pixel_offset
3082
+ .wrapping_add((colon_height * font_width) as u32);
3083
+
3084
+ // Step 6a: memset top portion of FIRST new glyph with palette_value's
3085
+ // low byte. Length = (colon_height * font_width) with sign-correct /2.
3086
+ let memset_len = {
3087
+ let v = colon_height.wrapping_mul(font_width);
3088
+ // CDQ + SUB EAX,EDX + SAR EAX,1 = signed div by 2 with rounding toward zero
3089
+ let v_signed = v;
3090
+ let cdq = v_signed >> 31;
3091
+ ((v_signed - cdq) >> 1) as usize
3092
+ };
3093
+ core::ptr::write_bytes(new_pixel_area, palette_value as u8, memset_len);
3094
+
3095
+ // Step 6b: copy bottom half rows of ':' into FIRST new glyph at
3096
+ // font-width row stride.
3097
+ let half = (colon_height as u32 >> 1) as i32;
3098
+ if half < colon_height {
3099
+ for row in half..colon_height {
3100
+ let src = pixel_data.add(colon_pixel_offset + (row * colon_width) as usize);
3101
+ let dst = new_pixel_area.add((row * font_width) as usize);
3102
+ core::ptr::copy_nonoverlapping(src, dst, colon_width as usize);
3103
+ }
3104
+ }
3105
+
3106
+ // Step 7: pixel-by-pixel copy for SECOND new glyph with eight→minus
3107
+ // foreground substitution. The destination is offset by
3108
+ // colon_height * font_width within new_pixel_area.
3109
+ if colon_height > 0 {
3110
+ for row in 0..colon_height {
3111
+ for col in 0..colon_width {
3112
+ let src_addr =
3113
+ pixel_data.add(colon_pixel_offset + (row * colon_width + col) as usize);
3114
+ let dst_addr =
3115
+ new_pixel_area.add(((colon_height + row) * font_width + col) as usize);
3116
+ let src_byte = *src_addr;
3117
+ if src_byte == eight_fg {
3118
+ *dst_addr = minus_fg;
3119
+ } else {
3120
+ *dst_addr = src_byte;
3121
+ }
3122
+ }
3123
+ }
3124
+ }
3125
+
3126
+ // Step 8: update font_obj fields.
3127
+ (*font_obj).glyph_table = new_glyph_table;
3128
+ (*font_obj).height = new_height as u16;
3129
+ 1
3130
+ }
3131
+
3132
+ /// Port of `DisplayGfx::SetFontPalette` (vtable slot 36, 0x523690).
3133
+ ///
3134
+ /// Despite the name, this is the entry point for `font_set_palette_impl` —
3135
+ /// see that function's doc for what it actually does (it extends the
3136
+ /// digital font with derived `'.'` and `';'` glyphs). The wrapper is just
3137
+ /// a thin index lookup into `font_table`; the original has no bounds or
3138
+ /// null check on the index, so we mirror that.
3139
+ ///
3140
+ /// # Safety
3141
+ /// `this` must be a valid `*mut DisplayGfx` and `font_index` must be a
3142
+ /// valid index into `font_table` whose entry is a non-null `FontObject`.
3143
+ pub unsafe extern "thiscall" fn set_font_palette(
3144
+ this: *mut DisplayGfx,
3145
+ font_index: u32,
3146
+ palette_value: u32,
3147
+ ) {
3148
+ let font_obj = (*this).base.font_table[font_index as usize] as *mut FontObject;
3149
+ let _ = font_set_palette_impl(font_obj, palette_value);
3150
+ }
crates/openwa-dll/src/replacements/render.rs CHANGED
@@ -1296,39 +1296,10 @@ unsafe extern "thiscall" fn draw_scaled_sprite(
1296
  // Font vtable method bridges and implementations
1297
  // =========================================================================
1298
  //
1299
- // Font methods are thin wrappers: validate font_id (1..31), check font slot
1300
- // is populated, then delegate to an internal usercall function on the font
1301
- // object. The font object pointer is stored at DisplayBase+0x309C[font_id].
1302
-
1303
- /// Port of DisplayGfx::SetFontPalette (vtable slot 36, 0x523690).
1304
- ///
1305
- /// Loads font object from font_table[font_count], then calls
1306
- /// Font__SetPalette (0x4f9f20): usercall(ESI=font_obj) + stack(palette_value), RET 0x4.
1307
- ///
1308
- /// The original has NO bounds check on font_count — it trusts the caller.
1309
- unsafe extern "thiscall" fn set_font_palette(
1310
- this: *mut DisplayGfx,
1311
- font_count: u32,
1312
- palette_value: u32,
1313
- ) {
1314
- let font_obj = (*this).base.font_table[font_count as usize];
1315
- wa_font_set_palette(font_obj, palette_value, rb(va::FONT_OBJ_SET_PALETTE));
1316
- }
1317
-
1318
- /// Bridge to Font__SetPalette (0x4f9f20).
1319
- /// Usercall: ESI=font_obj, stack(palette_value), RET 0x4.
1320
- #[unsafe(naked)]
1321
- unsafe extern "cdecl" fn wa_font_set_palette(_font_obj: u32, _palette_value: u32, _target: u32) {
1322
- core::arch::naked_asm!(
1323
- "push esi",
1324
- "mov esi, [esp+8]", // font_obj
1325
- "mov ecx, [esp+16]", // target
1326
- "push dword ptr [esp+12]", // palette_value
1327
- "call ecx", // RET 0x4 cleans 1 param
1328
- "pop esi",
1329
- "ret",
1330
- );
1331
- }
1332
 
1333
  /// Port of DisplayGfx::DrawTextOnBitmap (vtable slot 7, 0x5236B0).
1334
  ///
@@ -1552,7 +1523,7 @@ fn install_display() -> Result<(), String> {
1552
  draw_scaled_sprite => draw_scaled_sprite,
1553
  set_layer_visibility => display_vtable::set_layer_visibility,
1554
  update_palette => display_vtable::update_palette,
1555
- set_font_palette => set_font_palette,
1556
  slot 19 => blit_sprite,
1557
  load_sprite_by_layer => load_sprite_by_layer,
1558
  load_font => load_font,
 
1296
  // Font vtable method bridges and implementations
1297
  // =========================================================================
1298
  //
1299
+ // Font slot wrappers (slots 7/8/9/10/34/35/36) all live in openwa-core's
1300
+ // `display::vtable` module. The wrappers below for slots 7/31/34/35/37 are
1301
+ // thin forwarders only because they need to capture a bridge function
1302
+ // pointer (`wa_load_sprite_from_vfs` etc.) at install time.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1303
 
1304
  /// Port of DisplayGfx::DrawTextOnBitmap (vtable slot 7, 0x5236B0).
1305
  ///
 
1523
  draw_scaled_sprite => draw_scaled_sprite,
1524
  set_layer_visibility => display_vtable::set_layer_visibility,
1525
  update_palette => display_vtable::update_palette,
1526
+ set_font_palette => display_vtable::set_font_palette,
1527
  slot 19 => blit_sprite,
1528
  load_sprite_by_layer => load_sprite_by_layer,
1529
  load_font => load_font,