paavohtl Claude Opus 4.6 (1M context) commited on
Commit
bdfa78f
·
1 Parent(s): 4f85ac3

refactor: introduce Layer newtype, type layer_contexts and palette_ctx

Browse files

Adds `Layer(NonZeroU8)` covering the 1..=3 display-layer index used by
`set_layer_color`, `set_active_layer`, `set_layer_visibility`,
`load_sprite`, `load_font`, etc. Vtable signatures still take i32/u32
to match WA's binary contract; conversion happens at function entry,
which both deletes the open-coded `(layer as u32).wrapping_sub(1) >= 3`
checks and statically guarantees array indices are in range.

Bundled with the matching pointer-typing pass:

- `DisplayBase::layer_contexts: [u32; 4]` -> `[*mut PaletteContext; 4]`
- `LayerSprite::palette_ctx: u32` -> `*mut PaletteContext`
- `wa_load_sprite_from_vfs` and the `load_sprite_from_vfs` callback
param retyped from `u32` to `*mut PaletteContext`

Both types are 4 bytes on i686-pc-windows-msvc, so the struct layouts
and bridge calling conventions are unchanged. Replaces every
`if layer_ctx == 0` with `if layer_ctx.is_null()` and deletes the dead
`is_valid_layer` helper.

Verification: cargo test (4 new Layer tests pass), 15/15 headless
replay tests pass. Pure type refactor with no logic changes, so no
headful verification needed.

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

