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

feat: port DisplayGfx::LoadFontExtension (slot 35) to pure Rust

Browse files

Adds palette_context_lookup_entry (FUN_00541200) and
palette_find_nearest_cached (FUN_00541340) helpers, ports the .fex loader
chain (FUN_004f9ad0 fopen/fread/fclose + bounding-box scan + LUT remap),
and corrects GlyphEntry layout so the +4 field is recognised as `width`
to match how Font__GetMetric reads it. DisplayGfx vtable patches now
cover 31 of 38 slots.

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

crates/openwa-core/src/address.rs CHANGED
@@ -767,6 +767,10 @@ pub mod va {
767
  fn/Cdecl WA_FREE = 0x005D_0D2B;
768
  /// WA's CRT _fopen
769
  fn/Cdecl WA_FOPEN = 0x005D_3271;
 
 
 
 
770
  /// WA's CRT _fileno
771
  fn/Cdecl WA_FILENO = 0x005D_5155;
772
  /// WA's CRT _get_osfhandle
 
767
  fn/Cdecl WA_FREE = 0x005D_0D2B;
768
  /// WA's CRT _fopen
769
  fn/Cdecl WA_FOPEN = 0x005D_3271;
770
+ /// WA's CRT fread
771
+ fn/Cdecl WA_FREAD = 0x005D_4531;
772
+ /// WA's CRT fclose
773
+ fn/Cdecl WA_FCLOSE = 0x005D_399B;
774
  /// WA's CRT _fileno
775
  fn/Cdecl WA_FILENO = 0x005D_5155;
776
  /// WA's CRT _get_osfhandle
crates/openwa-core/src/render/display/vtable.rs CHANGED
@@ -2065,18 +2065,27 @@ 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);
@@ -2339,3 +2348,325 @@ pub unsafe fn load_font(
2339
 
2340
  1
2341
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2065
 
2066
  /// Per-glyph metadata in a `.fnt` file (12 bytes).
2067
  ///
2068
+ /// Indexed by `FontObject::glyph_table`. Layout matches the loop stride of 0xC
2069
+ /// used by the parser, `Font__GetInfo` (0x4fa7d0), and `Font__GetMetric` (0x4fa780).
2070
+ ///
2071
+ /// `Font__GetMetric` reads `width` (offset +4) as the character advance metric.
2072
+ /// `Font__DrawText` uses `pixel_offset` as a byte delta from
2073
+ /// `FontObject::pixel_data` to locate the glyph's bitmap rows. The offset can
2074
+ /// be negative when `font_extend` adds glyphs from a separate allocation.
2075
  #[repr(C)]
2076
  pub struct GlyphEntry {
2077
+ /// 0x00: Glyph bounding-box top-left X within the glyph cell.
2078
+ pub start_x: u16,
2079
+ /// 0x02: Glyph bounding-box top-left Y within the glyph cell.
2080
+ pub start_y: u16,
2081
+ /// 0x04: Glyph bounding-box width in pixels (also used as the advance metric).
2082
+ pub width: u16,
2083
+ /// 0x06: Glyph bounding-box height in pixels.
2084
+ pub height: u16,
2085
+ /// 0x08: Byte delta from `FontObject::pixel_data` to this glyph's bitmap.
2086
+ /// Treated as a signed offset by drawing code so `font_extend` can point
2087
+ /// glyphs at pixels that live in a different allocation.
2088
+ pub pixel_offset: u32,
2089
  }
2090
 
2091
  const _: () = assert!(core::mem::size_of::<GlyphEntry>() == 0xC);
 
2348
 
2349
  1
2350
  }
2351
+
2352
+ /// Port of `FUN_004f9ad0` — extends an existing `FontObject` with new glyphs
2353
+ /// loaded from a `.fex` (font extension) file.
2354
+ ///
2355
+ /// Original convention: usercall with `EAX = filename`, `ESI = font_obj`,
2356
+ /// stack args = (`layer_ctx`, `char_map`, `palette_value`), `RET 0xC`.
2357
+ ///
2358
+ /// The `.fex` file contains raw pixel data for new glyphs (no header). Each
2359
+ /// glyph occupies `font.width * stride` bytes (where `stride = font.width`,
2360
+ /// or `font.width + 1` for fonts with `width >= 16`). The new bitmap also
2361
+ /// gets remapped through a 256-byte LUT built by walking R/G/B in lockstep
2362
+ /// with the per-channel steps packed into `palette_value` and using
2363
+ /// `palette_find_nearest_cached` to map each composed RGB to a palette index.
2364
+ ///
2365
+ /// On success the function:
2366
+ /// - allocates a single new buffer holding `[old + new glyph entries][new pixels]`
2367
+ /// - copies the old glyph entries verbatim into the front of the new buffer
2368
+ /// - writes per-codepoint indices into the font's char-width table (the bytes
2369
+ /// referenced by `font_obj.palette_end`)
2370
+ /// - reads the new pixel rows from disk via `_fread`
2371
+ /// - computes a tight bounding box per new glyph and writes it as a new entry
2372
+ /// - remaps every new pixel byte through the LUT
2373
+ /// - swaps `font_obj.glyph_table` to the new buffer and bumps `font_obj.height`
2374
+ /// - records the allocation in `font_obj._unknown_18`
2375
+ ///
2376
+ /// The `pixel_offset` field in each new glyph entry stores
2377
+ /// `(absolute_pixel_addr - font_obj.pixel_data)` so the existing
2378
+ /// `Font__DrawText` lookup `pixel_data + offset` still resolves correctly,
2379
+ /// even though the new pixels live in a separate allocation.
2380
+ ///
2381
+ /// # Safety
2382
+ /// `font_obj` must be a fully-parsed `FontObject` (created by `load_font`).
2383
+ /// `palette_ctx` must be a valid `PaletteContext`. `filename` and `char_map`
2384
+ /// must be valid null-terminated C strings.
2385
+ pub unsafe fn font_extend(
2386
+ font_obj: *mut FontObject,
2387
+ palette_ctx: *mut PaletteContext,
2388
+ filename: *const core::ffi::c_char,
2389
+ char_map: *const u8,
2390
+ palette_value: u32,
2391
+ ) {
2392
+ use crate::address::va;
2393
+ use crate::rebase::rb;
2394
+ use crate::render::palette::{palette_find_nearest_cached, remap_pixels_through_lut};
2395
+ use crate::wa_alloc::wa_malloc;
2396
+ use core::ffi::c_char;
2397
+
2398
+ // Open the .fex file via WA's CRT _fopen.
2399
+ let fopen: unsafe extern "cdecl" fn(*const c_char, *const c_char) -> *mut u8 =
2400
+ core::mem::transmute(rb(va::WA_FOPEN) as usize);
2401
+ let fread: unsafe extern "cdecl" fn(*mut u8, u32, u32, *mut u8) -> u32 =
2402
+ core::mem::transmute(rb(va::WA_FREAD) as usize);
2403
+ let fclose: unsafe extern "cdecl" fn(*mut u8) -> i32 =
2404
+ core::mem::transmute(rb(va::WA_FCLOSE) as usize);
2405
+
2406
+ let file = fopen(filename, c"rb".as_ptr());
2407
+ if file.is_null() {
2408
+ return;
2409
+ }
2410
+
2411
+ // ── Geometry ──
2412
+ let font_width = (*font_obj).width as i32;
2413
+ // Per-glyph row stride in the .fex file. Wide fonts use width+1 to allow
2414
+ // an extra byte after each row (matches the `if (font.width > 15)` test).
2415
+ let row_stride = if font_width > 15 {
2416
+ font_width + 1
2417
+ } else {
2418
+ font_width
2419
+ };
2420
+
2421
+ // strlen(char_map) — count of new glyphs.
2422
+ let mut new_count: i32 = 0;
2423
+ let mut p = char_map;
2424
+ while *p != 0 {
2425
+ p = p.add(1);
2426
+ new_count += 1;
2427
+ }
2428
+
2429
+ let old_height = (*font_obj).height as i32; // existing glyph count
2430
+ let total_glyphs = old_height + new_count; // resulting glyph count
2431
+ let glyph_entries_bytes = total_glyphs * 12;
2432
+ let new_pixels_bytes = new_count * font_width * row_stride;
2433
+ let total_size = (glyph_entries_bytes + new_pixels_bytes) as u32;
2434
+
2435
+ // Allocate (matches WA_MallocMemset alignment + 0x20 guard).
2436
+ let alloc = wa_malloc(((total_size + 3) & !3u32).wrapping_add(0x20));
2437
+ if alloc.is_null() {
2438
+ let _ = fclose(file);
2439
+ return;
2440
+ }
2441
+ core::ptr::write_bytes(alloc, 0, total_size as usize);
2442
+
2443
+ // pixel data area starts immediately after the glyph entries.
2444
+ let new_pixels = alloc.add(glyph_entries_bytes as usize);
2445
+
2446
+ // Record the allocation so font cleanup paths can free it.
2447
+ (*font_obj)._unknown_18 = alloc as u32;
2448
+
2449
+ // ── Copy existing glyph entries verbatim into the new buffer ──
2450
+ let old_glyphs = (*font_obj).glyph_table as *const u8;
2451
+ if old_height > 0 && !old_glyphs.is_null() {
2452
+ core::ptr::copy_nonoverlapping(old_glyphs, alloc, (old_height * 12) as usize);
2453
+ }
2454
+
2455
+ // ── Update the per-codepoint char-width table ──
2456
+ // For each new char in char_map, write `(old_height + idx + 1)` into
2457
+ // char_widths[char_byte]. The base table lives at `font_obj.palette_end`
2458
+ // (set by the original .fnt parser).
2459
+ let char_widths = (*font_obj).palette_end;
2460
+ let mut q = char_map;
2461
+ for i in 0..new_count {
2462
+ let ch = *q as usize;
2463
+ q = q.add(1);
2464
+ *char_widths.add(ch) = (old_height + i + 1) as u8;
2465
+ }
2466
+
2467
+ // ── Build 256-byte palette LUT by stepping R/G/B per `palette_value` ──
2468
+ // The original walks 256 entries, accumulating signed R/G/B values via the
2469
+ // step bytes packed in `palette_value` (low byte=R step, mid=G, high=B),
2470
+ // each reduced mod 255 with sign correction (the `IMUL 0x80808081` trick).
2471
+ // Each composed RGB is fed to `palette_find_nearest_cached` and the
2472
+ // returned palette index becomes lut[i].
2473
+ let r_step = (palette_value & 0xff) as i32;
2474
+ let g_step = ((palette_value >> 8) & 0xff) as i32;
2475
+ let b_step = ((palette_value >> 16) & 0xff) as i32;
2476
+
2477
+ let mut palette_lut = [0u8; 256];
2478
+ let mut acc_r: i32 = 0;
2479
+ let mut acc_g: i32 = 0;
2480
+ let mut acc_b: i32 = 0;
2481
+ for slot in palette_lut.iter_mut() {
2482
+ // Reduce each accumulator mod 255 with sign correction. The original
2483
+ // uses signed magic division by 255; for 0..255 the result is just
2484
+ // `acc - 0` for 0..254 and `acc - 1` for 255. We use rem_euclid for
2485
+ // a clean cross-platform equivalent (palette steps are non-negative
2486
+ // so this matches the original even on edge cases).
2487
+ let r = mod_255_signed(acc_r);
2488
+ let g = mod_255_signed(acc_g);
2489
+ let b = mod_255_signed(acc_b);
2490
+ let composed = (r as u32) | ((g as u32) << 8) | ((b as u32) << 16);
2491
+
2492
+ let mut distance: i32 = 0;
2493
+ let idx = palette_find_nearest_cached(palette_ctx, composed, &mut distance) as u8;
2494
+ *slot = idx;
2495
+
2496
+ acc_r += r_step;
2497
+ acc_g += g_step;
2498
+ acc_b += b_step;
2499
+ }
2500
+
2501
+ // ── Read the new pixel rows from disk ──
2502
+ // fread(new_pixels, new_count, font_width * row_stride, file)
2503
+ let _ = fread(
2504
+ new_pixels,
2505
+ new_count as u32,
2506
+ (font_width * row_stride) as u32,
2507
+ file,
2508
+ );
2509
+ let _ = fclose(file);
2510
+
2511
+ // ── Bounding-box scan for each new glyph + write its glyph entry ──
2512
+ let mut row_offset_in_pixels: i32 = 0; // i.e. iVar16 in the original
2513
+ let new_glyph_base = alloc as *mut GlyphEntry;
2514
+ for new_idx in 0..new_count {
2515
+ // Find the LEFTMOST non-empty column (== start_x).
2516
+ let mut start_x: u16 = 0;
2517
+ let mut found_left = false;
2518
+ 'outer_left: for col in 0..font_width as u16 {
2519
+ for row in 0..font_width as i32 {
2520
+ let addr = new_pixels
2521
+ .offset(((row + row_offset_in_pixels) * font_width + col as i32) as isize);
2522
+ if *addr != 0 {
2523
+ start_x = col;
2524
+ found_left = true;
2525
+ break 'outer_left;
2526
+ }
2527
+ }
2528
+ }
2529
+ // Find the TOPMOST non-empty row (== start_y).
2530
+ let mut start_y: u16 = 0;
2531
+ if found_left {
2532
+ 'outer_top: for row in 0..font_width as u16 {
2533
+ for col in 0..font_width as i32 {
2534
+ let addr = new_pixels
2535
+ .offset((col + (row as i32 + row_offset_in_pixels) * font_width) as isize);
2536
+ if *addr != 0 {
2537
+ start_y = row;
2538
+ break 'outer_top;
2539
+ }
2540
+ }
2541
+ }
2542
+ }
2543
+ // Find the RIGHTMOST non-empty column (scan right→left from font_width-1).
2544
+ let mut right: u16 = (font_width - 1).max(0) as u16;
2545
+ if found_left && right != 0 {
2546
+ 'outer_right: for col in (1..font_width as u16).rev() {
2547
+ let mut empty = true;
2548
+ for row in 0..font_width as i32 {
2549
+ let addr = new_pixels
2550
+ .offset(((row + row_offset_in_pixels) * font_width + col as i32) as isize);
2551
+ if *addr != 0 {
2552
+ empty = false;
2553
+ break;
2554
+ }
2555
+ }
2556
+ if !empty {
2557
+ right = col;
2558
+ break 'outer_right;
2559
+ }
2560
+ right = col - 1;
2561
+ }
2562
+ }
2563
+ // Find the BOTTOMMOST non-empty row (scan bottom→top from font_width-1).
2564
+ let mut bottom: u16 = (font_width - 1).max(0) as u16;
2565
+ if found_left {
2566
+ loop {
2567
+ let mut empty = true;
2568
+ for col in 0..font_width as i32 {
2569
+ let addr = new_pixels.offset(
2570
+ (col + (bottom as i32 + row_offset_in_pixels) * font_width) as isize,
2571
+ );
2572
+ if *addr != 0 {
2573
+ empty = false;
2574
+ break;
2575
+ }
2576
+ }
2577
+ if !empty || bottom == 0 {
2578
+ break;
2579
+ }
2580
+ bottom -= 1;
2581
+ }
2582
+ }
2583
+
2584
+ // Write the new glyph entry. The slot index is (old_height + new_idx).
2585
+ let entry = new_glyph_base.add((old_height + new_idx) as usize);
2586
+ (*entry).start_x = start_x;
2587
+ (*entry).start_y = start_y;
2588
+ (*entry).width = right.saturating_sub(start_x) + 1;
2589
+ (*entry).height = bottom.saturating_sub(start_y) + 1;
2590
+ // pixel_offset = absolute address of the glyph's bitmap top-left,
2591
+ // expressed as a delta from font_obj.pixel_data so existing draw code
2592
+ // can do `pixel_data + offset` and reach the new allocation.
2593
+ let abs_addr = new_pixels.offset(
2594
+ (start_x as i32 + (start_y as i32 + row_offset_in_pixels) * font_width) as isize,
2595
+ );
2596
+ (*entry).pixel_offset = (abs_addr as u32).wrapping_sub((*font_obj).pixel_data as u32);
2597
+
2598
+ // Advance to the next glyph's pixel rows.
2599
+ row_offset_in_pixels += row_stride;
2600
+ }
2601
+
2602
+ // ── Remap every new pixel byte through the LUT ──
2603
+ let total_pixel_bytes = font_width * new_count * row_stride;
2604
+ if total_pixel_bytes > 0 {
2605
+ // remap_pixels_through_lut takes (data, pitch, lut, width_dwords, height).
2606
+ // The original walks each byte one at a time, so use width_dwords = 1
2607
+ // and height = total_bytes / 4 ... actually simpler: do a flat single-row
2608
+ // remap with width_dwords = total/4 and height = 1.
2609
+ let dword_count = (total_pixel_bytes as u32 + 3) / 4;
2610
+ remap_pixels_through_lut(new_pixels, 0, palette_lut.as_ptr(), dword_count, 1);
2611
+ }
2612
+
2613
+ // ── Swap glyph_table and bump glyph count ──
2614
+ (*font_obj).glyph_table = alloc as *mut GlyphEntry;
2615
+ (*font_obj).height = (old_height + new_count) as u16;
2616
+ }
2617
+
2618
+ /// Helper for `font_extend`'s palette stepping. Reduces a non-negative i32
2619
+ /// modulo 255 the way the original signed-magic-division sequence does.
2620
+ #[inline]
2621
+ fn mod_255_signed(value: i32) -> i32 {
2622
+ // For non-negative values this is just `value % 255`.
2623
+ // The original uses signed magic-division so we mirror that for safety.
2624
+ let q = value / 255;
2625
+ value - q * 255
2626
+ }
2627
+
2628
+ /// Port of `DisplayGfx::LoadFontExtension` (vtable slot 35, 0x523620).
2629
+ ///
2630
+ /// Validates `font_id`, looks up the existing font object, resolves the
2631
+ /// palette context for the font's mode (recorded in `_gap_301c` by `load_font`),
2632
+ /// resolves the RGB color from `palette_value` via `palette_context_lookup_entry`,
2633
+ /// and dispatches to `font_extend`.
2634
+ ///
2635
+ /// # Safety
2636
+ /// `this` must be a valid `*mut DisplayGfx`. `path` and `char_map` must be
2637
+ /// valid null-terminated C strings.
2638
+ pub unsafe fn load_font_extension(
2639
+ this: *mut DisplayGfx,
2640
+ font_id: i32,
2641
+ path: *const core::ffi::c_char,
2642
+ char_map: *const u8,
2643
+ palette_value: u32,
2644
+ _flag: i32,
2645
+ ) -> u32 {
2646
+ use crate::render::palette::palette_context_lookup_entry;
2647
+
2648
+ if !(1..=31).contains(&font_id) {
2649
+ return 0;
2650
+ }
2651
+ let base = &mut (*this).base;
2652
+ let font_obj_addr = base.font_table[font_id as usize];
2653
+ if font_obj_addr == 0 {
2654
+ return 0;
2655
+ }
2656
+ let font_obj = font_obj_addr as *mut FontObject;
2657
+
2658
+ // Resolve the RGB color via the layer-1 palette context (DisplayBase+0x3120).
2659
+ // The original always reads layer_contexts[1] here, regardless of which
2660
+ // mode owns the font. This matches the disassembly at 0x52364d:
2661
+ // `MOV ECX, [EDI+0x3120]` (= layer_contexts[1])
2662
+ let layer1_ctx = base.layer_contexts[1] as *mut PaletteContext;
2663
+ let mut resolved_rgb: u32 = 0;
2664
+ let _ = palette_context_lookup_entry(layer1_ctx, palette_value as i32, &mut resolved_rgb);
2665
+
2666
+ // The actual font extension call uses the font's owning mode's palette ctx.
2667
+ let mode = base._gap_301c[font_id as usize] as usize;
2668
+ let layer_ctx = base.layer_contexts[mode] as *mut PaletteContext;
2669
+
2670
+ font_extend(font_obj, layer_ctx, path, char_map, resolved_rgb);
2671
+ 1
2672
+ }
crates/openwa-core/src/render/palette.rs CHANGED
@@ -102,6 +102,112 @@ pub unsafe fn palette_map_color(ctx: *mut PaletteContext, rgb: u32) -> u32 {
102
  slot_idx as u32
103
  }
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  /// Remap each pixel in a buffer through a 256-byte lookup table.
106
  ///
