[PATCH 0/1] MR10792: winepulse.drv: Don't advance read pointer past data not yet consumed by PA
Adds a simple throttling mechanism to prevent pointer overlaps when winepulse is faster than the audio server. Winepulse uses a fake clock to advance the pointers: ```c /* 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; ``` And if winepulse is just a little bit faster than the audio server (does not matter PW or PA), it inevitably leads to pointer overlaps and therefore crackling. This patch proposes a throttling mechanism that is somewhat similar to winealsa - don't ever tell the application we have more space than we actually do. The problem could probably be resolved by making the fake clock perfectly mimic the audio server, but this is orders of magnitude more difficult. Winealsa achieves similar idea this way: `data_frames_played = min(stream->data_in_alsa_frames, avail); stream->held_frames -= data_frames_played;` The issue I am trying to fix is very real - it can cause crackling in any game you try. You don't have to hear the crackling to see that it's mathematically inevitable by logging the difference between held_bytes and pa_held_bytes. If it becomes smaller and goes negative - crackling is inevitable, because pointers will overlap. The issue is not tied to PW or PA specifically, I observed the behavior with both. In regards to how it interacts with MR https://gitlab.winehq.org/wine/wine/-/merge_requests/8628 - no issues in GoWR in my testing, unless you try enabling PROTON_LOG for pulse (which is very spammy in this game), and spam yes on all threads - which would also cause stream resets with unchanged winepulse, just later. Additionally, this patch has already been implemented in Proton GE and Proton CachyOS for about two months now - no issues and it has helped some people. There are not a lot of open issues regarding audio because unfortunately crackling is very underreported - you can look at GoWR protondb page before the 8628 MR - it definitely crackled, but many reports were not stating so. There are a lot of reports of crackling on virtually any game on protondb. You can also see people solve crackling issues for Arc Raiders (which I personally had myself before patching winepulse) by using winealsa: https://github.com/ValveSoftware/Proton/issues/9164#issuecomment-3529760335. One thing to look out for though - in CI spatialaudio tests will fail, but they fail literally the same way they do with winealsa - this is because there is nowhere for audio to go in a virtual environment, pa_held_bytes balloons while held_bytes drops, they overlap and held_bytes stops moving, and the tests start failing. No issues locally. I have previously tried tackling crackling in this pr: https://gitlab.winehq.org/wine/wine/-/merge_requests/9840 (although it was for underflows instead). My approach was admittedly incorrect, however it did help me discover this issue. Good audio is fundamental in my opinion, I would really love to see crackling-free audio for everyone regardless of proton version. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10792
From: Dzmitry Keremsha <vyro@lumencoil.com> --- dlls/winepulse.drv/pulse.c | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/dlls/winepulse.drv/pulse.c b/dlls/winepulse.drv/pulse.c index abc5e60821f..c512d5e5717 100644 --- a/dlls/winepulse.drv/pulse.c +++ b/dlls/winepulse.drv/pulse.c @@ -1565,6 +1565,7 @@ static NTSTATUS pulse_timer_loop(void *args) LARGE_INTEGER delay; pa_usec_t last_time; UINT32 adv_bytes; + SIZE_T safe_bytes; int success; pulse_lock(); @@ -1626,12 +1627,22 @@ static NTSTATUS pulse_timer_loop(void *args) if (stream->dataflow == eRender) { 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; + safe_bytes = stream->period_bytes; + + if ((stream->held_bytes > stream->pa_held_bytes)) + { + SIZE_T limit = stream->held_bytes - stream->pa_held_bytes; + if (safe_bytes > limit) + safe_bytes = limit; + } + else { + safe_bytes = 0; + } + + adv_bytes = safe_bytes; + stream->lcl_offs_bytes += adv_bytes; + stream->lcl_offs_bytes %= stream->real_bufsize_bytes; + stream->held_bytes -= adv_bytes; } else if(stream->dataflow == eCapture) { -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10792
I doubt this is correct. Advancing read pointer affects app-visible timing, and that is an invariant: the audio timing should advance strictly real time based. It is not only audio related even, some game engines base game time on mmdevapi time. That's why '`/* regardless of what PA does, advance one period */'.` Whenever the actual crackling problem is I suppose it should be solved else wise, without compromising app's audio timing following pulse audio setup. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10792#note_138462
On Thu Apr 30 21:33:35 2026 +0000, Paul Gofman wrote:
I doubt this is correct. Advancing read pointer affects app-visible timing, and that is an invariant: the audio timing should advance strictly real time based. It is not only audio related even, some game engines base game time on mmdevapi time. That's why '`/* regardless of what PA does, advance one period */'.` Whenever the actual crackling problem is I suppose it should be solved else wise, without compromising app's audio timing following pulse audio setup. Thanks for the feedback. Then this looks like a pretty serious problem, because the timer is incorrect. I still think it is sensible to not lie about the reality - but the timer must be fixed to avoid the issues altogether. There is no winning here otherwise - either some games are slower and don't crackle, or they aren't slow and crackle.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10792#note_138463
There is no winning here otherwise - either some games are slower and don't crackle, or they aren't slow and crackle.
There are other options. First of all, yes, I guess the current timer management itself with relative delays is not accurate and it is solved with https://gitlab.winehq.org/wine/wine/-/merge_requests/8628 which is however stuck for some reason. Then, the audio surely doesn't crackle for every PA setup and every game. The first thing here is to understand what is going wrong with the specific setup and game. Then, worst case, if there is indeed a systematic problem and it can't be solved by avoiding incompatible PA setups the solution is maybe introducing a separate PA update loop with its own buffer, so that PA audio pushes are decoupled from mmdevapi timeline. But before going for this complication I believe the actual exact problem must be understood. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10792#note_138464
On Thu Apr 30 21:46:15 2026 +0000, Paul Gofman wrote:
There is no winning here otherwise - either some games are slower and don't crackle, or they aren't slow and crackle. There are other options. First of all, yes, I guess the current timer management itself with relative delays is not accurate and it is solved with https://gitlab.winehq.org/wine/wine/-/merge_requests/8628 which is however stuck for some reason. Then, the audio surely doesn't crackle for every PA setup and every game. The first thing here is to understand what is going wrong with the specific setup and game. Then, worst case, if there is indeed a systematic problem and it can't be solved by avoiding incompatible PA setups the solution is maybe introducing a separate PA update loop with its own buffer, so that PA audio pushes are decoupled from mmdevapi timeline. But before going for this complication I believe the actual exact problem must be understood. I actually compared your PR to old winepulse and indeed it seemingly was starting to crackle a lot faster with the old one. The problem is that pointers still overlap maybe half an hour in even with your patch. The problem is widespread (from my experience on forums and such), but unfortunately I only have one PC at my disposal. If i can provide some additional logs for this I'd be happy to, but I need to know what exactly besides the ones proving pointers overlap.
FWIW I tried two distros, two soundcards and different kernels. CachyOS kernel helps with underflows but it's a different issue altogether. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10792#note_138465
On Thu Apr 30 21:59:40 2026 +0000, Dzmitry Keremsha wrote:
I actually compared your PR to old winepulse and indeed it seemingly was starting to crackle a lot faster with the old one. The problem is that pointers still overlap maybe half an hour in even with your patch. The problem is widespread (from my experience on forums and such), but unfortunately I only have one PC at my disposal. If i can provide some additional logs for this I'd be happy to, but I need to know what exactly besides the ones proving pointers overlap. FWIW I tried two distros, two soundcards and different kernels. CachyOS kernel helps with underflows but it's a different issue altogether. Today with some help I tested:
Honkai Star Rail, Hades, Balatro Tested on three PCs, PC 1 and 3 for honkai, PC 1 and 2 for Balatro and PC 1 for Hades. All show the same behavior - slow timer drift. I attached a log that looks like this in the pulse.c code: ``` pulse_write(stream); TRACE("Buffer comparison: pa_held_bytes: %lu, held_bytes: %lu, diff: %ld\n", (unsigned long)stream->pa_held_bytes, (unsigned long)stream->held_bytes, (long)((long)stream->held_bytes - (long)stream->pa_held_bytes)); /* 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; ``` Tested Proton 11 and Proton GE, Proton GE had the last winepulse commit from Proton 10 without any additional patches, and Proton 11 is unchanged aside from the trace. Logged like this: WINEDEBUG=+pid,+loaddll,+timestamp,+debugstr,+threadname,+dsound,+dsound3d,+xaudio2,+mmdevapi,+pulse and PULSE_LATENCY_MSEC=60 to avoid crackling while getting the log spam. Without the latency variable, for example in Hades, I would get a very quick timer drift compared with frequent underflows (underflows do not happen without the logs so it's caused by them) [balatro-proton11-log.tar.gz](/uploads/8702ffc0fb47211004b28710591fb09d/balatro-proton11-log.tar.gz) To make sure this isn't about the differences in upstream Proton and GE, I tested Balatro with both and had the same outcome. I will only attach the Proton 11 Balatro log because of the size limits, however I can send the rest if needed. Regarding pipewire setup - everything is default and untouched except added RT capabilities on every machine. The pattern looks like this - winepulse timer is mostly in lockstep with the real time (by which I assume PipeWire). Sometimes, however, the audio server would consume less than it received, indicating that winepulse was a bit too fast. Since we don't have a throttling mechanism, we are now permanently ahead of reality. Wait long enough and the pointers would overlap and therefore it will crackle. An interesting discovery is that with the default pipewire-pulse quant of 256 (as of the new default in PipeWire 1.6) and PULSE_LATENCY_MSEC=60 the mmdevapi period is 15ms. In Honkai this is actually a huge problem - several crackles per second about one-two minutes in, specifically not underflow related. With the quant manually set to 480 - and therefore mmdevapi period becoming 10ms - there is a very slow drift which results in crackling in about 30 minutes for me. For PC 3 (pw-pa quant 480 and PULSE_LATENCY_MSEC=60) the drift actually only started occuring in 20-30 minutes of gameplay and was perfect before that. Yet another thing I noticed - the crackling is genuinely harder to spot when playing with speakers, for instance with bad HSR crackling and my laptop speakers I had to stand next to the speakers to hear them (probably something with the frequency they produce), while they are very loud and clear when listening through headphones. So as we can see, with the current winepulse timer implementation and without some sort of a throttling mechanism crackling is inevitable, be it in 10 minutes or 10 hours, at least on the hardware I tested. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10792#note_138566
An interesting discovery is that with the default pipewire-pulse quant of 256 (as of the new default in PipeWire 1.6) and PULSE_LATENCY_MSEC=60 the mmdevapi period is 15ms.
Pulse latency should be \<10ms for Wine to work correctly, anything \>10ms is de-facto broken now. It is not about crackling in the first place, currently mmdevapi effectively sets minimum period to max(10ms, pulse latency). It can't force lower (unless we are going to implement a separate data feed thread for pulse decoupled from application data consuming thread). On Windows min period is always \~10ms and anything bigger outright breaks some games (make them crash) or may induce audio problems in other. The way is to avoid ever setting pulse latency \>10ms (\~3ms is probably a goo dstarting poin), I think there is no reason to have higher pulse latencies. I know people sometimes try to work around some problems this way but for Wine that is just broken.
The pattern looks like this - winepulse timer is mostly in lockstep with the real time (by which I assume PipeWire). Sometimes, however, the audio server would consume less than it received, indicating that winepulse was a bit too fast. Since we don't have a throttling mechanism, we are now permanently ahead of reality. Wait long enough and the pointers would overlap and therefore it will crackle.
If there is a slow time drift I'd expect it to lead to starvation or dropping buffer data once in that mentioned 30 minutes and then things to work just fine for another 30 minutes. The current way is supposed to effectively sync-time, possibly with data drop once in a while but that would be hard to notice. If that doesn't happen with the suitable pulse parameters (latency \< 10ms) maybe there is some other issue with implicit time catch up logic and we should be fixing that one. It is also possible that the issue is related to specific games which misbehave on Wine due to some incompatibility not even related to pulse timing. Then, does those affected games use mmdevapi directly even, or the issue might in fact be in, say, xaudio or dinput? \\>So as we can see, with the current winepulse timer implementation and without some sort of a throttling mechanism crackling is inevitable, be it in 10 minutes or 10 hours, at least on the hardware I tested. Well, I don't quite see that from the existing info. Maybe it worth opening Wine bug ticket with detailed repro instructions, which would describe the issue with specific game and reproduced with upstream Wine, along with pulse customization details (and provided latency is \<10ms). If it is reproducible only with official Proton, a proper place for that is Proton github issue tracker (reproducing and attaching PROTON_LOG=+pulse,+mmdevapi,+xaudio2,+dsound , strictly using official Proton and not downstream modifications). -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10792#note_138620
On Sat May 2 00:31:07 2026 +0000, Paul Gofman wrote:
An interesting discovery is that with the default pipewire-pulse quant of 256 (as of the new default in PipeWire 1.6) and PULSE_LATENCY_MSEC=60 the mmdevapi period is 15ms. Pulse latency should be \<10ms for Wine to work correctly, anything \>10ms is de-facto broken now. It is not about crackling in the first place, currently mmdevapi effectively sets minimum period to max(10ms, pulse latency). It can't force lower (unless we are going to implement a separate data feed thread for pulse decoupled from application data consuming thread). On Windows min period is always \~10ms and anything bigger outright breaks some games (make them crash) or may induce audio problems in other. The way is to avoid ever setting pulse latency \>10ms (\~3ms is probably a goo dstarting poin), I think there is no reason to have higher pulse latencies. I know people sometimes try to work around some problems this way but for Wine that is just broken. The pattern looks like this - winepulse timer is mostly in lockstep with the real time (by which I assume PipeWire). Sometimes, however, the audio server would consume less than it received, indicating that winepulse was a bit too fast. Since we don't have a throttling mechanism, we are now permanently ahead of reality. Wait long enough and the pointers would overlap and therefore it will crackle. If there is a slow time drift I'd expect it to lead to starvation or dropping buffer data once in that mentioned 30 minutes and then things to work just fine for another 30 minutes. The current way is supposed to effectively sync-time, possibly with data drop once in a while but that would be hard to notice. If that doesn't happen with the suitable pulse parameters (latency \< 10ms) maybe there is some other issue with implicit time catch up logic and we should be fixing that one. It is also possible that the issue is related to specific games which misbehave on Wine due to some incompatibility not even related to pulse timing. Then, does those affected games use mmdevapi directly even, or the issue might in fact be in, say, xaudio or dinput? \\>So as we can see, with the current winepulse timer implementation and without some sort of a throttling mechanism crackling is inevitable, be it in 10 minutes or 10 hours, at least on the hardware I tested. Well, I don't quite see that from the existing info. Maybe it worth opening Wine bug ticket with detailed repro instructions, which would describe the issue with specific game and reproduced with upstream Wine, along with pulse customization details (and provided latency is \<10ms). If it is reproducible only with official Proton, a proper place for that is Proton github issue tracker (reproducing and attaching PROTON_LOG=+pulse,+mmdevapi,+xaudio2,+dsound , strictly using official Proton and not downstream modifications). To explain things better and as to why my patch is necessary with the timer drift:
1. Timer drift means that wine mmdevapi is slightly faster than pipewire (which is the real time), so the rate of refill is higher than the rate of consumption. 2. pa_held_bytes balloons because on average we, for instance, refill 480 frames and consume 479. Held bytes, as you can see from the log, is consistent, it does not deviate from the margins of it's refill and drain long-term 3. pa_held_bytes grows infinitely -\> held bytes is the same -\> game thinks we have space for 500 frames, we have space for 30. Result - the game would refill it's usual 480 (an example) frames and we have an overlap of 450 frames - sounds like a crackle or static buzz. 4. pa_held_bytes is permanently high now, any overfill = crackle. You can see in that balatro log we have pa_held_bytes that is hovering around 0 and then after some point it stops (just look at the end of the log), as it got higher permanently. Please keep in mind it's happens even proper mmdevapi period so it's not about that. With my patch we never lie to the game - if we only have space for 30 frames, we are telling the game the truth, and hit backpressure which is the correct mechanism for that situation. Now if the wine mmdevapi timer is perfect, we would literally never hit the throttle, gain 480, spend 480, perfect equilibrium. Moreover, this mechanism is in alsa already. Ultimately the incorrect timer is what should be fixed, but the throttle is not harmful in any way. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10792#note_138634
Timer drift means that wine mmdevapi is slightly faster than pipewire (which is the real time), so the rate of refill is higher than the rate of consumption.
It would be interesting why. Note that at least current time logic in Wine bases adjustments on PA stream time so it is supposed to match stream time of average. So if it consistently getting less writes and that builds up over some 30 minutes there might be some bug in that part (or some rounding error, so it would constistently err on one side somewhere estimating time or getting number of samples??). Note that this alone, while interesting, might not give the whole story anyway. Explanation sounds like this is the generic universal Wine problem, but this doesn't look to be the case. The audio doesn't crackle on the majority of setups for every game. There is no undertstadning currently what is special here, special inventive PA setup or maybe not but some issue with the specific game or engine (and the issue can in theory be in the specific audio API it is using even, e. g., xaudio).
pa_held_bytes is permanently high now, any overfill = crackle. Since pa_held_bytes is at it's limit, it now happens quite frequently.
Once again, the logic of app-visible update is an invariant, we are not free to change it to follow PA specifics without breaking compatibility with apps. But this is not the only option. I was mentioning an (rather complicated) alternative approach before. Another probably better way would be to just drop 'pa_held' buffer part once such a situation is detected. So at the moment of dropping it might have (probably hardly noticable?) hitch but then be fine for another half an hour. Either way, the way is it to adapt / interface PA side, not compromise app-visible side. For the latter approach with dropping PA buffer I personally would not be strictly opposed to that (unlike the current approach), even if there is no 100% understanding of the actual issue (which would probably be very useful to fix the actual problem). Dealing with the consequences which already happened for some reason (which reason can be incompatible PA setup out of our control) is probably better this way than letting pa_held_bytes stay permanently high. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10792#note_138647
On Sat May 2 15:16:09 2026 +0000, Paul Gofman wrote:
Timer drift means that wine mmdevapi is slightly faster than pipewire (which is the real time), so the rate of refill is higher than the rate of consumption. It would be interesting why. Note that at least current time logic in Wine bases adjustments on PA stream time so it is supposed to match stream time of average. So if it consistently getting less writes and that builds up over some 30 minutes there might be some bug in that part (or some rounding error, so it would constistently err on one side somewhere estimating time or getting number of samples??). Note that this alone, while interesting, might not give the whole story anyway. Explanation sounds like this is the generic universal Wine problem, but this doesn't look to be the case. The audio doesn't crackle on the majority of setups for every game. There is no undertstadning currently what is special here, special inventive PA setup or maybe not but some issue with the specific game or engine (and the issue can in theory be in the specific audio API it is using even, e. g., xaudio). pa_held_bytes is permanently high now, any overfill = crackle. Since pa_held_bytes is at it's limit, it now happens quite frequently. Once again, the logic of app-visible update is an invariant, we are not free to change it to follow PA specifics without breaking compatibility with apps. But this is not the only option. I was mentioning an (rather complicated) alternative approach before. Another probably better way would be to just drop 'pa_held' buffer part once such a situation is detected. So at the moment of dropping it might have (probably hardly noticable?) hitch but then be fine for another half an hour. Either way, the way is it to adapt / interface PA side, not compromise app-visible side. For the latter approach with dropping PA buffer I personally would not be strictly opposed to that (unlike the current approach), even if there is no 100% understanding of the actual issue (which would probably be very useful to fix the actual problem). Dealing with the consequences which already happened for some reason (which reason can be incompatible PA setup out of our control) is probably better this way than letting pa_held_bytes stay permanently high. Honestly I am not sure myself what is happening either and why the reports aren't universal. So far there has not been a single case where I could play any game without the timer drifting (and surely enough crackling with enough time), and the problem has persisted across the hardware/kernels/etc I've tested.
I'd like to ask you what bad things can hypothetically can happen if held_bytes gets throttled - and I'd like to test some games/programs you know of where something breaking could happen. As it stands now - I am literally unable to play without crackling ever without either this patch or winealsa which basically does the same thing, so I am curious how the edge cases behave. Most games are fine though, they are okay with the fact that the audio thread can be stopped. Maybe it can cause something bad, but so far the logic I describe has to exist at least as a variable in downstream protons so people with such issues (me included) can play games without constant crackling half an hour in. If I understand correctly then the games that tie their game logic to mmdevapi would be a tiny bit faster when mmdevapi is fast, and if they hit the throttle they would slow down to compensate for their overspeed. Maybe I misunderstand something though. There have been no cases so far of games outright crashing or audio glitches from the public reports. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10792#note_138651
Another probably better way would be to just drop 'pa_held' buffer part once such a situation is detected.
pa_held is physical audio though, when it's full it's very roughly \~25ms usually. That would be a loud and annoying crackle happening every half an hour, or worst case much often. There has to be a way to somehow solve this without compromising anything. Regarding the PA setups - I honestly don't even know what there is to break in the first place, my tests were 1. just regular PipeWire 1.6 with absolutely nothing added 2. quant 480 for pipewire-pulse Native games and apps do not ever have any issues either. Moreover I did try pulseaudio instead of pipewire, and without touching any config files and the behavior was the same. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10792#note_138655
pa_held is physical audio though, when it's full it's very roughly \~25ms usually. That would be a loud and annoying crackle happening every half an hour, or worst case much often. There has to be a way to somehow solve this without compromising anything.
Why loud? That is supposed to be just a ps_held length skip in audio, without loud garbage added. But anyway, I would agree that it is not ideal, it would be better to understand why time difference builds up and fix that. Currently it is supposed to catch up with app's visible mmdevapi audio quantum interval slightly dynamically adjusted, if that doesn't happen it is interesting why exactly and probably that is the part which should ideally be fixed one or another way.
quant 480 for pipewire-pulse
Which exactly units and parameter is that? What is the minimum period our mmdevapi actually ends up with? If that is more than 10 ms (stupulated by pulse latency) this can't work right now. We did have examples of issues (at least on Proton tracker) with such setup just breaking games (due to too high device period which never happens on Windows). -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10792#note_138672
Why loud?
Yeah maybe not the most appropriate description, in my head underflow = loud, overlap = quiet, deleting data sounds like an underflow, hence loud. My exact pulse.properties config in \~/.config/pipewire: ``` #server.dbus-name = "org.pulseaudio.Server" #pulse.allow-module-loading = true pulse.min.req = 480/48000 pulse.default.req = 480/48000 pulse.min.frag = 480/48000 #pulse.default.frag = 96000/48000 # 2 seconds #pulse.default.tlength = 96000/48000 # 2 seconds pulse.min.quantum = 480/48000 #pulse.idle.timeout = 0 # don't pause after underruns #pulse.default.format = F32 #pulse.default.position = [ FL FR ] #pulse.fix.position = [ FL FR ] ``` Allows for 10ms mmdev period with PULSE_LATENCY_MSEC=60 (so I can properly log without underflows) -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10792#note_138673
participants (3)
-
Dzmitry Keremsha -
Dzmitry Keremsha (@Vyrolian) -
Paul Gofman (@gofman)