feat: define DSSoundVtable (24 slots) and patch is_slot_loaded to Rust
Browse filesAdd typed DSSoundVtable struct with all 24 virtual method signatures,
named from decompilation analysis (update_channels, play_sound,
set_master_volume, load_wav, stop_channel, etc.).
Refine ChannelDescriptor fields from vtable method analysis: flags_a/b,
volume_num/val, ds_buffer, pool_index. Rename buffer pool fields to
reflect free-list/used-list semantics. Rename _field_bb0 to
total_bytes_loaded (incremented by load_wav).
Implement is_slot_loaded (slot 13) in Rust and patch it into WA's
DSSound vtable via patch_vtable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crates/openwa-core/src/audio/dssound.rs
CHANGED
|
@@ -3,8 +3,8 @@ use crate::fixed::Fixed;
|
|
| 3 |
/// DSSound β DirectSound audio subsystem.
|
| 4 |
///
|
| 5 |
/// Created by DSSound__Constructor (0x573D50).
|
| 6 |
-
/// Vtable: 0x66AF20
|
| 7 |
-
/// Size: 0xBE0 bytes
|
| 8 |
///
|
| 9 |
/// Manages sound playback through DirectSound.
|
| 10 |
/// Has 8 channel descriptors, 500 sound channel slots, a 64-entry
|
|
@@ -13,23 +13,90 @@ use crate::fixed::Fixed;
|
|
| 13 |
type Ptr32 = u32;
|
| 14 |
|
| 15 |
/// Channel descriptor (0x18 bytes). 8 entries at DSSound+0x14.
|
| 16 |
-
///
|
|
|
|
|
|
|
| 17 |
#[repr(C)]
|
| 18 |
#[derive(Clone, Copy)]
|
| 19 |
pub struct ChannelDescriptor {
|
| 20 |
-
|
| 21 |
-
pub
|
| 22 |
-
|
| 23 |
-
pub
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 33 |
/// 0x004: HWND (set after construction)
|
| 34 |
pub hwnd: Ptr32,
|
| 35 |
/// 0x008: IDirectSound* (from DirectSoundCreate)
|
|
@@ -40,25 +107,30 @@ pub struct DSSound {
|
|
| 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 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
///
|
| 52 |
-
pub
|
| 53 |
-
///
|
| 54 |
-
pub
|
| 55 |
-
///
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
@@ -69,6 +141,16 @@ pub struct DSSound {
|
|
| 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.
|
|
@@ -84,7 +166,7 @@ impl DSSound {
|
|
| 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;
|
|
@@ -92,23 +174,23 @@ impl DSSound {
|
|
| 92 |
// Volume = 1.0 (16.16 fixed-point)
|
| 93 |
snd.volume = Fixed(0x10000);
|
| 94 |
|
| 95 |
-
// 8 channel descriptors:
|
| 96 |
// (rest is zero from mem::zeroed)
|
| 97 |
for desc in &mut snd.channel_descs {
|
| 98 |
-
desc.
|
| 99 |
-
desc.
|
| 100 |
}
|
| 101 |
|
| 102 |
// 500 channel slots: already zeroed
|
| 103 |
|
| 104 |
-
// Buffer pool: shadow
|
| 105 |
-
for (i, slot) in snd.
|
| 106 |
*slot = i as u32;
|
| 107 |
}
|
| 108 |
for entry in &mut snd.buffer_pool_shadow {
|
| 109 |
-
*entry =
|
| 110 |
}
|
| 111 |
-
snd.
|
| 112 |
|
| 113 |
// Status flags
|
| 114 |
snd.status_1 = 1;
|
|
|
|
| 3 |
/// DSSound β DirectSound audio subsystem.
|
| 4 |
///
|
| 5 |
/// Created by DSSound__Constructor (0x573D50).
|
| 6 |
+
/// Vtable: 0x66AF20 (24 slots).
|
| 7 |
+
/// Size: 0xBE0 bytes.
|
| 8 |
///
|
| 9 |
/// Manages sound playback through DirectSound.
|
| 10 |
/// Has 8 channel descriptors, 500 sound channel slots, a 64-entry
|
|
|
|
| 13 |
type Ptr32 = u32;
|
| 14 |
|
| 15 |
/// Channel descriptor (0x18 bytes). 8 entries at DSSound+0x14.
|
| 16 |
+
///
|
| 17 |
+
/// Each descriptor tracks one active DirectSound buffer being played.
|
| 18 |
+
/// Fields identified from vtable methods (update_channels, set_volume, etc.).
|
| 19 |
#[repr(C)]
|
| 20 |
#[derive(Clone, Copy)]
|
| 21 |
pub struct ChannelDescriptor {
|
| 22 |
+
/// Flags A (init -1, -1 = unused/free)
|
| 23 |
+
pub flags_a: i32,
|
| 24 |
+
/// Flags B (init -1, checked for < 0 in update_channels)
|
| 25 |
+
pub flags_b: i32,
|
| 26 |
+
/// Volume numerator (used in volume calculations, 0 = default)
|
| 27 |
+
pub volume_num: i32,
|
| 28 |
+
/// Volume value (scaled, used by set_channel_volume)
|
| 29 |
+
pub volume_val: i32,
|
| 30 |
+
/// IDirectSoundBuffer* for the active buffer on this channel (0 = empty)
|
| 31 |
+
pub ds_buffer: Ptr32,
|
| 32 |
+
/// Pool index tracking which buffer pool entry this channel uses
|
| 33 |
+
pub pool_index: u32,
|
| 34 |
}
|
| 35 |
|
| 36 |
const _: () = assert!(core::mem::size_of::<ChannelDescriptor>() == 0x18);
|
| 37 |
|
| 38 |
+
/// DSSound vtable layout (24 slots at 0x66AF20).
|
| 39 |
+
///
|
| 40 |
+
/// Methods operate on the 8 channel descriptors and 64-entry buffer pool.
|
| 41 |
+
/// Trivial slots (stubs, noops, return-constant) are marked.
|
| 42 |
+
#[repr(C)]
|
| 43 |
+
pub struct DSSoundVtable {
|
| 44 |
+
/// Slot 0 (0x573DB0): destructor β thiscall(this, flags)
|
| 45 |
+
pub destructor: unsafe extern "thiscall" fn(*mut DSSound, u8) -> *mut DSSound,
|
| 46 |
+
/// Slot 1 (0x574400): update_channels β iterates 8 descs, releases finished buffers
|
| 47 |
+
pub update_channels: unsafe extern "thiscall" fn(*mut DSSound),
|
| 48 |
+
/// Slot 2 (0x574460): set_volume_params β sets status_1/2, adjusts channel volumes
|
| 49 |
+
pub set_volume_params: unsafe extern "thiscall" fn(*mut DSSound, u32, i32),
|
| 50 |
+
/// Slot 3 (0x574730): play_sound β wrapper around core play, returns bool
|
| 51 |
+
pub play_sound: unsafe extern "thiscall" fn(*mut DSSound, u32, u32, u32, u32) -> bool,
|
| 52 |
+
/// Slot 4 (0x574770): play_sound_pooled β allocates from buffer pool, plays
|
| 53 |
+
pub play_sound_pooled: unsafe extern "thiscall" fn(*mut DSSound, u32, u32, u32, u32, u32) -> i32,
|
| 54 |
+
/// Slot 5 (0x574900): set_pan β sets pan on channel (dB lookup)
|
| 55 |
+
pub set_pan: unsafe extern "thiscall" fn(*mut DSSound, u32, u32) -> u32,
|
| 56 |
+
/// Slot 6 (0x505430): **stub** β returns 0
|
| 57 |
+
pub stub_6: unsafe extern "thiscall" fn(*mut DSSound) -> u32,
|
| 58 |
+
/// Slot 7 (0x574A10): set_master_volume β sets volume, adjusts all channels
|
| 59 |
+
pub set_master_volume: unsafe extern "thiscall" fn(*mut DSSound, i32) -> u32,
|
| 60 |
+
/// Slot 8 (0x574980): set_channel_volume β volume on specific channel
|
| 61 |
+
pub set_channel_volume: unsafe extern "thiscall" fn(*mut DSSound, i32, i32) -> u32,
|
| 62 |
+
/// Slot 9 (0x5747F0): is_channel_playing β checks buffer status
|
| 63 |
+
pub is_channel_playing: unsafe extern "thiscall" fn(*mut DSSound, i32) -> u8,
|
| 64 |
+
/// Slot 10 (0x574840): stop_channel β stops + releases buffer, returns to pool
|
| 65 |
+
pub stop_channel: unsafe extern "thiscall" fn(*mut DSSound, i32) -> u32,
|
| 66 |
+
/// Slot 11 (0x574AB0): release_finished β releases finished buffers, returns count
|
| 67 |
+
pub release_finished: unsafe extern "thiscall" fn(*mut DSSound) -> i32,
|
| 68 |
+
/// Slot 12 (0x573FF0): load_wav β opens WAV file, parses RIFF, creates buffer
|
| 69 |
+
pub load_wav: unsafe extern "thiscall" fn(*mut DSSound, i32, *const u8) -> u32,
|
| 70 |
+
/// Slot 13 (0x573FD0): is_slot_loaded β returns channel_slots[idx] != 0
|
| 71 |
+
pub is_slot_loaded: unsafe extern "thiscall" fn(*mut DSSound, i32) -> bool,
|
| 72 |
+
/// Slot 14 (0x573D30): sub_destructor β sets secondary vtable
|
| 73 |
+
pub sub_destructor: unsafe extern "thiscall" fn(*mut DSSound, u8) -> *mut DSSound,
|
| 74 |
+
/// Slot 15 (0x4AA060): **noop** β returns void
|
| 75 |
+
pub noop_15: unsafe extern "thiscall" fn(*mut DSSound),
|
| 76 |
+
/// Slot 16 (0x5931C0): **noop** β returns void
|
| 77 |
+
pub noop_16: unsafe extern "thiscall" fn(*mut DSSound),
|
| 78 |
+
/// Slot 17 (0x573D20): **returns_0**
|
| 79 |
+
pub returns_0_17: unsafe extern "thiscall" fn(*mut DSSound) -> u32,
|
| 80 |
+
/// Slot 18 (0x573D20): **returns_0** (same as 17)
|
| 81 |
+
pub returns_0_18: unsafe extern "thiscall" fn(*mut DSSound) -> u32,
|
| 82 |
+
/// Slot 19 (0x505430): **stub** β returns 0
|
| 83 |
+
pub stub_19: unsafe extern "thiscall" fn(*mut DSSound) -> u32,
|
| 84 |
+
/// Slot 20 (0x505430): **stub** β returns 0
|
| 85 |
+
pub stub_20: unsafe extern "thiscall" fn(*mut DSSound) -> u32,
|
| 86 |
+
/// Slot 21 (0x571AF0): **returns_0**
|
| 87 |
+
pub returns_0_21: unsafe extern "thiscall" fn(*mut DSSound) -> u32,
|
| 88 |
+
/// Slot 22 (0x505430): **stub** β returns 0
|
| 89 |
+
pub stub_22: unsafe extern "thiscall" fn(*mut DSSound) -> u32,
|
| 90 |
+
/// Slot 23 (0x4260E0): **returns_1**
|
| 91 |
+
pub returns_1_23: unsafe extern "thiscall" fn(*mut DSSound) -> u32,
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
const _: () = assert!(core::mem::size_of::<DSSoundVtable>() == 24 * 4);
|
| 95 |
+
|
| 96 |
#[repr(C)]
|
| 97 |
pub struct DSSound {
|
| 98 |
/// 0x000: Vtable pointer (0x66AF20)
|
| 99 |
+
pub vtable: *const DSSoundVtable,
|
| 100 |
/// 0x004: HWND (set after construction)
|
| 101 |
pub hwnd: Ptr32,
|
| 102 |
/// 0x008: IDirectSound* (from DirectSoundCreate)
|
|
|
|
| 107 |
pub primary_buffer: Ptr32,
|
| 108 |
/// 0x014-0xD3: 8 channel descriptors (0x18 bytes each = 0xC0 total)
|
| 109 |
pub channel_descs: [ChannelDescriptor; 8],
|
| 110 |
+
/// 0x0D4-0x8A3: 500 channel slot indices (zeroed by constructor).
|
| 111 |
+
/// Each slot holds a pointer to an IDirectSoundBuffer for that sound effect.
|
| 112 |
+
/// Index is 1-based (slot 0 unused). slot[i] != 0 means sound i is loaded.
|
| 113 |
pub channel_slots: [u32; 500],
|
| 114 |
/// 0x8A4: Master volume (Fixed, init 0x10000 = 1.0)
|
| 115 |
pub volume: Fixed,
|
| 116 |
+
/// 0x8A8-0x9A7: 64 buffer pool shadow entries (init all -1 by FUN_00574260).
|
| 117 |
+
/// Maps pool index β channel descriptor index (-1 = free).
|
| 118 |
+
pub buffer_pool_shadow: [i32; 64],
|
| 119 |
+
/// 0x9A8-0xAA7: 64 buffer pool indices (init 0..63 by FUN_00574260).
|
| 120 |
+
/// Free-list stack of available pool indices.
|
| 121 |
+
pub buffer_pool_free: [u32; 64],
|
| 122 |
+
/// 0xAA8: Number of free entries in buffer_pool_free (init 0x40 = 64).
|
| 123 |
+
pub buffer_pool_free_count: u32,
|
| 124 |
+
/// 0xAAC-0xBAB: Used pool tracking (0x100 bytes = 64 u32 entries).
|
| 125 |
+
/// Tracks which pool indices are in use.
|
| 126 |
+
pub buffer_pool_used: [u32; 64],
|
| 127 |
+
/// 0xBAC: Number of used entries in buffer_pool_used (init 0).
|
| 128 |
+
pub buffer_pool_used_count: u32,
|
| 129 |
+
/// 0xBB0: Total bytes loaded (incremented by load_wav)
|
| 130 |
+
pub total_bytes_loaded: u32,
|
| 131 |
+
/// 0xBB4: Status flag 1 (init 1, used by set_volume_params)
|
| 132 |
pub status_1: u32,
|
| 133 |
+
/// 0xBB8: Status flag 2 (init 1, used by set_volume_params)
|
| 134 |
pub status_2: u32,
|
| 135 |
/// 0xBBC: Init success flag β set to 1 when DirectSoundCreate +
|
| 136 |
/// init_buffers + IDirectSoundBuffer::Play all succeed.
|
|
|
|
| 141 |
|
| 142 |
const _: () = assert!(core::mem::size_of::<DSSound>() == 0xBE0);
|
| 143 |
|
| 144 |
+
// ββ Trivial vtable method implementations βββββββββββββββββββββββββββββββββ
|
| 145 |
+
|
| 146 |
+
/// Slot 13: is_slot_loaded β returns whether sound at `slot_idx` has a buffer loaded.
|
| 147 |
+
/// One-liner: `channel_slots[slot_idx] != 0`.
|
| 148 |
+
pub unsafe extern "thiscall" fn is_slot_loaded(this: *mut DSSound, slot_idx: i32) -> bool {
|
| 149 |
+
(*this).channel_slots.get((slot_idx - 1) as usize).copied().unwrap_or(0) != 0
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
// ββ Construction ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 153 |
+
|
| 154 |
impl DSSound {
|
| 155 |
/// Construct a DSSound with all fields initialized, matching the
|
| 156 |
/// original constructor (0x573D50) + helpers FUN_005742A0/FUN_00574260.
|
|
|
|
| 166 |
let mut snd: Self = core::mem::zeroed();
|
| 167 |
|
| 168 |
// Vtable (WA's .rdata, identity-checked)
|
| 169 |
+
snd.vtable = rb(va::DS_SOUND_VTABLE) as *const DSSoundVtable;
|
| 170 |
|
| 171 |
// HWND
|
| 172 |
snd.hwnd = hwnd;
|
|
|
|
| 174 |
// Volume = 1.0 (16.16 fixed-point)
|
| 175 |
snd.volume = Fixed(0x10000);
|
| 176 |
|
| 177 |
+
// 8 channel descriptors: flags_a=-1, flags_b=-1
|
| 178 |
// (rest is zero from mem::zeroed)
|
| 179 |
for desc in &mut snd.channel_descs {
|
| 180 |
+
desc.flags_a = -1;
|
| 181 |
+
desc.flags_b = -1;
|
| 182 |
}
|
| 183 |
|
| 184 |
// 500 channel slots: already zeroed
|
| 185 |
|
| 186 |
+
// Buffer pool: shadow all -1, free list 0..63
|
| 187 |
+
for (i, slot) in snd.buffer_pool_free.iter_mut().enumerate() {
|
| 188 |
*slot = i as u32;
|
| 189 |
}
|
| 190 |
for entry in &mut snd.buffer_pool_shadow {
|
| 191 |
+
*entry = -1;
|
| 192 |
}
|
| 193 |
+
snd.buffer_pool_free_count = 64;
|
| 194 |
|
| 195 |
// Status flags
|
| 196 |
snd.status_1 = 1;
|
crates/openwa-core/src/audio/mod.rs
CHANGED
|
@@ -6,7 +6,7 @@ pub mod speech;
|
|
| 6 |
pub mod streaming;
|
| 7 |
|
| 8 |
pub use active_sound::{ActiveSoundEntry, ActiveSoundTable};
|
| 9 |
-
pub use dssound::DSSound;
|
| 10 |
pub use music::Music;
|
| 11 |
pub use sound::SoundId;
|
| 12 |
pub use speech::{SpeechLineId, SpeechLineTableEntry, SpeechSlotTable, SPEECH_LINE_TABLE_COUNT};
|
|
|
|
| 6 |
pub mod streaming;
|
| 7 |
|
| 8 |
pub use active_sound::{ActiveSoundEntry, ActiveSoundTable};
|
| 9 |
+
pub use dssound::{DSSound, DSSoundVtable, ChannelDescriptor, is_slot_loaded};
|
| 10 |
pub use music::Music;
|
| 11 |
pub use sound::SoundId;
|
| 12 |
pub use speech::{SpeechLineId, SpeechLineTableEntry, SpeechSlotTable, SPEECH_LINE_TABLE_COUNT};
|
crates/openwa-wormkit/src/replacements/sound.rs
CHANGED
|
@@ -155,7 +155,31 @@ pub fn install() -> Result<(), String> {
|
|
| 155 |
va::PLAY_SOUND_LOCAL,
|
| 156 |
trampoline_play_sound_local as *const (),
|
| 157 |
)?;
|
|
|
|
|
|
|
|
|
|
| 158 |
}
|
| 159 |
|
| 160 |
Ok(())
|
| 161 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
va::PLAY_SOUND_LOCAL,
|
| 156 |
trampoline_play_sound_local as *const (),
|
| 157 |
)?;
|
| 158 |
+
|
| 159 |
+
// Patch DSSound vtable: replace trivial slots with Rust implementations.
|
| 160 |
+
patch_dssound_vtable()?;
|
| 161 |
}
|
| 162 |
|
| 163 |
Ok(())
|
| 164 |
}
|
| 165 |
+
|
| 166 |
+
/// Patch DSSound vtable (0x66AF20) to replace trivial methods with Rust.
|
| 167 |
+
unsafe fn patch_dssound_vtable() -> Result<(), String> {
|
| 168 |
+
use openwa_core::rebase::rb;
|
| 169 |
+
use openwa_core::vtable::patch_vtable;
|
| 170 |
+
use openwa_core::audio::is_slot_loaded;
|
| 171 |
+
|
| 172 |
+
let vtable = rb(va::DS_SOUND_VTABLE) as *mut u32;
|
| 173 |
+
|
| 174 |
+
patch_vtable(vtable, 24, |vt| {
|
| 175 |
+
let mut patched = 0u32;
|
| 176 |
+
|
| 177 |
+
// Slot 13: is_slot_loaded β one-liner channel_slots check
|
| 178 |
+
*vt.add(13) = is_slot_loaded as *const () as u32;
|
| 179 |
+
patched += 1;
|
| 180 |
+
|
| 181 |
+
let _ = log_line(&format!(
|
| 182 |
+
"[Sound] DSSound vtable: patched {patched}/24 slots with Rust"
|
| 183 |
+
));
|
| 184 |
+
}).map_err(|e| e.to_string())
|
| 185 |
+
}
|