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

refactor: port GfxDir resource loading system to Rust

Browse files

Port the entire .dir archive I/O layer β€” GfxDir vtable (read, seek,
load_cached, release), GfxDirStream vtable (all 6 slots), LoadImage,
cache slot management, and cleanup β€” eliminating all WA.exe asm bridges.

- Move gfx_dir module from render/sprite/ to new asset/ module
- Type cache slots as GfxCacheSlot struct, rename _unknown fields
- Convert GfxDirStreamVtable to #[vtable] macro with bind_! wrappers
- Replace call_gfx_load_dir/call_gfx_load_image bridges with direct calls
- Add WA_FSEEK address, GFX_DIR_STREAM_VTABLE address
- Fix GfxDirStream.gfx_dir field type from *mut u8 to *mut GfxDir
- Rename GfxDirEntry._unknown_08 to data_size

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

crates/openwa-core/src/address.rs CHANGED
@@ -236,6 +236,11 @@ pub mod va {
236
  fn GFX_DIR_LOAD_IMAGE = 0x0056_66D0;
237
  }
238
 
 
 
 
 
 
239
  class "DisplayBase" {
240
  /// DisplayBase__AllocPaletteSlots β€” usercall EAX=count, 1 stack(this)
241
  fn/Usercall DISPLAY_BASE_ALLOC_PALETTE_SLOTS = 0x0052_3190;
@@ -801,6 +806,8 @@ pub mod va {
801
  fn/Cdecl WA_FOPEN = 0x005D_3271;
802
  /// WA's CRT fread
803
  fn/Cdecl WA_FREAD = 0x005D_4531;
 
 
804
  /// WA's CRT fclose
805
  fn/Cdecl WA_FCLOSE = 0x005D_399B;
806
  /// WA's CRT _fileno
 
236
  fn GFX_DIR_LOAD_IMAGE = 0x0056_66D0;
237
  }
238
 
239
+ class "GfxDirStream" {
240
+ /// GfxDirStream vtable (6 slots)
241
+ vtable GFX_DIR_STREAM_VTABLE = 0x0066_A1C0;
242
+ }
243
+
244
  class "DisplayBase" {
245
  /// DisplayBase__AllocPaletteSlots β€” usercall EAX=count, 1 stack(this)
246
  fn/Usercall DISPLAY_BASE_ALLOC_PALETTE_SLOTS = 0x0052_3190;
 
806
  fn/Cdecl WA_FOPEN = 0x005D_3271;
807
  /// WA's CRT fread
808
  fn/Cdecl WA_FREAD = 0x005D_4531;
809
+ /// WA's CRT fseek
810
+ fn/Cdecl WA_FSEEK = 0x005D_38A4;
811
  /// WA's CRT fclose
812
  fn/Cdecl WA_FCLOSE = 0x005D_399B;
813
  /// WA's CRT _fileno
crates/openwa-core/src/{render/sprite β†’ asset}/gfx_dir.rs RENAMED
@@ -10,6 +10,24 @@ use crate::address::va;
10
  use crate::rebase::rb;
11
  use crate::wa_alloc::{wa_malloc, wa_malloc_struct_zeroed};
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  /// .dir archive reader (0x19C bytes, vtable 0x66B280).
14
  ///
15
  /// Loaded from `.dir` files via `gfx_dir_load`. Contains a 1024-bucket
@@ -22,16 +40,16 @@ pub struct GfxDir {
22
  pub bucket_array: *mut u8,
23
  /// 0x008: 1 if bucket_array was allocated via malloc fallback (needs free).
24
  pub fallback_alloc: u32,
25
- /// 0x00C-0x10B: 16 cache slots (0x10 bytes each, first u32 zeroed on reset).
26
- pub cache_slots: [u8; 0x100],
27
- /// 0x10C-0x14B: Index table (16 u32 entries, identity permutation on reset).
28
  pub index_table: [u32; 16],
29
- /// 0x14C-0x18B: Unknown padding.
30
- pub _unknown_14c: [u8; 0x40],
31
- /// 0x18C: Number of cache slots (always 0x10).
32
  pub slot_count: u32,
33
- /// 0x190: Unknown (zeroed on reset).
34
- pub _unknown_190: u32,
35
  /// 0x194: Loaded flag (1 after successful gfx_dir_load).
36
  pub loaded: u32,
37
  /// 0x198: FILE* handle to the open .dir file.
@@ -59,8 +77,8 @@ impl GfxDir {
59
  pub struct GfxDirVtable {
60
  /// Slot 0 (0x58BBD0): Read `size` bytes into `buf`. Returns bytes read.
61
  pub read: fn(this: *mut GfxDir, buf: *mut u8, size: u32) -> u32,
62
- /// Slot 1 (0x58BBB0): Seek forward `skip` bytes in the file.
63
- pub seek: fn(this: *mut GfxDir, skip: u32),
64
  /// Slot 2 (0x571AF0): Return cached data pointer for `entry_val`, or null if not mapped.
65
  pub load_cached: fn(this: *mut GfxDir, entry_val: u32) -> *mut u8,
66
  /// Slot 3 (0x58BB70): Release resources; if `flags & 1`, free the object.
@@ -70,13 +88,15 @@ pub struct GfxDirVtable {
70
  bind_GfxDirVtable!(GfxDir, vtable);
71
 
72
  /// Entry in a GfxDir hash bucket linked list.
73
- /// Each entry maps a name string to a cached resource value.
74
  #[repr(C)]
75
  pub struct GfxDirEntry {
76
  pub next: *mut GfxDirEntry,
77
- /// Passed to GfxDir vtable[2] for cached resource lookup.
 
78
  pub value: u32,
79
- pub _unknown_08: u32,
 
80
  // +0x0C: null-terminated name (variable-length, not in struct)
81
  }
82
 
@@ -219,12 +239,11 @@ unsafe fn gfx_dir_reset(handler: *mut u8) {
219
  gfx.loaded = 0;
220
 
221
  for i in 0..16u32 {
222
- // Zero first u32 of each cache slot (stride 0x10)
223
- *(gfx.cache_slots.as_mut_ptr().add(i as usize * 0x10) as *mut u32) = 0;
224
  gfx.index_table[i as usize] = i;
225
  }
226
 
227
- gfx._unknown_190 = 0;
228
  gfx.slot_count = 0x10;
229
  }
230
 
@@ -337,6 +356,98 @@ pub unsafe fn gfx_dir_load_dir(handler: *mut u8) -> i32 {
337
  1 // success
338
  }
339
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  /// Pure Rust implementation of GfxResource__Create_Maybe (0x4F6300).
341
  ///
342
  /// Convention: usercall(ECX=gfx_dir, EAX=name) + 1 stack(output), RET 0x4.
@@ -367,7 +478,7 @@ pub unsafe fn gfx_resource_create(
367
  }
368
 
369
  // 2. Fallback: LoadImage β†’ IMG_Decode
370
- let raw_image = call_gfx_load_image(gfx_dir, name);
371
  if raw_image.is_null() {
372
  return core::ptr::null_mut();
373
  }
@@ -417,7 +528,7 @@ pub(crate) unsafe fn call_gfx_load_and_wrap(
417
  name: *const core::ffi::c_char,
418
  display_ctx: *mut crate::render::palette::PaletteContext,
419
  ) -> *mut u8 {
420
- let image = call_gfx_load_image(gfx_dir, name);
421
  if image.is_null() {
422
  return core::ptr::null_mut();
423
  }
@@ -442,106 +553,301 @@ pub struct GfxDirStream {
442
  /// 0x04: Cache slot index within the parent GfxDir.
443
  pub slot_index: u32,
444
  /// 0x08: Parent GfxDir pointer.
445
- pub gfx_dir: *mut u8,
446
  }
447
 
448
  /// Vtable for GfxDirStream (6 slots at 0x66A1C0).
449
- #[repr(C)]
 
 
 
450
  pub struct GfxDirStreamVtable {
451
  /// Slot 0 (0x5661E0): Destructor β€” releases cache slot, optionally frees.
452
- pub destructor: unsafe extern "thiscall" fn(this: *mut GfxDirStream, flags: u32),
453
- /// Slot 1 (0x566210): Returns 1 if read position < end position.
454
- pub has_data: unsafe extern "thiscall" fn(this: *mut GfxDirStream) -> u32,
455
- /// Slot 2 (0x566240): Returns remaining bytes (end - current position).
456
- pub remaining: unsafe extern "thiscall" fn(this: *mut GfxDirStream) -> u32,
457
- /// Slot 3 (0x566270): Unknown.
458
- pub _slot_3: usize,
459
- /// Slot 4 (0x5662C0): Return total byte size of this stream's data region.
460
- pub get_total_size: unsafe extern "thiscall" fn(this: *mut GfxDirStream) -> u32,
461
  /// Slot 5 (0x5662F0): Read `size` bytes into `dest`.
462
- pub read: unsafe extern "thiscall" fn(this: *mut GfxDirStream, dest: *mut u8, size: u32),
463
  }
464
 
 
 
465
  impl GfxDirStream {
466
- /// Read `size` bytes from the stream into `dest`.
467
- ///
468
- /// # Safety
469
- /// `this` must be a valid stream pointer. `dest` must have room for `size` bytes.
470
  #[inline]
471
- pub unsafe fn read_raw(this: *mut Self, dest: *mut u8, size: u32) {
472
- ((*(*this).vtable).read)(this, dest, size);
473
  }
474
 
475
- /// Returns the number of bytes remaining in the stream.
 
476
  #[inline]
477
- pub unsafe fn remaining_raw(this: *mut Self) -> u32 {
478
- ((*(*this).vtable).remaining)(this)
 
 
 
 
 
 
 
 
479
  }
 
480
 
481
- /// Returns the total byte size of this stream's data region.
482
- #[inline]
483
- pub unsafe fn total_size_raw(this: *mut Self) -> u32 {
484
- ((*(*this).vtable).get_total_size)(this)
 
 
 
 
 
 
 
 
 
 
485
  }
 
486
 
487
- /// Destroy the stream reader, releasing its cache slot.
488
- #[inline]
489
- pub unsafe fn destroy_raw(this: *mut Self) {
490
- ((*(*this).vtable).destructor)(this, 1);
 
 
 
491
  }
 
492
  }
493
 
494
- static mut GFX_LOAD_DIR_ADDR: u32 = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
 
496
- // GfxDir__LoadImage is usercall(ESI=gfx_dir) + 1 stack(name), RET 0x4.
497
- // Returns raw image pointer or null.
498
- static mut GFX_LOAD_IMAGE_ADDR: u32 = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
 
500
- /// Bridge to GfxHandler__LoadDir (0x5663E0).
501
- /// Convention: usercall(EAX=handler), plain RET. Returns nonzero on success.
 
 
 
 
 
 
 
 
502
  #[cfg(target_arch = "x86")]
503
- pub(crate) unsafe fn call_gfx_load_dir(handler: *mut u8, addr: u32) -> i32 {
504
- let result: i32;
505
- core::arch::asm!(
506
- "call {addr}",
507
- addr = in(reg) addr,
508
- inlateout("eax") handler => result,
509
- out("ecx") _,
510
- out("edx") _,
511
- clobber_abi("C"),
512
- );
513
- result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  }
515
 
 
 
 
516
  #[cfg(target_arch = "x86")]
517
- #[unsafe(naked)]
518
- pub unsafe extern "C" fn call_gfx_load_image(
519
- _gfx_dir: *mut GfxDir,
520
- _name: *const core::ffi::c_char,
521
- ) -> *mut GfxDirStream {
522
- core::arch::naked_asm!(
523
- "pushl %esi",
524
- "movl 8(%esp), %esi", // ESI = gfx_dir
525
- "pushl 12(%esp)", // push name (callee cleans via RET 0x4)
526
- "calll *({addr})",
527
- "popl %esi",
528
- "retl",
529
- addr = sym GFX_LOAD_IMAGE_ADDR,
530
- options(att_syntax),
531
- );
 
532
  }
533
 
534
- /// Initialize runtime addresses for GfxHandler bridges.
535
- /// Must be called once at DLL startup.
536
- pub fn init_addrs() {
537
- unsafe {
538
- GFX_LOAD_IMAGE_ADDR = rb(va::GFX_DIR_LOAD_IMAGE);
539
- GFX_LOAD_DIR_ADDR = rb(va::GFX_DIR_LOAD_DIR);
 
 
 
 
 
 
 
540
  }
 
541
  }
542
 
543
- /// Returns the rebased runtime address for GfxHandler__LoadDir.
544
- /// Used by callers that pass the address to `call_gfx_load_dir`.
545
- pub fn gfx_load_dir_addr() -> u32 {
546
- unsafe { GFX_LOAD_DIR_ADDR }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  }
 
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
+ ///
15
+ /// Tracks a currently-open stream's read region within the `.dir` archive file.
16
+ /// `stream_ptr` is null when the slot is free.
17
+ #[repr(C)]
18
+ pub struct GfxCacheSlot {
19
+ /// Pointer to the GfxDirStream using this slot, or null if free.
20
+ pub stream_ptr: *mut GfxDirStream,
21
+ /// Absolute file offset of the data region start.
22
+ pub start_offset: u32,
23
+ /// Absolute file offset of the data region end (start + size).
24
+ pub end_offset: u32,
25
+ /// Current read position (between start_offset and end_offset).
26
+ pub current_pos: u32,
27
+ }
28
+
29
+ const _: () = assert!(core::mem::size_of::<GfxCacheSlot>() == 0x10);
30
+
31
  /// .dir archive reader (0x19C bytes, vtable 0x66B280).
32
  ///
33
  /// Loaded from `.dir` files via `gfx_dir_load`. Contains a 1024-bucket
 
40
  pub bucket_array: *mut u8,
41
  /// 0x008: 1 if bucket_array was allocated via malloc fallback (needs free).
42
  pub fallback_alloc: u32,
43
+ /// 0x00C-0x10B: 16 cache slots tracking open stream read regions.
44
+ pub cache_slots: [GfxCacheSlot; 16],
45
+ /// 0x10C-0x14B: Free slot index stack (top at `[slot_count - 1]`).
46
  pub index_table: [u32; 16],
47
+ /// 0x14C-0x18B: In-use slot index array (count in `inuse_count`).
48
+ pub inuse_slots: [u32; 16],
49
+ /// 0x18C: Number of free cache slots (starts at 16, decremented on alloc).
50
  pub slot_count: u32,
51
+ /// 0x190: Number of in-use cache slots (starts at 0, incremented on alloc).
52
+ pub inuse_count: u32,
53
  /// 0x194: Loaded flag (1 after successful gfx_dir_load).
54
  pub loaded: u32,
55
  /// 0x198: FILE* handle to the open .dir file.
 
77
  pub struct GfxDirVtable {
78
  /// Slot 0 (0x58BBD0): Read `size` bytes into `buf`. Returns bytes read.
79
  pub read: fn(this: *mut GfxDir, buf: *mut u8, size: u32) -> u32,
80
+ /// Slot 1 (0x58BBB0): Absolute seek β€” `fseek(file, offset, SEEK_SET)`. Returns 1 on success.
81
+ pub seek: fn(this: *mut GfxDir, offset: u32) -> u32,
82
  /// Slot 2 (0x571AF0): Return cached data pointer for `entry_val`, or null if not mapped.
83
  pub load_cached: fn(this: *mut GfxDir, entry_val: u32) -> *mut u8,
84
  /// Slot 3 (0x58BB70): Release resources; if `flags & 1`, free the object.
 
88
  bind_GfxDirVtable!(GfxDir, vtable);
89
 
90
  /// Entry in a GfxDir hash bucket linked list.
91
+ /// Each entry maps a name string to a resource in the `.dir` archive.
92
  #[repr(C)]
93
  pub struct GfxDirEntry {
94
  pub next: *mut GfxDirEntry,
95
+ /// File offset of the resource data within the `.dir` archive.
96
+ /// Also used as the key for cached resource lookup (vtable[2]).
97
  pub value: u32,
98
+ /// Size of the resource data in bytes.
99
+ pub data_size: u32,
100
  // +0x0C: null-terminated name (variable-length, not in struct)
101
  }
102
 
 
239
  gfx.loaded = 0;
240
 
241
  for i in 0..16u32 {
242
+ gfx.cache_slots[i as usize].stream_ptr = core::ptr::null_mut();
 
243
  gfx.index_table[i as usize] = i;
244
  }
245
 
246
+ gfx.inuse_count = 0;
247
  gfx.slot_count = 0x10;
248
  }
249
 
 
356
  1 // success
357
  }
358
 
359
+ // ─── GfxDir vtable method implementations ───────────────────────────────────
360
+
361
+ /// Pure Rust implementation of GfxHandler__Read (vtable slot 0, 0x58BBD0).
362
+ ///
363
+ /// `fread(buf, 1, size, self->file_handle)`. Returns `size` on success, 0 on error.
364
+ #[cfg(target_arch = "x86")]
365
+ pub unsafe extern "thiscall" fn gfx_dir_read(this: *mut GfxDir, buf: *mut u8, size: u32) -> u32 {
366
+ let fread: unsafe extern "cdecl" fn(*mut u8, u32, u32, *mut u8) -> u32 =
367
+ core::mem::transmute(rb(va::WA_FREAD) as usize);
368
+ let bytes_read = fread(buf, 1, size, (*this).file_handle);
369
+ if bytes_read != size {
370
+ 0
371
+ } else {
372
+ size
373
+ }
374
+ }
375
+
376
+ /// Pure Rust implementation of GfxHandler__Seek (vtable slot 1, 0x58BBB0).
377
+ ///
378
+ /// `fseek(self->file_handle, offset, SEEK_SET)`. Returns 1 on success, 0 on failure.
379
+ #[cfg(target_arch = "x86")]
380
+ pub unsafe extern "thiscall" fn gfx_dir_seek(this: *mut GfxDir, offset: u32) -> u32 {
381
+ let fseek: unsafe extern "cdecl" fn(*mut u8, i32, i32) -> i32 =
382
+ core::mem::transmute(rb(va::WA_FSEEK) as usize);
383
+ let result = fseek((*this).file_handle, offset as i32, 0); // SEEK_SET = 0
384
+ if result == 0 {
385
+ 1
386
+ } else {
387
+ 0
388
+ }
389
+ }
390
+
391
+ /// Pure Rust implementation of GfxHandler__LoadCached (vtable slot 2, 0x571AF0).
392
+ ///
393
+ /// Base implementation is a no-op β€” always returns null.
394
+ pub unsafe extern "thiscall" fn gfx_dir_load_cached(
395
+ _this: *mut GfxDir,
396
+ _entry_val: u32,
397
+ ) -> *mut u8 {
398
+ core::ptr::null_mut()
399
+ }
400
+
401
+ /// Pure Rust implementation of FUN_566330 (GfxDir cleanup).
402
+ ///
403
+ /// Resets vtable to the "cleaned up" vtable (0x66A1B0), invalidates all
404
+ /// active cache slots, and frees the bucket array if it was malloc'd.
405
+ #[cfg(target_arch = "x86")]
406
+ unsafe fn gfx_dir_cleanup(gfx: *mut GfxDir) {
407
+ // Set vtable to the "cleaned up" vtable (0x66A1B0)
408
+ (*gfx).vtable = rb(0x0066_A1B0) as *const GfxDirVtable;
409
+
410
+ if (*gfx).loaded != 0 {
411
+ // Walk in-use slots, invalidate cache entries
412
+ let inuse_count = (*gfx).inuse_count as usize;
413
+ for i in 0..inuse_count {
414
+ let slot_idx = (*gfx).inuse_slots[i] as usize;
415
+ let slot = &mut (*gfx).cache_slots[slot_idx];
416
+ // Clear the stream's backpointer (offset +0x08 in GfxDirStream)
417
+ if !slot.stream_ptr.is_null() {
418
+ (*slot.stream_ptr).gfx_dir = core::ptr::null_mut();
419
+ }
420
+ // Mark slot index as invalid (-1)
421
+ (*slot.stream_ptr).slot_index = 0xFFFF_FFFF;
422
+ }
423
+
424
+ if (*gfx).fallback_alloc != 0 {
425
+ crate::wa_alloc::wa_free((*gfx).bucket_array);
426
+ }
427
+ }
428
+ }
429
+
430
+ /// Pure Rust implementation of GfxHandler__Release (vtable slot 3, 0x58BB70).
431
+ ///
432
+ /// Resets vtable, closes the file handle, runs cleanup, and optionally frees self.
433
+ #[cfg(target_arch = "x86")]
434
+ pub unsafe extern "thiscall" fn gfx_dir_release(this: *mut GfxDir, flags: u32) {
435
+ // Reset vtable to base GfxHandler vtable
436
+ (*this).vtable = rb(va::GFX_DIR_VTABLE) as *const GfxDirVtable;
437
+
438
+ if !(*this).file_handle.is_null() {
439
+ let fclose: unsafe extern "cdecl" fn(*mut u8) -> i32 =
440
+ core::mem::transmute(rb(va::WA_FCLOSE) as usize);
441
+ fclose((*this).file_handle);
442
+ }
443
+
444
+ gfx_dir_cleanup(this);
445
+
446
+ if (flags & 1) != 0 {
447
+ crate::wa_alloc::wa_free(this as *mut u8);
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.
 
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
  }
 
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
  }
 
553
  /// 0x04: Cache slot index within the parent GfxDir.
554
  pub slot_index: u32,
555
  /// 0x08: Parent GfxDir pointer.
556
+ pub gfx_dir: *mut GfxDir,
557
  }
558
 
559
  /// Vtable for GfxDirStream (6 slots at 0x66A1C0).
560
+ ///
561
+ /// Provides a sequential read interface over cached resource data within
562
+ /// a `.dir` archive. The stream reads from a cache slot in the parent GfxDir.
563
+ #[vtable(size = 6, va = 0x0066_A1C0, class = "GfxDirStream")]
564
  pub struct GfxDirStreamVtable {
565
  /// Slot 0 (0x5661E0): Destructor β€” releases cache slot, optionally frees.
566
+ pub destructor: fn(this: *mut GfxDirStream, flags: u32),
567
+ /// Slot 1 (0x566210): Returns 1 if current read position < end position.
568
+ pub has_data: fn(this: *mut GfxDirStream) -> u32,
569
+ /// Slot 2 (0x566240): Returns bytes consumed (current_pos - start_offset).
570
+ pub bytes_consumed: fn(this: *mut GfxDirStream) -> u32,
571
+ /// Slot 3 (0x566270): Seek β€” repositions read cursor within [start, end].
572
+ pub seek: fn(this: *mut GfxDirStream, offset: u32) -> u32,
573
+ /// Slot 4 (0x5662C0): Returns total byte size (end_offset - start_offset).
574
+ pub get_total_size: fn(this: *mut GfxDirStream) -> u32,
575
  /// Slot 5 (0x5662F0): Read `size` bytes into `dest`.
576
+ pub read: fn(this: *mut GfxDirStream, dest: *mut u8, size: u32),
577
  }
578
 
579
+ bind_GfxDirStreamVtable!(GfxDirStream, vtable);
580
+
581
  impl GfxDirStream {
582
+ /// Destroy the stream reader, releasing its cache slot.
 
 
 
583
  #[inline]
584
+ pub unsafe fn destroy_raw(this: *mut Self) {
585
+ Self::destructor_raw(this, 1);
586
  }
587
 
588
+ /// Returns a pointer to this stream's cache slot in the parent GfxDir.
589
+ /// Returns null if the stream is invalid (null gfx_dir or slot_index >= 16).
590
  #[inline]
591
+ unsafe fn cache_slot(this: *mut Self) -> *mut GfxCacheSlot {
592
+ let gfx = (*this).gfx_dir;
593
+ if gfx.is_null() {
594
+ return core::ptr::null_mut();
595
+ }
596
+ let idx = (*this).slot_index;
597
+ if idx >= 16 {
598
+ return core::ptr::null_mut();
599
+ }
600
+ &raw mut (*gfx).cache_slots[idx as usize]
601
  }
602
+ }
603
 
604
+ // ─── GfxDirStream vtable method implementations ─────────────────────────────
605
+
606
+ /// Pure Rust implementation of GfxDirStream has_data (vtable slot 1, 0x566210).
607
+ ///
608
+ /// Returns 1 if `current_pos < end_offset`, 0 otherwise.
609
+ pub unsafe extern "thiscall" fn gfx_dir_stream_has_data(this: *mut GfxDirStream) -> u32 {
610
+ let slot = GfxDirStream::cache_slot(this);
611
+ if slot.is_null() || (*slot).stream_ptr.is_null() {
612
+ return 0;
613
+ }
614
+ if (*slot).current_pos < (*slot).end_offset {
615
+ 1
616
+ } else {
617
+ 0
618
  }
619
+ }
620
 
621
+ /// Pure Rust implementation of GfxDirStream bytes_consumed (vtable slot 2, 0x566240).
622
+ ///
623
+ /// Returns `current_pos - start_offset` (bytes consumed from data start).
624
+ pub unsafe extern "thiscall" fn gfx_dir_stream_bytes_consumed(this: *mut GfxDirStream) -> u32 {
625
+ let slot = GfxDirStream::cache_slot(this);
626
+ if slot.is_null() || (*slot).stream_ptr.is_null() {
627
+ return 0;
628
  }
629
+ (*slot).current_pos.wrapping_sub((*slot).start_offset)
630
  }
631
 
632
+ /// Pure Rust implementation of GfxDirStream seek (vtable slot 3, 0x566270).
633
+ ///
634
+ /// Repositions the read cursor to `start_offset + offset`, clamped to [start, end].
635
+ /// Returns 1 on success, 0 on failure.
636
+ pub unsafe extern "thiscall" fn gfx_dir_stream_seek(this: *mut GfxDirStream, offset: u32) -> u32 {
637
+ let slot = GfxDirStream::cache_slot(this);
638
+ if slot.is_null() || (*slot).stream_ptr.is_null() {
639
+ return 0;
640
+ }
641
+ let start = (*slot).start_offset;
642
+ let end = (*slot).end_offset;
643
+ let mut new_pos = start.wrapping_add(offset);
644
+ // Clamp: if wrapping underflow (new_pos < start), use start
645
+ if new_pos < start {
646
+ new_pos = start;
647
+ }
648
+ if new_pos > end {
649
+ new_pos = end;
650
+ }
651
+ (*slot).current_pos = new_pos;
652
+ 1
653
+ }
654
 
655
+ /// Pure Rust implementation of GfxDirStream get_total_size (vtable slot 4, 0x5662C0).
656
+ ///
657
+ /// Returns `end_offset - start_offset` (total data size), or -1 if gfx_dir is null.
658
+ pub unsafe extern "thiscall" fn gfx_dir_stream_get_total_size(this: *mut GfxDirStream) -> u32 {
659
+ if (*this).gfx_dir.is_null() {
660
+ return 0xFFFF_FFFF; // -1 as u32, matching original
661
+ }
662
+ let slot = GfxDirStream::cache_slot(this);
663
+ if slot.is_null() || (*slot).stream_ptr.is_null() {
664
+ return 0;
665
+ }
666
+ (*slot).end_offset.wrapping_sub((*slot).start_offset)
667
+ }
668
+
669
+ /// Pure Rust implementation of cache slot release (FUN_566640).
670
+ ///
671
+ /// Finds the stream's slot in the parent GfxDir's in-use list, removes it,
672
+ /// and returns the slot index to the free list. Clears the slot's stream_ptr.
673
+ ///
674
+ /// Returns 1 on success, 0 if the slot was not found.
675
+ pub unsafe fn gfx_dir_release_slot(gfx: *mut GfxDir, stream: *mut GfxDirStream) -> u32 {
676
+ if stream.is_null() {
677
+ return 0;
678
+ }
679
+ let slot_idx = (*stream).slot_index;
680
+ if slot_idx >= 16 || (*gfx).cache_slots[slot_idx as usize].stream_ptr.is_null() {
681
+ return 0;
682
+ }
683
+
684
+ // Find slot_idx in the in-use list
685
+ let inuse_count = (*gfx).inuse_count as usize;
686
+ let mut found = false;
687
+ let mut found_pos = 0usize;
688
+ for i in 0..inuse_count {
689
+ if (*gfx).inuse_slots[i] == slot_idx {
690
+ found = true;
691
+ found_pos = i;
692
+ break;
693
+ }
694
+ }
695
+ if !found {
696
+ return 0;
697
+ }
698
+
699
+ // Remove from in-use list: swap with last, decrement count
700
+ let new_inuse_count = inuse_count - 1;
701
+ (*gfx).inuse_count = new_inuse_count as u32;
702
+ (*gfx).inuse_slots[found_pos] = (*gfx).inuse_slots[new_inuse_count];
703
+
704
+ // Push slot index back to free list
705
+ let free_idx = (*gfx).slot_count as usize;
706
+ (*gfx).index_table[free_idx] = slot_idx;
707
+ (*gfx).slot_count += 1;
708
 
709
+ // Clear the cache slot
710
+ (*gfx).cache_slots[slot_idx as usize].stream_ptr = core::ptr::null_mut();
711
+
712
+ 1
713
+ }
714
+
715
+ /// Pure Rust implementation of GfxDirStream read helper (FUN_566760).
716
+ ///
717
+ /// Reads data from the parent GfxDir's file through its vtable, advancing
718
+ /// the cache slot's current read position.
719
  #[cfg(target_arch = "x86")]
720
+ unsafe fn gfx_dir_stream_read_inner(
721
+ gfx: *mut GfxDir,
722
+ slot_idx: u32,
723
+ dest: *mut u8,
724
+ size: u32,
725
+ ) -> u32 {
726
+ if slot_idx >= 16 || (*gfx).cache_slots[slot_idx as usize].stream_ptr.is_null() {
727
+ return 0;
728
+ }
729
+
730
+ let slot = &mut (*gfx).cache_slots[slot_idx as usize];
731
+ let current = slot.current_pos;
732
+ let end = slot.end_offset;
733
+
734
+ // Clamp read size to remaining bytes
735
+ let mut actual_end = current.wrapping_add(size);
736
+ if actual_end > end {
737
+ actual_end = end;
738
+ }
739
+ let actual_size = actual_end.wrapping_sub(current);
740
+ if actual_size == 0 {
741
+ return 0;
742
+ }
743
+
744
+ // Seek parent GfxDir to current position (vtable slot 1 = absolute seek)
745
+ let seek_ok = GfxDir::seek_raw(gfx, current);
746
+ if seek_ok == 0 {
747
+ return 0;
748
+ }
749
+
750
+ // Read through parent GfxDir (vtable slot 0)
751
+ let bytes_read = GfxDir::read_raw(gfx, dest, actual_size);
752
+ if bytes_read != actual_size {
753
+ return 0;
754
+ }
755
+
756
+ // Advance read position
757
+ slot.current_pos = current.wrapping_add(actual_size);
758
+
759
+ actual_size
760
  }
761
 
762
+ /// Pure Rust implementation of GfxDirStream destructor (vtable slot 0, 0x5661E0).
763
+ ///
764
+ /// Releases the cache slot and optionally frees the stream object.
765
  #[cfg(target_arch = "x86")]
766
+ pub unsafe extern "thiscall" fn gfx_dir_stream_destructor(this: *mut GfxDirStream, flags: u32) {
767
+ // Set vtable to GfxDirStream vtable (matches original behavior)
768
+ (*this).vtable = rb(va::GFX_DIR_STREAM_VTABLE) as *const GfxDirStreamVtable;
769
+
770
+ // Release the cache slot in the parent GfxDir
771
+ let gfx = (*this).gfx_dir;
772
+ if !gfx.is_null() {
773
+ gfx_dir_release_slot(gfx, this);
774
+ }
775
+
776
+ // Reset vtable to the "released" vtable (0x66A198)
777
+ (*this).vtable = rb(0x0066_A198) as *const GfxDirStreamVtable;
778
+
779
+ if (flags & 1) != 0 {
780
+ crate::wa_alloc::wa_free(this as *mut u8);
781
+ }
782
  }
783
 
784
+ /// Pure Rust implementation of GfxDirStream read (vtable slot 5, 0x5662F0).
785
+ ///
786
+ /// Reads `size` bytes from the stream into `dest` by delegating to the
787
+ /// parent GfxDir's file I/O vtable methods.
788
+ #[cfg(target_arch = "x86")]
789
+ pub unsafe extern "thiscall" fn gfx_dir_stream_read(
790
+ this: *mut GfxDirStream,
791
+ dest: *mut u8,
792
+ size: u32,
793
+ ) {
794
+ let gfx = (*this).gfx_dir;
795
+ if gfx.is_null() {
796
+ return;
797
  }
798
+ gfx_dir_stream_read_inner(gfx, (*this).slot_index, dest, size);
799
  }
800
 
801
+ /// Pure Rust implementation of GfxDir__LoadImage (0x5666D0).
802
+ ///
803
+ /// Convention: usercall(ESI=gfx_dir) + 1 stack(name), RET 0x4.
804
+ ///
805
+ /// Looks up `name` in the GfxDir's hash table, allocates a cache slot,
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();
816
+ }
817
+
818
+ // Look up the entry
819
+ let entry = gfx_dir_find_entry(name, gfx_dir);
820
+ if entry.is_null() {
821
+ return core::ptr::null_mut();
822
+ }
823
+ let entry = entry as *const GfxDirEntry;
824
+
825
+ // Pop a free slot from index_table
826
+ (*gfx_dir).slot_count -= 1;
827
+ let slot_idx = (*gfx_dir).index_table[(*gfx_dir).slot_count as usize];
828
+
829
+ // Push to in-use list
830
+ let inuse_pos = (*gfx_dir).inuse_count as usize;
831
+ (*gfx_dir).inuse_slots[inuse_pos] = slot_idx;
832
+ (*gfx_dir).inuse_count += 1;
833
+
834
+ // Allocate the 0xC-byte GfxDirStream object
835
+ let stream = wa_malloc(0xC) as *mut GfxDirStream;
836
+ if stream.is_null() {
837
+ return core::ptr::null_mut();
838
+ }
839
+
840
+ // Initialize stream
841
+ (*stream).vtable = rb(va::GFX_DIR_STREAM_VTABLE) as *const GfxDirStreamVtable;
842
+ (*stream).gfx_dir = gfx_dir;
843
+ (*stream).slot_index = slot_idx;
844
+
845
+ // Set up cache slot
846
+ let slot = &mut (*gfx_dir).cache_slots[slot_idx as usize];
847
+ slot.stream_ptr = stream;
848
+ slot.start_offset = (*entry).value;
849
+ slot.end_offset = (*entry).value.wrapping_add((*entry).data_size);
850
+ slot.current_pos = (*entry).value;
851
+
852
+ stream
853
  }
crates/openwa-core/src/asset/mod.rs ADDED
@@ -0,0 +1 @@
 
 
1
+ pub mod gfx_dir;
crates/openwa-core/src/engine/ddgame_constructor.rs CHANGED
@@ -9,6 +9,10 @@
9
 
10
  use super::ddgame::DDGame;
11
  use crate::address::va;
 
 
 
 
12
  use crate::audio::active_sound::ActiveSoundTable;
13
  use crate::audio::dssound::DSSound;
14
  use crate::audio::music::Music;
@@ -24,10 +28,6 @@ use crate::render::display::gfx::DisplayGfx;
24
  use crate::render::display::gradient::compute_complex_gradient;
25
  use crate::render::display::palette::Palette;
26
  use crate::render::landscape::PCLandscape;
27
- use crate::render::sprite::gfx_dir::{
28
- call_gfx_find_and_load, call_gfx_load_and_wrap, call_gfx_load_dir, GfxDir, GfxDirVtable,
29
- };
30
- pub use crate::render::sprite::gfx_dir::{gfx_dir_find_entry, gfx_resource_create};
31
  use crate::wa_alloc::{wa_malloc, wa_malloc_zeroed};
32
 
33
  // ============================================================
@@ -156,7 +156,6 @@ pub fn init_constructor_addrs() {
156
  LOADING_PROGRESS_TICK_ADDR = rb(va::DDGAME_WRAPPER_LOADING_PROGRESS_TICK);
157
  GFX_LOAD_SPRITES_ADDR = rb(va::GFX_DIR_LOAD_SPRITES);
158
  }
159
- crate::render::sprite::gfx_dir::init_addrs();
160
  }
161
 
162
  // ── Typed wrappers for WA stdcall functions ────────────────────────────────
@@ -475,12 +474,7 @@ unsafe fn init_graphics_and_resources(
475
  for (i, path) in paths.iter().enumerate() {
476
  let fp = fopen(path.as_ptr(), c"rb".as_ptr());
477
  (*gfx1).file_handle = fp;
478
- if !fp.is_null()
479
- && call_gfx_load_dir(
480
- gfx1 as *mut u8,
481
- crate::render::sprite::gfx_dir::gfx_load_dir_addr(),
482
- ) != 0
483
- {
484
  gfx_loaded_idx = i as u32;
485
  break;
486
  }
@@ -500,8 +494,6 @@ unsafe fn init_graphics_and_resources(
500
  let game_version = (*game_info).game_version;
501
  let threshold = if (*wrapper).gfx_mode != 0 { 33 } else { -2i32 };
502
  if game_version < threshold {
503
- use crate::render::sprite::gfx_dir::gfx_load_dir_addr;
504
-
505
  let c_digit = if game_version > -3 { b'2' } else { b'1' };
506
  let mut gfx_c_path = *b"data\\Gfx\\GfxC_3_0.dir\0";
507
  gfx_c_path[14] = c_digit;
@@ -511,11 +503,10 @@ unsafe fn init_graphics_and_resources(
511
 
512
  let fp = fopen(gfx_c_path.as_ptr().cast(), c"rb".as_ptr());
513
  (*gfx2).file_handle = fp;
514
- let load_addr = gfx_load_dir_addr();
515
- if fp.is_null() || call_gfx_load_dir(gfx2 as *mut u8, load_addr) == 0 {
516
  let fp2 = fopen(c"data\\Gfx\\Gfx.dir".as_ptr(), c"rb".as_ptr());
517
  (*gfx2).file_handle = fp2;
518
- if fp2.is_null() || call_gfx_load_dir(gfx2 as *mut u8, load_addr) == 0 {
519
  panic!("DDGame: couldn't open secondary Gfx.dir");
520
  }
521
  }
 
9
 
10
  use super::ddgame::DDGame;
11
  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;
 
28
  use crate::render::display::gradient::compute_complex_gradient;
29
  use crate::render::display::palette::Palette;
30
  use crate::render::landscape::PCLandscape;
 
 
 
 
31
  use crate::wa_alloc::{wa_malloc, wa_malloc_zeroed};
32
 
33
  // ============================================================
 
156
  LOADING_PROGRESS_TICK_ADDR = rb(va::DDGAME_WRAPPER_LOADING_PROGRESS_TICK);
157
  GFX_LOAD_SPRITES_ADDR = rb(va::GFX_DIR_LOAD_SPRITES);
158
  }
 
159
  }
160
 
161
  // ── Typed wrappers for WA stdcall functions ────────────────────────────────
 
474
  for (i, path) in paths.iter().enumerate() {
475
  let fp = fopen(path.as_ptr(), c"rb".as_ptr());
476
  (*gfx1).file_handle = fp;
477
+ if !fp.is_null() && gfx_dir_load_dir(gfx1 as *mut u8) != 0 {
 
 
 
 
 
478
  gfx_loaded_idx = i as u32;
479
  break;
480
  }
 
494
  let game_version = (*game_info).game_version;
495
  let threshold = if (*wrapper).gfx_mode != 0 { 33 } else { -2i32 };
496
  if game_version < threshold {
 
 
497
  let c_digit = if game_version > -3 { b'2' } else { b'1' };
498
  let mut gfx_c_path = *b"data\\Gfx\\GfxC_3_0.dir\0";
499
  gfx_c_path[14] = c_digit;
 
503
 
504
  let fp = fopen(gfx_c_path.as_ptr().cast(), c"rb".as_ptr());
505
  (*gfx2).file_handle = fp;
506
+ if fp.is_null() || gfx_dir_load_dir(gfx2 as *mut u8) == 0 {
 
507
  let fp2 = fopen(c"data\\Gfx\\Gfx.dir".as_ptr(), c"rb".as_ptr());
508
  (*gfx2).file_handle = fp2;
509
+ if fp2.is_null() || gfx_dir_load_dir(gfx2 as *mut u8) == 0 {
510
  panic!("DDGame: couldn't open secondary Gfx.dir");
511
  }
512
  }
crates/openwa-core/src/engine/ddgame_wrapper.rs CHANGED
@@ -1,10 +1,10 @@
 
1
  use crate::audio::dssound::DSSound;
2
  use crate::engine::ddgame::DDGame;
3
  use crate::engine::net_bridge::NetBridge;
4
  use crate::render::display::gfx::DisplayGfx;
5
  use crate::render::landscape::PCLandscape;
6
  use crate::render::palette::PaletteContext;
7
- use crate::render::sprite::gfx_dir::GfxDir;
8
  use crate::FieldRegistry;
9
 
10
  /// Speech name table entry size (0x40 = 64 bytes, null-terminated C string).
 
1
+ use crate::asset::gfx_dir::GfxDir;
2
  use crate::audio::dssound::DSSound;
3
  use crate::engine::ddgame::DDGame;
4
  use crate::engine::net_bridge::NetBridge;
5
  use crate::render::display::gfx::DisplayGfx;
6
  use crate::render::landscape::PCLandscape;
7
  use crate::render::palette::PaletteContext;
 
8
  use crate::FieldRegistry;
9
 
10
  /// Speech name table entry size (0x40 = 64 bytes, null-terminated C string).
crates/openwa-core/src/lib.rs CHANGED
@@ -28,6 +28,7 @@ pub use openwa_derive::FieldRegistry;
28
  #[macro_use]
29
  pub mod macros;
30
 
 
31
  pub mod audio;
32
  pub mod bitgrid;
33
  pub mod engine;
 
28
  #[macro_use]
29
  pub mod macros;
30
 
31
+ pub mod asset;
32
  pub mod audio;
33
  pub mod bitgrid;
34
  pub mod engine;
crates/openwa-core/src/render/display/font.rs CHANGED
@@ -4,7 +4,8 @@
4
 
5
  use std::ffi::c_char;
6
 
7
- use crate::render::{palette::PaletteContext, sprite::gfx_dir::GfxDir};
 
8
 
9
  /// Font object β€” 0x1C bytes.
10
  ///
@@ -222,8 +223,8 @@ pub unsafe fn font_load_from_gfx(
222
  palette_ctx: *mut PaletteContext,
223
  name: *const c_char,
224
  ) -> u32 {
225
- use crate::render::sprite::gfx_dir::{
226
- call_gfx_load_image, gfx_dir_find_entry, GfxDirEntry, GfxDirStream,
227
  };
228
  use crate::wa_alloc::wa_malloc;
229
 
@@ -238,12 +239,12 @@ pub unsafe fn font_load_from_gfx(
238
  }
239
 
240
  // 2. Fallback: LoadImage β†’ get_total_size β†’ wa_malloc β†’ read β†’ destroy
241
- let image = call_gfx_load_image(gfx_dir, name);
242
  if image.is_null() {
243
  return 0;
244
  }
245
 
246
- let size = GfxDirStream::total_size_raw(image);
247
 
248
  // Match the original's allocation: round size up to 4-byte multiple, add 0x20 guard.
249
  let alloc_size = ((size + 3) & !3u32).wrapping_add(0x20);
 
4
 
5
  use std::ffi::c_char;
6
 
7
+ use crate::asset::gfx_dir::GfxDir;
8
+ use crate::render::palette::PaletteContext;
9
 
10
  /// Font object β€” 0x1C bytes.
11
  ///
 
223
  palette_ctx: *mut PaletteContext,
224
  name: *const c_char,
225
  ) -> u32 {
226
+ use crate::asset::gfx_dir::{
227
+ gfx_dir_find_entry, gfx_dir_load_image, GfxDirEntry, GfxDirStream,
228
  };
229
  use crate::wa_alloc::wa_malloc;
230
 
 
239
  }
240
 
241
  // 2. Fallback: LoadImage β†’ get_total_size β†’ wa_malloc β†’ read β†’ destroy
242
+ let image = gfx_dir_load_image(gfx_dir, name);
243
  if image.is_null() {
244
  return 0;
245
  }
246
 
247
+ let size = GfxDirStream::get_total_size_raw(image);
248
 
249
  // Match the original's allocation: round size up to 4-byte multiple, add 0x20 guard.
250
  let alloc_size = ((size + 3) & !3u32).wrapping_add(0x20);
crates/openwa-core/src/render/display/gradient.rs CHANGED
@@ -6,12 +6,12 @@
6
  //! may have subtle color differences due to the tangled decompiler output.
7
 
8
  use crate::address::va;
 
 
 
9
  use crate::bitgrid::{BitGrid, BitGridBaseVtable};
10
  use crate::engine::ddgame::DDGame;
11
  use crate::rebase::rb;
12
- use crate::render::sprite::gfx_dir::call_gfx_find_and_load;
13
- #[cfg(target_arch = "x86")]
14
- use crate::render::sprite::gfx_dir::GfxDir;
15
  #[cfg(target_arch = "x86")]
16
  use crate::wa_alloc::wa_malloc_struct_zeroed;
17
  use crate::wa_alloc::{wa_free, wa_malloc};
 
6
  //! may have subtle color differences due to the tangled decompiler output.
7
 
8
  use crate::address::va;
9
+ use crate::asset::gfx_dir::call_gfx_find_and_load;
10
+ #[cfg(target_arch = "x86")]
11
+ use crate::asset::gfx_dir::GfxDir;
12
  use crate::bitgrid::{BitGrid, BitGridBaseVtable};
13
  use crate::engine::ddgame::DDGame;
14
  use crate::rebase::rb;
 
 
 
15
  #[cfg(target_arch = "x86")]
16
  use crate::wa_alloc::wa_malloc_struct_zeroed;
17
  use crate::wa_alloc::{wa_free, wa_malloc};
crates/openwa-core/src/render/display/vtable.rs CHANGED
@@ -2,6 +2,7 @@ use core::ffi::c_char;
2
 
3
  use openwa_core::vtable;
4
 
 
5
  use crate::fixed::Fixed;
6
  use crate::render::display::font::{
7
  font_extend, font_get_info_impl, font_get_metric_impl, font_load_from_gfx,
@@ -9,7 +10,6 @@ use crate::render::display::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};
14
  use crate::render::sprite::sprite_op::SpriteOp;
15
  use crate::render::SpriteCache;
@@ -1679,10 +1679,10 @@ pub unsafe fn load_sprite_by_name(
1679
  name: *const c_char,
1680
  ) -> i32 {
1681
  use crate::address::va;
 
1682
  use crate::rebase::rb;
1683
  use crate::render::display::context::{FastcallResult, RenderContext, Surface};
1684
  use crate::render::palette::{palette_map_color, remap_pixels_through_lut};
1685
- use crate::render::sprite::gfx_dir::{call_gfx_load_image, GfxDirStream};
1686
 
1687
  use crate::wa_alloc::wa_malloc;
1688
 
@@ -1702,7 +1702,7 @@ pub unsafe fn load_sprite_by_name(
1702
  (*sprite).gfx_dir = gfx_dir;
1703
  (*sprite).palette_ctx = palette_ctx;
1704
 
1705
- let stream = call_gfx_load_image(gfx_dir, name);
1706
  if stream.is_null() {
1707
  return 0;
1708
  }
@@ -1710,7 +1710,7 @@ pub unsafe fn load_sprite_by_name(
1710
  let display_mode_flag = *(rb(va::G_DISPLAY_MODE_FLAG) as *const u8);
1711
  if display_mode_flag == 0 {
1712
  // The original calls remaining() and discards the result.
1713
- GfxDirStream::remaining_raw(stream);
1714
 
1715
  // Read the .spr header as 4+4+2+2 separate calls β€” matching the
1716
  // original's read granularity may matter for internal stream state.
@@ -1785,7 +1785,7 @@ pub unsafe fn load_sprite_by_name(
1785
 
1786
  // Skip alignment padding: while (remaining() & 3) != 0, read 1 byte.
1787
  loop {
1788
- let remaining = GfxDirStream::remaining_raw(stream);
1789
  if remaining & 3 == 0 {
1790
  break;
1791
  }
 
2
 
3
  use openwa_core::vtable;
4
 
5
+ use crate::asset::gfx_dir::GfxDir;
6
  use crate::fixed::Fixed;
7
  use crate::render::display::font::{
8
  font_extend, font_get_info_impl, font_get_metric_impl, font_load_from_gfx,
 
10
  };
11
  use crate::render::display::layer::Layer;
12
  use crate::render::display::line_draw::Vertex;
 
13
  use crate::render::sprite::sprite::{LayerSprite, LayerSpriteFrame};
14
  use crate::render::sprite::sprite_op::SpriteOp;
15
  use crate::render::SpriteCache;
 
1679
  name: *const c_char,
1680
  ) -> i32 {
1681
  use crate::address::va;
1682
+ use crate::asset::gfx_dir::{gfx_dir_load_image, GfxDirStream};
1683
  use crate::rebase::rb;
1684
  use crate::render::display::context::{FastcallResult, RenderContext, Surface};
1685
  use crate::render::palette::{palette_map_color, remap_pixels_through_lut};
 
1686
 
1687
  use crate::wa_alloc::wa_malloc;
1688
 
 
1702
  (*sprite).gfx_dir = gfx_dir;
1703
  (*sprite).palette_ctx = palette_ctx;
1704
 
1705
+ let stream = gfx_dir_load_image(gfx_dir, name);
1706
  if stream.is_null() {
1707
  return 0;
1708
  }
 
1710
  let display_mode_flag = *(rb(va::G_DISPLAY_MODE_FLAG) as *const u8);
1711
  if display_mode_flag == 0 {
1712
  // The original calls remaining() and discards the result.
1713
+ GfxDirStream::bytes_consumed_raw(stream);
1714
 
1715
  // Read the .spr header as 4+4+2+2 separate calls β€” matching the
1716
  // original's read granularity may matter for internal stream state.
 
1785
 
1786
  // Skip alignment padding: while (remaining() & 3) != 0, read 1 byte.
1787
  loop {
1788
+ let remaining = GfxDirStream::bytes_consumed_raw(stream);
1789
  if remaining & 3 == 0 {
1790
  break;
1791
  }
crates/openwa-core/src/render/landscape.rs CHANGED
@@ -1,4 +1,4 @@
1
- use crate::{engine::ddgame::DDGame, render::sprite::gfx_dir::GfxDir};
2
 
3
  /// PCLandscape β€” terrain/landscape subsystem (0xB40 bytes).
4
  ///
 
1
+ use crate::{asset::gfx_dir::GfxDir, engine::ddgame::DDGame};
2
 
3
  /// PCLandscape β€” terrain/landscape subsystem (0xB40 bytes).
4
  ///
crates/openwa-core/src/render/sprite/mod.rs CHANGED
@@ -1,5 +1,4 @@
1
  pub mod frame_cache;
2
- pub mod gfx_dir;
3
  pub mod lzss;
4
  pub mod spr;
5
  pub mod sprite;
 
1
  pub mod frame_cache;
 
2
  pub mod lzss;
3
  pub mod spr;
4
  pub mod sprite;
crates/openwa-core/src/render/sprite/sprite.rs CHANGED
@@ -1,6 +1,5 @@
1
- use crate::render::{
2
- display::gfx::DisplayGfx, palette::PaletteContext, sprite::gfx_dir::GfxDir, SpriteCache,
3
- };
4
 
5
  // Re-export sprite types from their own modules to keep this file focused on struct layouts.
6
  pub use super::sprite_id::KnownSpriteId;
 
1
+ use crate::asset::gfx_dir::GfxDir;
2
+ use crate::render::{display::gfx::DisplayGfx, palette::PaletteContext, SpriteCache};
 
3
 
4
  // Re-export sprite types from their own modules to keep this file focused on struct layouts.
5
  pub use super::sprite_id::KnownSpriteId;
crates/openwa-dll/src/replacements/ddgame_init.rs CHANGED
@@ -14,6 +14,9 @@
14
  use crate::hook;
15
  use core::ffi::c_char;
16
  use openwa_core::address::va;
 
 
 
17
  use openwa_core::bitgrid::BitGrid;
18
  use openwa_core::engine::game_state_init::{
19
  check_weapon_avail, init_alliance_data, init_landscape_flags, init_team_scoring,
@@ -22,9 +25,6 @@ use openwa_core::engine::game_state_init::{
22
  use openwa_core::engine::{
23
  ddgame_init_fields, ddgame_init_render_indices, display_layer_color_init, DDGame, DDGameWrapper,
24
  };
25
- use openwa_core::render::sprite::gfx_dir::{
26
- gfx_dir_find_entry, gfx_dir_load_dir, gfx_resource_create, GfxDir,
27
- };
28
 
29
  // ─── DDGame__InitFields (0x526120) ──────────────────────────────────────────
30
 
 
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::{
22
  check_weapon_avail, init_alliance_data, init_landscape_flags, init_team_scoring,
 
25
  use openwa_core::engine::{
26
  ddgame_init_fields, ddgame_init_render_indices, display_layer_color_init, DDGame, DDGameWrapper,
27
  };
 
 
 
28
 
29
  // ─── DDGame__InitFields (0x526120) ──────────────────────────────────────────
30
 
crates/openwa-dll/src/replacements/gfx_dir.rs ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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;
8
+ use openwa_core::asset::gfx_dir::{
9
+ gfx_dir_load_cached, gfx_dir_load_image, gfx_dir_read, gfx_dir_release, gfx_dir_seek,
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.
17
+
18
+ extern "cdecl" fn impl_load_image(gfx_dir: *mut GfxDir, name: *const c_char) -> *mut GfxDirStream {
19
+ unsafe { gfx_dir_load_image(gfx_dir, name) }
20
+ }
21
+
22
+ #[unsafe(naked)]
23
+ unsafe extern "C" fn load_image_trampoline() {
24
+ core::arch::naked_asm!(
25
+ "push edx",
26
+ "push [esp+8]", // name (stack param, shifted by push edx)
27
+ "push esi", // gfx_dir (ESI)
28
+ "call {impl_fn}",
29
+ "add esp, 8",
30
+ "pop edx",
31
+ "ret 0x4", // callee cleans 1 stack param
32
+ impl_fn = sym impl_load_image,
33
+ );
34
+ }
35
+
36
+ pub fn install() -> Result<(), String> {
37
+ use openwa_core::vtable_replace;
38
+
39
+ vtable_replace!(openwa_core::asset::gfx_dir::GfxDirVtable, va::GFX_DIR_VTABLE, {
40
+ read => gfx_dir_read,
41
+ seek => gfx_dir_seek,
42
+ load_cached => gfx_dir_load_cached,
43
+ release => gfx_dir_release,
44
+ })?;
45
+
46
+ vtable_replace!(openwa_core::asset::gfx_dir::GfxDirStreamVtable, va::GFX_DIR_STREAM_VTABLE, {
47
+ destructor => gfx_dir_stream_destructor,
48
+ has_data => gfx_dir_stream_has_data,
49
+ bytes_consumed => gfx_dir_stream_bytes_consumed,
50
+ seek => gfx_dir_stream_seek,
51
+ get_total_size => gfx_dir_stream_get_total_size,
52
+ read => gfx_dir_stream_read,
53
+ })?;
54
+
55
+ unsafe {
56
+ crate::hook::install(
57
+ "GfxDir__LoadImage",
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
+ }
crates/openwa-dll/src/replacements/mod.rs CHANGED
@@ -7,6 +7,7 @@ mod frame_hook;
7
  mod frontend;
8
  mod game_session;
9
  mod game_state_hooks;
 
10
  mod hardware_init;
11
  mod headless;
12
  mod music;
@@ -59,6 +60,7 @@ pub fn install_all() -> Result<(), String> {
59
  speech::install()?;
60
  music::install()?;
61
  ddgame_init::install()?;
 
62
  game_state_hooks::install()?;
63
  replay::install()?;
64
  task::install()?;
 
7
  mod frontend;
8
  mod game_session;
9
  mod game_state_hooks;
10
+ mod gfx_dir;
11
  mod hardware_init;
12
  mod headless;
13
  mod music;
 
60
  speech::install()?;
61
  music::install()?;
62
  ddgame_init::install()?;
63
+ gfx_dir::install()?;
64
  game_state_hooks::install()?;
65
  replay::install()?;
66
  task::install()?;
crates/openwa-dll/src/replacements/render/display_vtable.rs CHANGED
@@ -4,6 +4,7 @@
4
  use core::ffi::c_char;
5
 
6
  use openwa_core::address::va;
 
7
  use openwa_core::bitgrid::DisplayBitGrid;
8
  use openwa_core::fixed::Fixed;
9
  use openwa_core::rebase::rb;
@@ -11,7 +12,6 @@ use openwa_core::render::display::destructor as display_destructor;
11
  use openwa_core::render::display::vtable::{self as display_vtable_impl, DisplayGfxVtable};
12
  use openwa_core::render::display::DisplayBase;
13
  use openwa_core::render::display::DisplayGfx;
14
- use openwa_core::render::sprite::gfx_dir::GfxDir;
15
  use openwa_core::render::SpriteOp;
16
  use openwa_core::vtable::patch_vtable;
17
  use openwa_core::vtable_replace;
 
4
  use core::ffi::c_char;
5
 
6
  use openwa_core::address::va;
7
+ use openwa_core::asset::gfx_dir::GfxDir;
8
  use openwa_core::bitgrid::DisplayBitGrid;
9
  use openwa_core::fixed::Fixed;
10
  use openwa_core::rebase::rb;
 
12
  use openwa_core::render::display::vtable::{self as display_vtable_impl, DisplayGfxVtable};
13
  use openwa_core::render::display::DisplayBase;
14
  use openwa_core::render::display::DisplayGfx;
 
15
  use openwa_core::render::SpriteOp;
16
  use openwa_core::vtable::patch_vtable;
17
  use openwa_core::vtable_replace;