paavohtl Claude Opus 4.6 (1M context) commited on
Commit
32368d0
Β·
1 Parent(s): 9148328

feat: pure Rust DSSound::new() and typed COM init via windows crate

Browse files

Replace DSSound constructor FFI bridge (call_dssound_ctor) with pure
Rust DSSound::new() that initializes all fields: 8 channel descriptors,
500 channel slots, 64-entry buffer pool, volume, and status flags.

Replace DSSOUND_INIT_BUFFERS FFI bridge (call_dssound_init_buffers) with
typed windows crate COM calls: IDirectSound::SetCooperativeLevel,
CreateSoundBuffer(DSBCAPS_PRIMARYBUFFER), IDirectSoundBuffer::GetCaps.

Expand DSSound struct from opaque blob to named fields matching the
constructor analysis (channel_descs, channel_slots, buffer_pool, etc.).

Remove dead naked asm bridges and statics (DSSOUND_CTOR_ADDR,
DSSOUND_INIT_BUF_ADDR, DSSOUND_INIT_EAX, DSSOUND_INIT_SAVED_RET).

Trap DSSound__Constructor and DSSOUND_INIT_BUFFERS -- both had
GameEngine__InitHardware as their only caller (now Rust).

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

crates/openwa-core/src/audio/dssound.rs CHANGED
@@ -1,4 +1,5 @@
1
  use crate::fixed::Fixed;
 
2
  /// DSSound β€” DirectSound audio subsystem.
3
  ///
4
  /// Created by DSSound__Constructor (0x573D50).
@@ -6,36 +7,114 @@ use crate::fixed::Fixed;
6
  /// Size: 0xBE0 bytes (allocated by GameEngine__InitHardware).
7
  ///
8
  /// Manages sound playback through DirectSound.
9
- /// Has 500 sound channel slots and a master volume control.
10
- ///
11
- /// PARTIAL: Only constructor-confirmed and InitHardware-confirmed fields.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  #[repr(C)]
13
  pub struct DSSound {
14
  /// 0x000: Vtable pointer (0x66AF20)
15
- pub vtable: *mut u8,
16
- /// 0x004: HWND (set to hwnd after construction)
17
- pub hwnd: u32,
18
  /// 0x008: IDirectSound* (from DirectSoundCreate)
19
- pub direct_sound: *mut u8,
20
- /// 0x00C: Output param from DSSOUND_INIT_BUFFERS (primary buffer caps/format)
21
  pub primary_buffer_caps: u32,
22
- /// 0x010: IDirectSoundBuffer* (primary buffer, from DSSOUND_INIT_BUFFERS)
23
- pub primary_buffer: *mut u8,
24
- /// 0x014-0x8A3: Unknown fields (includes 500-entry channel slot table)
25
- pub _unknown_014: [u8; 0x890],
 
 
26
  /// 0x8A4: Master volume (Fixed, init 0x10000 = 1.0)
27
  pub volume: Fixed,
28
- /// 0x8A8-0xBB3: Unknown
29
- pub _unknown_8a8: [u8; 0x30C],
 
 
 
 
 
 
 
 
 
 
30
  /// 0xBB4: Status flag 1 (init 1)
31
  pub status_1: u32,
32
  /// 0xBB8: Status flag 2 (init 1)
33
  pub status_2: u32,
34
  /// 0xBBC: Init success flag β€” set to 1 when DirectSoundCreate +
35
- /// DSSOUND_INIT_BUFFERS + IDirectSoundBuffer::Play all succeed.
36
  pub init_success: u32,
37
  /// 0xBC0-0xBDF: Unknown trailing fields
38
  pub _unknown_bc0: [u8; 0x20],
39
  }
40
 
