diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Labels.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Labels.cs index 4cb3dac..c15fb34 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Labels.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Labels.cs @@ -435,15 +435,21 @@ public sealed partial class GpuCommandInterpreter throw _host.Fatal($"The occlusion-counter dump is invalid: index=0x{eventIndex:X8} address=0x{eventAddress:X16}."); } - // Publish visible occlusion results for each depth block. + // Until real sample queries are implemented, conservatively report visibility. + // A one-sample delta can disappear when the guest scales or reduces the counts. + const ulong visibleCounterStep = 0x2FFFFFF; const ulong readyBit = 1UL << 63; var result = readyBit | SyntheticOcclusionCounter; for (var depthBlock = 0u; depthBlock < 16u; depthBlock++) { - WriteQword(eventAddress + ((ulong)depthBlock * 2 * sizeof(ulong)), result); + // Query reducers can read a GPU-cached copy of this range. Use the same + // ordered transfer path as clears instead of changing only CPU backing. + var destination = eventAddress + ((ulong)depthBlock * 2 * sizeof(ulong)); + _host.FillBuffer(destination, sizeof(uint), (uint)result, false); + _host.FillBuffer(destination + sizeof(uint), sizeof(uint), (uint)(result >> 32), false); } - SyntheticOcclusionCounter = (SyntheticOcclusionCounter + 1) & (readyBit - 1); + SyntheticOcclusionCounter = (SyntheticOcclusionCounter + visibleCounterStep) & (readyBit - 1); break; } diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterLabelTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterLabelTests.cs index 1425f35..a79f769 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterLabelTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterLabelTests.cs @@ -325,12 +325,16 @@ public sealed class GpuCommandInterpreterLabelTests Assert.Contains("event type is unknown", runner.RunExpectingFatal(StreamRunner.Packet(PacketOpcode.EventWrite, 0x3F)).Message); runner.Run(StreamRunner.Packet(PacketOpcode.EventWrite, 0x39 | (1u << 8), StreamRunner.Low(Label), StreamRunner.High(Label))); + // CPU-only stores leave GPU query-reduction inputs stale. Require the + // host transfer path for both words of every counter, including ready. + Assert.Equal(32, runner.Host.Calls.Count); + Assert.All(runner.Host.Calls, call => Assert.StartsWith("fill ", call)); Assert.Equal(1UL << 63, runner.Host.ReadQword(Label)); Assert.Equal(1UL << 63, runner.Host.ReadQword(Label + 15 * 16)); Assert.Equal(0UL, runner.Host.ReadQword(Label + 8)); runner.Run(StreamRunner.Packet(PacketOpcode.EventWrite, 0x39 | (1u << 8), StreamRunner.Low(Label), StreamRunner.High(Label))); - Assert.Equal((1UL << 63) | 1, runner.Host.ReadQword(Label)); + Assert.Equal((1UL << 63) | 0x2FFFFFFUL, runner.Host.ReadQword(Label)); } [Fact]