Report more accurate data for Pixel history with discards & fragments

* For draws where some fragments discard, D3D12 & Vulkan currently can't
  distinguish which specific fragments did and didn't discard because they're
  processed batched in separate callbacks rather than incrementally like D3D11 &
  GL which can track a discarded offset by doing one stencil count at a time.
  This means we can't get primitive information or ordering, we only get a list
  of the passing fragments and a set of failing fragments.
* Note that on all APIs we also can't distinguish when instancing is present,
  because there could be multiple "identical" primitives from different
  primitives, some of which discard and some don't.
This commit is contained in:
baldurk
2026-09-15 17:00:54 +01:00
parent 3ce17193e9
commit 510d16d900
5 changed files with 116 additions and 35 deletions
+38 -3
View File
@@ -3297,8 +3297,10 @@ rdcarray<PixelModification> D3D12Replay::PixelHistory(rdcarray<EventUsage> event
int32_t frags = int32_t(ei.dsWithoutShaderDiscard[0]);
int32_t fragsClipped = int32_t(ei.dsWithShaderDiscard[0]);
bool someFragsClipped = (fragsClipped < frags);
mod.primitiveID = someFragsClipped;
if(fragsClipped < frags)
mod.primitiveID = fragsClipped;
else
mod.primitiveID = 0;
if(frags > 0)
{
@@ -3344,6 +3346,7 @@ rdcarray<PixelModification> D3D12Replay::PixelHistory(rdcarray<EventUsage> event
// Retrieve primitive ID values where fragment shader discarded some fragments. For these
// primitives we are going to perform an occlusion query to see if a primitive was discarded.
std::map<uint32_t, rdcarray<int32_t>> discardedPrimsEvents;
std::map<uint32_t, uint32_t> fragsClipped;
uint32_t primitivesToCheck = 0;
for(size_t h = 0; h < history.size(); h++)
{
@@ -3353,11 +3356,23 @@ rdcarray<PixelModification> D3D12Replay::PixelHistory(rdcarray<EventUsage> event
if(perFragmentCB.ContainsEvent(eid))
{
uint32_t f = history[h].fragIndex;
bool someFragsClipped = (history[h].primitiveID == 1);
fragsClipped[eid] = history[h].primitiveID;
bool someFragsClipped = (history[h].primitiveID >= 1);
int32_t primId = fragInfo[perFragmentCB.GetEventOffset(eid) + f].primitiveID;
history[h].primitiveID = primId;
if(someFragsClipped)
{
// in scenarios with multiple fragments with some discarding, both the primitive IDs for
// all fragments (discarding and non-discarding) as well as shader outs are all fetched at
// once. Because we use stencil counting to fetch (potentially discarded) shader outs we
// will just get all the non-discarded shader outs in the first N fragments without
// knowing which is which.
//
// we could leave the primitive IDs and then below in DiscardedFragmentsCallback try to
// reorder once we know which primitives discarded and which didn't, and assign the first
// N successful fragments, but for now we drop the primitive ID information
history[h].primitiveID = ~0U;
discardedPrimsEvents[eid].push_back(primId);
primitivesToCheck++;
}
@@ -3394,6 +3409,25 @@ rdcarray<PixelModification> D3D12Replay::PixelHistory(rdcarray<EventUsage> event
for(size_t h = 0; h < history.size(); h++)
{
// if we dropped the primitive IDs we don't know which individual fragments discarded.
// We've already removed any primitive ID information so to stay consistent since we know
// how many total fragments discarded and how many didn't, we keep the shader out & tex
// after for the first N successful fragments, and explicily mark the last M as discarded.
// This loses ordering information but maintains accuracy about relative numbers.
if(history[h].primitiveID == ~0U)
{
const uint32_t eid = history[h].eventId;
if(eventsWithFrags.find(eid) != eventsWithFrags.end() &&
history[h].fragIndex >= fragsClipped[eid])
{
if(history[h].Passed())
{
history[h].shaderDiscarded = true;
}
}
continue;
}
history[h].shaderDiscarded =
discardedCb.PrimitiveDiscarded(history[h].eventId, history[h].primitiveID);
}
@@ -3423,6 +3457,7 @@ rdcarray<PixelModification> D3D12Replay::PixelHistory(rdcarray<EventUsage> event
// Copy previous post-mod value if its not the first event
if(h > 0)
{
history[h].preMod = history[h - 1].postMod;
history[h].postMod.col = history[h - 1].postMod.col;
history[h].postMod.depth = history[h - 1].postMod.depth;
if(!hasDepth)
+17 -12
View File
@@ -1035,7 +1035,8 @@ struct ScopedReadPixelsSanitiser
void readPixelValues(WrappedOpenGL *driver, const GLPixelHistoryResources &resources,
const CopyFramebuffer &copyFramebuffer, rdcarray<PixelModification> &history,
int historyIndex, ModType modType, bool readStencil, uint32_t numPixels)
int historyIndex, ModType modType, bool readStencil, uint32_t numPixels,
bool perfrag)
{
ScopedReadPixelsSanitiser scope;
@@ -1139,6 +1140,9 @@ void readPixelValues(WrappedOpenGL *driver, const GLPixelHistoryResources &resou
}
}
if(perfrag && history[historyIndex + i].shaderDiscarded)
continue;
if(modType == ModType::PreMod)
history[historyIndex + i].preMod = modValue;
else
@@ -1269,7 +1273,7 @@ void QueryPrePostModPixelValues(WrappedOpenGL *driver, GLPixelHistoryResources &
if(premodCopyFramebuffer.framebufferId != ~0u)
{
readPixelValues(driver, resources, premodCopyFramebuffer, history, preModLastIdx,
ModType::PreMod, true, (uint32_t)(i - preModLastIdx));
ModType::PreMod, true, (uint32_t)(i - preModLastIdx), false);
}
preModLastIdx = int(i);
}
@@ -1342,7 +1346,7 @@ void QueryPrePostModPixelValues(WrappedOpenGL *driver, GLPixelHistoryResources &
if(postmodCopyFramebuffer.framebufferId != ~0u)
{
readPixelValues(driver, resources, postmodCopyFramebuffer, history, postModLastIdx,
ModType::PostMod, true, (uint32_t)(i - postModLastIdx));
ModType::PostMod, true, (uint32_t)(i - postModLastIdx), false);
}
postModLastIdx = int(i);
}
@@ -1376,12 +1380,12 @@ void QueryPrePostModPixelValues(WrappedOpenGL *driver, GLPixelHistoryResources &
if(numSamples == 1 && premodCopyFramebuffer.framebufferId != 0u)
{
readPixelValues(driver, resources, premodCopyFramebuffer, history, preModLastIdx,
ModType::PreMod, true, int(modEvents.size()) - preModLastIdx);
ModType::PreMod, true, int(modEvents.size()) - preModLastIdx, false);
}
if(numSamples == 1 && postmodCopyFramebuffer.framebufferId != 0u)
{
readPixelValues(driver, resources, postmodCopyFramebuffer, history, postModLastIdx,
ModType::PostMod, true, int(modEvents.size()) - postModLastIdx);
ModType::PostMod, true, int(modEvents.size()) - preModLastIdx, false);
}
}
@@ -1972,6 +1976,8 @@ void QueryShaderOutPerFragment(WrappedOpenGL *driver, GLReplay *replay,
historyIndex->postMod.stencil = -2;
else
historyIndex->postMod.stencil = -1;
if(historyIndex > history.begin() && (historyIndex - 1)->eventId == historyIndex->eventId)
historyIndex->preMod = (historyIndex - 1)->postMod;
historyIndex++;
continue;
}
@@ -2356,8 +2362,6 @@ void QueryPrePostModPerFragment(WrappedOpenGL *driver, GLReplay *replay,
if(historyIndex + j + 1 < history.end() &&
(curFragHistoryIndex + 1)->eventId == curFragHistoryIndex->eventId)
curFragHistoryIndex->postMod.stencil = -2;
else
curFragHistoryIndex->postMod.stencil = -1;
if(numSamples > 1)
historyIndex++;
@@ -2403,7 +2407,7 @@ void QueryPrePostModPerFragment(WrappedOpenGL *driver, GLReplay *replay,
{
readPixelValues(driver, resources, premodCopyFramebuffer, history,
preModLastJ + int(historyIndex - history.begin()), ModType::PreMod,
false, (uint32_t)(j - preModLastJ));
false, (uint32_t)(j - preModLastJ), true);
}
preModLastJ = int(j);
}
@@ -2464,7 +2468,7 @@ void QueryPrePostModPerFragment(WrappedOpenGL *driver, GLReplay *replay,
{
readPixelValues(driver, resources, postmodCopyFramebuffer, history,
postModLastJ + int(historyIndex - history.begin()), ModType::PostMod,
false, (uint32_t)(j - postModLastJ));
false, (uint32_t)(j - postModLastJ), true);
}
postModLastJ = int(j);
}
@@ -2583,14 +2587,14 @@ void QueryPrePostModPerFragment(WrappedOpenGL *driver, GLReplay *replay,
{
readPixelValues(driver, resources, premodCopyFramebuffer, history,
preModLastJ + int(historyIndex - history.begin()), ModType::PreMod, false,
numFragments - preModLastJ);
numFragments - preModLastJ, true);
}
if(numSamples == 1 && postmodCopyFramebuffer.framebufferId != ~0u)
{
readPixelValues(driver, resources, postmodCopyFramebuffer, history,
postModLastJ + int(historyIndex - history.begin()), ModType::PostMod, false,
numFragments - postModLastJ);
numFragments - postModLastJ, true);
}
state.ApplyState(driver);
@@ -2838,7 +2842,8 @@ void CalculateFragmentDepthTests(WrappedOpenGL *driver, GLPixelHistoryResources
depthBits = 16;
}
history[historyIndex].CheckDepthTestQuantised(depthBits, MakeCompareFunc(depthFunc));
if(history[historyIndex].preMod.depth >= 0.0f && history[historyIndex].shaderOut.depth >= 0.0f)
history[historyIndex].CheckDepthTestQuantised(depthBits, MakeCompareFunc(depthFunc));
if(HasExt[EXT_depth_bounds_test] && GL.glIsEnabled(eGL_DEPTH_BOUNDS_TEST_EXT))
{
+41 -3
View File
@@ -4822,8 +4822,11 @@ rdcarray<PixelModification> VulkanReplay::PixelHistory(rdcarray<EventUsage> even
int32_t frags = int32_t(ei.dsWithoutShaderDiscard[4]);
int32_t fragsClipped = int32_t(ei.dsWithShaderDiscard[4]);
bool someFragsClipped = (fragsClipped < frags);
mod.primitiveID = someFragsClipped;
if(fragsClipped < frags)
mod.primitiveID = fragsClipped;
else
mod.primitiveID = 0;
// Draws in secondary command buffers will fail this check,
// so nothing else needs to be checked in the callback itself.
if(frags > 0)
@@ -4911,6 +4914,7 @@ rdcarray<PixelModification> VulkanReplay::PixelHistory(rdcarray<EventUsage> even
// fragments. For these primitives we are going to perform an occlusion
// query to see if a primitive was discarded.
std::map<uint32_t, rdcarray<int32_t>> discardedPrimsEvents;
std::map<uint32_t, uint32_t> fragsClipped;
uint32_t primitivesToCheck = 0;
for(size_t h = 0; h < history.size(); h++)
{
@@ -4918,11 +4922,23 @@ rdcarray<PixelModification> VulkanReplay::PixelHistory(rdcarray<EventUsage> even
if(eventsWithFrags.find(eid) == eventsWithFrags.end())
continue;
uint32_t f = history[h].fragIndex;
bool someFragsClipped = (history[h].primitiveID == 1);
fragsClipped[eid] = history[h].primitiveID;
bool someFragsClipped = (history[h].primitiveID >= 1);
int32_t primId = bp[perFragmentCB.GetEventOffset(eid) + f].primitiveID;
history[h].primitiveID = primId;
if(someFragsClipped)
{
// in scenarios with multiple fragments with some discarding, both the primitive IDs for
// all fragments (discarding and non-discarding) as well as shader outs are all fetched at
// once. Because we use stencil counting to fetch (potentially discarded) shader outs we
// will just get all the non-discarded shader outs in the first N fragments without
// knowing which is which.
//
// we could leave the primitive IDs and then below in DiscardedFragmentsCallback try to
// reorder once we know which primitives discarded and which didn't, and assign the first
// N successful fragments, but for now we drop the primitive ID information
history[h].primitiveID = ~0U;
discardedPrimsEvents[eid].push_back(primId);
primitivesToCheck++;
}
@@ -4948,8 +4964,29 @@ rdcarray<PixelModification> VulkanReplay::PixelHistory(rdcarray<EventUsage> even
ObjDisp(dev)->DestroyQueryPool(Unwrap(dev), occlPool, NULL);
for(size_t h = 0; h < history.size(); h++)
{
// if we dropped the primitive IDs we don't know which individual fragments discarded.
// We've already removed any primitive ID information so to stay consistent since we know
// how many total fragments discarded and how many didn't, we keep the shader out & tex
// after for the first N successful fragments, and explicily mark the last M as discarded.
// This loses ordering information but maintains accuracy about relative numbers.
if(history[h].primitiveID == ~0U)
{
const uint32_t eid = history[h].eventId;
if(eventsWithFrags.find(eid) != eventsWithFrags.end() &&
history[h].fragIndex >= fragsClipped[eid])
{
if(history[h].Passed())
{
history[h].shaderDiscarded = true;
}
}
continue;
}
history[h].shaderDiscarded =
discardedCb.PrimitiveDiscarded(history[h].eventId, history[h].primitiveID);
}
}
}
else
@@ -4982,6 +5019,7 @@ rdcarray<PixelModification> VulkanReplay::PixelHistory(rdcarray<EventUsage> even
// Copy previous post-mod value if its not the first event
if(h > 0)
{
history[h].preMod = history[h - 1].postMod;
history[h].postMod.col = history[h - 1].postMod.col;
history[h].postMod.depth = history[h - 1].postMod.depth;
if(cb.GetDepthFormat(eid) == VK_FORMAT_UNDEFINED)
+6 -5
View File
@@ -913,15 +913,16 @@ void init()
{Vec3f(-0.4f, 0.8f, 0.33f), Vec4f(0.0f, 0.0f, -1.0f, 1.0f), Vec2f(0.0f, 0.0f)},
});
// scissor does clip some but passes where above fails
// multiple fragments overlapping the same pixel where only one discards
PerFragDiscard = makeDraw({
{Vec3f(-0.7f, -0.2f, 0.33f), Vec4f(-1.0f, -1.0f, -1.0f, 1.0f), Vec2f(0.0f, 1.0f)},
{Vec3f(-0.8f, 0.0f, 0.33f), Vec4f(-1.0f, -1.0f, -1.0f, 1.0f), Vec2f(1.0f, 0.0f)},
{Vec3f(-0.6f, 0.0f, 0.33f), Vec4f(-1.0f, -1.0f, -1.0f, 1.0f), Vec2f(0.0f, 0.0f)},
// for now we discard the first one, because Vulkan/D3D12 can't determine ordering
{Vec3f(-0.7f, -0.2f, 0.33f), Vec4f(1.0f, 1.0f, 1.0f, 1.0f), Vec2f(0.0f, 1.0f)},
{Vec3f(-0.8f, 0.0f, 0.33f), Vec4f(1.0f, 1.0f, 1.0f, 1.0f), Vec2f(1.0f, 0.0f)},
{Vec3f(-0.6f, 0.0f, 0.33f), Vec4f(1.0f, 1.0f, 1.0f, 1.0f), Vec2f(0.0f, 0.0f)},
{Vec3f(-0.7f, -0.2f, 0.33f), Vec4f(-1.0f, -1.0f, -1.0f, 1.0f), Vec2f(0.0f, 1.0f)},
{Vec3f(-0.8f, 0.0f, 0.33f), Vec4f(-1.0f, -1.0f, -1.0f, 1.0f), Vec2f(1.0f, 0.0f)},
{Vec3f(-0.6f, 0.0f, 0.33f), Vec4f(-1.0f, -1.0f, -1.0f, 1.0f), Vec2f(0.0f, 0.0f)},
});
};
+14 -12
View File
@@ -840,22 +840,20 @@ class Pixel_History(rdtest.TestCase):
'event_name': 'Begin RenderPass',
'passed': True
},
{
'event_name': 'Per-Fragment discarding',
'passed': True,
'shader_out_col': fmt_adjusted(1, 1, 1, alpha_value),
'shader_out_depth': 0.33,
'post_mod_col': fmt_clamped(1, 1, 1, alpha_value),
'unknown_post_mod_stencil': True,
},
{
'event_name': 'Per-Fragment discarding',
'passed': False,
'shader_discarded': True,
'shader_out_col': (0, 0, 0, 0),
'shader_out_depth': -1,
'unknown_post_mod_stencil': True,
'primitive_id': 0,
},
{
'event_name': 'Per-Fragment discarding',
'passed': True,
'primitive_id': 1,
'shader_out_col': fmt_adjusted(1, 1, 1, alpha_value),
'shader_out_depth': 0.33,
'post_mod_col': fmt_clamped(1, 1, 1, alpha_value),
},
]
# can't distinguish per-fragment results in secondaries
@@ -973,10 +971,10 @@ class Pixel_History(rdtest.TestCase):
self.error(
f"postmod stencil at EID {m.eventId} primitive {m.primitiveID}: {m.postMod.stencil} is not unknown")
if not rdtest.value_compare(m.postMod.depth, n.preMod.depth):
if not rdtest.value_compare(m.postMod.depth, n.preMod.depth, eps = 1.0e-5):
self.error(
f"postmod depth at EID {m.eventId} primitive {m.primitiveID}: {m.postMod.depth} " +
f"doesn't match premod at next primitive {n.primitiveID}: {m.preMod.depth}")
f"doesn't match premod at next primitive {n.primitiveID}: {n.preMod.depth}")
epsilon = self.epsilon
@@ -984,6 +982,10 @@ class Pixel_History(rdtest.TestCase):
a = (m.postMod.depth, m.postMod.stencil)
b = (n.preMod.depth, n.preMod.stencil)
if a[1] == -2 or b[1] == -2:
a = (a[0], -2)
b = (b[0], -2)
epsilon = 1.0e-5
if not rdtest.value_compare(a, b, eps=epsilon):