41
  const _: () = assert!(core::mem::size_of::<DSSound>() == 0xBE0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  use crate::fixed::Fixed;
2
+
3
  /// DSSound β€” DirectSound audio subsystem.
4
  ///
5
  /// Created by DSSound__Constructor (0x573D50).
 
7
  /// Size: 0xBE0 bytes (allocated by GameEngine__InitHardware).
8
  ///
9
  /// Manages sound playback through DirectSound.
10
+ /// Has 8 channel descriptors, 500 sound channel slots, a 64-entry
11
+ /// buffer pool, and a master volume control.
12
+
13
+ type Ptr32 = u32;
14
+
15
+ /// Channel descriptor (0x18 bytes). 8 entries at DSSound+0x14.
16
+ /// FUN_005742A0 initializes: field_00=-1, field_04=-1, field_10=0.
17
+ #[repr(C)]
18
+ #[derive(Clone, Copy)]
19
+ pub struct ChannelDescriptor {
20
+ pub field_00: u32, // init -1
21
+ pub field_04: u32, // init -1
22
+ pub _pad_08: [u32; 2],
23
+ pub field_10: u32, // init 0
24
+ pub _pad_14: u32,
25
+ }
26
+
27
+ const _: () = assert!(core::mem::size_of::<ChannelDescriptor>() == 0x18);
28
+
29
  #[repr(C)]
30
  pub struct DSSound {
31
  /// 0x000: Vtable pointer (0x66AF20)
32
+ pub vtable: Ptr32,
33
+ /// 0x004: HWND (set after construction)
34
+ pub hwnd: Ptr32,
35
  /// 0x008: IDirectSound* (from DirectSoundCreate)
36
+ pub direct_sound: Ptr32,
37
+ /// 0x00C: Primary buffer caps/format output from init_buffers
38
  pub primary_buffer_caps: u32,
39
+ /// 0x010: IDirectSoundBuffer* (primary buffer)
40
+ pub primary_buffer: Ptr32,
41
+ /// 0x014-0xD3: 8 channel descriptors (0x18 bytes each = 0xC0 total)
42
+ pub channel_descs: [ChannelDescriptor; 8],
43
+ /// 0x0D4-0x8A3: 500 channel slot indices (zeroed by constructor)
44
+ pub channel_slots: [u32; 500],
45
  /// 0x8A4: Master volume (Fixed, init 0x10000 = 1.0)
46
  pub volume: Fixed,
47
+ /// 0x8A8-0x9A7: 64 buffer pool shadow entries (init all -1 by FUN_00574260)
48
+ pub buffer_pool_shadow: [u32; 64],
49
+ /// 0x9A8-0xAA7: 64 buffer pool indices (init 0..63 by FUN_00574260)
50
+ pub buffer_pool: [u32; 64],
51
+ /// 0xAA8: Buffer pool count (init 0x40 = 64)
52
+ pub buffer_pool_count: u32,
53
+ /// 0xAAC-0xBAB: Unknown (0x100 bytes)
54
+ pub _unknown_aac: [u8; 0x100],
55
+ /// 0xBAC: Buffer pool state (init 0 by FUN_00574260)
56
+ pub buffer_pool_state: u32,
57
+ /// 0xBB0: Unknown field (zeroed by constructor)
58
+ pub _field_bb0: u32,
59
  /// 0xBB4: Status flag 1 (init 1)
60
  pub status_1: u32,
61
  /// 0xBB8: Status flag 2 (init 1)
62
  pub status_2: u32,
63
  /// 0xBBC: Init success flag β€” set to 1 when DirectSoundCreate +
64
+ /// init_buffers + IDirectSoundBuffer::Play all succeed.
65
  pub init_success: u32,
66
  /// 0xBC0-0xBDF: Unknown trailing fields
67
  pub _unknown_bc0: [u8; 0x20],
68
  }
69
 
70
  const _: () = assert!(core::mem::size_of::<DSSound>() == 0xBE0);
71
+
72
+ impl DSSound {
73
+ /// Construct a DSSound with all fields initialized, matching the
74
+ /// original constructor (0x573D50) + helpers FUN_005742A0/FUN_00574260.
75
+ ///
76
+ /// Uses WA's vtable pointer for identity (same pattern as DisplayBase).
77
+ ///
78
+ /// # Safety
79
+ /// Must be called from within the WA.exe process (needs rebased vtable).
80
+ pub unsafe fn new(hwnd: u32) -> Self {
81
+ use crate::address::va;
82
+ use crate::rebase::rb;
83
+
84
+ let mut snd: Self = core::mem::zeroed();
85
+
86
+ // Vtable (WA's .rdata, identity-checked)
87
+ snd.vtable = rb(va::DS_SOUND_VTABLE);
88
+
89
+ // HWND
90
+ snd.hwnd = hwnd;
91
+
92
+ // Volume = 1.0 (16.16 fixed-point)
93
+ snd.volume = Fixed(0x10000);
94
+
95
+ // 8 channel descriptors: field_00=-1, field_04=-1, field_10=0
96
+ // (rest is zero from mem::zeroed)
97
+ for desc in &mut snd.channel_descs {
98
+ desc.field_00 = 0xFFFF_FFFF;
99
+ desc.field_04 = 0xFFFF_FFFF;
100
+ }
101
+
102
+ // 500 channel slots: already zeroed
103
+
104
+ // Buffer pool: shadow entries all -1, indices 0..63
105
+ for (i, slot) in snd.buffer_pool.iter_mut().enumerate() {
106
+ *slot = i as u32;
107
+ }
108
+ for entry in &mut snd.buffer_pool_shadow {
109
+ *entry = 0xFFFF_FFFF;
110
+ }
111
+ snd.buffer_pool_count = 64;
112
+
113
+ // Status flags
114
+ snd.status_1 = 1;
115
+ snd.status_2 = 1;
116
+ // init_success stays 0 until COM init succeeds
117
+
118
+ snd
119
+ }
120
+ }
crates/openwa-wormkit/src/replacements/hardware_init.rs CHANGED
@@ -61,8 +61,6 @@ static mut SAVED_RET: u32 = 0;
61
 
62
  /// Function addresses, set in `install()`.
63
  static mut TIMER_CTOR_ADDR: u32 = 0;
64
- static mut DSSOUND_CTOR_ADDR: u32 = 0;
65
- static mut DSSOUND_INIT_BUF_ADDR: u32 = 0;
66
  static mut INPUT_CTRL_INIT_ADDR: u32 = 0;
67
  static mut STREAM_CTOR_ADDR: u32 = 0;
68
  static mut DDISPLAY_INIT_ADDR: u32 = 0;
@@ -75,8 +73,6 @@ static mut INPUT_CTRL_ESI: u32 = 0;
75
  /// Saved ESI across the `call_input_ctrl_init` call.
76
  static mut INPUT_CTRL_SAVED_ESI: u32 = 0;
77
 
78
- /// Implicit EAX for `call_dssound_init_buffers` (set by `impl_init_hardware`).
79
- static mut DSSOUND_INIT_EAX: u32 = 0;
80
 
81
  // ─── Bridges ─────────────────────────────────────────────────────────────────
82
  //
@@ -102,58 +98,6 @@ unsafe extern "cdecl" fn call_timer_ctor(_timer_ptr: *mut GameTimer, _crosshair_
102
  );
103
  }