107
  /// Port of FUN_005b2beb (stdcall, RET 0x14). Used by `load_sprite_by_name`
 
102
  slot_idx as u32
103
  }
104
 
105
+ /// Look up an existing palette entry by slot index.
106
+ ///
107
+ /// Rust port of `FUN_00541200` (usercall: ECX=ctx, EAX=index, stack=out_rgb,
108
+ /// RET 0x4). Validates that the index is within the context's `dirty_range_min`
109
+ /// / `dirty_range_max` window AND that the slot is currently in use, then
110
+ /// writes the slot's stored RGB to `out_rgb` and returns 1. On any validation
111
+ /// failure, returns 0 without touching `out_rgb`.
112
+ ///
113
+ /// Note: despite the field names, `dirty_range_min/max` here behave as a
114
+ /// generic "valid index range" — `FUN_00541200` is a pure lookup, not part
115
+ /// of an update batch.
116
+ ///
117
+ /// # Safety
118
+ /// `ctx` must point to a valid `PaletteContext`. `out_rgb` must point to a
119
+ /// writable u32 (only written on success).
120
+ pub unsafe fn palette_context_lookup_entry(
121
+ ctx: *mut PaletteContext,
122
+ index: i32,
123
+ out_rgb: *mut u32,
124
+ ) -> u32 {
125
+ let range_min = (*ctx).dirty_range_min as i32;
126
+ let range_max = (*ctx).dirty_range_max as i32;
127
+ if index < range_min || index > range_max {
128
+ return 0;
129
+ }
130
+ if (*ctx).in_use[index as usize] == 0 {
131
+ return 0;
132
+ }
133
+ *out_rgb = (*ctx).rgb_table[index as usize];
134
+ 1
135
+ }
136
+
137
+ /// Find the closest palette index for an RGB color in the recently-mapped cache.
138
+ ///
139
+ /// Rust port of `FUN_00541340` (usercall: EDI=ctx, stack=rgb, stack=out_distance).
140
+ /// Walks `cache[0..cache_count]` and computes a perceptual distance
141
+ /// `5*|dG| + 2*|dB| + 3*|dR|` against each cached slot's stored RGB.
142
+ ///
143
+ /// Returns the cached slot index of the closest match, or 0 if the cache is
144
+ /// empty / contains no usable entries. Writes a 0..100 distance score to
145
+ /// `*out_distance` (0 means exact match), unless the cache had no entries.
146
+ ///
147
+ /// Differs from `palette_map_color`: this only searches the cache, never
148
+ /// allocates a new slot, and reports the perceptual distance to the caller.
149
+ /// Used by font palette LUT building to gauge how well an interpolated color
150
+ /// matches the existing palette.
151
+ ///
152
+ /// # Safety
153
+ /// `ctx` must point to a valid `PaletteContext`. `out_distance` must point
154
+ /// to a writable i32.
155
+ pub unsafe fn palette_find_nearest_cached(
156
+ ctx: *mut PaletteContext,
157
+ rgb: u32,
158
+ out_distance: *mut i32,
159
+ ) -> u32 {
160
+ let p = ctx as *mut u8;
161
+ let cache_count = *(p.add(0x606) as *const i16);
162
+ if cache_count <= 0 {
163
+ return 0;
164
+ }
165
+
166
+ let target_r = (rgb & 0xff) as i32;
167
+ let target_g = ((rgb >> 8) & 0xff) as i32;
168
+ let target_b = ((rgb >> 16) & 0xff) as i32;
169
+
170
+ let mut best_dist = i32::MAX;
171
+ let mut best_idx: u32 = 0;
172
+ let mut last_idx: u32 = 0;
173
+
174
+ for i in 0..cache_count as usize {
175
+ let slot = *p.add(0x608 + i) as u32;
176
+ last_idx = slot;
177
+ if slot == 0 {
178
+ continue;
179
+ }
180
+ // rgb_table entries are u32 in low 3 bytes (R, G, B). Read each byte.
181
+ let entry_base = p.add(0x04 + slot as usize * 4);
182
+ let er = *entry_base as i32;
183
+ let eg = *entry_base.add(1) as i32;
184
+ let eb = *entry_base.add(2) as i32;
185
+
186
+ let dr = (er - target_r).abs();
187
+ let dg = (eg - target_g).abs();
188
+ let db = (eb - target_b).abs();
189
+ let dist = dg * 5 + db * 2 + dr * 3;
190
+
191
+ if dist == 0 {
192
+ *out_distance = 0;
193
+ return slot;
194
+ }
195
+ if dist < best_dist {
196
+ best_dist = dist;
197
+ best_idx = slot;
198
+ }
199
+ }
200
+
201
+ if best_idx != 0 {
202
+ // Original computes (best_dist * 100) / 0x2fd ≈ scaled to 0..100
203
+ *out_distance = (best_dist * 100) / 0x2fd;
204
+ best_idx
205
+ } else {
206
+ // No usable entries — return whatever last slot we saw (matches original).
207
+ last_idx
208
+ }
209
+ }
210
+
211
  /// Remap each pixel in a buffer through a 256-byte lookup table.