crates/openwa-core/src/render/display/base.rs CHANGED
@@ -25,6 +25,7 @@
25
  use crate::{
26
  render::{
27
  display::font::Font,
 
28
  sprite::{Sprite, SpriteBank},
29
  },
30
  task::base::Vtable,
@@ -65,9 +66,10 @@ pub struct DisplayBase<V: Vtable = *const DisplayBaseVtable> {
65
  // Indexed by font_id (valid range 1-31). Used by GetFontInfo, GetFontMetric, SetFontParam.
66
  pub font_table: [*mut Font; 32],
67
  // +0x311C..0x312C: layer context pointers (4 entries), zeroed by ctor.
68
- // Indexed by layer (valid range 1-3). Returned by set_active_layer (vtable slot 5).
69
- // Used as palette data input for update_palette (vtable slot 24).
70
- pub layer_contexts: [u32; 4],
 
71
  // +0x312C: Palette slot table guard. Always 0 — prevents palette_slot_alloc
72
  // from allocating starting at index 0. Part of the contiguous scan area:
73
  // [slot_table_guard(0)] [slot_table(1s)...] [slot_table_sentinel(-1)].
 
25
  use crate::{
26
  render::{
27
  display::font::Font,
28
+ palette::PaletteContext,
29
  sprite::{Sprite, SpriteBank},
30
  },
31
  task::base::Vtable,
 
66
  // Indexed by font_id (valid range 1-31). Used by GetFontInfo, GetFontMetric, SetFontParam.
67
  pub font_table: [*mut Font; 32],
68
  // +0x311C..0x312C: layer context pointers (4 entries), zeroed by ctor.
69
+ // Indexed by `Layer` (valid range 1-3); index 0 unused. Returned by
70
+ // set_active_layer (vtable slot 5). Used as palette data input for
71
+ // update_palette (vtable slot 24). `null_mut()` = no context allocated.
72
+ pub layer_contexts: [*mut PaletteContext; 4],
73
  // +0x312C: Palette slot table guard. Always 0 — prevents palette_slot_alloc
74
  // from allocating starting at index 0. Part of the contiguous scan area:
75
  // [slot_table_guard(0)] [slot_table(1s)...] [slot_table_sentinel(-1)].
crates/openwa-core/src/render/display/layer.rs ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Display layer index newtype.
2
+
3
+ use core::num::NonZeroU8;
4
+
5
+ /// Display layer index — valid values are 1, 2, 3.
6
+ ///
7
+ /// Used as the index for `DisplayBase::layer_contexts`,
8
+ /// `DisplayBase::layer_visibility`, and as the value space stored in
9
+ /// `DisplayBase::font_layers`. Surfaces in `DisplayGfx` vtable slots:
10
+ ///
11
+ /// - slot 4 `set_layer_color`
12
+ /// - slot 5 `set_active_layer`
13
+ /// - slot 23 `set_layer_visibility`
14
+ /// - slot 34 `load_font` (the WA "mode" parameter — same value space)
15
+ ///
16
+ /// Stored as `NonZeroU8` so `Option<Layer>` is one byte and round-trips
17
+ /// through 0 = "no layer" sentinels in adjacent code.
18
+ ///
19
+ /// The vtable signatures themselves still take `i32`/`u32` because they
20
+ /// match WA's binary contract; conversion happens at the function entry
21
+ /// via [`Layer::try_from_i32`] / [`Layer::try_from_u32`].
22
+ #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
23
+ #[repr(transparent)]
24
+ pub struct Layer(NonZeroU8);
25
+
26
+ impl Layer {
27
+ /// Layer 1 — backdrop (gradient/landscape).
28
+ pub const ONE: Layer = match NonZeroU8::new(1) {
29
+ Some(n) => Layer(n),
30
+ None => unreachable!(),
31
+ };
32
+ /// Layer 2 — water/foreground sprites.
33
+ pub const TWO: Layer = match NonZeroU8::new(2) {
34
+ Some(n) => Layer(n),
35
+ None => unreachable!(),
36
+ };
37
+ /// Layer 3 — main game layer.
38
+ pub const THREE: Layer = match NonZeroU8::new(3) {
39
+ Some(n) => Layer(n),
40
+ None => unreachable!(),
41
+ };
42
+
43
+ /// Construct from a raw `u8`. Returns `None` outside `1..=3`.
44
+ pub const fn new(value: u8) -> Option<Self> {
45
+ match value {
46
+ 1 | 2 | 3 => match NonZeroU8::new(value) {
47
+ Some(n) => Some(Layer(n)),
48
+ None => None,
49
+ },
50
+ _ => None,
51
+ }
52
+ }
53
+
54
+ /// Construct from a signed integer (vtable param). `None` outside `1..=3`.
55
+ #[inline]
56
+ pub const fn try_from_i32(value: i32) -> Option<Self> {
57
+ if value >= 1 && value <= 3 {
58
+ Self::new(value as u8)
59
+ } else {
60
+ None
61
+ }
62
+ }
63
+
64
+ /// Construct from an unsigned integer (vtable param). `None` outside `1..=3`.
65
+ #[inline]
66
+ pub const fn try_from_u32(value: u32) -> Option<Self> {
67
+ if value >= 1 && value <= 3 {
68
+ Self::new(value as u8)
69
+ } else {
70
+ None
71
+ }
72
+ }
73
+
74
+ /// Index into `[T; 4]` arrays keyed by layer (slot 0 unused).
75
+ #[inline]
76
+ pub const fn idx(self) -> usize {
77
+ self.0.get() as usize
78
+ }
79
+
80
+ /// Raw value as `u32`.
81
+ #[inline]
82
+ pub const fn as_u32(self) -> u32 {
83
+ self.0.get() as u32
84
+ }
85
+
86
+ /// Raw value as `i32`.
87
+ #[inline]
88
+ pub const fn as_i32(self) -> i32 {
89
+ self.0.get() as i32
90
+ }
91
+ }
92
+
93
+ #[cfg(test)]
94
+ mod tests {
95
+ use super::*;
96
+
97
+ #[test]
98
+ fn rejects_out_of_range() {
99
+ assert!(Layer::try_from_i32(0).is_none());
100
+ assert!(Layer::try_from_i32(-1).is_none());
101
+ assert!(Layer::try_from_i32(4).is_none());
102
+ assert!(Layer::try_from_u32(0).is_none());
103
+ assert!(Layer::try_from_u32(4).is_none());
104
+ }
105
+
106
+ #[test]
107
+ fn accepts_in_range() {
108
+ assert_eq!(Layer::try_from_i32(1), Some(Layer::ONE));
109
+ assert_eq!(Layer::try_from_i32(2), Some(Layer::TWO));
110
+ assert_eq!(Layer::try_from_i32(3), Some(Layer::THREE));
111
+ }
112
+
113
+ #[test]
114
+ fn idx_round_trip() {
115
+ assert_eq!(Layer::ONE.idx(), 1);
116
+ assert_eq!(Layer::TWO.idx(), 2);
117
+ assert_eq!(Layer::THREE.idx(), 3);
118
+ }
119
+
120
+ #[test]
121
+ fn option_layer_is_one_byte() {
122
+ assert_eq!(core::mem::size_of::<Option<Layer>>(), 1);
123
+ }
124
+ }
crates/openwa-core/src/render/display/mod.rs CHANGED
@@ -3,6 +3,7 @@ pub mod context;
3
  pub mod font;
4
  pub mod gfx;
5
  pub mod gradient;
 
6
  pub mod line_draw;
7
  pub mod palette;
8
  pub mod sprite_blit;
@@ -11,5 +12,6 @@ pub mod vtable;
11
  pub use base::{DisplayBase, DisplayBaseVtable, SpriteBufferCtrl, SpriteCache};
12
  pub use context::{FastcallResult, RenderContext};
13
  pub use gfx::DisplayGfx;
 
14
  pub use palette::{Palette, PaletteVtable};
15
  pub use vtable::{DisplayGfxVtable, DrawScaledSpriteResult};
 
3
  pub mod font;
4
  pub mod gfx;
5
  pub mod gradient;
6
+ pub mod layer;
7
  pub mod line_draw;
8
  pub mod palette;
9
  pub mod sprite_blit;
 
12
  pub use base::{DisplayBase, DisplayBaseVtable, SpriteBufferCtrl, SpriteCache};
13
  pub use context::{FastcallResult, RenderContext};
14
  pub use gfx::DisplayGfx;
15
+ pub use layer::Layer;
16
  pub use palette::{Palette, PaletteVtable};
17
  pub use vtable::{DisplayGfxVtable, DrawScaledSpriteResult};
crates/openwa-core/src/render/display/vtable.rs CHANGED
@@ -7,6 +7,7 @@ use crate::render::display::font::{
7
  font_extend, font_get_info_impl, font_get_metric_impl, font_load_from_gfx,
8
  font_set_palette_impl, font_set_param_impl, Font,
9
  };
 
10
  use crate::render::display::line_draw::Vertex;
11
  use crate::render::sprite::gfx_dir::GfxDir;
12
  use crate::render::sprite::sprite::{LayerSprite, LayerSpriteFrame};
@@ -904,12 +905,6 @@ fn is_valid_sprite_id(id: i32) -> bool {
904
  (1..=0x3FF).contains(&id)
905
  }
906
 
907
- /// Check if a layer ID is in the valid range [1, 3].
908
- #[inline]
909
- fn is_valid_layer(layer: u32) -> bool {
910
- (1..=3).contains(&layer)
911
- }
912
-
913
  /// Port of DisplayGfx::GetSpriteInfo (vtable slot 6, 0x523500).
914
  ///
915
  /// Looks up sprite metadata for `layer` (valid: 1..=0x3FF). Checks
@@ -1386,13 +1381,12 @@ pub unsafe extern "thiscall" fn draw_pixel_strip(
1386
  /// # Safety
1387
  /// `this` must be a valid `*mut DisplayGfx`.
1388
  pub unsafe extern "thiscall" fn set_layer_color(this: *mut DisplayGfx, layer: i32, color: i32) {
1389
- // Layer must be 1, 2, or 3
1390
- if (layer as u32).wrapping_sub(1) >= 3 {
1391
  return;
1392
- }
1393
 
1394
  // Only allocate if no context exists for this layer
1395
- if (*this).base.layer_contexts[layer as usize] != 0 {
1396
  return;
1397
  }
1398
 
@@ -1405,15 +1399,15 @@ pub unsafe extern "thiscall" fn set_layer_color(this: *mut DisplayGfx, layer: i3
1405
  core::ptr::write_bytes(ctx, 0, 0x70C);
1406
 
1407
  let result = if ctx.is_null() {
1408
- 0u32
1409
  } else {
1410
  let ctx = ctx as *mut PaletteContext;
1411
  palette_context_init(ctx, start as i16, (start as i32 + color - 1) as i16);
1412
- ctx as u32
1413
  };
1414
 
1415
- (*this).base.layer_contexts[layer as usize] = result;
1416
- (*this).base.layer_visibility[layer as usize] = 0;
1417
  }
1418
 
1419
  /// Palette slot allocator — port of FUN_00523190.
@@ -1493,10 +1487,9 @@ unsafe fn palette_context_init(ctx: *mut PaletteContext, range_min: i16, range_m
1493
  /// # Safety
1494
  /// `this` must be a valid `*mut DisplayGfx`.
1495
  pub unsafe extern "thiscall" fn set_active_layer(this: *mut DisplayGfx, layer: i32) -> *mut u8 {
1496
- if (layer as u32).wrapping_sub(1) < 3 {
1497
- (*this).base.layer_contexts[layer as usize] as *mut u8
1498
- } else {
1499
- core::ptr::null_mut()
1500
  }
1501
  }
1502
 
@@ -1616,13 +1609,17 @@ pub unsafe extern "thiscall" fn set_layer_visibility(
1616
  layer: i32,
1617
  visible: i32,
1618
  ) {
1619
- let layer_ctx = set_active_layer(this, layer) as *mut PaletteContext;
 
 
 
 
1620
  if !layer_ctx.is_null() {
1621
  update_palette(this, layer_ctx, visible);
1622
  }
1623
 
1624
  if visible < 0 {
1625
- (*this).base.layer_visibility[layer as usize] = 0;
1626
  }
1627
  }
1628
 
@@ -1672,7 +1669,7 @@ pub unsafe fn load_sprite(
1672
  sprite: *mut Sprite,
1673
  gfx_dir: *mut GfxDir,
1674
  name: *const c_char,
1675
- layer_ctx: u32,
1676
  ) -> i32,
1677
  ) -> i32 {
1678
  // Bit 23 set = already loaded sentinel
@@ -1680,11 +1677,12 @@ pub unsafe fn load_sprite(
1680
  return 1;
1681
  }
1682
 
1683
- if !is_valid_layer(layer) {
1684
  return 0;
1685
- }
1686
  let base = &mut (*this).base;
1687
- if base.layer_contexts[layer as usize] == 0 {
 
1688
  return 0;
1689
  }
1690
  if !is_valid_sprite_id(id as i32) {
@@ -1703,7 +1701,6 @@ pub unsafe fn load_sprite(
1703
  construct_sprite(sprite, base.sprite_cache);
1704
 
1705
  // Load from VFS
1706
- let layer_ctx = base.layer_contexts[layer as usize];
1707
  let result = load_sprite_from_vfs(sprite, gfx_dir, _name, layer_ctx);
1708
  if result == 0 {
1709
  // Load failed — destroy sprite via vtable[0]
@@ -1717,8 +1714,8 @@ pub unsafe fn load_sprite(
1717
  // Store in sprite table
1718
  let base = &mut (*this).base;
1719
  base.sprite_ptrs[id as usize] = sprite;
1720
- base.sprite_layers[id as usize] = layer;
1721
- base.layer_visibility[layer as usize] += 1;
1722
 
1723
  // Update max_frames on the sprite if flag is set
1724
  if flag != 0 {
@@ -1774,7 +1771,7 @@ pub unsafe fn load_sprite_by_name(
1774
 
1775
  // 2. Store gfx_dir and palette_ctx in sprite
1776
  (*sprite).gfx_dir = gfx_dir;
1777
- (*sprite).palette_ctx = palette_ctx as u32;
1778
 
1779
  // 3. Load image stream from GfxDir
1780
  let stream = call_gfx_load_image(gfx_dir, name) as *mut GfxDirStream;
@@ -2057,7 +2054,8 @@ pub unsafe fn load_sprite_by_layer(
2057
  return 1;
2058
  }
2059
 
2060
- // Call set_active_layer (vtable slot 5) — returns PaletteContext*
 
2061
  let palette_ctx = set_active_layer(this, layer as i32) as *mut PaletteContext;
2062
 
2063
  if !is_valid_sprite_id(id as i32) {
@@ -2130,13 +2128,13 @@ pub unsafe fn load_font(
2130
  ) -> u32 {
2131
  use crate::wa_alloc::wa_malloc_struct_zeroed;
2132
 
2133
- if !(1..=3).contains(&layer) {
2134
  return 0;
2135
- }
2136
 
2137
  let base = &mut (*this).base;
2138
- let layer_ctx = base.layer_contexts[layer as usize];
2139
- if layer_ctx == 0 {
2140
  return 0;
2141
  }
2142
 
@@ -2156,12 +2154,7 @@ pub unsafe fn load_font(
2156
  }
2157
 
2158
  // Load and parse the font data.
2159
- let result = font_load_from_gfx(
2160
- font_obj,
2161
- gfx_dir,
2162
- layer_ctx as *mut PaletteContext,
2163
- filename,
2164
- );
2165
  if result == 0 {
2166
  // Original leaks here on failure (via an unported cleanup helper).
2167
  // We match that behavior rather than introducing a free path that
@@ -2173,8 +2166,8 @@ pub unsafe fn load_font(
2173
  // this font slot at DisplayBase + 0x301C + font_id*4 (the gap region
2174
  // next to font_table), then bumps the layer visibility counter.
2175
  base.font_table[font_id as usize] = font_obj;
2176
- base.font_layers[font_id as usize] = layer;
2177
- base.layer_visibility[layer as usize] += 1;
2178
 
2179
  1
2180
  }
@@ -2213,13 +2206,17 @@ pub unsafe fn load_font_extension(
2213
  // The original always reads layer_contexts[1] here, regardless of which
2214
  // mode owns the font. This matches the disassembly at 0x52364d:
2215
  // `MOV ECX, [EDI+0x3120]` (= layer_contexts[1])
2216
- let layer1_ctx = base.layer_contexts[1] as *mut PaletteContext;
2217
  let mut resolved_rgb: u32 = 0;
2218
  let _ = palette_context_lookup_entry(layer1_ctx, palette_value as i32, &mut resolved_rgb);
2219
 
2220
- // The actual font extension call uses the font's owning layer's palette ctx.
2221
- let layer = base.font_layers[font_id as usize] as usize;
2222
- let layer_ctx = base.layer_contexts[layer] as *mut PaletteContext;
 
 
 
 
2223
 
2224
  font_extend(font_obj, layer_ctx, path, char_map, resolved_rgb);
2225
 
 
7
  font_extend, font_get_info_impl, font_get_metric_impl, font_load_from_gfx,
8
  font_set_palette_impl, font_set_param_impl, Font,
9
  };
10
+ use crate::render::display::layer::Layer;
11
  use crate::render::display::line_draw::Vertex;
12
  use crate::render::sprite::gfx_dir::GfxDir;
13
  use crate::render::sprite::sprite::{LayerSprite, LayerSpriteFrame};
 
905
  (1..=0x3FF).contains(&id)
906
  }
907
 
 
 
 
 
 
 
908
  /// Port of DisplayGfx::GetSpriteInfo (vtable slot 6, 0x523500).
909
  ///
910
  /// Looks up sprite metadata for `layer` (valid: 1..=0x3FF). Checks
 
1381
  /// # Safety
1382
  /// `this` must be a valid `*mut DisplayGfx`.
1383
  pub unsafe extern "thiscall" fn set_layer_color(this: *mut DisplayGfx, layer: i32, color: i32) {
1384
+ let Some(layer) = Layer::try_from_i32(layer) else {
 
1385
  return;
1386
+ };
1387
 
1388
  // Only allocate if no context exists for this layer
1389
+ if !(*this).base.layer_contexts[layer.idx()].is_null() {
1390
  return;
1391
  }
1392
 
 
1399
  core::ptr::write_bytes(ctx, 0, 0x70C);
1400
 
1401
  let result = if ctx.is_null() {
1402
+ core::ptr::null_mut()
1403
  } else {
1404
  let ctx = ctx as *mut PaletteContext;
1405
  palette_context_init(ctx, start as i16, (start as i32 + color - 1) as i16);
1406
+ ctx
1407
  };
1408
 
1409
+ (*this).base.layer_contexts[layer.idx()] = result;
1410
+ (*this).base.layer_visibility[layer.idx()] = 0;
1411
  }
1412
 
1413
  /// Palette slot allocator — port of FUN_00523190.
 
1487
  /// # Safety
1488
  /// `this` must be a valid `*mut DisplayGfx`.
1489
  pub unsafe extern "thiscall" fn set_active_layer(this: *mut DisplayGfx, layer: i32) -> *mut u8 {
1490
+ match Layer::try_from_i32(layer) {
1491
+ Some(layer) => (*this).base.layer_contexts[layer.idx()] as *mut u8,
1492
+ None => core::ptr::null_mut(),
 
1493
  }
1494
  }
1495
 
 
1609
  layer: i32,
1610
  visible: i32,
1611
  ) {
1612
+ let Some(layer) = Layer::try_from_i32(layer) else {
1613
+ return;
1614
+ };
1615
+
1616
+ let layer_ctx = (*this).base.layer_contexts[layer.idx()];
1617
  if !layer_ctx.is_null() {
1618
  update_palette(this, layer_ctx, visible);
1619
  }
1620
 
1621
  if visible < 0 {
1622
+ (*this).base.layer_visibility[layer.idx()] = 0;
1623
  }
1624
  }
1625
 
 
1669
  sprite: *mut Sprite,
1670
  gfx_dir: *mut GfxDir,
1671
  name: *const c_char,
1672
+ layer_ctx: *mut PaletteContext,
1673
  ) -> i32,
1674
  ) -> i32 {
1675
  // Bit 23 set = already loaded sentinel
 
1677
  return 1;
1678
  }
1679
 
1680
+ let Some(layer) = Layer::try_from_u32(layer) else {
1681
  return 0;
1682
+ };
1683
  let base = &mut (*this).base;
1684
+ let layer_ctx = base.layer_contexts[layer.idx()];
1685
+ if layer_ctx.is_null() {
1686
  return 0;
1687
  }
1688
  if !is_valid_sprite_id(id as i32) {
 
1701
  construct_sprite(sprite, base.sprite_cache);
1702
 
1703
  // Load from VFS
 
1704
  let result = load_sprite_from_vfs(sprite, gfx_dir, _name, layer_ctx);
1705
  if result == 0 {
1706
  // Load failed — destroy sprite via vtable[0]
 
1714
  // Store in sprite table
1715
  let base = &mut (*this).base;
1716
  base.sprite_ptrs[id as usize] = sprite;
1717
+ base.sprite_layers[id as usize] = layer.as_u32();
1718
+ base.layer_visibility[layer.idx()] += 1;
1719
 
1720
  // Update max_frames on the sprite if flag is set
1721
  if flag != 0 {
 
1771
 
1772
  // 2. Store gfx_dir and palette_ctx in sprite
1773
  (*sprite).gfx_dir = gfx_dir;
1774
+ (*sprite).palette_ctx = palette_ctx;
1775
 
1776
  // 3. Load image stream from GfxDir
1777
  let stream = call_gfx_load_image(gfx_dir, name) as *mut GfxDirStream;
 
2054
  return 1;
2055
  }
2056
 
2057
+ // Call set_active_layer (vtable slot 5) — returns the layer's
2058
+ // PaletteContext, or null if `layer` is out of range.
2059
  let palette_ctx = set_active_layer(this, layer as i32) as *mut PaletteContext;
2060
 
2061
  if !is_valid_sprite_id(id as i32) {
 
2128
  ) -> u32 {
2129
  use crate::wa_alloc::wa_malloc_struct_zeroed;
2130
 
2131
+ let Some(layer) = Layer::try_from_u32(layer) else {
2132
  return 0;
2133
+ };
2134
 
2135
  let base = &mut (*this).base;
2136
+ let layer_ctx = base.layer_contexts[layer.idx()];
2137
+ if layer_ctx.is_null() {
2138
  return 0;
2139
  }
2140
 
 
2154
  }
2155
 
2156
  // Load and parse the font data.
2157
+ let result = font_load_from_gfx(font_obj, gfx_dir, layer_ctx, filename);
 
 
 
 
 
2158
  if result == 0 {
2159
  // Original leaks here on failure (via an unported cleanup helper).
2160
  // We match that behavior rather than introducing a free path that
 
2166
  // this font slot at DisplayBase + 0x301C + font_id*4 (the gap region
2167
  // next to font_table), then bumps the layer visibility counter.
2168
  base.font_table[font_id as usize] = font_obj;
2169
+ base.font_layers[font_id as usize] = layer.as_u32();
2170
+ base.layer_visibility[layer.idx()] += 1;
2171
 
2172
  1
2173
  }
 
2206
  // The original always reads layer_contexts[1] here, regardless of which
2207
  // mode owns the font. This matches the disassembly at 0x52364d:
2208
  // `MOV ECX, [EDI+0x3120]` (= layer_contexts[1])
2209
+ let layer1_ctx = base.layer_contexts[Layer::ONE.idx()];
2210
  let mut resolved_rgb: u32 = 0;
2211
  let _ = palette_context_lookup_entry(layer1_ctx, palette_value as i32, &mut resolved_rgb);
2212
 
2213
+ // The actual font extension call uses the font's owning layer's palette
2214
+ // ctx. The original reads `layer_contexts[font_layers[font_id]]` directly
2215
+ // without validation; if `font_layers` is 0 it reads index 0 (always
2216
+ // null on a properly-initialized DisplayBase). We preserve that exact
2217
+ // index lookup rather than going through `Layer` here.
2218
+ let layer_idx = base.font_layers[font_id as usize] as usize;
2219
+ let layer_ctx = base.layer_contexts[layer_idx];
2220
 
2221
  font_extend(font_obj, layer_ctx, path, char_map, resolved_rgb);
2222
 
crates/openwa-core/src/render/sprite/sprite.rs CHANGED
@@ -1,4 +1,6 @@
1
- use crate::render::{display::gfx::DisplayGfx, sprite::gfx_dir::GfxDir, SpriteCache};
 
 
2
 
3
  // Re-export SpriteId from its own module to keep this file focused on struct layouts.
4
  pub use super::sprite_id::SpriteId;
@@ -194,8 +196,8 @@ pub struct LayerSprite {
194
  pub gfx_dir: *mut GfxDir,
195
  /// 0x58: DisplayGfx pointer (set by caller before load).
196
  pub display_gfx: *mut DisplayGfx,
197
- /// 0x5C: PaletteContext pointer (as u32, set by load_sprite_by_name).
198
- pub palette_ctx: u32,
199
  /// 0x60: Header field from stream (4 bytes).
200
  pub field_60: u32,
201
  /// 0x64: Header field from stream (2 bytes).
 
1
+ use crate::render::{
2
+ display::gfx::DisplayGfx, palette::PaletteContext, sprite::gfx_dir::GfxDir, SpriteCache,
3
+ };
4
 
5
  // Re-export SpriteId from its own module to keep this file focused on struct layouts.
6
  pub use super::sprite_id::SpriteId;
 
196
  pub gfx_dir: *mut GfxDir,
197
  /// 0x58: DisplayGfx pointer (set by caller before load).
198
  pub display_gfx: *mut DisplayGfx,
199
+ /// 0x5C: PaletteContext pointer (set by load_sprite_by_name).
200
+ pub palette_ctx: *mut PaletteContext,
201
  /// 0x60: Header field from stream (4 bytes).
202
  pub field_60: u32,
203
  /// 0x64: Header field from stream (2 bytes).
crates/openwa-dll/src/replacements/render.rs CHANGED
@@ -1341,7 +1341,7 @@ unsafe extern "cdecl" fn wa_load_sprite_from_vfs(
1341
  _sprite: *mut openwa_core::render::sprite::Sprite,
1342
  _gfx_dir: *mut GfxDir,
1343
  _name: *const c_char,
1344
- _layer_ctx: u32,
1345
  ) -> i32 {
1346
  core::arch::naked_asm!(
1347
  // cdecl: +4=sprite, +8=gfx, +12=name, +16=layer_ctx
 
1341
  _sprite: *mut openwa_core::render::sprite::Sprite,
1342
  _gfx_dir: *mut GfxDir,
1343
  _name: *const c_char,
1344
+ _layer_ctx: *mut openwa_core::render::palette::PaletteContext,
1345
  ) -> i32 {
1346
  core::arch::naked_asm!(
1347
  // cdecl: +4=sprite, +8=gfx, +12=name, +16=layer_ctx