104
 
105
- /// DSSound constructor: `usercall(EAX=this)`, plain RET. Void.
106
- ///
107
- /// IMPORTANT: FUN_00573D50 clobbers ECX (MOV ECX, 0x1F4 for REP STOSD).
108
- /// Do NOT use ECX to save bridge_ret β€” read it from the stack instead.
109
- #[unsafe(naked)]
110
- unsafe extern "cdecl" fn call_dssound_ctor(_dssound: *mut DSSound) {
111
- core::arch::naked_asm!(
112
- // [esp+0]=bridge_ret, [esp+4]=dssound
113
- "movl 4(%esp), %eax", // EAX = dssound (bridge_ret stays on stack untouched)
114
- "calll *({fn})", // FUN_00573D50: plain RET; calll pushes continuation here
115
- // After FUN_00573D50 RET: stack = [bridge_ret, dssound]
116
- "retl", // pop bridge_ret, return; caller cleans dssound
117
- fn = sym DSSOUND_CTOR_ADDR,
118
- options(att_syntax),
119
- );
120
- }
121
-
122
- /// Saved bridge_ret for `call_dssound_init_buffers` (can't use ECX β€” callee clobbers it).
123
- static mut DSSOUND_INIT_SAVED_RET: u32 = 0;
124
-
125
- /// FUN_00573E50: `usercall(EAX=dssound)` + `__stdcall(out_0x10, out_0x0C)`, `RET 0x8`.
126
- /// Declared as `extern "stdcall"` so Rust caller does NOT emit `add esp, 8` after the call
127
- /// (callee already cleans via RET 0x8).
128
- /// Caller sets `DSSOUND_INIT_EAX = dssound` before calling.
129
- #[unsafe(naked)]
130
- unsafe extern "stdcall" fn call_dssound_init_buffers(
131
- _out_primary_buffer: *mut *mut u8,
132
- _out_primary_caps: *mut u32,
133
- ) -> u32 {
134
- core::arch::naked_asm!(
135
- // Entry: ESP=E, [E+0]=bridge_ret, [E+4]=out_primary_buffer, [E+8]=out_primary_caps
136
- "movl {eax_val}, %eax", // EAX = dssound
137
- "movl (%esp), %ecx", // ECX = bridge_ret (temp)
138
- "movl %ecx, {saved_ret}", // save to static (ECX clobbered by FUN_00573E50)
139
- "movl 4(%esp), %ecx", // ECX = out_primary_buffer
140
- "movl 8(%esp), %edx", // EDX = out_primary_caps
141
- "addl $0xC, %esp", // ESP = E+12 (discard bridge_ret + 2 args)
142
- "pushl %edx", // ESP = E+8, [E+8] = out_primary_caps
143
- "pushl %ecx", // ESP = E+4, [E+4] = out_primary_buffer
144
- "calll *({fn})", // ESP = E, [E] = cont
145
- // FUN_00573E50 RET 0x8: pops cont β†’ E+4, +8 β†’ E+12
146
- // At cont: ESP = E+12; EAX = return value
147
- "pushl {saved_ret}", // ESP = E+8, [E+8] = bridge_ret
148
- "retl", // pops bridge_ret β†’ ESP = E+12
149
- // stdcall: caller skips cleanup. ESP = E+12 = E+4+8 βœ“
150
- eax_val = sym DSSOUND_INIT_EAX,
151
- fn = sym DSSOUND_INIT_BUF_ADDR,
152
- saved_ret = sym DSSOUND_INIT_SAVED_RET,
153
- options(att_syntax),
154
- );
155
- }
156
-
157
  /// FUN_0058C0D0: `usercall(ESI=input_ctrl)` + stdcall(4 params), RET 0x10.
