[PATCH v11 0/1] MR9840: winepulse.drv: A potential solution to audio crackling
I have noticed that winepulse leaves too little headroom when draining the buffer, which very often leads to crackling that can't be fully resolved by manipulating latency. Let's check some math: Let's assume that the buffer (tlength) is at 33ms, and minreq is at 13.3ms. If 13.2 ms were drained, the refill would be denied. If the next timer trigger takes 14ms due to jitter, that leaves us with a 6.1ms margin. As such, if we get any jitter longer than that during refill, we get an underflow. I think this is what leads to crackles. My solution increases timer polling to 5x the frequency, so if 13.2ms were drained, the next timer trigger would happen at around 16ms. That would leave us with a 17ms headroom. The math might not be 100% representation of what happens, but I think it's quite close to reality. On practice, this does indeed help to fully get rid of crackling for an indefinite amount of time, when before they were unevitable. I think this problem is quite urgent, because while this does not happen on every system, it's frequent enough that we see "Audio: Crackling" reports on most games on protondb and there are many forum threads about audio crackling. From my experience, the only solution was switching to winealsa, which is very consistent and does not crackle at all. My experience has been very consistent with forum threads when it came to the nature of crackling. I first noticed it in Balatro, which is a very light game and should not be crackling at all on any setup. Some games have very frequent crackling every 20-30 seconds, or buzzsaw crackling every 10-15 minutes. I hope my idea brings us closer to the ultimate fix, I really look forward to your feedback! -- v11: winepulse.drv: Increase timer frequency https://gitlab.winehq.org/wine/wine/-/merge_requests/9840
From: Dzmitry Keremsha <vyro@lumencoil.com> --- dlls/winepulse.drv/pulse.c | 54 +++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/dlls/winepulse.drv/pulse.c b/dlls/winepulse.drv/pulse.c index 7c8d872c604..72ba67aab19 100644 --- a/dlls/winepulse.drv/pulse.c +++ b/dlls/winepulse.drv/pulse.c @@ -85,6 +85,7 @@ struct pulse_stream struct list packet_free_head; struct list packet_filled_head; + SIZE_T rem_samples; }; typedef struct _ACPacket @@ -1180,7 +1181,7 @@ static NTSTATUS pulse_create_stream(void *args) stream->bufsize_frames = ceil((params->duration / 10000000.) * params->fmt->nSamplesPerSec); bufsize_bytes = stream->bufsize_frames * pa_frame_size(&stream->ss); - stream->mmdev_period_usec = params->period / 10; + stream->mmdev_period_usec = params->period / 50; stream->share = params->share; stream->flags = params->flags; @@ -1572,9 +1573,8 @@ static NTSTATUS pulse_timer_loop(void *args) struct pulse_stream *stream = handle_get_stream(params->stream); LARGE_INTEGER delay; pa_usec_t last_time; - UINT32 adv_bytes; + UINT32 adv_bytes=0; int success; - pulse_lock(); delay.QuadPart = -stream->mmdev_period_usec * 10; pa_stream_get_time(stream->stream, &last_time); @@ -1612,7 +1612,7 @@ static NTSTATUS pulse_timer_loop(void *args) if (diff > stream->mmdev_period_usec) { stream->just_started = FALSE; - last_time = now; + last_time = now; } } else @@ -1631,20 +1631,43 @@ static NTSTATUS pulse_timer_loop(void *args) last_time += stream->mmdev_period_usec; } - if (stream->dataflow == eRender) + if (stream->dataflow == eRender) { + SIZE_T frame_size = pa_frame_size(&stream->ss); + UINT64 total_samples; + SIZE_T safe_bytes; + SIZE_T limit; + + pulse_write(stream); - /* regardless of what PA does, advance one period */ - adv_bytes = min(stream->period_bytes, stream->held_bytes); - stream->lcl_offs_bytes += adv_bytes; - stream->lcl_offs_bytes %= stream->real_bufsize_bytes; - stream->held_bytes -= adv_bytes; + total_samples = stream->mmdev_period_usec * stream->ss.rate + stream->rem_samples; + adv_bytes = (total_samples / 1000000) * frame_size; + stream->rem_samples = total_samples % 1000000; + + if (adv_bytes > stream->held_bytes) + { + adv_bytes = stream->held_bytes; + stream->rem_samples = 0; + } + + safe_bytes = adv_bytes; + if (stream->held_bytes > stream->pa_held_bytes) + { + limit = stream->held_bytes - stream->pa_held_bytes; + if (safe_bytes > limit) + safe_bytes = limit; + } + + stream->lcl_offs_bytes = (stream->lcl_offs_bytes + safe_bytes) % stream->real_bufsize_bytes; + stream->held_bytes -= safe_bytes; } + else if(stream->dataflow == eCapture) { pulse_read(stream); } + } else { @@ -1653,8 +1676,15 @@ static NTSTATUS pulse_timer_loop(void *args) } } - if (stream->event) - NtSetEvent(stream->event, NULL); + if (stream->event) + { + if (stream->dataflow == eCapture || + stream->held_bytes == 0 || + ((stream->held_bytes + adv_bytes) / stream->period_bytes != stream->held_bytes / stream->period_bytes)) + { + NtSetEvent(stream->event, NULL); + } + } TRACE("%p after update, adv usec: %d, held: %u, delay usec: %u\n", stream, (int)adv_usec, -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9840
Paul Gofman (@gofman) commented about dlls/winepulse.drv/pulse.c:
stream->bufsize_frames = ceil((params->duration / 10000000.) * params->fmt->nSamplesPerSec); bufsize_bytes = stream->bufsize_frames * pa_frame_size(&stream->ss); - stream->mmdev_period_usec = params->period / 10; + stream->mmdev_period_usec = params->period / 50;
I am afraid the period can't be arbitrary changed like that, that is exposed to applications and changing it this way will break apps' expectations. I didn't look at the patch at full though. Just FYI, there is also https://gitlab.winehq.org/wine/wine/-/merge_requests/8628 which is largely orthogonal to your changes and didn't get any attention yet, but I believe that is something needed to be done and introducing arbitrary timer adjustments would be unfortunate. Overall, it could probably help if you described the conditions / steps how the actual problem can be reproduced. As it is not like we observe audio crackling with winepulse anywhere often, that is probably something winepulse setup specific or happens with specific app? What if we should approach this entirely differently. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9840#note_126568
On Thu Jan 8 21:47:26 2026 +0000, Paul Gofman wrote:
I am afraid the period can't be arbitrary changed like that, that is exposed to applications and changing it this way will break apps' expectations. I didn't look at the patch at full though. Just FYI, there is also https://gitlab.winehq.org/wine/wine/-/merge_requests/8628 which is largely orthogonal to your changes and didn't get any attention yet, but I believe that is something needed to be done and introducing arbitrary timer adjustments would be unfortunate. Overall, it could probably help if you described the conditions / steps how the actual problem can be reproduced. As it is not like we observe audio crackling with winepulse anywhere often, that is probably something winepulse setup specific or happens with specific app? What if we should approach this entirely differently. It took a gargantuan amount of effort, but I believe my logic is looking pretty good right now. Programs are not overwhelmed with requests for audio, I did consider this scenario. Audio tests seem to pass (winmm:mci failed for the first time, not sure if it's related at all?) and audio is consistently good in games. Of course, I might be missing something.
Unfortunately, I don't believe it is matter of config, I am running Fedora KDE 43 with pipewire and RT audio permissions. It's nothing too far from the default, my CPU just seems to be quite jittery and that causes crackling. Honkai: Star Rail is the worst game in terms of crackling by far, but crackling happens in every single game with winepulse, and there is no crackling at all with winealsa. Honkai is forcing 44.1k sample rate, so it gives audio in 441 chunks and it's consumed in chunks of 471 (with 48k quant at 512). I don't quite understand why Honkai specifically acts like this, but the crackling happens every 20-30 seconds with 512 quant and it's actually better with 256. Maybe something with tlength to minreq scenario? with winepulse it varies between 2.5:1-3.5:1 depending on quant. If I am not mistaken, winealsa is always 4:1 if used with alsa pulse plugin (standard with Proton, as Steam runtime doesn't have pipewire libraries yet) The issue is definitely not exclusive to my setup, because I have seen mentions of crackling of similar nature when it came to Honkai. Although, setting quant to 256 might have helped that person (they reported it worked, but crackles can appear 20-30 minutes into the session). There is a mention of Balatro crackling on r/linux_gaming as well, and I believe it is definitely related to CPU jitter as well, because I can't think of any other explanation. In my case, every single game crackles under winepulse, even with quant set at 1024-2048. Games never crackle with winealsa on my setup. So far, with my patch even Honkai is perfect, though I can't quite get quant as low as with winealsa. I can go down to 220 at 48k with winealsa and 240 crackles with my patched winepulse (512 is fine). I don't like that I use an arbitrary number either, but I believe the root of the problem is insufficient safety margin and it needs to be addressed in some way. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9840#note_126570
On Thu Jan 8 22:23:06 2026 +0000, Dzmitry Keremsha wrote:
It took a gargantuan amount of effort, but I believe my logic is looking pretty good right now. Programs are not overwhelmed with requests for audio, I did consider this scenario. Audio tests seem to pass (winmm:mci failed for the first time, not sure if it's related at all?) and audio is consistently good in games. Of course, I might be missing something. Unfortunately, I don't believe it is a matter of config, I am running Fedora KDE 43 with pipewire and RT audio permissions. It's nothing too far from the default, my CPU just seems to be quite jittery and that causes crackling. Honkai: Star Rail is the worst game in terms of crackling by far, but crackling happens in every single game with winepulse, and there is no crackling at all with winealsa. Honkai is forcing 44.1k sample rate, so it gives audio in 441 chunks and it's consumed in chunks of 471 (with 48k quant at 512). I don't quite understand why Honkai specifically acts like this, but the crackling happens every 20-30 seconds with 512 quant and it's actually better with 256. Maybe something with tlength to minreq scenario? with winepulse it varies between 2.5:1-3.5:1 depending on quant. If I am not mistaken, winealsa is always 4:1 if used with alsa pulse plugin (standard with Proton, as Steam runtime doesn't have pipewire libraries yet) The issue is definitely not exclusive to my setup, because I have seen mentions of crackling of similar nature when it came to Honkai. Although, setting quant to 256 might have helped that person (they reported it worked, but crackles can appear 20-30 minutes into the session). There is a mention of Balatro crackling on r/linux_gaming as well, and I believe it is definitely related to CPU jitter as well, because I can't think of any other explanation. In my case, every single game crackles under winepulse, even with quant set at 1024-2048. Games never crackle with winealsa on my setup. So far, with my patch even Honkai is perfect, though I can't quite get quant as low as with winealsa. I can go down to 220 at 48k with winealsa and 240 crackles with my patched winepulse (512 is fine). I don't like that I use an arbitrary number either, but I believe the root of the problem is insufficient safety margin and it needs to be addressed in some way. Perhaps the first steps are figure out what's special with the setup if *every* game is crackling with winepulse.drv, as that definitely doesn't happen universally.
Then, if there is specific problem with Honkai it would be interesting to debug what is special about it, starting from which audio paths it is using, which periods... maybe the problem is somewhere else. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9840#note_126571
On Thu Jan 8 22:24:58 2026 +0000, Paul Gofman wrote:
Perhaps the first steps are figure out what's special with the setup if *every* game is crackling with winepulse.drv, as that definitely doesn't happen universally. Then, if there is specific problem with Honkai it would be interesting to debug what is special about it, starting from which audio paths it is using, which periods... maybe the problem is somewhere else. Honkai seems to refill 441 frames every tick, while draining 470 (512 quant at 48k, the game is forced 44.1k). So that's a 29 frame bleed every tick, therefore crackling is mathematically guaranteed. My logic makes NtSetEvent dependent on held_bytes level, so it refills properly.
I truly don't believe the general issue of winepulse crackling is limited to my setup. For instance, there are numerous reports of occasional static buzzing sound in Arc Raiders on protondb, with advice to switch to winealsa because it resolves the issue. I personally experienced that problem with original winepulse, and it's gone with both winealsa and my fix. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9840#note_127754
On Fri Jan 23 04:09:39 2026 +0000, Dzmitry Keremsha wrote:
Honkai seems to refill 441 frames every tick, while draining 470 (512 quant at 48k, the game is forced 44.1k). So that's a 29 frame bleed every tick, therefore crackling is mathematically guaranteed. My logic makes NtSetEvent dependent on held_bytes level, so it refills properly. EDIT: Sorry, this was an incorrect assumption and the difference is managed properly. The problem is indeed CPU stall sensitivity as in all other cases. Logs show last successful tick at the time 76702276705, and the next tick after an at the time 76702291787. The difference of 15082 microseconds or 15ms. So this is exactly the problem I was describing. I truly don't believe the general issue of winepulse crackling is limited to my setup. For instance, there are numerous reports of occasional static buzzing sound in Arc Raiders on protondb, with advice to switch to winealsa because it resolves the issue. I personally experienced that problem with the original winepulse, and it's gone with both winealsa and my fix. Thing is, the fix looks wrong, the period of audio ticks is constant on Windows (I presume the game works without crackling there?) and adjusting it dynamically, while maybe helps this specific game for some reasons, doesn't look right.
It needs to be understood what is the actual problem and why exactly the application underfills it. The very first thing is to understand which audio API the game is even using, is that mmdevapi directly, or maybe xaudio, or dsound? The problem might happen to be entirely not in [wine]pulse or mmdevapi in theory. Could you maybe attach the full (compressed) log recorded with WINEDEBUG=+pid,+loaddll,+timestamp,+seh,+debugstr,+threadname,+dsound,+dsound3d,+xaudio2,+mmdevapi,+pulse ? That will at least clearly show what API is being used and what if there is something special in how the audio is initialized. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9840#note_127781
On Fri Jan 23 16:46:22 2026 +0000, Paul Gofman wrote:
Thing is, the fix looks wrong, the period of audio ticks is constant on Windows (I presume the game works without crackling there?) and adjusting it dynamically, while maybe helps this specific game for some reasons, doesn't look right. It needs to be understood what is the actual problem and why exactly the application underfills it. The very first thing is to understand which audio API the game is even using, is that mmdevapi directly, or maybe xaudio, or dsound? The problem might happen to be entirely not in [wine]pulse or mmdevapi in theory. Could you maybe attach the full (compressed) log recorded with WINEDEBUG=+pid,+loaddll,+timestamp,+seh,+debugstr,+threadname,+dsound,+dsound3d,+xaudio2,+mmdevapi,+pulse ? That will at least clearly show what API is being used and what if there is something special in how the audio is initialized. We are not free to adjust app-visible state (e. g., we set app's event), that is something which should match Windows behaviour (fwiw, that is not exactly perfect now and that what I am trying to improve in the MR linked above, but in any case we shouldn't be making it to diverse more from Windows). One thing I wouid like to see in the log is how MM device is set up. Specified 'minreq' of 13.3ms is suspicious, minreq is based on mmdevapi 'period' and normally with shared device we should always end up with 10ms period (if pulse setup stipulates more period that setup is essentially broken for us, we can't really work that around and we have to maintain that 10ms to be compatible and avoid issues). Yet I can imagine maybe we can have some problem with those period rounding somewhere and maybe we should be fixing that one.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/9840#note_127782
On Fri Jan 23 17:12:29 2026 +0000, Paul Gofman wrote:
We are not free to adjust app-visible state (e. g., we set app's event), that is something which should match Windows behaviour (fwiw, that is not exactly perfect now and that what I am trying to improve in the MR linked above, but in any case we shouldn't be making it to diverse more from Windows). One thing I wouid like to see in the log is how MM device is set up. Specified 'minreq' of 13.3ms is suspicious, minreq is based on mmdevapi 'period' and normally with shared device we should always end up with 10ms period (if pulse setup stipulates more period that setup is essentially broken for us, we can't really work that around and we have to maintain that 10ms to be compatible and avoid issues). Yet I can imagine maybe we can have some problem with those period rounding somewhere and maybe we should be fixing that one. From the existing info I can maybe suspect that minreq which we get from pulse is configured to that mentioned 13.3ms, and in that case lowering it should probably help. In that case there is a question if we want to try to workaround such setups or just suggest to lower it (fwiw I am not sure if there are actual reasons to configure pulse latency that big). If we'd want to workaround that (not exactly sure if we want) I think it can only be correctly done by decoupling app visible buffer draining (the only one which we have now) from actual draining it to pulse. That is, introduce another (bigger) buffer and only copy to that one in the current winepulse loop. Then, have a separate loop which be pushing the data from that backing buffer to pulse and now that loop will be free to adjust timing of those pushes as it is best fit for PA config.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/9840#note_127784
On Fri Jan 23 17:30:43 2026 +0000, Paul Gofman wrote:
From the existing info I can maybe suspect that minreq which we get from pulse is configured to that mentioned 13.3ms, and in that case lowering it should probably help. In that case there is a question if we want to try to workaround such setups or just suggest to lower it (fwiw I am not sure if there are actual reasons to configure pulse latency that big). If we'd want to workaround that (not exactly sure if we want) I think it can only be correctly done by decoupling app visible buffer draining (the only one which we have now) from actual draining it to pulse. That is, introduce another (bigger) buffer and only copy to that one in the current winepulse loop. Then, have a separate loop which be pushing the data from that backing buffer to pulse and now that loop will be free to adjust timing of those pushes as it is best fit for PA config. Honkai doesn't crackle on Windows, nor does it crackle with winealsa. It's not an underfill, but I don't entirely understand how it refills properly.
Regarding app's event - as you can see I have a logic that tries to make it fire every period (I think we only expose padding and NtSetEvent to the app? I honestly don't know if there is something else), but because of the safe bytes offset it becomes inconsistent. Unfortunately I have to agree this is not the way, I tried making it so NtSetEvent triggers strictly once per period, but I keep having issues with the write pointer overwriting the read pointer. Actually I think it might be happening with the original winepulse - I haven't tested it yet, but in games like Arc Raiders with buzz issues it does sound very much like my pointer overlap problems, so these might not be underruns. If you don't mind answering, what exactly happens when we trigger NtSetEvent late? For instance, instead of triggering NtSetEvent at T=10ms, we trigger it at T=12ms and get a 10ms refill. Physics run for the entire 12ms, so where do the 2ms go? All the data is fit into 10ms worth of samples, or are the last 2ms discarded? I am asking because maybe my idea could at least be wrapped in an environment variable and added as a band aid fix in downstream protons. Inconsistent NtSetEvent surprisingly doesn't affect even spatial audio games, so if no audio is discarded it could be of some use at least. [steam-13203515507082264576.tar.gz](/uploads/f6f8c24d926ab9622e5611f726593a05/steam-13203515507082264576.tar.gz) -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9840#note_127786
On Fri Jan 23 17:34:40 2026 +0000, Dzmitry Keremsha wrote:
Honkai doesn't crackle on Windows, nor does it crackle with winealsa. It's not an underfill, but I don't entirely understand how it refills properly. Regarding app's event - as you can see I have a logic that tries to make it fire every period (I think we only expose padding and NtSetEvent to the app? I honestly don't know if there is something else), but because of the safe bytes offset it becomes inconsistent. Unfortunately I have to agree this is not the way, I tried making it so NtSetEvent triggers strictly once per period, but I keep having issues with the write pointer overwriting the read pointer. Actually I think it might be happening with the original winepulse - I haven't tested it yet, but in games like Arc Raiders with buzz issues it does sound very much like my pointer overlap problems, so these might not be underruns. If you don't mind answering, what exactly happens when we trigger NtSetEvent late? For instance, instead of triggering NtSetEvent at T=10ms, we trigger it at T=12ms and get a 10ms refill. Physics run for the entire 12ms, so where do the 2ms go? All the data is fit into 10ms worth of samples, or are the last 2ms discarded? I am asking because maybe my idea could at least be wrapped in an environment variable and added as a band aid fix in downstream protons. Inconsistent NtSetEvent surprisingly doesn't affect even spatial audio games, so if no audio is discarded it could be of some use at least. [steam-13203515507082264576.tar.gz](/uploads/f6f8c24d926ab9622e5611f726593a05/steam-13203515507082264576.tar.gz) Minreq is equal to period_bytes, which is equal to the quant, while a game's refill is whatever the engine decides. Tlength to minreq ratio is 3.5-2.5:1, and even hardcoding higher, I couldn't change it. It probably has something to do with how pipewire-pulse sets it. Winealsa is 4:1 though.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/9840#note_127787
First of all, you are using Proton-GE which is a downstream fork of Proton, and there are things which work quite different. I personally wouldn't mind to look at logs from official Proton Experimental, but it is a bit difficult for GE because I am not tracking what else could be there. Mind that Proton (and Proton GE from which this log is too) has the MR I was referring to above, so these things can work a bit different. Can you check with upstream Wine if the issue is the same? Or, with official Proton with commit "winepulse.drv: Process streams timer updates from PA main loop." reverted? What if that commit changes things here in certain way.
If you don't mind answering, what exactly happens when we trigger NtSetEvent late?
That highly depends on the game, but a lot depends on the timing going accurate (to the point they may have fixed hardcoded 10ms buffer lengths and to just crash or refuse to fill more). Besides event timing, the effect of how we drain the buffer is seen, e. g., through _GetCurrentPadding. See description in https://gitlab.winehq.org/wine/wine/-/merge_requests/8628 for one (not the most obvious) example how those may affect things.
Minreq is equal to period_bytes, which is equal to the quant, while a game's refill is whatever the engine decides. Tlength to minreq ratio is 3.5-2.5:1, and even hardcoding higher, I couldn't change it. It probably has something to do with how pipewire-pulse sets it. Winealsa is 4:1 though.
That is controlable through pulse config (if pipewire is used, pipewire-pulse.conf global on inside /home/<user>/... config if present) or /usr/share/, or original pulse config files if pipewire is not used).
From the log, I see that advertised minreq is actually 10666mcs. I guess lowering it in config to something below 10ms might happen to help, it is unfortunate to have anything bigger than 10ms there. Although maybe it doesn't explain it all in this case.
From the log I also see that the game is using xaudio, those mmdevapi calls and buffer refills are most likely done by it. And it fills maximum 441 samples each time which (with samples per sec of 44100 as I see in the log) is almost exactly 10ms. So I guess it is at least clear what's likely going on, xaudio is firmly bound to 10ms periods (it minds _GetCurrentPadding() number of samples in a sense it won't output more than there is available buffer space but also won't output more it looks like). Our mmdevapi period is 10.6ms (stipulated by pulse setup), so that is doomed to underrun at some moment as you describe. So I think making sure that our period is 10ms should be helping (can be checked by lowering pulse latency in setup), this way the buffer will be filled in full. That is by the way an example how things may depend on accurate timing matching Windows (even though in this case it is our xaudio, the apps may be doing the same).
So, besides adjusting setup so we are compatible, I think there are two directions in principle which could be considered (not sure offhand about any if we want to pursue that): - Working around PA setup in winepulse.drv between the lines of what I suggested above (leaving the current loop timings alone but introducing extra backing buffer and a separate loop to push data to PA). I think this is technically feasible but also don't know if we want to complicate things like that and introduce extra buffers (probably depends on whether those PA configs with >10ms latency are really needed or maybe can be changed instead); - Doing something in FAudio to fill more data if the minimum period it gets from mmedvapi suggests so). But I don't think it can be reasonably done there. Those 10ms in xaudio / FAudio are not arbitrary at all too. Back then it was just following backend period recommendation (and could end up with app visible buffer >10ms), that resulted in some games outright crashing because they were just hardcoding 10ms buffer length at allocation and didn't consider what xaudio tells them about actual lengths. So it works with 10ms quantums and probably simply doesn't have more data to pass once it sees that more padding is available. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9840#note_127788
Maybe it could use a bit of actual debug through FAudio, what if there is something actually fixable with timing / buffer refill. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9840#note_127789
On Sat Jan 24 05:44:39 2026 +0000, Paul Gofman wrote: > First of all, you are using Proton-GE which is a downstream fork of > Proton, and there are things which work quite different. I personally > wouldn't mind to look at logs from official Proton Experimental, but it > is a bit difficult for GE because I am not tracking what else could be > there. Mind that Proton (and Proton GE from which this log is too) has > the MR I was referring to above, so these things can work a bit > different. Can you check with upstream Wine if the issue is the same? > Or, with official Proton with commit "winepulse.drv: Process streams > timer updates from PA main loop." reverted? What if that commit changes > things here in certain way. > > If you don't mind answering, what exactly happens when we trigger > NtSetEvent late? > That highly depends on the game, but a lot depends on the timing going > accurate (to the point they may have fixed hardcoded 10ms buffer lengths > and to just crash or refuse to fill more). Besides event timing, the > effect of how we drain the buffer is seen, e. g., through > _GetCurrentPadding. See description in > https://gitlab.winehq.org/wine/wine/-/merge_requests/8628 for one (not > the most obvious) example how those may affect things. > > Minreq is equal to period_bytes, which is equal to the quant, while a > game's refill is whatever the engine decides. Tlength to minreq ratio is > 3.5-2.5:1, and even hardcoding higher, I couldn't change it. It probably > has something to do with how pipewire-pulse sets it. Winealsa is 4:1 though. > That is controlable through pulse config (if pipewire is used, > pipewire-pulse.conf global on inside /home/<user>/... config if present) > or /usr/share/, or original pulse config files if pipewire is not used). > From the log, I see that advertised minreq is actually 10666mcs. I guess > lowering it in config to something below 10ms might happen to help, it > is unfortunate to have anything bigger than 10ms there. Although maybe > it doesn't explain it all in this case. > From the log I also see that the game is using xaudio, those mmdevapi > calls and buffer refills are most likely done by it. And it fills > maximum 441 samples each time which (with samples per sec of 44100 as I > see in the log) is almost exactly 10ms. So I guess it is at least clear > what's likely going on, xaudio is firmly bound to 10ms periods (it minds > _GetCurrentPadding() number of samples in a sense it won't output more > than there is available buffer space but also won't output more it looks > like). Our mmdevapi period is 10.6ms (stipulated by pulse setup), so > that is doomed to underrun at some moment as you describe. So I think > making sure that our period is 10ms should be helping (can be checked by > lowering pulse latency in setup), this way the buffer will be filled in > full. That is by the way an example how things may depend on accurate > timing matching Windows (even though in this case it is our xaudio, the > apps may be doing the same). > So, besides adjusting setup so we are compatible, I think there are two > directions in principle which could be considered (not sure offhand > about any if we want to pursue that): > - Working around PA setup in winepulse.drv between the lines of what I > suggested above (leaving the current loop timings alone but introducing > extra backing buffer and a separate loop to push data to PA). I think > this is technically feasible but also don't know if we want to > complicate things like that and introduce extra buffers (probably > depends on whether those PA configs with >10ms latency are really needed > or maybe can be changed instead); > - Doing something in FAudio to fill more data if the minimum period it > gets from mmedvapi suggests so). But I don't think it can be reasonably > done there. Those 10ms in xaudio / FAudio are not arbitrary at all too. > Back then it was just following backend period recommendation (and could > end up with app visible buffer >10ms), that resulted in some games > outright crashing because they were just hardcoding 10ms buffer length > at allocation and didn't consider what xaudio tells them about actual > lengths. So it works with 10ms quantums and probably simply doesn't have > more data to pass once it sees that more padding is available. The game does not underfill, I added a log that shows held_bytes, pa_held and adv_bytes. Note that I added it right after adv_bytes calculation. The point is, held_bytes never dips to zero, and adv_bytes isn't breaking. I genuinely do not understand how that makes sense though. I also tested with the commit reversed, also crackles. Changing quant used to help (I'd get audio issues only maybe 15-20 minutes in), but it still wouldn't solve all issues. Attaching both logs. I tested Arc Raiders and Zenless Zone Zero. I think my suspicions about write pointer stepping over read pointer were correct. I attached the Zenless log, there were no underflows at the time of buzz. Also ignore the first 3 underflows, they happened during launch. Overflow has to do with held_bytes dipping below pa_held_bytes, but I don't fully understand the relationship. The problem is that the server audio consumption is slower than adv_bytes movement. But shouldn't there be a resampler on the server side to maintain an equilibrium? Because right now it looks like a catch-22, you can get steady pointer advancement with write stepping over read, or you can throttle adv_bytes and get mathematecally guaranteed ram buffer overfill. Or, like in my code, inconsistent NtSetEvent. Of course, none of the options sound right. I am not familiar with winealsa's logic, but maybe we can take an inspiration from there, since it's very stable? [pulse-logs.tar.gz](/uploads/35c45ffa35f3e0bc408d4dfb1cd6df80/pulse-logs.tar.gz) -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9840#note_127900
On Sat Jan 24 08:53:09 2026 +0000, Dzmitry Keremsha wrote:
The game does not underfill, I added a log that shows held_bytes, pa_held and adv_bytes. Note that I added it right after adv_bytes calculation. The point is, held_bytes never dips to zero, and adv_bytes isn't braking. I genuinely do not understand how that makes sense though. I also tested with the commit reversed, also crackles. Attaching both logs. Changing quant used to help (I'd get audio issues only maybe 15-20 minutes in), but it still wouldn't solve all issues. I tested Arc Raiders and Zenless Zone Zero. I think my suspicions about write pointer stepping over read pointer were correct. I attached the Zenless log, there were no underflows at the time of buzz (it is right at the very end, I turned the game off as soon as the buzz ended). Also ignore the first 3 underflows, they happened during launch. Overflow has to do with held_bytes dipping below pa_held_bytes, but I don't fully understand the relationship. The problem is that the server audio consumption is slower than adv_bytes movement. But shouldn't there be a resampler on the server side to maintain an equilibrium? Because right now it looks like a catch-22, you can get steady pointer advancement with write stepping over read, or you can throttle adv_bytes and get mathematecally guaranteed ram buffer overfill. Or, like in my code, inconsistent NtSetEvent. Of course, none of the options sound right. I am not familiar with winealsa's logic, but maybe we can take an inspiration from there, since it's very stable? [pulse-logs.tar.gz](/uploads/35c45ffa35f3e0bc408d4dfb1cd6df80/pulse-logs.tar.gz) Looked at what winealsa does, it seems the logic there is similar to my "safe_bytes" logic. If the audio server is slow - throttle pointer advancement. Since NtSetEvent is consistent, it eventually leads to full buffer RAM and denied refills. I guess this is okay? I don't know where this data goes then, but it never sounds like a part of audio is missing.
It was easy to implement it with 5x timer polling without breaking anything, every tick add +1 to a counter, every 5 ticks (so exactly once per period) fire NtSetEvent. Unless the stream held and pa_held equal 0 (the stream just started), then fire immediately. Passes tests, doesn't crackle, and unless I misunderstand something, looks exactly like winealsa to the game. Attaching a winealsa log with some additional logs from me so it's clear when a refill is missed. [honkai-winealsa.tar.gz](/uploads/c12ab1c11a1c36e7cf3bd784ec5bdf43/honkai-winealsa.tar.gz) -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9840#note_127927
participants (3)
-
Dzmitry Keremsha -
Dzmitry Keremsha (@Vyrolian) -
Paul Gofman (@gofman)