Compare commits

...
22 Commits
Author SHA1 Message Date
swung0x48 90564aa82e [Test] (Review): give the geometry point-size case its own capability probe 2026-08-27 15:42:04 -04:00
swung0x48 7520607d47 [Fix] (Review): decline a point-size program the device cannot run, bound the compile-failure source dump, and correct the pass-through rationale 2026-08-27 15:42:04 -04:00
swung0x48 57635a9198 [Test] (Tessellation): unit and headless-GPU coverage for capturing a patch draw's per-vertex payload 2026-08-27 15:06:02 -04:00
swung0x48 c19d0f0b75 [Fix] (DirectVulkan): mirror gl_PointSize for capture and enable the feature its stages need 2026-08-27 15:06:02 -04:00
swung0x48 e42e7d00f5 [Fix] (DirectGLES): request the point-size extension a tessellation or geometry stage's ESSL needs 2026-08-27 15:06:01 -04:00
swung0x48 62695ee3c2 [Test] (Review): pin the single-sample sample mask, the completeness-flip sampled set, and the integer multisample placeholders 2026-08-27 13:17:56 -04:00
swung0x48 02cc0ce83c [Fix] (Review): scope the sample mask to a multisample target, version the sampled-set memo on completeness, clamp the mask word count, give the multisample placeholder every numeric domain, unwind the fixup revert one pass at a time, and bound the validator log 2026-08-27 13:03:51 -04:00
swung0x48 9e52a0b23e [Test] (DirectVulkan): pin the incomplete-default-texture draw, the unbound multisample sampler, and the unwritten redeclared gl_Position 2026-08-27 12:52:19 -04:00
swung0x48 9dee53337f [Fix] (DirectVulkan): give an unbound multisample sampler a placeholder, clear a multisample texture through a load-op pass, and plumb glSampleMaski into the pipeline 2026-08-27 12:52:19 -04:00
swung0x48 28c5badf8f [Fix] (SPIR-V): keep the entry-point interface consistent when the clip fixup and the XFB position mirror inject through gl_Position 2026-08-27 12:52:18 -04:00
swung0x48 01116f7b41 [Fix] (DirectVulkan): treat an incomplete sampled texture as unbound in the collect path too, and stop dereferencing a declined texture sync 2026-08-27 12:52:18 -04:00
swung0x48 05d627ba2d [Fix] (Xfb): never issue a transform feedback capture-point bind the application did not ask for, and error-check the driver span instead of losing it silently 2026-08-27 12:28:32 -04:00
swung0x48 ea5d52f126 [Test] (Tessellation): assert the patch-array cross-stage link outright now that the fork pin carries the guard 2026-08-27 10:52:00 -04:00
swung0x48 3c70b4fc0f [Fix] (Program): read an API colour index of zero as no override, matching the IO resolver and ProgramInterface 2026-08-27 10:52:00 -04:00
swung0x48 b6d6316333 [Fix] (Blend): decline a GL_SRC1_* factor on an incapable GLES driver even when blending is off 2026-08-27 10:51:59 -04:00
swung0x48 3c9ab5a68f [Test] (Blend): make the dual-source scenario's capability gate skip the case, not just the helper 2026-08-27 10:48:22 -04:00
swung0x48 532b5e9cc5 [Fix] (Program): let two fragment outputs share a colour number when their colour index differs 2026-08-27 10:48:22 -04:00
swung0x48 06605ed0ea [Test] (Tessellation): include the standard headers the link test uses directly 2026-08-27 10:48:21 -04:00
swung0x48 e1d5bdc4a5 [Fix] (Blend): decline an unsupported dual-source blend instead of throwing through the GL ABI 2026-08-27 10:48:21 -04:00
swung0x48 66867a41ba [Fix] (Program): count the tessellation control stage as a transform-feedback capture stage 2026-08-27 10:48:20 -04:00
swung0x48 ebff4b21f7 [Fix] (Query): tie the two tessellation pipeline-statistics targets to the GL_ARB_tessellation_shader advertisement 2026-08-27 10:48:20 -04:00
swung0x48 d4e7378868 [Chore] (ShaderTranspiler): pin glslang fork at d89cf443 (patch-array io-resize guard for the evaluation stage) 2026-08-27 10:22:37 -04:00
39 changed files with 5172 additions and 186 deletions
+317 -38
View File
@@ -393,6 +393,65 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
} }
// The capture points the CAPTURE PROGRAM uses, and nothing else.
//
// This used to go through SyncBufferBindingPoints, which walks the application's
// GLOBAL touched-binding-point high-water mark and binds 0 to every point with no
// frontend buffer. deqp/glcts permanently raises that mark to
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS by clearing all of them after each test
// case, so every capture using fewer points than that - i.e. every INTERLEAVED_ATTRIBS
// capture - had glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, i, 0) issued for the
// unused tail immediately before glBeginTransformFeedback. The Mali G1-Ultra driver
// then recorded NOTHING: no GL error, GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN 0, the
// application's buffer left holding its pre-draw bytes. Confirmed on device - the
// separate/interleaved split in KHR-GL46.transform_feedback follows exactly whether
// all four points were left bound.
//
// Those binds were never needed for correctness either. A capture only writes the
// points the program's buffer mode uses (GL 4.6 core 13.2.2), so a point past
// bufferCount cannot be written whatever is left bound there, and a point the program
// DOES use with no buffer bound is already an error the frontend raised at
// glBeginTransformFeedback. The rule this encodes: never issue a capture-point bind
// the application did not ask for.
//
// Scoping it to the program (rather than skipping redundant binds behind the shadow)
// is what makes it ORDER-INDEPENDENT: the shadow has to drop to unknown whenever a
// transform feedback OBJECT is bound, since the points belong to the object, and the
// clears came straight back for the next capture in the process.
void SyncTransformFeedbackBindingPoints(SizeT bufferCount) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const SizeT pointCount = std::min<SizeT>(
bufferCount, MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS);
for (SizeT i = 0; i < pointCount; ++i) {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback, i);
const auto& obj = point.GetBoundObject();
// A stride-0 slot (two consecutive gl_NextBuffer entries) captures nothing and
// needs no binding; anything else with no buffer never got past the frontend.
if (!obj) continue;
auto* backendResource = EnsureBufferResource(obj);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer for GL_TRANSFORM_FEEDBACK_BUFFER capture point %zu; the capture "
"will not reach the application's buffer.",
i);
continue;
}
const auto& range = point.GetRange();
const auto backendBufferId = backendResource->id;
if (range.start == 0 && range.end >= obj->GetSize()) {
BindBufferBaseCached(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLuint>(i), backendBufferId);
} else {
const auto start = std::min(range.start, obj->GetSize());
const auto end = std::min(range.end, obj->GetSize());
BindBufferRangeCached(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLuint>(i), backendBufferId,
static_cast<GLintptr>(start), static_cast<GLsizeiptr>(end - start));
}
}
}
// Called once the storage-buffer points are bound and the draw/dispatch is about to // Called once the storage-buffer points are bound and the draw/dispatch is about to
// go out: whatever the shader writes there lands in the ES driver's buffers, behind // go out: whatever the shader writes there lands in the ES driver's buffers, behind
// the frontend's CPU shadow. Flagging them makes the next MapBuffer/GetBufferSubData // the frontend's CPU shadow. Flagging them makes the next MapBuffer/GetBufferSubData
@@ -654,6 +713,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint backendId = 0; Uint backendId = 0;
SizeT start = 0; SizeT start = 0;
SizeT end = 0; SizeT end = 0;
// WHICH capture buffer of the program this is. The list is COMPACTED - a
// capture buffer with no bound buffer object contributes no entry - so the
// position in the vector is not the program's buffer index, and everything
// that asks the program about a target (its stride, which varyings land in
// it) has to ask about this index instead. A capture list beginning with
// gl_NextBuffer is the shape that makes them differ: buffer 0 has stride 0
// and nothing bound, so target 0 describes buffer 1.
SizeT bufferIndex = 0;
}; };
// Per frontend transform feedback object. The default object (name 0) maps to // Per frontend transform feedback object. The default object (name 0) maps to
@@ -698,6 +765,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
return *g_currentXfbState; return *g_currentXfbState;
} }
// EVERY way this path can lose a capture used to be silent: three unlogged early
// returns before the driver Begin, an unchecked glBeginTransformFeedback, and two
// `continue`s in the readback. The application sees a buffer that kept its
// pre-draw bytes, GL_NO_ERROR, and GL_LINK_STATUS true - which is how one defect
// reached ~320 conformance bodies across four families before anyone could say
// which of the branches fired. Nothing below changes what MobileGL DOES on a
// healthy capture; it only makes a lost one name itself in /sdcard/MG/latest.log.
//
// MGLOG_E_ONCE (not _D) on purpose: these have to be readable in an INFO-level
// artifact, the same reason the backend link failure at Managers.cpp is MGLOG_E.
constexpr Int kMaxDrainedXfbErrors = 32;
// The ES error raised by the call just issued, GL_NO_ERROR if it succeeded. Drains
// the rest of the queue so the next probe cannot read this one as its own.
GLenum TakeXfbDriverError() {
const GLenum first = g_GLESFuncs.glGetError();
if (first == GL_NO_ERROR) return GL_NO_ERROR;
for (Int i = 0; i < kMaxDrainedXfbErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
}
return first;
}
Bool AreTransformFeedbackObjectsSupported() { Bool AreTransformFeedbackObjectsSupported() {
return g_GLESFuncs.glGenTransformFeedbacks != nullptr && return g_GLESFuncs.glGenTransformFeedbacks != nullptr &&
g_GLESFuncs.glBindTransformFeedback != nullptr && g_GLESFuncs.glBindTransformFeedback != nullptr &&
@@ -712,6 +801,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the backend already owns (coherent persistent map) need nothing: reads resolve // the backend already owns (coherent persistent map) need nothing: reads resolve
// against that storage directly. // against that storage directly.
void ReadbackCapturedRanges(Vector<XfbCaptureTarget>& targets) { void ReadbackCapturedRanges(Vector<XfbCaptureTarget>& targets) {
if (g_GLESFuncs.glMapBufferRange == nullptr || g_GLESFuncs.glUnmapBuffer == nullptr) {
MGLOG_E_ONCE("EndTransformFeedback: the ES driver exposes no glMapBufferRange/glUnmapBuffer, so "
"captured data can never reach the application's buffers");
}
if (targets.empty()) {
// The span closed with nothing to mirror back. Either the deferred Begin
// never ran (a span with no draw - legal) or it ran and found no bound
// capture buffer, which is not.
MGLOG_D("EndTransformFeedback: capture span closed with no recorded targets");
}
if (g_GLESFuncs.glMapBufferRange != nullptr && g_GLESFuncs.glUnmapBuffer != nullptr) { if (g_GLESFuncs.glMapBufferRange != nullptr && g_GLESFuncs.glUnmapBuffer != nullptr) {
for (const auto& target : targets) { for (const auto& target : targets) {
if (!target.buffer || target.buffer->IsBackendPersistentMapped()) continue; if (!target.buffer || target.buffer->IsBackendPersistentMapped()) continue;
@@ -721,8 +820,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<GLintptr>(target.start), static_cast<GLintptr>(target.start),
static_cast<GLsizeiptr>(size), GL_MAP_READ_BIT); static_cast<GLsizeiptr>(size), GL_MAP_READ_BIT);
if (mapped == nullptr) { if (mapped == nullptr) {
MGLOG_E_ONCE("EndTransformFeedback: failed to map backend buffer %u for capture readback", // Silent before: the capture landed in the ES buffer and the
target.backendId); // application's next glMapBuffer read the untouched shadow, which
// is indistinguishable from "the draw wrote nothing".
MGLOG_E_ONCE("EndTransformFeedback: failed to map backend buffer %u [%zu, %zu) for "
"capture readback (ES error %s); the captured data will NOT be visible to "
"the application",
target.backendId, target.start, target.end,
MG_Util::ConvertGLEnumToString(TakeXfbDriverError()).c_str());
continue; continue;
} }
target.buffer->WritebackFromBackend({mapped, size}, target.start); target.buffer->WritebackFromBackend({mapped, size}, target.start);
@@ -755,15 +860,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
GL_DYNAMIC_COPY); GL_DYNAMIC_COPY);
g_scatterBufferSize = required; g_scatterBufferSize = required;
} }
// Point 0 carries every captured varying (the ES capture is INTERLEAVED); the // Point 0 carries every captured varying: the gl_NextBuffer / gl_SkipComponents
// other points must be cleared or the driver would still write the app's buffers. // entries are consumed at link time and never reach the driver, so the ES
// program is declared INTERLEAVED over a single buffer and point 0 is the only
// point it can write (GL 4.6 core 13.2.2).
//
// The other points are therefore left exactly as they are. Clearing them - which
// this used to do, across the application's whole touched high-water mark - is
// both unnecessary (the ES program cannot write an unused point) and the precise
// trigger for the Mali G1-Ultra capture loss: see
// SyncTransformFeedbackBindingPoints for the mechanism and the device evidence.
// KHR-GL46.transform_feedback.capture_special_interleaved_test is the case that
// reaches this path.
BufferImpl::BindBufferRangeCached(GL_TRANSFORM_FEEDBACK_BUFFER, 0, g_scatterBufferId, 0, BufferImpl::BindBufferRangeCached(GL_TRANSFORM_FEEDBACK_BUFFER, 0, g_scatterBufferId, 0,
static_cast<GLsizeiptr>(required)); static_cast<GLsizeiptr>(required));
const SizeT pointCount =
MG_State::pGLContext->GetTouchedBufferBindingPointCount(BufferTarget::TransformFeedback);
for (SizeT i = 1; i < pointCount; ++i) {
BufferImpl::BindBufferBaseCached(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<Uint>(i), 0);
}
return true; return true;
} }
@@ -777,10 +887,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (g_GLESFuncs.glMapBufferRange == nullptr || g_GLESFuncs.glUnmapBuffer == nullptr) return; if (g_GLESFuncs.glMapBufferRange == nullptr || g_GLESFuncs.glUnmapBuffer == nullptr) return;
const SizeT packedStride = program->GetTransformFeedbackPackedStride(); const SizeT packedStride = program->GetTransformFeedbackPackedStride();
const SizeT vertices = std::min<SizeT>( const SizeT modelledVertices =
static_cast<SizeT>(MG_State::pGLContext->GetTransformFeedbackCapturedVertices()), static_cast<SizeT>(MG_State::pGLContext->GetTransformFeedbackCapturedVertices());
xfb.scatterCapacityVertices); const SizeT vertices = std::min<SizeT>(modelledVertices, xfb.scatterCapacityVertices);
if (packedStride == 0 || vertices == 0) return; if (packedStride == 0 || vertices == 0) {
// The scatter path redirected the DRIVER's capture into the scratch buffer,
// so bailing here leaves the application's buffers holding their pre-draw
// bytes - a total data loss, not a no-op. The vertex count is the CPU model
// (AccountTransformFeedbackPrimitives), which is 0 for any draw mode
// CountPrimitivesForDraw does not know and for the instanced/indirect entry
// points that never call it.
MGLOG_E_ONCE("EndTransformFeedback: scattered capture discarded - packedStride=%zu, "
"CPU-modelled captured vertices=%zu, scratch capacity=%zu. The capture buffers keep "
"their pre-draw contents.",
packedStride, modelledVertices, xfb.scatterCapacityVertices);
return;
}
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId); BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId);
const void* packed = g_GLESFuncs.glMapBufferRange(BufferImpl::TempBufferTarget, 0, const void* packed = g_GLESFuncs.glMapBufferRange(BufferImpl::TempBufferTarget, 0,
@@ -798,14 +920,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (SizeT targetIndex = 0; targetIndex < xfb.targets.size(); ++targetIndex) { for (SizeT targetIndex = 0; targetIndex < xfb.targets.size(); ++targetIndex) {
const auto& target = xfb.targets[targetIndex]; const auto& target = xfb.targets[targetIndex];
if (!target.buffer) continue; if (!target.buffer) continue;
const SizeT stride = program->GetTransformFeedbackStride(static_cast<Uint32>(targetIndex)); // By BUFFER index, not by position in the compacted list - see XfbCaptureTarget.
const SizeT stride = program->GetTransformFeedbackStride(static_cast<Uint32>(target.bufferIndex));
if (stride == 0) continue; if (stride == 0) continue;
const SizeT rangeBytes = target.end - target.start; const SizeT rangeBytes = target.end - target.start;
Vector<Uint8> staged(rangeBytes); Vector<Uint8> staged(rangeBytes);
Memcpy(staged.data(), target.buffer->MappedData() + target.start, rangeBytes); Memcpy(staged.data(), target.buffer->MappedData() + target.start, rangeBytes);
for (const auto& varying : program->GetTransformFeedbackVaryings()) { for (const auto& varying : program->GetTransformFeedbackVaryings()) {
if (varying.bufferIndex != targetIndex) continue; if (varying.bufferIndex != target.bufferIndex) continue;
for (SizeT v = 0; v < vertices; ++v) { for (SizeT v = 0; v < vertices; ++v) {
const SizeT dstOffset = v * stride + varying.offsetBytes; const SizeT dstOffset = v * stride + varying.offsetBytes;
if (dstOffset + varying.byteSize > rangeBytes) break; if (dstOffset + varying.byteSize > rangeBytes) break;
@@ -860,9 +983,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
// not captured, and opening the span would also subject it to the capture // not captured, and opening the span would also subject it to the capture
// primitive-mode rule the paused draw is exempt from. // primitive-mode rule the paused draw is exempt from.
if (!xfb.pending || xfb.paused) return; if (!xfb.pending || xfb.paused) return;
xfb.pending = false;
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram(); const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
if (!program) return; if (!program) {
// The pending flag is deliberately NOT consumed here. It used to be cleared
// before this check, so a single draw that could not see the capture program
// retired the span permanently: every later draw of the same span found
// pending==false, the driver Begin never happened, and End found started==false
// and skipped the readback - a whole capture lost with no GL error anywhere.
// The frontend only reaches a draw with an active span after glBeginTransformFeedback
// stored a program, so this is a "cannot happen" that must stay recoverable.
MGLOG_E_ONCE("StartPendingTransformFeedback: an active capture span has no capture program; the "
"driver span stays closed and this draw is not captured");
return;
}
xfb.pending = false;
// Snapshot what the driver is about to capture into. GL forbids rebinding the // Snapshot what the driver is about to capture into. GL forbids rebinding the
// capture buffers while the span is open, so this stays valid until End, and // capture buffers while the span is open, so this stays valid until End, and
@@ -879,10 +1013,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT start = std::min(range.start, bufferObject->GetSize()); const SizeT start = std::min(range.start, bufferObject->GetSize());
const SizeT end = std::min(range.end, bufferObject->GetSize()); const SizeT end = std::min(range.end, bufferObject->GetSize());
if (end <= start) continue; if (end <= start) continue;
xfb.targets.push_back({bufferObject, backendResource->id, start, end}); xfb.targets.push_back({bufferObject, backendResource->id, start, end, i});
} }
BufferImpl::SyncBufferBindingPoints(BufferTarget::TransformFeedback, GL_TRANSFORM_FEEDBACK_BUFFER); BufferImpl::SyncTransformFeedbackBindingPoints(bufferCount);
// A layout with holes or several interleaved buffers is not expressible on ES: // A layout with holes or several interleaved buffers is not expressible on ES:
// capture gap-free into scratch storage and place the records at End instead. // capture gap-free into scratch storage and place the records at End instead.
@@ -891,31 +1025,99 @@ namespace MobileGL::MG_Backend::DirectGLES {
xfb.scatterCapacityVertices = 0; xfb.scatterCapacityVertices = 0;
if (program->NeedsScatteredTransformFeedbackCapture()) { if (program->NeedsScatteredTransformFeedbackCapture()) {
SizeT capacityVertices = ~SizeT(0); SizeT capacityVertices = ~SizeT(0);
for (SizeT i = 0; i < xfb.targets.size(); ++i) { for (const auto& target : xfb.targets) {
const SizeT stride = program->GetTransformFeedbackStride(static_cast<Uint32>(i)); // By BUFFER index. Reading the stride at the target's POSITION made a
// capture list beginning with gl_NextBuffer - buffer 0 has stride 0 and
// nothing bound, so target 0 describes buffer 1 - read stride 0, skip every
// target, and leave the capacity at zero.
const SizeT stride = program->GetTransformFeedbackStride(static_cast<Uint32>(target.bufferIndex));
if (stride == 0) continue; if (stride == 0) continue;
capacityVertices = capacityVertices = std::min<SizeT>(capacityVertices, (target.end - target.start) / stride);
std::min<SizeT>(capacityVertices, (xfb.targets[i].end - xfb.targets[i].start) / stride);
} }
if (capacityVertices == ~SizeT(0)) capacityVertices = 0; if (capacityVertices == ~SizeT(0)) capacityVertices = 0;
if (BindScatterCaptureBuffer(program->GetTransformFeedbackPackedStride(), capacityVertices)) { if (BindScatterCaptureBuffer(program->GetTransformFeedbackPackedStride(), capacityVertices)) {
xfb.scattered = true; xfb.scattered = true;
xfb.scatterProgram = program; xfb.scatterProgram = program;
xfb.scatterCapacityVertices = capacityVertices; xfb.scatterCapacityVertices = capacityVertices;
} else {
// NO SPAN RATHER THAN A SPAN THAT WRITES SOMEWHERE ELSE. The ES program for a
// scattered capture is a single-buffer INTERLEAVED one (the gl_NextBuffer /
// gl_SkipComponents entries are consumed at link time and never reach the
// driver), so it writes capture point 0 and nothing else. Point 0 here is
// either unbound or - the dangerous case - still holds whatever an earlier
// capture in this process bound there, because the frontend's own
// glBindBufferBase is state-only and nothing else in the backend touches the
// indexed points. Opening the span would then have the driver capture over an
// application buffer that has nothing to do with this draw, and the frontend
// shadow would never learn of it.
//
// Leaving the span closed reproduces exactly what the old high-water clear
// loop achieved by binding 0 here and letting the driver refuse the Begin -
// the capture records nothing - without issuing a capture-point bind the
// application did not ask for, which is the thing that loses captures whole
// on Mali (see SyncTransformFeedbackBindingPoints).
MGLOG_E_ONCE("StartPendingTransformFeedback: no scratch storage for a scattered capture "
"(packed stride %zu, capacity %zu vertices); leaving the driver span CLOSED so the "
"capture cannot land in a stale binding. Nothing will be captured.",
program->GetTransformFeedbackPackedStride(), capacityVertices);
xfb.targets.clear();
return;
} }
} }
// A capture program with buffers bound must have produced at least one target;
// an empty list means End has nothing to mirror back and the application will
// read its buffer's pre-draw bytes however well the GPU captured.
if (xfb.targets.empty()) {
MGLOG_E_ONCE("StartPendingTransformFeedback: opening a capture span with NO capture targets "
"(program declares %zu capture buffer(s), none of them resolved to a bound backend "
"buffer with a non-empty range); nothing will be read back",
bufferCount);
}
g_GLESFuncs.glBeginTransformFeedback(xfb.primitiveMode); g_GLESFuncs.glBeginTransformFeedback(xfb.primitiveMode);
// Unchecked before. Every ES error condition here (already active, a current
// program with no capture set, a capture point the program uses with no buffer)
// ends the same way: the driver records nothing, GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
// reads 0 and the application sees no error at all - MobileGL's own error state is
// separate from the driver's, so a driver rejection here is invisible to it.
if (const GLenum beginError = TakeXfbDriverError(); beginError != GL_NO_ERROR) {
// The mode is printed as a number as well as a name: GL_POINTS is 0, which the
// enum converter spells "GL_FALSE", and a reader chasing a lost capture should
// not have to know that.
MGLOG_E_ONCE("StartPendingTransformFeedback: the ES driver REJECTED "
"glBeginTransformFeedback(%s / 0x%04x) with %s - nothing will be captured. Backend "
"program %u, %zu capture buffer(s), %zu target(s), mode=%s.",
MG_Util::ConvertGLEnumToString(xfb.primitiveMode).c_str(),
static_cast<unsigned>(xfb.primitiveMode),
MG_Util::ConvertGLEnumToString(beginError).c_str(),
PrgramImpl::g_lastUsedBackendProgramId, bufferCount,
xfb.targets.size(),
MG_Util::ConvertGLEnumToString(program->GetTransformFeedbackBufferMode()).c_str());
}
xfb.started = true; xfb.started = true;
} }
void EndTransformFeedback() { void EndTransformFeedback() {
auto& xfb = CurrentXfb(); auto& xfb = CurrentXfb();
const Bool wasPending = xfb.pending;
xfb.pending = false; xfb.pending = false;
xfb.paused = false; xfb.paused = false;
if (!xfb.started) return; if (!xfb.started) {
// A span that never drew is legal and captures nothing by definition; one that
// is STILL pending here drew nothing the backend saw, which for a span the
// application expected data from is the whole bug in one line.
MGLOG_D("EndTransformFeedback: closing a span the driver never opened (pending=%d)",
wasPending ? 1 : 0);
return;
}
xfb.started = false; xfb.started = false;
g_GLESFuncs.glEndTransformFeedback(); g_GLESFuncs.glEndTransformFeedback();
if (const GLenum endError = TakeXfbDriverError(); endError != GL_NO_ERROR) {
MGLOG_E_ONCE("EndTransformFeedback: the ES driver rejected glEndTransformFeedback with %s - the "
"driver's capture state and MobileGL's have diverged",
MG_Util::ConvertGLEnumToString(endError).c_str());
}
if (xfb.scattered) { if (xfb.scattered) {
ScatterCapturedRecords(xfb); ScatterCapturedRecords(xfb);
xfb.scattered = false; xfb.scattered = false;
@@ -943,6 +1145,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BindTransformFeedback(GLuint name) { void BindTransformFeedback(GLuint name) {
g_currentXfbState = nullptr; // name changes; operator[] below may also rehash g_currentXfbState = nullptr; // name changes; operator[] below may also rehash
// The capture buffer bindings are the OBJECT's, not the context's: the bind below
// swaps all of them for whatever the target object holds, which the redundant-bind
// shadow has never seen.
BufferImpl::InvalidateTransformFeedbackBindingShadows();
if (!AreTransformFeedbackObjectsSupported()) { if (!AreTransformFeedbackObjectsSupported()) {
// Without driver objects there is only the default span; keep the frontend // Without driver objects there is only the default span; keep the frontend
// name so the bookkeeping below stays consistent. // name so the bookkeeping below stays consistent.
@@ -979,6 +1185,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_currentXfbName = 0; g_currentXfbName = 0;
g_scatterBufferId = 0; g_scatterBufferId = 0;
g_scatterBufferSize = 0; g_scatterBufferSize = 0;
BufferImpl::InvalidateTransformFeedbackBindingShadows();
} }
} // namespace XfbImpl } // namespace XfbImpl
@@ -1772,6 +1979,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// clear-then-draw pair on an unchanged parameter block early-outs and the draw inherits // clear-then-draw pair on an unchanged parameter block early-outs and the draw inherits
// the clear's undoctored mask. // the clear's undoctored mask.
static Uint32 g_syncedColorMaskAlphaWidenMask = 0; static Uint32 g_syncedColorMaskAlphaWidenMask = 0;
// Scratch for the dual-source-blend decline path in the blend block below. File-scope
// rather than a local so the ordinary draw pays nothing for it: it is written only on a
// driver with no GL_EXT_blend_func_extended that is also handed a GL_SRC1_* factor, and
// SyncRenderState runs on the GL thread only.
static Array<PerBufferBlendState, MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS>
g_dualSourceDeclinedBlendStates;
void InvalidateSyncedRenderState() { void InvalidateSyncedRenderState() {
g_forceFullRenderStateResync = true; g_forceFullRenderStateResync = true;
g_hasSyncedRenderState = false; g_hasSyncedRenderState = false;
@@ -1931,31 +2144,85 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& ToGLBoolean = [](Bool b) -> GLboolean { return b ? GL_TRUE : GL_FALSE; }; const auto& ToGLBoolean = [](Bool b) -> GLboolean { return b ? GL_TRUE : GL_FALSE; };
// Which draw buffers the blend block below DECLINED (see it for why). Needed again at
// the shadow write-back at the end of this function: the span memcpy there clones the
// FRONTEND block, which for a declined draw buffer is not what the driver was handed.
Uint32 dualSourceDeclinedMask = 0;
if (blendSpanDirty) { // Blend State if (blendSpanDirty) { // Blend State
using FBO = MG_State::GLState::FramebufferObject; using FBO = MG_State::GLState::FramebufferObject;
const auto& targetStates = parameters.BlendStates;
auto& syncedStates = g_syncedRenderStateParameters.BlendStates; auto& syncedStates = g_syncedRenderStateParameters.BlendStates;
// Dual-source blending (GL_SRC1_* factors from glBlendFunc paired with // Dual-source blending (GL_SRC1_* factors from glBlendFunc paired with
// glBindFragDataLocationIndexed) needs GL_EXT_blend_func_extended; GLES core has none. // glBindFragDataLocationIndexed) needs GL_EXT_blend_func_extended; GLES core has none.
// Detected at load and surfaced in the POST. There is no fallback, so if a draw actually // Detected at load and surfaced in the POST. There is no fallback that BLENDS
// enables blending with a SRC1 factor on a driver that lacks it, hard-fail here at use // correctly, so a draw that asks for a SRC1 factor on a driver without the extension
// time rather than let the driver reject glBlendFuncSeparate and silently mis-blend. // gets the blend DECLINED: that draw buffer is pushed with blending off and neutral
// One/Zero factors, and the loss is logged once. The two rejected alternatives are
// both worse - pushing GL_SRC1_* at glBlendFuncSeparate leaves the driver to raise
// GL_INVALID_ENUM and keep whatever factors were there before (a silent mis-blend
// against stale state), and throwing, which is what this did until now, takes the
// whole process down over one unsupported blend factor. Declining is defined,
// survivable and visible in the log.
//
// NOT gated on Enabled, deliberately, and the same way the Vulkan twin is not gated
// on effectiveBlendEnabled: what has to be kept away from the driver is the FACTOR
// ENUM, and the factor push below never consults Enabled - one glBlendFuncSeparate
// serves every draw buffer when they agree, and the per-index arm diffs factors
// alone. So `glDisable(GL_BLEND); glBlendFunc(GL_SRC1_ALPHA, ...)` followed by any
// draw OR clear would otherwise hand a GL_SRC1_ALPHA to a driver that answers
// GL_INVALID_ENUM, leaving a spurious error in ITS queue for the next internal
// no-error probe to read as its own, and leaving this shadow recording factors the
// ES context rejected. Blending being off makes the picture unaffected; it does not
// make the enum acceptable.
const auto* effectiveBlendStates = &parameters.BlendStates;
if (!g_GLESCapabilities.SupportsDualSourceBlend) { if (!g_GLESCapabilities.SupportsDualSourceBlend) {
Uint32 declinedWithBlendingOnMask = 0;
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
const auto& s = targetStates[i]; const auto& s = parameters.BlendStates[i];
if (s.Enabled && if (IsDualSourceBlendFactor(s.SrcFactorRGB) || IsDualSourceBlendFactor(s.DstFactorRGB) ||
(IsDualSourceBlendFactor(s.SrcFactorRGB) || IsDualSourceBlendFactor(s.DstFactorRGB) || IsDualSourceBlendFactor(s.SrcFactorAlpha) || IsDualSourceBlendFactor(s.DstFactorAlpha)) {
IsDualSourceBlendFactor(s.SrcFactorAlpha) || IsDualSourceBlendFactor(s.DstFactorAlpha))) { dualSourceDeclinedMask |= 1u << i;
THROW_EXCEPTION( if (s.Enabled) declinedWithBlendingOnMask |= 1u << i;
"Dual-source blending (GL_SRC1_* blend factor) was used on draw buffer " +
std::to_string(i) +
", but the GLES driver does not expose GL_EXT_blend_func_extended (see the "
"dual-source blend row in the driver POST). No fallback exists; the draw "
"cannot proceed.");
} }
} }
if (dualSourceDeclinedMask != 0) {
// Two masks in the message because they mean different things to whoever
// reads the log: the second one is where a PICTURE was lost. A draw buffer
// in the first mask but not the second had blending off anyway, so nothing
// was blended and nothing was dropped - only the unusable enum was kept out
// of the driver.
MGLOG_E_ONCE(
"SyncRenderState: a GL_SRC1_* (dual-source) blend factor was set on draw buffer "
"mask 0x%x, but the GLES driver does not expose GL_EXT_blend_func_extended (see "
"the dual-source blend row in the driver POST). Those draw buffers are pushed "
"with neutral One/Zero factors instead. Blending was actually ENABLED on mask "
"0x%x, and only there is anything lost: the fragment's first output is written "
"unblended and the second source is dropped.",
dualSourceDeclinedMask, declinedWithBlendingOnMask);
g_dualSourceDeclinedBlendStates = parameters.BlendStates;
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
if ((dualSourceDeclinedMask & (1u << i)) == 0) continue;
auto& s = g_dualSourceDeclinedBlendStates[i];
// Both halves, for the same reason the Vulkan arm neutralises both: the
// enable so nothing blends against a source the driver cannot produce,
// the factors so no GL_SRC1_* enum is ever handed over. Clearing Enabled
// on a buffer that was already off is a no-op, which is what makes one
// ungated rule serve both cases.
s.Enabled = false;
s.SrcFactorRGB = BlendFactor::One;
s.DstFactorRGB = BlendFactor::Zero;
s.SrcFactorAlpha = BlendFactor::One;
s.DstFactorAlpha = BlendFactor::Zero;
}
effectiveBlendStates = &g_dualSourceDeclinedBlendStates;
}
} }
// The rest of the block reads the EFFECTIVE state. The per-field writes it makes
// into `syncedStates` are provisional - the span memcpy at the end of this function
// overwrites the whole blend span with the frontend's own bytes - so the declined
// draw buffers are put back there, see the write-back below.
const auto& targetStates = *effectiveBlendStates;
Bool allEnabled = true; Bool allEnabled = true;
Bool allDisabled = true; Bool allDisabled = true;
@@ -2355,6 +2622,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (blendSpanDirty) { if (blendSpanDirty) {
std::memcpy(syncedBytesMut + kBlendSpanBegin, currentBytes + kBlendSpanBegin, std::memcpy(syncedBytesMut + kBlendSpanBegin, currentBytes + kBlendSpanBegin,
kBlendSpanEnd - kBlendSpanBegin); kBlendSpanEnd - kBlendSpanBegin);
// ...except for a draw buffer whose dual-source blend was DECLINED, where the
// frontend block is precisely what did NOT reach the driver. The shadow has to hold
// what was pushed or the next diff compares against state the ES context never got:
// going from a SRC1 factor to an ordinary one leaves Enabled equal on both sides,
// the enable block finds nothing to do, and blending stays off from the decline.
// The span stays permanently "dirty" against the frontend as a result, which costs
// one memcmp plus this block per render-state VERSION change - the top-of-function
// version early-out still skips repeat draws entirely.
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
if ((dualSourceDeclinedMask & (1u << i)) == 0) continue;
g_syncedRenderStateParameters.BlendStates[i] = g_dualSourceDeclinedBlendStates[i];
}
} }
if (tailSpanDirty) { if (tailSpanDirty) {
std::memcpy(syncedBytesMut + kBlendSpanEnd, currentBytes + kBlendSpanEnd, std::memcpy(syncedBytesMut + kBlendSpanEnd, currentBytes + kBlendSpanEnd,
+176 -7
View File
@@ -1383,10 +1383,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
constexpr SizeT kMaxIndexedBufferBindings = 64; constexpr SizeT kMaxIndexedBufferBindings = 64;
IndexedBufferBinding g_indexedUBOBindings[kMaxIndexedBufferBindings]; IndexedBufferBinding g_indexedUBOBindings[kMaxIndexedBufferBindings];
IndexedBufferBinding g_indexedSSBOBindings[kMaxIndexedBufferBindings]; IndexedBufferBinding g_indexedSSBOBindings[kMaxIndexedBufferBindings];
// Transform feedback gets a shadow for a reason the other two do not have: the
// capture points are synced from the application's TOUCHED high-water mark, which
// deqp/glcts permanently raises to GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS by
// clearing every point after each test case. Without a shadow every capture that
// uses fewer points than that (i.e. every INTERLEAVED_ATTRIBS capture) re-issued a
// redundant glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, i, 0) for the unused
// tail immediately before glBeginTransformFeedback - calls a plain GL application
// never makes there, and the only thing MobileGL does differently from one.
//
// Unlike the UBO/SSBO points these are NOT context state: they belong to the bound
// transform feedback OBJECT, so XfbImpl::BindTransformFeedback drops the whole
// shadow to unknown on every object switch (InvalidateTransformFeedbackBindingShadows).
IndexedBufferBinding g_indexedXFBBindings[kMaxIndexedBufferBindings];
IndexedBufferBinding* IndexedBindingShadow(GLenum glTarget, Uint index) { IndexedBufferBinding* IndexedBindingShadow(GLenum glTarget, Uint index) {
if (index >= kMaxIndexedBufferBindings) return nullptr; // out of range: never cache if (index >= kMaxIndexedBufferBindings) return nullptr; // out of range: never cache
if (glTarget == GL_UNIFORM_BUFFER) return &g_indexedUBOBindings[index]; if (glTarget == GL_UNIFORM_BUFFER) return &g_indexedUBOBindings[index];
if (glTarget == GL_SHADER_STORAGE_BUFFER) return &g_indexedSSBOBindings[index]; if (glTarget == GL_SHADER_STORAGE_BUFFER) return &g_indexedSSBOBindings[index];
if (glTarget == GL_TRANSFORM_FEEDBACK_BUFFER) return &g_indexedXFBBindings[index];
return nullptr; return nullptr;
} }
@@ -1403,6 +1417,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (auto& binding : g_indexedSSBOBindings) { for (auto& binding : g_indexedSSBOBindings) {
if (binding.id == id) binding = {}; if (binding.id == id) binding = {};
} }
for (auto& binding : g_indexedXFBBindings) {
if (binding.id == id) binding = {};
}
if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) { if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) {
g_boundPixelPackBufferId = 0; g_boundPixelPackBufferId = 0;
} }
@@ -1419,9 +1436,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (auto& binding : g_indexedSSBOBindings) { for (auto& binding : g_indexedSSBOBindings) {
if (binding.id == id) binding.known = false; if (binding.id == id) binding.known = false;
} }
for (auto& binding : g_indexedXFBBindings) {
if (binding.id == id) binding.known = false;
}
} }
} // namespace } // namespace
// The capture points belong to the bound transform feedback object, so a bind (or a
// delete, which reverts to the default object) replaces all of them at once with
// state this shadow has never seen. Distrust rather than scrub: the driver's bindings
// are whatever the newly bound object holds, which is NOT necessarily base(0), and
// scrubbing would let a later bind of 0 be false-skipped.
void InvalidateTransformFeedbackBindingShadows() {
for (auto& binding : g_indexedXFBBindings) binding.known = false;
}
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id) { void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id) {
auto* s = IndexedBindingShadow(glTarget, index); auto* s = IndexedBindingShadow(glTarget, index);
if (s && s->known && s->isBase && s->id == id) return; if (s && s->known && s->isBase && s->id == id) return;
@@ -1439,6 +1468,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void InvalidateIndexedBufferBindingCache() { void InvalidateIndexedBufferBindingCache() {
for (auto& b : g_indexedUBOBindings) b = {}; for (auto& b : g_indexedUBOBindings) b = {};
for (auto& b : g_indexedSSBOBindings) b = {}; for (auto& b : g_indexedSSBOBindings) b = {};
for (auto& b : g_indexedXFBBindings) b = {};
} }
void TrimBufferPool() { void TrimBufferPool() {
@@ -5753,6 +5783,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint g_fragColorBroadcastCount = 1; Uint g_fragColorBroadcastCount = 1;
Uint32 g_unormFallbackClampOutputMask = 0; Uint32 g_unormFallbackClampOutputMask = 0;
Uint g_lastUsedBackendProgramId = 0; Uint g_lastUsedBackendProgramId = 0;
// Every error-queue drain in the program build path is bounded by this: a lost
// context never answers GL_NO_ERROR, and the build runs on the thread that would
// then spin forever.
constexpr Int kMaxDrainedProgramErrors = 32;
StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl> g_backendProgramObjects; StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl> g_backendProgramObjects;
BackendProgramObjectImpl::BackendProgramObjectImpl() { BackendProgramObjectImpl::BackendProgramObjectImpl() {
@@ -6812,10 +6846,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
const String outMembers = const String outMembers =
ExtractPerVertexBlockMembers(tessEvalStageEssl, /*input=*/true).value_or(String()); ExtractPerVertexBlockMembers(tessEvalStageEssl, /*input=*/true).value_or(String());
const String source = BuildPassthroughTessControlEssl(ResolveBackendEsslVersion(), patchVertices, String source = BuildPassthroughTessControlEssl(ResolveBackendEsslVersion(), patchVertices,
inMembers, outMembers, inMembers, outMembers,
m_passthroughTessControlOuterLevel, m_passthroughTessControlOuterLevel,
m_passthroughTessControlInnerLevel); m_passthroughTessControlInnerLevel);
// The mirrored member lists can carry gl_PointSize - the neighbour stage declared it,
// so matching it is the whole point - and a redeclaration is exactly as illegal as a
// reference in ESSL without the extension. Same directive, same never-speculative
// rule as the per-stage loop; a driver with neither spelling gets nothing added and
// fails below with its own message, which is the honest outcome for a shape it
// cannot express.
const char* passthroughPointSizeExtension =
source.find("gl_PointSize") != String::npos
? PointSizeExtensionName(g_GLESCapabilities.TessellationPointSizeSupport, /*tessellation=*/true)
: nullptr;
source = RequestPointSizeExtension(Move(source), passthroughPointSizeExtension);
const GLuint backendShaderId = g_GLESFuncs.glCreateShader(GL_TESS_CONTROL_SHADER); const GLuint backendShaderId = g_GLESFuncs.glCreateShader(GL_TESS_CONTROL_SHADER);
if (backendShaderId == 0) { if (backendShaderId == 0) {
@@ -7339,6 +7384,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
source.find("gl_ViewportIndex") != String::npos; source.find("gl_ViewportIndex") != String::npos;
source = RequestViewportArrayExtension(std::move(source), needsViewportArrayExtension); source = RequestViewportArrayExtension(std::move(source), needsViewportArrayExtension);
// The fourth header-level rewrite, and the same shape as the third: ESSL has no
// gl_PointSize in a tessellation or geometry stage at ANY version - 320 makes the
// stages core and still leaves the built-in behind EXT/OES_..._point_size - while
// SPIRV-Cross prints it bare. Without the directive the stage fails to compile
// with "`gl_PointSize' undeclared", which takes the whole program to program 0:
// the draw renders nothing AND glBeginTransformFeedback is rejected, so a capture
// of anything at all off that program silently comes back empty. The token probe
// keeps the line off every other program and PointSizeExtensionName returns
// nullptr - i.e. nothing is emitted - on a driver advertising neither spelling.
if (source.find("gl_PointSize") != String::npos) {
const Bool tessellationStage = glShaderType == GL_TESS_CONTROL_SHADER ||
glShaderType == GL_TESS_EVALUATION_SHADER;
if (tessellationStage || glShaderType == GL_GEOMETRY_SHADER) {
const auto tier = tessellationStage ? g_GLESCapabilities.TessellationPointSizeSupport
: g_GLESCapabilities.GeometryPointSizeSupport;
const char* pointSizeExtension = PointSizeExtensionName(tier, tessellationStage);
if (pointSizeExtension == nullptr) {
// Latched, and an ERROR rather than a warning: what follows is a
// driver compile failure whose text names a built-in the application
// never mis-spelled, and the reason is a missing driver capability
// rather than anything in the shader. Saying so here is the whole
// difference between a legible skip and an unexplained black draw.
MGLOG_E_ONCE("This driver advertises neither the EXT nor the OES %s_point_size "
"extension, so its ESSL has no gl_PointSize in a %s stage; program %u "
"will fail to compile. Point size from a non-vertex stage is not "
"available on this device.",
tessellationStage ? "tessellation" : "geometry",
tessellationStage ? "tessellation" : "geometry",
stateProgramObject->GetExternalIndex());
}
source = RequestPointSizeExtension(std::move(source), pointSizeExtension);
}
}
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject); source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
// The completion half of the format bake, for the formats SPIRV-Cross throws on // The completion half of the format bake, for the formats SPIRV-Cross throws on
// rather than prints (r8ui and the rest of its desktop-only set). Empty for every // rather than prints (r8ui and the rest of its desktop-only set). Empty for every
@@ -7460,12 +7539,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
// carried 294 INFO lines and zero ERROR lines while two generated shaders // carried 294 INFO lines and zero ERROR lines while two generated shaders
// were being rejected outright, and the lane could not say why it was // were being rejected outright, and the lane could not say why it was
// rendering an empty translucent layer. A shader the driver refuses is // rendering an empty translucent layer. A shader the driver refuses is
// never noise, and one line per refused shader is bounded by program count. // never noise.
//
// A BOUNDED EXCERPT of the source goes with it. The driver log names a line
// and a column in text that exists nowhere but here, so without any source at
// all the only way to read "`gl_PointSize' undeclared" is to rebuild the whole
// library at DEBUG - but the full dump cannot go at E either. This is not
// "one line per refused shader": SyncToBackend's rebuild gate keys on
// per-draw state (the enabled-draw-buffer count among it), so a program used
// across passes with different draw-buffer counts re-transpiles, re-compiles
// and re-fails on every alternation, i.e. per frame. At E - live at the
// production INFO level - each of those records would push the whole
// post-SPIRV-Cross ESSL through the global log mutex with a forced flush onto
// /sdcard/MG/latest.log, the file users are asked to share. The excerpt keeps
// the record O(1); the full text is still there at D, printed against this
// same backend shader id by the "Setting shader source" line above, so
// nothing needs to be dumped twice.
constexpr SizeT kMaxLoggedSourceBytes = 2048;
String truncatedSource;
const char* sourceForLog = source.c_str();
if (source.size() > kMaxLoggedSourceBytes) {
// Back up to a line boundary when there is one inside the window, so the
// excerpt ends on a whole statement rather than mid-token. Built only on
// this branch: a stage that fits keeps its own buffer and is not copied.
SizeT cut = kMaxLoggedSourceBytes;
if (const SizeT lastNewline = source.rfind('\n', cut);
lastNewline != String::npos && lastNewline > 0) {
cut = lastNewline + 1;
}
truncatedSource = source.substr(0, cut);
truncatedSource += "... [" + std::to_string(source.size() - cut) +
" more bytes; the whole stage is printed at the DEBUG level]\n";
sourceForLog = truncatedSource.c_str();
}
MGLOG_E("Shader compilation failed. State program ID: %u, stage: %s, backend shader ID: " MGLOG_E("Shader compilation failed. State program ID: %u, stage: %s, backend shader ID: "
"%u, driver log: %s", "%u, driver log: %s\nSource:\n%s",
stateProgramObject->GetExternalIndex(), stateProgramObject->GetExternalIndex(),
MG_Util::ConvertGLEnumToString(glShaderType).c_str(), backendShaderId, MG_Util::ConvertGLEnumToString(glShaderType).c_str(), backendShaderId,
log.data()); log.data(), sourceForLog);
m_backendProgramUsable = false; m_backendProgramUsable = false;
// Nothing will ever attach this one, so nothing else can free it. // Nothing will ever attach this one, so nothing else can free it.
g_GLESFuncs.glDeleteShader(backendShaderId); g_GLESFuncs.glDeleteShader(backendShaderId);
@@ -7523,6 +7634,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// program before it links. SPIRV-Cross keeps user output names verbatim in // program before it links. SPIRV-Cross keeps user output names verbatim in
// the transpiled ESSL (`out vec4 result_0;` stays `result_0`), so the // the transpiled ESSL (`out vec4 result_0;` stays `result_0`), so the
// frontend's requested names carry over unchanged. // frontend's requested names carry over unchanged.
SizeT declaredXfbVaryingCount = 0;
if (stateProgramObject->GetTransformFeedbackVaryingCount() > 0 && if (stateProgramObject->GetTransformFeedbackVaryingCount() > 0 &&
g_GLESFuncs.glTransformFeedbackVaryings != nullptr) { g_GLESFuncs.glTransformFeedbackVaryings != nullptr) {
const auto& xfbVaryings = stateProgramObject->GetTransformFeedbackVaryings(); const auto& xfbVaryings = stateProgramObject->GetTransformFeedbackVaryings();
@@ -7548,9 +7660,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
MGLOG_D("Declaring %zu transform feedback varyings on program %u", xfbNames.size(), MGLOG_D("Declaring %zu transform feedback varyings on program %u", xfbNames.size(),
m_backendProgramId); m_backendProgramId);
// Bounded: a lost context never answers GL_NO_ERROR, and this runs on the
// thread that would then spin forever.
for (Int i = 0; i < kMaxDrainedProgramErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
}
g_GLESFuncs.glTransformFeedbackVaryings(m_backendProgramId, static_cast<GLsizei>(xfbNames.size()), g_GLESFuncs.glTransformFeedbackVaryings(m_backendProgramId, static_cast<GLsizei>(xfbNames.size()),
xfbNames.data(), xfbNames.data(),
stateProgramObject->GetTransformFeedbackBufferMode()); stateProgramObject->GetTransformFeedbackBufferMode());
// Unchecked before. A rejected capture set leaves the program linking happily
// with NO capture set at all, and then every draw of every span records
// nothing while the application reads its buffer's pre-draw bytes and
// GL_NO_ERROR - the signature four conformance families were stuck on.
if (const GLenum xfbError = g_GLESFuncs.glGetError(); xfbError != GL_NO_ERROR) {
String declared;
for (const auto& xfbName : rewrittenXfbNames) {
if (!declared.empty()) declared += ", ";
declared += xfbName;
}
MGLOG_E("The ES driver REJECTED the transform feedback capture set for backend program %u with "
"%s (mode %s): [%s]. Every capture made with GL program %u will record nothing.",
m_backendProgramId, MG_Util::ConvertGLEnumToString(xfbError).c_str(),
MG_Util::ConvertGLEnumToString(
stateProgramObject->GetTransformFeedbackBufferMode()).c_str(),
declared.c_str(), stateProgramObject->GetExternalIndex());
}
declaredXfbVaryingCount = xfbNames.size();
} }
// Link program // Link program
@@ -7593,6 +7727,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
} else { } else {
MGLOG_D("Program linked successfully. ID: %u", m_backendProgramId); MGLOG_D("Program linked successfully. ID: %u", m_backendProgramId);
// A link that SUCCEEDS can still have dropped the capture set: ESSL rejects a
// requested name the transpiled shader does not actually declare by simply not
// capturing it, and a program whose last vertex-processing stage was rewritten
// by a SPIR-V pass (viewport-index lowering, gl_PerVertex handling, the
// synthesized pass-through tessellation control stage) can end up spelling its
// outputs differently from the frontend's request. Asking the driver what it
// ACTUALLY linked is the only way to tell that apart from a driver that just
// captures nothing - which is the whole ambiguity the empty-capture failures
// across geometry_shader / tessellation_shader / gpu_shader5 / DSA sat on.
if (declaredXfbVaryingCount > 0) {
GLint linkedXfbVaryings = 0;
GLint linkedXfbBufferMode = 0;
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_TRANSFORM_FEEDBACK_VARYINGS,
&linkedXfbVaryings);
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_TRANSFORM_FEEDBACK_BUFFER_MODE,
&linkedXfbBufferMode);
for (Int i = 0; i < kMaxDrainedProgramErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
}
const GLenum requestedMode = stateProgramObject->GetTransformFeedbackBufferMode();
if (static_cast<SizeT>(std::max(linkedXfbVaryings, 0)) != declaredXfbVaryingCount ||
static_cast<GLenum>(linkedXfbBufferMode) != requestedMode) {
MGLOG_E("Backend program %u (GL program %u) linked with a capture set the driver does not "
"agree with: asked for %zu varying(s) in mode %s, the driver reports %d varying(s) "
"in mode %s. Captures made with it will be empty or wrongly laid out.",
m_backendProgramId, stateProgramObject->GetExternalIndex(), declaredXfbVaryingCount,
MG_Util::ConvertGLEnumToString(requestedMode).c_str(), linkedXfbVaryings,
MG_Util::ConvertGLEnumToString(
static_cast<GLenum>(linkedXfbBufferMode)).c_str());
} else {
MGLOG_D("Backend program %u capture set confirmed by the driver: %d varying(s), mode %s",
m_backendProgramId, linkedXfbVaryings,
MG_Util::ConvertGLEnumToString(
static_cast<GLenum>(linkedXfbBufferMode)).c_str());
}
}
} }
// The driver program was relinked IN PLACE, so its GL name no longer identifies // The driver program was relinked IN PLACE, so its GL name no longer identifies
// the executable behind it - and that name is exactly what Use()'s // the executable behind it - and that name is exactly what Use()'s
+12 -3
View File
@@ -544,12 +544,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
// BackendVertexArrayObject::SyncToBackend. // BackendVertexArrayObject::SyncToBackend.
extern Uint64 g_bufferBackendIdGeneration; extern Uint64 g_bufferBackendIdGeneration;
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on // Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the // GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER / GL_TRANSFORM_FEEDBACK_BUFFER):
// (id, range) already at that index matches, like the array-buffer/texture/ // skips the GL call when the (id, range) already at that index matches, like the
// sampler caches already do. Invalidated on MakeCurrent (context may reset). // array-buffer/texture/sampler caches already do. Invalidated on MakeCurrent
// (context may reset).
// Binds the transform feedback capture points [0, bufferCount) from the frontend
// state, and touches nothing else - in particular it never binds a zero the
// application did not ask for. See the definition for why that matters on Mali.
void SyncTransformFeedbackBindingPoints(SizeT bufferCount);
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id); void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size); void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
void InvalidateIndexedBufferBindingCache(); void InvalidateIndexedBufferBindingCache();
// The transform feedback capture points are per-transform-feedback-OBJECT state, so
// every glBindTransformFeedback swaps all of them under the shadow above. XfbImpl
// calls this on each bind/delete.
void InvalidateTransformFeedbackBindingShadows();
// Re-issues the GL_ATOMIC_COUNTER_BUFFER binding points a program's shaders declare as // Re-issues the GL_ATOMIC_COUNTER_BUFFER binding points a program's shaders declare as
// GL_SHADER_STORAGE_BUFFER bindings at the reserved slots the transpiled ESSL was built // GL_SHADER_STORAGE_BUFFER bindings at the reserved slots the transpiled ESSL was built
// against (BackendProgramObjectImpl::GetAtomicCounterBindings / // against (BackendProgramObjectImpl::GetAtomicCounterBindings /
+41
View File
@@ -713,6 +713,47 @@ namespace MobileGL::MG_Backend::DirectGLES {
return glslCode; return glslCode;
} }
const char* PointSizeExtensionName(MG_External::GLESCapabilities::PointSizeTier tier, Bool tessellation) {
using Tier = MG_External::GLESCapabilities::PointSizeTier;
switch (tier) {
case Tier::ExtensionEXT:
return tessellation ? "GL_EXT_tessellation_point_size" : "GL_EXT_geometry_point_size";
case Tier::ExtensionOES:
return tessellation ? "GL_OES_tessellation_point_size" : "GL_OES_geometry_point_size";
default:
return nullptr;
}
}
String RequestPointSizeExtension(String glslCode, const char* extensionName) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// The gl_ViewportIndex story, one built-in over: ESSL 320 makes the tessellation and
// geometry STAGES core but leaves gl_PointSize out of their gl_PerVertex entirely,
// and SPIRV-Cross - which only ever sees a SPIR-V BuiltIn PointSize decoration -
// prints the identifier with no directive behind it. Same hard rule as the two
// neighbours: never emitted speculatively, because `#extension` on a name the driver
// does not advertise is a compile error of its own.
if (extensionName == nullptr || glslCode.find(extensionName) != String::npos) {
return glslCode;
}
const String directive = String("#extension ") + extensionName + " : require\n";
// Right after the #version line, the one position that must stay first;
// ForceSupporterOutput's scan for the LAST #extension directive still finds
// whichever one that ends up being.
const SizeT versionPos = glslCode.find("#version");
if (versionPos == String::npos) {
return directive + glslCode;
}
const SizeT lineEnd = glslCode.find('\n', versionPos);
if (lineEnd == String::npos) {
return glslCode + "\n" + directive;
}
glslCode.insert(lineEnd + 1, directive);
return glslCode;
}
String BakeImageFormatQualifiers(String glslCode, String BakeImageFormatQualifiers(String glslCode,
const UnorderedMap<String, String>& esslFormatByUniformName) { const UnorderedMap<String, String>& esslFormatByUniformName) {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
+16
View File
@@ -273,6 +273,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
// error, so this is never emitted speculatively. A no-op when not needed or already // error, so this is never emitted speculatively. A no-op when not needed or already
// present. // present.
String RequestViewportArrayExtension(String glslCode, Bool needed); String RequestViewportArrayExtension(String glslCode, Bool needed);
// Adds `#extension <extensionName> : require` when a TESSELLATION or GEOMETRY stage's
// emitted ESSL names gl_PointSize. Desktop GL has that built-in in gl_PerVertex for every
// vertex-processing stage; ESSL does NOT have it in those two at any version - not even
// 320, where the stages themselves are core - until EXT/OES_tessellation_point_size resp.
// EXT/OES_geometry_point_size is requested. SPIRV-Cross prints the identifier bare and
// asks for nothing, exactly as it does for gl_ViewportIndex, so without this the stage
// fails to compile with "`gl_PointSize' undeclared" and the WHOLE program is replaced by
// program 0 - the draw renders nothing and any transform-feedback capture it was carrying
// is rejected outright. `extensionName` is the caller's answer, nullptr when the driver
// advertises neither spelling, because requesting an unadvertised extension is itself a
// compile error. A no-op when nullptr or already present.
String RequestPointSizeExtension(String glslCode, const char* extensionName);
// The extension name RequestPointSizeExtension should be given for `tier`, or nullptr for
// PointSizeTier::None. `tessellation` picks the tessellation spellings over the geometry
// ones; the two extensions are separate and neither implies the other.
const char* PointSizeExtensionName(MG_External::GLESCapabilities::PointSizeTier tier, Bool tessellation);
// Writes a format layout qualifier into the image declarations named in // Writes a format layout qualifier into the image declarations named in
// `esslFormatByUniformName` that still have none. The completion half of the image-format // `esslFormatByUniformName` that still have none. The completion half of the image-format
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the // bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
@@ -203,6 +203,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
XXHASH_VERIFY( XXHASH_VERIFY(
@@ -443,6 +444,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the // Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the
// struct's bytes match the hash the payload was keyed by. // struct's bytes match the hash the payload was keyed by.
ms.minSampleShading = payload.minSampleShading; ms.minSampleShading = payload.minSampleShading;
// GL_SAMPLE_MASK / glSampleMaski. Left at nullptr - which Vulkan reads as all-ones - until
// now, so glSampleMaski was a silent no-op on this backend while DirectGLES forwarded it.
// The pointer has to outlive the vkCreateGraphicsPipelines call, which the payload does.
ms.pSampleMask = payload.sampleMask;
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE; depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
@@ -44,6 +44,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784). // (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784).
Bool sampleShadingEnable = false; Bool sampleShadingEnable = false;
Float minSampleShading = 0.0f; Float minSampleShading = 0.0f;
// glEnable(GL_SAMPLE_MASK) + glSampleMaski, the fixed-function coverage mask, already
// reduced to what GL says this draw gets (VulkanRenderer::ResolveEffectiveSampleMask:
// all-ones unless the target is genuinely multisampled). Pipeline state like the two
// above - Vulkan has no dynamic sample mask before VK_EXT_extended_dynamic_state3 -
// so it is hashed with them, and all-ones has to keep producing the pipeline a null
// pSampleMask always did.
//
// TWO words, though GL only ever fills the first. GL_MAX_SAMPLE_MASK_WORDS is clamped
// to 1 on both backends, so glSampleMaski writes index 0 and nothing else - but the
// count Vulkan READS is ceil(rasterizationSamples / 32), which is 2 on a 64-sample
// target, and GetAdvertisedMaxSamples does not cap the driver's sample count. A
// single Uint32 here let such a pipeline read one word past the member (the next
// struct field). The second word is all-ones: full coverage for samples 32..63, which
// is the only honest answer when GL has no state describing them.
Uint32 sampleMask[2] = {0xffffffffu, 0xffffffffu};
Uint32 subpass = 0; Uint32 subpass = 0;
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
Bool primitiveRestartEnable = false; Bool primitiveRestartEnable = false;
@@ -77,6 +77,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
}; };
// Where a gl_PerVertex built-in output lives, resolved from the module's annotations.
// Named for gl_Position because the clip-space fixup is what it was written for, and it
// is still the only shape that pass accepts - but the transform-feedback capture pass
// resolves gl_PointSize through the same struct, in which case `vectorTypeId` /
// `vectorPtrTypeId` hold the SCALAR float type and its Output pointer rather than a vec4.
struct PositionTargetInfo { struct PositionTargetInfo {
Uint32 variableId = 0; Uint32 variableId = 0;
Uint32 vectorTypeId = 0; Uint32 vectorTypeId = 0;
@@ -102,6 +107,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true; return true;
} }
// gl_PointSize's counterpart to IsVec4Float32. The two are the only shapes any
// gl_PerVertex member this file resolves can have, and each resolver takes whichever
// one its built-in is declared with, so a mismatched type declines rather than
// producing a mirror the driver would reject.
Bool IsFloat32Scalar(spvtools::opt::IRContext* context, Uint32 typeId, Uint32* outFloatTypeId) {
auto* floatInst = context->get_def_use_mgr()->GetDef(typeId);
if (!floatInst || floatInst->opcode() != spv::Op::OpTypeFloat) return false;
if (floatInst->GetSingleWordInOperand(0) != 32) return false;
if (outFloatTypeId) *outFloatTypeId = typeId;
return true;
}
// Which of the two shapes above a resolver should accept. A plain function pointer
// rather than a std::function: every call site is one of the two free functions.
using BuiltInTypeCheckFn = Bool (*)(spvtools::opt::IRContext*, Uint32, Uint32*);
spvc_basetype MapReflectInterfaceToSpvcBasetype(const SpvReflectInterfaceVariable& variable) { spvc_basetype MapReflectInterfaceToSpvcBasetype(const SpvReflectInterfaceVariable& variable) {
if (variable.type_description == nullptr) { if (variable.type_description == nullptr) {
return SPVC_BASETYPE_UNKNOWN; return SPVC_BASETYPE_UNKNOWN;
@@ -381,9 +403,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return used; return used;
} }
void ValidateTransformedSpirv(const Vector<Uint>& spirv, ShaderStage shaderStage, Uint programExternalIndex) { // What a failed validation says, for a caller that wants to put it in its own message.
struct SpirvValidationFailure {
String message;
Int result = 0;
SizeT index = 0;
};
// Returns whether the module validates. The result used to be discarded everywhere: the
// call was DEBUG-or-env gated and only logged, so an invalid module produced by a backend
// transform went straight to vkCreateShaderModule. That is not a survivable outcome on
// this hardware - Mali r54 SIGSEGVs building the pipeline instead of returning an error,
// the same "not a validating entry point" behaviour PipelineFactory already documents for
// vkCreateGraphicsPipelines - so the callers that feed the driver now act on it.
//
// This function does NOT log the failure at E any more. It used to, unlatched, on the
// stated grounds that "reaching here already requires the validation switch to be armed,
// which bounds the volume" - and that premise died when the two GetOrCreateProgram call
// sites became unconditional: MGLOG_E is live at the production INFO level, and Log.h's
// own rule is that anything at W or E on a repeatable path must be latched or demoted.
// The failure text now travels back through `outFailure` so the LATCHED call-site
// messages carry the VUID instead of an unlatched inner one repeating it; what stays here
// is the D-level detail and the process-wide counter the test lanes assert on.
Bool ValidateTransformedSpirv(const Vector<Uint>& spirv, ShaderStage shaderStage, Uint programExternalIndex,
SpirvValidationFailure* outFailure = nullptr) {
if (outFailure != nullptr) *outFailure = {};
if (spirv.empty()) { if (spirv.empty()) {
return; return true;
} }
spv_const_binary_t binary = {spirv.data(), spirv.size()}; spv_const_binary_t binary = {spirv.data(), spirv.size()};
@@ -405,18 +451,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
spv_diagnostic diagnostic = nullptr; spv_diagnostic diagnostic = nullptr;
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic); const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
if (result != SPV_SUCCESS) { if (result != SPV_SUCCESS) {
// MGLOG_E, unlatched: reaching here already requires the validation switch to const char* message =
// be armed, which bounds the volume, and each VUID names a different defect. diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<null>";
// (Parked at MGLOG_I until the Log.h level ordering was fixed, when E was const SizeT index = diagnostic != nullptr ? diagnostic->position.index : 0;
// compiled out of every INFO build.) The latch is what a test harness asserts on. // The test-lane signal (ShaderCompiler.h documents harnesses snapshotting it and
// asserting on the delta). Bumped for every failed validation, including one a
// caller goes on to recover from: a transform that produced an invalid module is
// a real defect whether or not this run survived it.
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure(); MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
MGLOG_E( if (outFailure != nullptr) {
*outFailure = {String(message), static_cast<Int>(result), index};
}
MGLOG_D(
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s", "ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s",
static_cast<Int>(shaderStage), static_cast<Int>(shaderStage),
programExternalIndex, programExternalIndex,
static_cast<Int>(result), static_cast<Int>(result),
diagnostic != nullptr ? diagnostic->position.index : 0, index,
diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<null>"); message);
} }
MOBILEGL_ASSERT( MOBILEGL_ASSERT(
result == SPV_SUCCESS, result == SPV_SUCCESS,
@@ -432,6 +484,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
spvDiagnosticDestroy(diagnostic); spvDiagnosticDestroy(diagnostic);
spvValidatorOptionsDestroy(options); spvValidatorOptionsDestroy(options);
spvContextDestroy(context); spvContextDestroy(context);
return result == SPV_SUCCESS;
} }
void ReflectStageInterfaceVariable(const SpvReflectInterfaceVariable& variable, void ReflectStageInterfaceVariable(const SpvReflectInterfaceVariable& variable,
@@ -743,8 +796,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
Bool ResolveDirectPositionTarget(spvtools::opt::IRContext* context, Uint32 variableId, Bool ResolveDirectBuiltInTarget(spvtools::opt::IRContext* context, Uint32 variableId,
PositionTargetInfo* outTarget) { BuiltInTypeCheckFn typeCheck, PositionTargetInfo* outTarget) {
auto* varInst = context->get_def_use_mgr()->GetDef(variableId); auto* varInst = context->get_def_use_mgr()->GetDef(variableId);
if (!varInst || varInst->opcode() != spv::Op::OpVariable) return false; if (!varInst || varInst->opcode() != spv::Op::OpVariable) return false;
if (varInst->GetSingleWordInOperand(0) != static_cast<Uint32>(spv::StorageClass::Output)) return false; if (varInst->GetSingleWordInOperand(0) != static_cast<Uint32>(spv::StorageClass::Output)) return false;
@@ -756,7 +809,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
PositionTargetInfo target{}; PositionTargetInfo target{};
target.variableId = variableId; target.variableId = variableId;
target.vectorTypeId = ptrTypeInst->GetSingleWordInOperand(1); target.vectorTypeId = ptrTypeInst->GetSingleWordInOperand(1);
if (!IsVec4Float32(context, target.vectorTypeId, &target.floatTypeId)) return false; if (!typeCheck(context, target.vectorTypeId, &target.floatTypeId)) return false;
target.vectorPtrTypeId = varInst->type_id(); target.vectorPtrTypeId = varInst->type_id();
target.isMember = false; target.isMember = false;
@@ -771,15 +824,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return context->get_type_mgr()->GetTypeInstruction(&ptrType); return context->get_type_mgr()->GetTypeInstruction(&ptrType);
} }
Bool ResolveMemberPositionTarget(spvtools::opt::IRContext* context, Uint32 structTypeId, Uint32 memberIndex, Bool ResolveMemberBuiltInTarget(spvtools::opt::IRContext* context, Uint32 structTypeId, Uint32 memberIndex,
PositionTargetInfo* outTarget) { BuiltInTypeCheckFn typeCheck, PositionTargetInfo* outTarget) {
auto* structInst = context->get_def_use_mgr()->GetDef(structTypeId); auto* structInst = context->get_def_use_mgr()->GetDef(structTypeId);
if (!structInst || structInst->opcode() != spv::Op::OpTypeStruct) return false; if (!structInst || structInst->opcode() != spv::Op::OpTypeStruct) return false;
if (memberIndex >= structInst->NumInOperands()) return false; if (memberIndex >= structInst->NumInOperands()) return false;
const Uint32 vectorTypeId = structInst->GetSingleWordInOperand(memberIndex); const Uint32 vectorTypeId = structInst->GetSingleWordInOperand(memberIndex);
Uint32 floatTypeId = 0; Uint32 floatTypeId = 0;
if (!IsVec4Float32(context, vectorTypeId, &floatTypeId)) return false; if (!typeCheck(context, vectorTypeId, &floatTypeId)) return false;
const Uint32 vectorPtrTypeId = FindOutputVectorPointerTypeId(context, vectorTypeId); const Uint32 vectorPtrTypeId = FindOutputVectorPointerTypeId(context, vectorTypeId);
if (vectorPtrTypeId == 0) return false; if (vectorPtrTypeId == 0) return false;
@@ -807,27 +860,130 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false; return false;
} }
Bool FindPositionTarget(spvtools::opt::IRContext* context, PositionTargetInfo* outTarget) { // The OUTPUT variable (or gl_PerVertex member) carrying `builtIn`, if the module
// declares one of the expected type. Annotations are the search space deliberately:
// they survive the link-time sanitize chain's interface delisting, which is the whole
// reason EnsureEntryPointInterface exists.
Bool FindBuiltInTarget(spvtools::opt::IRContext* context, spv::BuiltIn builtIn,
BuiltInTypeCheckFn typeCheck, PositionTargetInfo* outTarget) {
Vector<Pair<Uint32, Uint32>> memberCandidates; Vector<Pair<Uint32, Uint32>> memberCandidates;
constexpr auto kDecorationBuiltIn = static_cast<Uint32>(spv::Decoration::BuiltIn); constexpr auto kDecorationBuiltIn = static_cast<Uint32>(spv::Decoration::BuiltIn);
constexpr auto kBuiltInPosition = static_cast<Uint32>(spv::BuiltIn::Position); const auto wantedBuiltIn = static_cast<Uint32>(builtIn);
for (auto& inst : context->module()->annotations()) { for (auto& inst : context->module()->annotations()) {
if (inst.opcode() == spv::Op::OpDecorate) { if (inst.opcode() == spv::Op::OpDecorate) {
if (inst.NumInOperands() < 3) continue; if (inst.NumInOperands() < 3) continue;
if (inst.GetSingleWordInOperand(1) != kDecorationBuiltIn) continue; if (inst.GetSingleWordInOperand(1) != kDecorationBuiltIn) continue;
if (inst.GetSingleWordInOperand(2) != kBuiltInPosition) continue; if (inst.GetSingleWordInOperand(2) != wantedBuiltIn) continue;
if (ResolveDirectPositionTarget(context, inst.GetSingleWordInOperand(0), outTarget)) return true; if (ResolveDirectBuiltInTarget(context, inst.GetSingleWordInOperand(0), typeCheck, outTarget)) {
return true;
}
} else if (inst.opcode() == spv::Op::OpMemberDecorate) { } else if (inst.opcode() == spv::Op::OpMemberDecorate) {
if (inst.NumInOperands() < 4) continue; if (inst.NumInOperands() < 4) continue;
if (inst.GetSingleWordInOperand(2) != kDecorationBuiltIn) continue; if (inst.GetSingleWordInOperand(2) != kDecorationBuiltIn) continue;
if (inst.GetSingleWordInOperand(3) != kBuiltInPosition) continue; if (inst.GetSingleWordInOperand(3) != wantedBuiltIn) continue;
memberCandidates.emplace_back(inst.GetSingleWordInOperand(0), inst.GetSingleWordInOperand(1)); memberCandidates.emplace_back(inst.GetSingleWordInOperand(0), inst.GetSingleWordInOperand(1));
} }
} }
for (const auto& [structTypeId, memberIndex] : memberCandidates) { for (const auto& [structTypeId, memberIndex] : memberCandidates) {
if (ResolveMemberPositionTarget(context, structTypeId, memberIndex, outTarget)) return true; if (ResolveMemberBuiltInTarget(context, structTypeId, memberIndex, typeCheck, outTarget)) return true;
}
return false;
}
Bool FindPositionTarget(spvtools::opt::IRContext* context, PositionTargetInfo* outTarget) {
return FindBuiltInTarget(context, spv::BuiltIn::Position, IsVec4Float32, outTarget);
}
// Put `variableId` back on `entryPoint`'s interface list if it is not already there.
//
// SPIR-V requires every Input/Output global an entry point statically uses to be listed on
// its OpEntryPoint, and spirv-val enforces it ("Interface variable id <N> is used by entry
// point 'main' id <M>, but is not listed as an interface"). The link-time sanitize chain
// DELISTS a variable nothing referenced yet - ShaderCompiler::SanitizeAndOptimizeBinary
// runs CreateAggressiveDCEPass(false), which may never delete an Output, followed by
// CreateRemoveUnusedInterfaceVariablesPass, which rebuilds the operand list from the
// variables actually referenced. A TES that redeclares `out gl_PerVertex { vec4
// gl_Position; }` and never writes it therefore reaches the backend with the OpVariable
// and its BuiltIn Position decoration intact and its interface slot gone. Any pass that
// then injects a reference has to put the slot back, or it hands the driver a module no
// validator accepts - and Mali r54 answers that with a SIGSEGV inside pipeline creation
// rather than an error return.
//
// No SPIR-V version gate here, unlike GlFragCoordYFlipPass's identical call for its
// injected PRIVATE global: Input and Output belong on the interface in every version,
// and only 1.4 widened it to the other storage classes.
Bool EnsureEntryPointInterface(spvtools::opt::IRContext* context, spvtools::opt::Instruction& entryPoint,
Uint32 variableId) {
// In-operands: 0 = execution model, 1 = entry function id, 2 = name, 3.. = interface.
constexpr Uint32 kFirstInterfaceOperand = 3;
if (variableId == 0) return false;
for (Uint32 operand = kFirstInterfaceOperand; operand < entryPoint.NumInOperands(); ++operand) {
if (entryPoint.GetSingleWordInOperand(operand) == variableId) return false;
}
entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {variableId}});
context->AnalyzeUses(&entryPoint);
return true;
}
// Is `pointerId` the position target itself, or an access chain rooted at it?
Bool PointerReachesPositionTarget(spvtools::opt::IRContext* context, Uint32 pointerId,
const PositionTargetInfo& target) {
auto* defUse = context->get_def_use_mgr();
for (Uint32 current = pointerId; current != 0;) {
if (current == target.variableId) return true;
const auto* inst = defUse->GetDef(current);
if (inst == nullptr) return false;
switch (inst->opcode()) {
case spv::Op::OpAccessChain:
case spv::Op::OpInBoundsAccessChain:
case spv::Op::OpPtrAccessChain:
case spv::Op::OpInBoundsPtrAccessChain:
case spv::Op::OpCopyObject:
current = inst->GetSingleWordInOperand(0);
break;
default:
return false;
}
}
return false;
}
// Does anything in the module write the position target?
//
// Deliberately conservative - it answers "assume yes" for every shape it cannot read
// exactly, because a false "no" would silently drop the clip-space fixup from a shader
// that does write gl_Position, while a false "yes" only reinstates the behaviour this
// pass has always had. Scans every function rather than just the entry point's: a shader
// that assigns gl_Position inside a helper is still a shader that writes it, and passing
// the pointer to a call is a write as far as this can tell.
Bool ModuleWritesPositionTarget(spvtools::opt::IRContext* context, const PositionTargetInfo& target) {
for (auto& function : *context->module()) {
for (auto& block : function) {
for (const auto& inst : block) {
switch (inst.opcode()) {
case spv::Op::OpStore:
case spv::Op::OpCopyMemory:
case spv::Op::OpCopyMemorySized:
if (PointerReachesPositionTarget(context, inst.GetSingleWordInOperand(0), target)) {
return true;
}
break;
case spv::Op::OpFunctionCall:
// In-operand 0 is the callee; the rest are arguments.
for (Uint32 argument = 1; argument < inst.NumInOperands(); ++argument) {
if (PointerReachesPositionTarget(context, inst.GetSingleWordInOperand(argument),
target)) {
return true;
}
}
break;
default:
break;
}
}
}
} }
return false; return false;
} }
@@ -914,6 +1070,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
PositionTargetInfo target{}; PositionTargetInfo target{};
if (!FindPositionTarget(context(), &target)) return Status::SuccessWithoutChange; if (!FindPositionTarget(context(), &target)) return Status::SuccessWithoutChange;
// Nothing to remap in a Position the shader never writes. Declining is not just
// an optimisation: the fixup is load-modify-store, so on an unwritten Position it
// converts "undefined, never written" into "written with whatever the load
// returned", and the store is a reference to a variable the link-time sanitize
// chain has already delisted from the entry-point interface. glslang emits the
// OpVariable for every DECLARED interface block, so a redeclared-but-unwritten
// `out gl_PerVertex` is a shape real shaders have.
if (!ModuleWritesPositionTarget(context(), target)) {
MGLOG_D("gl-to-vulkan-position-fix: the shader never writes gl_Position; leaving it alone");
return Status::SuccessWithoutChange;
}
auto* floatType = context()->get_type_mgr()->GetType(target.floatTypeId); auto* floatType = context()->get_type_mgr()->GetType(target.floatTypeId);
if (!floatType) return Status::SuccessWithoutChange; if (!floatType) return Status::SuccessWithoutChange;
@@ -945,6 +1113,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto* function = context()->GetFunction(entryPoint.GetSingleWordInOperand(1)); auto* function = context()->GetFunction(entryPoint.GetSingleWordInOperand(1));
if (!function) continue; if (!function) continue;
Bool modifiedThisEntryPoint = false;
for (auto& bb : *function) { for (auto& bb : *function) {
for (auto instIter = bb.begin(); instIter != bb.end(); ++instIter) { for (auto instIter = bb.begin(); instIter != bb.end(); ++instIter) {
auto* inst = &*instIter; auto* inst = &*instIter;
@@ -953,10 +1122,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
(model != spv::ExecutionModel::Geometry && inst->opcode() == spv::Op::OpReturn); (model != spv::ExecutionModel::Geometry && inst->opcode() == spv::Op::OpReturn);
if (!needsFixup) continue; if (!needsFixup) continue;
modified |= InsertPositionFixup(context(), inst, target, halfConstId, doYFlip, doZRemap, modifiedThisEntryPoint |=
doSurfaceRotate90, doSurfaceRotate180, doSurfaceRotate270); InsertPositionFixup(context(), inst, target, halfConstId, doYFlip, doZRemap,
doSurfaceRotate90, doSurfaceRotate180, doSurfaceRotate270);
} }
} }
// Per entry point, and only for one this pass actually injected into: the
// injected load/store is a static use of the position variable, so the
// variable has to be on THIS entry point's interface list.
if (modifiedThisEntryPoint) {
EnsureEntryPointInterface(context(), entryPoint, target.variableId);
}
modified |= modifiedThisEntryPoint;
} }
if (!modified) return Status::SuccessWithoutChange; if (!modified) return Status::SuccessWithoutChange;
@@ -1235,6 +1412,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool needsPositionMirror = false; Bool needsPositionMirror = false;
Uint32 positionBufferIndex = 0; Uint32 positionBufferIndex = 0;
Uint32 positionOffset = 0; Uint32 positionOffset = 0;
// gl_PointSize is a gl_PerVertex MEMBER, never a variable of its own, so the
// debug-name lookup below can never resolve it - it used to fall through to
// "no SPIR-V variable named 'gl_PointSize'" and leave the frontend's reserved
// slot unwritten, or, when it was the only capture, leave the module with no
// Xfb execution mode at all and the whole span declined.
Bool needsPointSizeMirror = false;
Uint32 pointSizeBufferIndex = 0;
Uint32 pointSizeOffset = 0;
for (const auto& varying : m_varyings) { for (const auto& varying : m_varyings) {
if (varying.name == "gl_Position") { if (varying.name == "gl_Position") {
needsPositionMirror = true; needsPositionMirror = true;
@@ -1242,6 +1427,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
positionOffset = varying.offsetBytes; positionOffset = varying.offsetBytes;
continue; continue;
} }
if (varying.name == "gl_PointSize") {
needsPointSizeMirror = true;
pointSizeBufferIndex = varying.bufferIndex;
pointSizeOffset = varying.offsetBytes;
continue;
}
if (varying.blockMemberIndex >= 0) { if (varying.blockMemberIndex >= 0) {
// glslang names the block's instance variable and its struct type // glslang names the block's instance variable and its struct type
// separately; an anonymous instance leaves only the type named, so // separately; an anonymous instance leaves only the type named, so
@@ -1306,8 +1497,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
if (needsPositionMirror) { if (needsPositionMirror) {
modified |= MirrorPositionForCapture(entryFunctionId, *entryPoint, positionBufferIndex, modified |= MirrorPerVertexBuiltInForCapture(entryFunctionId, *entryPoint,
positionOffset, decorateForXfb); spv::BuiltIn::Position, IsVec4Float32,
"gl_Position", positionBufferIndex, positionOffset,
decorateForXfb);
}
if (needsPointSizeMirror) {
modified |= MirrorPerVertexBuiltInForCapture(entryFunctionId, *entryPoint,
spv::BuiltIn::PointSize, IsFloat32Scalar,
"gl_PointSize", pointSizeBufferIndex,
pointSizeOffset, decorateForXfb);
} }
if (!modified) return Status::SuccessWithoutChange; if (!modified) return Status::SuccessWithoutChange;
@@ -1347,19 +1546,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return 0; return 0;
} }
// gl_Position and gl_PointSize are captured the same way and differ only in which
// built-in is looked up and what type it has, so one injector serves both. Anything
// else in gl_PerVertex would need its own type check before it could be added here.
template <typename DecorateFn> template <typename DecorateFn>
Bool MirrorPositionForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint, Bool MirrorPerVertexBuiltInForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
Uint32 bufferIndex, Uint32 offsetBytes, const DecorateFn& decorateForXfb) { spv::BuiltIn builtIn, BuiltInTypeCheckFn typeCheck,
const char* glslName, Uint32 bufferIndex, Uint32 offsetBytes,
const DecorateFn& decorateForXfb) {
const Uint32 entryPointModel = entryPoint.GetSingleWordInOperand(0); const Uint32 entryPointModel = entryPoint.GetSingleWordInOperand(0);
using namespace spvtools::opt; using namespace spvtools::opt;
PositionTargetInfo target{}; PositionTargetInfo target{};
if (!FindPositionTarget(context(), &target)) { if (!FindBuiltInTarget(context(), builtIn, typeCheck, &target)) {
MGLOG_E("XfbCaptureDecoratePass: gl_Position capture requested but no position output found"); MGLOG_E("XfbCaptureDecoratePass: %s capture requested but no such output found", glslName);
return false; return false;
} }
if (!target.isMember) { if (!target.isMember) {
// Standalone gl_Position variable: decorate it directly. // Standalone built-in variable: decorate it directly. It still has to be
// on the interface - a transform-feedback decoration on a variable the entry
// point does not list captures nothing, and the sanitize chain delists an
// unwritten one (see EnsureEntryPointInterface).
decorateForXfb(target.variableId, bufferIndex, offsetBytes); decorateForXfb(target.variableId, bufferIndex, offsetBytes);
EnsureEntryPointInterface(context(), entryPoint, target.variableId);
return true; return true;
} }
@@ -1413,6 +1621,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
injected = true; injected = true;
} }
} }
// The mirror was listed on the entry point above, but the loop just added a READ
// of the SOURCE block through an access chain, and the interface rule covers
// reads exactly as it covers writes. A built-in capture on a shader whose
// block the sanitize chain delisted - a TES that redeclares `out gl_PerVertex`
// and never writes it, which is what the tessellation_control_to_tessellation_
// evaluation.gl_MaxPatchVertices_Position_PointSize bodies do - produced an
// invalid module here for the same reason the position fixup did.
if (injected) {
EnsureEntryPointInterface(context(), entryPoint, target.variableId);
}
return injected; return injected;
} }
@@ -3238,9 +3456,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& spirv = program.GetGeneratedSpirv(); auto& spirv = program.GetGeneratedSpirv();
Vector<Vector<Uint>> moduleSpirvs(spirv.size()); Vector<Vector<Uint>> moduleSpirvs(spirv.size());
const Bool enableSpirvValidation = program.GetSpirvValidationEnabled(); const Bool enableSpirvValidation = program.GetSpirvValidationEnabled();
if (enableSpirvValidation) { // Unconditional now: the two ValidateTransformedSpirv calls below run in every build,
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation(); // not only when the switch is armed, so the validator's static tables have to be pinned
} // against process exit in every build too.
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
const ShaderStage fixupStage = PickClipFixupStage(stages); const ShaderStage fixupStage = PickClipFixupStage(stages);
@@ -3264,6 +3483,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
TransformSpirvForVulkanPositionFix(*fixupInput, moduleSpirvs[i], flags); TransformSpirvForVulkanPositionFix(*fixupInput, moduleSpirvs[i], flags);
// These two passes INJECT references - a store for the clip fixup, an access
// chain and a load for the gl_Position capture mirror - and a reference to a
// variable the link-time sanitize chain delisted from the entry-point interface
// is invalid SPIR-V that Mali r54 turns into a SIGSEGV inside pipeline creation
// rather than an error return. EnsureEntryPointInterface keeps them honest; this
// is the backstop.
//
// The fallback UNWINDS ONE PASS AT A TIME, which matters because the two passes
// are not equally optional. Rewinding straight to `spv` would also throw away the
// XfbBuffer/XfbStride/Offset decorations, the TransformFeedback capability and the
// Xfb execution mode - while the renderer decides to call
// vkCmdBeginTransformFeedbackEXT purely from GL state and never looks at the
// module. That ships a pipeline whose last pre-rasterization stage has no Xfb mode
// into a transform-feedback span, violating
// VUID-vkCmdBeginTransformFeedbackEXT-None-04128 on exactly the driver class this
// guard exists for. So: try the post-XFB, pre-clip-fixup module first, which keeps
// capture working and costs only the clip-space remap.
//
// Once per program on a cache miss, and only for the single stage that carries the
// fixups - not per draw and not per module.
SpirvValidationFailure fixupFailure{};
if (!ValidateTransformedSpirv(moduleSpirvs[i], stages[i], program.GetExternalIndex(),
&fixupFailure)) {
SpirvValidationFailure xfbFailure{};
if (fixupInput != &spv &&
ValidateTransformedSpirv(*fixupInput, stages[i], program.GetExternalIndex(), &xfbFailure)) {
MGLOG_E_ONCE("ProgramFactory: the clip fixup produced an invalid module for program %u "
"stage %d (%s); keeping the capture-decorated one, so this program draws "
"without the clip-space remap",
program.GetExternalIndex(), static_cast<Int>(stages[i]),
fixupFailure.message.c_str());
moduleSpirvs[i] = *fixupInput;
} else {
MGLOG_E_ONCE("ProgramFactory: the clip/XFB fixups produced an invalid module for program %u "
"stage %d (%s); keeping the untransformed one",
program.GetExternalIndex(), static_cast<Int>(stages[i]),
fixupFailure.message.c_str());
moduleSpirvs[i] = spv;
}
}
} else { } else {
moduleSpirvs[i] = spv; moduleSpirvs[i] = spv;
} }
@@ -3472,15 +3731,63 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& moduleSpv = moduleSpirvs[i]; auto& moduleSpv = moduleSpirvs[i];
if (moduleSpv.empty()) continue; if (moduleSpv.empty()) continue;
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG // Last look at the exact bytes the driver receives, in EVERY build rather than only
ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex()); // in DEBUG or with MOBILEGL_ENABLE_SPIRV_VALIDATION armed. This one only reports:
#else // by here the descriptor bindings have been remapped and the layout about to be
// Final module the driver receives; also checked in the INFO-level CI/test // reflected describes the remapped module, so there is no module left that is both
// lanes, where the DEBUG gate above is compiled out. // valid and consistent with it to fall back to. The recovery lives one step earlier,
if (enableSpirvValidation) { // at the clip/XFB fixups (see the revert there) - which is where a transform can
ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex()); // introduce a reference to a delisted interface variable, the failure this whole
// guard exists for. Anything that reaches this line names itself in the log of a
// shipping build instead of dying anonymously inside the driver.
SpirvValidationFailure finalFailure{};
if (!ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex(), &finalFailure)) {
MGLOG_E_ONCE("ProgramFactory: handing vkCreateShaderModule an INVALID module for program %u stage %d - "
"a backend transform after the clip/XFB fixups broke it (%s)",
program.GetExternalIndex(), static_cast<Int>(stages[i]),
finalFailure.message.c_str());
}
// Does the stage the driver will treat as the last pre-rasterization one actually
// carry Xfb? Asked of the FINAL bytes, so it answers for whatever the whole transform
// chain produced - a rewound clip/XFB backstop, a capture pass that resolved no
// varying and changed nothing, anything later that might strip it. The renderer picks
// its capture commands from GL state alone and would otherwise open a span against a
// pipeline that cannot feed it.
if (stages[i] == fixupStage && (flags & ProgramFactory::CompileOptionBit::XfbCapture) &&
program.GetTransformFeedbackVaryingCount() > 0 &&
!MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresTransformFeedback(moduleSpv)) {
MGLOG_E_ONCE("ProgramFactory: program %u was built as a transform-feedback capture variant but its "
"stage %d carries no Xfb execution mode; its capture spans will be declined rather "
"than recorded against a pipeline that cannot feed them",
program.GetExternalIndex(), static_cast<Int>(stages[i]));
entry.xfbCaptureDeclined = true;
}
// Does this stage need a device feature the device did not give us? Asked ONLY when
// the feature is off, so a device that has it - the common case - pays nothing: the
// whole test is short-circuited before the module is parsed.
//
// gl_PointSize is an ordinary per-vertex output in desktop GL and any
// vertex-processing stage may write it, but Vulkan puts the built-in behind
// shaderTessellationAndGeometryPointSize in the tessellation and geometry stages
// (VUID-RuntimeSpirv-PointSize-06439). glslang emits TessellationPointSize /
// GeometryPointSize from the application's own access, so this program is legal GL
// that this device cannot run - the same shape the DirectGLES arm reports when a
// driver advertises neither EXT nor OES point-size extension, and it deserves the
// same named message rather than a pipeline the driver may fault on.
if (!m_tessellationAndGeometryPointSizeEnabled &&
(stages[i] == ShaderStage::TessControl || stages[i] == ShaderStage::TessEval ||
stages[i] == ShaderStage::Geometry) &&
MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(
moduleSpv)) {
MGLOG_E_ONCE("ProgramFactory: program %u stage %d accesses gl_PointSize, but this device does not "
"support shaderTessellationAndGeometryPointSize; its draws are refused rather than "
"built into a pipeline the driver may fault on. Point size from a non-vertex stage "
"is not available on this device.",
program.GetExternalIndex(), static_cast<Int>(stages[i]));
entry.pointSizeCapabilityUnsupported = true;
} }
#endif
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
smci.codeSize = moduleSpv.size() * sizeof(Uint); smci.codeSize = moduleSpv.size() * sizeof(Uint);
@@ -3758,11 +4065,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch, and it now // PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch, and it now
// links the program at both 430 and 460. // links the program at both 430 and 460.
// //
// Only gl_Position is written. gl_PointSize is declared but left alone deliberately: // Only gl_Position is written, and gl_PointSize is declared without being forwarded. That
// writing it from a tessellation stage requires the shaderTessellationAndGeometryPointSize // is a KNOWN GAP, not a design: GL 4.6 core 11.2.2 says the fixed-function pass-through
// feature, which this renderer does not enable, so a program whose evaluation stage reads // hands the input patch to the evaluation stage unmodified, so an evaluation stage
// gl_in[].gl_PointSize gets an undefined point size instead of the vertex stage's - a gap // reading gl_in[].gl_PointSize should see the vertex stage's value and instead sees
// this trades for not making every tessellated pipeline depend on an optional feature. // whatever this stage left in gl_out[] - which is nothing. A capture of it (the mirror in
// XfbCaptureDecoratePass) faithfully records that nothing.
//
// The reason this comment used to give - "the renderer does not enable
// shaderTessellationAndGeometryPointSize" - stopped being true when
// VulkanRenderer::CreateLogicalDeviceAndQueues started taking the feature wherever the
// device advertises it. Closing the gap is therefore possible now, but it is not free:
// the forwarding store has to be gated on that feature, because on a device without it
// the store is exactly the invalid usage the build-time refusal
// (VkProgramObject::pointSizeCapabilityUnsupported) exists to keep away from the driver -
// and this synthesized stage is not the application's, so refusing the program because
// MobileGL's own pass-through named a built-in would be the wrong trade. Nothing pins
// the shape either: every case in TessellationXfbCaptureScenario builds an explicit
// control stage, so a TES-without-TCS test has to come with the fix.
const String perVertexBody = BuildPerVertexMemberDeclarations(perVertexMembers); const String perVertexBody = BuildPerVertexMemberDeclarations(perVertexMembers);
source += "in gl_PerVertex {\n" + perVertexBody + "} gl_in[gl_MaxPatchVertices];\n"; source += "in gl_PerVertex {\n" + perVertexBody + "} gl_in[gl_MaxPatchVertices];\n";
source += "out gl_PerVertex {\n" + perVertexBody + "} gl_out[];\n"; source += "out gl_PerVertex {\n" + perVertexBody + "} gl_out[];\n";
@@ -3862,14 +4182,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
const Vector<Uint>& spirv = binary.value().front(); const Vector<Uint>& spirv = binary.value().front();
{
// Still switch-gated, unlike the two in GetOrCreateProgram: this stage is synthesized
// by MobileGL from a fixed template rather than transformed from application SPIR-V,
// so a failure here is a MobileGL bug to catch in a validating lane, not something a
// shipping build can be handed by an application. The message is latched all the same
// - the pass-through cache is keyed on patchVertices, so a broken template would
// otherwise re-report once per distinct patch size.
Bool validateThisOne = false;
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0); validateThisOne = true;
#else #else
if (m_enableSpirvValidation) { validateThisOne = m_enableSpirvValidation;
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation(); if (validateThisOne) MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
}
#endif #endif
SpirvValidationFailure passthroughFailure{};
if (validateThisOne &&
!ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0, &passthroughFailure)) {
MGLOG_E_ONCE("ProgramFactory: the synthesized pass-through tessellation control stage for "
"patchVertices=%u does not validate (%s)",
patchVertices, passthroughFailure.message.c_str());
}
}
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
smci.codeSize = spirv.size() * sizeof(Uint); smci.codeSize = spirv.size() * sizeof(Uint);
@@ -202,6 +202,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// tessellation stages are present or neither // tessellation stages are present or neither
// (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). So the draw path has to supply // (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). So the draw path has to supply
// the pass-through stage GL describes; see GetOrCreatePassthroughTessControlStage. // the pass-through stage GL describes; see GetOrCreatePassthroughTessControlStage.
// True when this program was built AS a transform-feedback capture variant but its
// last pre-rasterization module does NOT carry the Xfb execution mode - so the
// renderer must decline the capture span instead of issuing
// vkCmdBeginTransformFeedbackEXT against it
// (VUID-vkCmdBeginTransformFeedbackEXT-None-04128).
//
// Two ways to get here, and neither is visible from GL state, which is all
// BeginXfbCaptureForDraw otherwise consults: the clip/XFB validation backstop had to
// rewind past the capture decoration, or XfbCaptureDecoratePass resolved none of the
// requested varyings and returned without changing anything (its own MGLOG_E path)
// while its runner still reported success. Both used to ship a non-Xfb module under
// an Xfb-flagged cache entry - the flag and the layout are part of the program cache
// key, so it was sticky for every later captured draw of the program, not a glitch.
Bool xfbCaptureDeclined = false;
// The program has a tessellation or geometry module declaring TessellationPointSize /
// GeometryPointSize on a device whose shaderTessellationAndGeometryPointSize feature
// is off, so a pipeline built from it is invalid usage
// (VUID-RuntimeSpirv-PointSize-06439). Its draws are refused in SetupDraw rather than
// handed to the driver - the same contract PipelineFactory's half-tessellated refusal
// implements one level up, and the counterpart of the DirectGLES arm that reports a
// driver with neither point-size extension by name.
//
// Sticky by construction, which is what makes ONE log line honest: the flag lives on
// the cache entry, so every later draw of the same program variant reads the same
// answer instead of re-deciding it.
Bool pointSizeCapabilityUnsupported = false;
Bool needsPassthroughTessControl = false; Bool needsPassthroughTessControl = false;
// ...and the pass-through this renderer can synthesize carries gl_Position and // ...and the pass-through this renderer can synthesize carries gl_Position and
// nothing else, so it is only correct when the evaluation stage's inputs are // nothing else, so it is only correct when the evaluation stage's inputs are
@@ -426,12 +452,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings, explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings,
Bool shaderDrawParametersEnabled, Bool shaderDrawParametersEnabled,
Bool unformattedFloatStorageImagesEnabled, Bool unformattedFloatStorageImagesEnabled,
Bool tessellationAndGeometryPointSizeEnabled,
Bool enableSpirvValidation, Bool enableSpirvValidation,
UpdateAfterBindLimits updateAfterBindLimits, UpdateAfterBindLimits updateAfterBindLimits,
SubgroupLoweringPolicy subgroupPolicy) SubgroupLoweringPolicy subgroupPolicy)
: m_device(device), m_maxBindings(maxBindings), m_config(config), : m_device(device), m_maxBindings(maxBindings), m_config(config),
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled), m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled), m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled),
m_tessellationAndGeometryPointSizeEnabled(tessellationAndGeometryPointSizeEnabled),
m_enableSpirvValidation(enableSpirvValidation), m_enableSpirvValidation(enableSpirvValidation),
m_updateAfterBindLimits(updateAfterBindLimits), m_updateAfterBindLimits(updateAfterBindLimits),
m_subgroupPolicy(subgroupPolicy) { m_subgroupPolicy(subgroupPolicy) {
@@ -591,6 +619,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// True only when the logical device enabled both // True only when the logical device enabled both
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat. // shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
Bool m_unformattedFloatStorageImagesEnabled = false; Bool m_unformattedFloatStorageImagesEnabled = false;
// True when the logical device enabled shaderTessellationAndGeometryPointSize. When it is
// FALSE a program whose tessellation or geometry module declares TessellationPointSize /
// GeometryPointSize is refused at build time (see VkProgramObject::
// pointSizeCapabilityUnsupported) instead of being handed to the driver as invalid usage.
Bool m_tessellationAndGeometryPointSizeEnabled = false;
// Startup snapshot used only by internally synthesized shader modules, which do not // Startup snapshot used only by internally synthesized shader modules, which do not
// originate from a ProgramLinkTask. // originate from a ProgramLinkTask.
Bool m_enableSpirvValidation = false; Bool m_enableSpirvValidation = false;
@@ -37,6 +37,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// glGenTextures ever hands this out, and nothing looks a placeholder up by name - so the // glGenTextures ever hands this out, and nothing looks a placeholder up by name - so the
// id only has to stay clear of the application's, exactly like the sampled fallback's. // id only has to stay clear of the application's, exactly like the sampled fallback's.
constexpr Uint kUnboundStorageImageExternalIndex = 0xFFFFFF01u; constexpr Uint kUnboundStorageImageExternalIndex = 0xFFFFFF01u;
// The multisample sampled fallbacks: one per (target, numeric domain), because unlike the
// single-sampled fallback they cannot be reinterpreted into another domain at view time
// (see GetFallbackMultisampleTexture). Six reserved ids, contiguous from this base for the
// same reason as the two above - they must not collide with anything glGenTextures can
// hand out.
constexpr Uint kFallbackMultisampleExternalIndexBase = 0xFFFFFF02u;
constexpr Uint kFallbackMultisampleExternalIndexCount = 6u;
// MobileGL's own stand-in textures, by the reserved ids above. Nothing an application can
// do reaches one, so anything keyed on the GL object an application bound - image-unit
// aliasing above all - has to leave them alone.
Bool IsPlaceholderTexture(const MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) return false;
const Uint index = static_cast<Uint>(texture->GetExternalIndex());
return index == kFallbackTexture2DExternalIndex || index == kUnboundStorageImageExternalIndex ||
(index >= kFallbackMultisampleExternalIndexBase &&
index < kFallbackMultisampleExternalIndexBase + kFallbackMultisampleExternalIndexCount);
}
// The R32 member of each numeric class. Every one of the three is a MANDATORY-support // The R32 member of each numeric class. Every one of the three is a MANDATORY-support
// format for uniform texel buffers, storage texel buffers and storage images alike // format for uniform texel buffers, storage texel buffers and storage images alike
@@ -360,6 +378,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_textureManager = nullptr; m_textureManager = nullptr;
m_samplerManager = nullptr; m_samplerManager = nullptr;
m_fallbackTexture2D.reset(); m_fallbackTexture2D.reset();
m_fallbackMultisampleTextures.clear();
} }
void UniformManager::BeginFrame(Uint32 frameIndex) { void UniformManager::BeginFrame(Uint32 frameIndex) {
@@ -496,7 +515,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
texture = nullptr; texture = nullptr;
} }
if (texture == nullptr) { if (texture == nullptr) {
fallbackHolder = GetFallbackTexture(preferredTarget); // The binding's sampler class, read here rather than through the `numericDomain`
// local further down (it is declared after this point): the multisample placeholder
// has to be built in the class the shader will read it in.
fallbackHolder = GetFallbackTexture(preferredTarget, programObj.samplerNumericDomainByBinding[binding]);
texture = fallbackHolder.get(); texture = fallbackHolder.get();
if (texture == nullptr) { if (texture == nullptr) {
MGLOG_E_ONCE("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') " MGLOG_E_ONCE("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
@@ -1367,18 +1389,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return outImageInfo.imageView != VK_NULL_HANDLE; return outImageInfo.imageView != VK_NULL_HANDLE;
} }
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(TextureTarget target) const { SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(
// The fallback is a single-sampled 2D image, so it can only stand in for a sampler that TextureTarget target, SamplerNumericDomain numericDomain) const {
// would accept one. A multisample sampler in particular cannot: its descriptor demands a // A multisample sampler cannot be served by the single-sampled 2D image below - its
// multisample view, and handing it this one is invalid Vulkan, not a degraded picture. // descriptor demands a multisample view - so it gets its own placeholder rather than no
// Report that there is no fallback and let the caller decline the draw - aborting the // placeholder at all. Without one, ResolveSamplerDescriptor declined and
// process over an unbound sampler is never the right answer. // BindProgramUniformBuffers dropped the WHOLE draw, which is how every
// sample_variables.*.samples_0 body failed: the CTS's resolve program declares both a
// sampler2D and a sampler2DMS and deliberately points the unused one at an empty texture
// unit, and at samples_0 the unused one is the sampler2DMS. GL says sampling an
// incomplete texture is undefined, not fatal, so the draw has to happen.
if (target == TextureTarget::Texture2DMultisample ||
target == TextureTarget::Texture2DMultisampleArray) {
return GetFallbackMultisampleTexture(target, numericDomain);
}
if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) { if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) {
MGLOG_E_ONCE("UniformManager::GetFallbackTexture: no fallback exists for target=%d", MGLOG_E_ONCE("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
static_cast<Int>(target)); static_cast<Int>(target));
return nullptr; return nullptr;
} }
// The single-sampled fallback stays domain-agnostic: it is storage-image capable, so its
// image carries VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT and ResolveSampledImageViewFormat can
// hand an integer sampler an R8G8B8A8_UINT view of these same RGBA8 texels. A multisample
// image can never carry that bit, which is why the arm above needs one object per domain.
if (m_fallbackTexture2D == nullptr) { if (m_fallbackTexture2D == nullptr) {
auto fallbackTexture = MakeShared<MG_State::GLState::TextureObject2D>(kFallbackTexture2DExternalIndex); auto fallbackTexture = MakeShared<MG_State::GLState::TextureObject2D>(kFallbackTexture2DExternalIndex);
fallbackTexture->SetInternalFormat(TextureInternalFormat::RGBA8); fallbackTexture->SetInternalFormat(TextureInternalFormat::RGBA8);
@@ -1396,6 +1430,83 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return m_fallbackTexture2D; return m_fallbackTexture2D;
} }
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackMultisampleTexture(
TextureTarget target, SamplerNumericDomain numericDomain) const {
// ONE PLACEHOLDER PER NUMERIC DOMAIN, unlike the single-sampled fallback.
//
// A descriptor whose image format is in a different numeric class than the sampler that
// reads it needs a format-reinterpreting view, and building one needs
// VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT on the image. A multisample image can never have it:
// SyncTextureResource computes storageImageCapable as `!isMultisampleTexture && ...`, and
// the only other source of the bit is the sRGB twin, which RGBA8 is not. So an RGBA8
// placeholder handed to a usampler2DMS made GetOrCreateSampledImageView bail with "needs
// mutable image format", ResolveSamplerDescriptor return false, and the draw be dropped -
// the exact outcome the placeholder exists to prevent, just reached later. Matching the
// image's own format to the sampler's class instead means no reinterpreting view is
// needed at all.
const Bool arrayed = target == TextureTarget::Texture2DMultisampleArray;
TextureInternalFormat internalFormat = TextureInternalFormat::RGBA8;
Uint32 domainSlot = 0;
switch (numericDomain) {
case SamplerNumericDomain::SignedInteger:
internalFormat = TextureInternalFormat::RGBA8I;
domainSlot = 1;
break;
case SamplerNumericDomain::UnsignedInteger:
internalFormat = TextureInternalFormat::RGBA8UI;
domainSlot = 2;
break;
case SamplerNumericDomain::Float:
case SamplerNumericDomain::Unknown:
default:
// Unknown reads as float, matching PlaceholderFormatForNumericDomain's own default:
// a shader whose sampler class could not be reflected is far likelier to be a plain
// sampler2DMS than an integer one, and a float view is the only one buildable without
// the mutable bit anyway.
break;
}
const Uint32 key = (arrayed ? kFallbackMultisampleExternalIndexCount / 2 : 0u) + domainSlot;
auto cached = m_fallbackMultisampleTextures.find(key);
if (cached != m_fallbackMultisampleTextures.end()) {
return cached->second;
}
const TextureUploadTarget uploadTarget = arrayed ? TextureUploadTarget::Texture2DMultisampleArray
: TextureUploadTarget::Texture2DMultisample;
const Uint externalIndex = kFallbackMultisampleExternalIndexBase + key;
SharedPtr<MG_State::GLState::TextureObjectMipmap> texture;
if (arrayed) {
texture = MakeShared<MG_State::GLState::TextureObject2DMultisampleArray>(externalIndex);
} else {
texture = MakeShared<MG_State::GLState::TextureObject2DMultisample>(externalIndex);
}
texture->SetInternalFormat(internalFormat);
// TWO samples, never one. VUID-RuntimeSpirv-samples-08726 forbids an OpTypeImage with
// MS = 1 from reading a VK_SAMPLE_COUNT_1_BIT image, which is exactly the hazard
// VkTextureManager::SyncTextureResource's one-sample floor exists to avoid; a placeholder
// that re-created it would be worse than none.
texture->SetSamples(2);
texture->SetFixedSampleLocations(true);
// No upload, and MarkStorageDirty(dirty = false) to say so: a multisample image cannot be
// written by a transfer at all - it deliberately carries no TRANSFER_DST usage - so unlike
// the 2D fallback this one cannot be given (0, 0, 0, 1) content. Its texels are undefined,
// which is precisely what GL 4.6 core 8.17 promises for a texelFetch on a multisample
// texture that is not complete. The point of the placeholder is that the DRAW happens.
texture->AllocateStorage(uploadTarget, 0, {.texelSize = {1, 1, 1}, .byteSize = 0});
texture->TruncateMipmapLevels(uploadTarget, 1);
texture->MarkStorageDirty(uploadTarget, 0, false);
// Worth knowing if it ever fires: an integer multisample format can legitimately support
// no count above one on a device (framebufferIntegerColorSampleCounts is allowed to be
// VK_SAMPLE_COUNT_1_BIT), and SyncTextureResource's round-down would then hand this
// placeholder a single-sampled image, which is the samples-08726 shape the SetSamples(2)
// above exists to avoid. It already warns from there; nothing better is available - a
// one-sample integer image is still a draw, and declining is the outcome this whole
// placeholder replaced.
MGLOG_D("UniformManager::GetFallbackMultisampleTexture: created placeholder target=%d domain=%d format=%d",
static_cast<Int>(target), static_cast<Int>(numericDomain), static_cast<Int>(internalFormat));
return m_fallbackMultisampleTextures.emplace(key, Move(texture)).first->second;
}
VkBufferView UniformManager::AcquireUnboundTexelBufferView(VkFormat declaredFormat, VkBufferView UniformManager::AcquireUnboundTexelBufferView(VkFormat declaredFormat,
SamplerNumericDomain numericDomain, Bool storage) { SamplerNumericDomain numericDomain, Bool storage) {
MOBILEGL_ASSERT(m_bufferManager != nullptr, "AcquireUnboundTexelBufferView: buffer manager is null"); MOBILEGL_ASSERT(m_bufferManager != nullptr, "AcquireUnboundTexelBufferView: buffer manager is null");
@@ -1551,25 +1662,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding]; const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
MG_State::GLState::ITextureObject* texture = MG_State::GLState::ITextureObject* texture =
textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get(); textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
// The sampler in effect, resolved BEFORE the completeness test below rather than after:
// GL's completeness rules are a property of (texture, sampler in effect), so the test
// cannot be asked without it.
const auto& samplerOverride = textureUnit.GetSamplerObject();
const MG_State::GLState::SamplerObject* effectiveSampler =
samplerOverride ? samplerOverride.get()
: (texture != nullptr ? texture->GetSamplerObject().get() : nullptr);
// Undefined default texture (name 0, no image) resolves as "unbound", exactly // Undefined default texture (name 0, no image) resolves as "unbound", exactly
// like ResolveSamplerTextureRaw reports it. // like ResolveSamplerTextureRaw reports it.
if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) { if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) {
texture = nullptr; texture = nullptr;
} }
// ...and so does a texture that fails the completeness rules for the filter in effect,
// because that is precisely what ResolveSamplerDescriptor does with it. The two used to
// disagree: this one asked only whether the default texture was UNDEFINED, so a default
// texture that had been given a base level but no mip chain - which is what the GL-CTS
// state reset between test cases leaves behind, and what any application that uploads to
// texture 0 has - stayed in the sampled set while the descriptor path swapped it for the
// fallback. SetupDraw then synced a texture no descriptor would use, the sync declined
// (GL calls it incomplete), and the null it returned was dereferenced one line later.
// Keeping the two predicates identical is the invariant; CollectSampledTextures exists to
// pre-sync exactly the textures the descriptors will hold.
if (MG_State::GLState::SamplesAsIncompleteTexture(texture, effectiveSampler)) {
texture = nullptr;
}
if (texture == nullptr) { if (texture == nullptr) {
// ResolveSamplerDescriptor will substitute the fallback texture for this binding; // ResolveSamplerDescriptor will substitute the fallback texture for this binding;
// include it in the sampled set so the pre-render-pass sync/transition pass covers // include it in the sampled set so the pre-render-pass sync/transition pass covers
// its first use instead of leaving that work to happen inside an active pass. // its first use instead of leaving that work to happen inside an active pass.
if (preferredTarget != TextureTarget::Texture2D && // Ask GetFallbackTexture rather than re-listing the targets it serves: that list grew
preferredTarget != TextureTarget::TextureRectangle) { // a multisample arm and the two must not drift apart.
texture = GetFallbackTexture(preferredTarget, programObj.samplerNumericDomainByBinding[binding]).get();
if (texture == nullptr) {
return false; return false;
} }
texture = GetFallbackTexture(preferredTarget).get(); // The substitution changed the texture, so the "no override" arm of the effective
// sampler has to follow it to the fallback's own.
if (!samplerOverride) {
effectiveSampler = texture != nullptr ? texture->GetSamplerObject().get() : nullptr;
}
} }
const auto& samplerOverride = textureUnit.GetSamplerObject();
outTexture = texture; outTexture = texture;
outSampler = samplerOverride ? samplerOverride.get() outSampler = effectiveSampler;
: (texture != nullptr ? texture->GetSamplerObject().get() : nullptr);
return true; return true;
} }
@@ -1765,9 +1900,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!ResolveSampledBinding(program, programObj, samplerBinding, samplerElement, if (!ResolveSampledBinding(program, programObj, samplerBinding, samplerElement,
sampledTexture, sampledSampler) || sampledTexture, sampledSampler) ||
sampledTexture == nullptr || sampledSampler == nullptr || sampledTexture == nullptr || sampledSampler == nullptr ||
MG_State::GLState::SamplesAsIncompleteTexture(sampledTexture, sampledSampler)) { IsPlaceholderTexture(sampledTexture)) {
// ResolveSamplerDescriptor uses a fallback in these cases, which cannot // ResolveSamplerDescriptor uses a fallback in these cases, which cannot
// alias the image-unit binding of the original texture. // alias the image-unit binding of the original texture. The unbound and
// incomplete cases both arrive here AS that fallback now that
// ResolveSampledBinding applies the completeness rule itself, so the test is
// "is this one of ours" rather than a second completeness check.
continue; continue;
} }
// Multisample source images intentionally omit TRANSFER_SRC usage. Keep their existing // Multisample source images intentionally omit TRANSFER_SRC usage. Keep their existing
@@ -179,7 +179,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw( static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element); const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const; // `numericDomain` is the sampler's class, and it matters only for the multisample arm -
// see GetFallbackMultisampleTexture for why the single-sampled fallback can ignore it.
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(
TextureTarget target, SamplerNumericDomain numericDomain) const;
// The multisample arm of GetFallbackTexture. One object per (target, numeric domain) and
// no upload path: a multisample image cannot be written by a transfer, so its texels stay
// undefined - which is what GL promises for a texelFetch on an incomplete multisample
// texture - and it cannot carry MUTABLE_FORMAT, so its format has to match the sampler's
// class outright rather than being reinterpreted at view time.
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackMultisampleTexture(
TextureTarget target, SamplerNumericDomain numericDomain) const;
// ---- placeholders for UNBOUND image-backed descriptors ------------------------- // ---- placeholders for UNBOUND image-backed descriptors -------------------------
// GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing // GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing
// to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete // to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete
@@ -293,6 +303,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkTextureManager* m_textureManager = nullptr; VkTextureManager* m_textureManager = nullptr;
VkSamplerManager* m_samplerManager = nullptr; VkSamplerManager* m_samplerManager = nullptr;
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D; mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
// Keyed by (arrayed, numeric domain); see GetFallbackMultisampleTexture. Lazily populated,
// never evicted - at most six tiny 1x1 images - and torn down with the manager.
mutable UnorderedMap<Uint32, SharedPtr<MG_State::GLState::ITextureObject>> m_fallbackMultisampleTextures;
// See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily // See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily
// populated, never evicted (a program's declared formats are a fixed, tiny set) and torn // populated, never evicted (a program's declared formats are a fixed, tiny set) and torn
// down with the manager. The texel views are keyed by format AND by storage-vs-sampled // down with the manager. The texel views are keyed by format AND by storage-vs-sampled
@@ -3159,6 +3159,7 @@ void main() {
m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings, m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings,
m_shaderDrawParametersFeatureEnabled, m_shaderDrawParametersFeatureEnabled,
m_unformattedFloatStorageImagesEnabled, m_unformattedFloatStorageImagesEnabled,
m_tessellationAndGeometryPointSizeFeatureEnabled,
MG_Config::Features.EnableSpirvValidation, MG_Config::Features.EnableSpirvValidation,
m_updateAfterBindLimits, subgroupPolicy); m_updateAfterBindLimits, subgroupPolicy);
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed."); MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
@@ -4767,16 +4768,43 @@ void main() {
// build in GetOrCreatePipeline - any new GL-state read there must be added here: // build in GetOrCreatePipeline - any new GL-state read there must be added here:
// - capability bits: CullFace, DepthTest, PolygonOffsetFill (mode gating rides // - capability bits: CullFace, DepthTest, PolygonOffsetFill (mode gating rides
// the memo's mode key), RasterizerDiscard, ColorLogicOp, StencilTest, // the memo's mode key), RasterizerDiscard, ColorLogicOp, StencilTest,
// PrimitiveRestart(+FixedIndex), SampleShading, plus the depth write mask // PrimitiveRestart(+FixedIndex), SampleShading, SampleMask, plus the depth write mask
// - patch vertices, polygon mode, cull face mode, depth func, logic op, // - patch vertices, polygon mode, cull face mode, depth func, logic op,
// min sample shading // min sample shading, the glSampleMaski word
// - front/back stencil ops + compare funcs (ref/mask are dynamic state) // - front/back stencil ops + compare funcs (ref/mask are dynamic state)
// - per draw buffer up to the render pass's colour span: indexed blend enable, // - per draw buffer up to the render pass's colour span: indexed blend enable,
// blend factors/equations, indexed colour write mask (broadcast from index 0 // blend factors/equations, indexed colour write mask (broadcast from index 0
// when the device lacks independentBlend - the same read the payload does) // when the device lacks independentBlend - the same read the payload does)
// FBO-derived payload inputs (attachment presence/formats/draw-buffer gating) are // FBO-derived payload inputs (attachment presence/formats/draw-buffer gating) are
// pinned by the render-pass hash key, exactly as the version-keyed memo relied on. // pinned by the render-pass hash key, exactly as the version-keyed memo relied on.
Uint64 VulkanRenderer::ComputePipelineStateHash(Uint32 colorAttachmentCount) const { // The fixed-function sample mask this draw actually gets, and the ONE place that decides it.
//
// GL 4.6 core 17.3.3 puts SAMPLE_MASK/SAMPLE_MASK_VALUE among the multisample fragment
// operations and says they make no change "if MULTISAMPLE is disabled, or if the value of
// SAMPLE_BUFFERS is not one" - so on a single-sample draw framebuffer the mask is a no-op.
// Vulkan has no such rule: pSampleMask is ANDed with coverage at every rasterizationSamples,
// and at one sample that coverage is bit 0 alone. Handing the raw GL word straight through
// therefore turned `glEnable(GL_SAMPLE_MASK); glSampleMaski(0, 0x2);` followed by a draw to
// the default framebuffer - the ordinary MSAA-render-then-present shape, and what dEQP's
// multisample cases leave enabled - into a fully discarded, black draw. All-ones restores
// the null-pSampleMask meaning the pipeline had before the mask was plumbed at all.
//
// SAMPLE_BUFFERS is the load-bearing half: MultisampleEnabled defaults to TRUE, so the
// capability check alone would gate nothing. It is here for spec completeness - GL lets
// glDisable(GL_MULTISAMPLE) switch the whole step off on a multisample target too.
//
// Both callers - the payload and ComputePipelineStateHash's memo word - go through this, so
// the memo key cannot describe a different mask than the pipeline was built with.
Uint32 VulkanRenderer::ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const {
constexpr Uint32 kFullCoverage = 0xffffffffu;
if (rasterizationSamples == VK_SAMPLE_COUNT_1_BIT) return kFullCoverage;
if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample)) return kFullCoverage;
if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask)) return kFullCoverage;
return MG_State::pGLContext->GetRenderStateParameters().SampleMaskValue;
}
Uint64 VulkanRenderer::ComputePipelineStateHash(Uint32 colorAttachmentCount,
VkSampleCountFlagBits rasterizationSamples) const {
// One bulk fetch instead of ~17 per-field accessor calls into MG_State: every // One bulk fetch instead of ~17 per-field accessor calls into MG_State: every
// input below is a plain field of RenderStateParameters, and each accessor this // input below is a plain field of RenderStateParameters, and each accessor this
// replaces (IsCapabilityEnabled / Get*) is a verified pure read of that same // replaces (IsCapabilityEnabled / Get*) is a verified pure read of that same
@@ -4795,6 +4823,13 @@ void main() {
capabilityBits |= p.PrimitiveRestartFixedIndexEnabled ? 1ull << 7 : 0; capabilityBits |= p.PrimitiveRestartFixedIndexEnabled ? 1ull << 7 : 0;
capabilityBits |= p.DepthMask ? 1ull << 8 : 0; capabilityBits |= p.DepthMask ? 1ull << 8 : 0;
capabilityBits |= p.SampleShadingEnabled ? 1ull << 9 : 0; capabilityBits |= p.SampleShadingEnabled ? 1ull << 9 : 0;
// The EFFECTIVE mask enable, not the raw GL bit: at one sample GL says the whole
// multisample fragment-operations step makes no change, so the pipeline is built with
// full coverage and the memo word has to say so too. Keying on the raw bit here while
// the payload gates on the sample count would let one FBO's cached pipeline answer for
// another whose sample count reads the mask differently.
const Bool sampleMaskEffective = ResolveEffectiveSampleMask(rasterizationSamples) != 0xffffffffu;
capabilityBits |= sampleMaskEffective ? 1ull << 10 : 0;
Uint64 hash = CombinePipelineStateWord(0x243F6A8885A308D3ull, capabilityBits); Uint64 hash = CombinePipelineStateWord(0x243F6A8885A308D3ull, capabilityBits);
// glMinSampleShading. Hashed by BITS, not by value: this memo compares hashes rather than // glMinSampleShading. Hashed by BITS, not by value: this memo compares hashes rather than
// versions, so an unhashed float would let a pipeline built at one rate be handed back // versions, so an unhashed float would let a pipeline built at one rate be handed back
@@ -4804,6 +4839,13 @@ void main() {
std::memcpy(&minSampleShadingBits, &p.MinSampleShadingValue, sizeof(minSampleShadingBits)); std::memcpy(&minSampleShadingBits, &p.MinSampleShadingValue, sizeof(minSampleShadingBits));
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(minSampleShadingBits)); hash = CombinePipelineStateWord(hash, static_cast<Uint64>(minSampleShadingBits));
} }
// glSampleMaski's word, for the same reason glMinSampleShading's bits are hashed above:
// this memo compares hashes, not versions, so a mask that moved between two otherwise
// identical draws has to key a different pipeline. Hashed unconditionally rather than only
// while GL_SAMPLE_MASK is enabled - the enable bit is already in capabilityBits, and
// folding one more word costs nothing on a path that only recomputes when the
// pipeline-state version moved.
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(ResolveEffectiveSampleMask(rasterizationSamples)));
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.PatchVertices)); hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.PatchVertices));
// The default tessellation levels belong here for the same reason PatchVertices does: // The default tessellation levels belong here for the same reason PatchVertices does:
// when a program has an evaluation stage and no control stage, both are compiled into the // when a program has an evaluation stage and no control stage, both are compiled into the
@@ -4927,10 +4969,13 @@ void main() {
// The version only guards recomputing the hash - unchanged version, unchanged bytes. // The version only guards recomputing the hash - unchanged version, unchanged bytes.
const Uint renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion(); const Uint renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion || if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
m_pipelineStateHashColorCount != renderPassEntry.colorAttachmentCount) { m_pipelineStateHashColorCount != renderPassEntry.colorAttachmentCount ||
m_pipelineStateHash = ComputePipelineStateHash(renderPassEntry.colorAttachmentCount); m_pipelineStateHashSampleCount != renderPassEntry.sampleCount) {
m_pipelineStateHash =
ComputePipelineStateHash(renderPassEntry.colorAttachmentCount, renderPassEntry.sampleCount);
m_pipelineStateHashVersion = renderStateVersion; m_pipelineStateHashVersion = renderStateVersion;
m_pipelineStateHashColorCount = renderPassEntry.colorAttachmentCount; m_pipelineStateHashColorCount = renderPassEntry.colorAttachmentCount;
m_pipelineStateHashSampleCount = renderPassEntry.sampleCount;
m_pipelineStateHashValid = true; m_pipelineStateHashValid = true;
} }
const Uint64 pipelineStateHash = m_pipelineStateHash; const Uint64 pipelineStateHash = m_pipelineStateHash;
@@ -5199,6 +5244,8 @@ void main() {
.sampleShadingEnable = m_sampleRateShadingFeatureEnabled && .sampleShadingEnable = m_sampleRateShadingFeatureEnabled &&
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading), MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading),
.minSampleShading = MG_State::pGLContext->GetMinSampleShadingValue(), .minSampleShading = MG_State::pGLContext->GetMinSampleShadingValue(),
// Word 1 keeps its all-ones initialiser: GL has no state for samples 32..63.
.sampleMask = {ResolveEffectiveSampleMask(renderPassEntry.sampleCount), 0xffffffffu},
.subpass = 0, .subpass = 0,
.topology = vkTopology, .topology = vkTopology,
.primitiveRestartEnable = primitiveRestartEnabled, .primitiveRestartEnable = primitiveRestartEnabled,
@@ -5533,18 +5580,32 @@ void main() {
} }
} }
// Dual-source blending (GL_SRC1_* factors from glBlendFunc paired with // Dual-source blending (GL_SRC1_* factors from glBlendFunc paired with
// glBindFragDataLocationIndexed) requires the dualSrcBlend device feature. It is detected at // glBindFragDataLocationIndexed) requires the dualSrcBlend device feature. It is detected
// device creation and surfaced in the POST; if a shader actually issues a draw with a SRC1 // at device creation and surfaced in the POST; there is no fallback that BLENDS correctly,
// factor on a device that lacks it, there is no fallback, so hard-fail here at use time // so a draw that asks for a SRC1 factor on a device without the feature gets the blend
// rather than silently mistranslating the blend equation. // DECLINED - this attachment is baked with blending off and neutral One/Zero factors, and
if (effectiveBlendEnabled && !m_dualSrcBlendFeatureEnabled && // the loss is logged once. Both the factors AND the enable have to be neutralised:
// VUID-VkPipelineColorBlendAttachmentState-srcColorBlendFactor-00608 and its three
// siblings forbid a VK_BLEND_FACTOR_SRC1_* in the struct without the feature whatever
// blendEnable says, so clearing only the enable would still be invalid pipeline state.
// The previous behaviour, throwing, took the whole process down over one unsupported
// blend factor; this is defined, survivable and visible in the log, and it matches what
// the non-blendable-format arm above already does.
if (!m_dualSrcBlendFeatureEnabled &&
(IsDualSourceBlendFactor(srcRGB) || IsDualSourceBlendFactor(dstRGB) || (IsDualSourceBlendFactor(srcRGB) || IsDualSourceBlendFactor(dstRGB) ||
IsDualSourceBlendFactor(srcAlpha) || IsDualSourceBlendFactor(dstAlpha))) { IsDualSourceBlendFactor(srcAlpha) || IsDualSourceBlendFactor(dstAlpha))) {
THROW_EXCEPTION( MGLOG_E_ONCE(
"Dual-source blending (GL_SRC1_* blend factor) was used on color attachment " + "GetOrCreatePipeline: dual-source blending (GL_SRC1_* blend factor) was requested on "
std::to_string(i) + "color attachment %u, but the Vulkan device does not support the dualSrcBlend feature "
", but the Vulkan device does not support the dualSrcBlend feature (see the " "(see the dualSrcBlend row in the driver POST). Blending is DECLINED on that "
"dualSrcBlend row in the driver POST). No fallback exists; the draw cannot proceed."); "attachment - the fragment's first output is written unblended and the second source "
"is dropped (program=%u)",
i, program.GetExternalIndex());
effectiveBlendEnabled = false;
srcRGB = BlendFactor::One;
dstRGB = BlendFactor::Zero;
srcAlpha = BlendFactor::One;
dstAlpha = BlendFactor::Zero;
} }
payload.colorBlendAttachments[i] = MakeColorBlendAttachmentState( payload.colorBlendAttachments[i] = MakeColorBlendAttachmentState(
effectiveBlendEnabled, effectiveBlendEnabled,
@@ -6047,6 +6108,16 @@ void main() {
snap.programFactoryEpoch = m_programFactory->GetCacheStructureEpoch(); snap.programFactoryEpoch = m_programFactory->GetCacheStructureEpoch();
} }
const auto& programObj = *programObjPtr; const auto& programObj = *programObjPtr;
// Pinned for BeginXfbCaptureForDraw, which otherwise decides from GL state alone and has
// no way to know the bound pipeline's last pre-rasterization module lost (or never got)
// its Xfb execution mode. See VkProgramObject::xfbCaptureDeclined.
m_currentDrawXfbCaptureDeclined = programObj.xfbCaptureDeclined;
// A refused program cannot reach here today - the full path refuses before it ever
// records a snapshot - but declining the fast path costs one compare and means the
// refusal does not depend on that ordering staying true.
if (programObj.pointSizeCapabilityUnsupported) {
return false;
}
// The pipeline and the vertex-input pre-flight depend on the VAO only through // The pipeline and the vertex-input pre-flight depend on the VAO only through
// its resolved LAYOUT (layoutHash folds the attribute formats, bindings and the // its resolved LAYOUT (layoutHash folds the attribute formats, bindings and the
@@ -6209,10 +6280,13 @@ void main() {
// instead of missing forever on a monotonic version. A miss falls through // instead of missing forever on a monotonic version. A miss falls through
// to the full lookup. // to the full lookup.
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion || if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
m_pipelineStateHashColorCount != snap.renderPassColorCount) { m_pipelineStateHashColorCount != snap.renderPassColorCount ||
m_pipelineStateHash = ComputePipelineStateHash(snap.renderPassColorCount); m_pipelineStateHashSampleCount != snap.renderPassSampleCount) {
m_pipelineStateHash =
ComputePipelineStateHash(snap.renderPassColorCount, snap.renderPassSampleCount);
m_pipelineStateHashVersion = renderStateVersion; m_pipelineStateHashVersion = renderStateVersion;
m_pipelineStateHashColorCount = snap.renderPassColorCount; m_pipelineStateHashColorCount = snap.renderPassColorCount;
m_pipelineStateHashSampleCount = snap.renderPassSampleCount;
m_pipelineStateHashValid = true; m_pipelineStateHashValid = true;
} }
const auto memoTransformFlags = const auto memoTransformFlags =
@@ -6447,6 +6521,16 @@ void main() {
} }
} }
const auto& programObj = *resolvedProgramObj; const auto& programObj = *resolvedProgramObj;
// Pinned for BeginXfbCaptureForDraw, which otherwise decides from GL state alone and has
// no way to know the bound pipeline's last pre-rasterization module lost (or never got)
// its Xfb execution mode. See VkProgramObject::xfbCaptureDeclined.
m_currentDrawXfbCaptureDeclined = programObj.xfbCaptureDeclined;
// The build already said why, once, naming the program and the stage. Refusing here -
// before any pipeline is built from it - is what makes that message a decline rather
// than a note attached to invalid usage the driver still receives.
if (programObj.pointSizeCapabilityUnsupported) {
return false;
}
// For the snapshot's memoised entry pointer: if anything below inserts into the // For the snapshot's memoised entry pointer: if anything below inserts into the
// program cache (blit/aux program compiles), the epoch moves and the snapshot // program cache (blit/aux program compiles), the epoch moves and the snapshot
// stores no pointer for this draw - the fast path then re-looks-up once. // stores no pointer for this draw - the fast path then re-looks-up once.
@@ -6485,11 +6569,31 @@ void main() {
const Uint64 programLifetimeId = program.GetLifetimeId(); const Uint64 programLifetimeId = program.GetLifetimeId();
const Uint32 programVersion = program.GetBackendStateVersion(); const Uint32 programVersion = program.GetBackendStateVersion();
const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration(); const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
// The bind generation alone stopped covering this set the moment ResolveSampledBinding
// started asking SamplesAsIncompleteTexture: membership now depends on the effective
// sampler PARAMETERS (MIN_FILTER decides whether the mip chain is read at all) and on
// the texture SHAPE, and neither moves the bind generation. A texture that flips
// incomplete -> complete under a fixed binding - one glTexParameteri, one
// glSamplerParameteri, a BASE_LEVEL/MAX_LEVEL change, or an upload that fills the
// chain - would keep replaying the FALLBACK out of this memo, so the real texture
// never got its pre-pass sync, its pending-clear materialisation or its sampled-layout
// transition, and the descriptor path would then transition it from INSIDE the open
// render pass, which the subpass declares no self-dependency for.
//
// The sampling-resolution generation is exactly the counter for that family and is
// deliberately coarse (any texture, any sampler), so this one term covers every input
// the predicate reads that the bind generation does not: TextureObjectBase::
// BumpShapeVersion and SamplerObject::BumpVersion both bump it, while WHICH sampler
// object a unit carries goes through TextureUnit::SetSamplerObject and moves the bind
// generation instead. Same term the SetupDrawSnapshot fast path and the LOD memo
// already carry.
const Uint64 samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
const Bool sampledSetUnchanged = const Bool sampledSetUnchanged =
m_lastSampledSetValid && m_lastSampledSetProgramLifetimeId == programLifetimeId && m_lastSampledSetValid && m_lastSampledSetProgramLifetimeId == programLifetimeId &&
m_lastSampledSetProgramVersion == programVersion && m_lastSampledSetProgramVersion == programVersion &&
m_lastSampledSetTransformFlags == transformFlags && m_lastSampledSetTransformFlags == transformFlags &&
m_lastSampledSetBindGeneration == bindGeneration; m_lastSampledSetBindGeneration == bindGeneration &&
m_lastSampledSetSamplingGeneration == samplingGeneration;
if (!sampledSetUnchanged) { if (!sampledSetUnchanged) {
const Bool hasSampledTextures = m_uniformManager->CollectSampledTextures( const Bool hasSampledTextures = m_uniformManager->CollectSampledTextures(
program, programObj, sampledTextures, &m_sampledBindingRecordsScratch); program, programObj, sampledTextures, &m_sampledBindingRecordsScratch);
@@ -6499,6 +6603,7 @@ void main() {
m_lastSampledSetProgramVersion = programVersion; m_lastSampledSetProgramVersion = programVersion;
m_lastSampledSetTransformFlags = transformFlags; m_lastSampledSetTransformFlags = transformFlags;
m_lastSampledSetBindGeneration = bindGeneration; m_lastSampledSetBindGeneration = bindGeneration;
m_lastSampledSetSamplingGeneration = samplingGeneration;
} }
// Complete a freshly-made LOD decision (see above): its params sum // Complete a freshly-made LOD decision (see above): its params sum
// can only be taken once the sampled set is known. A genuine // can only be taken once the sampled set is known. A genuine
@@ -6544,9 +6649,21 @@ void main() {
} }
auto* textureResource = m_textureManager->SyncTextureAndGetDescriptor(*sampledTexture); auto* textureResource = m_textureManager->SyncTextureAndGetDescriptor(*sampledTexture);
MOBILEGL_ASSERT(textureResource != nullptr, if (textureResource == nullptr) {
"%s: SyncTextureAndGetDescriptor failed for textureId=%d", // SyncTextureAndGetDescriptor has a real failure channel - an incomplete or
__func__, sampledTexture->GetExternalIndex()); // otherwise unbackable texture declines and returns nullptr with its own log
// line - and the assert that used to be the only guard here is compiled out of
// every build past DEBUG. The next line dereferenced it, so a sampler left
// pointing at a texture GL calls incomplete was a SIGSEGV inside SetupDraw
// rather than a degraded draw. Leave the slot null and carry on: the descriptor
// resolve substitutes the fallback texture for exactly these bindings
// (ResolveSamplerDescriptor's SamplesAsIncompleteTexture branch), and the fast
// path at the top of SetupDraw already treats a null resource as "re-resolve".
MGLOG_E_ONCE("SetupDraw: no texture resource for sampled textureId=%d; leaving the binding to the "
"descriptor resolve's fallback",
sampledTexture->GetExternalIndex());
continue;
}
sampledResources[sampledIndex] = textureResource; sampledResources[sampledIndex] = textureResource;
MGLOG_D("SetupDraw: sampled textureId=%d layout(before)=%s(%d)", MGLOG_D("SetupDraw: sampled textureId=%d layout(before)=%s(%d)",
sampledTexture->GetExternalIndex(), VkImageLayoutToString(textureResource->layout), sampledTexture->GetExternalIndex(), VkImageLayoutToString(textureResource->layout),
@@ -6610,9 +6727,14 @@ void main() {
MOBILEGL_ASSERT(ready, "%s: TransitionTextureForSampling failed for textureId=%d", MOBILEGL_ASSERT(ready, "%s: TransitionTextureForSampling failed for textureId=%d",
__func__, sampledTexture->GetExternalIndex()); __func__, sampledTexture->GetExternalIndex());
auto* transitionedResource = m_textureManager->SyncTextureAndGetDescriptor(*sampledTexture); auto* transitionedResource = m_textureManager->SyncTextureAndGetDescriptor(*sampledTexture);
MOBILEGL_ASSERT(transitionedResource != nullptr, if (transitionedResource == nullptr) {
"%s: post-transition SyncTextureAndGetDescriptor failed for textureId=%d", // Same declined-sync channel as the first loop, and the same reason not to
__func__, sampledTexture->GetExternalIndex()); // dereference it: StampResourceRecordingUse below takes a reference.
MGLOG_E_ONCE("SetupDraw: no texture resource after transitioning sampled textureId=%d; leaving the "
"binding to the descriptor resolve's fallback",
sampledTexture->GetExternalIndex());
continue;
}
// Pre-pass stream bookkeeping: the draw about to be recorded reads // Pre-pass stream bookkeeping: the draw about to be recorded reads
// this image, so later out-of-pass work on it can no longer jump // this image, so later out-of-pass work on it can no longer jump
// ahead of the recording. // ahead of the recording.
@@ -6780,6 +6902,7 @@ void main() {
snap.drawUsesDepthStencil = drawUsesDepthStencil; snap.drawUsesDepthStencil = drawUsesDepthStencil;
snap.renderPassExtent = renderPassEntry->extent; snap.renderPassExtent = renderPassEntry->extent;
snap.renderPassColorCount = renderPassEntry->colorAttachmentCount; snap.renderPassColorCount = renderPassEntry->colorAttachmentCount;
snap.renderPassSampleCount = renderPassEntry->sampleCount;
snap.pipeline = pipeline; snap.pipeline = pipeline;
// The layout identity the fast path's aux-memo compare answers against. // The layout identity the fast path's aux-memo compare answers against.
// A memo hit here, not a rebuild: the pre-flight above resolved this // A memo hit here, not a rebuild: the pre-flight above resolved this
@@ -7579,7 +7702,8 @@ void main() {
Bool VulkanRenderer::ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer, Bool VulkanRenderer::ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel, MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 depthSlice, const VkClearValue& clearValue) { Uint32 depthSlice, const VkClearValue& clearValue,
VkImageLayout finalLayout) {
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture); auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE) return false; if (resource == nullptr || resource->image == VK_NULL_HANDLE) return false;
if (m_frameContext.GetCurrentFrameIndex() >= m_deferredDepthMipmapCleanup.size()) return false; if (m_frameContext.GetCurrentFrameIndex() >= m_deferredDepthMipmapCleanup.size()) return false;
@@ -7592,7 +7716,11 @@ void main() {
VkAttachmentDescription colorAttachment{}; VkAttachmentDescription colorAttachment{};
colorAttachment.format = resource->format; colorAttachment.format = resource->format;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; // The image's own count, not a hardcoded one: a render-pass attachment must match the
// image it is given (VUID-VkFramebufferCreateInfo-pAttachments-00880), and this helper is
// now also the multisample path - a multisample image carries no TRANSFER_DST usage, so a
// load-op clear is the only legal way to clear it at all.
colorAttachment.samples = resource->sampleCount;
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
@@ -7600,7 +7728,7 @@ void main() {
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
// Hand the slice back in the layout the caller already tracks for the whole image, so its // Hand the slice back in the layout the caller already tracks for the whole image, so its
// closing barrier stays truthful and resource->layout is never touched from in here. // closing barrier stays truthful and resource->layout is never touched from in here.
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; colorAttachment.finalLayout = finalLayout;
VkAttachmentReference colorRef{}; VkAttachmentReference colorRef{};
colorRef.attachment = 0; colorRef.attachment = 0;
@@ -7668,9 +7796,26 @@ void main() {
"MaterializePendingClearForTexture requires no active render pass on the target buffer"); "MaterializePendingClearForTexture requires no active render pass on the target buffer");
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture); auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture);
MOBILEGL_ASSERT(resource != nullptr, if (resource == nullptr) {
"MaterializePendingClearForTexture: SyncTextureAndGetDescriptor failed for textureId=%d", // Declined sync (an incomplete texture, say). Nothing to clear into, and every line
texture.GetExternalIndex()); // below dereferences this - the assert that used to stand here is compiled out of
// every build past DEBUG.
MGLOG_E_ONCE("MaterializePendingClearForTexture: no texture resource for textureId=%d; the queued clears "
"stay queued",
texture.GetExternalIndex());
return false;
}
// A multisample image is not a transfer target: SyncTextureResource deliberately withholds
// TRANSFER_DST/TRANSFER_SRC from every one of them, so the vkCmdClearColorImage below -
// and the TRANSFER_DST transition ahead of it - are invalid usage
// (VUID-vkCmdClearColorImage-image-00002) on exactly the shape a
// glClearBufferfv-then-sample sequence produces. Clear it the one way that is legal at
// any sample count instead: a throwaway render pass whose whole content is its load-op
// clear, which is also what the 3D-slice case below already does.
if (resource->sampleCount != VK_SAMPLE_COUNT_1_BIT) {
return MaterializeMultisamplePendingClear(commandBuffer, texture, *resource, pendingClears);
}
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags srcAccessMask = 0; VkAccessFlags srcAccessMask = 0;
@@ -7810,6 +7955,77 @@ void main() {
return true; return true;
} }
Bool VulkanRenderer::MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture,
VkTextureManager::TextureResource& resource,
const Vector<PendingClearEntry>& pendingClears) {
// Colour only. GL can queue a depth/stencil clear on a multisample texture too, and the
// load-op idiom would serve it just as well, but this helper attaches its view as a
// COLOUR attachment; declining is honest and leaves the queue intact for a later path.
if ((resource.aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
MGLOG_E_ONCE("MaterializeMultisamplePendingClear: textureId=%d is a multisample depth/stencil texture; "
"its queued clear cannot be materialised out of a render pass yet",
texture.GetExternalIndex());
return false;
}
Bool allCleared = true;
for (const auto& pendingClear : pendingClears) {
if (pendingClear.key.mipLevel >= resource.mipLevels) {
MGLOG_E_ONCE("MaterializeMultisamplePendingClear: textureId=%d pending clear mip=%u out of range %u",
texture.GetExternalIndex(), pendingClear.key.mipLevel, resource.mipLevels);
allCleared = false;
continue;
}
auto clearPayload = pendingClear.payload;
PreCompensateSrgbClearColor(clearPayload, resource.format);
VkClearValue clearValue{};
clearValue.color = MakeVkClearColorValue(clearPayload, ColorFormatLacksAlpha(&texture));
// A multisample texture has exactly one level and, for the 2D target, one layer; the
// array target's layers are cleared one at a time, which is what this helper's
// per-layer view gives us.
const Uint32 firstLayer = pendingClear.key.baseArrayLayer;
const Uint32 layerCount = std::max(pendingClear.key.layerCount, 1u);
for (Uint32 layer = firstLayer; layer < firstLayer + layerCount; ++layer) {
if (layer >= resource.arrayLayers) break;
// COLOR_ATTACHMENT_OPTIMAL, not TRANSFER_DST: the image never has transfer usage,
// and a render target is where it came from and where it is going.
if (!ClearDepthSliceWithRenderPass(commandBuffer, texture, pendingClear.key.mipLevel, layer,
clearValue, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL)) {
MGLOG_E_ONCE("MaterializeMultisamplePendingClear: textureId=%d layer %u could not be cleared",
texture.GetExternalIndex(), layer);
allCleared = false;
}
}
}
if (!allCleared) {
return false;
}
// The load-op clear left every touched layer in COLOR_ATTACHMENT_OPTIMAL (each pass's
// finalLayout), so that - not the tracked layout on entry - is what the closing barrier
// has to start from.
// TransitionImageLayout takes the tracked layout by reference and updates it, so seeding
// it is both how the barrier learns its source and how resource->layout ends up right.
resource.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
const Bool ok = VkTextureManager::TransitionImageLayout(
commandBuffer, resource.image, resource.layout,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
resource.aspect, 0, resource.mipLevels);
if (!ok) {
MGLOG_E_ONCE("MaterializeMultisamplePendingClear: failed to transition textureId=%d to the sampled layout",
texture.GetExternalIndex());
return false;
}
m_clearManager->PopPendingClear(&texture);
MGLOG_D("MaterializeMultisamplePendingClear: textureId=%d pending clear materialised through a load-op pass",
texture.GetExternalIndex());
return true;
}
Bool VulkanRenderer::MaterializePendingClearForRenderbuffer( Bool VulkanRenderer::MaterializePendingClearForRenderbuffer(
VkCommandBuffer commandBuffer, const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer) { VkCommandBuffer commandBuffer, const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer) {
if (renderbuffer == nullptr) { if (renderbuffer == nullptr) {
@@ -10919,6 +11135,19 @@ void main() {
if (!program || program->GetTransformFeedbackVaryingCount() == 0) { if (!program || program->GetTransformFeedbackVaryingCount() == 0) {
return false; return false;
} }
// The bound pipeline's last pre-rasterization stage has to have been declared with Xfb
// (VUID-vkCmdBeginTransformFeedbackEXT-None-04128). Everything above this line reads GL
// state, which cannot answer that: a program can be built as a capture variant and still
// end up with a module carrying no Xfb mode - the clip/XFB validation backstop rewinding
// past the decoration, or XfbCaptureDecoratePass resolving none of the requested varyings
// and changing nothing. Declining the span leaves the capture buffers untouched, which is
// the same nothing the driver would have written, without the undefined behaviour.
if (m_currentDrawXfbCaptureDeclined) {
MGLOG_E_ONCE("BeginXfbCaptureForDraw: declining the capture span - the bound program's last "
"pre-rasterization stage carries no Xfb execution mode, so recording one would be "
"undefined behaviour rather than a capture");
return false;
}
const SizeT bufferCount = std::min<SizeT>(program->GetTransformFeedbackBufferCount(), 4); const SizeT bufferCount = std::min<SizeT>(program->GetTransformFeedbackBufferCount(), 4);
if (bufferCount == 0) { if (bufferCount == 0) {
return false; return false;
@@ -12977,6 +13206,18 @@ void main() {
: supportedDeviceFeatures.robustBufferAccess; : supportedDeviceFeatures.robustBufferAccess;
deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader; deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader;
deviceFeatures.tessellationShader = supportedDeviceFeatures.tessellationShader; deviceFeatures.tessellationShader = supportedDeviceFeatures.tessellationShader;
// gl_PointSize is an ORDINARY per-vertex output in desktop GL - a tessellation
// evaluation or geometry shader may write it, and a program may capture it by name -
// but in Vulkan the PointSize built-in is only usable from those two stages when this
// feature is on (VUID-RuntimeSpirv-PointSize-06439; SPIR-V spells the requirement as
// the TessellationPointSize / GeometryPointSize capabilities, which glslang emits from
// any such write). Left off, every one of those programs is invalid usage that a lenient
// driver silently gives an undefined point size and a strict one faults on. Nothing here
// asks for it speculatively: the feature is taken only where the device advertises it.
deviceFeatures.shaderTessellationAndGeometryPointSize =
supportedDeviceFeatures.shaderTessellationAndGeometryPointSize;
m_tessellationAndGeometryPointSizeFeatureEnabled =
deviceFeatures.shaderTessellationAndGeometryPointSize == VK_TRUE;
// Sampled-read barriers may only name the shader stages whose device feature is // Sampled-read barriers may only name the shader stages whose device feature is
// actually enabled (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), so the // actually enabled (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), so the
// mask is assembled here, next to the feature decision, and handed to consumers. // mask is assembled here, next to the feature decision, and handed to consumers.
@@ -584,6 +584,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent. // needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
Bool m_dualSrcBlendFeatureEnabled = false; Bool m_dualSrcBlendFeatureEnabled = false;
Bool m_primitiveTopologyListRestartFeatureEnabled = false; Bool m_primitiveTopologyListRestartFeatureEnabled = false;
// shaderTessellationAndGeometryPointSize gates the PointSize built-in in a tessellation
// or geometry stage, which desktop GL treats as an ordinary per-vertex output (writable,
// and capturable by name through transform feedback). Cached at device creation and
// handed to ProgramFactory, which refuses a program whose tessellation or geometry module
// declares the matching SPIR-V capability while this is false - SetupDraw then skips its
// draws (VkProgramObject::pointSizeCapabilityUnsupported) rather than building a pipeline
// that is invalid usage.
Bool m_tessellationAndGeometryPointSizeFeatureEnabled = false;
// VK_EXT_custom_border_color. Vulkan's four predefined VkBorderColor values cover only // VK_EXT_custom_border_color. Vulkan's four predefined VkBorderColor values cover only
// transparent/opaque black and opaque white; GL_TEXTURE_BORDER_COLOR is an arbitrary vec4 (or // transparent/opaque black and opaque white; GL_TEXTURE_BORDER_COLOR is an arbitrary vec4 (or
// an arbitrary ivec4/uvec4 through the "I" entry points). Without this extension a border // an arbitrary ivec4/uvec4 through the "I" entry points). Without this extension a border
@@ -768,9 +776,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// version: the version is monotonic and bumps on every pipeline-state // version: the version is monotonic and bumps on every pipeline-state
// change, so an unchanged (version, colorAttachmentCount) proves the state // change, so an unchanged (version, colorAttachmentCount) proves the state
// bytes are unchanged and the hash can be reused without re-reading them. // bytes are unchanged and the hash can be reused without re-reading them.
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount) const; Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount,
VkSampleCountFlagBits rasterizationSamples) const;
// The effective GL_SAMPLE_MASK word for a draw at this rasterization sample count; see
// the definition for the GL-vs-Vulkan rule it reconciles. Shared by the pipeline payload
// and the pipeline-state memo word so the two cannot disagree.
Uint32 ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const;
Uint m_pipelineStateHashVersion = 0; Uint m_pipelineStateHashVersion = 0;
Uint32 m_pipelineStateHashColorCount = 0; Uint32 m_pipelineStateHashColorCount = 0;
// The sample count the cached hash was computed at. A pipeline-state input now depends on
// it (the effective sample mask), so a draw that changes only the target's sample count
// has to recompute rather than reuse.
VkSampleCountFlagBits m_pipelineStateHashSampleCount = VK_SAMPLE_COUNT_1_BIT;
Uint64 m_pipelineStateHash = 0; Uint64 m_pipelineStateHash = 0;
Bool m_pipelineStateHashValid = false; Bool m_pipelineStateHashValid = false;
// GetShaderTransformFlags memo. NOT pure in the pre-transform alone: the // GetShaderTransformFlags memo. NOT pure in the pre-transform alone: the
@@ -812,7 +829,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Skip the per-draw CollectSampledTextures walk (~5% of the render thread) when the sampled // Skip the per-draw CollectSampledTextures walk (~5% of the render thread) when the sampled
// texture SET is provably unchanged from the previous draw: same program (lifetime id + // texture SET is provably unchanged from the previous draw: same program (lifetime id +
// backend-state version, which covers sampler-uniform reassignment / relink) and transform // backend-state version, which covers sampler-uniform reassignment / relink) and transform
// flags, and no texture bind/unbind/delete since (GetTextureBindGeneration). On a hit, // flags, no texture bind/unbind/delete since (GetTextureBindGeneration), and nothing that
// moves a texture's shape or a sampler's parameters since (GetSamplingResolutionGeneration
// - membership depends on mipmap-completeness, which both of those decide). On a hit,
// m_sampledTexturesScratch still holds the previous draw's list and steps 2-4 (feedback / // m_sampledTexturesScratch still holds the previous draw's list and steps 2-4 (feedback /
// layout probe / transition) re-run on it, so layout correctness is unaffected - only the GL // layout probe / transition) re-run on it, so layout correctness is unaffected - only the GL
// walk is skipped. The program lifetime id (never reused, unlike the GL name) and the // walk is skipped. The program lifetime id (never reused, unlike the GL name) and the
@@ -823,6 +842,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 m_lastSampledSetProgramVersion = 0; Uint32 m_lastSampledSetProgramVersion = 0;
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {}; ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
Uint64 m_lastSampledSetBindGeneration = 0; Uint64 m_lastSampledSetBindGeneration = 0;
Uint64 m_lastSampledSetSamplingGeneration = 0;
// Set from the draw's resolved VkProgramObject on both the full and the fast setup paths;
// read by BeginXfbCaptureForDraw, which has only GL state otherwise. See
// VkProgramObject::xfbCaptureDeclined.
Bool m_currentDrawXfbCaptureDeclined = false;
// Memo for the per-draw explicit-LOD-0 eligibility probe // Memo for the per-draw explicit-LOD-0 eligibility probe
// (ProgramSamplesOnlySingleLevelTextures): same key family as the // (ProgramSamplesOnlySingleLevelTextures): same key family as the
@@ -921,6 +945,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// probe the pipeline memo after a state change without re-fetching the // probe the pipeline memo after a state change without re-fetching the
// render-pass entry (the pass itself is pinned by renderPassHash above). // render-pass entry (the pass itself is pinned by renderPassHash above).
Uint32 renderPassColorCount = 0; Uint32 renderPassColorCount = 0;
// Pinned with the colour count and for the same reason: the fast path recomputes the
// pipeline-state value hash from the snapshot, and that hash reads the sample count.
VkSampleCountFlagBits renderPassSampleCount = VK_SAMPLE_COUNT_1_BIT;
VkPipeline pipeline = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE;
// layoutHash of the snapshotting draw's vertex-input state. The pipeline and // layoutHash of the snapshotting draw's vertex-input state. The pipeline and
// the vertex-input pre-flight depend on the VAO only through this (plus the // the vertex-input pre-flight depend on the VAO only through this (plus the
@@ -1279,13 +1306,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLenum filter); GLenum filter);
// Clears one z slice of a VK_IMAGE_TYPE_3D colour image. See the call site in // Clears one layer of a colour image through a throwaway render pass whose entire content
// MaterializePendingClearForTexture for why a transfer clear cannot do this. // is its LOAD_OP_CLEAR. Two callers, both of which a transfer clear cannot serve: a z
// slice of a VK_IMAGE_TYPE_3D image (vkCmdClearColorImage cannot name one), and a
// MULTISAMPLE image (which carries no TRANSFER_DST usage at all). `finalLayout` is the
// layout the caller already tracks for the whole image, so this never has to touch
// resource->layout.
Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer, Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel, MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 depthSlice, const VkClearValue& clearValue); Uint32 depthSlice, const VkClearValue& clearValue,
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer, Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture); MG_State::GLState::ITextureObject& texture);
// The multisample arm of the above. Split out rather than branched inline because it
// shares none of the transfer path: a multisample image carries no TRANSFER_DST usage, so
// neither the TRANSFER_DST transition nor vkCmdClearColorImage is legal on one.
Bool MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture,
VkTextureManager::TextureResource& resource,
const Vector<PendingClearEntry>& pendingClears);
Bool MaterializePendingClearForRenderbuffer( Bool MaterializePendingClearForRenderbuffer(
VkCommandBuffer commandBuffer, VkCommandBuffer commandBuffer,
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer); const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
+32 -2
View File
@@ -146,6 +146,20 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TRIANGLES: return static_cast<Uint64>(count / 3); case GL_TRIANGLES: return static_cast<Uint64>(count / 3);
case GL_TRIANGLE_STRIP: case GL_TRIANGLE_STRIP:
case GL_TRIANGLE_FAN: return count >= 3 ? static_cast<Uint64>(count - 2) : 0; case GL_TRIANGLE_FAN: return count >= 3 ? static_cast<Uint64>(count - 2) : 0;
// Adjacency primitives (GL 4.6 core table 10.1). Only a geometry stage can consume
// them, and it is the ADJACENT-free primitive count that reaches it: 4 vertices per
// line, 6 per triangle, one per step for the strips. Answering 0 here - which is what
// the default arm did - made AccountTransformFeedbackPrimitives bail before it had
// recorded anything, so an adjacency capture advanced neither the captured-vertex
// counter the scattered-capture path is bounded by nor the geometry-capture-draw flag
// that routes the transform feedback queries to the driver's own counter.
case GL_LINES_ADJACENCY: return static_cast<Uint64>(count / 4);
case GL_LINE_STRIP_ADJACENCY: return count >= 4 ? static_cast<Uint64>(count - 3) : 0;
case GL_TRIANGLES_ADJACENCY: return static_cast<Uint64>(count / 6);
case GL_TRIANGLE_STRIP_ADJACENCY: return count >= 6 ? static_cast<Uint64>((count - 4) / 2) : 0;
// GL_PATCHES is deliberately absent: the tessellator's amplification is not knowable
// on the CPU, and answering 0 is what defers GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
// to the driver's own counter, which is the only correct source for a patch capture.
default: return 0; default: return 0;
} }
} }
@@ -172,11 +186,17 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_LINES: case GL_LINES:
case GL_LINE_STRIP: case GL_LINE_STRIP:
case GL_LINE_LOOP: case GL_LINE_LOOP:
// An adjacency primitive delivers the same line/triangle to the geometry stage; the
// adjacent vertices are context, not part of the primitive.
case GL_LINES_ADJACENCY:
case GL_LINE_STRIP_ADJACENCY:
verticesPerPrimitive = 2; verticesPerPrimitive = 2;
break; break;
case GL_TRIANGLES: case GL_TRIANGLES:
case GL_TRIANGLE_STRIP: case GL_TRIANGLE_STRIP:
case GL_TRIANGLE_FAN: case GL_TRIANGLE_FAN:
case GL_TRIANGLES_ADJACENCY:
case GL_TRIANGLE_STRIP_ADJACENCY:
verticesPerPrimitive = 3; verticesPerPrimitive = 3;
break; break;
default: default:
@@ -381,11 +401,21 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_POINTS: case GL_POINTS:
compatible = mode == GL_POINTS; compatible = mode == GL_POINTS;
break; break;
// The adjacency modes belong here too (GL 4.6 core table 13.1, ES 3.2 table 12.1).
// This arm is only reached when the program has NO geometry or tessellation
// evaluation stage, and without a geometry stage the adjacent vertices are simply
// ignored (GL 4.6 core 10.1) - the primitive assembled IS a plain line or triangle,
// so the combination is legal and must capture. Omitting them raised a spurious
// GL_INVALID_OPERATION and dropped the draw entirely, leaving the capture buffer
// with its pre-draw bytes. The geometry-stage input table above already carries the
// same four arms; this is the second table catching up with it.
case GL_LINES: case GL_LINES:
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP; compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP ||
mode == GL_LINES_ADJACENCY || mode == GL_LINE_STRIP_ADJACENCY;
break; break;
case GL_TRIANGLES: case GL_TRIANGLES:
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN; compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN ||
mode == GL_TRIANGLES_ADJACENCY || mode == GL_TRIANGLE_STRIP_ADJACENCY;
break; break;
default: default:
break; break;
+34 -9
View File
@@ -64,21 +64,43 @@ namespace MobileGL::MG_Impl::GLImpl {
// and none of them has any state beyond "which object is counting". // and none of them has any state beyond "which object is counting".
UnorderedMap<GLenum, GLuint> g_activePipelineStatisticsQueryIds; UnorderedMap<GLenum, GLuint> g_activePipelineStatisticsQueryIds;
// Whether MobileGL puts GL_ARB_tessellation_shader in its extension string. Read from the
// ADVERTISED list rather than from a capability bit for the same reason
// BackendSupportsTextureViews does (GL_Texture.cpp): it makes "MobileGL claims tessellation
// support" and "the tessellation-conditional API surface is open" the same fact by
// construction, so the day a backend starts advertising the string the surface below opens
// with it and no second edit is owed.
Bool AdvertisesTessellationShaderExtension() {
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) return false;
const auto& extensions = activeBackendObject->GetRendererInfo().RendererGLInfo.Extensions;
return std::find(extensions.begin(), extensions.end(), E_GL_ARB_tessellation_shader) != extensions.end();
}
// The eleven pipeline-statistics counters (GL 4.6 core table 4.3 / ARB_pipeline_statistics_query). // The eleven pipeline-statistics counters (GL 4.6 core table 4.3 / ARB_pipeline_statistics_query).
// A 4.6 core context has to ACCEPT all of them at glBeginQuery - the extension is core // A 4.6 core context ACCEPTS the nine unconditional ones at glBeginQuery - there is no query
// since 4.6 and there is no query by which an application could learn otherwise before // by which an application could learn otherwise before calling. MobileGL instruments none of
// calling. MobileGL instruments none of them, and says so the way GL 4.6 core 4.2.1 // them, and says so the way GL 4.6 core 4.2.1 provides for: GL_QUERY_COUNTER_BITS answers
// provides for: GL_QUERY_COUNTER_BITS answers zero for these targets, which is the // zero for these targets, which is the spec's own signal that the counter is unsupported and
// spec's own signal that the counter is unsupported and its results indeterminate. That // its results indeterminate. That is an honest zero, not an advertised capability - the
// is an honest zero, not an advertised capability - the alternative, GL_INVALID_ENUM on a // alternative, GL_INVALID_ENUM on a core entry point, is both non-conformant AND less
// core entry point, is both non-conformant AND less informative. // informative.
//
// The two TESSELLATION targets are the exception, because ARB_pipeline_statistics_query
// makes them conditional on tessellation support rather than unconditional, and the only
// thing an application (or the conformance suite) can read to decide whether an
// implementation has it is the GL_ARB_tessellation_shader string. MobileGL does not emit it
// today, so these two answer GL_INVALID_ENUM: an API surface that accepts a
// tessellation-conditional token while withholding the string that announces the condition
// is self-contradictory, and it is the contradiction the suite catches
// (KHR-GL46.pipeline_statistics_query_tests_ARB.api_coverage_unsupported_calls, whose
// support probe is gl4cPipelineStatisticsQueryTests.cpp:1166-1176). The gate is the
// advertisement itself, not a hardcoded "no", so this is one switch and not two.
Bool IsPipelineStatisticsQueryTarget(GLenum target) { Bool IsPipelineStatisticsQueryTarget(GLenum target) {
switch (target) { switch (target) {
case GL_VERTICES_SUBMITTED: case GL_VERTICES_SUBMITTED:
case GL_PRIMITIVES_SUBMITTED: case GL_PRIMITIVES_SUBMITTED:
case GL_VERTEX_SHADER_INVOCATIONS: case GL_VERTEX_SHADER_INVOCATIONS:
case GL_TESS_CONTROL_SHADER_PATCHES:
case GL_TESS_EVALUATION_SHADER_INVOCATIONS:
case GL_GEOMETRY_SHADER_INVOCATIONS: case GL_GEOMETRY_SHADER_INVOCATIONS:
case GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED: case GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED:
case GL_FRAGMENT_SHADER_INVOCATIONS: case GL_FRAGMENT_SHADER_INVOCATIONS:
@@ -86,6 +108,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_CLIPPING_INPUT_PRIMITIVES: case GL_CLIPPING_INPUT_PRIMITIVES:
case GL_CLIPPING_OUTPUT_PRIMITIVES: case GL_CLIPPING_OUTPUT_PRIMITIVES:
return true; return true;
case GL_TESS_CONTROL_SHADER_PATCHES:
case GL_TESS_EVALUATION_SHADER_INVOCATIONS:
return AdvertisesTessellationShaderExtension();
default: default:
return false; return false;
} }
@@ -58,6 +58,9 @@ add_executable(MobileGLIntegrationTest
Scenarios/DrawParametersScenario.cpp Scenarios/DrawParametersScenario.cpp
Scenarios/AsyncCompileScenario.cpp Scenarios/AsyncCompileScenario.cpp
Scenarios/XfbAfterClipDistanceScenario.cpp Scenarios/XfbAfterClipDistanceScenario.cpp
Scenarios/UnwrittenPositionOutputScenario.cpp
Scenarios/SampleMaskScopeScenario.cpp
Scenarios/SampledSetStalenessScenario.cpp
Scenarios/ThreeChannelAttachmentScenario.cpp Scenarios/ThreeChannelAttachmentScenario.cpp
Scenarios/SnormAttachmentScenario.cpp Scenarios/SnormAttachmentScenario.cpp
Scenarios/PipelineFailureScenario.cpp Scenarios/PipelineFailureScenario.cpp
@@ -97,6 +100,8 @@ add_executable(MobileGLIntegrationTest
Scenarios/VertexAttribBindingScenario.cpp Scenarios/VertexAttribBindingScenario.cpp
Scenarios/XfbCaptureBufferReuseScenario.cpp Scenarios/XfbCaptureBufferReuseScenario.cpp
Scenarios/XfbPrimitiveQueryScenario.cpp Scenarios/XfbPrimitiveQueryScenario.cpp
Scenarios/XfbRepeatedCaptureScenario.cpp
Scenarios/TessellationXfbCaptureScenario.cpp
Scenarios/VertexArrayEnableDisableScenario.cpp Scenarios/VertexArrayEnableDisableScenario.cpp
Scenarios/CopyImageLevelRangeScenario.cpp Scenarios/CopyImageLevelRangeScenario.cpp
Scenarios/CopyImageLayeredScenario.cpp Scenarios/CopyImageLayeredScenario.cpp
@@ -115,6 +120,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/IntegerBorderColorScenario.cpp Scenarios/IntegerBorderColorScenario.cpp
Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp
Scenarios/RenderbufferBlendFormatScenario.cpp Scenarios/RenderbufferBlendFormatScenario.cpp
Scenarios/DualSourceBlendScenario.cpp
) )
target_include_directories(MobileGLIntegrationTest PRIVATE target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -0,0 +1,238 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DualSourceBlendScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - A DUAL-SOURCE BLEND DRAW HAS TO SURVIVE ON EVERY DRIVER.
//
// GL_SRC1_COLOR / GL_ONE_MINUS_SRC1_COLOR / GL_SRC1_ALPHA / GL_ONE_MINUS_SRC1_ALPHA
// (ARB_blend_func_extended, core since 3.3) need a backend capability that not every device has:
// GL_EXT_blend_func_extended on the ES driver, or the dualSrcBlend device feature on Vulkan. When
// the capability IS there both backends translate the factors properly, and that has always
// worked. When it is NOT, both backends used to THROW_EXCEPTION at draw time - and
// MG_Util/Types.h's THROW_EXCEPTION is a plain `throw`, with no catch anywhere in MG_Impl or
// MG_Backend, so the exception unwound out through the C GL ABI and killed the process. An
// application asking for a blend factor the device cannot do is a picture problem, never a reason
// to take the process down.
//
// Both are now a DECLINE: the attachment is drawn with blending off and neutral One/Zero factors,
// and the loss is logged once. So a dual-source draw has exactly two defined outcomes, and this
// scenario pins that it lands on one of them and never on a crash:
//
// capability present - src0 * src1 + dst * (1 - src1)
// capability absent - src0, written straight through
//
// What each CI lane actually reaches: lavapipe has dualSrcBlend, so the DirectVulkan lane runs the
// whole sequence and measures the blend. Mesa's GLES front end on llvmpipe has no
// GL_EXT_blend_func_extended, so the ESSL stage carrying `layout(index = 1)` never compiles and the
// program renders nothing - the DirectGLES lane therefore SKIPS on the capability probe in SetUp
// rather than measuring a picture the driver never produced. The DECLINE arm itself - the path this
// scenario exists for - is unit-tested against stubbed capabilities in
// MG_Test/Framebuffer/FramebufferTest.cpp (DualSourceBlendIsDeclinedRatherThanThrownWhenTheExtensionIsMissing),
// which is the only place it can be reached without a driver that lacks the extension.
//
// The Vulkan half has a second edge the last case covers: the dual-source VUIDs
// (VUID-VkPipelineColorBlendAttachmentState-srcColorBlendFactor-00608 and its three siblings)
// forbid a VK_BLEND_FACTOR_SRC1_* anywhere in VkPipelineColorBlendAttachmentState without the
// feature, whatever blendEnable says - so leaving the factors in place while clearing the enable
// would still be invalid pipeline state.
#include <cstdint>
#include <string>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kExtent = 16;
constexpr const char* kVertexSource = R"(#version 330 core
void main()
{
switch (gl_VertexID)
{
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
}
}
)";
// Two outputs on the SAME location, indices 0 and 1: the shader-side spelling of
// dual-source output (GLSL 3.30 4.4.2, the `index` layout qualifier). No
// glBindFragDataLocationIndexed needed, which keeps the program buildable through the
// harness's compile-and-link helper.
constexpr const char* kDualSourceFragmentSource = R"(#version 330 core
uniform vec4 uSrc0;
uniform vec4 uSrc1;
layout(location = 0, index = 0) out vec4 fragColor0;
layout(location = 0, index = 1) out vec4 fragColor1;
void main()
{
fragColor0 = uSrc0;
fragColor1 = uSrc1;
}
)";
class DualSourceBlendScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glGenRenderbuffers(1, &m_renderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kExtent, kExtent);
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
std::string error;
m_program = CompileProgram(kVertexSource, kDualSourceFragmentSource, &error);
m_programError = error;
glViewport(0, 0, kExtent, kExtent);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
// Capability probe, not an assertion. A GL link that succeeded is not proof that
// the BACKEND can run the program: DirectGLES transpiles to ESSL lazily at first
// use, and GLSL ES has no `index` layout qualifier outside
// GL_EXT_blend_func_extended, so on a driver without it the stage never compiles
// and the draw renders nothing. One unblended white draw tells the two apart, and
// the cases skip rather than measure a picture the driver never produced.
if (m_program != 0) {
glDisable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ZERO);
Draw(/*src0=*/1.0f, /*src1=*/1.0f);
glFinish();
const Image probe = ReadPixels(kExtent, kExtent);
m_programRenders =
!probe.Empty() && static_cast<int>(probe.At(kExtent / 2, kExtent / 2).r) > 245;
}
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
void TearDown() override {
if (!Ready()) return;
glDisable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ZERO);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
if (m_renderbuffer != 0) glDeleteRenderbuffers(1, &m_renderbuffer);
if (m_program != 0) glDeleteProgram(m_program);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
}
void Draw(float src0, float src1) {
glUseProgram(m_program);
glUniform4f(glGetUniformLocation(m_program, "uSrc0"), src0, src0, src0, 1.0f);
glUniform4f(glGetUniformLocation(m_program, "uSrc1"), src1, src1, src1, 1.0f);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
glUseProgram(0);
}
// Both cases share this gate: nothing below can be measured on a backend that cannot
// run a dual-source fragment program at all. Returns the skip reason, empty when the
// program runs - NOT a void helper that calls GTEST_SKIP itself, because GTEST_SKIP
// expands to a `return` and would leave only the HELPER, letting the case run its
// assertions anyway and report Failed instead of Skipped.
std::string WhyTheProgramCannotRun() const {
if (m_program == 0) {
return "this driver cannot build a dual-source fragment shader: " + m_programError;
}
if (!m_programRenders) {
return "this backend links a dual-source fragment program but renders nothing with it "
"(GLSL ES has no `index` layout qualifier without GL_EXT_blend_func_extended)";
}
return {};
}
GLuint m_renderbuffer = 0;
GLuint m_fbo = 0;
GLuint m_vao = 0;
unsigned int m_program = 0;
bool m_programRenders = false;
std::string m_programError;
};
} // namespace
// The whole point of the scenario: this sequence used to be a process kill on any device
// without the capability, and it has to be a picture either way.
//
// dst is black, src0 is white and src1 is mid-grey, with SRC1_COLOR / ONE_MINUS_SRC1_COLOR.
// blended = 1.0 * 0.5 + 0.0 * 0.5 = 0.5 -> ~128
// declined = 1.0 -> 255
// Anything else means the factors were mistranslated rather than either honoured or declined.
TEST_F(DualSourceBlendScenario, DualSourceBlendDrawProducesOneOfTheTwoDefinedResults) {
if (!Ready()) GTEST_SKIP();
if (const std::string reason = WhyTheProgramCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
glDisable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ZERO);
Draw(/*src0=*/0.0f, /*src1=*/0.0f);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR);
EXPECT_EQ(FirstGLError(), 0u) << "glBlendFunc must accept the GL_SRC1_* factors - they are core since 3.3";
Draw(/*src0=*/1.0f, /*src1=*/0.5f);
glFinish();
glDisable(GL_BLEND);
EXPECT_EQ(FirstGLError(), 0u) << "the dual-source draw left a GL error behind";
const Image image = ReadPixels(kExtent, kExtent);
ASSERT_FALSE(image.Empty());
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
const int red = static_cast<int>(centre.r);
const bool blended = red > 100 && red < 160;
const bool declined = red > 245;
EXPECT_TRUE(blended || declined)
<< "got " << centre << ", which is neither the dual-source blend (~128) nor the declined "
<< "straight-through source (255) - the SRC1 factors were mistranslated";
Gl().EndFrame();
}
// The same factors with blending DISABLED. Nothing may blend, and on the Vulkan side nothing
// may reach VkPipelineColorBlendAttachmentState carrying a VK_BLEND_FACTOR_SRC1_* on a device
// without dualSrcBlend - the VUIDs bind to the struct, not to blendEnable. The picture is the
// source either way, so this case is really "no crash, no error, no surprise".
TEST_F(DualSourceBlendScenario, DualSourceFactorsWithBlendingDisabledJustWriteTheSource) {
if (!Ready()) GTEST_SKIP();
if (const std::string reason = WhyTheProgramCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
glDisable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ZERO);
Draw(/*src0=*/0.0f, /*src1=*/0.0f);
glBlendFunc(GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA);
Draw(/*src0=*/1.0f, /*src1=*/0.25f);
glFinish();
EXPECT_EQ(FirstGLError(), 0u) << "a draw with SRC1 factors and blending off left a GL error behind";
const Image image = ReadPixels(kExtent, kExtent);
ASSERT_FALSE(image.Empty());
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
EXPECT_GT(static_cast<int>(centre.r), 245)
<< "got " << centre << ": blending is disabled, so the source has to be written straight through";
Gl().EndFrame();
}
} // namespace MGITest
@@ -0,0 +1,178 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SampleMaskScopeScenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - GL_SAMPLE_MASK IS A MULTISAMPLE FRAGMENT OPERATION, SO IT DOES NOTHING AT ONE SAMPLE.
//
// GL 4.6 core 17.3.3 groups alpha-to-coverage, sample coverage and the sample mask together and
// says they make no change "if MULTISAMPLE is disabled, or if the value of SAMPLE_BUFFERS is not
// one". SAMPLE_BUFFERS is 0 for a single-sample framebuffer, so on one the mask is inert whatever
// glSampleMaski last wrote.
//
// Vulkan has no such rule. VkPipelineMultisampleStateCreateInfo::pSampleMask is ANDed with
// rasterization coverage at every rasterizationSamples, and at one sample that coverage is bit 0
// alone - so a mask with bit 0 clear discards every fragment of every primitive. Plumbing
// glSampleMaski straight into pSampleMask therefore turned an ordinary and legal GL sequence into
// a fully black draw:
//
// glEnable(GL_SAMPLE_MASK); glSampleMaski(0, 0x2); // while an MSAA target is bound
// ... render ...
// glBindFramebuffer(GL_FRAMEBUFFER, 0); draw a fullscreen quad to present
//
// Neither piece of state is per-framebuffer, so nothing resets it when the target changes, and
// dEQP/GL-CTS multisample cases leave exactly these masks behind. That is the MSAA-then-present
// shape every application uses.
//
// The cases below are single-sample by construction (the scenario harness's colour FBO), so each
// one asserts that the mask changed nothing.
#include <string>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kFboSize = 32;
constexpr const char* kQuadVertexSource = R"(#version 430 core
void main() {
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
gl_Position = vec4(corner, 0.0, 1.0);
}
)";
constexpr const char* kGreenFragmentSource = R"(#version 430 core
out vec4 o_color;
void main() {
o_color = vec4(0.0, 1.0, 0.0, 1.0);
}
)";
class SampleMaskScopeScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_target = MakeColorFbo(kFboSize, kFboSize);
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
glGenVertexArrays(1, &m_vao);
std::string error;
m_program = CompileProgram(kQuadVertexSource, kGreenFragmentSource, &error);
ASSERT_NE(m_program, 0u) << error;
}
void TearDown() override {
if (!Ready()) return;
// Process-wide GL state: leaving it set would hand the next scenario in this
// process the very bug under test.
glDisable(GL_SAMPLE_MASK);
glSampleMaski(0, 0xFFFFFFFFu);
glBindVertexArray(0);
glUseProgram(0);
if (m_program != 0) glDeleteProgram(m_program);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
DestroyColorFbo(m_target);
ScenarioTest::TearDown();
}
void ExpectQuadStillPaints(const char* what) {
BindFbo(m_target);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glBindVertexArray(m_vao);
glUseProgram(m_program);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
EXPECT_EQ(FirstGLError(), 0u) << what << ": the draw raised a GL error";
const Image image = ReadPixels(kFboSize, kFboSize);
ASSERT_FALSE(image.Empty()) << what << ": the readback came back empty";
EXPECT_TRUE(RegionIsMostly(image, 0, kFboSize - 1, 0, kFboSize - 1, "green", 0.0, what))
<< what << ": an all-black target means the sample mask discarded every fragment, "
<< "which GL says it cannot do on a single-sample framebuffer";
}
ColorFbo m_target{};
GLuint m_vao = 0;
unsigned int m_program = 0;
};
} // namespace
// The exact reported shape: bit 0 clear, so the single sample of a single-sample target is
// masked off if the mask is applied at all.
TEST_F(SampleMaskScopeScenario, AMaskWithBitZeroClearDoesNotDiscardASingleSampleDraw) {
if (!Ready() || IsSkipped()) return;
glEnable(GL_SAMPLE_MASK);
glSampleMaski(0, 0x2);
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
ExpectQuadStillPaints("GL_SAMPLE_MASK enabled with mask 0x2");
}
// Zero is the strongest form of the same thing, and the mask value the CTS's mask_zero cases
// set.
TEST_F(SampleMaskScopeScenario, AZeroMaskDoesNotDiscardASingleSampleDraw) {
if (!Ready() || IsSkipped()) return;
glEnable(GL_SAMPLE_MASK);
glSampleMaski(0, 0x0);
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
ExpectQuadStillPaints("GL_SAMPLE_MASK enabled with mask 0");
}
// Control: the same mask word with the capability disabled has never had any effect, so this
// one passed before the fix too. It is here so a regression that ignores the enable bit
// instead of the sample count is still caught.
TEST_F(SampleMaskScopeScenario, ADisabledSampleMaskDoesNotDiscardASingleSampleDraw) {
if (!Ready() || IsSkipped()) return;
glDisable(GL_SAMPLE_MASK);
glSampleMaski(0, 0x0);
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
ExpectQuadStillPaints("GL_SAMPLE_MASK disabled with mask 0");
}
// The mask is state, not a draw parameter, so a second draw after the first must not inherit
// a pipeline built while the memo word and the payload disagreed. Two draws either side of a
// mask change, both to the same single-sample target, both required to paint.
TEST_F(SampleMaskScopeScenario, ChangingTheMaskBetweenSingleSampleDrawsKeepsBothPainting) {
if (!Ready() || IsSkipped()) return;
glEnable(GL_SAMPLE_MASK);
glSampleMaski(0, 0xFFFFFFFFu);
ExpectQuadStillPaints("first draw, full mask");
glSampleMaski(0, 0x2);
ASSERT_EQ(FirstGLError(), 0u) << "changing the sample mask raised a GL error";
ExpectQuadStillPaints("second draw, mask 0x2");
}
// GL_MAX_SAMPLE_MASK_WORDS must be 1 on both backends: MobileGL stores one word and
// SampleMaski_State raises GL_INVALID_VALUE for any maskNumber above 0, so advertising more
// makes dEQP's per-case gluStateReset - which issues glSampleMaski up to the advertised count
// - fail every case. DirectGLES clamped; DirectVulkan forwarded the raw device limit.
TEST_F(SampleMaskScopeScenario, TheAdvertisedSampleMaskWordCountMatchesWhatSampleMaskiAccepts) {
if (!Ready() || IsSkipped()) return;
GLint words = 0;
glGetIntegerv(GL_MAX_SAMPLE_MASK_WORDS, &words);
ASSERT_EQ(FirstGLError(), 0u) << "querying GL_MAX_SAMPLE_MASK_WORDS raised a GL error";
EXPECT_EQ(words, 1) << "every word below the advertised count must be writable, and only word 0 is";
for (GLint word = 0; word < words; ++word) {
glSampleMaski(static_cast<GLuint>(word), 0xFFFFFFFFu);
EXPECT_EQ(FirstGLError(), 0u) << "glSampleMaski(" << word << ", ...) was refused although "
<< "GL_MAX_SAMPLE_MASK_WORDS advertises " << words << " words";
}
}
} // namespace MGITest
@@ -0,0 +1,231 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SampledSetStalenessScenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - A TEXTURE THAT BECOMES COMPLETE WITHOUT A REBIND MUST RE-ENTER THE SAMPLED SET.
//
// DirectVulkan does not bind a texture GL calls incomplete: it substitutes a fallback so the
// sampler reads (0,0,0,1) instead of losing the draw. That decision is made twice per draw - once
// by CollectSampledTextures, which builds the list SetupDraw syncs, materialises pending clears
// for and transitions to a sampled layout BEFORE the render pass opens, and once by the descriptor
// resolve inside the pass. Both ask SamplesAsIncompleteTexture.
//
// The per-draw memo that lets the first of those be skipped was keyed only on the program, the
// transform flags and the texture BIND generation. Completeness is not a function of any of them:
// it moves on a filter change (glTexParameteri / glSamplerParameteri), on a level-range change,
// and on an upload that fills the mip chain - none of which bind anything. So a texture that went
// incomplete -> complete under a fixed binding kept being answered out of the memo as "not in the
// set", and the work SetupDraw does for the set never happened for it:
//
// * its queued clear was never materialised, so the draw sampled pre-clear content - wrong
// pixels, no validation layer needed, which is what the case below detects; and
// * its layout transition moved into the descriptor resolve, which records
// vkCmdPipelineBarrier inside an already-open render pass whose subpass declares no
// self-dependency - the exact hazard CollectSampledTextures exists to prevent.
//
// The fix adds the sampling-resolution generation to that memo key, which is the counter the
// codebase already maintains for "what a unit resolves to changed without a bind" and which both
// TextureObjectBase::BumpShapeVersion and SamplerObject::BumpVersion move.
#include <cstddef>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kFboSize = 32;
constexpr int kTexSize = 8;
constexpr const char* kQuadVertexSource = R"(#version 430 core
void main() {
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
gl_Position = vec4(corner, 0.0, 1.0);
}
)";
// texelFetch, not texture(): the point is WHICH image is sampled, and a fetch cannot be
// explained away by filtering.
constexpr const char* kSampleFragmentSource = R"(#version 430 core
uniform sampler2D u_tex;
out vec4 o_color;
void main() {
o_color = texelFetch(u_tex, ivec2(0, 0), 0);
}
)";
class SampledSetStalenessScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_target = MakeColorFbo(kFboSize, kFboSize);
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
glGenVertexArrays(1, &m_vao);
std::string error;
m_program = CompileProgram(kQuadVertexSource, kSampleFragmentSource, &error);
ASSERT_NE(m_program, 0u) << error;
// The sampled texture: ONE level, and no glTexParameteri at all, so MIN_FILTER
// keeps its initial GL_NEAREST_MIPMAP_LINEAR and GL calls it mipmap-incomplete.
glGenTextures(1, &m_texture);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_texture);
std::vector<unsigned char> green(static_cast<std::size_t>(kTexSize * kTexSize * 4), 0);
for (std::size_t i = 0; i < green.size(); i += 4) {
green[i + 1] = 255;
green[i + 3] = 255;
}
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
green.data());
ASSERT_EQ(FirstGLError(), 0u) << "defining the sampled texture raised a GL error";
}
void TearDown() override {
if (!Ready()) return;
glBindVertexArray(0);
glUseProgram(0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, 0);
if (m_texture != 0) glDeleteTextures(1, &m_texture);
if (m_program != 0) glDeleteProgram(m_program);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
DestroyColorFbo(m_target);
ScenarioTest::TearDown();
}
// One draw of the fullscreen quad sampling texel (0,0) of whatever unit 0 holds, and
// NO readback. That matters: a readback submits and waits, which ends the command
// buffer and resets the per-draw memos with it - so a case that read back between its
// two draws would never leave a stale entry to catch. The two draws here have to land
// in one recording.
void DrawOnly() {
BindFbo(m_target);
glBindVertexArray(m_vao);
glUseProgram(m_program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_texture);
const GLint location = glGetUniformLocation(m_program, "u_tex");
if (location != -1) glUniform1i(location, 0);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
EXPECT_EQ(FirstGLError(), 0u) << "the sampling draw raised a GL error";
}
ColorFbo m_target{};
GLuint m_vao = 0;
GLuint m_texture = 0;
unsigned int m_program = 0;
};
} // namespace
// The full sequence, ordered so the ONLY state change between the two draws is the filter.
TEST_F(SampledSetStalenessScenario, AQueuedClearIsMaterialisedWhenAFilterChangeCompletesTheTexture) {
if (!Ready() || IsSkipped()) return;
// 1. Queue a clear on the texture through an FBO and take it straight back out, with no
// draw in between - the "attach -> clear -> detach" shape that leaves the clear
// pending for whoever samples the texture next.
GLuint clearFbo = 0;
glGenFramebuffers(1, &clearFbo);
glBindFramebuffer(GL_FRAMEBUFFER, clearFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
const GLfloat red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
glClearBufferfv(GL_COLOR, 0, red);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &clearFbo);
ASSERT_EQ(FirstGLError(), 0u) << "queueing the clear raised a GL error";
BindFbo(m_target);
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
// 2. Draw while the texture is still incomplete. The backend substitutes its fallback,
// and the per-draw memo records the resulting sampled set.
DrawOnly();
// 3. Make it complete. No bind, no upload, no program change - one filter write, which is
// exactly the state the old memo key could not see.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
ASSERT_EQ(FirstGLError(), 0u) << "changing the filter raised a GL error";
// 4. Draw again, into the same recording, and only now read back. The texture is in the
// sampled set now, so its queued clear has to be materialised before the pass opens and
// the fetch has to see RED. Reading the green the texture was uploaded with means the
// clear was never materialised, i.e. the texture never entered the set - the stale-memo
// bug. Black means the fallback was still being handed out.
DrawOnly();
const Image afterFlip = ReadPixels(kFboSize, kFboSize);
ASSERT_FALSE(afterFlip.Empty()) << "the readback came back empty";
EXPECT_TRUE(RegionIsMostly(afterFlip, 0, kFboSize - 1, 0, kFboSize - 1, "red", 0.0,
"the draw after the completeness flip"))
<< "green means the queued clear was never materialised, so the texture never re-entered "
"the sampled set after the filter change; blue means the draw did not happen at all";
}
// The same flip driven from a SAMPLER OBJECT rather than the texture's own parameters. It is
// the other half of what feeds the completeness predicate, it moves the same generation, and
// it likewise binds nothing.
TEST_F(SampledSetStalenessScenario, AQueuedClearIsMaterialisedWhenASamplerObjectCompletesTheTexture) {
if (!Ready() || IsSkipped()) return;
GLuint sampler = 0;
glGenSamplers(1, &sampler);
// Bound BEFORE the first draw, still carrying the mipmapping default, so binding it is
// not what changes between the two draws.
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
glBindSampler(0, sampler);
ASSERT_EQ(FirstGLError(), 0u) << "binding the sampler object raised a GL error";
GLuint clearFbo = 0;
glGenFramebuffers(1, &clearFbo);
glBindFramebuffer(GL_FRAMEBUFFER, clearFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
const GLfloat red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
glClearBufferfv(GL_COLOR, 0, red);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &clearFbo);
ASSERT_EQ(FirstGLError(), 0u) << "queueing the clear raised a GL error";
BindFbo(m_target);
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
DrawOnly();
// One parameter write on an ALREADY-BOUND sampler object.
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
ASSERT_EQ(FirstGLError(), 0u) << "changing the sampler filter raised a GL error";
DrawOnly();
const Image afterFlip = ReadPixels(kFboSize, kFboSize);
ASSERT_FALSE(afterFlip.Empty()) << "the readback came back empty";
EXPECT_TRUE(RegionIsMostly(afterFlip, 0, kFboSize - 1, 0, kFboSize - 1, "red", 0.0,
"the draw after the sampler-object flip"))
<< "green means the queued clear was never materialised after the sampler parameter change";
glBindSampler(0, 0);
glDeleteSamplers(1, &sampler);
}
} // namespace MGITest
@@ -0,0 +1,896 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TessellationXfbCaptureScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - WHAT A TESSELLATION EVALUATION STAGE OWES A TRANSFORM FEEDBACK CAPTURE.
//
// XfbRepeatedCaptureScenario already pins that a capture from a GL_PATCHES draw records
// AT ALL. Everything below is the part of the same pipeline it does not reach, and every
// case here is the reduced form of a conformance body that fails on a device:
//
// * CAPTURING THE BUILT-INS BY NAME. glTransformFeedbackVaryings("gl_Position") /
// ("gl_PointSize") on a program whose last vertex-processing stage is the evaluation
// shader. Nothing in the tree captured a built-in from a tessellation stage, and the
// two backends reach it by completely different routes - DirectGLES has to name a
// real ESSL output on the driver's own glTransformFeedbackVaryings, DirectVulkan has
// to decorate a SPIR-V built-in that lives inside gl_PerVertex.
//
// * THE PER-VERTEX PAYLOAD THE CONTROL STAGE HANDS OVER. gl_PointSize and a
// user-declared per-vertex interface block, both read back out of gl_in[] by the
// evaluation stage and only then captured. This is the shape of
// KHR-GL4x.tessellation_shader.tessellation_control_to_tessellation_evaluation.
// gl_MaxPatchVertices_Position_PointSize, which is 216 of the ~240 conformance bodies
// the family still fails: gl_Position arrives, and everything travelling beside it in
// the same patch does not.
//
// The assertions are on the captured BYTES against a CPU-computed reference, never on the
// absence of a GL error: every failure this guards against is silent.
#include <cmath>
#include <cstring>
#include <string>
#include <utility>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// Nothing a capture can legitimately produce, so a component that still reads it
// names the failure instead of looking like an ordinary numeric mismatch.
constexpr float kPoison = -987654.0f;
const char* const kFragmentSource = R"(#version 420 core
out vec4 fragColor;
void main()
{
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
)";
class TessellationXfbCaptureScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
DrainErrors();
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (const GLuint program : m_programs) {
glDeleteProgram(program);
}
m_programs.clear();
glBindVertexArray(0);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
m_vao = 0;
ScenarioTest::TearDown();
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
static bool BackendHostsTessellation() {
GLint maxTessGenLevel = 0;
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
DrainErrors();
return maxTessGenLevel >= 1;
}
static GLint MaxPatchVertices() {
GLint value = 0;
glGetIntegerv(GL_MAX_PATCH_VERTICES, &value);
DrainErrors();
return value;
}
static std::string InfoLog(GLuint object, bool isShader) {
GLint length = 0;
if (isShader) {
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
} else {
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
}
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
if (isShader) {
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
} else {
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
}
return buffer.data();
}
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, std::string>>& stages,
const std::vector<const char*>& varyings) {
m_buildLog.clear();
std::vector<GLuint> shaders;
bool ok = true;
for (const auto& [stage, source] : stages) {
const GLuint shader = glCreateShader(stage);
const char* text = source.c_str();
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
shaders.push_back(shader);
if (compiled == GL_FALSE) {
m_buildLog = InfoLog(shader, true) + "\n--- source ---\n" + source;
ok = false;
break;
}
}
GLuint program = 0;
if (ok) {
program = glCreateProgram();
for (const GLuint shader : shaders) {
glAttachShader(program, shader);
}
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
GL_INTERLEAVED_ATTRIBS);
glLinkProgram(program);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
m_buildLog = InfoLog(program, false);
glDeleteProgram(program);
program = 0;
}
}
for (const GLuint shader : shaders) {
glDeleteShader(shader);
}
if (program != 0) m_programs.push_back(program);
return program;
}
// One capture span over a single patch. Returns the capture buffer read back as
// floats; `capturedFloats` is the whole buffer, poison-filled beforehand.
std::vector<float> RunPatchCaptureSpan(GLuint program, GLenum captureMode, std::size_t capturedFloats) {
const std::vector<float> poison(capturedFloats, kPoison);
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedFloats * sizeof(float)), poison.data(),
GL_STATIC_COPY);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
glBindVertexArray(m_vao);
glUseProgram(program);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(captureMode);
glDrawArrays(GL_PATCHES, 0, 1);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
std::vector<float> readback(capturedFloats, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(capturedFloats * sizeof(float)), readback.data());
glUseProgram(0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glDeleteBuffers(1, &xfbBuffer);
return readback;
}
static ::testing::AssertionResult ComponentIs(const std::vector<float>& data, std::size_t index,
float expected, float epsilon = 1e-4f) {
if (index >= data.size()) {
return ::testing::AssertionFailure() << "component " << index << " is past the capture buffer";
}
const float actual = data[index];
if (actual == kPoison) {
return ::testing::AssertionFailure()
<< "component " << index << " still holds the poison value - the capture never reached "
<< "these bytes (expected " << expected << ")";
}
if (std::isnan(actual) || std::abs(actual - expected) > epsilon) {
return ::testing::AssertionFailure()
<< "component " << index << " is " << actual << ", expected " << expected;
}
return ::testing::AssertionSuccess();
}
// Defined below the shader builders it uses. `withPointSize` is the conformance
// body's own should_pass_pointsize_data axis.
void RunPerVertexPayloadCase(bool withPointSize);
// Why the gl_PointSize cases cannot be run here, or empty when they can.
//
// gl_PointSize from a tessellation stage is a real DRIVER capability on both
// targets - GL_EXT/OES_tessellation_point_size on an ES driver, the
// shaderTessellationAndGeometryPointSize feature on a Vulkan device - and desktop GL
// has no query that reports either, so this probes for it by running a program.
//
// The probe is deliberately NOT a gl_PointSize capture: it captures an ordinary user
// varying out of a tessellation evaluation stage that ALSO writes gl_PointSize, and
// compares that against the identical program without the write. A backend that
// cannot express the built-in loses the whole stage (DirectGLES fails to compile it
// and binds program 0; DirectVulkan cannot build the pipeline), so the plain varying
// comes back untouched too - which is a capability answer, not a capture answer. If
// BOTH come back untouched the probe itself is meaningless and it returns empty, so
// the cases run and FAIL rather than skipping on an unrelated breakage.
//
// Returns the reason as a string instead of skipping directly: GTEST_SKIP expands to
// a `return`, so a void helper would leave only the helper and let the case run its
// assertions anyway and report Failed instead of Skipped.
std::string WhyPointSizeCasesCannotRun();
// The geometry stage's own answer, and it has to BE its own answer: the two ESSL
// extensions are independent (Loader models them as two PointSizeTier fields fed by
// four distinct strings, and neither implies the other), so a driver with
// tessellation point size and no geometry point size passes the probe above and
// still cannot run the case below. Same two-program shape, one stage over.
//
// It also replaces a guard that could never fire: GL_MAX_GEOMETRY_OUTPUT_VERTICES is
// a hardcoded frontend constant (256) with no capability behind it, so "does this
// stack have a geometry stage at all" can only be answered by trying to build one -
// which is what this does, exactly as IoBlockNameCollisionScenario does for the same
// reason.
std::string WhyGeometryPointSizeCaseCannotRun();
std::vector<GLuint> m_programs;
std::string m_buildLog;
GLuint m_vao = 0;
};
// ---------------------------------------------------------------------------------
// Built-ins captured BY NAME from the evaluation stage.
// ---------------------------------------------------------------------------------
const char* const kMinimalVertexSource = R"(#version 420 core
void main()
{
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
const char* const kMinimalTessControlSource = R"(#version 420 core
layout(vertices = 1) out;
void main()
{
gl_out[gl_InvocationID].gl_Position = gl_in[0].gl_Position;
gl_TessLevelOuter[0] = 1.0;
gl_TessLevelOuter[1] = 1.0;
gl_TessLevelOuter[2] = 1.0;
gl_TessLevelInner[0] = 1.0;
}
)";
// Values no stale buffer would hold by accident. The two sources differ ONLY by
// gl_PointSize, so the pair isolates it: on a backend that lowers to ESSL the
// built-in is not even declared in a tessellation stage without
// GL_EXT_tessellation_point_size, and the whole shader then fails to compile.
const char* const kPositionTessEvalSource = R"(#version 420 core
layout(triangles, equal_spacing, cw, point_mode) in;
void main()
{
gl_Position = vec4(11.0, 12.0, 13.0, 14.0);
}
)";
const char* const kPositionAndPointSizeTessEvalSource = R"(#version 420 core
layout(triangles, equal_spacing, cw, point_mode) in;
void main()
{
gl_Position = vec4(11.0, 12.0, 13.0, 14.0);
gl_PointSize = 5.0;
}
)";
// The two probe programs. They differ by one statement; both capture `probe_value`,
// which has nothing to do with point size.
const char* const kPointSizeProbeTessEvalSource = R"(#version 420 core
layout(triangles, equal_spacing, cw, point_mode) in;
out float probe_value;
void main()
{
probe_value = 42.0;
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 3.0;
}
)";
const char* const kPointSizeFreeProbeTessEvalSource = R"(#version 420 core
layout(triangles, equal_spacing, cw, point_mode) in;
out float probe_value;
void main()
{
probe_value = 42.0;
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
std::string TessellationXfbCaptureScenario::WhyPointSizeCasesCannotRun() {
glPatchParameteri(GL_PATCH_VERTICES, 1);
DrainErrors();
const auto probeCaptures = [&](const char* tessEvalSource) {
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
{GL_TESS_EVALUATION_SHADER, tessEvalSource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
{"probe_value"});
if (program == 0) return false;
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 3);
DrainErrors();
return captured[0] == 42.0f;
};
const bool withPointSize = probeCaptures(kPointSizeProbeTessEvalSource);
if (withPointSize) return {};
if (!probeCaptures(kPointSizeFreeProbeTessEvalSource)) {
// The control failed too, so nothing here is about point size.
return {};
}
return "this backend cannot express gl_PointSize in a tessellation stage at all - the same "
"program captures an ordinary varying with the gl_PointSize write removed and captures "
"nothing with it present (an ES driver without GL_EXT/OES_tessellation_point_size, or a "
"Vulkan device without shaderTessellationAndGeometryPointSize)";
}
TEST_F(TessellationXfbCaptureScenario, CapturesGlPositionByNameFromTheEvaluationStage) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsTessellation()) {
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
<< ")";
}
glPatchParameteri(GL_PATCH_VERTICES, 1);
DrainErrors();
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
{GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
{"gl_Position"});
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
// point_mode with every level at 1 emits three points, all carrying the same
// constant; only the first record has to be right for the mechanism to be proven.
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 4 * 3);
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
TEST_F(TessellationXfbCaptureScenario, CapturesGlPositionAndGlPointSizeByNameFromTheEvaluationStage) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsTessellation()) {
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
<< ")";
}
if (const std::string reason = WhyPointSizeCasesCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
glPatchParameteri(GL_PATCH_VERTICES, 1);
DrainErrors();
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
{GL_TESS_EVALUATION_SHADER, kPositionAndPointSizeTessEvalSource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
{"gl_Position", "gl_PointSize"});
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 5 * 3);
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
EXPECT_TRUE(ComponentIs(captured, 4, 5.0f));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// ---------------------------------------------------------------------------------
// The per-vertex payload the control stage hands to the evaluation stage.
// ---------------------------------------------------------------------------------
// The conformance body's own shapes, reduced to one patch and parameterised by the
// output patch size so the caller can run the real GL_MAX_PATCH_VERTICES. The
// `withPointSize` axis is the conformance body's own `should_pass_pointsize_data`,
// which it varies together with point_mode - and which decides whether the whole
// program even involves the per-vertex built-in that ESSL gates behind an extension.
std::string PayloadVertexSource(bool withPointSize) {
return R"(#version 420 core
out gl_PerVertex {
vec4 gl_Position;
)" + std::string(withPointSize ? " float gl_PointSize;\n" : "") +
R"(};
void main()
{
}
)";
}
std::string PayloadTessControlSource(int outputVertices, bool withPointSize) {
const std::string perVertexTail = withPointSize ? " float gl_PointSize;\n" : "";
return R"(#version 420 core
layout(vertices = )" + std::to_string(outputVertices) +
R"() out;
in gl_PerVertex {
vec4 gl_Position;
)" + perVertexTail +
R"(} gl_in[gl_MaxPatchVertices];
out gl_PerVertex {
vec4 gl_Position;
)" + perVertexTail +
R"(} gl_out[];
out OUT_TC
{
vec2 value1;
ivec4 value2;
} result[];
void main()
{
)" + std::string(withPointSize
? " gl_out[gl_InvocationID].gl_PointSize = 1.0 / float(gl_InvocationID + 1);\n"
: "") +
R"( gl_out[gl_InvocationID].gl_Position = vec4(float(gl_InvocationID * 4 + 0), float(gl_InvocationID * 4 + 1),
float(gl_InvocationID * 4 + 2), float(gl_InvocationID * 4 + 3));
result[gl_InvocationID].value1 = vec2(1.0 / float(gl_InvocationID + 1), 1.0 / float(gl_InvocationID + 2));
result[gl_InvocationID].value2 = ivec4(gl_InvocationID + 1, gl_InvocationID + 2,
gl_InvocationID + 3, gl_InvocationID + 4);
gl_TessLevelInner[0] = 1.0;
gl_TessLevelInner[1] = 1.0;
gl_TessLevelOuter[0] = 1.0;
gl_TessLevelOuter[1] = 1.0;
gl_TessLevelOuter[2] = 1.0;
gl_TessLevelOuter[3] = 1.0;
}
)";
}
// Deliberately NEVER writes gl_Position, exactly as the conformance shader does not:
// the redeclared block is there so the evaluation stage can READ gl_in[], and an
// output nothing stores is what UnwrittenPositionOutputScenario pins separately.
std::string PayloadTessEvalSource(int inputVertices, bool withPointSize) {
const std::string perVertexTail = withPointSize ? " float gl_PointSize;\n" : "";
return R"(#version 420 core
layout(isolines, equal_spacing, ccw, point_mode) in;
in gl_PerVertex {
vec4 gl_Position;
)" + perVertexTail +
R"(} gl_in[gl_MaxPatchVertices];
out gl_PerVertex {
vec4 gl_Position;
)" + perVertexTail +
R"(};
in OUT_TC
{
vec2 value1;
ivec4 value2;
} tc_data[];
)" + std::string(withPointSize ? "out float te_pointsize;\n" : "") +
R"(out vec4 te_position;
out vec2 te_value1;
out flat ivec4 te_value2;
void main()
{
)" + std::string(withPointSize ? " te_pointsize = 0.0;\n" : "") +
R"( te_position = vec4 (0.0);
te_value1 = vec2 (0.0);
te_value2 = ivec4(0);
for (int n = 0; n < )" + std::to_string(inputVertices) +
R"(; ++n)
{
)" + std::string(withPointSize ? " te_pointsize += gl_in [n].gl_PointSize;\n" : "") +
R"( te_position += gl_in [n].gl_Position;
te_value1 += tc_data[n].value1;
te_value2 += tc_data[n].value2;
}
}
)";
}
// The reduced conformance body. `withPointSize` selects between its two halves;
// everything else - one input vertex, an output patch of GL_MAX_PATCH_VERTICES, a
// user per-vertex block travelling beside gl_PerVertex, the capture taken off the
// evaluation stage - is the same on both.
void TessellationXfbCaptureScenario::RunPerVertexPayloadCase(bool withPointSize) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsTessellation()) {
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
<< ")";
}
if (withPointSize) {
if (const std::string reason = WhyPointSizeCasesCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
}
const GLint patchVertices = MaxPatchVertices();
ASSERT_GE(patchVertices, 32) << "GL_MAX_PATCH_VERTICES is below the guaranteed minimum";
// One input vertex per patch, an output patch of GL_MAX_PATCH_VERTICES vertices:
// the control stage runs that many invocations and every one of them contributes.
glPatchParameteri(GL_PATCH_VERTICES, 1);
DrainErrors();
std::vector<const char*> varyings = {"te_position", "te_value1", "te_value2"};
if (withPointSize) varyings.push_back("te_pointsize");
const GLuint program =
BuildCaptureProgram({{GL_VERTEX_SHADER, PayloadVertexSource(withPointSize)},
{GL_TESS_CONTROL_SHADER, PayloadTessControlSource(patchVertices, withPointSize)},
{GL_TESS_EVALUATION_SHADER, PayloadTessEvalSource(patchVertices, withPointSize)},
{GL_FRAGMENT_SHADER, kFragmentSource}},
varyings);
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
float referencePointSize = 0.0f;
float referencePosition[4] = {0.0f, 0.0f, 0.0f, 0.0f};
float referenceValue1[2] = {0.0f, 0.0f};
int referenceValue2[4] = {0, 0, 0, 0};
for (int n = 0; n < patchVertices; ++n) {
referencePointSize += 1.0f / static_cast<float>(n + 1);
for (int c = 0; c < 4; ++c) {
referencePosition[c] += static_cast<float>(n * 4 + c);
referenceValue2[c] += n + 1 + c;
}
referenceValue1[0] += 1.0f / static_cast<float>(n + 1);
referenceValue1[1] += 1.0f / static_cast<float>(n + 2);
}
// isolines with every level at 1 emits two points; the record stride is
// vec4 + vec2 + ivec4 [+ float] components.
const std::size_t stride = withPointSize ? 11 : 10;
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, stride * 4);
for (int c = 0; c < 4; ++c) {
EXPECT_TRUE(ComponentIs(captured, static_cast<std::size_t>(c), referencePosition[c], 1e-2f))
<< "te_position." << c << " (gl_in[].gl_Position)";
}
for (int c = 0; c < 2; ++c) {
EXPECT_TRUE(ComponentIs(captured, static_cast<std::size_t>(4 + c), referenceValue1[c], 1e-3f))
<< "te_value1." << c << " (the user per-vertex block the control stage wrote)";
}
for (int c = 0; c < 4; ++c) {
const std::size_t index = static_cast<std::size_t>(6 + c);
ASSERT_LT(index, captured.size());
int actual = 0;
std::memcpy(&actual, &captured[index], sizeof(actual));
EXPECT_EQ(actual, referenceValue2[c])
<< "te_value2." << c << " (the user per-vertex block's integer member)";
}
if (withPointSize) {
EXPECT_TRUE(ComponentIs(captured, 10, referencePointSize, 1e-3f))
<< "te_pointsize (gl_in[].gl_PointSize)";
}
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
TEST_F(TessellationXfbCaptureScenario, TheEvaluationStageSeesTheUserPerVertexBlockOfItsPatch) {
RunPerVertexPayloadCase(false);
}
// ---------------------------------------------------------------------------------
// The same built-in, one stage over.
// ---------------------------------------------------------------------------------
// ESSL gates gl_PointSize behind a per-stage extension in BOTH non-vertex
// vertex-processing stages - EXT/OES_tessellation_point_size for the two tessellation
// stages, EXT/OES_geometry_point_size for the geometry one - and they are separate
// extensions that do not imply each other, so the geometry arm is a second code path
// rather than the same one. Nothing else in the tree writes gl_PointSize from a geometry
// shader, so without this case the arm ships untested.
const char* const kPointSizeGeometrySource = R"(#version 420 core
layout(points) in;
layout(points, max_vertices = 1) out;
out float gs_value;
void main()
{
gs_value = 7.0;
gl_Position = gl_in[0].gl_Position;
gl_PointSize = 4.0;
EmitVertex();
}
)";
// The control: identical but for the gl_PointSize write, so the pair answers "can this
// stack host a geometry stage that names the built-in" without asking anything about
// capture.
const char* const kPointSizeFreeGeometrySource = R"(#version 420 core
layout(points) in;
layout(points, max_vertices = 1) out;
out float gs_value;
void main()
{
gs_value = 7.0;
gl_Position = gl_in[0].gl_Position;
EmitVertex();
}
)";
std::string TessellationXfbCaptureScenario::WhyGeometryPointSizeCaseCannotRun() {
const auto probeCaptures = [&](const char* geometrySource) {
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
{GL_GEOMETRY_SHADER, geometrySource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
{"gs_value"});
if (program == 0) return false;
const std::vector<float> poison(1, kPoison);
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(sizeof(float)), poison.data(),
GL_STATIC_DRAW);
glBindVertexArray(m_vao);
glUseProgram(program);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, 1);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
float captured = kPoison;
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast<GLsizeiptr>(sizeof(float)),
&captured);
glUseProgram(0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glDeleteBuffers(1, &xfbBuffer);
DrainErrors();
return captured == 7.0f;
};
if (probeCaptures(kPointSizeGeometrySource)) return {};
if (!probeCaptures(kPointSizeFreeGeometrySource)) {
// The control failed too, so this stack cannot run a capturing geometry stage at
// all - which is not what this case is about, and is the question the dead
// GL_MAX_GEOMETRY_OUTPUT_VERTICES guard was trying to ask. Skipping rather than
// failing loses nothing: XfbRepeatedCaptureScenario pins plain geometry capture
// and goes red on its own if that is what actually broke.
return "this backend cannot capture from a geometry stage at all, with or without gl_PointSize";
}
return "this backend cannot express gl_PointSize in a geometry stage - the same program captures an "
"ordinary varying with the gl_PointSize write removed and captures nothing with it present "
"(an ES driver without GL_EXT/OES_geometry_point_size, which is a SEPARATE extension from the "
"tessellation one, or a Vulkan device without shaderTessellationAndGeometryPointSize)";
}
TEST_F(TessellationXfbCaptureScenario, CapturesGlPointSizeByNameFromTheGeometryStage) {
if (!Ready()) GTEST_SKIP();
if (const std::string reason = WhyGeometryPointSizeCaseCannotRun(); !reason.empty()) {
GTEST_SKIP() << reason << " (" << Gl().BackendName() << ", " << Gl().RendererString() << ")";
}
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
{GL_GEOMETRY_SHADER, kPointSizeGeometrySource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
{"gs_value", "gl_PointSize"});
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
const std::vector<float> poison(2, kPoison);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(poison.size() * sizeof(float)),
poison.data(), GL_STATIC_DRAW);
glBindVertexArray(m_vao);
glUseProgram(program);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, 1);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
std::vector<float> captured(2, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(captured.size() * sizeof(float)), captured.data());
EXPECT_TRUE(ComponentIs(captured, 0, 7.0f)) << "gs_value - an ordinary varying, which is lost too when "
"the stage carrying it fails to compile";
EXPECT_TRUE(ComponentIs(captured, 1, 4.0f)) << "gl_PointSize";
EXPECT_EQ(glGetError(), GL_NO_ERROR);
glUseProgram(0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glDeleteBuffers(1, &xfbBuffer);
}
// ---------------------------------------------------------------------------------
// The conformance body's own READBACK, which is not glGetBufferSubData.
// ---------------------------------------------------------------------------------
// Every case above reads the capture back with glGetBufferSubData because that is the
// shortest path to the bytes. The conformance bodies do something else: they respecify
// the buffer through the GENERIC GL_TRANSFORM_FEEDBACK_BUFFER binding with glBufferData
// while it is simultaneously bound to indexed capture point 0, and then read it with
// glMapBufferRange / glUnmapBuffer - twice, once per iteration of the same case, with no
// fresh buffer in between. On a device the tessellation bodies stop at exactly that map
// call, so the sequence itself is worth pinning: none of the map path's error conditions
// may fire, and the mapped bytes must be the captured ones.
TEST_F(TessellationXfbCaptureScenario, MapsTheCaptureBufferAfterEachOfTwoPatchDraws) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsTessellation()) {
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
<< ")";
}
glPatchParameteri(GL_PATCH_VERTICES, 1);
DrainErrors();
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
{GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
{"gl_Position"});
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "binding the capture point";
constexpr std::size_t kFloats = 4 * 3;
constexpr GLsizeiptr kBytes = static_cast<GLsizeiptr>(kFloats * sizeof(float));
for (int iteration = 0; iteration < 2; ++iteration) {
// Respecified through the generic binding, exactly as the conformance body does,
// while the same buffer is still bound to capture point 0.
const std::vector<float> poison(kFloats, kPoison);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kBytes, poison.data(), GL_STATIC_DRAW);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glBufferData, iteration " << iteration;
glBindVertexArray(m_vao);
glUseProgram(program);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(GL_POINTS);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glBeginTransformFeedback, iteration " << iteration;
glDrawArrays(GL_PATCHES, 0, 1);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glDrawArrays, iteration " << iteration;
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glEndTransformFeedback, iteration " << iteration;
const auto* mapped =
static_cast<const float*>(glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, kBytes,
GL_MAP_READ_BIT));
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glMapBufferRange, iteration " << iteration;
ASSERT_NE(mapped, nullptr) << "iteration " << iteration;
const std::vector<float> captured(mapped, mapped + kFloats);
EXPECT_EQ(glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER), GL_TRUE) << "iteration " << iteration;
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "glUnmapBuffer, iteration " << iteration;
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f)) << "iteration " << iteration;
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f)) << "iteration " << iteration;
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f)) << "iteration " << iteration;
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f)) << "iteration " << iteration;
glUseProgram(0);
}
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glDeleteBuffers(1, &xfbBuffer);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// The same patch with gl_PointSize travelling in gl_PerVertex beside gl_Position.
// In ESSL gl_PointSize does not EXIST in a tessellation stage unless
// GL_EXT_tessellation_point_size is requested, so a backend that lowers to ESSL
// without asking for it does not merely lose the value - the stage fails to compile
// and the whole program is replaced by program 0.
TEST_F(TessellationXfbCaptureScenario, TheEvaluationStageSeesGlPointSizeAcrossItsPatch) {
RunPerVertexPayloadCase(true);
}
// ---------------------------------------------------------------------------------
// The same capture through a PROGRAM PIPELINE OBJECT.
// ---------------------------------------------------------------------------------
// The conformance body runs each of its configurations twice: once with a monolithic
// program object and once with a pipeline of four separable programs, the capture
// declared on the separable EVALUATION program. That second shape goes through the
// hidden composite the pipeline object builds for the draw, and it is the only place a
// tessellation capture and the composite meet - so the capture list has to survive being
// taken from a program that is not the one bound.
TEST_F(TessellationXfbCaptureScenario, CapturesFromASeparableEvaluationProgramInAPipelineObject) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsTessellation()) {
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
<< ")";
}
glPatchParameteri(GL_PATCH_VERTICES, 1);
DrainErrors();
// One separable program per stage. Only the evaluation program carries the capture
// list, because it is the one whose outputs are captured.
const auto buildSeparable = [&](GLenum stage, const char* source,
const std::vector<const char*>& varyings) -> GLuint {
const GLuint shader = glCreateShader(stage);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
m_buildLog = InfoLog(shader, true);
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE);
glAttachShader(program, shader);
if (!varyings.empty()) {
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
GL_INTERLEAVED_ATTRIBS);
}
glLinkProgram(program);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
glDeleteShader(shader);
if (linked == GL_FALSE) {
m_buildLog = InfoLog(program, false);
glDeleteProgram(program);
return 0;
}
m_programs.push_back(program);
return program;
};
m_buildLog.clear();
const GLuint vertexProgram = buildSeparable(GL_VERTEX_SHADER, kMinimalVertexSource, {});
ASSERT_NE(vertexProgram, 0u) << "separable vertex program: " << m_buildLog;
const GLuint controlProgram = buildSeparable(GL_TESS_CONTROL_SHADER, kMinimalTessControlSource, {});
ASSERT_NE(controlProgram, 0u) << "separable control program: " << m_buildLog;
const GLuint evalProgram =
buildSeparable(GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource, {"gl_Position"});
ASSERT_NE(evalProgram, 0u) << "separable evaluation program: " << m_buildLog;
const GLuint fragmentProgram = buildSeparable(GL_FRAGMENT_SHADER, kFragmentSource, {});
ASSERT_NE(fragmentProgram, 0u) << "separable fragment program: " << m_buildLog;
GLuint pipeline = 0;
glGenProgramPipelines(1, &pipeline);
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vertexProgram);
glUseProgramStages(pipeline, GL_TESS_CONTROL_SHADER_BIT, controlProgram);
glUseProgramStages(pipeline, GL_TESS_EVALUATION_SHADER_BIT, evalProgram);
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fragmentProgram);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "assembling the pipeline object";
constexpr std::size_t kFloats = 4 * 3;
const std::vector<float> poison(kFloats, kPoison);
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(kFloats * sizeof(float)),
poison.data(), GL_STATIC_DRAW);
glBindVertexArray(m_vao);
glUseProgram(0);
glBindProgramPipeline(pipeline);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(GL_POINTS);
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "glBeginTransformFeedback on a pipeline object";
glDrawArrays(GL_PATCHES, 0, 1);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
std::vector<float> captured(kFloats, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(kFloats * sizeof(float)), captured.data());
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
glBindProgramPipeline(0);
glDeleteProgramPipelines(1, &pipeline);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glDeleteBuffers(1, &xfbBuffer);
}
} // namespace
} // namespace MGITest
@@ -119,6 +119,73 @@ void main() {
imageStore(u_unbound, int(index), uvec4(7u)); imageStore(u_unbound, int(index), uvec4(7u));
g_data[index] = index + 1u; g_data[index] = index + 1u;
} }
)";
// A plain sampler2D on a unit the test leaves alone. Two cases point at it: a unit with
// nothing bound at all, and a unit whose DEFAULT texture (name 0) has been given a base
// level and no mip chain - GL calls the second one incomplete for the initial
// NEAREST_MIPMAP_LINEAR filter, and both must resolve to the fallback rather than to a
// texture the backend then fails to back.
constexpr const char* kSampler2DFragmentSource = R"(#version 430 core
uniform sampler2D u_unbound;
uniform int u_readUnbound;
out vec4 o_color;
void main() {
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
if (u_readUnbound != 0) {
color = texture(u_unbound, vec2(0.0));
}
o_color = color;
}
)";
// The multisample spelling of the same thing. GL_ARB_sample_variables' own conformance
// cases declare a sampler2D and a sampler2DMS side by side and deliberately point the
// unused one at an empty unit, so whichever of the two is unused has to have a
// placeholder - a multisample descriptor demands a multisample view, so the 2D fallback
// cannot stand in for it.
constexpr const char* kSampler2DMSFragmentSource = R"(#version 430 core
uniform sampler2DMS u_unbound;
uniform int u_readUnbound;
out vec4 o_color;
void main() {
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
if (u_readUnbound != 0) {
color = texelFetch(u_unbound, ivec2(0), 0);
}
o_color = color;
}
)";
// The integer spellings of the same thing. These are the ones a plain RGBA8 multisample
// placeholder cannot serve: a multisample image can never carry MUTABLE_FORMAT, so the
// reinterpreting view an integer sampler would need over UNORM texels is unbuildable and
// the descriptor resolve used to fail, losing the draw after the placeholder had already
// been created.
constexpr const char* kUsampler2DMSFragmentSource = R"(#version 430 core
uniform usampler2DMS u_unbound;
uniform int u_readUnbound;
out vec4 o_color;
void main() {
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
if (u_readUnbound != 0) {
color = vec4(texelFetch(u_unbound, ivec2(0), 0));
}
o_color = color;
}
)";
constexpr const char* kIsampler2DMSFragmentSource = R"(#version 430 core
uniform isampler2DMS u_unbound;
uniform int u_readUnbound;
out vec4 o_color;
void main() {
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
if (u_readUnbound != 0) {
color = vec4(texelFetch(u_unbound, ivec2(0), 0));
}
o_color = color;
}
)"; )";
constexpr const char* kImage2DFragmentSource = R"(#version 430 core constexpr const char* kImage2DFragmentSource = R"(#version 430 core
@@ -369,6 +436,65 @@ void main() {
ExpectDrawStillRuns(kImage2DFragmentSource, "image2D"); ExpectDrawStillRuns(kImage2DFragmentSource, "image2D");
} }
// ---- sampler2D / sampler2DMS (VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER) ----------------
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSampler2DDoesNotLoseTheDraw) {
if (!Ready() || IsSkipped()) return;
ExpectDrawStillRuns(kSampler2DFragmentSource, "sampler2D");
}
// The regression this file exists for, in its sharpest form: a sampler pointing at a texture
// unit whose DEFAULT texture object has an image but no mip chain.
//
// DirectVulkan resolved such a binding twice, through two different predicates that
// disagreed. The collect pass (CollectSampledTextures -> ResolveSampledBinding), which
// pre-syncs and transitions every texture the draw will sample, asked only whether the
// default texture was UNDEFINED - texture 0 with an image is not - and kept it. The
// descriptor pass (ResolveSamplerDescriptor) asked the real GL question, whether it
// SAMPLES AS INCOMPLETE for the filter in effect, and swapped it for the fallback. So the
// collect pass synced a texture no descriptor would ever hold, VkTextureManager declined it
// ("mipmap not complete") and returned nullptr, and SetupDraw dereferenced that nullptr -
// a SIGSEGV inside the draw, not a degraded picture.
//
// The GL-CTS reaches this on its own: its between-case state reset gives the default 2D
// texture a base level, so the FIRST case in a process survived and every later one with an
// unbound sampler2D died. That is the whole of the 380-record sample_variables crash family
// on Mali-G1-Ultra. Any application that uploads to texture 0 has the same shape.
TEST_F(UnboundImageDescriptorScenario, ASamplerOnAUnitWhoseDefaultTextureIsIncompleteDoesNotLoseTheDraw) {
if (!Ready() || IsSkipped()) return;
// Unit 0 is where the sampler's default uniform value points. Give the DEFAULT texture
// object bound there a FORMAT and a zero-sized level - which is what a bare
// glTexImage2D(..., 0, 0, ...) with no data does, and what the GL-CTS's between-case
// state reset issues for every texture target. That combination is the whole point:
// * it is DEFINED, so IsUndefinedDefaultTexture (the collect path's old test) is false
// and the texture stays in the sampled set;
// * it is INCOMPLETE, so SamplesAsIncompleteTexture (the descriptor path's test) is
// true and the descriptor holds the fallback instead;
// * and it has no valid mip level, so the sync declines and hands back nullptr.
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(FirstGLError(), 0u) << "defining a zero-sized level 0 on the default texture raised a GL error";
ExpectDrawStillRuns(kSampler2DFragmentSource, "sampler2D on an incomplete default texture");
}
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSampler2DMSDoesNotLoseTheDraw) {
if (!Ready() || IsSkipped()) return;
ExpectDrawStillRuns(kSampler2DMSFragmentSource, "sampler2DMS");
}
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundUnsignedSampler2DMSDoesNotLoseTheDraw) {
if (!Ready() || IsSkipped()) return;
ExpectDrawStillRuns(kUsampler2DMSFragmentSource, "usampler2DMS");
}
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSignedSampler2DMSDoesNotLoseTheDraw) {
if (!Ready() || IsSkipped()) return;
ExpectDrawStillRuns(kIsampler2DMSFragmentSource, "isampler2DMS");
}
TEST_F(UnboundImageDescriptorScenario, AFormatlessWriteonlyImage2DLeftUnboundDoesNotLoseTheDispatch) { TEST_F(UnboundImageDescriptorScenario, AFormatlessWriteonlyImage2DLeftUnboundDoesNotLoseTheDispatch) {
if (!Ready() || IsSkipped()) return; if (!Ready() || IsSkipped()) return;
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) { if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
@@ -0,0 +1,276 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnwrittenPositionOutputScenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - A SHADER REDECLARES gl_PerVertex AND NEVER WRITES gl_Position.
//
// Legal, ordinary GLSL, and until now a process kill on DirectVulkan. The chain, all of it
// inside MobileGL's own SPIR-V plumbing:
//
// 1. glslang emits every DECLARED interface variable, used or not, and lists it on
// OpEntryPoint. So `out gl_PerVertex { vec4 gl_Position; };` with no write still produces
// the OpVariable, the OpMemberDecorate BuiltIn Position, and an interface slot.
// 2. At link, ShaderCompiler::SanitizeAndOptimizeBinary runs AggressiveDCE(remove_outputs =
// false) - which may never delete an Output - and then RemoveUnusedInterfaceVariables,
// which rebuilds the interface list from the variables instructions actually reference.
// The OpVariable and its BuiltIn decoration SURVIVE; the interface slot is DELISTED.
// 3. At pipeline build, ProgramFactory picks the last pre-rasterisation stage and runs two
// passes over it. GlToVulkanPositionFixPass finds the position target through the
// surviving ANNOTATION and injects a load-modify-STORE through it. When gl_Position is in
// the transform-feedback capture list, XfbCaptureDecoratePass::MirrorPositionForCapture
// also injects an access chain and a LOAD through it.
// 4. Either injection is a static use of a variable that is no longer on the entry point's
// interface, which is invalid SPIR-V ("Interface variable id <N> is used by entry point
// 'main' id <M>, but is not listed as an interface"). Mali r54 does not reject such a
// module - it faults inside pipeline creation and takes the process down.
//
// Measured on a Mali-G1-Ultra as 216 KHR-GL44/45/46.tessellation_shader.tessellation_control_
// to_tessellation_evaluation.gl_MaxPatchVertices_Position_PointSize_* crashes; the CTS's TES
// there is exactly the shape below. It is not tessellation-specific and not XFB-specific: a
// vertex shader is enough, which is what these cases use.
//
// Every test captures a USER varying through transform feedback under GL_RASTERIZER_DISCARD.
// Position is undefined in the first two by construction, so it is never asserted on - what is
// asserted is that the capture came back at all, which it can only do if the driver accepted
// the module and built a pipeline.
#include <cstddef>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr std::size_t kCaptureFloats = 4;
constexpr GLsizeiptr kCaptureBytes = static_cast<GLsizeiptr>(kCaptureFloats * sizeof(float));
// The defect's shape: gl_PerVertex redeclared, gl_Position never assigned.
constexpr const char* kUnwrittenPositionVertexSource = R"(#version 430 core
layout(location = 0) in vec4 vs_in_value;
out gl_PerVertex {
vec4 gl_Position;
};
out vec4 vs_out_value;
void main() {
vs_out_value = vs_in_value;
}
)";
// The control that isolates the redeclaration: identical but for the one assignment.
// This one keeps its interface slot through the sanitize chain, so both injections were
// always legal on it - it must stay working.
constexpr const char* kWrittenPositionVertexSource = R"(#version 430 core
layout(location = 0) in vec4 vs_in_value;
out gl_PerVertex {
vec4 gl_Position;
};
out vec4 vs_out_value;
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
vs_out_value = vs_in_value;
}
)";
// The second control, and the one the CTS calls data_pass_through: no gl_PerVertex
// redeclaration at all, so there is no Position annotation for the passes to find and
// nothing to delist. It was never affected and proves the crash needs the redeclaration.
constexpr const char* kNoPositionBlockVertexSource = R"(#version 430 core
layout(location = 0) in vec4 vs_in_value;
out vec4 vs_out_value;
void main() {
vs_out_value = vs_in_value;
}
)";
GLuint CompileVertexShader(const std::string& source, std::string* log) {
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
const char* text = source.c_str();
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint status = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteShader(shader);
return 0;
}
return shader;
}
// `captureNames` is what goes to glTransformFeedbackVaryings. Passing gl_Position in it
// is what puts MirrorPositionForCapture on the path.
GLuint BuildCaptureProgram(const char* vertexSource, const std::vector<const char*>& captureNames,
std::string* log) {
const GLuint vertexShader = CompileVertexShader(vertexSource, log);
if (vertexShader == 0) return 0;
const GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glTransformFeedbackVaryings(program, static_cast<GLsizei>(captureNames.size()), captureNames.data(),
GL_INTERLEAVED_ATTRIBS);
glLinkProgram(program);
glDeleteShader(vertexShader);
GLint status = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteProgram(program);
return 0;
}
return program;
}
class UnwrittenPositionOutputScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
const float vertex[kCaptureFloats] = {1.0f, 2.0f, 3.0f, 4.0f};
glBufferData(GL_ARRAY_BUFFER, kCaptureBytes, vertex, GL_STATIC_DRAW);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
void TearDown() override {
if (!Ready()) return;
glBindVertexArray(0);
glUseProgram(0);
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
ScenarioTest::TearDown();
}
// Links `vertexSource` with `captureNames`, runs one captured point, and checks that
// the USER varying came back. `captureStride` is how many floats one captured vertex
// occupies, so the user varying can be read out from behind a captured gl_Position.
void ExpectUserVaryingIsCaptured(const char* vertexSource, const std::vector<const char*>& captureNames,
std::size_t captureStride, std::size_t userVaryingOffset,
const char* what) {
std::string log;
const GLuint program = BuildCaptureProgram(vertexSource, captureNames, &log);
ASSERT_NE(program, 0u) << what << ": the capture program failed to build: " << log;
const GLsizeiptr captureBytes = static_cast<GLsizeiptr>(captureStride * sizeof(float));
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffer);
// Pre-fill with a value the shader cannot produce, so "captured nothing" is
// distinguishable from "captured the wrong thing".
const std::vector<float> poison(captureStride, -1.0f);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, captureBytes, poison.data(), GL_DYNAMIC_READ);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
ASSERT_EQ(FirstGLError(), 0u) << what << ": setting up the capture buffer raised a GL error";
glEnable(GL_RASTERIZER_DISCARD);
glUseProgram(program);
glBindVertexArray(m_vao);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, 1);
glEndTransformFeedback();
glBindVertexArray(0);
glUseProgram(0);
glDisable(GL_RASTERIZER_DISCARD);
EXPECT_EQ(FirstGLError(), 0u) << what << ": the captured draw raised a GL error";
std::vector<float> readback(captureStride, -2.0f);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffer);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, readback.data());
for (std::size_t i = 0; i < kCaptureFloats; ++i) {
EXPECT_FLOAT_EQ(readback[userVaryingOffset + i], static_cast<float>(i + 1))
<< what << ": captured float " << i << " came back as "
<< readback[userVaryingOffset + i]
<< "; the pre-fill value means the draw never produced a vertex, which is what an "
"invalid shader module looks like from out here";
}
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
glDeleteBuffers(1, &xfbBuffer);
glDeleteProgram(program);
}
GLuint m_vao = 0;
GLuint m_vbo = 0;
};
} // namespace
// The clip fixup's half: PositionZRemap is on for every draw, so the fixup runs on this
// program and used to inject a store through the delisted block.
TEST_F(UnwrittenPositionOutputScenario, ARedeclaredButUnwrittenPositionStillDraws) {
if (!Ready() || IsSkipped()) return;
ExpectUserVaryingIsCaptured(kUnwrittenPositionVertexSource, {"vs_out_value"}, kCaptureFloats, 0,
"redeclared, never written");
}
// The XFB half: capturing gl_Position adds an access chain and a LOAD through the same
// delisted block, which the interface rule covers exactly as it covers the store. Position
// itself is undefined here - only the user varying behind it is asserted.
TEST_F(UnwrittenPositionOutputScenario, CapturingAnUnwrittenPositionStillDraws) {
if (!Ready() || IsSkipped()) return;
// DirectVulkan only, and not because the defect was backend-specific in principle - the
// injection this pins lives in DirectVulkan's ProgramFactory, and DirectGLES cannot
// reach the case at all: capturing gl_Position BY NAME off a shader that never writes it
// comes back empty there, because the ESSL the transpiler emits has no such output for
// the capture list to name. That is a known, separate DirectGLES gap (the same one that
// blocks gl_Position/gl_PointSize capture in the tessellation capture segment), tracked
// outside this scenario; asserting it here would only re-report it.
if (Gl().BackendName() != "DirectVulkan") {
GTEST_SKIP() << "capturing an unwritten gl_Position by name is a separate, known "
<< "DirectGLES gap; this case pins the DirectVulkan injection";
}
ExpectUserVaryingIsCaptured(kUnwrittenPositionVertexSource, {"gl_Position", "vs_out_value"},
kCaptureFloats * 2, kCaptureFloats, "capturing an unwritten gl_Position");
}
// Control: the same shader with the one assignment restored. Its block is never delisted,
// so it exercises the path the fixup is actually for and must keep working.
TEST_F(UnwrittenPositionOutputScenario, AWrittenRedeclaredPositionStillDraws) {
if (!Ready() || IsSkipped()) return;
ExpectUserVaryingIsCaptured(kWrittenPositionVertexSource, {"vs_out_value"}, kCaptureFloats, 0,
"redeclared and written");
}
TEST_F(UnwrittenPositionOutputScenario, CapturingAWrittenPositionStillDraws) {
if (!Ready() || IsSkipped()) return;
ExpectUserVaryingIsCaptured(kWrittenPositionVertexSource, {"gl_Position", "vs_out_value"},
kCaptureFloats * 2, kCaptureFloats, "capturing a written gl_Position");
}
// Control: no gl_PerVertex redeclaration, so no Position annotation and nothing to delist.
TEST_F(UnwrittenPositionOutputScenario, AShaderWithNoPositionBlockStillDraws) {
if (!Ready() || IsSkipped()) return;
ExpectUserVaryingIsCaptured(kNoPositionBlockVertexSource, {"vs_out_value"}, kCaptureFloats, 0,
"no gl_PerVertex block");
}
} // namespace MGITest
@@ -0,0 +1,657 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/XfbRepeatedCaptureScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - A CAPTURE MUST STILL RECORD WHEN IT IS NOT THE FIRST ONE IN THE PROCESS,
// AND THE CAPTURE STAGE MAY BE ANY OF THE FOUR THAT CAN BE THE LAST ONE.
//
// The conformance suite exposed a whole family of transform feedback failures that no
// existing scenario could reproduce, because every one of them ran ONE capture, from a
// VERTEX stage, in a freshly initialised process. What the suite actually does is
// different in three ways at once, and each of them turned out to matter:
//
// * it runs case after case in ONE GL context, resetting state between them - and the
// reset is not a fresh context. Its transform feedback part
// (framework/opengl/gluStateReset.cpp resetStateGLCore) unbinds the generic
// GL_TRANSFORM_FEEDBACK_BUFFER and then clears every indexed capture point from 0 to
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS, which permanently raises MobileGL's
// touched-binding-point high-water mark. Every later capture that uses fewer points
// than that - i.e. every INTERLEAVED_ATTRIBS capture - then had the unused tail
// re-cleared on the driver immediately before glBeginTransformFeedback.
// ReplayDeqpStateReset below is that reset, reduced to the calls that touch capture
// state, so a defect that only appears from the second capture onwards is reachable
// here instead of only on a device.
//
// * the capture stage is frequently a GEOMETRY or a TESSELLATION EVALUATION shader,
// never a plain vertex shader. The tree had zero coverage for either: none of the
// Xfb* scenarios mentioned tessellation and neither TessellationDrawModeScenario nor
// GeometryDrawModeScenario mentioned transform feedback.
//
// * the capture program frequently has NO FRAGMENT STAGE at all, because it draws
// under GL_RASTERIZER_DISCARD and never rasterises anything. That is legal in
// desktop GL and the shape most "use transform feedback as a readback channel"
// tests are built on.
//
// Every case here asserts the captured BYTES, never just the absence of a GL error: the
// failure this guards against writes nothing and raises nothing, so a buffer that kept
// its poison is the only thing that distinguishes it from success.
#include <cmath>
#include <string>
#include <utility>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// Nothing a capture can legitimately produce, so a component that still reads it
// names the failure ("the capture never reached these bytes") instead of looking
// like an ordinary numeric mismatch.
constexpr int kPoison = -987654;
const char* const kPassthroughVertexSource = R"(#version 420 core
layout(location = 0) in int vs_in_value;
flat out int vs_out_value;
void main()
{
vs_out_value = vs_in_value;
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
// The primitive_counter shape: one flat int per emitted vertex, several vertices
// per input primitive, so the capture is geometry-AMPLIFIED and the CPU-side
// primitive model cannot predict its length.
const char* const kPointAmplifyingGeometrySource = R"(#version 420 core
layout(points) in;
layout(points, max_vertices = 2) out;
flat in int vs_out_value[];
flat out int gs_out_value;
void main()
{
for (int i = 0; i < 2; ++i)
{
gs_out_value = vs_out_value[0];
gl_Position = gl_in[0].gl_Position;
EmitVertex();
EndPrimitive();
}
}
)";
// Adjacency input. Only a geometry stage can consume it, and CountPrimitivesForDraw
// used to answer 0 for every adjacency mode, which silently excluded the whole draw
// from the capture accounting.
const char* const kAdjacencyGeometrySource = R"(#version 420 core
layout(lines_adjacency) in;
layout(points, max_vertices = 1) out;
flat in int vs_out_value[];
flat out int gs_out_value;
void main()
{
gs_out_value = vs_out_value[1];
gl_Position = gl_in[1].gl_Position;
EmitVertex();
EndPrimitive();
}
)";
const char* const kTessControlSource = R"(#version 420 core
layout(vertices = 1) out;
flat in int vs_out_value[];
patch out int tcs_out_value;
void main()
{
tcs_out_value = vs_out_value[0];
gl_TessLevelOuter[0] = 1.0;
gl_TessLevelOuter[1] = 1.0;
gl_TessLevelOuter[2] = 1.0;
gl_TessLevelInner[0] = 1.0;
gl_out[gl_InvocationID].gl_Position = gl_in[0].gl_Position;
}
)";
const char* const kTessEvalSource = R"(#version 420 core
layout(triangles, equal_spacing, cw) in;
patch in int tcs_out_value;
flat out int tes_out_value;
void main()
{
tes_out_value = tcs_out_value;
gl_Position = gl_in[0].gl_Position;
}
)";
const char* const kFragmentSource = R"(#version 420 core
flat in int gs_out_value;
out vec4 fragColor;
void main()
{
fragColor = vec4(float(gs_out_value), 0.0, 0.0, 1.0);
}
)";
class XfbRepeatedCaptureScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
const int values[kInputVertices] = {10, 11, 12, 13};
glBufferData(GL_ARRAY_BUFFER, sizeof(values), values, GL_STATIC_DRAW);
glVertexAttribIPointer(0, 1, GL_INT, 0, nullptr);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
DrainErrors();
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (const GLuint program : m_programs) {
glDeleteProgram(program);
}
m_programs.clear();
glBindVertexArray(0);
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
m_vbo = 0;
m_vao = 0;
ScenarioTest::TearDown();
}
static constexpr int kInputVertices = 4;
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
static bool BackendHostsGeometry() {
GLint maxGeometryOutputVertices = 0;
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
DrainErrors();
return maxGeometryOutputVertices >= 2;
}
static bool BackendHostsTessellation() {
GLint maxTessGenLevel = 0;
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
DrainErrors();
return maxTessGenLevel >= 1;
}
// The transform-feedback-relevant half of deqp's resetStateGLCore, in its order.
// It runs between EVERY pair of conformance cases, and running one capture
// through it is the difference between "the first capture in the process" and
// every other one.
static void ReplayDeqpStateReset() {
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glDisable(GL_RASTERIZER_DISCARD);
glUseProgram(0);
GLint maxSeparateAttribs = 0;
glGetIntegerv(GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS, &maxSeparateAttribs);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
for (GLint index = 0; index < maxSeparateAttribs; ++index) {
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLuint>(index), 0);
}
DrainErrors();
}
static std::string InfoLog(GLuint object, bool isShader) {
GLint length = 0;
if (isShader) {
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
} else {
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
}
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
if (isShader) {
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
} else {
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
}
return buffer.data();
}
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, const char*>>& stages,
const char* varying) {
return BuildCaptureProgram(stages, std::vector<const char*>{varying});
}
// Builds a capture program out of `stages` capturing `varyings` interleaved.
// Returns 0 and fills m_buildLog on failure.
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, const char*>>& stages,
const std::vector<const char*>& varyings) {
m_buildLog.clear();
std::vector<GLuint> shaders;
bool ok = true;
for (const auto& [stage, source] : stages) {
const GLuint shader = glCreateShader(stage);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
shaders.push_back(shader);
if (compiled == GL_FALSE) {
m_buildLog = InfoLog(shader, true);
ok = false;
break;
}
}
GLuint program = 0;
if (ok) {
program = glCreateProgram();
for (const GLuint shader : shaders) {
glAttachShader(program, shader);
}
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
GL_INTERLEAVED_ATTRIBS);
glLinkProgram(program);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
m_buildLog = InfoLog(program, false);
glDeleteProgram(program);
program = 0;
}
}
for (const GLuint shader : shaders) {
glDeleteShader(shader);
}
if (program != 0) m_programs.push_back(program);
return program;
}
// One capture span. `captureMode` is the transform feedback primitive mode,
// `drawMode`/`count` the draw. Returns the capture buffer's contents.
std::vector<int> RunCaptureSpan(GLuint program, GLenum captureMode, GLenum drawMode, GLsizei count,
std::size_t capturedInts) {
std::vector<int> poison(capturedInts, kPoison);
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedInts * sizeof(int)), poison.data(),
GL_STATIC_COPY);
glBindBuffer(GL_ARRAY_BUFFER, 0);
// The capture point is the ONLY thing bound; the generic
// GL_TRANSFORM_FEEDBACK_BUFFER binding comes along for the ride, exactly as
// the conformance tests rely on (GL 4.6 core 6.1.1).
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
glBindVertexArray(m_vao);
glUseProgram(program);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(captureMode);
glDrawArrays(drawMode, 0, count);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
std::vector<int> readback(capturedInts, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(capturedInts * sizeof(int)), readback.data());
glUseProgram(0);
glDeleteBuffers(1, &xfbBuffer);
return readback;
}
static ::testing::AssertionResult CapturedNothing(const std::vector<int>& data) {
for (std::size_t i = 0; i < data.size(); ++i) {
if (data[i] != kPoison) {
return ::testing::AssertionFailure() << "component " << i << " is " << data[i];
}
}
return ::testing::AssertionSuccess();
}
static ::testing::AssertionResult CapturedIs(const std::vector<int>& data,
const std::vector<int>& expected) {
if (data.size() != expected.size()) {
return ::testing::AssertionFailure()
<< "captured " << data.size() << " value(s), expected " << expected.size();
}
for (std::size_t i = 0; i < data.size(); ++i) {
if (data[i] != expected[i]) {
::testing::AssertionResult failure = ::testing::AssertionFailure();
failure << "component " << i << " is " << data[i] << ", expected " << expected[i];
if (data[i] == kPoison) {
failure << " (the capture never reached these bytes)";
}
return failure;
}
}
return ::testing::AssertionSuccess();
}
std::vector<GLuint> m_programs;
std::string m_buildLog;
GLuint m_vao = 0;
GLuint m_vbo = 0;
};
// THE REGRESSION GUARD FOR THE WHOLE FAMILY. Two geometry-stage captures in one
// process with the conformance suite's own state reset between them; the assertion
// that matters is on the SECOND one, which is the one every device run failed while
// whichever body happened to land first in its process passed.
TEST_F(XfbRepeatedCaptureScenario, ASecondGeometryCaptureAfterADeqpStateResetStillRecords) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsGeometry()) {
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
}
// Two vertices emitted per input point, so the capture is amplified beyond what
// the CPU primitive model can predict from the draw alone.
const std::vector<int> expected = {10, 10, 11, 11, 12, 12, 13, 13};
for (int capture = 0; capture < 3; ++capture) {
// A fresh program per capture, because that is what a fresh conformance case
// builds - and it is what makes the driver recycle program and buffer names.
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
{GL_GEOMETRY_SHADER, kPointAmplifyingGeometrySource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
"gs_out_value");
ASSERT_NE(program, 0u) << "capture " << capture << " program failed to build: " << m_buildLog;
const std::vector<int> captured =
RunCaptureSpan(program, GL_POINTS, GL_POINTS, kInputVertices, expected.size());
EXPECT_TRUE(CapturedIs(captured, expected))
<< "capture " << capture << " of 3 in this process"
<< (capture == 0 ? "" : " (every earlier one was followed by a deqp-shaped state reset)");
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "capture " << capture;
glDeleteProgram(program);
m_programs.pop_back();
ReplayDeqpStateReset();
glBindVertexArray(m_vao);
}
}
// The tessellation half, which had no coverage anywhere in the tree: a capture taken
// from a GL_PATCHES draw, whose last vertex-processing stage is the evaluation shader
// and whose record count only the tessellator knows.
TEST_F(XfbRepeatedCaptureScenario, ACaptureFromAPatchesDrawRecords) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsTessellation()) {
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
<< ")";
}
// One input patch of one vertex, all levels at 1: the tessellator emits exactly
// one triangle, so three captured vertices all carrying the first input value.
glPatchParameteri(GL_PATCH_VERTICES, 1);
DrainErrors();
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
{GL_TESS_CONTROL_SHADER, kTessControlSource},
{GL_TESS_EVALUATION_SHADER, kTessEvalSource}},
"tes_out_value");
ASSERT_NE(program, 0u) << "patch capture program failed to build: " << m_buildLog;
const std::vector<int> expected = {10, 10, 10};
const std::vector<int> captured = RunCaptureSpan(program, GL_TRIANGLES, GL_PATCHES, 1, expected.size());
EXPECT_TRUE(CapturedIs(captured, expected));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// A capture program with NO FRAGMENT STAGE, drawn under GL_RASTERIZER_DISCARD. Legal
// in desktop GL, and the shape most transform-feedback-as-readback tests use; the
// program above only differs from it by the fragment shader, so a failure here is
// specifically about the missing stage.
TEST_F(XfbRepeatedCaptureScenario, ACaptureFromAFragmentlessProgramRecords) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsGeometry()) {
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
}
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
{GL_GEOMETRY_SHADER, kPointAmplifyingGeometrySource}},
"gs_out_value");
ASSERT_NE(program, 0u) << "fragmentless capture program failed to build: " << m_buildLog;
const std::vector<int> expected = {10, 10, 11, 11, 12, 12, 13, 13};
const std::vector<int> captured =
RunCaptureSpan(program, GL_POINTS, GL_POINTS, kInputVertices, expected.size());
EXPECT_TRUE(CapturedIs(captured, expected));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// An ADJACENCY draw feeding the capture. CountPrimitivesForDraw answered 0 for all
// four adjacency modes, which made the transform feedback accounting skip the draw
// entirely - so neither the captured-vertex counter nor the geometry-capture-draw
// flag moved, and anything downstream of either was working from "nothing happened".
TEST_F(XfbRepeatedCaptureScenario, ACaptureFromAnAdjacencyDrawRecords) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsGeometry()) {
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
}
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
{GL_GEOMETRY_SHADER, kAdjacencyGeometrySource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
"gs_out_value");
ASSERT_NE(program, 0u) << "adjacency capture program failed to build: " << m_buildLog;
// Four vertices of GL_LINES_ADJACENCY are one line primitive; the shader emits
// the second vertex of the four, which is the line's first real endpoint.
const std::vector<int> expected = {11};
const std::vector<int> captured =
RunCaptureSpan(program, GL_POINTS, GL_LINES_ADJACENCY, kInputVertices, expected.size());
EXPECT_TRUE(CapturedIs(captured, expected));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// An adjacency draw with NO geometry stage. GL 4.6 core table 13.1 admits
// GL_LINES_ADJACENCY and GL_LINE_STRIP_ADJACENCY under capture mode GL_LINES (and the
// triangle pair under GL_TRIANGLES): without a geometry shader the adjacent vertices
// are ignored and the primitive assembled is a plain line, so the combination is legal
// and must capture. MobileGL's active-capture primitive-mode table listed only the
// non-adjacency modes, so this raised GL_INVALID_OPERATION and dropped the draw
// entirely - the buffer kept its pre-draw bytes and the application saw an error the
// spec does not allow. Distinct from ACaptureFromAnAdjacencyDrawRecords above, which
// HAS a geometry stage and therefore bypasses that table completely.
TEST_F(XfbRepeatedCaptureScenario, AVertexOnlyAdjacencyCaptureRecords) {
if (!Ready()) GTEST_SKIP();
const GLuint program =
BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource}}, "vs_out_value");
ASSERT_NE(program, 0u) << "vertex-only capture program failed to build: " << m_buildLog;
// Four vertices of GL_LINES_ADJACENCY are one line whose real endpoints are the
// middle pair, so the capture is those two vertices in order.
const std::vector<int> expected = {11, 12};
const std::vector<int> captured =
RunCaptureSpan(program, GL_LINES, GL_LINES_ADJACENCY, kInputVertices, expected.size());
// THE GUARD FOR THE DEFECT ITSELF, and it is backend-independent: the frontend
// validator must not reject the combination. It used to record
// GL_INVALID_OPERATION and return before the draw was ever issued.
EXPECT_EQ(glGetError(), GL_NO_ERROR)
<< "a capture-mode/draw-mode pair GL 4.6 core table 13.1 admits must raise no error";
// Whether the capture then RECORDS is a backend question, and the two answer it
// differently. ES 3.2 (10.1) supports the adjacency primitive types only for a
// pipeline with a geometry shader, so DirectGLES has nothing to forward this draw
// to; desktop GL and Vulkan both assemble the plain line and capture it. Asserting
// the data unconditionally would be asserting that DirectGLES emulates a whole ES
// restriction away, which is a separate piece of work and not what this guards.
if (Gl().BackendName() == "DirectGLES") {
GTEST_SKIP() << "DirectGLES cannot forward a geometry-shader-less adjacency draw: ES 3.2 10.1 "
"supports the adjacency primitive types only with a geometry stage. The frontend "
"no longer rejects the draw (checked above), which is the defect this covers.";
}
EXPECT_TRUE(CapturedIs(captured, expected));
}
// A CAPTURE MUST NEVER LAND IN A BUFFER THE APPLICATION DID NOT BIND FOR IT.
//
// A capture list may legally begin with gl_NextBuffer, which leaves capture buffer 0
// with stride 0 and nothing to capture - so glBeginTransformFeedback does not require a
// buffer at point 0 and the application binds only point 1. The driver-side program is
// a single-buffer interleaved capture (the pseudo-varyings are consumed at link time),
// so it writes capture point 0, and MobileGL redirects that into scratch storage and
// scatters the records afterwards.
//
// Two ways that went wrong, both fixed here: the scratch was sized by reading each
// target's stride at its POSITION in a list that skips unbound buffers, which for this
// layout read stride 0 for everything and produced a zero capacity; and when the
// scratch then failed to bind, the span opened anyway onto whatever capture point 0
// still held from an earlier capture in the process - silently overwriting an unrelated
// application buffer. The first span below exists purely to leave such a binding behind.
TEST_F(XfbRepeatedCaptureScenario, ACaptureListBeginningWithGlNextBufferSparesTheEarlierBuffer) {
if (!Ready()) GTEST_SKIP();
const std::size_t capturedInts = 4;
const GLsizeiptr captureBytes = static_cast<GLsizeiptr>(capturedInts * sizeof(int));
// Span A: an ordinary capture, so capture point 0 is left holding bufferA.
const GLuint programA =
BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource}}, "vs_out_value");
ASSERT_NE(programA, 0u) << "plain capture program failed to build: " << m_buildLog;
std::vector<int> poison(capturedInts, kPoison);
GLuint bufferA = 0;
glGenBuffers(1, &bufferA);
glBindBuffer(GL_ARRAY_BUFFER, bufferA);
glBufferData(GL_ARRAY_BUFFER, captureBytes, poison.data(), GL_STATIC_COPY);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, bufferA);
glBindVertexArray(m_vao);
glUseProgram(programA);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, kInputVertices);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
glUseProgram(0);
std::vector<int> afterA(capturedInts, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, afterA.data());
const std::vector<int> spanAExpected = {10, 11, 12, 13};
ASSERT_TRUE(CapturedIs(afterA, spanAExpected)) << "the setup span itself did not capture";
// Span B: gl_NextBuffer first, so buffer 0 captures nothing and only point 1 is bound.
const GLuint programB = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource}},
{"gl_NextBuffer", "vs_out_value"});
if (programB == 0) {
GTEST_SKIP() << "gl_NextBuffer capture lists are not linkable on " << Gl().BackendName() << " ("
<< Gl().RendererString() << "): " << m_buildLog;
}
GLuint bufferB = 0;
glGenBuffers(1, &bufferB);
glBindBuffer(GL_ARRAY_BUFFER, bufferB);
glBufferData(GL_ARRAY_BUFFER, captureBytes, poison.data(), GL_STATIC_COPY);
glBindBuffer(GL_ARRAY_BUFFER, 0);
// Point 0 released, point 1 is the only destination this capture asks for.
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, bufferB);
glUseProgram(programB);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, kInputVertices);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
glUseProgram(0);
// THE ASSERTION THAT MATTERS: bufferA was not a destination of this capture, so it
// must still read exactly what span A left in it. A failure here is the corruption.
std::vector<int> bufferAAfterB(capturedInts, 0);
glBindBuffer(GL_ARRAY_BUFFER, bufferA);
glGetBufferSubData(GL_ARRAY_BUFFER, 0, captureBytes, bufferAAfterB.data());
glBindBuffer(GL_ARRAY_BUFFER, 0);
EXPECT_TRUE(CapturedIs(bufferAAfterB, spanAExpected))
<< "the gl_NextBuffer capture wrote into the buffer the PREVIOUS span had bound";
EXPECT_EQ(glGetError(), GL_NO_ERROR);
// ...and, where the backend places this layout at all, the buffer it WAS asked to
// write gets the records. That placement is the DirectGLES scatter path, whose
// scratch sizing used to read each target's stride at its POSITION in a list that
// skips unbound capture buffers - which for a leading gl_NextBuffer read stride 0
// for every target and sized the scratch at zero. DirectVulkan does not implement a
// leading-gl_NextBuffer layout at all (it captures nothing into bufferB); that is a
// pre-existing gap of its own, and the assertion above - that it corrupts nothing
// while declining - is what matters for it.
const bool backendPlacesLeadingNextBuffer = Gl().BackendName() != "DirectVulkan";
if (backendPlacesLeadingNextBuffer) {
std::vector<int> bufferBAfter(capturedInts, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, bufferBAfter.data());
EXPECT_TRUE(CapturedIs(bufferBAfter, spanAExpected));
}
// Unbound and deleted BEFORE any skip: a capture point left pointing at a buffer
// this test deleted would follow the process into the next scenario.
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, 0);
glDeleteBuffers(1, &bufferA);
glDeleteBuffers(1, &bufferB);
if (!backendPlacesLeadingNextBuffer) {
GTEST_SKIP() << "DirectVulkan does not place a capture list beginning with gl_NextBuffer; it "
"captures nothing, which the no-corruption assertion above has already covered.";
}
}
// The control for all of the above: a span that never draws must leave the capture
// buffer alone. Without it "the buffer kept its poison" could be read as the correct
// outcome of some path rather than as the bug, and the tightened early returns in
// StartPendingTransformFeedback have to keep this legal case legal.
TEST_F(XfbRepeatedCaptureScenario, ASpanThatNeverDrawsLeavesTheCaptureBufferAlone) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsGeometry()) {
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
}
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
{GL_GEOMETRY_SHADER, kPointAmplifyingGeometrySource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
"gs_out_value");
ASSERT_NE(program, 0u) << "capture program failed to build: " << m_buildLog;
const std::size_t capturedInts = 8;
std::vector<int> poison(capturedInts, kPoison);
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedInts * sizeof(int)), poison.data(),
GL_STATIC_COPY);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
glUseProgram(program);
glBeginTransformFeedback(GL_POINTS);
glEndTransformFeedback();
glUseProgram(0);
std::vector<int> readback(capturedInts, 0);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(capturedInts * sizeof(int)), readback.data());
EXPECT_TRUE(CapturedNothing(readback));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
glDeleteBuffers(1, &xfbBuffer);
}
} // namespace
} // namespace MGITest
+8 -1
View File
@@ -710,8 +710,15 @@ namespace MobileGL::MG_State {
// must not, or failing the link and killing every draw. A capture stage with an // must not, or failing the link and killing every draw. A capture stage with an
// empty list is not a reason to look further down: it is the answer, and // empty list is not a reason to look further down: it is the answer, and
// glBeginTransformFeedback's INVALID_OPERATION is the correct consequence. // glBeginTransformFeedback's INVALID_OPERATION is the correct consequence.
//
// The order is the pipeline read backwards and includes the tessellation CONTROL
// stage, which is a vertex-processing stage too (GL 4.6 core 11): it can only be
// the last one in a pipeline that has a TCS but no evaluation or geometry stage,
// which is why it sits after TessEval. Same four stages, same order, as
// ProgramLinkTask::ResolveTransformFeedbackVaryings - see rule (2).
for (const ShaderStage captureStage: for (const ShaderStage captureStage:
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::Vertex}) { {ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::TessControl,
ShaderStage::Vertex}) {
if (!compositeHasStage[static_cast<SizeT>(captureStage)]) continue; if (!compositeHasStage[static_cast<SizeT>(captureStage)]) continue;
const auto& captureProgram = pipeline->GetStageProgram(captureStage); const auto& captureProgram = pipeline->GetStageProgram(captureStage);
if (!captureProgram) continue; if (!captureProgram) continue;
@@ -1856,7 +1856,13 @@ namespace MobileGL::MG_State::GLState {
// them to the draw-buffer range fails the link of every such program. // them to the draw-buffer range fails the link of every such program.
if (artifacts.program->getIntermediate(EShLangFragment) == nullptr) return true; if (artifacts.program->getIntermediate(EShLangFragment) == nullptr) return true;
UnorderedMap<Int, String> colorNumberOwners; // Keyed on (colour number, COLOUR INDEX), not on the colour number alone. Two fragment
// outputs may share a location as long as their index differs - that pair IS dual-source
// blending (GL 4.6 core 11.1.3 / ARB_blend_func_extended, core since 3.3), spelled either
// `layout(location = 0, index = 0)` + `layout(location = 0, index = 1)` in the shader or
// through two glBindFragDataLocationIndexed calls. Aliasing on the number alone made every
// such program fail to link with "alias color number 0", which is the whole feature.
UnorderedMap<Int64, String> colorSlotOwners;
const Int outputCount = artifacts.program->getNumPipeOutputs(); const Int outputCount = artifacts.program->getNumPipeOutputs();
for (Int index = 0; index < outputCount; ++index) { for (Int index = 0; index < outputCount; ++index) {
const auto& output = artifacts.program->getPipeOutput(index); const auto& output = artifacts.program->getPipeOutput(index);
@@ -1869,6 +1875,46 @@ namespace MobileGL::MG_State::GLState {
const Int location = explicitLocation != in.explicitFragDataLocation.end() const Int location = explicitLocation != in.explicitFragDataLocation.end()
? static_cast<Int>(explicitLocation->second) ? static_cast<Int>(explicitLocation->second)
: static_cast<Int>(output.layoutLocation()); : static_cast<Int>(output.layoutLocation());
// The colour INDEX, under the one precedence rule the whole codebase uses: a NON-ZERO
// glBindFragDataLocationIndexed index wins, and a zero (or absent) one falls back to
// the shader's own layout(index = N).
//
// Zero has to mean "no override" rather than "index 0", because glBindFragDataLocation
// IS glBindFragDataLocationIndexed with index 0 (GL_Program.cpp) and writes a real 0
// into this map. Reading that 0 as an override made a blanket
// `glBindFragDataLocation(prog, 0, "b")` over a shader that declares
// `layout(location = 0, index = 1) out vec4 b;` collapse b onto slot (0,0) next to the
// index-0 output and fail the link as an alias - while the IO resolver had left b's
// qualifier at 1, the SPIR-V still carried Index 1, and glGetProgramResourceLocationIndex
// still answered 1. Validation was rejecting a program the backend had already emitted
// correctly, which is the one case where this branch can change the answer at all: this
// runs AFTER ShaderCompiler::LinkProgram/mapIO, so for every other shape the qualifier
// already carries the resolver's verdict.
//
// The two other consumers spell the same rule: TMglGlslIoResolver only writes the API
// index into the qualifier when it is non-zero, and ProgramInterface falls back to
// type.layoutIndex when GetFragmentDataIndex answers 0. All three now agree.
//
// Against the spec (GL 4.6 core 15.2.3): where a fragment output's index is given by a
// shader layout qualifier, that value is used and anything bound through
// BindFragDataLocation(Indexed) is IGNORED - the same precedence layout(location) has
// over glBindAttribLocation. That is stricter than "non-zero API wins", and the two
// differ in exactly one shape: an explicit `index = 0` in the shader against an API
// index of 1, where the spec keeps 0 and this codebase takes 1. That divergence lives
// in the resolver (it decides what is emitted); it is pre-existing, out of scope here,
// and deliberately not re-litigated in a third place - matching the resolver is what
// keeps validation checking what was actually built.
Int colorIndex = 0;
if (const auto explicitIndex = in.explicitFragDataIndex.find(outputName);
explicitIndex != in.explicitFragDataIndex.end()) {
colorIndex = static_cast<Int>(explicitIndex->second);
}
if (colorIndex == 0) {
if (const glslang::TType* outputType = output.getType();
outputType != nullptr && outputType->getQualifier().hasIndex()) {
colorIndex = static_cast<Int>(outputType->getQualifier().layoutIndex);
}
}
const Int span = std::max<Int>(output.size, 1); const Int span = std::max<Int>(output.size, 1);
if (location < 0 || location + span > in.maxFragmentOutputColorNumber) { if (location < 0 || location + span > in.maxFragmentOutputColorNumber) {
@@ -1881,10 +1927,16 @@ namespace MobileGL::MG_State::GLState {
} }
for (Int colorNumber = location; colorNumber < location + span; ++colorNumber) { for (Int colorNumber = location; colorNumber < location + span; ++colorNumber) {
auto [owner, inserted] = colorNumberOwners.emplace(colorNumber, outputName); const Int64 slot = (static_cast<Int64>(colorIndex) << 32) |
static_cast<Int64>(static_cast<Uint32>(colorNumber));
auto [owner, inserted] = colorSlotOwners.emplace(slot, outputName);
if (!inserted) { if (!inserted) {
artifacts.infoLog = std::format("Fragment outputs '{}' and '{}' alias color number {}.", artifacts.infoLog =
owner->second, outputName, colorNumber); colorIndex == 0
? std::format("Fragment outputs '{}' and '{}' alias color number {}.", owner->second,
outputName, colorNumber)
: std::format("Fragment outputs '{}' and '{}' alias color number {} at index {}.",
owner->second, outputName, colorNumber, colorIndex);
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog)); DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
ProgramObject::ResetLinkArtifacts(artifacts); ProgramObject::ResetLinkArtifacts(artifacts);
return false; return false;
@@ -1910,10 +1962,18 @@ namespace MobileGL::MG_State::GLState {
return true; return true;
} }
// Capture happens at the last vertex-processing stage (geometry, then // Capture happens at the last vertex-processing stage (geometry, then tessellation
// tessellation evaluation, then vertex). // evaluation, then tessellation CONTROL, then vertex). All four are vertex-processing
// stages in GL 4.6 core 11 - the control shader included - and in a separable program
// whose only stage is a TCS it is the last one that exists, so it is the capture stage
// and such a program MUST link (GL 4.6 core 7.3/11.1.2.1; the conformance suite spells
// the API split out at esextcTessellationShaderXFB.cpp:390-416, where a non-ES context
// takes should_succeed=true). TessControl sits AFTER TessEvaluation so a complete
// pipeline still captures at the evaluation stage and only a TCS-only program falls
// through to it. If MobileGL ever serves an ES context this arm has to be gated on the
// advertised API: ES requires the very same link to FAIL.
const glslang::TIntermediate* captureIntermediate = nullptr; const glslang::TIntermediate* captureIntermediate = nullptr;
for (EShLanguage stage : {EShLangGeometry, EShLangTessEvaluation, EShLangVertex}) { for (EShLanguage stage : {EShLangGeometry, EShLangTessEvaluation, EShLangTessControl, EShLangVertex}) {
captureIntermediate = artifacts.program->getIntermediate(stage); captureIntermediate = artifacts.program->getIntermediate(stage);
if (captureIntermediate != nullptr) { if (captureIntermediate != nullptr) {
break; break;
@@ -554,7 +554,8 @@ namespace MobileGL::MG_State::GLState {
// asked for is the safer of the two readings. // asked for is the safer of the two readings.
if (task->in.requestedXfbVaryings.empty()) { if (task->in.requestedXfbVaryings.empty()) {
for (const ShaderStage captureStage: for (const ShaderStage captureStage:
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::Vertex}) { {ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::TessControl,
ShaderStage::Vertex}) {
Bool stagePresent = false; Bool stagePresent = false;
for (const auto& shader : m_shaders) { for (const auto& shader : m_shaders) {
if (!shader || shader->GetShaderStage() != captureStage) continue; if (!shader || shader->GetShaderStage() != captureStage) continue;
@@ -99,9 +99,14 @@ namespace MobileGL::MG_State::GLState {
// is deliberately left unbound so it samples as (0,0,0,1)). Deliberately coarse - ANY // is deliberately left unbound so it samples as (0,0,0,1)). Deliberately coarse - ANY
// texture, ANY sampler - so that no mutation can slip past a per-unit binding memo; the // texture, ANY sampler - so that no mutation can slip past a per-unit binding memo; the
// setters that feed it all early-out when the value is unchanged, so the redundant // setters that feed it all early-out when the value is unchanged, so the redundant
// glTexParameteri calls applications issue every frame do not churn it. Kept separate // glTexParameteri calls applications issue every frame do not churn it.
// from the bind generation because the sampled texture SET is unaffected by these, and //
// the Vulkan backend's set memo keys on that one. // Kept separate from the bind generation because the two answer different questions, NOT
// because the sampled texture SET is immune to this one - it is not, and the claim that
// it was is what this comment used to say. DirectVulkan leaves an incomplete texture out
// of the set entirely and substitutes a fallback, so completeness decides membership, and
// its sampled-set memo carries THIS generation alongside the bind one. Any memo of a
// resolved per-unit binding - or of which textures a draw samples at all - needs both.
Uint64 GetSamplingResolutionGeneration() const { return m_samplingResolutionGeneration; } Uint64 GetSamplingResolutionGeneration() const { return m_samplingResolutionGeneration; }
void BumpSamplingResolutionGeneration() { ++m_samplingResolutionGeneration; } void BumpSamplingResolutionGeneration() { ++m_samplingResolutionGeneration; }
@@ -29,7 +29,9 @@ using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITEONLY_ALIAS_PREFIX; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITEONLY_ALIAS_PREFIX;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ImageArrayUnitPlan; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ImageArrayUnitPlan;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemapImageArrayElementUnits; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemapImageArrayElementUnits;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::PointSizeExtensionName;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestPointSizeExtension;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestViewportArrayExtension; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestViewportArrayExtension;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::SplitReadWriteImageUniforms; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::SplitReadWriteImageUniforms;
@@ -1289,6 +1291,80 @@ void main() { gl_ViewportIndex = 1; imageStore(uni_image, ivec2(0), uvec4(1u));
EXPECT_TRUE(Contains(out, "#extension GL_OES_viewport_array : require\n")) << out; EXPECT_TRUE(Contains(out, "#extension GL_OES_viewport_array : require\n")) << out;
} }
// --- tessellation / geometry gl_PointSize directive ---------------------------------------------
//
// ESSL 320 makes the tessellation and geometry STAGES core and still leaves gl_PointSize out of
// their gl_PerVertex entirely - it is only there under EXT/OES_tessellation_point_size resp.
// EXT/OES_geometry_point_size. SPIRV-Cross only ever sees a SPIR-V BuiltIn PointSize decoration
// and prints the identifier bare, so without this directive the stage fails to compile with
// "`gl_PointSize' undeclared", which takes the WHOLE program to program 0: the draw renders
// nothing and glBeginTransformFeedback on that program is rejected outright, so a capture of
// anything at all off it silently comes back empty. That is the shape of the 108 conformance
// bodies (36 per API tree) in tessellation_control_to_tessellation_evaluation.gl_MaxPatch-
// Vertices_Position_PointSize whose point_mode half puts gl_PointSize in the patch.
TEST(PointSizeExtensionNameTest, NamesBothSpellingsOfBothExtensions) {
using Tier = MG_External::GLESCapabilities::PointSizeTier;
EXPECT_STREQ(PointSizeExtensionName(Tier::ExtensionEXT, true), "GL_EXT_tessellation_point_size");
EXPECT_STREQ(PointSizeExtensionName(Tier::ExtensionOES, true), "GL_OES_tessellation_point_size");
EXPECT_STREQ(PointSizeExtensionName(Tier::ExtensionEXT, false), "GL_EXT_geometry_point_size");
EXPECT_STREQ(PointSizeExtensionName(Tier::ExtensionOES, false), "GL_OES_geometry_point_size");
}
// The two extensions are separate and neither implies the other, so the tessellation answer must
// never be handed to a geometry stage or the other way round - an `#extension` naming a string
// the driver does not advertise is itself a compile error on a strict compiler.
TEST(PointSizeExtensionNameTest, NoTierMeansNoDirective) {
using Tier = MG_External::GLESCapabilities::PointSizeTier;
EXPECT_EQ(PointSizeExtensionName(Tier::None, true), nullptr);
EXPECT_EQ(PointSizeExtensionName(Tier::None, false), nullptr);
}
TEST(RequestPointSizeExtensionTest, TheDirectiveGoesRightAfterTheVersionLine) {
const String source = R"(#version 320 es
layout(triangles, point_mode, cw, equal_spacing) in;
void main() { gl_Position = vec4(0.0); gl_PointSize = 5.0; }
)";
const String out = RequestPointSizeExtension(source, "GL_EXT_tessellation_point_size");
EXPECT_TRUE(Contains(out, "#version 320 es\n#extension GL_EXT_tessellation_point_size : require\n")) << out;
}
// The nullptr contract, and the reason it exists: a driver that advertises neither spelling gets
// NOTHING added rather than a directive it would reject on top of the error it already has.
TEST(RequestPointSizeExtensionTest, ANullNameMeansNotEmitted) {
const String source = R"(#version 320 es
layout(triangles, point_mode, cw, equal_spacing) in;
void main() { gl_Position = vec4(0.0); gl_PointSize = 5.0; }
)";
EXPECT_EQ(RequestPointSizeExtension(source, nullptr), source);
}
TEST(RequestPointSizeExtensionTest, AnAlreadyPresentDirectiveIsNotDuplicated) {
const String source = R"(#version 320 es
#extension GL_OES_tessellation_point_size : require
layout(triangles, point_mode, cw, equal_spacing) in;
void main() { gl_PointSize = 5.0; }
)";
const String out = RequestPointSizeExtension(source, "GL_OES_tessellation_point_size");
EXPECT_EQ(out, source);
EXPECT_EQ(CountOf(out, "GL_OES_tessellation_point_size"), 1u) << out;
}
// Shares its insertion point with the viewport-array and image-format directives, so a stage
// needing more than one must end up with all of them and with #version still first.
TEST(RequestPointSizeExtensionTest, CoexistsWithTheOtherHeaderDirectives) {
const String source = R"(#version 320 es
layout(points) in;
layout(points, max_vertices = 1) out;
void main() { gl_ViewportIndex = 1; gl_PointSize = 2.0; EmitVertex(); }
)";
const String out = RequestPointSizeExtension(RequestViewportArrayExtension(source, true),
"GL_EXT_geometry_point_size");
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
EXPECT_TRUE(Contains(out, "#extension GL_OES_viewport_array : require\n")) << out;
EXPECT_TRUE(Contains(out, "#extension GL_EXT_geometry_point_size : require\n")) << out;
}
// --- pass-through tessellation control stage -------------------------------------------------- // --- pass-through tessellation control stage --------------------------------------------------
// //
// Desktop GL makes the tessellation control stage optional and takes the levels from // Desktop GL makes the tessellation control stage optional and takes the levels from
@@ -1303,6 +1379,20 @@ namespace {
const FloatVec2 kDefaultInner(1.0f, 1.0f); const FloatVec2 kDefaultInner(1.0f, 1.0f);
} // namespace } // namespace
// The synthesized stage mirrors its neighbours' gl_PerVertex, so it can be the thing that
// declares gl_PointSize - and in ESSL a redeclaration is exactly as illegal as a reference
// without the extension. The directive has to survive being applied to its output.
TEST(RequestPointSizeExtensionTest, CoversAMirroredPassthroughControlStage) {
const String out = RequestPointSizeExtension(
BuildPassthroughTessControlEssl(320, 4, " highp vec4 gl_Position; highp float gl_PointSize; ",
" highp vec4 gl_Position; highp float gl_PointSize; ", kDefaultOuter,
kDefaultInner),
"GL_EXT_tessellation_point_size");
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
EXPECT_TRUE(Contains(out, "#extension GL_EXT_tessellation_point_size : require\n")) << out;
EXPECT_TRUE(Contains(out, "float gl_PointSize")) << out;
}
TEST(PassthroughTessControlEsslTest, DeclaresThePatchSizeAndWritesEveryTessLevel) { TEST(PassthroughTessControlEsslTest, DeclaresThePatchSizeAndWritesEveryTessLevel) {
const String out = BuildPassthroughTessControlEssl(320, 4, "", "", kDefaultOuter, kDefaultInner); const String out = BuildPassthroughTessControlEssl(320, 4, "", "", kDefaultOuter, kDefaultInner);
EXPECT_EQ(out.find("#version 320 es"), 0u) << out; EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
@@ -1034,14 +1034,69 @@ namespace {
if (index < kRecordedDrawBuffers) g_driverIndexedColorMasks[index] = {true, r, g, b, a}; if (index < kRecordedDrawBuffers) g_driverIndexedColorMasks[index] = {true, r, g, b, a};
} }
// What the blend block of SyncRenderState pushed. Enough to answer the two questions the
// dual-source cases ask: is blending on for a draw buffer, and which factor enums reached
// the driver.
struct RecordedBlend {
Bool enabled = false;
Bool enableSeen = false;
Bool factorsSeen = false;
GLenum srcRGB = 0, dstRGB = 0, srcAlpha = 0, dstAlpha = 0;
};
RecordedBlend g_driverBlend[kRecordedDrawBuffers];
void ResetRecordedBlend() {
for (auto& recorded : g_driverBlend) recorded = {};
}
void RecordBlendEnable(Bool enabled) {
for (auto& recorded : g_driverBlend) {
recorded.enabled = enabled;
recorded.enableSeen = true;
}
}
void RecordBlendFactors(GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha) {
for (auto& recorded : g_driverBlend) {
recorded.factorsSeen = true;
recorded.srcRGB = srcRGB;
recorded.dstRGB = dstRGB;
recorded.srcAlpha = srcAlpha;
recorded.dstAlpha = dstAlpha;
}
}
void StubViewport(GLint, GLint, GLsizei, GLsizei) {} void StubViewport(GLint, GLint, GLsizei, GLsizei) {}
void StubScissor(GLint, GLint, GLsizei, GLsizei) {} void StubScissor(GLint, GLint, GLsizei, GLsizei) {}
void StubEnable(GLenum) {} void StubEnable(GLenum cap) {
void StubDisable(GLenum) {} if (cap == GL_BLEND) RecordBlendEnable(true);
void StubEnablei(GLenum, GLuint) {} }
void StubDisablei(GLenum, GLuint) {} void StubDisable(GLenum cap) {
void StubBlendFuncSeparate(GLenum, GLenum, GLenum, GLenum) {} if (cap == GL_BLEND) RecordBlendEnable(false);
void StubBlendFuncSeparatei(GLuint, GLenum, GLenum, GLenum, GLenum) {} }
void StubEnablei(GLenum cap, GLuint index) {
if (cap == GL_BLEND && index < kRecordedDrawBuffers) {
g_driverBlend[index].enabled = true;
g_driverBlend[index].enableSeen = true;
}
}
void StubDisablei(GLenum cap, GLuint index) {
if (cap == GL_BLEND && index < kRecordedDrawBuffers) {
g_driverBlend[index].enabled = false;
g_driverBlend[index].enableSeen = true;
}
}
void StubBlendFuncSeparate(GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha) {
RecordBlendFactors(srcRGB, dstRGB, srcAlpha, dstAlpha);
}
void StubBlendFuncSeparatei(GLuint index, GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha) {
if (index >= kRecordedDrawBuffers) return;
g_driverBlend[index].factorsSeen = true;
g_driverBlend[index].srcRGB = srcRGB;
g_driverBlend[index].dstRGB = dstRGB;
g_driverBlend[index].srcAlpha = srcAlpha;
g_driverBlend[index].dstAlpha = dstAlpha;
}
void StubBlendEquationSeparate(GLenum, GLenum) {} void StubBlendEquationSeparate(GLenum, GLenum) {}
void StubBlendEquationSeparatei(GLuint, GLenum, GLenum) {} void StubBlendEquationSeparatei(GLuint, GLenum, GLenum) {}
void StubBlendColor(GLfloat, GLfloat, GLfloat, GLfloat) {} void StubBlendColor(GLfloat, GLfloat, GLfloat, GLfloat) {}
@@ -1066,7 +1121,9 @@ namespace {
// into a driver that this process never made current. // into a driver that this process never made current.
class ScopedRenderStateDriverStubs { class ScopedRenderStateDriverStubs {
public: public:
ScopedRenderStateDriverStubs(): // dualSourceBlendSupported models GL_EXT_blend_func_extended on the ES driver, which is
// the one capability in here that a real device is commonly WITHOUT.
explicit ScopedRenderStateDriverStubs(Bool dualSourceBlendSupported = true):
m_funcs(MG_Backend::DirectGLES::g_GLESFuncs), m_caps(MG_Backend::DirectGLES::g_GLESCapabilities) { m_funcs(MG_Backend::DirectGLES::g_GLESFuncs), m_caps(MG_Backend::DirectGLES::g_GLESCapabilities) {
auto& gl = MG_Backend::DirectGLES::g_GLESFuncs; auto& gl = MG_Backend::DirectGLES::g_GLESFuncs;
gl = MG_External::GLESFunctionsTable{}; gl = MG_External::GLESFunctionsTable{};
@@ -1102,9 +1159,10 @@ namespace {
caps.SupportsIndexedColorMask = true; caps.SupportsIndexedColorMask = true;
caps.SupportsSrgbWriteControl = false; caps.SupportsSrgbWriteControl = false;
caps.SupportsPolygonMode = false; caps.SupportsPolygonMode = false;
caps.SupportsDualSourceBlend = true; caps.SupportsDualSourceBlend = dualSourceBlendSupported;
ResetRecordedColorMasks(); ResetRecordedColorMasks();
ResetRecordedBlend();
// The viewport and scissor blocks fall back to querying the surface size when the // The viewport and scissor blocks fall back to querying the surface size when the
// frontend's rectangle is degenerate, and there is no surface in this process. // frontend's rectangle is degenerate, and there is no surface in this process.
MG_Impl::GLImpl::Viewport(0, 0, 4, 4); MG_Impl::GLImpl::Viewport(0, 0, 4, 4);
@@ -1119,6 +1177,11 @@ namespace {
// The shadow now describes pushes that went to the stubs, not to any driver. // The shadow now describes pushes that went to the stubs, not to any driver.
MG_Backend::DirectGLES::RenderStateImpl::InvalidateSyncedRenderState(); MG_Backend::DirectGLES::RenderStateImpl::InvalidateSyncedRenderState();
MG_Impl::GLImpl::ColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); MG_Impl::GLImpl::ColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
// Blend state is per-CONTEXT and the context outlives the fixture, so a case that
// enabled blending or asked for an exotic factor has to put it back or every later
// case in this binary inherits it.
MG_Impl::GLImpl::Disable(GL_BLEND);
MG_Impl::GLImpl::BlendFunc(GL_ONE, GL_ZERO);
} }
private: private:
@@ -1253,6 +1316,143 @@ TEST_F(FramebufferTest, ApplicationAlphaMaskOffIsStillHonouredOnANativeDrawBuffe
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
} }
// --- Dual-source blending without GL_EXT_blend_func_extended ------------------------------------
//
// GL_SRC1_* blend factors are core GL since 3.3, GLES core has nothing equivalent, and the ES
// driver may or may not carry GL_EXT_blend_func_extended. When it does, the factors translate and
// blend properly - the positive case below. When it does not, the blend block used to
// THROW_EXCEPTION, which is a plain `throw` (MG_Util/Types.h) with no catch anywhere in MG_Impl or
// MG_Backend, so it unwound out through the C GL ABI and killed the process over one unsupported
// blend factor. It now DECLINES: the draw buffer is pushed with blending off and neutral One/Zero
// factors, and the loss is logged once.
//
// Both halves are asserted at the seam that matters - what the ES driver is actually handed -
// because a GL_SRC1_* enum reaching a driver without the extension is the other failure mode: the
// driver answers GL_INVALID_ENUM, keeps whatever factors were set before, and mis-blends silently.
TEST_F(FramebufferTest, DualSourceBlendFactorsReachTheDriverWhenTheExtensionIsThere) {
ScopedRenderStateDriverStubs driver(/*dualSourceBlendSupported=*/true);
MG_Impl::GLImpl::Enable(GL_BLEND);
MG_Impl::GLImpl::BlendFunc(GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_SRC1_* is core since 3.3; glBlendFunc must take it";
ResetRecordedBlend();
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
EXPECT_TRUE(g_driverBlend[0].enabled) << "nothing may decline a blend the driver can do";
EXPECT_EQ(g_driverBlend[0].srcRGB, static_cast<GLenum>(GL_SRC1_COLOR));
EXPECT_EQ(g_driverBlend[0].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC1_COLOR));
EXPECT_EQ(g_driverBlend[0].srcAlpha, static_cast<GLenum>(GL_SRC1_COLOR));
EXPECT_EQ(g_driverBlend[0].dstAlpha, static_cast<GLenum>(GL_ONE_MINUS_SRC1_COLOR));
}
TEST_F(FramebufferTest, DualSourceBlendIsDeclinedRatherThanThrownWhenTheExtensionIsMissing) {
ScopedRenderStateDriverStubs driver(/*dualSourceBlendSupported=*/false);
MG_Impl::GLImpl::Enable(GL_BLEND);
MG_Impl::GLImpl::BlendFunc(GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR)
<< "the FRONTEND accepts the factor whatever the driver can do - the decline is a backend decision";
ResetRecordedBlend();
// The whole point: this used to be `throw std::runtime_error` straight through the GL ABI.
ASSERT_NO_THROW(MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false));
ASSERT_TRUE(g_driverBlend[0].enableSeen) << "the blend enable still has to be pushed";
EXPECT_FALSE(g_driverBlend[0].enabled) << "a blend the driver cannot do is declined, not attempted";
for (Uint i = 0; i < kRecordedDrawBuffers; ++i) {
EXPECT_NE(g_driverBlend[i].srcRGB, static_cast<GLenum>(GL_SRC1_ALPHA))
<< "draw buffer " << i << ": no GL_SRC1_* enum may reach a driver without the extension";
EXPECT_NE(g_driverBlend[i].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC1_ALPHA)) << "draw buffer " << i;
EXPECT_NE(g_driverBlend[i].srcAlpha, static_cast<GLenum>(GL_SRC1_ALPHA)) << "draw buffer " << i;
EXPECT_NE(g_driverBlend[i].dstAlpha, static_cast<GLenum>(GL_ONE_MINUS_SRC1_ALPHA)) << "draw buffer " << i;
}
// The decline is scoped to the offending factor, not to blending as a whole: an ordinary
// blend on the same driver still goes through, and the SAME sync that declined the first one
// is what has to push it.
MG_Impl::GLImpl::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
ResetRecordedBlend();
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
EXPECT_TRUE(g_driverBlend[0].enabled);
EXPECT_EQ(g_driverBlend[0].srcRGB, static_cast<GLenum>(GL_SRC_ALPHA));
EXPECT_EQ(g_driverBlend[0].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC_ALPHA));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The half the first version of the decline missed: the FACTOR push is not gated on Enabled, so
// GL_BLEND being OFF does not keep a GL_SRC1_* enum away from a driver that cannot parse it. This
// is the sequence - `glDisable(GL_BLEND); glBlendFunc(GL_SRC1_ALPHA, ...)` then any draw or clear -
// and it needs no dual-source shader at all, which is why it survived both the enabled-path unit
// case above and the integration scenario (that one skips on exactly the extension-less lanes this
// concerns, because its probe needs a dual-source program to render).
//
// What a leaked enum costs: the driver answers GL_INVALID_ENUM and keeps its previous factors, so
// the error sits in the ES context's own queue for the next internal `glGetError() == GL_NO_ERROR`
// probe to read as its own failure, and this backend's shadow records factors the context rejected.
TEST_F(FramebufferTest, DualSourceFactorsAreDeclinedEvenWithBlendingDisabled) {
ScopedRenderStateDriverStubs driver(/*dualSourceBlendSupported=*/false);
MG_Impl::GLImpl::Disable(GL_BLEND);
MG_Impl::GLImpl::BlendFunc(GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
ResetRecordedBlend();
ASSERT_NO_THROW(MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false));
for (Uint i = 0; i < kRecordedDrawBuffers; ++i) {
EXPECT_FALSE(g_driverBlend[i].enabled) << "draw buffer " << i << ": blending was never enabled";
EXPECT_NE(g_driverBlend[i].srcRGB, static_cast<GLenum>(GL_SRC1_ALPHA))
<< "draw buffer " << i
<< ": a GL_SRC1_* enum must not reach a driver without the extension even with GL_BLEND off";
EXPECT_NE(g_driverBlend[i].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC1_ALPHA)) << "draw buffer " << i;
EXPECT_NE(g_driverBlend[i].srcAlpha, static_cast<GLenum>(GL_SRC1_ALPHA)) << "draw buffer " << i;
EXPECT_NE(g_driverBlend[i].dstAlpha, static_cast<GLenum>(GL_ONE_MINUS_SRC1_ALPHA)) << "draw buffer " << i;
}
// A clear reaches the same block by the same route (SyncRenderState(forColorClear=true)), and
// the flag only steers the alpha-widen colour mask, so it must not reopen this either.
MG_Impl::GLImpl::BlendFunc(GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR);
ResetRecordedBlend();
ASSERT_NO_THROW(MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/true));
for (Uint i = 0; i < kRecordedDrawBuffers; ++i) {
EXPECT_NE(g_driverBlend[i].srcRGB, static_cast<GLenum>(GL_SRC1_COLOR)) << "draw buffer " << i;
EXPECT_NE(g_driverBlend[i].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC1_COLOR)) << "draw buffer " << i;
}
// And the shadow records what was PUSHED, not what the frontend holds - otherwise the next
// switch to an ordinary factor diffs against state the ES context never received.
MG_Impl::GLImpl::Enable(GL_BLEND);
MG_Impl::GLImpl::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
ResetRecordedBlend();
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
EXPECT_TRUE(g_driverBlend[0].enabled) << "the enable has to be pushed - the shadow said 'off' because it was";
EXPECT_EQ(g_driverBlend[0].srcRGB, static_cast<GLenum>(GL_SRC_ALPHA));
EXPECT_EQ(g_driverBlend[0].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC_ALPHA));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The capable driver is unaffected by the ungating: GL_BLEND off with SRC1 factors set is a state
// an application may legitimately hold, and the factors still have to reach a driver that parses
// them - otherwise the next glEnable(GL_BLEND) would blend against neutralised state.
TEST_F(FramebufferTest, DualSourceFactorsWithBlendingDisabledStillReachACapableDriver) {
ScopedRenderStateDriverStubs driver(/*dualSourceBlendSupported=*/true);
MG_Impl::GLImpl::Disable(GL_BLEND);
MG_Impl::GLImpl::BlendFunc(GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA);
ResetRecordedBlend();
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
EXPECT_FALSE(g_driverBlend[0].enabled);
EXPECT_EQ(g_driverBlend[0].srcRGB, static_cast<GLenum>(GL_SRC1_ALPHA));
EXPECT_EQ(g_driverBlend[0].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC1_ALPHA));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// --- glFramebufferTexture error conditions (GL 4.6 core 9.2.8) --------------------------------- // --- glFramebufferTexture error conditions (GL 4.6 core 9.2.8) ---------------------------------
// //
// Four of them were missing from the bound-target path while its DSA sibling // Four of them were missing from the bound-target path while its DSA sibling
+17
View File
@@ -180,6 +180,22 @@ target_link_libraries(
${LINK_LIBRARIES} ${LINK_LIBRARIES}
) )
add_executable(
TessellationLinkTest
TessellationLinkTest.cpp
)
target_include_directories(TessellationLinkTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
TessellationLinkTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
add_executable( add_executable(
ProgramPipelineCompositeTest ProgramPipelineCompositeTest
ProgramPipelineCompositeTest.cpp ProgramPipelineCompositeTest.cpp
@@ -229,6 +245,7 @@ gtest_discover_tests(ProgramTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(ProgramInterfaceTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) gtest_discover_tests(ProgramInterfaceTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(ProgramPipelineCompositeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) gtest_discover_tests(ProgramPipelineCompositeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(XfbBlockVaryingTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) gtest_discover_tests(XfbBlockVaryingTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(TessellationLinkTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
# Heavier than the rest of the unit suite by design: several cases deliberately saturate the # Heavier than the rest of the unit suite by design: several cases deliberately saturate the
# compile pool so there is something in flight to race against. # compile pool so there is something in flight to race against.
gtest_discover_tests(AsyncCompileTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300) gtest_discover_tests(AsyncCompileTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
+125
View File
@@ -4023,6 +4023,131 @@ void main() { mgColor = vec4(1.0); }
)"; )";
} // namespace } // namespace
// Two fragment outputs on ONE location with DIFFERENT colour indices is not an aliasing error -
// it is dual-source blending (GL 4.6 core 11.1.3 / ARB_blend_func_extended, core since 3.3), and
// the GL_SRC1_* blend factors have nothing to read without it. The link-time aliasing check keyed
// on the colour number alone, so every such program failed to link with "alias color number 0"
// and the whole feature was unreachable from shader-side GLSL.
TEST_F(ProgramTest, FragmentOutputsMayShareALocationWhenTheirColorIndexDiffers) {
constexpr const char* dualSourceFs = R"(#version 460 core
layout(location = 0, index = 0) out vec4 fragColor0;
layout(location = 0, index = 1) out vec4 fragColor1;
void main() { fragColor0 = vec4(1.0); fragColor1 = vec4(0.5); }
)";
const GLuint program = LinkStages({{GL_VERTEX_SHADER, kPassthroughVs}, {GL_FRAGMENT_SHADER, dualSourceFs}});
GLint linkStatus = GL_FALSE;
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
ASSERT_EQ(linkStatus, GL_TRUE) << [&] {
char log[512] = "";
GetProgramInfoLog(program, sizeof(log), nullptr, log);
return std::string(log);
}();
// Both outputs are active and both sit on colour number 0 - which is the shape that used to be
// refused. (glGetFragDataIndex still answers 0 for the index-1 output: it reports only what
// glBindFragDataLocationIndexed bound, and reflecting the shader-side qualifier is a separate
// gap, so it is deliberately not asserted here.)
EXPECT_EQ(GetFragDataLocation(program, "fragColor0"), 0);
EXPECT_EQ(GetFragDataLocation(program, "fragColor1"), 0);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// The check it must NOT stop making: two outputs on the same colour number AND the same index
// really do alias, and that link has to fail. Aliased through glBindFragDataLocation rather than
// through two `layout(location = 0)` qualifiers on purpose - the qualifier form is caught by
// glslang at COMPILE time, so it would never reach the link-time rule this pins.
TEST_F(ProgramTest, FragmentOutputsSharingAColorNumberAtTheSameIndexStillFailToLink) {
constexpr const char* twoOutputFs = R"(#version 460 core
out vec4 fragColorA;
out vec4 fragColorB;
void main() { fragColorA = vec4(1.0); fragColorB = vec4(0.5); }
)";
const GLuint program = CreateProgram();
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &kPassthroughVs, nullptr);
CompileShader(vs);
AttachShader(program, vs);
DeleteShader(vs);
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &twoOutputFs, nullptr);
CompileShader(fs);
AttachShader(program, fs);
DeleteShader(fs);
BindFragDataLocation(program, 0, "fragColorA");
BindFragDataLocation(program, 0, "fragColorB");
LinkProgram(program);
GLint linkStatus = GL_TRUE;
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
EXPECT_EQ(linkStatus, GL_FALSE);
char infoLog[512] = "";
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
EXPECT_NE(std::string(infoLog).find("alias color number"), std::string::npos) << infoLog;
// ...and the same pair separated by the colour INDEX links, which is the whole point of the
// key being a pair.
BindFragDataLocationIndexed(program, 0, 1, "fragColorB");
LinkProgram(program);
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
EXPECT_EQ(linkStatus, GL_TRUE) << [&] {
char log[512] = "";
GetProgramInfoLog(program, sizeof(log), nullptr, log);
return std::string(log);
}();
for (int i = 0; i < 32 && GetError() != GL_NO_ERROR; ++i) {
}
}
// An API colour index of ZERO is "no override", not "index 0". glBindFragDataLocation is
// glBindFragDataLocationIndexed with index 0 (GL_Program.cpp), so the blanket-bind pattern -
// portable code that binds every output name it knows about, without caring about dual-source -
// writes a real 0 into the frag-data index map for an output whose shader qualifier says 1.
// Reading that 0 as an override collapsed both outputs onto slot (0,0) and failed the link as an
// alias, while the IO resolver had left the qualifier at 1 and the emitted SPIR-V still carried
// Index 1 - validation rejecting a program the backend had already built correctly.
//
// The rule pinned here is the codebase's (non-zero API index wins, zero falls back to the shader
// qualifier), which is also what GL 4.6 core 15.2.3 gives for THIS shape: a shader layout
// qualifier is used and the bound value ignored.
TEST_F(ProgramTest, AnApiColorIndexOfZeroDoesNotOverrideTheShaderIndexQualifier) {
constexpr const char* dualSourceFs = R"(#version 460 core
layout(location = 0, index = 0) out vec4 fragColor0;
layout(location = 0, index = 1) out vec4 fragColor1;
void main() { fragColor0 = vec4(1.0); fragColor1 = vec4(0.5); }
)";
const GLuint program = CreateProgram();
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &kPassthroughVs, nullptr);
CompileShader(vs);
AttachShader(program, vs);
DeleteShader(vs);
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &dualSourceFs, nullptr);
CompileShader(fs);
AttachShader(program, fs);
DeleteShader(fs);
// The blanket bind: colour number 0, index 0, on the output the shader put at index 1.
BindFragDataLocation(program, 0, "fragColor0");
BindFragDataLocation(program, 0, "fragColor1");
LinkProgram(program);
GLint linkStatus = GL_FALSE;
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
EXPECT_EQ(linkStatus, GL_TRUE) << [&] {
char log[512] = "";
GetProgramInfoLog(program, sizeof(log), nullptr, log);
return std::string(log);
}();
// The explicit indexed form with a NON-zero index is still an override, and still links.
BindFragDataLocationIndexed(program, 0, 1, "fragColor1");
LinkProgram(program);
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
EXPECT_EQ(linkStatus, GL_TRUE);
EXPECT_EQ(GetFragDataIndex(program, "fragColor1"), 1);
for (int i = 0; i < 32 && GetError() != GL_NO_ERROR; ++i) {
}
}
TEST_F(ProgramTest, GetProgramivReportsTheGeometryStageLinkProperties) { TEST_F(ProgramTest, GetProgramivReportsTheGeometryStageLinkProperties) {
constexpr const char* gs = R"(#version 460 core constexpr const char* gs = R"(#version 460 core
layout(triangles, invocations = 3) in; layout(triangles, invocations = 3) in;
@@ -0,0 +1,285 @@
// MobileGL - MobileGL/MG_Test/Program/TessellationLinkTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// Link-time properties of programs that carry a tessellation control stage. GPU-free:
// everything asserted here is a property of the link, not of any driver.
//
// Two independent defects live here, both found by KHR-GL4x.tessellation_shader:
//
// (1) The transform-feedback capture stage. GL 4.6 core 11 makes the tessellation CONTROL
// shader a vertex-processing stage like the other three, so in a separable program whose
// only stage is a TCS it is the LAST vertex-processing stage and therefore the capture
// stage - such a program must link with transform-feedback varyings requested. MobileGL
// searched {geometry, tessellation evaluation, vertex} only and refused the link with
// "Transform feedback varyings requested but the program has no vertex-processing stage",
// failing KHR-GL4x.tessellation_shader.single.xfb_captures_data_from_correct_stage on all
// three API versions (esextcTessellationShaderXFB.cpp:390-416 passes should_succeed=true
// for a non-ES context; ES demands the opposite, which is why the new arm is documented as
// desktop-GL-only at the search site).
//
// (2) `patch out T name[N]` against `patch in T name[N]`. Legal, identically spelled on both
// sides, and rejected until the glslang fork was re-pinned at d89cf443 - see the last case,
// which carries the diagnosis and now guards the pin.
#include <gtest/gtest.h>
#include <ios>
#include <string>
#include <utility>
#include <vector>
#include "Includes.h"
#include "Init.h"
#include "MG_Impl/GLImpl/Getter/GL_Getter.h"
#include "MG_Impl/GLImpl/Program/GL_Program.h"
#include "MG_State/GLState/Core.h"
using namespace MobileGL;
using namespace MobileGL::MG_Impl::GLImpl;
namespace {
class TessellationLinkTest: public ::testing::Test {
protected:
void SetUp() override {
MobileGL::Initialize();
for (int i = 0; i < 32 && GetError() != GL_NO_ERROR; ++i) {
}
}
};
std::string ShaderLog(GLuint shader) {
char log[4096] = "";
GetShaderInfoLog(shader, sizeof(log), nullptr, log);
return std::string(log);
}
std::string LinkLog(GLuint program) {
char log[4096] = "";
GetProgramInfoLog(program, sizeof(log), nullptr, log);
return std::string(log);
}
GLint Programiv(GLuint program, GLenum pname) {
GLint value = -1;
GetProgramiv(program, pname, &value);
return value;
}
// Attaches one compiled shader of each requested stage. Compilation is asserted, so a
// failure here is a shader bug in the test rather than a link result.
GLuint MakeProgram(const std::vector<std::pair<GLenum, const char*>>& stages, Bool separable) {
const GLuint program = CreateProgram();
if (separable) {
ProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE);
}
for (const auto& [type, source]: stages) {
const GLuint shader = CreateShader(type);
ShaderSource(shader, 1, &source, nullptr);
CompileShader(shader);
GLint compiled = GL_FALSE;
GetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
EXPECT_EQ(compiled, GL_TRUE) << "stage 0x" << std::hex << type << "\n" << ShaderLog(shader);
AttachShader(program, shader);
}
return program;
}
// The conformance suite's own tessellation control shader
// (esextcTessellationShaderXFB.cpp:360-381), with the ES-only ${...} expansions dropped -
// on a desktop context they expand to nothing.
constexpr const char* kCtsTessControl = R"(#version 460 core
layout (vertices=4) out;
in BLOCK_INOUT { vec4 value; } user_in[];
out BLOCK_INOUT { vec4 value; } user_out[];
void main()
{
gl_out [gl_InvocationID].gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
user_out [gl_InvocationID].value = vec4(2.0, 3.0, 4.0, 5.0);
gl_TessLevelOuter[0] = 1.0;
gl_TessLevelOuter[1] = 1.0;
}
)";
// A tessellation control shader IS a vertex-processing stage (GL 4.6 core 11), and in a
// TCS-only separable program it is the last one - so it is the capture stage and the link
// must succeed with the block member resolved against ITS outputs.
TEST_F(TessellationLinkTest, TcsOnlySeparableProgramWithXfbVaryingsLinks) {
const GLuint program = MakeProgram({{GL_TESS_CONTROL_SHADER, kCtsTessControl}}, /*separable=*/true);
const GLchar* const varyings[1] = {"BLOCK_INOUT.value"};
TransformFeedbackVaryings(program, 1, varyings, GL_SEPARATE_ATTRIBS);
LinkProgram(program);
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_NO_ERROR));
// The request resolved rather than being quietly dropped: the interface reports it back.
EXPECT_EQ(Programiv(program, GL_TRANSFORM_FEEDBACK_VARYINGS), 1);
EXPECT_EQ(Programiv(program, GL_TRANSFORM_FEEDBACK_BUFFER_MODE), GL_SEPARATE_ATTRIBS);
GLchar name[128] = {'\0'};
GLsizei length = 0;
GLsizei size = 0;
GLenum type = 0;
GetTransformFeedbackVarying(program, 0, sizeof(name), &length, &size, &type, name);
EXPECT_EQ(std::string(name, name + (length < 0 ? 0 : length)), "BLOCK_INOUT.value");
EXPECT_EQ(type, static_cast<GLenum>(GL_FLOAT_VEC4));
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_NO_ERROR));
}
// Control: the same program with no capture request. It linked before the fix too, which is
// what keeps this case honest about WHICH half of the link moved.
TEST_F(TessellationLinkTest, TcsOnlySeparableProgramWithoutXfbVaryingsLinks) {
const GLuint program = MakeProgram({{GL_TESS_CONTROL_SHADER, kCtsTessControl}}, /*separable=*/true);
LinkProgram(program);
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
EXPECT_EQ(Programiv(program, GL_TRANSFORM_FEEDBACK_VARYINGS), 0);
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_NO_ERROR));
}
// A program with no vertex-processing stage at all still has to be refused - the fix widened
// the search, it did not remove the check.
TEST_F(TessellationLinkTest, FragmentOnlySeparableProgramWithXfbVaryingsStillFailsToLink) {
constexpr const char* fs = R"(#version 460 core
out vec4 color;
void main() { color = vec4(1.0); }
)";
const GLuint program = MakeProgram({{GL_FRAGMENT_SHADER, fs}}, /*separable=*/true);
const GLchar* const varyings[1] = {"color"};
TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
LinkProgram(program);
EXPECT_EQ(Programiv(program, GL_LINK_STATUS), GL_FALSE);
EXPECT_NE(LinkLog(program).find("no vertex-processing stage"), std::string::npos) << LinkLog(program);
for (int i = 0; i < 32 && GetError() != GL_NO_ERROR; ++i) {
}
}
constexpr const char* kPassthroughVs = R"(#version 460 core
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
)";
constexpr const char* kTcsWithPatchScalar = R"(#version 460 core
layout (vertices = 3) out;
patch out vec4 tcs_patch;
out vec4 tcs_per_vertex[];
void main() {
tcs_patch = vec4(1.0);
tcs_per_vertex[gl_InvocationID] = vec4(2.0);
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
gl_TessLevelOuter[0] = 1.0; gl_TessLevelOuter[1] = 1.0; gl_TessLevelOuter[2] = 1.0;
gl_TessLevelInner[0] = 1.0;
}
)";
constexpr const char* kTesWithPatchScalar = R"(#version 460 core
layout (triangles) in;
patch in vec4 tcs_patch;
in vec4 tcs_per_vertex[];
out vec4 tes_out;
void main() {
tes_out = tcs_patch + tcs_per_vertex[0];
gl_Position = gl_in[0].gl_Position;
}
)";
// The capture stage of a COMPLETE pipeline is unchanged by the widened search: tessellation
// control sits AFTER tessellation evaluation in the order, so a program that has both still
// resolves its capture names against the EVALUATION stage's outputs. Both halves are pinned -
// an evaluation output resolves, a control output does not.
TEST_F(TessellationLinkTest, CompletePipelineStillCapturesAtTheEvaluationStage) {
{
const GLuint program = MakeProgram({{GL_VERTEX_SHADER, kPassthroughVs},
{GL_TESS_CONTROL_SHADER, kTcsWithPatchScalar},
{GL_TESS_EVALUATION_SHADER, kTesWithPatchScalar}},
/*separable=*/true);
const GLchar* const varyings[1] = {"tes_out"};
TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
LinkProgram(program);
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
EXPECT_EQ(Programiv(program, GL_TRANSFORM_FEEDBACK_VARYINGS), 1);
}
{
// tcs_per_vertex is an output of the CONTROL stage, which is not the capture stage
// here. Resolving it would mean capturing at the wrong stage, so the link must fail.
const GLuint program = MakeProgram({{GL_VERTEX_SHADER, kPassthroughVs},
{GL_TESS_CONTROL_SHADER, kTcsWithPatchScalar},
{GL_TESS_EVALUATION_SHADER, kTesWithPatchScalar}},
/*separable=*/true);
const GLchar* const varyings[1] = {"tcs_per_vertex"};
TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
LinkProgram(program);
EXPECT_EQ(Programiv(program, GL_LINK_STATUS), GL_FALSE) << LinkLog(program);
}
for (int i = 0; i < 32 && GetError() != GL_NO_ERROR; ++i) {
}
}
// A patch-qualified SCALAR crosses the TCS/TES boundary today. It is the control for the
// array case below: same qualifier, same stages, only the arrayness differs.
TEST_F(TessellationLinkTest, PatchQualifiedScalarLinksAcrossTheTessellationStages) {
const GLuint program = MakeProgram({{GL_VERTEX_SHADER, kPassthroughVs},
{GL_TESS_CONTROL_SHADER, kTcsWithPatchScalar},
{GL_TESS_EVALUATION_SHADER, kTesWithPatchScalar}},
/*separable=*/true);
LinkProgram(program);
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_NO_ERROR));
}
// `patch out int a[N]` against `patch in int a[N]`: legal GLSL, identical spellings, and until
// the glslang fork was re-pinned at d89cf443 refused with "Array sizes must be compatible"
// while printing the two sides as the same type. That is what failed
// KHR-GL4x.tessellation_shader.tessellation_shader_tc_barriers.* on all three API versions.
//
// The defect was one asymmetric clause in glslang, never in MobileGL:
// 3rdparty/glslang/glslang/MachineIndependent/linkValidate.cpp, TIntermediate::isIoResizeArray.
// The TessControl arm is guarded with `&& ! type.getQualifier().patch`; the TessEvaluation arm
// was not. A patch-qualified array therefore answered false on the control side and true on the
// evaluation side, and the caller's dimension arithmetic (linkValidate.cpp:1200-1218) computed
// (numDim - firstDim) == (unitNumDim - unitFirstDim) as (1 - 0) == (1 - 1), i.e. false. The
// fork now mirrors the control arm, so both sides answer false, the comparison falls to
// sameArrayness, and identical int[16] declarations match.
//
// A hard assertion, with no escape hatch: this case carried a message-matched GTEST_SKIP while
// the fix was outstanding, and leaving it in after the pin moved would turn a rolled-back fork
// into a silent skip instead of the failure it should be.
TEST_F(TessellationLinkTest, PatchQualifiedArrayLinksAcrossTheTessellationStages) {
constexpr const char* tcs = R"(#version 460 core
layout (vertices = 3) out;
patch out int tcs_patch_result[16];
void main() {
for (int i = 0; i < 16; ++i) { tcs_patch_result[i] = i; }
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
gl_TessLevelOuter[0] = 1.0; gl_TessLevelOuter[1] = 1.0; gl_TessLevelOuter[2] = 1.0;
gl_TessLevelInner[0] = 1.0;
}
)";
constexpr const char* tes = R"(#version 460 core
layout (triangles) in;
patch in int tcs_patch_result[16];
out vec4 tes_out;
void main() {
tes_out = vec4(float(tcs_patch_result[0] + tcs_patch_result[15]));
gl_Position = gl_in[0].gl_Position;
}
)";
const GLuint program = MakeProgram({{GL_VERTEX_SHADER, kPassthroughVs},
{GL_TESS_CONTROL_SHADER, tcs},
{GL_TESS_EVALUATION_SHADER, tes}},
/*separable=*/true);
LinkProgram(program);
const std::string log = LinkLog(program);
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE)
<< "a patch-qualified array must cross the TCS/TES boundary; if this says \"Array sizes "
"must be compatible\" the glslang fork pin has lost the isIoResizeArray patch guard.\n"
<< log;
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_NO_ERROR));
}
} // namespace
+70 -2
View File
@@ -827,10 +827,11 @@ TEST_F(QueryTest, DisableTimerQueryFeatureMatchesEnvironment) {
// --------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------
TEST_F(QueryTest, PipelineStatisticsTargetsAreAcceptedAndReportZeroCounterBits) { TEST_F(QueryTest, PipelineStatisticsTargetsAreAcceptedAndReportZeroCounterBits) {
// The NINE unconditional targets. The two tessellation ones are conditional on tessellation
// support and have their own test below.
static constexpr GLenum kTargets[] = { static constexpr GLenum kTargets[] = {
GL_VERTICES_SUBMITTED, GL_PRIMITIVES_SUBMITTED, GL_VERTICES_SUBMITTED, GL_PRIMITIVES_SUBMITTED,
GL_VERTEX_SHADER_INVOCATIONS, GL_TESS_CONTROL_SHADER_PATCHES, GL_VERTEX_SHADER_INVOCATIONS, GL_GEOMETRY_SHADER_INVOCATIONS,
GL_TESS_EVALUATION_SHADER_INVOCATIONS, GL_GEOMETRY_SHADER_INVOCATIONS,
GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED, GL_FRAGMENT_SHADER_INVOCATIONS, GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED, GL_FRAGMENT_SHADER_INVOCATIONS,
GL_COMPUTE_SHADER_INVOCATIONS, GL_CLIPPING_INPUT_PRIMITIVES, GL_COMPUTE_SHADER_INVOCATIONS, GL_CLIPPING_INPUT_PRIMITIVES,
GL_CLIPPING_OUTPUT_PRIMITIVES, GL_CLIPPING_OUTPUT_PRIMITIVES,
@@ -872,6 +873,73 @@ TEST_F(QueryTest, PipelineStatisticsTargetsAreAcceptedAndReportZeroCounterBits)
} }
} }
// GL_TESS_CONTROL_SHADER_PATCHES / GL_TESS_EVALUATION_SHADER_INVOCATIONS are the two
// pipeline-statistics targets ARB_pipeline_statistics_query makes CONDITIONAL on tessellation
// support, and the extension string is the only thing an application can read to decide whether
// an implementation has it. So the target and the string have to move together: accepting a
// tessellation-conditional token while withholding the string that announces the condition is
// self-contradictory, and the conformance suite catches exactly that contradiction
// (KHR-GL46.pipeline_statistics_query_tests_ARB.api_coverage_unsupported_calls demands
// GL_INVALID_ENUM for every target its own probe calls unsupported, and its probe for these two
// is `compatibility(4,0) || GL_ARB_tessellation_shader` - a CORE context fails the first half).
//
// Written against the advertisement rather than against today's answer on purpose: the day a
// backend starts emitting GL_ARB_tessellation_shader this test keeps passing and keeps pinning
// the coupling, and it fails loudly if only one of the two halves moves.
TEST_F(QueryTest, TessellationPipelineStatisticsTargetsFollowTheTessellationShaderAdvertisement) {
const auto* extensionsString =
reinterpret_cast<const char*>(MG_Impl::GLImpl::GetString(GL_EXTENSIONS));
ASSERT_NE(extensionsString, nullptr);
const Bool advertised = String(extensionsString).find("GL_ARB_tessellation_shader") != String::npos;
const GLenum expectedError = advertised ? GL_NO_ERROR : GL_INVALID_ENUM;
static constexpr GLenum kTessTargets[] = {
GL_TESS_CONTROL_SHADER_PATCHES,
GL_TESS_EVALUATION_SHADER_INVOCATIONS,
};
for (const GLenum target: kTessTargets) {
GLuint id = 0;
MG_Impl::GLImpl::GenQueries(1, &id);
ASSERT_NE(id, 0u);
MG_Impl::GLImpl::BeginQuery(target, id);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), expectedError)
<< "glBeginQuery on tessellation pipeline-statistics target 0x" << std::hex << target
<< " must agree with the GL_ARB_tessellation_shader advertisement";
if (advertised) {
MG_Impl::GLImpl::EndQuery(target);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint counterBits = -1;
MG_Impl::GLImpl::GetQueryiv(target, GL_QUERY_COUNTER_BITS, &counterBits);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(counterBits, 0);
} else {
// Refused at glEndQuery too, not just at glBeginQuery: a target the implementation
// does not have is not half-accepted.
MG_Impl::GLImpl::EndQuery(target);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_ENUM);
// GL_QUERY_COUNTER_BITS still answers the honest zero rather than an error - the
// getter has never validated its target, and zero is what "no such counter" reads as
// (GL 4.6 core 4.2.1), so the refusal costs no information.
GLint counterBits = -1;
MG_Impl::GLImpl::GetQueryiv(target, GL_QUERY_COUNTER_BITS, &counterBits);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(counterBits, 0);
// And GL_CURRENT_QUERY reads as "no query" rather than tracking a slot that was
// never opened.
GLint current = -1;
MG_Impl::GLImpl::GetQueryiv(target, GL_CURRENT_QUERY, &current);
EXPECT_EQ(current, 0);
}
MG_Impl::GLImpl::DeleteQueries(1, &id);
while (MG_Impl::GLImpl::GetError() != GL_NO_ERROR) {
}
}
}
// The negative half the conformance case actually asserts: an object already latched onto one // The negative half the conformance case actually asserts: an object already latched onto one
// pipeline-statistics target must refuse a different one with GL_INVALID_OPERATION. This is what // pipeline-statistics target must refuse a different one with GL_INVALID_OPERATION. This is what
// per-target active slots buy - a single shared slot would have reported "a query is already // per-target active slots buy - a single shared slot would have reported "a query is already
@@ -984,6 +984,20 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_OES_viewport_array") == 0) { if (std::strcmp(extension, "GL_OES_viewport_array") == 0) {
caps.SupportsViewportArray = true; caps.SupportsViewportArray = true;
} }
// EXT wins where both are advertised: it is the spelling the Android Extension
// Pack mandates, so it is the one a driver is most likely to have tested.
if (std::strcmp(extension, "GL_EXT_tessellation_point_size") == 0) {
caps.TessellationPointSizeSupport = MG_External::GLESCapabilities::PointSizeTier::ExtensionEXT;
} else if (std::strcmp(extension, "GL_OES_tessellation_point_size") == 0 &&
caps.TessellationPointSizeSupport == MG_External::GLESCapabilities::PointSizeTier::None) {
caps.TessellationPointSizeSupport = MG_External::GLESCapabilities::PointSizeTier::ExtensionOES;
}
if (std::strcmp(extension, "GL_EXT_geometry_point_size") == 0) {
caps.GeometryPointSizeSupport = MG_External::GLESCapabilities::PointSizeTier::ExtensionEXT;
} else if (std::strcmp(extension, "GL_OES_geometry_point_size") == 0 &&
caps.GeometryPointSizeSupport == MG_External::GLESCapabilities::PointSizeTier::None) {
caps.GeometryPointSizeSupport = MG_External::GLESCapabilities::PointSizeTier::ExtensionOES;
}
} }
} }
// The pointer check on top of the extension check makes each flag sufficient on its own // The pointer check on top of the extension check makes each flag sufficient on its own
@@ -1055,6 +1069,19 @@ namespace MobileGL::MG_Util::BackendLoader {
MGLOG_I(" clip distances (EXT_clip_cull_distance): %s", caps.SupportsClipDistance ? "yes" : "no"); MGLOG_I(" clip distances (EXT_clip_cull_distance): %s", caps.SupportsClipDistance ? "yes" : "no");
MGLOG_I(" viewport array (OES_viewport_array; gl_ViewportIndex collapses to viewport 0 when absent): %s", MGLOG_I(" viewport array (OES_viewport_array; gl_ViewportIndex collapses to viewport 0 when absent): %s",
caps.SupportsViewportArray ? "yes" : "no"); caps.SupportsViewportArray ? "yes" : "no");
{
const auto pointSizeTierName = [](MG_External::GLESCapabilities::PointSizeTier tier) {
switch (tier) {
case MG_External::GLESCapabilities::PointSizeTier::ExtensionEXT: return "EXT";
case MG_External::GLESCapabilities::PointSizeTier::ExtensionOES: return "OES";
default: return "no";
}
};
MGLOG_I(" tessellation gl_PointSize (EXT/OES_tessellation_point_size): %s",
pointSizeTierName(caps.TessellationPointSizeSupport));
MGLOG_I(" geometry gl_PointSize (EXT/OES_geometry_point_size): %s",
pointSizeTierName(caps.GeometryPointSizeSupport));
}
// LOAD-BEARING STRING, not just a banner. android-plugin/trace-replay-ci.sh's // LOAD-BEARING STRING, not just a banner. android-plugin/trace-replay-ci.sh's
// is_angle_surface_lost() greps mobilegl.log for exactly "OpenGL ES capabilities:" to // is_angle_surface_lost() greps mobilegl.log for exactly "OpenGL ES capabilities:" to
@@ -1118,6 +1118,23 @@ namespace MobileGL {
ExtensionOES, // GL_OES_texture_buffer; ESSL below 320 must say GL_OES_texture_buffer ExtensionOES, // GL_OES_texture_buffer; ESSL below 320 must say GL_OES_texture_buffer
}; };
TextureBufferTier TextureBufferSupport = TextureBufferTier::None; TextureBufferTier TextureBufferSupport = TextureBufferTier::None;
// Which spelling of per-vertex point size a NON-VERTEX stage has, if any. In desktop
// GL gl_PointSize is an ordinary gl_PerVertex member that any vertex-processing stage
// may write and any program may capture by name; in ESSL it does not EXIST in a
// tessellation or geometry stage until GL_EXT/OES_tessellation_point_size (resp.
// ..._geometry_point_size) is requested - not even at 320, where the stages
// themselves are core. SPIRV-Cross prints the identifier bare and asks for nothing,
// exactly as it does for gl_ViewportIndex, so the directive has to be inserted into
// the emitted source (RequestPointSizeExtension) and a driver with neither spelling
// cannot compile such a stage at all. Extension string only: these add no entry
// points, so there is no pointer to require.
enum class PointSizeTier : Uint8 {
None = 0, // neither spelling; the stage cannot name gl_PointSize
ExtensionEXT, // GL_EXT_tessellation_point_size / GL_EXT_geometry_point_size
ExtensionOES, // GL_OES_tessellation_point_size / GL_OES_geometry_point_size
};
PointSizeTier TessellationPointSizeSupport = PointSizeTier::None;
PointSizeTier GeometryPointSizeSupport = PointSizeTier::None;
// GL_MAX_TEXTURE_BUFFER_SIZE actually came back from the driver. False means the value // GL_MAX_TEXTURE_BUFFER_SIZE actually came back from the driver. False means the value
// below is MobileGL's own floor, not a driver answer: the pname is only legal once // below is MobileGL's own floor, not a driver answer: the pname is only legal once
// buffer textures exist, and querying it on a driver without them raises // buffer textures exist, and querying it on a driver without them raises
@@ -9,6 +9,7 @@
#include "Loader.h" #include "Loader.h"
#include <Config.h> #include <Config.h>
#include <algorithm>
#include <cmath> #include <cmath>
#include <limits> #include <limits>
@@ -177,7 +178,15 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxFramebufferSamples = ResolveConservativeFramebufferSampleLimit(p.limits); caps.MaxFramebufferSamples = ResolveConservativeFramebufferSampleLimit(p.limits);
caps.MaxIntegerSamples = MaxSampleCountFromFlags(p.limits.sampledImageIntegerSampleCounts); caps.MaxIntegerSamples = MaxSampleCountFromFlags(p.limits.sampledImageIntegerSampleCounts);
caps.MaxSamples = caps.MaxFramebufferSamples; caps.MaxSamples = caps.MaxFramebufferSamples;
caps.MaxSampleMaskWords = SaturateToInt(p.limits.maxSampleMaskWords); // Clamped to one word, exactly as the GLES loader clamps the driver's value and for the
// same reason: MobileGL's sample-mask state IS a single 32-bit word
// (RenderState::SampleMaskValue) and SampleMaski_State() raises GL_INVALID_VALUE for any
// maskNumber other than 0. dEQP's per-case gluStateReset issues glSampleMaski up to
// GL_MAX_SAMPLE_MASK_WORDS, so advertising a device's real 2 would abort the whole glcts
// process after every single case - the failure da6f75dbd added the GLES clamp to stop,
// reproduced on this backend. One word is the spec minimum and therefore always legal.
// It is also what PipelineCreatePayload::sampleMask is sized for.
caps.MaxSampleMaskWords = std::min(SaturateToInt(p.limits.maxSampleMaskWords), 1);
caps.MaxTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages); caps.MaxTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages);
caps.MaxVertexTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages); caps.MaxVertexTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages);
caps.MaxComputeTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages); caps.MaxComputeTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages);
@@ -300,7 +309,15 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxFramebufferSamples = ResolveConservativeFramebufferSampleLimit(properties.limits); caps.MaxFramebufferSamples = ResolveConservativeFramebufferSampleLimit(properties.limits);
caps.MaxIntegerSamples = MaxSampleCountFromFlags(properties.limits.sampledImageIntegerSampleCounts); caps.MaxIntegerSamples = MaxSampleCountFromFlags(properties.limits.sampledImageIntegerSampleCounts);
caps.MaxSamples = caps.MaxFramebufferSamples; caps.MaxSamples = caps.MaxFramebufferSamples;
caps.MaxSampleMaskWords = SaturateToInt(properties.limits.maxSampleMaskWords); // Clamped to one word, exactly as the GLES loader clamps the driver's value and for the
// same reason: MobileGL's sample-mask state IS a single 32-bit word
// (RenderState::SampleMaskValue) and SampleMaski_State() raises GL_INVALID_VALUE for any
// maskNumber other than 0. dEQP's per-case gluStateReset issues glSampleMaski up to
// GL_MAX_SAMPLE_MASK_WORDS, so advertising a device's real 2 would abort the whole glcts
// process after every single case - the failure da6f75dbd added the GLES clamp to stop,
// reproduced on this backend. One word is the spec minimum and therefore always legal.
// It is also what PipelineCreatePayload::sampleMask is sized for.
caps.MaxSampleMaskWords = std::min(SaturateToInt(properties.limits.maxSampleMaskWords), 1);
caps.MaxTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages); caps.MaxTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages);
caps.MaxVertexTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages); caps.MaxVertexTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages);
caps.MaxComputeTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages); caps.MaxComputeTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages);
@@ -758,6 +758,63 @@ namespace MobileGL {
return false; return false;
} }
Bool ShaderCompiler::ModuleDeclaresTransformFeedback(const Vector<Uint32>& spirv) {
if (spirv.empty()) {
return false;
}
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleDeclaresTransformFeedback"),
spirv.data(), spirv.size());
if (!context) {
// Unparseable is not a capture verdict; say no, which makes the caller decline
// the span rather than issue transform-feedback commands against it.
return false;
}
// The exact question VUID-vkCmdBeginTransformFeedbackEXT-None-04128 asks of the
// bound pipeline's last pre-rasterization stage: was it declared with the Xfb
// execution mode. Reading the execution modes rather than the TransformFeedback
// capability because the capability can legally be declared by a module that has
// no Xfb entry point, and the VUID is about the mode.
for (const spvtools::opt::Instruction& mode : context->module()->execution_modes()) {
if (mode.NumInOperands() >= 2 &&
static_cast<spv::ExecutionMode>(mode.GetSingleWordInOperand(1)) ==
spv::ExecutionMode::Xfb) {
return true;
}
}
return false;
}
Bool ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(const Vector<Uint32>& spirv) {
if (spirv.empty()) {
return false;
}
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
SPV_ENV_VULKAN_1_1,
MakeSpirvMessageConsumer("ModuleDeclaresTessellationOrGeometryPointSize"), spirv.data(),
spirv.size());
if (!context) {
// Unparseable is not a verdict about point size. Say no, so the caller keeps
// building the program: the module is already broken for other reasons and
// the diagnostics that own that failure are better placed than this one.
return false;
}
// The CAPABILITY, not the BuiltIn decoration, because the capability is exactly
// what the feature gates: a module may declare gl_PerVertex with a PointSize
// member and never access it, and glslang then emits no capability
// (GlslangToSpv defers it to actual use) - such a module is legal without the
// feature and must not be declined.
for (const spvtools::opt::Instruction& capability : context->capabilities()) {
if (capability.NumInOperands() < 1) continue;
const auto declared = static_cast<spv::Capability>(capability.GetSingleWordInOperand(0));
if (declared == spv::Capability::TessellationPointSize ||
declared == spv::Capability::GeometryPointSize) {
return true;
}
}
return false;
}
Bool ShaderCompiler::ModuleDeclaresFloat64(const Vector<Uint32>& spirv) { Bool ShaderCompiler::ModuleDeclaresFloat64(const Vector<Uint32>& spirv) {
if (spirv.empty()) { if (spirv.empty()) {
// Same reasoning as ModuleDeclaresBufferTextureSampler: a stage that produced // Same reasoning as ModuleDeclaresBufferTextureSampler: a stage that produced
@@ -532,6 +532,21 @@ namespace MobileGL {
// check exists so that failure can be reported as the missing capability it is, // check exists so that failure can be reported as the missing capability it is,
// naming the shader, rather than as a driver info log nobody sees. // naming the shader, rather than as a driver info log nobody sees.
static Bool ModuleDeclaresBufferTextureSampler(const Vector<Uint32>& spirv); static Bool ModuleDeclaresBufferTextureSampler(const Vector<Uint32>& spirv);
// Does this module carry the Xfb execution mode - i.e. would a
// vkCmdBeginTransformFeedbackEXT against a pipeline whose last pre-rasterization
// stage is this module satisfy VUID-vkCmdBeginTransformFeedbackEXT-None-04128?
// Asked of the FINAL bytes, so it answers for whatever the backend transform
// chain actually produced rather than for what it was asked to produce.
static Bool ModuleDeclaresTransformFeedback(const Vector<Uint32>& spirv);
// Does this module declare TessellationPointSize or GeometryPointSize - i.e. does
// it need VkPhysicalDeviceFeatures::shaderTessellationAndGeometryPointSize before
// a pipeline built from it is legal usage (VUID-RuntimeSpirv-PointSize-06439)?
// glslang emits either capability from any access to the PointSize built-in in a
// tessellation or geometry stage, which desktop GL treats as an ordinary
// per-vertex output, so a program that is perfectly legal in GL can need a Vulkan
// feature the device does not have. Callers only ask when the feature is OFF, so
// the module parse costs nothing on a device that has it.
static Bool ModuleDeclaresTessellationOrGeometryPointSize(const Vector<Uint32>& spirv);
// True when the module still declares a 64-bit float type. After // True when the module still declares a 64-bit float type. After
// SanitizeAndOptimizeBinary that can only mean DemoteFloat64Pass declined the // SanitizeAndOptimizeBinary that can only mean DemoteFloat64Pass declined the