158
  /// Caller sets `INPUT_CTRL_ESI = input_ctrl` before calling.
159
  /// FUN_0058C0D0's `RET 0x10` cleans all 4 args, so `retl` here needs no cleanup.
@@ -248,34 +192,50 @@ unsafe extern "stdcall" fn call_ddisplay_init(
248
  /// if COM steps fail, matching original WA behavior).
249
  unsafe fn create_dssound(hwnd: u32) -> *mut DSSound {
250
  use windows::Win32::Media::Audio::DirectSound::{
251
- DirectSoundCreate, IDirectSound, IDirectSoundBuffer, DSBPLAY_LOOPING,
 
252
  };
 
253
 
254
- let snd = WABox::<DSSound>::alloc(0xBE0, 0xBC0).leak();
255
- call_dssound_ctor(snd);
256
- (*snd).hwnd = hwnd;
257
 
258
- // Create DirectSound COM object.
259
  let mut ds: Option<IDirectSound> = None;
260
  if DirectSoundCreate(None, &mut ds, None).is_ok() {
261
  let ds = ds.unwrap();
262
- (*snd).direct_sound = core::mem::transmute_copy(&ds);
263
- core::mem::forget(ds); // WA owns the COM reference
264
-
265
- // Initialize primary buffer (WA's usercall function).
266
- DSSOUND_INIT_EAX = snd as u32;
267
- let hr2 = call_dssound_init_buffers(
268
- &mut (*snd).primary_buffer,
269
- &mut (*snd).primary_buffer_caps,
270
- );
271
- if hr2 == 0 {
272
- // Start the primary buffer looping.
273
- let primary: &IDirectSoundBuffer =
274
- &*(&(*snd).primary_buffer as *const *mut u8 as *const IDirectSoundBuffer);
 
 
 
 
 
 
 
 
275
  if primary.Play(0, 0, DSBPLAY_LOOPING).is_ok() {
276
  (*snd).init_success = 1;
277
  }
 
 
 
 
278
  }
 
 
 
279
  }
280
 
281
  snd
@@ -409,7 +369,7 @@ unsafe extern "cdecl" fn impl_init_hardware(
409
  (*session).streaming_audio = core::ptr::null_mut();
410
  if !(*session).sound.is_null() && gi.speech_enabled != 0 {
411
  let stream = WABox::<StreamingAudio>::alloc(0x354, 0x334).leak();
412
- let ids = (*(*session).sound).direct_sound;
413
  call_streaming_audio_ctor(
414
  stream as *mut u8,
415
  ids,
@@ -500,8 +460,6 @@ unsafe extern "C" fn hook_init_hardware() {
500
  pub fn install() -> Result<(), String> {
501
  unsafe {
502
  TIMER_CTOR_ADDR = rb(va::GAME_ENGINE_TIMER_CTOR);
503
- DSSOUND_CTOR_ADDR = rb(va::CONSTRUCT_DS_SOUND);
504
- DSSOUND_INIT_BUF_ADDR = rb(va::DSSOUND_INIT_BUFFERS);
505
  INPUT_CTRL_INIT_ADDR = rb(va::INPUT_CTRL_INIT);
506
  STREAM_CTOR_ADDR = rb(va::STREAMING_AUDIO_CTOR);
507
  DDISPLAY_INIT_ADDR = rb(va::DDISPLAY_INIT);
@@ -512,6 +470,10 @@ pub fn install() -> Result<(), String> {
512
  va::GAME_ENGINE_INIT_HARDWARE,
513
  hook_init_hardware as *const (),
514
  )?;
 
 
 
 
515
  }
516
  Ok(())
517
  }
 
61
 
62
  /// Function addresses, set in `install()`.
63
  static mut TIMER_CTOR_ADDR: u32 = 0;
 
 
64
  static mut INPUT_CTRL_INIT_ADDR: u32 = 0;
65
  static mut STREAM_CTOR_ADDR: u32 = 0;
66
  static mut DDISPLAY_INIT_ADDR: u32 = 0;
 
73
  /// Saved ESI across the `call_input_ctrl_init` call.
74
  static mut INPUT_CTRL_SAVED_ESI: u32 = 0;
75
 
 
 
76
 
77
  // ─── Bridges ─────────────────────────────────────────────────────────────────
78
  //
 
98
  );
99
  }
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  /// FUN_0058C0D0: `usercall(ESI=input_ctrl)` + stdcall(4 params), RET 0x10.
102
  /// Caller sets `INPUT_CTRL_ESI = input_ctrl` before calling.
103
  /// FUN_0058C0D0's `RET 0x10` cleans all 4 args, so `retl` here needs no cleanup.
 
192
  /// if COM steps fail, matching original WA behavior).
193
  unsafe fn create_dssound(hwnd: u32) -> *mut DSSound {
194
  use windows::Win32::Media::Audio::DirectSound::{
195
+ DirectSoundCreate, IDirectSound, IDirectSoundBuffer,
196
+ DSBUFFERDESC, DSBCAPS_PRIMARYBUFFER, DSBPLAY_LOOPING, DSSCL_PRIORITY,
197
  };
198
+ use windows::Win32::Foundation::HWND;
199
 
200
+ // Pure Rust construction β€” replaces call_dssound_ctor bridge.
201
+ let snd = WABox::<DSSound>::from_value(DSSound::new(hwnd)).leak();
 
202
 
203
+ // DirectSound COM initialization β€” replaces call_dssound_init_buffers bridge.
204
  let mut ds: Option<IDirectSound> = None;
205
  if DirectSoundCreate(None, &mut ds, None).is_ok() {
206
  let ds = ds.unwrap();
207
+
208
+ // SetCooperativeLevel(hwnd, DSSCL_PRIORITY)
209
+ let _ = ds.SetCooperativeLevel(HWND(hwnd as _), DSSCL_PRIORITY);
210
+
211
+ // CreateSoundBuffer with DSBCAPS_PRIMARYBUFFER (no format β€” primary buffer)
212
+ let desc = DSBUFFERDESC {
213
+ dwSize: core::mem::size_of::<DSBUFFERDESC>() as u32,
214
+ dwFlags: DSBCAPS_PRIMARYBUFFER,
215
+ ..core::mem::zeroed()
216
+ };
217
+ let mut primary: Option<IDirectSoundBuffer> = None;
218
+ if ds.CreateSoundBuffer(&desc, &mut primary, None).is_ok() {
219
+ let primary = primary.unwrap();
220
+
221
+ // GetCaps to populate primary_buffer_caps
222
+ let mut caps = core::mem::zeroed::<windows::Win32::Media::Audio::DirectSound::DSBCAPS>();
223
+ caps.dwSize = core::mem::size_of_val(&caps) as u32;
224
+ let _ = primary.GetCaps(&mut caps);
225
+ (*snd).primary_buffer_caps = caps.dwBufferBytes;
226
+
227
+ // Start primary buffer looping
228
  if primary.Play(0, 0, DSBPLAY_LOOPING).is_ok() {
229
  (*snd).init_success = 1;
230
  }
231
+
232
+ // Store COM pointers as raw u32 (WA owns the references)
233
+ (*snd).primary_buffer = core::mem::transmute_copy(&primary);
234
+ core::mem::forget(primary);
235
  }
236
+
237
+ (*snd).direct_sound = core::mem::transmute_copy(&ds);
238
+ core::mem::forget(ds);
239
  }
240
 
241
  snd
 
369
  (*session).streaming_audio = core::ptr::null_mut();
370
  if !(*session).sound.is_null() && gi.speech_enabled != 0 {
371
  let stream = WABox::<StreamingAudio>::alloc(0x354, 0x334).leak();
372
+ let ids = (*(*session).sound).direct_sound as *mut u8;
373
  call_streaming_audio_ctor(
374
  stream as *mut u8,
375
  ids,
 
460
  pub fn install() -> Result<(), String> {
461
  unsafe {
462
  TIMER_CTOR_ADDR = rb(va::GAME_ENGINE_TIMER_CTOR);
 
 
463
  INPUT_CTRL_INIT_ADDR = rb(va::INPUT_CTRL_INIT);
464
  STREAM_CTOR_ADDR = rb(va::STREAMING_AUDIO_CTOR);
465
  DDISPLAY_INIT_ADDR = rb(va::DDISPLAY_INIT);
 
470
  va::GAME_ENGINE_INIT_HARDWARE,
471
  hook_init_hardware as *const (),
472
  )?;
473
+
474
+ // Trap functions whose only caller was GameEngine__InitHardware (now Rust).
475
+ hook::install_trap!("DSSound__Constructor", va::CONSTRUCT_DS_SOUND);
476
+ hook::install_trap!("DSSOUND_INIT_BUFFERS", va::DSSOUND_INIT_BUFFERS);
477
  }
478
  Ok(())
479
  }