212
  ///
213
  /// Port of FUN_005b2beb (stdcall, RET 0x14). Used by `load_sprite_by_name`
crates/openwa-dll/src/replacements/render.rs CHANGED
@@ -1627,6 +1627,17 @@ unsafe extern "thiscall" fn load_font(
1627
  display_vtable::load_font(this, mode, font_id, gfx, filename)
1628
  }
1629
 
 
 
 
 
 
 
 
 
 
 
 
1630
  // ---------------------------------------------------------------------------
1631
  // Display installation
1632
  // ---------------------------------------------------------------------------
@@ -1696,8 +1707,9 @@ fn install_display() -> Result<(), String> {
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(())
 
1627
  display_vtable::load_font(this, mode, font_id, gfx, filename)
1628
  }
1629
 
1630
+ unsafe extern "thiscall" fn load_font_extension(
1631
+ this: *mut DisplayGfx,
1632
+ font_id: i32,
1633
+ path: *const core::ffi::c_char,
1634
+ char_map: *const u8,
1635
+ palette_value: u32,
1636
+ flag: i32,
1637
+ ) -> u32 {
1638
+ display_vtable::load_font_extension(this, font_id, path, char_map, palette_value, flag)
1639
+ }
1640
+
1641
  // ---------------------------------------------------------------------------
1642
  // Display installation
1643
  // ---------------------------------------------------------------------------
 
1707
  slot 19 => blit_sprite,
1708
  load_sprite_by_layer => load_sprite_by_layer,
1709
  load_font => load_font,
1710
+ load_font_extension => load_font_extension,
1711
  })?;
1712
+ let _ = log_line("[Display] DisplayGfx: patched 31 methods -> Rust");
1713
  }
1714
 
1715
  Ok(())