feat: port DisplayGfx::LoadFontExtension (slot 35) to pure Rust
Browse filesAdds 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`.
|
| 2069 |
-
///
|
| 2070 |
-
///
|
| 2071 |
-
///
|
|
|
|
|
|
|
|
|
|
| 2072 |
#[repr(C)]
|
| 2073 |
pub struct GlyphEntry {
|
| 2074 |
-
/// 0x00:
|
| 2075 |
-
pub
|
| 2076 |
-
///
|
| 2077 |
-
pub
|
| 2078 |
-
///
|
| 2079 |
-
pub
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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(())
|