-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixing the game to run on multiple cores and not always use 100% of core #423
Comments
I'm not 100% sure what the issue is, but from what I could analyze the game is heavily abstracting most of the DirectSound access via CRI SDK: here's a huge list of what I could identify by using the XBox SDK libraries. The thing matches for the most and I suspect all issues are stemming from the fact that CRI pretty much ported to PC what was used on XBox, so probably not the most multi-core friendly code. I wanted to do some tests with their more recent SDK to see if patching that up can fix the issue in any way. It might be tricky, but at least with library reference it's a lot easier to isolate threaded functions. So far the only huge difference is that the PC version of SH2 seems to initialize DirectSound in its own function, which doesn't match any function in the library (so far, could be an oversight), nor the CRI SDK examples. EDIT: update to the huge list. Turns out the code works a little differently than the XBox version, just like the Dreamcast SDK has a different way to initialize the sound. So the part that calls DirectSoundCreate8 is definitively part of the engine, and so are a few helper functions with buffers. Still, considering how the whole ADX library is using a lot of threading internally, I do suspect the only way to fix looping issues would be to rewrite whatever does the update. |
Seems like a good solution to fix the problem is to override CreateThread in the exe, either as single call to isolate which routine is faulty, or by overriding the external pointer altogether. Here's the trick that I'm using currently for global override: HANDLE WINAPI _CreateThread(_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ __drv_aliasesMem LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
)
{
HANDLE th = CreateThread(lpThreadAttributes, dwStackSize, lpStartAddress, lpParameter, dwCreationFlags, lpThreadId);
if (th)
{
DWORD ret = SetThreadAffinityMask(th, 1);
// we need to do shit, damn
if (ret == 0)
{
CloseHandle(th);
}
}
return th;
}
void Inject_tests()
{
INJECT_EXT(0x24A6838, _CreateThread);
} This example should be safe enough since nothing in the game calls CreateThread besides the ADX lib. So the idea is to force just the faulty ADX code (or all of it) to run on a single core via SetThreadAffinityMask sitting hidden in our rerouted call. No idea how to detect the most suitable core to run the ADX lib, but I'm sure anything besides the game process should be good. |
All of the items on that list are related to the "voice" and background music (BGM). There are also sound effects (SFX), which simply use a wave file. I think we even get stuttering from these.
Yeah, I believe that the game includes a beta version of this library because the released version was not available when this game was created.
Thanks for your comments. I tried setting threads to the same processor affinity before and was unable to isolate the offending thread. However, your idea of hooking the CreateThread() function allowed me to collect more information about the threads. Using this method I was able to detect that there is a threading issue in the threads that run in "binkw32.dll" and at least one of the threads that run in "sh2pc.exe". There are only 5 threads (including the starting thread) that run in "sh2pc.exe" and only two in "binkw32.dll". The two that run in "binkw32.dll" are started dynamically and only run when a video is playing and exit after the video closes. BTW: @Polymega, this could be the cause of the delay when starting a video. I created a quick build that sets the affinity of all the threads running in "binkw32.dll" and "sh2pc.exe" to the same processor. This build also logs details about each thread that starts up, to help with any debugging. A quick test shows that this seems to work well. This allows the game to use more than double the amount of CPU then it did before without any issues (at least none that I could detect). Here is the testing build: d3d8.zip BTW, below is a list of all the normal threads that are started by the game. I excluded the threads created by the d3d8.dll module itself. The "Start:" parameter shows where the thread begins to execute code once it starts up.
|
Looks like I was wrong about this one. I think the issues I saw in "binkw32.dll" were related to my handling of threads in the There are only 5 threads that start in "sh2pc.exe". Each of these threads start when sh2pc.exe starts and runs until sh2pc.exe exits. I think that most (or all) of these threads have issues. Here are the starting addresses of each of these threads: 0x00401000, 0x0055FAA0, 0x0055FAE0, 0x0055FB50, 0x00560190 Here is an updated testing build (without the extra logging): d3d8.zip |
Hi Elisha, Does this test build have a solution for the sound looping issue in it? What Gemini suggested doing? If so, I'll play it this weekend and try to break it. To test, I would disable the |
There's indeed 5 threads for the ADX code. I couldn't find anything in the calls related to 0x401000, as the main ADXT thread would be located at 0x560390 (adxwin_server_proc). All the other procedures seem to be correct. Glad that simple trick worked! |
Yes, this is using the suggestions from Gemini. I sent you an email with more details on how to test it.
That is the initial place that the game starts running code after loading all the static dlls. My initial tests showed that this thread needs to be set to single processor affinity, but now I am not sure. I asked John to do some more testing on this.
Yeah, all of the 4 ADXT threads use a very tiny amount of the CPU. So setting them all to the same CPU core should have very little, if any, impact on the performance of the game. So this build gets us about 99% of the way to full multi-core support. I would still like to solve the root issue, if possible, so we don't need to set the affinity at all. But it may not be very urgent if this completely solves the issue. |
The problem might stem from the fact that two of those threads don't use any synchrony barriers with something like WaitForSingleObject or Sleep to synchronize, for whatever reason. The faulty ones are: adxm_safe_proc (0x55FAA0) and adxm_mwidle_proc (0x55FB50). Here's the code for them: void __stdcall adxm_safe_proc(LPVOID lpThreadParameter)
{
for ( ; !adxm_safe_loop; ++adxm_safe_cnt ); // adding the sync code here could do the trick
adxm_safe_exit = 1;
ExitThread(999);
}
void __stdcall adxm_mwidle_proc(LPVOID lpThreadParameter)
{
while ( !adxm_mwidle_loop )
{
++adxm_mwidle_cnt;
if ( !SVM_ExecSvrMwIdle() || adxm_goto_border_flag == 1 )
{
if ( adxm_goto_border_flag == 1 )
{
adxm_goto_border_flag = 0;
SetThreadPriority(adxm_mwidle_thrdhn, nPriority);
}
// sleep callback, this is where the synchrony code can be added
// the parameter should be a delay in milliseconds
if ( adxm_mwidle_sleep_cb )
adxm_mwidle_sleep_cb(dword_1D81CFC);
SuspendThread(adxm_mwidle_thrdhn);
}
}
adxm_mwidle_exit = 1;
ExitThread(999);
} |
Sorry to say that I have sound issues on both test builds. d3d8_b1.7.1907_WithoutStartThread d3d8_b1.7.1907_WithStartThread For both, Mary's beginning dialogue ("In my restless dreams...") doesn't play some of the time. I found if I hang out in the beginning bathroom for a while before leaving is when her dialogue will not play. If I then quit the game and start a new game, James' beginning dialogue in the bathroom ("Mary, are you really in this town?") won't play. |
Could the dialogue issue I described relate to your recent changes to I also haven't gotten the sound loop bug again on d3d8_b1.7.1907_WithoutStartThread with these new tests while Edit: With these tests, the past two times I went to quit to the main menu the game instantly crashed and closed itself, though. |
Yes, that is very possible. In most of my tests I had this feature disabled to reduce the number of threads used and help isolate the problem ones.
That is correct. This build sets one of the threads (the starting thread) to a single processor which will make it take all the CPU on that processor.
Nice! I will need to look at this in more details to understand it. |
If you're overriding CreateThread where it's called (instead of actually overriding it globally) the CPU core loop bug should be fixed in a similar way and the thread affinity is limited only to the ADX library. I tested this a bit earlier and couldn't trigger the bug with my dirty solution. If I understand correctly how you're doing it now, detecting the current thread is probably way more intensive and not a definitive solution. Haven't checked if overriding the thread loops fixes the issue, but I guess that's even a better solution and keeps the multicore structure intact without forcing it on a single core. |
I see. Okay, until further notice, I'll test with it disabled. Do you want me to go ahead and do a full playtest with d3d8_b1.7.1907_WithoutStartThread? Otherwise I'll wait in case of upcoming, additional adjustments. |
Let's hold off for the time being. I want to see if we can override or patch the existing thread loops to fix the issue. Then we could completely remove the code to set single core affinity. |
I tried reworking the threads to have sleep timers or the vsync wait in them, but neither seems to be working at all. At this point I suspect it's neither adxm_mwidle_proc or adxm_safe_proc that are faulty. #define SYNC_TRICK ADXM_WaitVsync() // calls internally WaitForSingleObject(hEvent, 0xFFFFFFFF)
#define SYNC_TRICK Sleep(10)
void __stdcall adxm_safe_proc(LPVOID lpThreadParameter)
{
for (; !adxm_safe_loop; ++adxm_safe_cnt)
SYNC_TRICK;
adxm_safe_exit = 1;
ExitThread(999);
}
void __stdcall adxm_mwidle_proc(LPVOID lpThreadParameter)
{
while (!adxm_mwidle_loop)
{
++adxm_mwidle_cnt;
if (!SVM_ExecSvrMwIdle() || adxm_goto_border_flag == 1)
{
if (adxm_goto_border_flag == 1)
{
adxm_goto_border_flag = 0;
SetThreadPriority(adxm_mwidle_thrdhn, nPriority);
}
//
//if (adxm_mwidle_sleep_cb)
// adxm_mwidle_sleep_cb(dword_1D81CFC);
SuspendThread(adxm_mwidle_thrdhn);
}
SYNC_TRICK;
}
adxm_mwidle_exit = 1;
ExitThread(999);
} Neither SYNC_TRICK definition seems to make them multicore-safe at all, but it does 100% work when CreateThread overrides with the affinity mask, and according to my tests so far. I haven't checked if the wrapped CreateThread is called in the bink dll, but I think that one probably builds its own internal import at boot. |
I believe there is an issue with this thread also: 0x0055FAE0 It seems that this thread handles the voice audio. |
I have found the exact issue: it's ADXM_WaitVsync(), the function located at 0x55FFC0 that would supposedly wait for vertical sync in order to synchronize two audio threads (0x55FAE0 adxm_vsync_proc and 0x560190 ADXWIN_EnableFsThrd). These threads work fine if executed on the same core, while the other threads are pretty much irrelevant and produce no change whatsoever, so can be left as they currently are. This is the function on the XBox SDK: void ADXM_WaitVsync(void)
{
D3DDevice_BlockUntilVerticalBlank(); // does not exist on C
} Same function on PC: void __cdecl ADXM_WaitVsync()
{
WaitForSingleObject(hvsync, 0xFFFFFFFF);
} The thread associated to the PC code uses these snippets of code: // code found at 0x55FC50
void __stdcall vsync(UINT uTimerID, UINT uMsg, DWORD_PTR dwUser, DWORD_PTR dw1, DWORD_PTR dw2)
{
LONG v5; // ebp
unsigned int v6; // edi
unsigned int v7; // ebx
__int64 v8; // rax
unsigned __int64 v9; // rax
__int64 v10; // rax
unsigned __int64 v11; // rax
int v12; // esi
LARGE_INTEGER PerformanceCount; // [esp+10h] [ebp-8h] BYREF
QueryPerformanceCounter(&PerformanceCount);
v5 = PerformanceCount.HighPart;
v6 = HIDWORD(qword_24A4C70);
v7 = qword_24A4C70;
do
{
++qword_1D81D28;
v8 = sub_5703F0(qword_1D81D28, HIDWORD(qword_1D81D28), v7, v6);
v9 = sub_5703F0(v8, HIDWORD(v8), 100, 0);
LODWORD(v10) = sub_570310(v9, (unsigned int)dword_8BF948);
qword_1D81D20 = qword_24A4C50 + v10;
v11 = sub_5703F0(
qword_24A4C50 + v10 - PerformanceCount.LowPart,
(qword_24A4C50 + v10 - __PAIR64__(v5, PerformanceCount.LowPart)) >> 32,
1000,
0);
v12 = sub_570310(v11, __SPAIR64__(v6, v7));
}
while ( v12 <= 0 );
timeKillEvent(::uTimerID);
timeEndPeriod(1u);
if ( dword_1D81D18 )
{
dword_1D81D18 = 0;
}
else
{
timeBeginPeriod(1u);
::uTimerID = timeSetEvent(v12, 0, vsync, 0, 0);
}
PulseEvent(hvsync);
}
// how the event is created in ADXM_SetupThrd at 0x55FDB7
qword_1D81D28 = 0i64;
qword_24A4C50 = PerformanceCount.QuadPart;
hvsync = CreateEventA(0, 1, 0, 0);
if ( !dword_1D81D30 )
{
timeBeginPeriod(1u);
uTimerID = timeSetEvent(1u, 0, vsync, 0, 0);
} So they effectively tried to simulate vsync by using the performance timers, but something went bonkers. Strangely enough, making ADXM_WaitVsync() completely blank seems to cause no harm at all to the game and actually fixes the loops, but I suspect it runs uncapped, so potentially polling like crazy. A good trick here would be to rewrite vsync() in a more precise way to simulate its intended behavior, which is really nothing more than a limiter capped at 60 fps. |
Nice! wait on vsync is still supported on PCs, see D3DKMTWaitForVerticalBlankEvent. I will rewrite this and we should be good. Thanks! |
That could be an easy way out if it works on all systems and with the game in true fullscreen. I would suggest to nop the calls that set the timer and the unitializer. On this matter, I tried to cap the code with my own frame limiter but it was still behaving incorrectly for whatever reason. Hopefully that gdi function proves useful! |
Ok, I created a test build. This build replaces I may rewrite this later in case vsync is not available. However, it should be available on any modern computer. To test this you need to have Test build: d3d8.zip |
That test build still has the looping issue. I may have found a more generic solution that supposedly works all the time and doesn't require any extra dependencies. Here's a little class of mine used to limit frame rate in Classic REbirth: #define FRAMES_PER_SECOND (60)
#define TICKS_PER_FRAME (1)
#define TICKS_PER_SECOND (TICKS_PER_FRAME * FRAMES_PER_SECOND)
class FrameLimiter
{
public:
FrameLimiter() :
TIME_Frequency(0.),
TIME_Ticks(0.)
{ }
void Init()
DWORD Sync();
private:
double TIME_Frequency,
TIME_Ticks;
void Ticks();
};
void FrameLimiter::Init()
{
LARGE_INTEGER frequency;
QueryPerformanceFrequency(&frequency);
TIME_Frequency = (double)frequency.QuadPart / (double)TICKS_PER_SECOND;
Ticks();
}
void FrameLimiter::Ticks()
{
LARGE_INTEGER counter;
QueryPerformanceCounter(&counter);
TIME_Ticks = (double)counter.QuadPart / TIME_Frequency;
}
DWORD FrameLimiter::Sync()
{
DWORD lastTicks, currentTicks;
LARGE_INTEGER counter;
QueryPerformanceCounter(&counter);
lastTicks = (DWORD)TIME_Ticks;
TIME_Ticks = (double)counter.QuadPart / TIME_Frequency;
currentTicks = (DWORD)TIME_Ticks;
return (currentTicks > lastTicks) ? currentTicks - lastTicks : 0;
} Throw that class somewhere in the dll solution, then change the old synchrony code with this: void ADXM_WaitVsync()
{
while (!timer.Sync());
} Also make sure to null all the code from 0x55FDB0 to 0x55FE32 and from 0x56008C to 0x56008F in order to remove the old synchrony timer altogether. Call Init() somewhere in your code or move it to the constructor and you're pretty much done. Edit: nevermind. Even with the new procedure the thing gets out of sync sooner or later. I guess either another solution is found or affinity is pretty much the only easy way out. |
Did you try disabling |
Elisha, I just tested it (without |
If I nop from 0x55FDB0 to 0x55FE32 then the game will hang on exit, even if all I did was go to the main menu and then exit.
Using this sync methods cause the two threads to be 2ms our of sync. But when I use the actual vsync method the two threads are exactly on sync. I believe the threading issues are all happening with this thread: 0x00560190. If anything happens on this thread the game will hang on exit even if I disable the other three threads. |
One more testing build. This build overrides CreateThread in the game itself rather than hooking the CreateThread API. It should be more reliable and only effect the calls we want. It sets all five ADXM audio threads to the same processor core, though one of these threads never seems to be called by the game. The build also nops the code from 0x56008C to 0x56008F in order to remove the old synchrony timer. And this build updates And finally, I put a fix in for Here is the testing build: d3d8.zip |
New DLL seems to be working perfectly, tested the lake section for about 10 minutes and no looping issues occurred. On the issue related to the old timer hanging the game on shutdown, I totally forgot to pinpoint a sync loop (had the same issues while testing on my self contained DLL). These are the spots in the ADX functions to eradicate the old timer code completely: // ADXM_SetupThrd
memset((void*)0x55FDB0, 0x90, 0x55FE32 - 0x55FDB0); // nop creation of vsync timer event and performance queries
// this one might need a reset of adxm_vsync_cnt (int variable located 0x1D81CEC)
// this variable is only used in adxm_vsync_proc() as an incremental thread counter
// but it doesn't seem to do anything at all, it's probably safe even with the base BSS segment value
// adxm_destroy_thrd
memset((void*)0x55FFFC, 0x90, 0x56000B - 0x55FFFC); // nop infinite while waiting for vsync timer
memset((void*)0x56008C, 0x90, 0x56008F - 0x56008C); // nop SetEvent on vsync timer |
Nice! It looks like that might solve the issue altogether. We may not even need to hook CreateThread(). I created an updated build, one that sets thread affinity on these threads and one that does not. Test build that sets thread affinity: d3d8-WithAffinity.zip |
The build that sets affinity works fine and doesn't have the loop bug for the time that I tested, the one without affinity deadlocks whenever I try to boot the game via saves (does the load sound, no red fade effect, completely stuck). Wrapped CreateThread with affinity shouldn't really be a problem as long as you add a condition to detect when lpStartAddress is either 0x55FAE0 or 0x560190. Should keep anything else away from polluting the ADX threads; game seems to juggle a lot across cores according to my tests, while audio is stable on the core Windows decides to pick according to mask set as 1. |
No problem. I will keep the thread affinity code in there.
It is easy enough to just override CreateThread in the game itself. I would rather just keep it as is so I don't need to find and check these addresses, as the addresses are different on different binaries. However, I am making all 4 threads run on the same core. The way the code is done makes it hard to hook only the two threads, and hooking all four should not be an issue.
Edit: Apparently even if we are assigned to a different processor group we can still use a mask of 1 and it will put us on the first core that that group. Anyways, I am going to keep this as is because I don't see any reason to change it. I updated the build again. I am hoping that this will be the final test build. It works on all three binaries (v1.0, v1.1 and vDC). However, some of the functions are rearranged in v1.1 and vDC so I had to put some custom code for the different binaries. Unfortunately, this means that full testing will need to be done on each binary for this feature. It is not good enough to fully test one binary and then smoke test the others. All three binaries will need to be tested. One final note: I kept the old Here is the updated testing build: d3d8.zip |
I'm sorry to say that I'm still getting issues. Here is a video example: https://youtu.be/1KLQTD3rkSM The "game start" SFX wigs out. I wasn't able to capture it, but one time this SFX was looping indefinitely. Also, immediately upon starting a new game, the BGM should instantly start playing (as in, it should start when the FMV starts). In some of those examples in the video, it doesn't start playing until after the "game start" SFX has finished. This happened both with and without Here's another example. It seems the game is waiting for a SFX to end (such as a "door close" SFX) before it'll start playing the BGM in a new room. Also, the BGM will start looping... or "pulsating"... along with crackling/popping. You'll need your headphones to best hear it: https://youtu.be/qU45FbfoZZ4 |
You can change the MS to fade out using this option: I changed the default from 20 to 5. Maybe 20 is better? |
Oh, awesome. 10 ms looks to be the magic number then. Thanks! |
Great! You can update the default setting here, if you want. |
I can start testing right now then :) Let the marathon begin |
bug.mp4I don't know why but i played this cutscene twice and always encountered this bug.When i kill the lying figure cutscene stucks at there . Do i need to change something in the d3d8 file for this issue? |
I got some frozen cutscenes too (check out my notes above). I've learned that you should be able to press Esc on them to resume normal gameplay. I'm thinking maybe the game is waiting for the associated dialogue file to begin playing or something? This would need to be troubleshooted by Gemini. |
Ah that's true i know what causes this bug, do you remember what i was said on this ticket #506 (comment) i said there was a memory address that prepares memory control buffer for adx, if game prepared buffer but you nop the "play voice.afs" function, that causes this frozen cutscene issue. As you said right now i think there is something wrong with the adx's play dialogue function. |
The multithreaded version of Criware for performance is operative; I left a pull request as usual. Tested this live for over 3 hours, played through main game and born from a wish. Nothing funny to report, just a subtitle not disappearing in the attic in Maria's scenario (same bug as Eddie's fake cutscene in bowling alley, I'll look into it later) and some random stutter at the hospital boss, which was probably OBS anyway. No crashing or other strange behavior on my side. Compiled binary for testing: d3d8.zip |
@AeroWidescreen @TheMachineAmbassador @nipkownix
If you guys can test this build that would be great! Remember to disable single core affinity ( I'll be testing this myself as well. Thanks for your testing/help here! |
Well, i made some tests right now, it seems that issue i mentioned above is fixed but while in cutscenes when game momentarily freezes there is some stuttering happens on voice. I guess it's not so noticable or maybe i have to disable some settings but for now game looks smooth and never did get any crashes or something like that. |
@Polymega
|
Those all are good. Are you sure |
Strange, it's working now and I can't make it happen again. Anyway, it doesn't matter. I only needed to confirm what the correct settings were. I'll have OBS going the entire time to capture anything fishy from this point forward, which there should be plenty of opportunity for. Thanks. |
Module Version: 1.8.1941.0 AudioClipDetection = 1 I completed the game from start to finish. No crashes. No audio loops. No desyncs. Nothing. I didn't skip any cutscenes, and I allowed some of the BGMs to play for an extended period of time. I exited the game multiple times, loaded my saves multiple times... you get the point. Everything was solid. |
@AeroWidescreen psst - just curious, did you use the audio enhancements or dsoal, or just vanilla audio? Don't mind me, just following along. |
I've finished tests as well i think it looks fine for me too,. Finished game twice and i didn't get any crashes or desync as Aero said. |
Thank you guys for testing! Went through the whole game as well and have no serious issues to report. |
@umbrellacorp53 |
Nice! Let me know if you want me to merge the pull request. We can always make changes to the code after it is merged. |
Well... I dropped that d3d8.dll into my Steam Deck install and added the ini changes. Frickin works. I'm up to the apartments. Only issue so far was one time immediately after a doorway transition the first enemy footstep had no reverb. Like dsoal took a second to kick in. Could be a Steam Deck specific bug. Otherwise, it just seems smoother. Amazing! |
I spoke with Gemini and he said the PR is good to be merged. :) |
I merged the criware changes and updated the code accordingly. Below is a testing build with all the latest changes in it. A few comments below:
For this build's main testing you should not need to worry about changing any settings. Of course you can change any setting you want, but it should have all the desirable settings set correctly by default. New testing build: d3d8.zip |
Here is the new build with the latest fixes from Gemini. It has New testing build: d3d8.zip |
Thank you, Elisha! I noticed the setting for this is no longer included in the INI file. Also, we'll need to make a mental note to possibly change this feature to do the following. Either:
|
Agreed. Once we get this implementation nailed down we can make these changes to it. I recommend we do the middle option. Once debugging options are added I don't like to remove them in case they are needed later. |
We should be good with Criware. Pushed a merge request. |
Ladies and gents, I think we are good to close this ticket out. What was a wild dream for all of us for 20 years is now a reality thanks to the wonderful, brilliant work by Gemini here. Gemini... you are the stuff of legends! Thank you with all sincerity and appreciation in my heart. Closing this ticket out is truly a wonderous feeling. 😊 |
@Gemini-Loboto3 @MeganGrass
Thank you both for taking an interest in this.
To give context to the notes below: Elisha and I were discussing via email the ability to restore the game save/game load SFX when selecting it through the pause menu. Being an audio-related thing, Elisha got to digging around with how the game handles/processes audio calls.
His notes may be helpful in trying to fix the audio issues to allow the game to run stable on multiple cores. Note that some of his talking points relate to the ticket I linked above, to give context to those parts of the discussion. (Specifically, when he talks about dialogue/voice files, it's in relation to that ticket.)
Also note that any addresses discussed are from the North American 1.0 executable for the game. And a side effect of the game not being able to run on a single core is that it always uses 100% of the core it has access to, which Elisha discusses more about below.
Elisha's Audio Notes
There is a lot of code responsible for playing voice files because of the way they are packaged in an asf file (\data\sound\adx\voice\voice.asf).
I did some debugging to see how the game handles SFX vs. BGM vs. Voice samples. I found out some interesting things. First of all it, I was able to create code to detect (with pretty good accuracy) when the game is running each type of sample. The game seems to keep 10 DirectSound buffers cached in memory at all times and rotates through the buffers as needed. This means that the game can likely only play up to 10 sound samples at once.
I was also able to find a way to detect when the game was using a voice sample via address 0x00B15140 (4-byte). This address will equal the value 0x46464952 when it is playing a voice sample.
Pretty much all the game code between address 0x0055BE8E and 0x0056F009 is responsible for playing voice files. It looks like this code is used to open and read the asf file data.
A function at address 0x0056EFF0 seems to be called once and only once each time a voice sample is played. This small function is responsible for checking the header of the audio file in the voice.asf package to ensure that it is a supported audio file.
I did try to force the game to keep the Game Load sound playing after loading a new save while in-game. However, when I did that it just caused the sound to stutter and repeat itself exactly like it does when you have SingleCoreAffinity disabled. I don’t think we will be able to fix this in DirectX, but will need to use the game’s code to fix this.
I think the reason the Load Game sound is played fully when loading from the main menu is because the there is no other sounds playing except that sound so there is nothing else to stop. However, when loading a save from in-game there could be all kinds of sounds playing (including continuous sounds like the BGM). Since the game has 10 cached buffers and does not know which buffer is playing what it just tells Windows to stop all buffers that are currently playing.
BTW: I think the issue with the SingleCoreAffinity looping has something to do with the 10 cached buffers. These buffers are accessed by different threads and I think the different threads get out of sync accessing these buffers and cause the sound issues we see.
I found out why the audio loops when SingleCoreAffinity is disabled. The game tell DirectSound to play a sound and loop (DSBPLAY_LOOPING) giving it around 200ms of buffer. Then right before the buffer is exhausted the game will update/append to the buffer new cache data for the next 200ms of audio. It continues this process until the audio is done playing. Then the game stops the audio. However, the looping sound issue happens when the game, for some reason, either no longer updates the cache for this buffer or fails to stop the audio for this buffer. In this case it just keeps looping the same 200ms of cache the game previously gave it.
This is also why I cannot fix the load game sound because even if I prevent the game from stopping the sound it will just cause it to repeat the same 200ms of audio, rather than all the audio. To fix the load game sound we need to find out what part of the code the game uses to stop all the buffers and bypass it so that the game continues to feed all of the buffers sound. Then I can manually stop all sound buffers except the load game sound, similar to what I did with the game ending. Or else we need to trick the game into feeding all the data into the buffer rather than just 200ms of data. More about that below…
This code that continuously feeds DirectSound 200ms of data to the buffer is also what is causing one of the threads to use 100% CPU time (there are two threads using 100% CPU each). The game’s thread continuously loops and on each loop checks to see if any of the buffers need more cached data. It needs to give the buffer data at just the correct time otherwise the sound will either skip (like what is happening in the issue on GitHub) or repeat itself (like what happens when SingleCoreAffinity is disabled).
Another idea I had here is that it is likely that the game likely only feeds around 200ms because when this game was created memory was small and there was not enough memory to feed more data into the buffer. Today we have lots of memory and if we can find out where the game injects data into the buffer maybe we can get the game to inject larger chunks of data into the buffer so that it does not need to loop so quickly. In fact, if the data is small we could just have the game feed all the data into the buffer and then not have it loop at all. This would eliminate a bunch of audio issues and free up a lot of CPU time for the code that handles graphics.
The text was updated successfully, but these errors were encountered: