paavohtl Claude Opus 4.6 (1M context) commited on
Commit
d7eb276
Β·
1 Parent(s): 3f42fbe

refactor: port IMG_Decode and rename GfxResource to IMG

Browse files

Port IMG_Decode (0x4F5F80) and DisplayGfx__Constructor (0x4F5E80) to
pure Rust, eliminating the last transmute bridges in the asset loading
pipeline. img_decode returns Option<DecodedBitGrid> with Display/Collision
discrimination; img_decode_cached always returns *mut DisplayBitGrid.

- Add asset/img.rs with img_decode and img_decode_cached
- Replace transmute bridges in gfx_dir.rs, ddgame_constructor.rs
- Hook IMG_DECODE and IMG_DECODE_CACHED for remaining WA callers
- Type DDGame fields: gradient_image, gradient_image_2, arrow_sprites
as *mut BitGrid instead of *mut u8
- Rename GfxResource__Create_Maybe β†’ IMG__LoadFromDir (Ghidra + Rust)
- Rename DisplayGfx__Constructor_Maybe β†’ IMG__DecodeCached (Ghidra + VA)
- Propagate Option<DecodedBitGrid> through call_gfx_find_and_load
and call_gfx_load_and_wrap

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

crates/openwa-core/src/address.rs CHANGED
@@ -249,8 +249,8 @@ pub mod va {
249
  class "DisplayGfx" {
250
  /// DisplayGfx constructor
251
  ctor/Stdcall DISPLAYGFX_CTOR = 0x0056_9C10;
252
- /// DisplayGfx constructor (from raw image)
253
- ctor/Stdcall DISPLAYGFX_CONSTRUCTOR = 0x004F_5E80;
254
  /// DisplayGfx construct full (5 params)
255
  fn/Stdcall DISPLAYGFX_CONSTRUCT_FULL = 0x0056_3FC0;
256
  /// DisplayGfx init team palette display objects
@@ -524,8 +524,8 @@ pub mod va {
524
  fn PALETTE_ANIMATE_MAYBE = 0x0053_3A80;
525
  fn LOAD_SPRITE = 0x0052_3400;
526
  fn OPENGL_INIT = 0x0059_F000;
527
- /// GfxResource__Create_Maybe
528
- fn GFX_RESOURCE_CREATE = 0x004F_6300;
529
  /// SpriteGfxTable__Init
530
  fn/Fastcall SPRITE_GFX_TABLE_INIT = 0x0054_1620;
531
  /// RingBuffer__Init
 
249
  class "DisplayGfx" {
250
  /// DisplayGfx constructor
251
  ctor/Stdcall DISPLAYGFX_CTOR = 0x0056_9C10;
252
+ /// IMG__DecodeCached: decode cached raw image buffer into DisplayBitGrid
253
+ fn/Stdcall IMG_DECODE_CACHED = 0x004F_5E80;
254
  /// DisplayGfx construct full (5 params)
255
  fn/Stdcall DISPLAYGFX_CONSTRUCT_FULL = 0x0056_3FC0;
256
  /// DisplayGfx init team palette display objects
 
524
  fn PALETTE_ANIMATE_MAYBE = 0x0053_3A80;
525
  fn LOAD_SPRITE = 0x0052_3400;
526
  fn OPENGL_INIT = 0x0059_F000;
527
+ /// IMG__LoadFromDir: look up + decode IMG resource from a .dir archive
528
+ fn IMG_LOAD_FROM_DIR = 0x004F_6300;
529
  /// SpriteGfxTable__Init
530
  fn/Fastcall SPRITE_GFX_TABLE_INIT = 0x0054_1620;
531
  /// RingBuffer__Init
crates/openwa-core/src/asset/gfx_dir.rs CHANGED
@@ -4,11 +4,19 @@
4
  //! `Gfx0.dir`). It provides nameβ†’resource lookup via a 1024-bucket hash table
5
  //! and delegates file I/O and caching to vtable methods.
6
 
 
 
 
7
  use openwa_core::vtable;
8
 
9
  use crate::address::va;
 
 
 
10
  use crate::rebase::rb;
11
  use crate::wa_alloc::{wa_malloc, wa_malloc_struct_zeroed};
 
 
12
 
13
  /// Cache slot within a GfxDir (0x10 bytes).
14
  ///
@@ -143,10 +151,7 @@ unsafe fn gfx_dir_hash(name: *const u8) -> u32 {
143
  /// `gfx_dir` must be a valid GfxHandler with initialized hash table at +0x04.
144
  /// `name` must be a valid null-terminated C string.
145
  #[cfg(target_arch = "x86")]
146
- pub unsafe fn gfx_dir_find_entry(
147
- name: *const core::ffi::c_char,
148
- gfx_dir: *const GfxDir,
149
- ) -> *mut u8 {
150
  let mut current_name = name as *const u8;
151
 
152
  loop {
@@ -215,7 +220,7 @@ pub unsafe fn gfx_dir_find_entry(
215
  }
216
 
217
  if match_found {
218
- return entry as *mut u8;
219
  }
220
 
221
  entry = (*entry).next;
@@ -448,94 +453,86 @@ pub unsafe extern "thiscall" fn gfx_dir_release(this: *mut GfxDir, flags: u32) {
448
  }
449
  }
450
 
451
- /// Pure Rust implementation of GfxResource__Create_Maybe (0x4F6300).
452
  ///
453
- /// Convention: usercall(ECX=gfx_dir, EAX=name) + 1 stack(output), RET 0x4.
454
  ///
455
- /// Looks up `name` in the GfxHandler's directory, tries cached load, wraps
456
- /// as DisplayGfx. Falls back to loading the raw image and decoding it.
 
457
  ///
458
  /// # Safety
459
  /// All pointers must be valid WA objects.
460
  #[cfg(target_arch = "x86")]
461
- pub unsafe fn gfx_resource_create(
462
  gfx_dir: *mut GfxDir,
463
- name: *const core::ffi::c_char,
464
- output: *mut crate::render::palette::PaletteContext,
465
- ) -> *mut u8 {
466
- // 1. Try FindEntry β†’ cached load β†’ DisplayGfx wrap
 
 
467
  let entry = gfx_dir_find_entry(name, gfx_dir);
468
  if !entry.is_null() {
469
- // gfx_dir->vtable[2](entry->field_4) β€” cached load
470
- let entry_val = (*(entry as *const GfxDirEntry)).value;
471
  let cached = GfxDir::load_cached_raw(gfx_dir, entry_val);
472
  if !cached.is_null() {
473
- // DisplayGfx__Constructor_Maybe: stdcall(raw_image), RET 0x4
474
- let ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
475
- core::mem::transmute(rb(va::DISPLAYGFX_CONSTRUCTOR) as usize);
476
- return ctor(cached);
477
  }
478
  }
479
 
480
- // 2. Fallback: LoadImage β†’ IMG_Decode
481
  let raw_image = gfx_dir_load_image(gfx_dir, name);
482
  if raw_image.is_null() {
483
  return core::ptr::null_mut();
484
  }
485
 
486
- // IMG_Decode: stdcall(output, raw_image, 1), RET 0xC
487
- let decode: unsafe extern "stdcall" fn(*mut u8, *mut u8, i32) -> *mut u8 =
488
- core::mem::transmute(rb(va::IMG_DECODE) as usize);
489
- let result = decode(output as *mut u8, raw_image as *mut u8, 1);
490
-
491
  GfxDirStream::destroy_raw(raw_image);
492
 
493
  result
494
  }
495
 
496
  /// Helper: find entry in GfxDir and load image, or load directly.
497
- /// Returns a DisplayGfx/sprite pointer or null.
498
  #[cfg(target_arch = "x86")]
499
  pub(crate) unsafe fn call_gfx_find_and_load(
500
  gfx_dir: *mut GfxDir,
501
- name: &core::ffi::CStr,
502
- display_ctx: *mut crate::render::palette::PaletteContext,
503
- ) -> *mut u8 {
504
- let name_ptr = name.as_ptr() as *const u8;
505
- let entry = gfx_dir_find_entry(name_ptr.cast(), gfx_dir);
506
 
507
  if !entry.is_null() {
508
- // Try cached load: gfx_dir->vtable[2](entry->field_4)
509
- let cached = GfxDir::load_cached_raw(gfx_dir, (*(entry as *const GfxDirEntry)).value);
510
  if !cached.is_null() {
511
- // Wrap with DisplayGfx__Constructor_Maybe (0x4F5E80)
512
- // This is stdcall(1 param), RET 0x4
513
- let ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
514
- core::mem::transmute(rb(va::DISPLAYGFX_CONSTRUCTOR) as usize);
515
- return ctor(cached);
516
  }
517
  }
518
 
519
  // Fallback: load image directly
520
- call_gfx_load_and_wrap(gfx_dir, name_ptr.cast(), display_ctx)
521
  }
522
 
523
- /// Helper: load image via GfxDir__LoadImage + wrap as DisplayGfx.
524
- /// Used by arrow sprite loop when GfxDir__FindEntry returns null.
525
  #[cfg(target_arch = "x86")]
526
  pub(crate) unsafe fn call_gfx_load_and_wrap(
527
  gfx_dir: *mut GfxDir,
528
- name: *const core::ffi::c_char,
529
- display_ctx: *mut crate::render::palette::PaletteContext,
530
- ) -> *mut u8 {
531
  let image = gfx_dir_load_image(gfx_dir, name);
532
  if image.is_null() {
533
- return core::ptr::null_mut();
534
  }
535
- // FUN_004F5F80(display_ctx, image, 1) β€” stdcall, RET 0xC (3 params)
536
- let f: unsafe extern "stdcall" fn(*mut u8, *mut u8, u32) -> *mut u8 =
537
- core::mem::transmute(rb(va::IMG_DECODE) as usize);
538
- let result = f(display_ctx as *mut u8, image as *mut u8, 1);
539
  GfxDirStream::destroy_raw(image);
540
  result
541
  }
@@ -806,10 +803,7 @@ pub unsafe extern "thiscall" fn gfx_dir_stream_read(
806
  /// creates a GfxDirStream for sequential reading, and returns it.
807
  /// Returns null if no free slots or name not found.
808
  #[cfg(target_arch = "x86")]
809
- pub unsafe fn gfx_dir_load_image(
810
- gfx_dir: *mut GfxDir,
811
- name: *const core::ffi::c_char,
812
- ) -> *mut GfxDirStream {
813
  // Check if there are free cache slots
814
  if (*gfx_dir).slot_count == 0 {
815
  return core::ptr::null_mut();
 
4
  //! `Gfx0.dir`). It provides nameβ†’resource lookup via a 1024-bucket hash table
5
  //! and delegates file I/O and caching to vtable methods.
6
 
7
+ #[cfg(target_arch = "x86")]
8
+ use core::ffi::{c_char, CStr};
9
+
10
  use openwa_core::vtable;
11
 
12
  use crate::address::va;
13
+ #[cfg(target_arch = "x86")]
14
+ use crate::asset::img::DecodedBitGrid;
15
+ use crate::bitgrid::BitGrid;
16
  use crate::rebase::rb;
17
  use crate::wa_alloc::{wa_malloc, wa_malloc_struct_zeroed};
18
+ #[cfg(target_arch = "x86")]
19
+ use crate::{asset, render::palette::PaletteContext};
20
 
21
  /// Cache slot within a GfxDir (0x10 bytes).
22
  ///
 
151
  /// `gfx_dir` must be a valid GfxHandler with initialized hash table at +0x04.
152
  /// `name` must be a valid null-terminated C string.
153
  #[cfg(target_arch = "x86")]
154
+ pub unsafe fn gfx_dir_find_entry(name: *const c_char, gfx_dir: *const GfxDir) -> *mut GfxDirEntry {
 
 
 
155
  let mut current_name = name as *const u8;
156
 
157
  loop {
 
220
  }
221
 
222
  if match_found {
223
+ return entry;
224
  }
225
 
226
  entry = (*entry).next;
 
453
  }
454
  }
455
 
456
+ /// Pure Rust implementation of IMG__LoadFromDir (0x4F6300).
457
  ///
458
+ /// Convention: usercall(ECX=gfx_dir, EAX=name) + 1 stack(palette_ctx), RET 0x4.
459
  ///
460
+ /// Looks up `name` in the GfxDir archive's hash table. If found in cache,
461
+ /// decodes via `img_decode_cached`. Otherwise loads from the archive stream
462
+ /// and decodes via `img_decode`.
463
  ///
464
  /// # Safety
465
  /// All pointers must be valid WA objects.
466
  #[cfg(target_arch = "x86")]
467
+ pub unsafe fn img_load_from_dir(
468
  gfx_dir: *mut GfxDir,
469
+ name: *const c_char,
470
+ palette_ctx: *mut PaletteContext,
471
+ ) -> *mut BitGrid {
472
+ use crate::asset::img::{img_decode, img_decode_cached};
473
+
474
+ // 1. Try FindEntry β†’ cached load β†’ decode from raw buffer
475
  let entry = gfx_dir_find_entry(name, gfx_dir);
476
  if !entry.is_null() {
477
+ let entry_val = (*entry).value;
 
478
  let cached = GfxDir::load_cached_raw(gfx_dir, entry_val);
479
  if !cached.is_null() {
480
+ return img_decode_cached(palette_ctx, cached) as *mut BitGrid;
 
 
 
481
  }
482
  }
483
 
484
+ // 2. Fallback: LoadImage β†’ IMG_Decode from stream
485
  let raw_image = gfx_dir_load_image(gfx_dir, name);
486
  if raw_image.is_null() {
487
  return core::ptr::null_mut();
488
  }
489
 
490
+ let result = match img_decode(palette_ctx, raw_image, 1) {
491
+ Some(decoded) => decoded.as_bitgrid_ptr(),
492
+ None => core::ptr::null_mut(),
493
+ };
 
494
  GfxDirStream::destroy_raw(raw_image);
495
 
496
  result
497
  }
498
 
499
  /// Helper: find entry in GfxDir and load image, or load directly.
 
500
  #[cfg(target_arch = "x86")]
501
  pub(crate) unsafe fn call_gfx_find_and_load(
502
  gfx_dir: *mut GfxDir,
503
+ name: &CStr,
504
+ palette_ctx: *mut PaletteContext,
505
+ ) -> Option<DecodedBitGrid> {
506
+ let name_ptr = name.as_ptr();
507
+ let entry = gfx_dir_find_entry(name_ptr, gfx_dir);
508
 
509
  if !entry.is_null() {
510
+ let cached = GfxDir::load_cached_raw(gfx_dir, (*entry).value);
 
511
  if !cached.is_null() {
512
+ let grid = asset::img::img_decode_cached(palette_ctx, cached);
513
+ if !grid.is_null() {
514
+ return Some(DecodedBitGrid::Display(grid));
515
+ }
516
+ return None;
517
  }
518
  }
519
 
520
  // Fallback: load image directly
521
+ call_gfx_load_and_wrap(gfx_dir, name_ptr.cast(), palette_ctx)
522
  }
523
 
524
+ /// Helper: load image via GfxDir__LoadImage + IMG_Decode.
 
525
  #[cfg(target_arch = "x86")]
526
  pub(crate) unsafe fn call_gfx_load_and_wrap(
527
  gfx_dir: *mut GfxDir,
528
+ name: *const c_char,
529
+ palette_ctx: *mut PaletteContext,
530
+ ) -> Option<DecodedBitGrid> {
531
  let image = gfx_dir_load_image(gfx_dir, name);
532
  if image.is_null() {
533
+ return None;
534
  }
535
+ let result = asset::img::img_decode(palette_ctx, image, 1);
 
 
 
536
  GfxDirStream::destroy_raw(image);
537
  result
538
  }
 
803
  /// creates a GfxDirStream for sequential reading, and returns it.
804
  /// Returns null if no free slots or name not found.
805
  #[cfg(target_arch = "x86")]
806
+ pub unsafe fn gfx_dir_load_image(gfx_dir: *mut GfxDir, name: *const c_char) -> *mut GfxDirStream {
 
 
 
807
  // Check if there are free cache slots
808
  if (*gfx_dir).slot_count == 0 {
809
  return core::ptr::null_mut();
crates/openwa-core/src/asset/img.rs ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! IMG format decoder β€” `.img` image files in `.dir` archives.
2
+ //!
3
+ //! WA uses a custom "IMG" format for static graphics (gradients, masks,
4
+ //! HUD elements). Format: magic "IMG\x1A", header with flags, optional
5
+ //! palette, width/height, then raw or LZSS-compressed pixel data.
6
+ //!
7
+ //! Two entry points:
8
+ //! - `img_decode` β€” reads from a `GfxDirStream` (stream-based path)
9
+ //! - `img_decode_cached` β€” reads from a raw memory buffer (cached path)
10
+
11
+ use crate::asset::gfx_dir::GfxDirStream;
12
+ use crate::bitgrid::{
13
+ BitGrid, CollisionBitGrid, DisplayBitGrid, BIT_GRID_COLLISION_VTABLE, BIT_GRID_DISPLAY_VTABLE,
14
+ };
15
+ use crate::rebase::rb;
16
+ use crate::render::palette::{palette_map_color, remap_pixels_through_lut, PaletteContext};
17
+ use crate::render::sprite::lzss::sprite_lzss_decode;
18
+ use crate::wa_alloc::wa_malloc_zeroed;
19
+
20
+ /// IMG file magic: "IMG\x1A" as little-endian u32.
21
+ const IMG_MAGIC: u32 = 0x1A47_4D49;
22
+
23
+ /// IMG header flags.
24
+ const FLAG_HAS_PALETTE: u16 = 0x8000;
25
+ const FLAG_LZSS_COMPRESSED: u16 = 0x4000;
26
+ const FLAG_EXTENDED_HEADER: u16 = 0x3FF6;
27
+
28
+ // ─── Global sprite palette byte counter (0x7A0870) ──────────────────────────
29
+ // IMG_Decode and DisplayGfx__Constructor both add palette_count * 3 to this.
30
+ crate::define_addresses! {
31
+ global G_SPRITE_PALETTE_BYTES = 0x007A_0870;
32
+ /// Global temp buffer for LZSS decompression (large static array in WA.exe)
33
+ global G_LZSS_TEMP_BUFFER = 0x006B_39C8;
34
+ }
35
+
36
+ pub enum DecodedBitGrid {
37
+ Display(*mut DisplayBitGrid),
38
+ Collision(*mut CollisionBitGrid),
39
+ }
40
+
41
+ impl DecodedBitGrid {
42
+ /// Get the underlying pointer as `*mut BitGrid` (common base type).
43
+ /// Valid because all BitGrid variants share the same `#[repr(C)]` layout.
44
+ pub fn as_bitgrid_ptr(self) -> *mut BitGrid {
45
+ match self {
46
+ DecodedBitGrid::Display(p) => p as *mut BitGrid,
47
+ DecodedBitGrid::Collision(p) => p as *mut BitGrid,
48
+ }
49
+ }
50
+ }
51
+
52
+ /// Pure Rust implementation of IMG_Decode (0x4F5F80).
53
+ ///
54
+ /// Reads an IMG image from a `GfxDirStream`, decodes it into a BitGrid
55
+ /// (DisplayBitGrid for 8bpp or CollisionBitGrid for 1bpp).
56
+ ///
57
+ /// Convention: stdcall(palette_ctx, stream, align_flag), RET 0xC.
58
+ ///
59
+ /// # Safety
60
+ /// All pointers must be valid WA objects. Must be called from within WA.exe.
61
+ #[cfg(target_arch = "x86")]
62
+ pub unsafe fn img_decode(
63
+ palette_ctx: *mut PaletteContext,
64
+ stream: *mut GfxDirStream,
65
+ align_flag: i32,
66
+ ) -> Option<DecodedBitGrid> {
67
+ // ── 1. Read and validate magic ──
68
+ let mut magic: u32 = 0;
69
+ GfxDirStream::read_raw(stream, &raw mut magic as *mut u8, 4);
70
+ if magic != IMG_MAGIC {
71
+ return None;
72
+ }
73
+
74
+ // ── 2. Read data_size (used for LZSS remaining calculation) ──
75
+ let mut data_size: u32 = 0;
76
+ GfxDirStream::read_raw(stream, &raw mut data_size as *mut u8, 4);
77
+
78
+ // ── 3. Read flags ──
79
+ let mut flags: u16 = 0;
80
+ GfxDirStream::read_raw(stream, &raw mut flags as *mut u8, 2);
81
+
82
+ // ── 4. Handle extended header (rare) ──
83
+ if (flags & FLAG_EXTENDED_HEADER) != 0 {
84
+ // Seek back before flags, skip bytes until null terminator, re-read flags
85
+ let pos = GfxDirStream::bytes_consumed_raw(stream);
86
+ GfxDirStream::seek_raw(stream, pos.wrapping_sub(2));
87
+ loop {
88
+ let mut b: u8 = 0;
89
+ GfxDirStream::read_raw(stream, &raw mut b, 1);
90
+ if b == 0 {
91
+ break;
92
+ }
93
+ }
94
+ GfxDirStream::read_raw(stream, &raw mut flags as *mut u8, 2);
95
+ }
96
+
97
+ // ── 5. Build palette LUT ──
98
+ let mut palette_lut = [0u8; 256];
99
+ if (flags & FLAG_HAS_PALETTE) != 0 {
100
+ let mut palette_count: u16 = 0;
101
+ GfxDirStream::read_raw(stream, &raw mut palette_count as *mut u8, 2);
102
+
103
+ // Read RGB triplets into temp buffer
104
+ let rgb_byte_count = palette_count as u32 * 3;
105
+ let mut rgb_buf = [0u8; 256 * 3]; // max 256 entries * 3 bytes
106
+ GfxDirStream::read_raw(stream, rgb_buf.as_mut_ptr(), rgb_byte_count);
107
+
108
+ // Map each RGB triplet through PaletteContext to build LUT
109
+ // lut[0] = 0 (transparent), lut[1+i] = MapColor(RGB[i])
110
+ for i in 0..palette_count as usize {
111
+ // Read as u32 from the RGB bytes (only low 3 bytes matter)
112
+ let r = rgb_buf[i * 3] as u32;
113
+ let g = rgb_buf[i * 3 + 1] as u32;
114
+ let b = rgb_buf[i * 3 + 2] as u32;
115
+ let rgb = r | (g << 8) | (b << 16);
116
+ palette_lut[i + 1] = palette_map_color(palette_ctx, rgb) as u8;
117
+ }
118
+
119
+ // Update global palette byte counter
120
+ let counter = rb(G_SPRITE_PALETTE_BYTES) as *mut u32;
121
+ *counter = (*counter).wrapping_add(rgb_byte_count);
122
+ }
123
+
124
+ // ── 6. Read width and height ──
125
+ let mut width: u16 = 0;
126
+ let mut height: u16 = 0;
127
+ GfxDirStream::read_raw(stream, &raw mut width as *mut u8, 2);
128
+ GfxDirStream::read_raw(stream, &raw mut height as *mut u8, 2);
129
+
130
+ // ── 7. Determine bpp from flags low byte ──
131
+ let bpp = (flags & 0xFF) as u8;
132
+ if bpp != 1 && bpp != 8 {
133
+ return None;
134
+ }
135
+
136
+ // ── 8. Allocate BitGrid (0x4C bytes, matching original allocation size) ──
137
+ let grid = wa_malloc_zeroed(0x4C) as *mut BitGrid;
138
+ if grid.is_null() {
139
+ return None;
140
+ }
141
+ BitGrid::init(grid, bpp as u32, width as u32, height as u32);
142
+
143
+ // Override vtable based on bpp
144
+ if bpp == 8 {
145
+ (*(grid as *mut DisplayBitGrid)).vtable =
146
+ rb(BIT_GRID_DISPLAY_VTABLE) as *const crate::bitgrid::BitGridDisplayVtable;
147
+ } else {
148
+ (*(grid as *mut CollisionBitGrid)).vtable =
149
+ rb(BIT_GRID_COLLISION_VTABLE) as *const crate::bitgrid::BitGridCollisionVtable;
150
+ }
151
+
152
+ let pixel_data = (*grid).data;
153
+ let row_stride = (*grid).row_stride;
154
+
155
+ // ── 9. Alignment padding ──
156
+ if align_flag != 0 {
157
+ loop {
158
+ let pos = GfxDirStream::bytes_consumed_raw(stream);
159
+ if (pos & 3) == 0 {
160
+ break;
161
+ }
162
+ let mut dummy: u8 = 0;
163
+ GfxDirStream::read_raw(stream, &raw mut dummy, 1);
164
+ }
165
+ }
166
+
167
+ // ── 10. Read pixel data ──
168
+ if (flags & FLAG_LZSS_COMPRESSED) != 0 {
169
+ // LZSS path: read remaining stream data into temp buffer, then decompress.
170
+ // sprite_lzss_decode applies the palette LUT during decompression,
171
+ // so no separate remap step is needed afterward.
172
+ let pos = GfxDirStream::bytes_consumed_raw(stream);
173
+ let remaining = data_size.wrapping_sub(pos);
174
+
175
+ // Use WA's global LZSS temp buffer
176
+ let lzss_buffer = rb(G_LZSS_TEMP_BUFFER) as *mut u8;
177
+ GfxDirStream::read_raw(stream, lzss_buffer, remaining);
178
+
179
+ sprite_lzss_decode(pixel_data, lzss_buffer, palette_lut.as_ptr());
180
+ } else {
181
+ // Row-by-row path: read raw pixel data
182
+ let bits_per_row = width as u32 * bpp as u32;
183
+ let bytes_per_row = (bits_per_row + 7) / 8;
184
+
185
+ for row in 0..height as u32 {
186
+ let row_ptr = pixel_data.add((row * row_stride) as usize);
187
+ GfxDirStream::read_raw(stream, row_ptr, bytes_per_row);
188
+ }
189
+
190
+ // Remap through palette LUT (only for non-LZSS, since LZSS applies the LUT inline)
191
+ if (flags & FLAG_HAS_PALETTE) != 0 && bpp == 8 {
192
+ let dwords_per_row = row_stride / 4;
193
+ remap_pixels_through_lut(
194
+ pixel_data,
195
+ row_stride,
196
+ palette_lut.as_ptr(),
197
+ dwords_per_row,
198
+ height as u32,
199
+ );
200
+ }
201
+ }
202
+
203
+ if bpp == 8 {
204
+ Some(DecodedBitGrid::Display(grid as *mut DisplayBitGrid))
205
+ } else {
206
+ Some(DecodedBitGrid::Collision(grid as *mut CollisionBitGrid))
207
+ }
208
+ }
209
+
210
+ /// Pure Rust implementation of DisplayGfx__Constructor (0x4F5E80).
211
+ ///
212
+ /// Reads from a raw memory buffer (cached `.dir` entry). Always creates an
213
+ /// 8bpp DisplayBitGrid. Format: raw_image+0x0A has palette_count, RGB data,
214
+ /// width, height, then pixel data.
215
+ ///
216
+ /// In the original WA code, the PaletteContext is passed implicitly via EBX
217
+ /// (callee-saved register from the caller). Our Rust API takes it explicitly.
218
+ ///
219
+ /// # Safety
220
+ /// `raw_image` must be a valid pointer to cached image data.
221
+ /// `palette_ctx` must be a valid PaletteContext pointer (from the caller).
222
+ #[cfg(target_arch = "x86")]
223
+ pub unsafe fn img_decode_cached(
224
+ palette_ctx: *mut PaletteContext,
225
+ raw_image: *mut u8,
226
+ ) -> *mut DisplayBitGrid {
227
+ use crate::wa_alloc::wa_malloc_struct_zeroed;
228
+
229
+ let mut ptr = raw_image.add(0x0A);
230
+
231
+ // ── 1. Read palette count and build LUT ──
232
+ let palette_count = *(ptr as *const u16) as usize;
233
+ ptr = ptr.add(2);
234
+
235
+ let mut palette_lut = [0u8; 256];
236
+ // palette_lut[0] = 0 (transparent, already zero)
237
+
238
+ for i in 0..palette_count {
239
+ // Read 3 RGB bytes as a u32 (4th byte is junk but MapColor only uses low 24 bits)
240
+ let rgb = *(ptr as *const u32);
241
+ palette_lut[i + 1] = palette_map_color(palette_ctx, rgb) as u8;
242
+ ptr = ptr.add(3);
243
+ }
244
+
245
+ // Update global palette byte counter
246
+ let counter = rb(G_SPRITE_PALETTE_BYTES) as *mut u32;
247
+ *counter = (*counter).wrapping_add(palette_count as u32 * 3);
248
+
249
+ // ── 2. Read width and height ──
250
+ let width = *(ptr as *const u16) as u32;
251
+ let height = *(ptr.add(2) as *const u16) as u32;
252
+ ptr = ptr.add(2); // advance past width (height pointer computed differently)
253
+
254
+ // ── 3. Allocate DisplayBitGrid (0x4C bytes, 8bpp only) ──
255
+ let grid = wa_malloc_struct_zeroed::<DisplayBitGrid>();
256
+ if grid.is_null() {
257
+ return core::ptr::null_mut();
258
+ }
259
+
260
+ // ── 4. Compute pixel data pointer (4-byte aligned after width+height) ──
261
+ // Original: (ptr - raw_image + 5) & ~3 + raw_image
262
+ // ptr is at raw_image + 0x0C + palette_count * 3 + 2
263
+ let offset = ptr.offset_from(raw_image) as usize + 5; // +5 accounts for remaining header
264
+ let aligned_offset = (offset & !3) as usize;
265
+ let pixel_data = raw_image.add(aligned_offset);
266
+
267
+ // ── 5. Set up fields manually (matching original, not via BitGrid::init) ──
268
+ (*grid).vtable = rb(BIT_GRID_DISPLAY_VTABLE) as *const crate::bitgrid::BitGridDisplayVtable;
269
+ (*grid).cells_per_unit = 8;
270
+ (*grid).width = width;
271
+ (*grid).height = height;
272
+ (*grid).row_stride = width; // row_stride = width for 8bpp uncompressed
273
+ (*grid).data = pixel_data;
274
+ (*grid).clip_left = 0;
275
+ (*grid).clip_top = 0;
276
+ (*grid).clip_right = width;
277
+ (*grid).clip_bottom = height;
278
+ (*grid).external_buffer = 1; // pixel data is owned by the cached buffer, not the grid
279
+
280
+ // ── 6. Remap pixels through palette LUT ──
281
+ if (*grid).cells_per_unit == 8 {
282
+ let dwords_per_row = width / 4;
283
+ remap_pixels_through_lut(
284
+ pixel_data,
285
+ width,
286
+ palette_lut.as_ptr(),
287
+ dwords_per_row,
288
+ height,
289
+ );
290
+ }
291
+
292
+ grid
293
+ }
crates/openwa-core/src/asset/mod.rs CHANGED
@@ -1 +1,2 @@
1
  pub mod gfx_dir;
 
 
1
  pub mod gfx_dir;
2
+ pub mod img;
crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -3,7 +3,7 @@ use crate::audio::dssound::DSSound;
3
  use crate::audio::music::Music;
4
  use crate::audio::speech::SpeechSlotTable;
5
  use crate::audio::SoundQueueEntry;
6
- use crate::bitgrid::{CollisionBitGrid, DisplayBitGrid};
7
  use crate::engine::game_info::GameInfo;
8
  use crate::engine::{CoordEntry, CoordList, TeamArena, TeamIndexMap};
9
  use crate::fixed::Fixed;
@@ -61,12 +61,12 @@ pub struct DDGame {
61
  pub net_game: *mut u8,
62
  /// 0x02C: Secondary PaletteContext (0x70C bytes, conditional on secondary GfxDir)
63
  pub secondary_palette_ctx: *mut crate::render::palette::PaletteContext,
64
- /// 0x030: Gradient image pointer
65
- pub gradient_image: *mut u8,
66
- /// 0x034: Gradient image 2 pointer
67
- pub gradient_image_2: *mut u8,
68
- /// 0x038-0x0B4: Arrow sprite object pointers (32 entries)
69
- pub arrow_sprites: [*mut u8; 32],
70
  /// 0x0B8-0x134: Arrow GfxDir pointers (32 entries, conditional)
71
  pub arrow_gfxdirs: [*mut u8; 32],
72
  /// 0x138: Primary display BitGrid (vtable 0x664144, 8bpp pixel buffer).
 
3
  use crate::audio::music::Music;
4
  use crate::audio::speech::SpeechSlotTable;
5
  use crate::audio::SoundQueueEntry;
6
+ use crate::bitgrid::{BitGrid, CollisionBitGrid, DisplayBitGrid};
7
  use crate::engine::game_info::GameInfo;
8
  use crate::engine::{CoordEntry, CoordList, TeamArena, TeamIndexMap};
9
  use crate::fixed::Fixed;
 
61
  pub net_game: *mut u8,
62
  /// 0x02C: Secondary PaletteContext (0x70C bytes, conditional on secondary GfxDir)
63
  pub secondary_palette_ctx: *mut crate::render::palette::PaletteContext,
64
+ /// 0x030: Gradient image (decoded from gradient.img via IMG_Decode).
65
+ pub gradient_image: *mut BitGrid,
66
+ /// 0x034: Gradient image 2 pointer.
67
+ pub gradient_image_2: *mut BitGrid,
68
+ /// 0x038-0x0B4: Arrow sprite BitGrid pointers (32 entries, decoded from arrow*.img).
69
+ pub arrow_sprites: [*mut BitGrid; 32],
70
  /// 0x0B8-0x134: Arrow GfxDir pointers (32 entries, conditional)
71
  pub arrow_gfxdirs: [*mut u8; 32],
72
  /// 0x138: Primary display BitGrid (vtable 0x664144, 8bpp pixel buffer).
crates/openwa-core/src/engine/ddgame_constructor.rs CHANGED
@@ -12,7 +12,8 @@ use crate::address::va;
12
  use crate::asset::gfx_dir::{
13
  call_gfx_find_and_load, call_gfx_load_and_wrap, gfx_dir_load_dir, GfxDir, GfxDirVtable,
14
  };
15
- pub use crate::asset::gfx_dir::{gfx_dir_find_entry, gfx_resource_create};
 
16
  use crate::audio::active_sound::ActiveSoundTable;
17
  use crate::audio::dssound::DSSound;
18
  use crate::audio::music::Music;
@@ -173,7 +174,7 @@ unsafe fn wa_init_version_flags(wrapper: *mut DDGameWrapper) {
173
  /// GfxHandler__LoadSprites (0x570B50): usercall(ESI=layer_ctx) + stdcall(4 params).
174
  ///
175
  /// ESI must hold the display layer context (from DisplayGfx::set_active_layer).
176
- /// The function uses ESI for LoadSpriteFromVfs and GfxResource__Create_Maybe
177
  /// when param4 (gfx_dir) is non-null.
178
  #[cfg(target_arch = "x86")]
179
  #[unsafe(naked)]
@@ -253,7 +254,7 @@ unsafe fn load_effect_wavs(wrapper: *mut DDGameWrapper) {
253
  unsafe fn wa_pc_landscape_ctor(
254
  this: *mut u8,
255
  ddgame: *mut DDGame,
256
- gfx_resource: *mut u8,
257
  display: *mut DisplayGfx,
258
  landscape_data: *const u8,
259
  byte_output: *mut u8,
@@ -266,7 +267,7 @@ unsafe fn wa_pc_landscape_ctor(
266
  let f: unsafe extern "stdcall" fn(
267
  *mut u8,
268
  *mut DDGame,
269
- *mut u8,
270
  *mut DisplayGfx,
271
  *const u8,
272
  *mut u8,
@@ -291,12 +292,13 @@ unsafe fn wa_pc_landscape_ctor(
291
  )
292
  }
293
 
294
- /// DisplayGfx__Constructor (0x4F5E80): wrap raw image data in DisplayGfx object.
295
  #[cfg(target_arch = "x86")]
296
- unsafe fn wa_displaygfx_ctor(raw_image: *mut u8) -> *mut u8 {
297
- let f: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
298
- core::mem::transmute(rb(va::DISPLAYGFX_CONSTRUCTOR) as usize);
299
- f(raw_image)
 
300
  }
301
 
302
  /// DDGame__InitDisplayFinal (0x56A830): finalize display for non-headless mode.
@@ -545,27 +547,27 @@ unsafe fn init_graphics_and_resources(
545
 
546
  // ── Display vtable slot 5 (offset 0x14) ──
547
  // Original: CALL EAX (vtable[5]), saves return value in ESI for use as
548
- // the `output` parameter in the color-entries GfxResource__Create call below.
549
  let layer_ctx = (*(*ddgame).display).set_active_layer(1);
550
 
551
  // ── GfxDir color entries DDGame+0x730C..0x732C ──
552
- // Original logic: if gfx_mode!=0, try GfxResource__Create for colours.img.
553
  // If gfx_mode==0 OR resource creation fails, fall back to LoadSprites.
554
  // The fallback's 4th param is primary_gfx_dir when gfx_mode==0, or 0 on resource fail.
555
  if (*wrapper).gfx_mode != 0 {
556
- let res = gfx_resource_create(
557
  (*wrapper).primary_gfx_dir,
558
  rb(va::STR_COLOURS_IMG) as *const core::ffi::c_char,
559
  layer_ctx,
560
  );
561
  if !res.is_null() {
562
  let rvt = *(res as *const *const u32);
563
- let get_color: unsafe extern "thiscall" fn(*mut u8, u32, u32) -> u32 =
564
  core::mem::transmute(*rvt.add(4));
565
  for i in 0..9u32 {
566
  (*ddgame).gfx_color_table[i as usize] = get_color(res, i, 0);
567
  }
568
- let release: unsafe extern "thiscall" fn(*mut u8, u8) =
569
  core::mem::transmute(*rvt.add(3));
570
  release(res, 1);
571
  } else {
@@ -638,16 +640,16 @@ unsafe fn init_graphics_and_resources(
638
  // ── GfxResource for masks.img ──
639
  // The original constructor uses a stack-local PaletteContext buffer (ESP+0x1A4)
640
  // initialized at the start with (word=1, word=0xFE, PaletteContext__Init).
641
- // This same buffer is reused as the output param for GfxResource__Create(masks.img).
642
  // We replicate this initialization here. Size 0x900 matches the stack allocation.
643
- let gfx_resource: *mut u8;
644
  {
645
  let gfx_dir = (*wrapper).primary_gfx_dir;
646
  let palette_ctx = wa_malloc_zeroed(0x900) as *mut crate::render::palette::PaletteContext;
647
  (*palette_ctx).dirty_range_min = 1;
648
  (*palette_ctx).dirty_range_max = 0xFE;
649
  crate::render::palette::palette_context_init(palette_ctx);
650
- gfx_resource = gfx_resource_create(
651
  gfx_dir,
652
  rb(va::STR_MASKS_IMG) as *const core::ffi::c_char,
653
  palette_ctx as *mut crate::render::palette::PaletteContext,
@@ -659,7 +661,7 @@ unsafe fn init_graphics_and_resources(
659
  let use_orig = std::env::var("OPENWA_USE_ORIG_CTOR").is_ok();
660
  let tag = if use_orig { "orig" } else { "rust" };
661
  // Dump GfxResource object + first sub-object
662
- let gr_data = core::slice::from_raw_parts(gfx_resource, 0x100);
663
  let _ = std::fs::write(format!("gfx_resource_{}.bin", tag), gr_data);
664
  }
665
 
@@ -707,7 +709,7 @@ unsafe fn init_graphics_and_resources(
707
  // SpriteRegion__Constructor: fastcall(ECX, EDX) + 6 stack(this, p2, p3, p4, p5, p6), RET 0x18
708
  {
709
  // (array_index, ECX, EDX, p2, p3, p4, p5, p6=gfx_resource)
710
- // p6 is the GfxResource pointer returned by GfxResource__Create_Maybe.
711
  let gr = gfx_resource as u32;
712
  let regions: [(usize, u32, u32, u32, u32, u32, u32, u32); 8] = [
713
  (2, 0x37, 0x36, 0x2E, 0x24, 0x41, 0x2D, gr),
@@ -756,22 +758,27 @@ unsafe fn init_graphics_and_resources(
756
 
757
  let entry = gfx_dir_find_entry(name_buf.as_ptr().cast(), gfx_dir);
758
 
759
- let sprite: *mut u8;
760
- if !entry.is_null() {
761
  let entry_val = *(entry.add(4) as *const u32);
762
  let cached = GfxDir::load_cached_raw(gfx_dir, entry_val);
763
  if !cached.is_null() {
764
- sprite = wa_displaygfx_ctor(cached);
 
 
 
 
 
765
  } else {
766
- // Fallback: load from file via GfxDir__LoadImage + IMG_Decode
767
- sprite = call_gfx_load_and_wrap(gfx_dir, name_buf.as_ptr().cast(), layer_ctx);
768
  }
769
  } else {
770
- // Entry not found β€” try direct file load
771
- sprite = call_gfx_load_and_wrap(gfx_dir, name_buf.as_ptr().cast(), layer_ctx);
772
- }
773
 
774
  // Store arrow sprite at DDGame+0x38+i*4
 
 
 
775
  (*ddgame).arrow_sprites[i as usize] = sprite;
776
 
777
  // Calculate collision region dimensions from sprite.
@@ -780,9 +787,8 @@ unsafe fn init_graphics_and_resources(
780
  // right/bottom = dim - margin
781
  // Result: this[5]=this[6]=10 for sprites >20px (the 10px inset).
782
  if !sprite.is_null() {
783
- let grid = &*(sprite as *const BitGrid);
784
- let sprite_w = grid.width as i32;
785
- let sprite_h = grid.height as i32;
786
  let margin_w = (sprite_w / 2 - 10).max(0);
787
  let margin_h = (sprite_h / 2 - 10).max(0);
788
 
@@ -807,12 +813,12 @@ unsafe fn init_graphics_and_resources(
807
 
808
  // Arrow GfxDir (conditional on secondary gfxdir)
809
  if !(*ddgame).secondary_palette_ctx.is_null() {
810
- let gfx_resource_create: unsafe extern "thiscall" fn(
811
  *mut GfxDir,
812
  *mut u8,
813
- ) -> *mut u8 = core::mem::transmute(rb(va::GFX_RESOURCE_CREATE) as usize);
814
  (*ddgame).arrow_gfxdirs[i as usize] =
815
- gfx_resource_create(gfx_dir, core::ptr::null_mut());
816
  }
817
  }
818
  }
@@ -821,7 +827,8 @@ unsafe fn init_graphics_and_resources(
821
  // vtable[3] = DisplayGfx__vmethod_3: thiscall(this, byte param_2), RET 4.
822
  if !gfx_resource.is_null() {
823
  let rvt = *(gfx_resource as *const *const u32);
824
- let release: unsafe extern "thiscall" fn(*mut u8, u8) = core::mem::transmute(*rvt.add(3));
 
825
  release(gfx_resource, 1);
826
  }
827
 
@@ -956,8 +963,10 @@ unsafe fn init_graphics_and_resources(
956
 
957
  if s_var1 < 0x61 && level_height == 0x2B8 {
958
  // Simple gradient: load gradient.img directly
959
- let gradient = call_gfx_find_and_load(land_dir, c"gradient.img", layer3_ctx);
960
- (*ddgame).gradient_image = gradient;
 
 
961
  } else {
962
  compute_complex_gradient(ddgame, land_dir, layer3_ctx, s_var1);
963
  }
@@ -965,19 +974,18 @@ unsafe fn init_graphics_and_resources(
965
  // ── Fill image β†’ fill_pixel (0x7338) ──
966
  {
967
  let layer2_ctx = (*(*ddgame).display).set_active_layer(2);
968
- // In the original, fill.img uses piStack_126c which the decompiler
969
- // shows was set from piVar3 (water_layer from landscape+0xB38).
970
  let fill_sprite = call_gfx_find_and_load(water_dir, c"fill.img", layer2_ctx);
971
- if !fill_sprite.is_null() {
972
- // Get pixel value: fill_sprite->vtable[4](0, 0)
973
- let fill_vt = *(fill_sprite as *const *const u32);
974
- let get_pixel: unsafe extern "thiscall" fn(*mut u8, i32, i32) -> u32 =
 
975
  core::mem::transmute(*fill_vt.add(4));
976
- (*ddgame).fill_pixel = get_pixel(fill_sprite, 0, 0);
977
- // Release fill sprite: vtable[3] = DisplayGfx__vmethod_3(this, param_2=1)
978
- let release: unsafe extern "thiscall" fn(*mut u8, u8) =
979
  core::mem::transmute(*fill_vt.add(3));
980
- release(fill_sprite, 1);
981
  }
982
  }
983
 
@@ -1006,7 +1014,7 @@ unsafe fn init_graphics_and_resources(
1006
  if !obj.is_null() {
1007
  (*obj).vtable = rb(va::BIT_GRID_BASE_VTABLE) as *const BitGridBaseVtable;
1008
  // height = 0 β†’ CTaskLand skips the gradient column loop
1009
- (*ddgame).gradient_image = obj as *mut u8;
1010
  }
1011
  }
1012
 
 
12
  use crate::asset::gfx_dir::{
13
  call_gfx_find_and_load, call_gfx_load_and_wrap, gfx_dir_load_dir, GfxDir, GfxDirVtable,
14
  };
15
+ pub use crate::asset::gfx_dir::{gfx_dir_find_entry, img_load_from_dir};
16
+ use crate::asset::img::DecodedBitGrid;
17
  use crate::audio::active_sound::ActiveSoundTable;
18
  use crate::audio::dssound::DSSound;
19
  use crate::audio::music::Music;
 
174
  /// GfxHandler__LoadSprites (0x570B50): usercall(ESI=layer_ctx) + stdcall(4 params).
175
  ///
176
  /// ESI must hold the display layer context (from DisplayGfx::set_active_layer).
177
+ /// The function uses ESI for LoadSpriteFromVfs and IMG__LoadFromDir
178
  /// when param4 (gfx_dir) is non-null.
179
  #[cfg(target_arch = "x86")]
180
  #[unsafe(naked)]
 
254
  unsafe fn wa_pc_landscape_ctor(
255
  this: *mut u8,
256
  ddgame: *mut DDGame,
257
+ gfx_resource: *mut crate::bitgrid::BitGrid,
258
  display: *mut DisplayGfx,
259
  landscape_data: *const u8,
260
  byte_output: *mut u8,
 
267
  let f: unsafe extern "stdcall" fn(
268
  *mut u8,
269
  *mut DDGame,
270
+ *mut crate::bitgrid::BitGrid,
271
  *mut DisplayGfx,
272
  *const u8,
273
  *mut u8,
 
292
  )
293
  }
294
 
295
+ /// Decode a cached raw image buffer into a BitGrid (0x4F5E80).
296
  #[cfg(target_arch = "x86")]
297
+ unsafe fn wa_displaygfx_ctor(
298
+ palette_ctx: *mut crate::render::palette::PaletteContext,
299
+ raw_image: *mut u8,
300
+ ) -> *mut BitGrid {
301
+ crate::asset::img::img_decode_cached(palette_ctx, raw_image) as *mut BitGrid
302
  }
303
 
304
  /// DDGame__InitDisplayFinal (0x56A830): finalize display for non-headless mode.
 
547
 
548
  // ── Display vtable slot 5 (offset 0x14) ──
549
  // Original: CALL EAX (vtable[5]), saves return value in ESI for use as
550
+ // the `output` parameter in the color-entries IMG__LoadFromDir call below.
551
  let layer_ctx = (*(*ddgame).display).set_active_layer(1);
552
 
553
  // ── GfxDir color entries DDGame+0x730C..0x732C ──
554
+ // Original logic: if gfx_mode!=0, try IMG__LoadFromDir for colours.img.
555
  // If gfx_mode==0 OR resource creation fails, fall back to LoadSprites.
556
  // The fallback's 4th param is primary_gfx_dir when gfx_mode==0, or 0 on resource fail.
557
  if (*wrapper).gfx_mode != 0 {
558
+ let res = img_load_from_dir(
559
  (*wrapper).primary_gfx_dir,
560
  rb(va::STR_COLOURS_IMG) as *const core::ffi::c_char,
561
  layer_ctx,
562
  );
563
  if !res.is_null() {
564
  let rvt = *(res as *const *const u32);
565
+ let get_color: unsafe extern "thiscall" fn(*mut BitGrid, u32, u32) -> u32 =
566
  core::mem::transmute(*rvt.add(4));
567
  for i in 0..9u32 {
568
  (*ddgame).gfx_color_table[i as usize] = get_color(res, i, 0);
569
  }
570
+ let release: unsafe extern "thiscall" fn(*mut BitGrid, u8) =
571
  core::mem::transmute(*rvt.add(3));
572
  release(res, 1);
573
  } else {
 
640
  // ── GfxResource for masks.img ──
641
  // The original constructor uses a stack-local PaletteContext buffer (ESP+0x1A4)
642
  // initialized at the start with (word=1, word=0xFE, PaletteContext__Init).
643
+ // This same buffer is reused as the output param for IMG__LoadFromDir(masks.img).
644
  // We replicate this initialization here. Size 0x900 matches the stack allocation.
645
+ let gfx_resource: *mut crate::bitgrid::BitGrid;
646
  {
647
  let gfx_dir = (*wrapper).primary_gfx_dir;
648
  let palette_ctx = wa_malloc_zeroed(0x900) as *mut crate::render::palette::PaletteContext;
649
  (*palette_ctx).dirty_range_min = 1;
650
  (*palette_ctx).dirty_range_max = 0xFE;
651
  crate::render::palette::palette_context_init(palette_ctx);
652
+ gfx_resource = img_load_from_dir(
653
  gfx_dir,
654
  rb(va::STR_MASKS_IMG) as *const core::ffi::c_char,
655
  palette_ctx as *mut crate::render::palette::PaletteContext,
 
661
  let use_orig = std::env::var("OPENWA_USE_ORIG_CTOR").is_ok();
662
  let tag = if use_orig { "orig" } else { "rust" };
663
  // Dump GfxResource object + first sub-object
664
+ let gr_data = core::slice::from_raw_parts(gfx_resource as *const u8, 0x100);
665
  let _ = std::fs::write(format!("gfx_resource_{}.bin", tag), gr_data);
666
  }
667
 
 
709
  // SpriteRegion__Constructor: fastcall(ECX, EDX) + 6 stack(this, p2, p3, p4, p5, p6), RET 0x18
710
  {
711
  // (array_index, ECX, EDX, p2, p3, p4, p5, p6=gfx_resource)
712
+ // p6 is the GfxResource pointer returned by IMG__LoadFromDir.
713
  let gr = gfx_resource as u32;
714
  let regions: [(usize, u32, u32, u32, u32, u32, u32, u32); 8] = [
715
  (2, 0x37, 0x36, 0x2E, 0x24, 0x41, 0x2D, gr),
 
758
 
759
  let entry = gfx_dir_find_entry(name_buf.as_ptr().cast(), gfx_dir);
760
 
761
+ let decoded = if !entry.is_null() {
 
762
  let entry_val = *(entry.add(4) as *const u32);
763
  let cached = GfxDir::load_cached_raw(gfx_dir, entry_val);
764
  if !cached.is_null() {
765
+ let grid = wa_displaygfx_ctor(layer_ctx, cached);
766
+ if !grid.is_null() {
767
+ Some(DecodedBitGrid::Display(grid as *mut DisplayBitGrid))
768
+ } else {
769
+ None
770
+ }
771
  } else {
772
+ call_gfx_load_and_wrap(gfx_dir, name_buf.as_ptr().cast(), layer_ctx)
 
773
  }
774
  } else {
775
+ call_gfx_load_and_wrap(gfx_dir, name_buf.as_ptr().cast(), layer_ctx)
776
+ };
 
777
 
778
  // Store arrow sprite at DDGame+0x38+i*4
779
+ let sprite = decoded
780
+ .map(|d| d.as_bitgrid_ptr())
781
+ .unwrap_or(core::ptr::null_mut());
782
  (*ddgame).arrow_sprites[i as usize] = sprite;
783
 
784
  // Calculate collision region dimensions from sprite.
 
787
  // right/bottom = dim - margin
788
  // Result: this[5]=this[6]=10 for sprites >20px (the 10px inset).
789
  if !sprite.is_null() {
790
+ let sprite_w = (*sprite).width as i32;
791
+ let sprite_h = (*sprite).height as i32;
 
792
  let margin_w = (sprite_w / 2 - 10).max(0);
793
  let margin_h = (sprite_h / 2 - 10).max(0);
794
 
 
813
 
814
  // Arrow GfxDir (conditional on secondary gfxdir)
815
  if !(*ddgame).secondary_palette_ctx.is_null() {
816
+ let wa_img_load_from_dir: unsafe extern "thiscall" fn(
817
  *mut GfxDir,
818
  *mut u8,
819
+ ) -> *mut u8 = core::mem::transmute(rb(va::IMG_LOAD_FROM_DIR) as usize);
820
  (*ddgame).arrow_gfxdirs[i as usize] =
821
+ wa_img_load_from_dir(gfx_dir, core::ptr::null_mut());
822
  }
823
  }
824
  }
 
827
  // vtable[3] = DisplayGfx__vmethod_3: thiscall(this, byte param_2), RET 4.
828
  if !gfx_resource.is_null() {
829
  let rvt = *(gfx_resource as *const *const u32);
830
+ let release: unsafe extern "thiscall" fn(*mut crate::bitgrid::BitGrid, u8) =
831
+ core::mem::transmute(*rvt.add(3));
832
  release(gfx_resource, 1);
833
  }
834
 
 
963
 
964
  if s_var1 < 0x61 && level_height == 0x2B8 {
965
  // Simple gradient: load gradient.img directly
966
+ (*ddgame).gradient_image =
967
+ call_gfx_find_and_load(land_dir, c"gradient.img", layer3_ctx)
968
+ .map(|d| d.as_bitgrid_ptr())
969
+ .unwrap_or(core::ptr::null_mut());
970
  } else {
971
  compute_complex_gradient(ddgame, land_dir, layer3_ctx, s_var1);
972
  }
 
974
  // ── Fill image β†’ fill_pixel (0x7338) ──
975
  {
976
  let layer2_ctx = (*(*ddgame).display).set_active_layer(2);
 
 
977
  let fill_sprite = call_gfx_find_and_load(water_dir, c"fill.img", layer2_ctx);
978
+ if let Some(decoded) = fill_sprite {
979
+ let ptr = decoded.as_bitgrid_ptr();
980
+ // Get pixel value: vtable[4](0, 0)
981
+ let fill_vt = *(ptr as *const *const u32);
982
+ let get_pixel: unsafe extern "thiscall" fn(*mut BitGrid, i32, i32) -> u32 =
983
  core::mem::transmute(*fill_vt.add(4));
984
+ (*ddgame).fill_pixel = get_pixel(ptr, 0, 0);
985
+ // Release: vtable[3](this, flags=1)
986
+ let release: unsafe extern "thiscall" fn(*mut BitGrid, u8) =
987
  core::mem::transmute(*fill_vt.add(3));
988
+ release(ptr, 1);
989
  }
990
  }
991
 
 
1014
  if !obj.is_null() {
1015
  (*obj).vtable = rb(va::BIT_GRID_BASE_VTABLE) as *const BitGridBaseVtable;
1016
  // height = 0 β†’ CTaskLand skips the gradient column loop
1017
+ (*ddgame).gradient_image = obj;
1018
  }
1019
  }
1020
 
crates/openwa-core/src/render/display/gradient.rs CHANGED
@@ -117,18 +117,19 @@ pub(crate) unsafe fn compute_complex_gradient(
117
  let mut palette = PaletteContext::new();
118
 
119
  // Step 1: Load gradient.img
120
- let gradient_sprite = call_gfx_find_and_load(land_dir, c"gradient.img", layer3_ctx);
121
- if gradient_sprite.is_null() {
122
- return;
123
- }
 
124
 
125
  // Sprite height from BitGrid layout (shared vtable 0x6640EC)
126
- let gradient_height = (*(gradient_sprite as *const BitGrid)).height as i32;
127
  if gradient_height <= 0 {
128
  return;
129
  }
130
 
131
- let get_pixel: unsafe extern "thiscall" fn(*mut u8, i32, i32) -> u32 =
132
  core::mem::transmute(*(*(gradient_sprite as *const *const u32)).add(4));
133
 
134
  // Step 2: Compute target rows and stretch gradient through palette
@@ -146,8 +147,9 @@ pub(crate) unsafe fn compute_complex_gradient(
146
 
147
  // Step 3: If heights match, also set gradient_image_2
148
  if target_rows == gradient_height {
149
- let gradient2 = call_gfx_find_and_load(land_dir, c"gradient.img", layer3_ctx);
150
- (*ddgame).gradient_image_2 = gradient2;
 
151
  }
152
 
153
  // Step 4: Sample 7 anchor colors by averaging 8Γ—2 pixel blocks.
@@ -184,7 +186,7 @@ pub(crate) unsafe fn compute_complex_gradient(
184
 
185
  // Release the gradient sprite
186
  let gvt = *(gradient_sprite as *const *const u32);
187
- let release: unsafe extern "thiscall" fn(*mut u8, u8) = core::mem::transmute(*gvt.add(3));
188
  release(gradient_sprite, 1);
189
 
190
  // Step 5: Establish initial/fallback color via palette closest-match.
@@ -256,5 +258,5 @@ pub(crate) unsafe fn compute_complex_gradient(
256
  (*gfx_obj).row_stride = stride;
257
  (*gfx_obj).width = 0;
258
  (*gfx_obj).height = total_height as u32;
259
- (*ddgame).gradient_image = gfx_obj as *mut u8;
260
  }
 
117
  let mut palette = PaletteContext::new();
118
 
119
  // Step 1: Load gradient.img
120
+ let gradient_decoded = call_gfx_find_and_load(land_dir, c"gradient.img", layer3_ctx);
121
+ let gradient_sprite = match gradient_decoded {
122
+ Some(d) => d.as_bitgrid_ptr(),
123
+ None => return,
124
+ };
125
 
126
  // Sprite height from BitGrid layout (shared vtable 0x6640EC)
127
+ let gradient_height = (*gradient_sprite).height as i32;
128
  if gradient_height <= 0 {
129
  return;
130
  }
131
 
132
+ let get_pixel: unsafe extern "thiscall" fn(*mut BitGrid, i32, i32) -> u32 =
133
  core::mem::transmute(*(*(gradient_sprite as *const *const u32)).add(4));
134
 
135
  // Step 2: Compute target rows and stretch gradient through palette
 
147
 
148
  // Step 3: If heights match, also set gradient_image_2
149
  if target_rows == gradient_height {
150
+ (*ddgame).gradient_image_2 = call_gfx_find_and_load(land_dir, c"gradient.img", layer3_ctx)
151
+ .map(|d| d.as_bitgrid_ptr())
152
+ .unwrap_or(core::ptr::null_mut());
153
  }
154
 
155
  // Step 4: Sample 7 anchor colors by averaging 8Γ—2 pixel blocks.
 
186
 
187
  // Release the gradient sprite
188
  let gvt = *(gradient_sprite as *const *const u32);
189
+ let release: unsafe extern "thiscall" fn(*mut BitGrid, u8) = core::mem::transmute(*gvt.add(3));
190
  release(gradient_sprite, 1);
191
 
192
  // Step 5: Establish initial/fallback color via palette closest-match.
 
258
  (*gfx_obj).row_stride = stride;
259
  (*gfx_obj).width = 0;
260
  (*gfx_obj).height = total_height as u32;
261
+ (*ddgame).gradient_image = gfx_obj;
262
  }
crates/openwa-dll/src/replacements/ddgame_init.rs CHANGED
@@ -8,14 +8,14 @@
8
  //! - `DDGame__InitFields` (0x526120): usercall(EDI=ddgame), plain RET β†’ EAX=ddgame
9
  //! - `DDGame__InitRenderIndices` (0x526080): usercall(ESI=base), plain RET β†’ EAX=base
10
  //! - `BitGrid__Init` (0x4F6370): usercall(ESI,ECX,EDI) + 1 stack, RET 0x4
11
- //! - `GfxResource__Create_Maybe` (0x4F6300): usercall(ECX,EAX) + 1 stack, RET 0x4
12
  //! - `FUN_570E20` (display layer init): usercall(ESI=wrapper), plain RET
13
 
14
  use crate::hook;
15
  use core::ffi::c_char;
16
  use openwa_core::address::va;
17
  use openwa_core::asset::gfx_dir::{
18
- gfx_dir_find_entry, gfx_dir_load_dir, gfx_resource_create, GfxDir,
19
  };
20
  use openwa_core::bitgrid::BitGrid;
21
  use openwa_core::engine::game_state_init::{
@@ -95,9 +95,9 @@ extern "cdecl" fn impl_display_layer_init(wrapper: *mut DDGameWrapper) -> *mut D
95
  wrapper
96
  }
97
 
98
- // ─── GfxResource__Create_Maybe (0x4F6300) ───────────────────────────────────
99
 
100
- extern "cdecl" fn impl_gfx_resource_create(
101
  gfx_dir: *mut GfxDir,
102
  name: *const c_char,
103
  output: *mut u8,
@@ -106,12 +106,12 @@ extern "cdecl" fn impl_gfx_resource_create(
106
  // WA caller always passes a `PaletteContext*` (verified at all known
107
  // call sites β€” `set_active_layer`'s return value).
108
  let output = output as *mut openwa_core::render::palette::PaletteContext;
109
- let result = unsafe { gfx_resource_create(gfx_dir, name, output) };
110
  result as u32
111
  }
112
 
113
  #[unsafe(naked)]
114
- unsafe extern "C" fn gfx_resource_create_trampoline() {
115
  core::arch::naked_asm!(
116
  "push edx",
117
  "push [esp+8]", // output (stack param)
@@ -121,7 +121,7 @@ unsafe extern "C" fn gfx_resource_create_trampoline() {
121
  "add esp, 12",
122
  "pop edx",
123
  "ret 0x4",
124
- impl_fn = sym impl_gfx_resource_create,
125
  );
126
  }
127
 
@@ -197,9 +197,9 @@ pub fn install() -> Result<(), String> {
197
  )?;
198
 
199
  hook::install(
200
- "GfxResource__Create",
201
- va::GFX_RESOURCE_CREATE,
202
- gfx_resource_create_trampoline as *const (),
203
  )?;
204
 
205
  hook::install(
 
8
  //! - `DDGame__InitFields` (0x526120): usercall(EDI=ddgame), plain RET β†’ EAX=ddgame
9
  //! - `DDGame__InitRenderIndices` (0x526080): usercall(ESI=base), plain RET β†’ EAX=base
10
  //! - `BitGrid__Init` (0x4F6370): usercall(ESI,ECX,EDI) + 1 stack, RET 0x4
11
+ //! - `IMG__LoadFromDir` (0x4F6300): usercall(ECX,EAX) + 1 stack, RET 0x4
12
  //! - `FUN_570E20` (display layer init): usercall(ESI=wrapper), plain RET
13
 
14
  use crate::hook;
15
  use core::ffi::c_char;
16
  use openwa_core::address::va;
17
  use openwa_core::asset::gfx_dir::{
18
+ gfx_dir_find_entry, gfx_dir_load_dir, img_load_from_dir, GfxDir,
19
  };
20
  use openwa_core::bitgrid::BitGrid;
21
  use openwa_core::engine::game_state_init::{
 
95
  wrapper
96
  }
97
 
98
+ // ─── IMG__LoadFromDir (0x4F6300) ───────────────────────────────────
99
 
100
+ extern "cdecl" fn impl_img_load_from_dir(
101
  gfx_dir: *mut GfxDir,
102
  name: *const c_char,
103
  output: *mut u8,
 
106
  // WA caller always passes a `PaletteContext*` (verified at all known
107
  // call sites β€” `set_active_layer`'s return value).
108
  let output = output as *mut openwa_core::render::palette::PaletteContext;
109
+ let result = unsafe { img_load_from_dir(gfx_dir, name, output) };
110
  result as u32
111
  }
112
 
113
  #[unsafe(naked)]
114
+ unsafe extern "C" fn img_load_from_dir_trampoline() {
115
  core::arch::naked_asm!(
116
  "push edx",
117
  "push [esp+8]", // output (stack param)
 
121
  "add esp, 12",
122
  "pop edx",
123
  "ret 0x4",
124
+ impl_fn = sym impl_img_load_from_dir,
125
  );
126
  }
127
 
 
197
  )?;
198
 
199
  hook::install(
200
+ "IMG__LoadFromDir",
201
+ va::IMG_LOAD_FROM_DIR,
202
+ img_load_from_dir_trampoline as *const (),
203
  )?;
204
 
205
  hook::install(
crates/openwa-dll/src/replacements/gfx_dir.rs CHANGED
@@ -1,7 +1,6 @@
1
- //! GfxDir and GfxDirStream vtable replacements + LoadImage hook.
2
  //!
3
- //! Replaces the file I/O and stream vtable methods with Rust implementations,
4
- //! and hooks GfxDir__LoadImage for any remaining WA callers.
5
 
6
  use core::ffi::c_char;
7
  use openwa_core::address::va;
@@ -10,7 +9,10 @@ use openwa_core::asset::gfx_dir::{
10
  gfx_dir_stream_bytes_consumed, gfx_dir_stream_destructor, gfx_dir_stream_get_total_size,
11
  gfx_dir_stream_has_data, gfx_dir_stream_read, gfx_dir_stream_seek, GfxDir, GfxDirStream,
12
  };
 
 
13
  use openwa_core::log::log_line;
 
14
 
15
  // ─── GfxDir__LoadImage (0x5666D0) ───────────────────────────────────────────
16
  // Convention: usercall(ESI=gfx_dir) + 1 stack(name), RET 0x4.
@@ -33,6 +35,47 @@ unsafe extern "C" fn load_image_trampoline() {
33
  );
34
  }
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  pub fn install() -> Result<(), String> {
37
  use openwa_core::vtable_replace;
38
 
@@ -58,8 +101,16 @@ pub fn install() -> Result<(), String> {
58
  va::GFX_DIR_LOAD_IMAGE,
59
  load_image_trampoline as *const (),
60
  )?;
 
 
 
 
 
 
 
 
61
  }
62
 
63
- let _ = log_line("[GfxDir] All vtable methods + LoadImage replaced");
64
  Ok(())
65
  }
 
1
+ //! GfxDir, GfxDirStream, and IMG decoder replacements.
2
  //!
3
+ //! Replaces vtable methods, LoadImage, IMG_Decode, and DisplayGfx__Constructor.
 
4
 
5
  use core::ffi::c_char;
6
  use openwa_core::address::va;
 
9
  gfx_dir_stream_bytes_consumed, gfx_dir_stream_destructor, gfx_dir_stream_get_total_size,
10
  gfx_dir_stream_has_data, gfx_dir_stream_read, gfx_dir_stream_seek, GfxDir, GfxDirStream,
11
  };
12
+ use openwa_core::asset::img::{img_decode, img_decode_cached};
13
+ use openwa_core::bitgrid::BitGrid;
14
  use openwa_core::log::log_line;
15
+ use openwa_core::render::palette::PaletteContext;
16
 
17
  // ─── GfxDir__LoadImage (0x5666D0) ───────────────────────────────────────────
18
  // Convention: usercall(ESI=gfx_dir) + 1 stack(name), RET 0x4.
 
35
  );
36
  }
37
 
38
+ // ─── IMG_Decode (0x4F5F80) ──────────────────────────────────────────────────
39
+ // Convention: stdcall(palette_ctx, stream, align_flag), RET 0xC.
40
+
41
+ unsafe extern "stdcall" fn img_decode_hook(
42
+ palette_ctx: *mut PaletteContext,
43
+ stream: *mut GfxDirStream,
44
+ align_flag: i32,
45
+ ) -> *mut BitGrid {
46
+ match img_decode(palette_ctx, stream, align_flag) {
47
+ Some(decoded) => decoded.as_bitgrid_ptr(),
48
+ None => core::ptr::null_mut(),
49
+ }
50
+ }
51
+
52
+ // ─── DisplayGfx__Constructor (0x4F5E80) ─────────────────────────────────────
53
+ // Convention: stdcall(raw_image), RET 0x4.
54
+ // PaletteContext passed implicitly via EBX (callee-saved from caller).
55
+
56
+ extern "cdecl" fn impl_displaygfx_ctor(
57
+ palette_ctx: *mut PaletteContext,
58
+ raw_image: *mut u8,
59
+ ) -> *mut u8 {
60
+ unsafe { img_decode_cached(palette_ctx, raw_image) as *mut u8 }
61
+ }
62
+
63
+ #[unsafe(naked)]
64
+ unsafe extern "C" fn displaygfx_ctor_trampoline() {
65
+ core::arch::naked_asm!(
66
+ "push edx",
67
+ "push ecx",
68
+ "push [esp+12]", // raw_image (stack param, shifted by 2 pushes)
69
+ "push ebx", // palette_ctx (EBX, callee-saved from caller)
70
+ "call {impl_fn}",
71
+ "add esp, 8",
72
+ "pop ecx",
73
+ "pop edx",
74
+ "ret 0x4", // callee cleans 1 stack param
75
+ impl_fn = sym impl_displaygfx_ctor,
76
+ );
77
+ }
78
+
79
  pub fn install() -> Result<(), String> {
80
  use openwa_core::vtable_replace;
81
 
 
101
  va::GFX_DIR_LOAD_IMAGE,
102
  load_image_trampoline as *const (),
103
  )?;
104
+
105
+ crate::hook::install("IMG_Decode", va::IMG_DECODE, img_decode_hook as *const ())?;
106
+
107
+ crate::hook::install(
108
+ "DisplayGfx__Constructor",
109
+ va::IMG_DECODE_CACHED,
110
+ displaygfx_ctor_trampoline as *const (),
111
+ )?;
112
  }
113
 
114
+ let _ = log_line("[GfxDir] All vtable methods + LoadImage + IMG_Decode replaced");
115
  Ok(())
116
  }