mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
Compare commits
16
Commits
08419a1fe6
...
81b17c0b75
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81b17c0b75 | ||
|
|
d1edf765f5 | ||
|
|
97e07190ac | ||
|
|
a4dcdf989e | ||
|
|
1c113e4b26 | ||
|
|
bf9cfb3079 | ||
|
|
e1818d497a | ||
|
|
d7f66722d1 | ||
|
|
92dc41ebf9 | ||
|
|
feea131d8b | ||
|
|
19f4402fbf | ||
|
|
1350031368 | ||
|
|
0ee3384b22 | ||
|
|
ba3f8d6774 | ||
|
|
3327784fd0 | ||
|
|
ff426da3a9 |
@@ -307,6 +307,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemotePointSizePass.cpp
|
||||||
|
|
||||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||||
@@ -314,6 +315,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
|
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
|
||||||
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
||||||
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
||||||
|
MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp
|
||||||
|
|
||||||
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
|
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
|
||||||
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
||||||
|
|||||||
@@ -152,6 +152,19 @@ namespace MobileGL::MG_Config {
|
|||||||
// lavapipe carry a located block correctly and would otherwise never run this code -
|
// lavapipe carry a located block correctly and would otherwise never run this code -
|
||||||
// and ForceOff is the negative control. See StripIoBlockLocationsPass.
|
// and ForceOff is the negative control. See StripIoBlockLocationsPass.
|
||||||
QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto;
|
QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto;
|
||||||
|
// MOBILEGL_POINT_SIZE_DEMOTION: demote gl_PointSize out of tessellation/geometry
|
||||||
|
// stages into an ordinary varying (ShaderCompiler::
|
||||||
|
// DemoteTessellationGeometryPointSizeForProgram) instead of declining such programs
|
||||||
|
// on a device that advertises neither EXT/OES_tessellation_point_size /
|
||||||
|
// geometry_point_size (DirectGLES) nor shaderTessellationAndGeometryPointSize
|
||||||
|
// (DirectVulkan). Auto arms it exactly where the detection says the capability is
|
||||||
|
// absent, which is the right setting everywhere. ForceOn exists so the demotion can
|
||||||
|
// be exercised on a healthy driver - llvmpipe and lavapipe host the built-in
|
||||||
|
// natively and would otherwise never run this code, which is what the pinned
|
||||||
|
// integration lane uses - and ForceOff restores the plain declines (escape hatch /
|
||||||
|
// negative control). Cross-backend by design: the demotion runs in the shared
|
||||||
|
// phase-B chain, so one switch covers both. See DemotePointSizePass.
|
||||||
|
QuirkOverride PointSizeDemotion = QuirkOverride::Auto;
|
||||||
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
|
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
|
||||||
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
|
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
|
||||||
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
|
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
|
||||||
@@ -184,6 +197,15 @@ namespace MobileGL::MG_Config {
|
|||||||
// on Mali both the immediate glBufferSubData and a staged copy into a busy
|
// on Mali both the immediate glBufferSubData and a staged copy into a busy
|
||||||
// mutable store ghost the whole destination on the CPU.
|
// mutable store ghost the whole destination on the CPU.
|
||||||
Bool EsprytDisableInvalidateFlush = false;
|
Bool EsprytDisableInvalidateFlush = false;
|
||||||
|
// MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION: keep mesh-arena-sized buffer stores
|
||||||
|
// (>= 16MiB) on the CPU-shadow model instead of backing them with the backend's
|
||||||
|
// persistently+coherently mapped storage at definition time (negative control /
|
||||||
|
// escape hatch). Frontend-scoped: it engages only where the active backend
|
||||||
|
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
|
||||||
|
// 128MB arena is a plain memcpy into GPU-visible memory; every driver-mediated
|
||||||
|
// route for the same write stalls the thread or ghost-copies the whole arena on
|
||||||
|
// this class of Mali driver, and the arena stops costing its size again in RAM.
|
||||||
|
Bool DisableLargeBufferAdoption = false;
|
||||||
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
|
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
|
||||||
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
|
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
|
||||||
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
|
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
|
||||||
@@ -278,6 +300,22 @@ namespace MobileGL::MG_Config {
|
|||||||
// negative control that replays the corruption. Costs 2x the memory of the affected
|
// negative control that replays the corruption. Costs 2x the memory of the affected
|
||||||
// formats where it engages, which is why Auto is probe-gated rather than always-on.
|
// formats where it engages, which is why Auto is probe-gated rather than always-on.
|
||||||
QuirkOverride EsprytWidenPacked16Storage = QuirkOverride::Auto;
|
QuirkOverride EsprytWidenPacked16Storage = QuirkOverride::Auto;
|
||||||
|
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE: DirectVulkan's GL_PRIMITIVES_GENERATED
|
||||||
|
// reroute for draws made while transform feedback is INACTIVE. The stream query
|
||||||
|
// (VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT primitivesNeeded) is defined to count
|
||||||
|
// them, but a Mali driver - and Mesa lavapipe - answers 0 unless a capture span is
|
||||||
|
// open, which is exactly the shape the CTS uses to measure the tessellator, so ~29
|
||||||
|
// tessellation tests per tree size a capture buffer from the 0 and die on the
|
||||||
|
// zero-length map. Auto defers to a device probe at renderer bring-up
|
||||||
|
// (SelfTest::RunPrimitivesGeneratedNoXfbProbe), which measures two substitutes on
|
||||||
|
// the same capture-less draws and arms the best proven one: the dedicated
|
||||||
|
// VK_EXT_primitives_generated_query (exact semantics by definition; lavapipe passes
|
||||||
|
// it, rasterizer discard included), else a clipping-invocations pipeline-statistics
|
||||||
|
// pool (see the verdict vocabulary for its rasterizer-discard split). ForceOn pins
|
||||||
|
// the reroute structurally wherever a pool can exist (the arming-observable lane,
|
||||||
|
// immune to the probe's verdict moving), and ForceOff is the negative control that
|
||||||
|
// replays the driver's silence.
|
||||||
|
QuirkOverride MagmaPrimGenQueryReroute = QuirkOverride::Auto;
|
||||||
};
|
};
|
||||||
extern FeaturesTable Features;
|
extern FeaturesTable Features;
|
||||||
} // namespace MobileGL::MG_Config
|
} // namespace MobileGL::MG_Config
|
||||||
|
|||||||
@@ -181,12 +181,14 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||||
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
|
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
|
||||||
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
|
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
|
||||||
|
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
|
||||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||||
features.EsprytDisableUboRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UBO_RING");
|
features.EsprytDisableUboRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UBO_RING");
|
||||||
features.EsprytDisableUnpackRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UNPACK_RING");
|
features.EsprytDisableUnpackRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UNPACK_RING");
|
||||||
features.EsprytDisableUploadRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING");
|
features.EsprytDisableUploadRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING");
|
||||||
features.EsprytDisableInvalidateFlush = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH");
|
features.EsprytDisableInvalidateFlush = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH");
|
||||||
|
features.DisableLargeBufferAdoption = QueryEnvFlag("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION");
|
||||||
features.EsprytForceDepthStencilReadbackEmulation =
|
features.EsprytForceDepthStencilReadbackEmulation =
|
||||||
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
||||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||||
@@ -204,6 +206,7 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION");
|
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION");
|
||||||
features.EsprytWidenPacked16Storage =
|
features.EsprytWidenPacked16Storage =
|
||||||
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
|
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
|
||||||
|
features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE");
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void InitBackendType() {
|
inline void InitBackendType() {
|
||||||
|
|||||||
@@ -495,6 +495,24 @@ namespace MobileGL {
|
|||||||
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
|
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
|
||||||
// still see one consistent world.
|
// still see one consistent world.
|
||||||
Bool SupportsFloat64VertexAttributes = false;
|
Bool SupportsFloat64VertexAttributes = false;
|
||||||
|
// Whether a TESSELLATION stage of this backend may access gl_PointSize - i.e.
|
||||||
|
// whether a module declaring OpCapability TessellationPointSize can reach the
|
||||||
|
// driver at all. DirectVulkan sets both this and the geometry twin from the one
|
||||||
|
// shaderTessellationAndGeometryPointSize feature; DirectGLES sets them
|
||||||
|
// independently from the EXT/OES_tessellation_point_size /
|
||||||
|
// geometry_point_size extension pairs (PointSizeTier), which really do come
|
||||||
|
// separately. When absent, ProgramSpirvTask demotes the built-in to an ordinary
|
||||||
|
// varying program-wide (ShaderCompiler::
|
||||||
|
// DemoteTessellationGeometryPointSizeForProgram); MOBILEGL_POINT_SIZE_DEMOTION
|
||||||
|
// overrides the detection in either direction at backend init.
|
||||||
|
//
|
||||||
|
// Defaults TRUE, deliberately against the house "assume absent" rule: false
|
||||||
|
// ARMS a rewrite, so the conservative no-backend answer (standalone compiles,
|
||||||
|
// unit tests) is the one that leaves modules untouched. A backend that never
|
||||||
|
// sets it gets standard modules and, at worst, the old honest declines.
|
||||||
|
Bool SupportsTessellationPointSize = true;
|
||||||
|
// The geometry-stage twin (OpCapability GeometryPointSize).
|
||||||
|
Bool SupportsGeometryPointSize = true;
|
||||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||||
Uint32 SubgroupSize = 0;
|
Uint32 SubgroupSize = 0;
|
||||||
Uint32 SubgroupSupportedStages = 0;
|
Uint32 SubgroupSupportedStages = 0;
|
||||||
|
|||||||
@@ -1479,6 +1479,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
|
// Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
|
||||||
// fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
|
// fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
|
||||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||||
|
// Whether a tessellation / geometry stage's ESSL may name gl_PointSize at all: the two
|
||||||
|
// extension pairs the loader probed, independently, because they really do come
|
||||||
|
// separately. False arms the shared phase-B demotion
|
||||||
|
// (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram), whose ESSL then
|
||||||
|
// never names the built-in in those stages and needs no extension.
|
||||||
|
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends both are absent so the demotion can be
|
||||||
|
// exercised on a healthy driver (the pinned integration lane); =0 restores the
|
||||||
|
// detected answer's declines.
|
||||||
|
m_dynamicParameters.SupportsTessellationPointSize =
|
||||||
|
m_GLESCapabilities.TessellationPointSizeSupport !=
|
||||||
|
MG_External::GLESCapabilities::PointSizeTier::None;
|
||||||
|
m_dynamicParameters.SupportsGeometryPointSize =
|
||||||
|
m_GLESCapabilities.GeometryPointSizeSupport !=
|
||||||
|
MG_External::GLESCapabilities::PointSizeTier::None;
|
||||||
|
switch (MG_Config::Features.PointSizeDemotion) {
|
||||||
|
case MG_Config::QuirkOverride::ForceOn:
|
||||||
|
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
|
||||||
|
"gl_PointSize as unhosted so the demotion runs on this driver");
|
||||||
|
m_dynamicParameters.SupportsTessellationPointSize = false;
|
||||||
|
m_dynamicParameters.SupportsGeometryPointSize = false;
|
||||||
|
break;
|
||||||
|
case MG_Config::QuirkOverride::ForceOff:
|
||||||
|
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
|
||||||
|
"plain declines regardless of the driver's extensions");
|
||||||
|
m_dynamicParameters.SupportsTessellationPointSize = true;
|
||||||
|
m_dynamicParameters.SupportsGeometryPointSize = true;
|
||||||
|
break;
|
||||||
|
case MG_Config::QuirkOverride::Auto:
|
||||||
|
break;
|
||||||
|
}
|
||||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||||
|
|||||||
@@ -783,6 +783,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
resource.storageInitialized = true;
|
resource.storageInitialized = true;
|
||||||
resource.pendingRespecify = false;
|
resource.pendingRespecify = false;
|
||||||
resource.pendingRanges.clear();
|
resource.pendingRanges.clear();
|
||||||
|
resource.pendingResidentWrites.clear();
|
||||||
resource.syncedChangeSerial = bufferObject.GetChangeSerial();
|
resource.syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||||
// A GROWN store keeps its indexed bindings, and BindBufferBaseCached skips a
|
// A GROWN store keeps its indexed bindings, and BindBufferBaseCached skips a
|
||||||
// rebind whenever the shadow already records this id at that index - so on a
|
// rebind whenever the shadow already records this id at that index - so on a
|
||||||
@@ -828,6 +829,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return RingAvailable(g_uploadRing);
|
return RingAvailable(g_uploadRing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A partial range below this goes through the staging ring instead of a
|
||||||
|
// range-invalidating map: the map's page-substitution fast path needs a
|
||||||
|
// sizeable (page-coverable) range to engage, and below it the driver
|
||||||
|
// falls back to waiting out the WAR hazard on the CPU.
|
||||||
|
constexpr SizeT kInvalidateRangeMinBytes = 128u * 1024u;
|
||||||
|
|
||||||
// Push every queued range of `resource` from the shadow into the backend
|
// Push every queued range of `resource` from the shadow into the backend
|
||||||
// store, without ever letting a driver resolve the WAR hazard against
|
// store, without ever letting a driver resolve the WAR hazard against
|
||||||
// in-flight frames at the WHOLE BUFFER's expense. Three tiers:
|
// in-flight frames at the WHOLE BUFFER's expense. Three tiers:
|
||||||
@@ -879,11 +886,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const SizeT start = std::min(range.start, end);
|
const SizeT start = std::min(range.start, end);
|
||||||
const SizeT size = end - start;
|
const SizeT size = end - start;
|
||||||
if (size == 0) continue;
|
if (size == 0) continue;
|
||||||
if (mapUsable) {
|
// The invalidating map's fast path is SHAPE-dependent on this Mali
|
||||||
|
// driver: a whole-buffer invalidation renames the store outright,
|
||||||
|
// and a large range gets fresh pages - but a small unaligned range
|
||||||
|
// of a busy store makes the map WAIT (osup_sync_object_wait, ~9%
|
||||||
|
// of a Minecraft 26.3 replay). So: whole buffer -> orphan-map;
|
||||||
|
// large range -> range-invalidating map; small range -> the staged
|
||||||
|
// ring copy, whose worst case (a whole-destination ghost) is only
|
||||||
|
// ever the small destination itself.
|
||||||
|
//
|
||||||
|
// The map covers EXACTLY the queued range: only those bytes are the
|
||||||
|
// shadow's to rewrite. Widening to page bounds looked free and was
|
||||||
|
// not - the widened bytes clobbered GPU-written data (an SSBO
|
||||||
|
// counter beside the app's SubData) with the stale shadow.
|
||||||
|
const Bool wholeBuffer = start == 0 && end == limit && limit == resource.storageSize;
|
||||||
|
if (mapUsable && (wholeBuffer || size >= kInvalidateRangeMinBytes)) {
|
||||||
BindBufferId(TempBufferTarget, resource.id);
|
BindBufferId(TempBufferTarget, resource.id);
|
||||||
|
const GLbitfield access =
|
||||||
|
GL_MAP_WRITE_BIT |
|
||||||
|
(wholeBuffer ? GL_MAP_INVALIDATE_BUFFER_BIT : GL_MAP_INVALIDATE_RANGE_BIT);
|
||||||
void* dst = g_GLESFuncs.glMapBufferRange(TempBufferTarget, (GLintptr)start,
|
void* dst = g_GLESFuncs.glMapBufferRange(TempBufferTarget, (GLintptr)start,
|
||||||
(GLsizeiptr)size,
|
(GLsizeiptr)size, access);
|
||||||
GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT);
|
|
||||||
if (dst) {
|
if (dst) {
|
||||||
Memcpy(dst, bufferObject.MappedData() + start, size);
|
Memcpy(dst, bufferObject.MappedData() + start, size);
|
||||||
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
|
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
|
||||||
@@ -904,6 +927,49 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Land the app bytes queued for an ADOPTED store on the GPU timeline: staged
|
||||||
|
// into the upload ring and delivered by glCopyBufferSubData. The destination
|
||||||
|
// is the IMMUTABLE persistent store, which the driver can neither rename nor
|
||||||
|
// ghost, so the copy is plain job ordering - after every in-flight reader,
|
||||||
|
// before the next consumer - which is exactly glBufferSubData's contract.
|
||||||
|
// (The in-place host write these bytes replaced tore the frames still
|
||||||
|
// reading the old vertex data: one-frame wrong geometry during fast camera
|
||||||
|
// movement.) Fallback: direct glBufferSubData - the adopted store carries
|
||||||
|
// DYNAMIC_STORAGE, and immutability again forbids the whole-store ghost.
|
||||||
|
void DrainResidentWritesNow(GLESBufferResource& resource, BufferObject& bufferObject) {
|
||||||
|
#ifdef TRACY_ENABLE
|
||||||
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
|
#endif
|
||||||
|
Vector<GLESBufferResource::PendingResidentWrite> writes;
|
||||||
|
{
|
||||||
|
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
|
||||||
|
if (resource.pendingResidentWrites.empty()) return;
|
||||||
|
writes = std::move(resource.pendingResidentWrites);
|
||||||
|
resource.pendingResidentWrites.clear();
|
||||||
|
}
|
||||||
|
const SizeT limit = resource.storageSize;
|
||||||
|
const Bool ringUsable = UploadRingUsableNow();
|
||||||
|
for (const auto& write : writes) {
|
||||||
|
if (write.offset >= limit) continue;
|
||||||
|
const SizeT size = std::min(write.bytes.size(), limit - write.offset);
|
||||||
|
if (size == 0) continue;
|
||||||
|
SizeT ringOffset = 0;
|
||||||
|
if (ringUsable && size <= kUploadRingMaxBytes &&
|
||||||
|
RingAllocate(g_uploadRing, size, ringOffset)) {
|
||||||
|
Memcpy(g_uploadRing.store.mappedPtr + ringOffset, write.bytes.data(), size);
|
||||||
|
BindBufferId(GL_COPY_READ_BUFFER, g_uploadRing.store.id);
|
||||||
|
BindBufferId(GL_COPY_WRITE_BUFFER, resource.id);
|
||||||
|
g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER,
|
||||||
|
(GLintptr)ringOffset, (GLintptr)write.offset,
|
||||||
|
(GLsizeiptr)size);
|
||||||
|
} else {
|
||||||
|
BindBufferId(TempBufferTarget, resource.id);
|
||||||
|
g_GLESFuncs.glBufferSubData(TempBufferTarget, (GLintptr)write.offset, (GLsizeiptr)size,
|
||||||
|
write.bytes.data());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// EXT_buffer_storage bit values (same numeric values as the desktop ARB
|
// EXT_buffer_storage bit values (same numeric values as the desktop ARB
|
||||||
// tokens); defined locally so this compiles regardless of which GLES headers
|
// tokens); defined locally so this compiles regardless of which GLES headers
|
||||||
// expose the EXT tokens.
|
// expose the EXT tokens.
|
||||||
@@ -989,6 +1055,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
{
|
{
|
||||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||||
resource->pendingRanges.clear();
|
resource->pendingRanges.clear();
|
||||||
|
resource->pendingResidentWrites.clear();
|
||||||
}
|
}
|
||||||
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||||
return ptr;
|
return ptr;
|
||||||
@@ -1026,12 +1093,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
resource->storageSize = 0;
|
resource->storageSize = 0;
|
||||||
resource->pendingRespecify = true;
|
resource->pendingRespecify = true;
|
||||||
resource->pendingRanges.clear();
|
resource->pendingRanges.clear();
|
||||||
|
resource->pendingResidentWrites.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!CanTouchGLNow() || resource->id == 0 ||
|
if (!CanTouchGLNow() || resource->id == 0 ||
|
||||||
resource->contextGeneration != g_bufferContextGeneration) {
|
resource->contextGeneration != g_bufferContextGeneration) {
|
||||||
resource->pendingRespecify = true;
|
resource->pendingRespecify = true;
|
||||||
resource->pendingRanges.clear();
|
resource->pendingRanges.clear();
|
||||||
|
resource->pendingResidentWrites.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (bufferObject.GetSize() == 0) {
|
if (bufferObject.GetSize() == 0) {
|
||||||
@@ -1039,6 +1108,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
resource->storageSize = 0;
|
resource->storageSize = 0;
|
||||||
resource->pendingRespecify = false;
|
resource->pendingRespecify = false;
|
||||||
resource->pendingRanges.clear();
|
resource->pendingRanges.clear();
|
||||||
|
resource->pendingResidentWrites.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
RespecifyStorageNow(*resource, bufferObject);
|
RespecifyStorageNow(*resource, bufferObject);
|
||||||
@@ -1055,17 +1125,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
resource->pendingRanges.Add({offset, offset + size});
|
resource->pendingRanges.Add({offset, offset + size});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// An adopted zero-copy persistent store already HAS the bytes (the
|
||||||
|
// frontend wrote them through the coherent mapping); a driver upload
|
||||||
|
// here would be a self-copy that re-synchronizes what coherent mapping
|
||||||
|
// made free.
|
||||||
|
if (resource->persistentMapped && resource->persistentPtr) {
|
||||||
|
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// An immediate glBufferSubData resolves the WAR hazard against frames
|
// An immediate glBufferSubData resolves the WAR hazard against frames
|
||||||
// still referencing this store on the CPU on some drivers - Mali parks
|
// still referencing this store on the CPU on some drivers - Mali parks
|
||||||
// the thread in osup_sync_object_wait until every referencing job
|
// the thread in osup_sync_object_wait until every referencing job
|
||||||
// retires, which serialized Minecraft 26.3's per-frame UBO/chunk-mesh
|
// retires, which serialized Minecraft 26.3's per-frame UBO/chunk-mesh
|
||||||
// update streams into ~1 fps. Queue the range instead (the shadow
|
// update streams into ~1 fps. Queue the range instead (the shadow
|
||||||
// already holds the bytes) and let draw-time sync push the merged
|
// already holds the bytes) and let draw-time sync push the merged
|
||||||
// ranges through the staging ring. The zero-copy persistent store
|
// ranges through the staging ring.
|
||||||
// keeps the legacy immediate upload: draw-time sync never flushes
|
if (MG_Config::Features.EsprytDisableUploadRing) {
|
||||||
// ranges for it, and its mapping publishes writes by itself.
|
|
||||||
if ((resource->persistentMapped && resource->persistentPtr) ||
|
|
||||||
MG_Config::Features.EsprytDisableUploadRing) {
|
|
||||||
UploadRangeNow(*resource, bufferObject, offset, offset + size);
|
UploadRangeNow(*resource, bufferObject, offset, offset + size);
|
||||||
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||||
return;
|
return;
|
||||||
@@ -1074,6 +1149,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
resource->pendingRanges.Add({offset, offset + size});
|
resource->pendingRanges.Add({offset, offset + size});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// App bytes for an ADOPTED store: queue them untouched-by-the-mapping; the
|
||||||
|
// draw-time sync (or a readback) lands them GPU-ordered through
|
||||||
|
// DrainResidentWritesNow. No GL here, so the op is thread-agnostic.
|
||||||
|
void Ops_ResidentSubData(BufferObject& bufferObject, SizeT offset, DataPtr data) {
|
||||||
|
auto* resource = ResourceOf(bufferObject);
|
||||||
|
if (!resource || data.size == 0) return;
|
||||||
|
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||||
|
auto& write = resource->pendingResidentWrites.emplace_back();
|
||||||
|
write.offset = offset;
|
||||||
|
const auto* bytes = static_cast<const Uint8*>(data.data);
|
||||||
|
write.bytes.assign(bytes, bytes + data.size);
|
||||||
|
}
|
||||||
|
|
||||||
void Ops_FlushMappedRange(BufferObject& bufferObject, Range1D range,
|
void Ops_FlushMappedRange(BufferObject& bufferObject, Range1D range,
|
||||||
Flags<BufferMappingAccessBit> appAccess) {
|
Flags<BufferMappingAccessBit> appAccess) {
|
||||||
auto* resource = ResourceOf(bufferObject);
|
auto* resource = ResourceOf(bufferObject);
|
||||||
@@ -1087,13 +1175,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same WAR-hazard rule as Ops_SubData: an immediate upload (mapped or
|
// An adopted zero-copy persistent store already HAS the bytes: the
|
||||||
// glBufferSubData) can park the thread on Mali until the frames still
|
// frontend shadow IS the coherent mapping the app (or UploadSubData)
|
||||||
// referencing this store retire. Queue the range for the staged-copy
|
// wrote into, so publishing is free. The self-copy that used to run
|
||||||
// flush at draw-time sync; only the zero-copy persistent store and the
|
// here mapped a buffer this backend keeps persistently mapped (an
|
||||||
// negative-control kill switch keep the immediate paths below.
|
// INVALID_OPERATION whose fallback was a WAR-stalling
|
||||||
if (!(resource->persistentMapped && resource->persistentPtr) &&
|
// glBufferSubData).
|
||||||
!MG_Config::Features.EsprytDisableUploadRing) {
|
if (resource->persistentMapped && resource->persistentPtr) {
|
||||||
|
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same WAR-hazard rule as Ops_SubData: an immediate synchronized upload
|
||||||
|
// (mapped or glBufferSubData) can park the thread on Mali until the
|
||||||
|
// frames still referencing this store retire. Queue the range for the
|
||||||
|
// staged flush at draw-time sync; the negative-control kill switch
|
||||||
|
// keeps the immediate paths below.
|
||||||
|
if (!MG_Config::Features.EsprytDisableUploadRing) {
|
||||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||||
resource->pendingRanges.Add(range);
|
resource->pendingRanges.Add(range);
|
||||||
return;
|
return;
|
||||||
@@ -1137,8 +1235,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (!resource || resource->id == 0 || !resource->storageInitialized) return;
|
if (!resource || resource->id == 0 || !resource->storageInitialized) return;
|
||||||
if (!CanTouchGLNow() || resource->contextGeneration != g_bufferContextGeneration) return;
|
if (!CanTouchGLNow() || resource->contextGeneration != g_bufferContextGeneration) return;
|
||||||
if (resource->persistentMapped) {
|
if (resource->persistentMapped) {
|
||||||
// Host writes to a persistent map must not race shader writes already queued
|
// Queued resident SubData bytes land first (GPU-ordered), then the
|
||||||
// on this context. There is no backend copy to read back in this case.
|
// finish makes them - and any shader writes already queued on this
|
||||||
|
// context - visible through the coherent mapping the reads use.
|
||||||
|
// There is no backend copy to read back in this case.
|
||||||
|
DrainResidentWritesNow(*resource, bufferObject);
|
||||||
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
|
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1204,6 +1305,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Ops_SubData(bufferObject, offset, size);
|
Ops_SubData(bufferObject, offset, size);
|
||||||
BumpBufferMutationEpoch();
|
BumpBufferMutationEpoch();
|
||||||
}
|
}
|
||||||
|
void Ops_ResidentSubDataTracked(BufferObject& bufferObject, SizeT offset, DataPtr data) {
|
||||||
|
Ops_ResidentSubData(bufferObject, offset, data);
|
||||||
|
BumpBufferMutationEpoch();
|
||||||
|
}
|
||||||
void Ops_FlushMappedRangeTracked(BufferObject& bufferObject, Range1D range,
|
void Ops_FlushMappedRangeTracked(BufferObject& bufferObject, Range1D range,
|
||||||
Flags<BufferMappingAccessBit> appAccess) {
|
Flags<BufferMappingAccessBit> appAccess) {
|
||||||
Ops_FlushMappedRange(bufferObject, range, appAccess);
|
Ops_FlushMappedRange(bufferObject, range, appAccess);
|
||||||
@@ -1228,6 +1333,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const BufferBackendOps g_glesBufferBackendOps = {
|
const BufferBackendOps g_glesBufferBackendOps = {
|
||||||
.Respecify = Ops_RespecifyTracked,
|
.Respecify = Ops_RespecifyTracked,
|
||||||
.SubData = Ops_SubDataTracked,
|
.SubData = Ops_SubDataTracked,
|
||||||
|
.ResidentSubData = Ops_ResidentSubDataTracked,
|
||||||
.FlushMappedRange = Ops_FlushMappedRangeTracked,
|
.FlushMappedRange = Ops_FlushMappedRangeTracked,
|
||||||
.OnDestroy = Ops_OnDestroyTracked,
|
.OnDestroy = Ops_OnDestroyTracked,
|
||||||
.AcquirePersistentMap = Ops_AcquirePersistentMapTracked,
|
.AcquirePersistentMap = Ops_AcquirePersistentMapTracked,
|
||||||
@@ -1329,7 +1435,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (resource->id == 0) return false;
|
if (resource->id == 0) return false;
|
||||||
// Zero-copy coherent persistent store: EnsureBufferResource's own early-out —
|
// Zero-copy coherent persistent store: EnsureBufferResource's own early-out —
|
||||||
// the app writes straight into the mapped GPU storage, nothing to sync.
|
// the app writes straight into the mapped GPU storage, nothing to sync.
|
||||||
if (resource->persistentMapped) return resource->persistentPtr != nullptr;
|
// Except queued resident SubData bytes, which land through the sync path
|
||||||
|
// (same unlocked emptiness probe as pendingRanges below).
|
||||||
|
if (resource->persistentMapped) {
|
||||||
|
return resource->persistentPtr != nullptr && resource->pendingResidentWrites.empty();
|
||||||
|
}
|
||||||
// A live non-zero-copy map may owe a per-draw SyncPersistentMappedRange push
|
// A live non-zero-copy map may owe a per-draw SyncPersistentMappedRange push
|
||||||
// (persistent maps mutate the shadow without bumping the change serial).
|
// (persistent maps mutate the shadow without bumping the change serial).
|
||||||
if (frontend->IsMapped()) return false;
|
if (frontend->IsMapped()) return false;
|
||||||
@@ -1361,6 +1471,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
resource->storageSize = 0;
|
resource->storageSize = 0;
|
||||||
resource->pendingRespecify = true;
|
resource->pendingRespecify = true;
|
||||||
resource->pendingRanges.clear();
|
resource->pendingRanges.clear();
|
||||||
|
resource->pendingResidentWrites.clear();
|
||||||
resource->contextGeneration = g_bufferContextGeneration;
|
resource->contextGeneration = g_bufferContextGeneration;
|
||||||
// The persistent map (and its pointer) died with the old context; the
|
// The persistent map (and its pointer) died with the old context; the
|
||||||
// frontend re-acquires a fresh one on its next map.
|
// frontend re-acquires a fresh one on its next map.
|
||||||
@@ -1390,6 +1501,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// persistently mapped immutable store, so there is nothing to (re)upload at
|
// persistently mapped immutable store, so there is nothing to (re)upload at
|
||||||
// draw time. This is where the per-draw whole-buffer glBufferSubData used to run.
|
// draw time. This is where the per-draw whole-buffer glBufferSubData used to run.
|
||||||
if (resource->persistentMapped && resource->persistentPtr && resource->id != 0) {
|
if (resource->persistentMapped && resource->persistentPtr && resource->id != 0) {
|
||||||
|
DrainResidentWritesNow(*resource, *bufferObject);
|
||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1411,6 +1523,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
{
|
{
|
||||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||||
resource->pendingRanges.clear();
|
resource->pendingRanges.clear();
|
||||||
|
resource->pendingResidentWrites.clear();
|
||||||
}
|
}
|
||||||
resource->syncedChangeSerial = bufferObject->GetChangeSerial();
|
resource->syncedChangeSerial = bufferObject->GetChangeSerial();
|
||||||
} else {
|
} else {
|
||||||
@@ -2628,7 +2741,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteTextures) {
|
// TEMP-EXP (leak texture deletes): /sdcard/MG/exp_leak_texture_deletes.
|
||||||
|
// Discriminator for the mali-mem-purge hiccup theory: never hand the
|
||||||
|
// driver a texture free, so the purge daemon has nothing to reclaim.
|
||||||
|
static const Bool s_expLeakTextureDeletes = [] {
|
||||||
|
FILE* f = std::fopen("/sdcard/MG/exp_leak_texture_deletes", "rb");
|
||||||
|
if (!f) return false;
|
||||||
|
std::fclose(f);
|
||||||
|
return true;
|
||||||
|
}();
|
||||||
|
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteTextures &&
|
||||||
|
!s_expLeakTextureDeletes) {
|
||||||
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
||||||
if (m_bufferImageSplitViewId != 0) {
|
if (m_bufferImageSplitViewId != 0) {
|
||||||
g_GLESFuncs.glDeleteTextures(1, &m_bufferImageSplitViewId);
|
g_GLESFuncs.glDeleteTextures(1, &m_bufferImageSplitViewId);
|
||||||
@@ -7219,6 +7342,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
m_backendProgramUsable = false;
|
m_backendProgramUsable = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (stateProgramObject->PointSizeDemoted()) {
|
||||||
|
// THE ARMING SIGNAL, INFO on purpose and latched: the integration lane that
|
||||||
|
// pins MOBILEGL_POINT_SIZE_DEMOTION=1 asserts on exactly this line, because
|
||||||
|
// every rendering assertion stays green on a healthy driver whether the
|
||||||
|
// demotion ran or was silently disarmed. See PointSizeDemotionScenario.
|
||||||
|
MGLOG_I_ONCE("DirectGLES is building programs whose tessellation/geometry gl_PointSize was "
|
||||||
|
"demoted to an ordinary varying, because this driver cannot host the built-in "
|
||||||
|
"in those stages.");
|
||||||
|
}
|
||||||
MGLOG_D("Attaching %zu shaders to program %u", linkedStages.size(), m_backendProgramId);
|
MGLOG_D("Attaching %zu shaders to program %u", linkedStages.size(), m_backendProgramId);
|
||||||
for (const auto& ref : stateProgramObject->GetLinkedShaderSnapshot()) {
|
for (const auto& ref : stateProgramObject->GetLinkedShaderSnapshot()) {
|
||||||
if (!ref.shader) continue;
|
if (!ref.shader) continue;
|
||||||
@@ -7889,6 +8021,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// for; it has the variable that replaced it. Everything else - including a
|
// for; it has the variable that replaced it. Everything else - including a
|
||||||
// member of a block that was left alone - keeps the application's spelling.
|
// member of a block that was left alone - keeps the application's spelling.
|
||||||
// Storage first, pointers after: xfbNames holds pointers into these strings.
|
// Storage first, pointers after: xfbNames holds pointers into these strings.
|
||||||
|
//
|
||||||
|
// Same rule for a demoted gl_PointSize: the capture stage's ESSL no longer
|
||||||
|
// spells the built-in at all - the value lives in the carrier the demotion
|
||||||
|
// named - so the driver-side request has to follow it there. Only when the
|
||||||
|
// capture stage IS a demoted one (geometry, else evaluation): a program whose
|
||||||
|
// capture stage is the vertex shader keeps the built-in and its spelling,
|
||||||
|
// whatever happened to a control stage behind it.
|
||||||
|
Bool captureStageDemoted = false;
|
||||||
|
if (stateProgramObject->PointSizeDemoted()) {
|
||||||
|
for (const ShaderStage linkedStage : linkedStages) {
|
||||||
|
if (linkedStage == ShaderStage::TessEval || linkedStage == ShaderStage::Geometry) {
|
||||||
|
captureStageDemoted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vector<String> rewrittenXfbNames(xfbVaryings.size());
|
Vector<String> rewrittenXfbNames(xfbVaryings.size());
|
||||||
for (SizeT nameIndex = 0; nameIndex < xfbVaryings.size(); ++nameIndex) {
|
for (SizeT nameIndex = 0; nameIndex < xfbVaryings.size(); ++nameIndex) {
|
||||||
String flatName;
|
String flatName;
|
||||||
@@ -7896,6 +8044,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
MG_Util::ShaderTranspiler::ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock(
|
MG_Util::ShaderTranspiler::ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock(
|
||||||
xfbVaryings[nameIndex].name, flattenedXfbBlockNames, flatName)) {
|
xfbVaryings[nameIndex].name, flattenedXfbBlockNames, flatName)) {
|
||||||
rewrittenXfbNames[nameIndex] = std::move(flatName);
|
rewrittenXfbNames[nameIndex] = std::move(flatName);
|
||||||
|
} else if (captureStageDemoted && xfbVaryings[nameIndex].name == "gl_PointSize") {
|
||||||
|
rewrittenXfbNames[nameIndex] =
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME;
|
||||||
} else {
|
} else {
|
||||||
rewrittenXfbNames[nameIndex] = xfbVaryings[nameIndex].name;
|
rewrittenXfbNames[nameIndex] = xfbVaryings[nameIndex].name;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,6 +461,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// the owning thread replaying them: guard both fields with pendingMutex.
|
// the owning thread replaying them: guard both fields with pendingMutex.
|
||||||
Bool pendingRespecify = false;
|
Bool pendingRespecify = false;
|
||||||
VecRange1D pendingRanges;
|
VecRange1D pendingRanges;
|
||||||
|
// App bytes for an ADOPTED store, awaiting their GPU-ordered landing (ring
|
||||||
|
// stage + glCopyBufferSubData at the next sync; see
|
||||||
|
// BufferBackendOps::ResidentSubData). The frontend keeps such writes out of
|
||||||
|
// the coherent mapping - an in-place host write tears the in-flight frames
|
||||||
|
// still reading the old bytes. Guarded by pendingMutex like pendingRanges.
|
||||||
|
struct PendingResidentWrite {
|
||||||
|
SizeT offset = 0;
|
||||||
|
Vector<Uint8> bytes;
|
||||||
|
};
|
||||||
|
Vector<PendingResidentWrite> pendingResidentWrites;
|
||||||
std::mutex pendingMutex;
|
std::mutex pendingMutex;
|
||||||
// Buffer-mutation epoch (see CurrentBufferMutationEpoch) at which this
|
// Buffer-mutation epoch (see CurrentBufferMutationEpoch) at which this
|
||||||
// resource last probed IsBufferDrawClean == true, 0 = never (epochs start
|
// resource last probed IsBufferDrawClean == true, 0 = never (epochs start
|
||||||
|
|||||||
@@ -1081,6 +1081,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// report VK_FALSE, so on every real mobile device this is false and the demotion runs
|
// report VK_FALSE, so on every real mobile device this is false and the demotion runs
|
||||||
// exactly as it always has.
|
// exactly as it always has.
|
||||||
m_dynamicParameters.SupportsShaderFloat64 = m_vulkanCaps.SupportsShaderFloat64;
|
m_dynamicParameters.SupportsShaderFloat64 = m_vulkanCaps.SupportsShaderFloat64;
|
||||||
|
// shaderTessellationAndGeometryPointSize, both stage families from the one feature.
|
||||||
|
// False arms the shared phase-B point-size demotion, whose modules then carry no
|
||||||
|
// TessellationPointSize/GeometryPointSize capability and build without the feature.
|
||||||
|
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends it is absent so the demotion can be
|
||||||
|
// exercised on a healthy driver (lavapipe advertises the feature); =0 restores the
|
||||||
|
// detected answer's declines.
|
||||||
|
{
|
||||||
|
Bool supportsStagePointSize = m_vulkanCaps.SupportsTessellationAndGeometryPointSize;
|
||||||
|
switch (MG_Config::Features.PointSizeDemotion) {
|
||||||
|
case MG_Config::QuirkOverride::ForceOn:
|
||||||
|
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
|
||||||
|
"gl_PointSize as unhosted so the demotion runs on this driver");
|
||||||
|
supportsStagePointSize = false;
|
||||||
|
break;
|
||||||
|
case MG_Config::QuirkOverride::ForceOff:
|
||||||
|
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
|
||||||
|
"plain declines regardless of the device feature");
|
||||||
|
supportsStagePointSize = true;
|
||||||
|
break;
|
||||||
|
case MG_Config::QuirkOverride::Auto:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
m_dynamicParameters.SupportsTessellationPointSize = supportsStagePointSize;
|
||||||
|
m_dynamicParameters.SupportsGeometryPointSize = supportsStagePointSize;
|
||||||
|
}
|
||||||
// Never, on any device, and DELIBERATELY NOT COUPLED to the line above even though it
|
// Never, on any device, and DELIBERATELY NOT COUPLED to the line above even though it
|
||||||
// once tracked the same feature. It used to, because a `dvec` input needed Float64 to
|
// once tracked the same feature. It used to, because a `dvec` input needed Float64 to
|
||||||
// exist in the module at all; a 64-bit vertex FETCH was already impossible
|
// exist in the module at all; a 64-bit vertex FETCH was already impossible
|
||||||
|
|||||||
@@ -1206,6 +1206,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
|
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
|
||||||
// Kind::Occlusion - pool slots recorded between Begin/End; summed at result time.
|
// Kind::Occlusion - pool slots recorded between Begin/End; summed at result time.
|
||||||
Vector<Uint32> occlusionSlots;
|
Vector<Uint32> occlusionSlots;
|
||||||
|
// Kind::XfbGenerated - reroute-pool slots for the span's XFB-INACTIVE
|
||||||
|
// draws, where the renderer's reroute is armed (the affected driver's
|
||||||
|
// stream query counts nothing without an open capture; see
|
||||||
|
// VulkanRenderer::BeginXfbQueryForDraw). Summed alongside the stream
|
||||||
|
// slots above, which keep the span's XFB-active draws.
|
||||||
|
Vector<Uint32> rerouteSlots;
|
||||||
// Renderer generation the records were written under (see
|
// Renderer generation the records were written under (see
|
||||||
// g_rendererGeneration). A stale generation resolves as available
|
// g_rendererGeneration). A stale generation resolves as available
|
||||||
// with a final zero result: the records' pool indices and frame
|
// with a final zero result: the records' pool indices and frame
|
||||||
@@ -1215,11 +1221,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// stale queries are always safe to delete.
|
// stale queries are always safe to delete.
|
||||||
Uint64 rendererGeneration = 0;
|
Uint64 rendererGeneration = 0;
|
||||||
// Kind::XfbGenerated - the frontend's paused-draw primitive counter when the
|
// Kind::XfbGenerated - the frontend's paused-draw primitive counter when the
|
||||||
// query began. VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT counts only what the
|
// query began. On the affected drivers VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT
|
||||||
// capture saw, so a draw made while the span was paused is invisible to it -
|
// counts only what the capture saw, so a draw made while the span was paused is
|
||||||
// but GL_PRIMITIVES_GENERATED counts what the last vertex processing stage
|
// invisible to it - but GL_PRIMITIVES_GENERATED counts what the last vertex
|
||||||
// emitted regardless. The delta closes that gap at result time.
|
// processing stage emitted regardless. The delta closes that gap at result time.
|
||||||
Uint64 pausedPrimitiveSnapshot = 0;
|
Uint64 pausedPrimitiveSnapshot = 0;
|
||||||
|
// ...unless the GPU already counted those paused draws when the span opened -
|
||||||
|
// through the reroute pool (VulkanRenderer::BeginXfbQueryForDraw reroutes every
|
||||||
|
// draw with no open capture, paused ones included) or, where the probe measured
|
||||||
|
// the stream query as counting capture-less draws, through the stream slot the
|
||||||
|
// paused draw still takes. Adding the CPU delta on top would count them twice,
|
||||||
|
// and the CPU counter is the weaker source anyway: only 3 of the ~15 draw entry
|
||||||
|
// points write it and it answers 0 for GL_PATCHES.
|
||||||
|
Bool pausedPrimitivesCountedByGpu = false;
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -1313,12 +1327,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (query->kind == VulkanTimerQuery::Kind::XfbWritten ||
|
if (query->kind == VulkanTimerQuery::Kind::XfbWritten ||
|
||||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated) {
|
query->kind == VulkanTimerQuery::Kind::XfbGenerated) {
|
||||||
Uint64 primitives = 0;
|
Uint64 primitives = 0;
|
||||||
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots,
|
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots, query->rerouteSlots,
|
||||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated,
|
query->kind == VulkanTimerQuery::Kind::XfbGenerated,
|
||||||
primitives)) {
|
primitives)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated && MG_State::pGLContext != nullptr) {
|
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated &&
|
||||||
|
!query->pausedPrimitivesCountedByGpu && MG_State::pGLContext != nullptr) {
|
||||||
primitives += MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() -
|
primitives += MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() -
|
||||||
query->pausedPrimitiveSnapshot;
|
query->pausedPrimitiveSnapshot;
|
||||||
}
|
}
|
||||||
@@ -1367,6 +1382,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
query->rendererGeneration = GetRendererGeneration();
|
query->rendererGeneration = GetRendererGeneration();
|
||||||
query->pausedPrimitiveSnapshot =
|
query->pausedPrimitiveSnapshot =
|
||||||
MG_State::pGLContext ? MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() : 0;
|
MG_State::pGLContext ? MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() : 0;
|
||||||
|
// Read AFTER StartXfbQueryCapture, which is where a failed reroute-pool creation
|
||||||
|
// disarms: the answer is then what this span will actually do for every draw.
|
||||||
|
query->pausedPrimitivesCountedByGpu = generated && pVulkanRenderer->ArePausedDrawsGpuCounted();
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1377,7 +1395,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pVulkanRenderer->StopXfbQueryCapture(
|
pVulkanRenderer->StopXfbQueryCapture(
|
||||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots);
|
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots,
|
||||||
|
query->rerouteSlots);
|
||||||
}
|
}
|
||||||
|
|
||||||
BackendQueryHandle BeginOcclusionQuery() {
|
BackendQueryHandle BeginOcclusionQuery() {
|
||||||
|
|||||||
@@ -1428,6 +1428,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (varying.name == "gl_PointSize") {
|
if (varying.name == "gl_PointSize") {
|
||||||
|
// A demoted module (ShaderCompiler::
|
||||||
|
// DemoteTessellationGeometryPointSizeForProgram) no longer ACCESSES the
|
||||||
|
// built-in member - the value lives in the carrier variable the demotion
|
||||||
|
// named - so the capture binds to the carrier directly. The mirror below
|
||||||
|
// must not run for it: reading the now-unwritten member would capture
|
||||||
|
// garbage, and the read itself is the capability access the demotion
|
||||||
|
// exists to remove. Detected off the module's own debug names, so a
|
||||||
|
// composite built from another program's stage answers for the module it
|
||||||
|
// actually contains.
|
||||||
|
const auto carrierIt = idsByName.find(
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME);
|
||||||
|
if (carrierIt != idsByName.end()) {
|
||||||
|
decorateForXfb(carrierIt->second, varying.bufferIndex, varying.offsetBytes);
|
||||||
|
modified = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
needsPointSizeMirror = true;
|
needsPointSizeMirror = true;
|
||||||
pointSizeBufferIndex = varying.bufferIndex;
|
pointSizeBufferIndex = varying.bufferIndex;
|
||||||
pointSizeOffset = varying.offsetBytes;
|
pointSizeOffset = varying.offsetBytes;
|
||||||
@@ -3454,6 +3470,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// `spirv` and `moduleSpirvs` for any program attached to after it linked.
|
// `spirv` and `moduleSpirvs` for any program attached to after it linked.
|
||||||
const Vector<ShaderStage> stages = program.GetLinkedShaderStages();
|
const Vector<ShaderStage> stages = program.GetLinkedShaderStages();
|
||||||
auto& spirv = program.GetGeneratedSpirv();
|
auto& spirv = program.GetGeneratedSpirv();
|
||||||
|
if (program.PointSizeDemoted()) {
|
||||||
|
// THE ARMING SIGNAL, INFO on purpose and latched: the integration lane that pins
|
||||||
|
// MOBILEGL_POINT_SIZE_DEMOTION=1 asserts on exactly this line, because every
|
||||||
|
// rendering assertion above it stays green on a healthy driver whether the
|
||||||
|
// demotion ran or was silently disarmed. See PointSizeDemotionScenario.
|
||||||
|
MGLOG_I_ONCE("DirectVulkan is building programs whose tessellation/geometry gl_PointSize was "
|
||||||
|
"demoted to an ordinary varying, because this device cannot host the built-in "
|
||||||
|
"in those stages.");
|
||||||
|
}
|
||||||
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
||||||
const Bool enableSpirvValidation = program.GetSpirvValidationEnabled();
|
const Bool enableSpirvValidation = program.GetSpirvValidationEnabled();
|
||||||
// Unconditional now: the two ValidateTransformedSpirv calls below run in every build,
|
// Unconditional now: the two ValidateTransformedSpirv calls below run in every build,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Math/HalfFloat.h"
|
#include "MG_Util/Math/HalfFloat.h"
|
||||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||||
|
#include "MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h"
|
||||||
#include "MG_Util/Texture/PixelStoreProcessor.h"
|
#include "MG_Util/Texture/PixelStoreProcessor.h"
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -3277,6 +3278,17 @@ void main() {
|
|||||||
vkDestroyQueryPool(m_device, m_xfbQueryPool, nullptr);
|
vkDestroyQueryPool(m_device, m_xfbQueryPool, nullptr);
|
||||||
m_xfbQueryPool = VK_NULL_HANDLE;
|
m_xfbQueryPool = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
|
if (m_primGenReroutePool != VK_NULL_HANDLE) {
|
||||||
|
vkDestroyQueryPool(m_device, m_primGenReroutePool, nullptr);
|
||||||
|
m_primGenReroutePool = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
m_primGenRerouteActiveSlots.clear();
|
||||||
|
m_primGenRerouteSlotCursor = 0;
|
||||||
|
m_primGenRerouteSlotOpen = false;
|
||||||
|
// Not sticky across renderers: the next bring-up re-decides both (from the
|
||||||
|
// per-process probe memo, so it re-decides without re-probing).
|
||||||
|
m_primGenRerouteKind = MG_Util::SelfTest::PrimGenRerouteKind::None;
|
||||||
|
m_primGenStreamCountsXfbInactiveDraws = false;
|
||||||
m_bufferManager.Shutdown();
|
m_bufferManager.Shutdown();
|
||||||
|
|
||||||
// Device is idle (vkDeviceWaitIdle above); query pools can be destroyed.
|
// Device is idle (vkDeviceWaitIdle above); query pools can be destroyed.
|
||||||
@@ -11299,7 +11311,7 @@ void main() {
|
|||||||
VkCommandBuffer& commandBuffer = frame.commandBuffer;
|
VkCommandBuffer& commandBuffer = frame.commandBuffer;
|
||||||
|
|
||||||
const Bool xfbActive = BeginXfbCaptureForDraw(frame);
|
const Bool xfbActive = BeginXfbCaptureForDraw(frame);
|
||||||
BeginXfbQueryForDraw(commandBuffer);
|
BeginXfbQueryForDraw(commandBuffer, xfbActive);
|
||||||
const Bool occlusionActive = BeginOcclusionForDraw(commandBuffer);
|
const Bool occlusionActive = BeginOcclusionForDraw(commandBuffer);
|
||||||
vkCmdDraw(commandBuffer,
|
vkCmdDraw(commandBuffer,
|
||||||
payload.params.vertexCount,
|
payload.params.vertexCount,
|
||||||
@@ -11385,23 +11397,81 @@ void main() {
|
|||||||
}
|
}
|
||||||
s_vkResetQueryPool(m_device, m_xfbQueryPool, 0, kXfbQuerySlots);
|
s_vkResetQueryPool(m_device, m_xfbQueryPool, 0, kXfbQuerySlots);
|
||||||
}
|
}
|
||||||
|
// The reroute pool, on the first GENERATED span that needs it. A creation
|
||||||
|
// failure disarms rather than failing the capture: the stream path still
|
||||||
|
// answers (with the driver's defect), which beats answering nothing.
|
||||||
|
if (kind == 1 && m_primGenRerouteKind != MG_Util::SelfTest::PrimGenRerouteKind::None &&
|
||||||
|
m_primGenReroutePool == VK_NULL_HANDLE) {
|
||||||
|
VkQueryPoolCreateInfo poolInfo{};
|
||||||
|
poolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
|
||||||
|
poolInfo.queryCount = kXfbQuerySlots;
|
||||||
|
if (m_primGenRerouteKind == MG_Util::SelfTest::PrimGenRerouteKind::PrimitivesGeneratedExt) {
|
||||||
|
// The query Vulkan defines for this GL target; counts vertex stream 0
|
||||||
|
// when begun with plain vkCmdBeginQuery.
|
||||||
|
poolInfo.queryType = VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT;
|
||||||
|
} else {
|
||||||
|
poolInfo.queryType = VK_QUERY_TYPE_PIPELINE_STATISTICS;
|
||||||
|
// The clipping-stage INVOCATION counter: one per primitive reaching
|
||||||
|
// primitive clipping (GL's CLIPPING_INPUT_PRIMITIVES) - post-tess/GS,
|
||||||
|
// pre-clip, and per spec still counted under rasterizer discard, which
|
||||||
|
// is exactly the set GL_PRIMITIVES_GENERATED is defined over. The
|
||||||
|
// stage's OUTPUT count (CLIPPING_PRIMITIVES_BIT) would be wrong:
|
||||||
|
// clipping may drop or split primitives.
|
||||||
|
poolInfo.pipelineStatistics = VK_QUERY_PIPELINE_STATISTIC_CLIPPING_INVOCATIONS_BIT;
|
||||||
|
}
|
||||||
|
if (vkCreateQueryPool(m_device, &poolInfo, nullptr, &m_primGenReroutePool) != VK_SUCCESS) {
|
||||||
|
MGLOG_E_ONCE("StartXfbQueryCapture: reroute pool creation failed; the "
|
||||||
|
"PRIMITIVES_GENERATED reroute is disarmed and XFB-inactive draws keep "
|
||||||
|
"the stream query");
|
||||||
|
m_primGenReroutePool = VK_NULL_HANDLE;
|
||||||
|
m_primGenRerouteKind = MG_Util::SelfTest::PrimGenRerouteKind::None;
|
||||||
|
} else {
|
||||||
|
s_vkResetQueryPool(m_device, m_primGenReroutePool, 0, kXfbQuerySlots);
|
||||||
|
}
|
||||||
|
}
|
||||||
m_xfbQueryActiveSlots[kind].clear();
|
m_xfbQueryActiveSlots[kind].clear();
|
||||||
m_xfbQueryCaptureActive[kind] = true;
|
m_xfbQueryCaptureActive[kind] = true;
|
||||||
|
if (kind == 1) {
|
||||||
|
m_primGenRerouteActiveSlots.clear();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void VulkanRenderer::StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots) {
|
Bool VulkanRenderer::ArePausedDrawsGpuCounted() const {
|
||||||
|
// Exactly the gate BeginXfbQueryForDraw applies per draw, so a span told "armed"
|
||||||
|
// really does get a reroute slot for every draw with no open capture - a paused
|
||||||
|
// span's draws included.
|
||||||
|
const Bool rerouteArmed = m_primGenRerouteKind != MG_Util::SelfTest::PrimGenRerouteKind::None &&
|
||||||
|
m_primGenReroutePool != VK_NULL_HANDLE;
|
||||||
|
// Otherwise the paused draw takes a stream slot, which is an exact count of it
|
||||||
|
// on a driver the probe measured as counting capture-less draws.
|
||||||
|
return rerouteArmed || m_primGenStreamCountsXfbInactiveDraws;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VulkanRenderer::StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots,
|
||||||
|
Vector<Uint32>& outRerouteSlots) {
|
||||||
if (kind > 1) {
|
if (kind > 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
outSlots = Move(m_xfbQueryActiveSlots[kind]);
|
outSlots = Move(m_xfbQueryActiveSlots[kind]);
|
||||||
m_xfbQueryActiveSlots[kind].clear();
|
m_xfbQueryActiveSlots[kind].clear();
|
||||||
m_xfbQueryCaptureActive[kind] = false;
|
m_xfbQueryCaptureActive[kind] = false;
|
||||||
|
outRerouteSlots.clear();
|
||||||
|
if (kind == 1) {
|
||||||
|
outRerouteSlots = Move(m_primGenRerouteActiveSlots);
|
||||||
|
m_primGenRerouteActiveSlots.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool VulkanRenderer::ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives) {
|
Bool VulkanRenderer::ResolveXfbQueryResult(const Vector<Uint32>& slots, const Vector<Uint32>& rerouteSlots,
|
||||||
|
Bool wantGenerated, Uint64& outPrimitives) {
|
||||||
outPrimitives = 0;
|
outPrimitives = 0;
|
||||||
if (slots.empty() || m_xfbQueryPool == VK_NULL_HANDLE) {
|
const Bool haveStreamSlots = !slots.empty() && m_xfbQueryPool != VK_NULL_HANDLE;
|
||||||
|
// Reroute slots only ever accumulate the GENERATED target (see
|
||||||
|
// BeginXfbQueryForDraw); WRITTEN never opens one.
|
||||||
|
const Bool haveRerouteSlots =
|
||||||
|
wantGenerated && !rerouteSlots.empty() && m_primGenReroutePool != VK_NULL_HANDLE;
|
||||||
|
if (!haveStreamSlots && !haveRerouteSlots) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
auto& frame = m_frameContext.GetCurrent();
|
auto& frame = m_frameContext.GetCurrent();
|
||||||
@@ -11413,44 +11483,108 @@ void main() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const Uint32 slot : slots) {
|
if (haveStreamSlots) {
|
||||||
Uint64 pair[2] = {0, 0}; // {primitivesWritten, primitivesNeeded}
|
for (const Uint32 slot : slots) {
|
||||||
const VkResult result =
|
Uint64 pair[2] = {0, 0}; // {primitivesWritten, primitivesNeeded}
|
||||||
vkGetQueryPoolResults(m_device, m_xfbQueryPool, slot, 1, sizeof(pair), pair, sizeof(pair),
|
const VkResult result =
|
||||||
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT);
|
vkGetQueryPoolResults(m_device, m_xfbQueryPool, slot, 1, sizeof(pair), pair, sizeof(pair),
|
||||||
if (result == VK_SUCCESS) {
|
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT);
|
||||||
outPrimitives += pair[wantGenerated ? 1 : 0];
|
if (result == VK_SUCCESS) {
|
||||||
|
outPrimitives += pair[wantGenerated ? 1 : 0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (haveRerouteSlots) {
|
||||||
|
for (const Uint32 slot : rerouteSlots) {
|
||||||
|
// Both reroute pool kinds answer one 64-bit primitive count per slot.
|
||||||
|
Uint64 generated = 0;
|
||||||
|
const VkResult result = vkGetQueryPoolResults(
|
||||||
|
m_device, m_primGenReroutePool, slot, 1, sizeof(generated), &generated,
|
||||||
|
sizeof(generated), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT);
|
||||||
|
if (result == VK_SUCCESS) {
|
||||||
|
outPrimitives += generated;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void VulkanRenderer::BeginXfbQueryForDraw(VkCommandBuffer commandBuffer) {
|
void VulkanRenderer::BeginXfbQueryForDraw(VkCommandBuffer commandBuffer, Bool xfbActive) {
|
||||||
m_xfbQuerySlotOpen = false;
|
m_xfbQuerySlotOpen = false;
|
||||||
|
m_primGenRerouteSlotOpen = false;
|
||||||
if ((!m_xfbQueryCaptureActive[0] && !m_xfbQueryCaptureActive[1]) || m_xfbQueryPool == VK_NULL_HANDLE) {
|
if ((!m_xfbQueryCaptureActive[0] && !m_xfbQueryCaptureActive[1]) || m_xfbQueryPool == VK_NULL_HANDLE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const Uint32 slot = m_xfbQuerySlotCursor;
|
// Every draw with no OPEN capture is the stream query's silent case, and that
|
||||||
m_xfbQuerySlotCursor = (m_xfbQuerySlotCursor + 1) % kXfbQuerySlots;
|
// includes a draw made while the GL span is merely PAUSED (the pause closes the
|
||||||
// Slots are never host-reset at read time (both GL targets may reference one
|
// capture, so BeginXfbCaptureForDraw already answered false for it). Paused
|
||||||
// slot); recycle them here instead.
|
// draws are rerouted like any other: the frontend's CPU paused-primitive
|
||||||
s_vkResetQueryPool(m_device, m_xfbQueryPool, slot, 1);
|
// counter cannot stand in for them - it is written by only 3 of the ~15 draw
|
||||||
s_vkCmdBeginQueryIndexedEXT(commandBuffer, m_xfbQueryPool, slot, 0, 0);
|
// entry points (never the instanced, indirect or multi-draw ones) and answers 0
|
||||||
for (Uint32 kind = 0; kind < 2; ++kind) {
|
// for GL_PATCHES by design, since the tessellator's amplification is not
|
||||||
if (m_xfbQueryCaptureActive[kind]) {
|
// knowable on the CPU - which is exactly the CTS's shape. Double counting is
|
||||||
m_xfbQueryActiveSlots[kind].push_back(slot);
|
// prevented on the other side instead: a GENERATED span opened while this
|
||||||
|
// reroute is armed ignores that CPU counter entirely (see
|
||||||
|
// ArePausedDrawsGpuCounted and DirectVulkan.cpp's XfbGenerated resolve), so
|
||||||
|
// every XFB-inactive draw in the span is priced exactly once, by this pool.
|
||||||
|
const Bool rerouteGenerated = m_xfbQueryCaptureActive[1] &&
|
||||||
|
m_primGenRerouteKind != MG_Util::SelfTest::PrimGenRerouteKind::None &&
|
||||||
|
m_primGenReroutePool != VK_NULL_HANDLE && !xfbActive;
|
||||||
|
// The stream slot stays for WRITTEN whatever the reroute does (with capture
|
||||||
|
// inactive its primitivesWritten is 0, which is the correct WRITTEN answer),
|
||||||
|
// and for GENERATED wherever this draw is not rerouted - so one GL query span
|
||||||
|
// may accumulate stream slots (XFB-active draws) and reroute slots
|
||||||
|
// (XFB-inactive draws) side by side.
|
||||||
|
const Bool wantStreamSlot =
|
||||||
|
m_xfbQueryCaptureActive[0] || (m_xfbQueryCaptureActive[1] && !rerouteGenerated);
|
||||||
|
if (wantStreamSlot) {
|
||||||
|
const Uint32 slot = m_xfbQuerySlotCursor;
|
||||||
|
m_xfbQuerySlotCursor = (m_xfbQuerySlotCursor + 1) % kXfbQuerySlots;
|
||||||
|
// Slots are never host-reset at read time (both GL targets may reference one
|
||||||
|
// slot); recycle them here instead.
|
||||||
|
s_vkResetQueryPool(m_device, m_xfbQueryPool, slot, 1);
|
||||||
|
s_vkCmdBeginQueryIndexedEXT(commandBuffer, m_xfbQueryPool, slot, 0, 0);
|
||||||
|
if (m_xfbQueryCaptureActive[0]) {
|
||||||
|
m_xfbQueryActiveSlots[0].push_back(slot);
|
||||||
}
|
}
|
||||||
|
if (m_xfbQueryCaptureActive[1] && !rerouteGenerated) {
|
||||||
|
m_xfbQueryActiveSlots[1].push_back(slot);
|
||||||
|
}
|
||||||
|
m_xfbQuerySlotOpen = true;
|
||||||
|
m_xfbQueryOpenSlot = slot;
|
||||||
|
}
|
||||||
|
if (rerouteGenerated) {
|
||||||
|
// Latched at INFO on purpose: it is the pinned integration lane's arming
|
||||||
|
// observable (the shape UnlocatedIoBlockScenario asserts), and the builds
|
||||||
|
// CI runs compile INFO in.
|
||||||
|
MGLOG_I_ONCE("PRIMITIVES_GENERATED reroute engaged: an XFB-inactive draw accumulates "
|
||||||
|
"through the %s pool",
|
||||||
|
m_primGenRerouteKind ==
|
||||||
|
MG_Util::SelfTest::PrimGenRerouteKind::PrimitivesGeneratedExt
|
||||||
|
? "VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT"
|
||||||
|
: "clipping-invocations statistics");
|
||||||
|
const Uint32 slot = m_primGenRerouteSlotCursor;
|
||||||
|
m_primGenRerouteSlotCursor = (m_primGenRerouteSlotCursor + 1) % kXfbQuerySlots;
|
||||||
|
// Same recycle-at-begin discipline as the stream pool. Both pool kinds
|
||||||
|
// are begun with plain vkCmdBeginQuery (a PRIMITIVES_GENERATED_EXT
|
||||||
|
// query begun this way counts vertex stream 0).
|
||||||
|
s_vkResetQueryPool(m_device, m_primGenReroutePool, slot, 1);
|
||||||
|
vkCmdBeginQuery(commandBuffer, m_primGenReroutePool, slot, 0);
|
||||||
|
m_primGenRerouteActiveSlots.push_back(slot);
|
||||||
|
m_primGenRerouteSlotOpen = true;
|
||||||
|
m_primGenRerouteOpenSlot = slot;
|
||||||
}
|
}
|
||||||
m_xfbQuerySlotOpen = true;
|
|
||||||
m_xfbQueryOpenSlot = slot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void VulkanRenderer::EndXfbQueryForDraw(VkCommandBuffer commandBuffer) {
|
void VulkanRenderer::EndXfbQueryForDraw(VkCommandBuffer commandBuffer) {
|
||||||
if (!m_xfbQuerySlotOpen) {
|
if (m_xfbQuerySlotOpen) {
|
||||||
return;
|
s_vkCmdEndQueryIndexedEXT(commandBuffer, m_xfbQueryPool, m_xfbQueryOpenSlot, 0);
|
||||||
|
m_xfbQuerySlotOpen = false;
|
||||||
|
}
|
||||||
|
if (m_primGenRerouteSlotOpen) {
|
||||||
|
vkCmdEndQuery(commandBuffer, m_primGenReroutePool, m_primGenRerouteOpenSlot);
|
||||||
|
m_primGenRerouteSlotOpen = false;
|
||||||
}
|
}
|
||||||
s_vkCmdEndQueryIndexedEXT(commandBuffer, m_xfbQueryPool, m_xfbQueryOpenSlot, 0);
|
|
||||||
m_xfbQuerySlotOpen = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool VulkanRenderer::BeginOcclusionForDraw(VkCommandBuffer commandBuffer) {
|
Bool VulkanRenderer::BeginOcclusionForDraw(VkCommandBuffer commandBuffer) {
|
||||||
@@ -11500,7 +11634,7 @@ void main() {
|
|||||||
VkCommandBuffer& commandBuffer = frame.commandBuffer;
|
VkCommandBuffer& commandBuffer = frame.commandBuffer;
|
||||||
|
|
||||||
const Bool xfbActive = BeginXfbCaptureForDraw(frame);
|
const Bool xfbActive = BeginXfbCaptureForDraw(frame);
|
||||||
BeginXfbQueryForDraw(commandBuffer);
|
BeginXfbQueryForDraw(commandBuffer, xfbActive);
|
||||||
const Bool occlusionActive = BeginOcclusionForDraw(commandBuffer);
|
const Bool occlusionActive = BeginOcclusionForDraw(commandBuffer);
|
||||||
vkCmdDrawIndexed(commandBuffer,
|
vkCmdDrawIndexed(commandBuffer,
|
||||||
payload.params.indexCount,
|
payload.params.indexCount,
|
||||||
@@ -13333,6 +13467,13 @@ void main() {
|
|||||||
// occlusion result still satisfies any-samples-style consumers.
|
// occlusion result still satisfies any-samples-style consumers.
|
||||||
deviceFeatures.occlusionQueryPrecise = supportedDeviceFeatures.occlusionQueryPrecise;
|
deviceFeatures.occlusionQueryPrecise = supportedDeviceFeatures.occlusionQueryPrecise;
|
||||||
m_occlusionQueryPreciseEnabled = deviceFeatures.occlusionQueryPrecise == VK_TRUE;
|
m_occlusionQueryPreciseEnabled = deviceFeatures.occlusionQueryPrecise == VK_TRUE;
|
||||||
|
m_tessellationShaderFeatureEnabled = deviceFeatures.tessellationShader == VK_TRUE;
|
||||||
|
// Backs the GL_PRIMITIVES_GENERATED reroute's statistics tier (see the
|
||||||
|
// m_primGenReroute* members): a VK_QUERY_TYPE_PIPELINE_STATISTICS pool may only
|
||||||
|
// be created with this feature enabled. Enabled wherever the device has it - the
|
||||||
|
// feature alone costs nothing; pools exist only where the reroute is armed.
|
||||||
|
deviceFeatures.pipelineStatisticsQuery = supportedDeviceFeatures.pipelineStatisticsQuery;
|
||||||
|
m_pipelineStatisticsQueryFeatureEnabled = deviceFeatures.pipelineStatisticsQuery == VK_TRUE;
|
||||||
|
|
||||||
VkDeviceCreateInfo deviceCreateInfo{};
|
VkDeviceCreateInfo deviceCreateInfo{};
|
||||||
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
|
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
|
||||||
@@ -13660,6 +13801,38 @@ void main() {
|
|||||||
MGLOG_I("Enabled optional device extension: %s", VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME);
|
MGLOG_I("Enabled optional device extension: %s", VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// VK_EXT_primitives_generated_query - the query Vulkan defines for GL's
|
||||||
|
// GL_PRIMITIVES_GENERATED precisely because the stream query above needs no
|
||||||
|
// capture by spec but drivers disagree. Taken with BOTH the base feature and the
|
||||||
|
// rasterizer-discard feature or not at all: without the latter, a discarding draw
|
||||||
|
// inside the query is invalid usage, and GL applications toggle discard freely.
|
||||||
|
// Only the PRIMITIVES_GENERATED reroute consumes it (see ArmPrimGenReroute).
|
||||||
|
m_primitivesGeneratedQueryFeatureEnabled = false;
|
||||||
|
m_primitivesGeneratedQueryDiscardFeatureEnabled = false;
|
||||||
|
VkPhysicalDevicePrimitivesGeneratedQueryFeaturesEXT primitivesGeneratedQueryFeatures{};
|
||||||
|
primitivesGeneratedQueryFeatures.sType =
|
||||||
|
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVES_GENERATED_QUERY_FEATURES_EXT;
|
||||||
|
if (IsExtensionSupported(availableExtensions, VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME) &&
|
||||||
|
getPhysicalDeviceFeatures2 != nullptr) {
|
||||||
|
VkPhysicalDeviceFeatures2 featureQuery{};
|
||||||
|
featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
|
||||||
|
featureQuery.pNext = &primitivesGeneratedQueryFeatures;
|
||||||
|
getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery);
|
||||||
|
if (primitivesGeneratedQueryFeatures.primitivesGeneratedQuery == VK_TRUE &&
|
||||||
|
primitivesGeneratedQueryFeatures.primitivesGeneratedQueryWithRasterizerDiscard == VK_TRUE) {
|
||||||
|
if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions,
|
||||||
|
VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME)) {
|
||||||
|
enabledDeviceExtensions.push_back(VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME);
|
||||||
|
}
|
||||||
|
primitivesGeneratedQueryFeatures.primitivesGeneratedQueryWithNonZeroStreams = VK_FALSE;
|
||||||
|
primitivesGeneratedQueryFeatures.pNext = const_cast<void*>(deviceCreateInfo.pNext);
|
||||||
|
deviceCreateInfo.pNext = &primitivesGeneratedQueryFeatures;
|
||||||
|
m_primitivesGeneratedQueryFeatureEnabled = true;
|
||||||
|
m_primitivesGeneratedQueryDiscardFeatureEnabled = true;
|
||||||
|
MGLOG_I("Enabled optional device extension: %s",
|
||||||
|
VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
// VK_EXT_provoking_vertex. Two independent features live behind one extension:
|
// VK_EXT_provoking_vertex. Two independent features live behind one extension:
|
||||||
// provokingVertexLast -> flat varyings, gl_Layer/gl_ViewportIndex and
|
// provokingVertexLast -> flat varyings, gl_Layer/gl_ViewportIndex and
|
||||||
// the input-assembler capture order.
|
// the input-assembler capture order.
|
||||||
@@ -14009,6 +14182,123 @@ void main() {
|
|||||||
m_timerQuerySupported = m_timestampValidBits > 0 && m_timestampPeriodNs > 0.0f;
|
m_timerQuerySupported = m_timestampValidBits > 0 && m_timestampPeriodNs > 0.0f;
|
||||||
MGLOG_I("Timer queries %s (timestampValidBits=%u, timestampPeriod=%f ns/tick)",
|
MGLOG_I("Timer queries %s (timestampValidBits=%u, timestampPeriod=%f ns/tick)",
|
||||||
m_timerQuerySupported ? "supported" : "not supported", m_timestampValidBits, m_timestampPeriodNs);
|
m_timerQuerySupported ? "supported" : "not supported", m_timestampValidBits, m_timestampPeriodNs);
|
||||||
|
|
||||||
|
// Last, because it records on m_graphicsQueue: decide the PRIMITIVES_GENERATED
|
||||||
|
// reroute for XFB-inactive draws. Nothing else has touched the queue yet.
|
||||||
|
ArmPrimGenReroute();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VulkanRenderer::ArmPrimGenReroute() {
|
||||||
|
using namespace MG_Util::SelfTest;
|
||||||
|
m_primGenRerouteKind = PrimGenRerouteKind::None;
|
||||||
|
const MG_Config::QuirkOverride overrideSetting = MG_Config::Features.MagmaPrimGenQueryReroute;
|
||||||
|
// Without stream queries the GENERATED path never opens a slot at all, so
|
||||||
|
// there is nothing to reroute - whatever the override says.
|
||||||
|
if (!m_xfbQueriesSupported || !m_hostQueryResetEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Bool primitivesGeneratedQueryUsable =
|
||||||
|
m_primitivesGeneratedQueryFeatureEnabled && m_primitivesGeneratedQueryDiscardFeatureEnabled;
|
||||||
|
PrimitivesGeneratedNoXfbVerdict verdict = PrimitivesGeneratedNoXfbVerdict::Inconclusive;
|
||||||
|
// The probe only matters under Auto (ForceOn bypasses the verdict, ForceOff
|
||||||
|
// never asks), and the answer is a device property - so it is memoized per
|
||||||
|
// process rather than re-paid on every renderer recreation.
|
||||||
|
if (overrideSetting == MG_Config::QuirkOverride::Auto) {
|
||||||
|
static const PrimitivesGeneratedNoXfbMeasurement s_measurement = [&]() {
|
||||||
|
PrimitivesGeneratedNoXfbProbeContext probeContext;
|
||||||
|
probeContext.device = m_device;
|
||||||
|
probeContext.queue = m_graphicsQueue;
|
||||||
|
probeContext.queueFamilyIndex =
|
||||||
|
static_cast<Uint32>(m_physicalDevice.queueFamilies.graphicsFamily);
|
||||||
|
probeContext.transformFeedbackQueriesUsable = m_xfbQueriesSupported;
|
||||||
|
probeContext.primitivesGeneratedQueryUsable = primitivesGeneratedQueryUsable;
|
||||||
|
probeContext.pipelineStatisticsEnabled = m_pipelineStatisticsQueryFeatureEnabled;
|
||||||
|
probeContext.tessellationEnabled = m_tessellationShaderFeatureEnabled;
|
||||||
|
auto& fns = probeContext.fns;
|
||||||
|
fns.vkCreateCommandPool = vkCreateCommandPool;
|
||||||
|
fns.vkDestroyCommandPool = vkDestroyCommandPool;
|
||||||
|
fns.vkAllocateCommandBuffers = vkAllocateCommandBuffers;
|
||||||
|
fns.vkBeginCommandBuffer = vkBeginCommandBuffer;
|
||||||
|
fns.vkEndCommandBuffer = vkEndCommandBuffer;
|
||||||
|
fns.vkCreateQueryPool = vkCreateQueryPool;
|
||||||
|
fns.vkDestroyQueryPool = vkDestroyQueryPool;
|
||||||
|
fns.vkCmdResetQueryPool = vkCmdResetQueryPool;
|
||||||
|
fns.vkCmdBeginQuery = vkCmdBeginQuery;
|
||||||
|
fns.vkCmdEndQuery = vkCmdEndQuery;
|
||||||
|
fns.vkCmdBeginQueryIndexedEXT = s_vkCmdBeginQueryIndexedEXT;
|
||||||
|
fns.vkCmdEndQueryIndexedEXT = s_vkCmdEndQueryIndexedEXT;
|
||||||
|
fns.vkCreateRenderPass = vkCreateRenderPass;
|
||||||
|
fns.vkDestroyRenderPass = vkDestroyRenderPass;
|
||||||
|
fns.vkCreateFramebuffer = vkCreateFramebuffer;
|
||||||
|
fns.vkDestroyFramebuffer = vkDestroyFramebuffer;
|
||||||
|
fns.vkCmdBeginRenderPass = vkCmdBeginRenderPass;
|
||||||
|
fns.vkCmdEndRenderPass = vkCmdEndRenderPass;
|
||||||
|
fns.vkCreateShaderModule = vkCreateShaderModule;
|
||||||
|
fns.vkDestroyShaderModule = vkDestroyShaderModule;
|
||||||
|
fns.vkCreatePipelineLayout = vkCreatePipelineLayout;
|
||||||
|
fns.vkDestroyPipelineLayout = vkDestroyPipelineLayout;
|
||||||
|
fns.vkCreateGraphicsPipelines = vkCreateGraphicsPipelines;
|
||||||
|
fns.vkDestroyPipeline = vkDestroyPipeline;
|
||||||
|
fns.vkCmdBindPipeline = vkCmdBindPipeline;
|
||||||
|
fns.vkCmdDraw = vkCmdDraw;
|
||||||
|
fns.vkCreateFence = vkCreateFence;
|
||||||
|
fns.vkDestroyFence = vkDestroyFence;
|
||||||
|
fns.vkQueueSubmit = vkQueueSubmit;
|
||||||
|
fns.vkWaitForFences = vkWaitForFences;
|
||||||
|
fns.vkGetQueryPoolResults = vkGetQueryPoolResults;
|
||||||
|
fns.vkDeviceWaitIdle = vkDeviceWaitIdle;
|
||||||
|
return RunPrimitivesGeneratedNoXfbProbe(probeContext);
|
||||||
|
}();
|
||||||
|
verdict = EvaluatePrimitivesGeneratedNoXfbVerdict(s_measurement);
|
||||||
|
if (s_measurement.fenceWaitTimedOut) {
|
||||||
|
// The probe's submission never signaled within its bound, so it left its
|
||||||
|
// command pool, query pools, render pass, framebuffer, shader modules,
|
||||||
|
// pipeline layout, pipelines and fence alive on purpose. This device is the
|
||||||
|
// renderer's own and outlives them, so nothing here may destroy them or
|
||||||
|
// wait the device idle - the queue may still be executing that submission,
|
||||||
|
// and an idle wait is the hang the bound exists to prevent. They leak for
|
||||||
|
// the process's life; a device this sick has bigger problems.
|
||||||
|
MGLOG_W("PRIMITIVES_GENERATED probe timed out waiting on its own submission (%s); its "
|
||||||
|
"Vulkan objects are deliberately leaked and XFB-inactive draws keep the stream "
|
||||||
|
"query", s_measurement.failureReason.c_str());
|
||||||
|
} else if (!s_measurement.ran) {
|
||||||
|
MGLOG_W("PRIMITIVES_GENERATED probe did not run (%s); XFB-inactive draws keep the "
|
||||||
|
"stream query", s_measurement.failureReason.c_str());
|
||||||
|
} else {
|
||||||
|
const auto logShape = [](const char* name,
|
||||||
|
const MG_Util::SelfTest::PrimitivesGeneratedNoXfbShapeMeasurement&
|
||||||
|
shape) {
|
||||||
|
MGLOG_I("PRIMITIVES_GENERATED probe %s: drawn=%d stream=%llu/%llu pgq=%llu(%d) "
|
||||||
|
"stat=%llu(%d)",
|
||||||
|
name, shape.drawn ? 1 : 0,
|
||||||
|
static_cast<unsigned long long>(shape.streamGenerated),
|
||||||
|
static_cast<unsigned long long>(shape.expectedPrimitives),
|
||||||
|
static_cast<unsigned long long>(shape.primitivesGeneratedExt),
|
||||||
|
shape.primitivesGeneratedExtMeasured ? 1 : 0,
|
||||||
|
static_cast<unsigned long long>(shape.statisticsClippingInput),
|
||||||
|
shape.statisticsMeasured ? 1 : 0);
|
||||||
|
};
|
||||||
|
logShape("triangles", s_measurement.trianglesPlain);
|
||||||
|
logShape("triangles+discard", s_measurement.trianglesDiscard);
|
||||||
|
logShape("patches+discard", s_measurement.patchesDiscard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A driver whose stream query counts capture-less draws counts a PAUSED span's
|
||||||
|
// draws through the stream slot they take, so that span's result must not have
|
||||||
|
// the frontend's CPU paused counter added on top of it either (the pre-reroute
|
||||||
|
// accounting did exactly that, double counting every paused draw the CPU could
|
||||||
|
// price). Measured, not assumed: the forced arms never ask the probe and leave
|
||||||
|
// this false.
|
||||||
|
m_primGenStreamCountsXfbInactiveDraws = verdict == PrimitivesGeneratedNoXfbVerdict::StreamCounts;
|
||||||
|
m_primGenRerouteKind = ChoosePrimitivesGeneratedReroute(
|
||||||
|
overrideSetting, verdict, primitivesGeneratedQueryUsable, m_pipelineStatisticsQueryFeatureEnabled);
|
||||||
|
if (m_primGenRerouteKind != PrimGenRerouteKind::None) {
|
||||||
|
MGLOG_I("PRIMITIVES_GENERATED for XFB-inactive draws will accumulate through a %s pool%s",
|
||||||
|
m_primGenRerouteKind == PrimGenRerouteKind::PrimitivesGeneratedExt
|
||||||
|
? "VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT"
|
||||||
|
: "clipping-invocations pipeline-statistics",
|
||||||
|
overrideSetting == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VulkanRenderer::CreateAllocator() {
|
void VulkanRenderer::CreateAllocator() {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include "MG_Util/Math/VectorTypes.h"
|
#include "MG_Util/Math/VectorTypes.h"
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
#include <MG_Backend/BackendObject.h>
|
#include <MG_Backend/BackendObject.h>
|
||||||
|
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
|
||||||
#include <vk_mem_alloc.h>
|
#include <vk_mem_alloc.h>
|
||||||
|
|
||||||
#include "../VkIncludes.h"
|
#include "../VkIncludes.h"
|
||||||
@@ -717,15 +718,74 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<Uint32> m_xfbQueryActiveSlots[2];
|
Vector<Uint32> m_xfbQueryActiveSlots[2];
|
||||||
Bool m_xfbQuerySlotOpen = false;
|
Bool m_xfbQuerySlotOpen = false;
|
||||||
Uint32 m_xfbQueryOpenSlot = 0;
|
Uint32 m_xfbQueryOpenSlot = 0;
|
||||||
|
// GL_PRIMITIVES_GENERATED reroute for draws made while transform feedback is
|
||||||
|
// INACTIVE. The stream pool's primitivesNeeded is defined to count those draws
|
||||||
|
// too, but a Mali driver (and Mesa lavapipe) answers 0 unless a capture span
|
||||||
|
// is open (the CTS's tessellator-measuring shape). Where the bring-up probe
|
||||||
|
// finds that defect with a working control - or
|
||||||
|
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE forces it - such draws accumulate the
|
||||||
|
// GENERATED count through this pool instead, whose type the arming picks:
|
||||||
|
// VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT where the device hosts the dedicated
|
||||||
|
// query with its rasterizer-discard feature (exact semantics by definition -
|
||||||
|
// the extension exists because GL needs this count without a capture), else a
|
||||||
|
// VK_QUERY_TYPE_PIPELINE_STATISTICS pool over clipping-stage invocations (one
|
||||||
|
// per primitive reaching primitive clipping - after every vertex processing
|
||||||
|
// stage, before rasterizer discard - which is the same set).
|
||||||
|
// XFB-ACTIVE draws keep the stream slot (exact today, and WRITTEN needs it);
|
||||||
|
// every draw with no open capture - a PAUSED span's draws included - takes a
|
||||||
|
// reroute slot, and the span then ignores the frontend's CPU paused-primitive
|
||||||
|
// counter rather than adding it on top (see IsPrimGenRerouteArmed): that
|
||||||
|
// counter is written by only 3 of the ~15 draw entry points and answers 0 for
|
||||||
|
// GL_PATCHES, so it cannot price the draws this reroute exists to repair. One
|
||||||
|
// GL query span may therefore hold slots of both pools.
|
||||||
|
Bool m_pipelineStatisticsQueryFeatureEnabled = false;
|
||||||
|
// VK_EXT_primitives_generated_query: base feature, and the
|
||||||
|
// ...WithRasterizerDiscard feature without which a discarding draw inside the
|
||||||
|
// query is invalid usage (so the reroute never picks the dedicated pool on a
|
||||||
|
// base-only device - GL applications toggle discard freely).
|
||||||
|
Bool m_primitivesGeneratedQueryFeatureEnabled = false;
|
||||||
|
Bool m_primitivesGeneratedQueryDiscardFeatureEnabled = false;
|
||||||
|
// tessellationShader was enabled at device creation (it is taken whenever the
|
||||||
|
// device advertises it); gates the probe's PATCHES shape.
|
||||||
|
Bool m_tessellationShaderFeatureEnabled = false;
|
||||||
|
MG_Util::SelfTest::PrimGenRerouteKind m_primGenRerouteKind =
|
||||||
|
MG_Util::SelfTest::PrimGenRerouteKind::None;
|
||||||
|
// The bring-up probe measured this device's stream query as counting draws made
|
||||||
|
// with no capture span open (the StreamCounts verdict) - so it counts the
|
||||||
|
// PAUSED-span ones too, through the stream slot they take when nothing is
|
||||||
|
// rerouted. Only the probe can know this, so it stays false wherever the probe
|
||||||
|
// is not consulted (the forced arms), which keeps those lanes' accounting as it
|
||||||
|
// was.
|
||||||
|
Bool m_primGenStreamCountsXfbInactiveDraws = false;
|
||||||
|
VkQueryPool m_primGenReroutePool = VK_NULL_HANDLE;
|
||||||
|
Uint32 m_primGenRerouteSlotCursor = 0;
|
||||||
|
Vector<Uint32> m_primGenRerouteActiveSlots;
|
||||||
|
Bool m_primGenRerouteSlotOpen = false;
|
||||||
|
Uint32 m_primGenRerouteOpenSlot = 0;
|
||||||
|
// Runs the bring-up probe (memoized per process) and decides
|
||||||
|
// m_primGenRerouteKind. Called at the end of device creation: it records on
|
||||||
|
// m_graphicsQueue, which nothing else is using yet.
|
||||||
|
void ArmPrimGenReroute();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
// Whether a GENERATED span opened now will have the draws made while the GL
|
||||||
|
// span is PAUSED counted on the GPU - through the reroute pool, which takes
|
||||||
|
// every draw with no open capture, or (where the reroute is not armed because
|
||||||
|
// the stream query was measured to count capture-less draws) through the stream
|
||||||
|
// slot such a draw still takes. The frontend's CPU paused-primitive counter
|
||||||
|
// must not be added on top of either: it would double count, and it cannot
|
||||||
|
// price the draws that matter anyway - only 3 of the ~15 draw entry points
|
||||||
|
// write it and it answers 0 for GL_PATCHES. Read once per span, after
|
||||||
|
// StartXfbQueryCapture (whose pool creation may disarm the reroute).
|
||||||
|
Bool ArePausedDrawsGpuCounted() const;
|
||||||
// kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED.
|
// kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED.
|
||||||
Bool StartXfbQueryCapture(Uint32 kind);
|
Bool StartXfbQueryCapture(Uint32 kind);
|
||||||
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots);
|
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots, Vector<Uint32>& outRerouteSlots);
|
||||||
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives);
|
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, const Vector<Uint32>& rerouteSlots,
|
||||||
|
Bool wantGenerated, Uint64& outPrimitives);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer, Bool xfbActive);
|
||||||
void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
||||||
|
|
||||||
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||||
|
|||||||
@@ -101,8 +101,10 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/VertexAttribBindingScenario.cpp
|
Scenarios/VertexAttribBindingScenario.cpp
|
||||||
Scenarios/XfbCaptureBufferReuseScenario.cpp
|
Scenarios/XfbCaptureBufferReuseScenario.cpp
|
||||||
Scenarios/XfbPrimitiveQueryScenario.cpp
|
Scenarios/XfbPrimitiveQueryScenario.cpp
|
||||||
|
Scenarios/PrimitivesGeneratedNoXfbScenario.cpp
|
||||||
Scenarios/XfbRepeatedCaptureScenario.cpp
|
Scenarios/XfbRepeatedCaptureScenario.cpp
|
||||||
Scenarios/TessellationXfbCaptureScenario.cpp
|
Scenarios/TessellationXfbCaptureScenario.cpp
|
||||||
|
Scenarios/PointSizeDemotionScenario.cpp
|
||||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||||
Scenarios/CopyImageLayeredScenario.cpp
|
Scenarios/CopyImageLayeredScenario.cpp
|
||||||
@@ -113,6 +115,7 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/LayeredAttachmentShapeScenario.cpp
|
Scenarios/LayeredAttachmentShapeScenario.cpp
|
||||||
Scenarios/LayeredTextureReadbackScenario.cpp
|
Scenarios/LayeredTextureReadbackScenario.cpp
|
||||||
Scenarios/AtomicCounterScenario.cpp
|
Scenarios/AtomicCounterScenario.cpp
|
||||||
|
Scenarios/LargeArenaAdoptionScenario.cpp
|
||||||
Scenarios/SsboArrayDynamicIndexScenario.cpp
|
Scenarios/SsboArrayDynamicIndexScenario.cpp
|
||||||
Scenarios/StorageBufferRegrowScenario.cpp
|
Scenarios/StorageBufferRegrowScenario.cpp
|
||||||
Scenarios/SpirvShaderBinaryScenario.cpp
|
Scenarios/SpirvShaderBinaryScenario.cpp
|
||||||
@@ -365,6 +368,25 @@ mgl_itest_join_environment(MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT
|
|||||||
${MGL_ITEST_COMMON_ENV})
|
${MGL_ITEST_COMMON_ENV})
|
||||||
mgl_itest_join_environment(MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT
|
mgl_itest_join_environment(MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT
|
||||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1" ${MGL_ITEST_COMMON_ENV})
|
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1" ${MGL_ITEST_COMMON_ENV})
|
||||||
|
# Same shape as the UnlocatedIoBlocks entry: the log path is where the reroute's latched
|
||||||
|
# MGLOG_I lands, and the arming case only trusts the bytes written after it started.
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_VULKAN_PRIMGEN_REROUTE_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1"
|
||||||
|
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/primgen-query-reroute.log"
|
||||||
|
${MGL_ITEST_VULKAN_ENV})
|
||||||
|
# The point-size demotion pinned on, per backend, with a per-lane log file for the arming
|
||||||
|
# assertion - the same MOBILEGL_LOG_FILE_PATH reasoning as the UnlocatedIoBlocks lane above.
|
||||||
|
# Two lanes because the demotion runs in the SHARED phase-B chain and each backend then
|
||||||
|
# consumes it differently (Espryt respells the driver-side capture request, Magma binds the
|
||||||
|
# SPIR-V Xfb decorations to the carrier).
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_GLES_POINT_SIZE_DEMOTION_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_POINT_SIZE_DEMOTION=1"
|
||||||
|
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/point-size-demotion-gles.log"
|
||||||
|
${MGL_ITEST_COMMON_ENV})
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_VULKAN_POINT_SIZE_DEMOTION_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_POINT_SIZE_DEMOTION=1"
|
||||||
|
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/point-size-demotion-vulkan.log"
|
||||||
|
${MGL_ITEST_VULKAN_ENV})
|
||||||
|
|
||||||
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
|
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
|
||||||
set(MGL_ITEST_TIMEOUT 120)
|
set(MGL_ITEST_TIMEOUT 120)
|
||||||
@@ -545,6 +567,25 @@ gtest_discover_tests(MobileGLIntegrationTest
|
|||||||
ENVIRONMENT "${MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT}"
|
ENVIRONMENT "${MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# PrimitivesGeneratedNoXfbScenario again, with the GL_PRIMITIVES_GENERATED statistics
|
||||||
|
# reroute PINNED ON. The ambient DirectVulkan registration runs the same cases under the
|
||||||
|
# bring-up probe's Auto verdict, so between the two entries both accounting paths answer
|
||||||
|
# the same GL questions and must produce the same numbers - the "two pools must agree"
|
||||||
|
# gate this machine can hold that the affected device cannot. The pinned entry is also
|
||||||
|
# the only one whose arming case runs: it asserts the renderer's latched MGLOG_I, so a
|
||||||
|
# silently-disarmed reroute (an inverted override mapping, a lost gate) fails here
|
||||||
|
# instead of leaving every equality case vacuously green. DirectVulkan only - the flag
|
||||||
|
# steers nothing on DirectGLES.
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectVulkan.PrimGenReroute."
|
||||||
|
TEST_FILTER "PrimitivesGeneratedNoXfbScenario.*"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS integration-gpu
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_VULKAN_PRIMGEN_REROUTE_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
|
||||||
# The packed16 copy scenarios again, with the 8-bit storage widening PINNED ON. The ambient
|
# The packed16 copy scenarios again, with the 8-bit storage widening PINNED ON. The ambient
|
||||||
# registrations above cover the narrow storage - on every CI driver the widening's POST
|
# registrations above cover the narrow storage - on every CI driver the widening's POST
|
||||||
# probe finds no field-order mirror, so Auto keeps the native 16-bit path - which means the
|
# probe finds no field-order mirror, so Auto keeps the native 16-bit path - which means the
|
||||||
@@ -562,3 +603,30 @@ gtest_discover_tests(MobileGLIntegrationTest
|
|||||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
ENVIRONMENT "${MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT}"
|
ENVIRONMENT "${MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# PointSizeDemotionScenario with the demotion PINNED ON, per backend, for the reason every
|
||||||
|
# pinned lane above exists: llvmpipe and lavapipe both HOST gl_PointSize in tessellation and
|
||||||
|
# geometry stages, so the ambient registrations run these captures through the built-in and
|
||||||
|
# the demotion - the path every affected Mali device actually takes - would execute nowhere.
|
||||||
|
# The ambient runs stay the negative control: same scenario, same CPU-computed bytes, native
|
||||||
|
# path. Both backends, because the demotion is shared phase-B work with two different
|
||||||
|
# consumers (the ESSL capture respelling vs the SPIR-V Xfb carrier binding).
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES.PointSizeDemotion."
|
||||||
|
TEST_FILTER "PointSizeDemotionScenario.*"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS integration-gpu
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_POINT_SIZE_DEMOTION_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectVulkan.PointSizeDemotion."
|
||||||
|
TEST_FILTER "PointSizeDemotionScenario.*"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS integration-gpu
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_VULKAN_POINT_SIZE_DEMOTION_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
|||||||
@@ -226,25 +226,21 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A CPU glBufferSubData issued AFTER a dispatch, read back with NO further GPU work in
|
// A CPU glBufferSubData issued AFTER a dispatch, read back with NO further GPU work in
|
||||||
// between. The DirectGLES backend queues app SubData ranges for the draw-time staged-copy
|
// between. Each backend has its own way to invert this pair, and both are pinned here.
|
||||||
// flush (the upload ring) instead of uploading in place, and readback of a GPU-written
|
// DirectGLES queues app SubData ranges for the draw-time staged-copy flush (the upload
|
||||||
// buffer overwrites the frontend shadow with the driver copy - so if the readback path
|
// ring) instead of uploading in place, and readback of a GPU-written buffer overwrites
|
||||||
// forgets to flush the queued range first, the newer CPU write is REVERTED by the readback
|
// the frontend shadow with the driver copy - so if the readback path forgets to flush the
|
||||||
// and offset 0 reads the dispatch's value instead of the reseed. Offset 4 pins the other
|
// queued range first, the newer CPU write is REVERTED by the readback and offset 0 reads
|
||||||
// direction: the flush must not clobber GPU results outside the written range.
|
// the dispatch's value instead of the reseed. DirectVulkan adopts the buffer into
|
||||||
|
// coherent GPU memory the moment the dispatch resolves its descriptor, so the SubData
|
||||||
|
// write lands in the very bytes the GPU reads - while the dispatch still sits recorded in
|
||||||
|
// the deferred frame command buffer. Unless the frontend retires that pending work before
|
||||||
|
// writing the adopted store (BufferObject::UploadSubData), the dispatch executes ON TOP
|
||||||
|
// of the reseed and offset 0 reads reseed + increments instead of the reseed. Offset 4
|
||||||
|
// pins the other direction for both: the upload must leave bytes outside its range - the
|
||||||
|
// dispatch's results - untouched.
|
||||||
TEST_F(AtomicCounterScenario, SubDataAfterDispatchSurvivesAnImmediateReadback) {
|
TEST_F(AtomicCounterScenario, SubDataAfterDispatchSurvivesAnImmediateReadback) {
|
||||||
if (!Ready() || IsSkipped()) return;
|
if (!Ready() || IsSkipped()) return;
|
||||||
// DirectGLES-only for now. DirectVulkan fails this case with or without the upload
|
|
||||||
// ring, on revisions that predate it: its buffer uploads submit immediately while the
|
|
||||||
// dispatch sits in the deferred frame command buffer, so the GPU increments the
|
|
||||||
// RESEEDED value (reads 4242 + increments instead of 4242) - a pre-existing
|
|
||||||
// upload-vs-recorded-work ordering gap in that backend, kept visible here rather than
|
|
||||||
// silently absorbed. Un-skip once DirectVulkan orders app uploads against already
|
|
||||||
// recorded GPU work.
|
|
||||||
if (Gl().BackendName() != std::string("DirectGLES")) {
|
|
||||||
GTEST_SKIP() << "SubData-after-dispatch ordering is a known DirectVulkan gap; this case pins the "
|
|
||||||
"DirectGLES readback pre-flush only";
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLuint zero = MakeCounterBuffer(0, {0u, 0u});
|
const GLuint zero = MakeCounterBuffer(0, {0u, 0u});
|
||||||
MakeCounterBuffer(1, {0u});
|
MakeCounterBuffer(1, {0u});
|
||||||
|
|||||||
@@ -0,0 +1,278 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.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 - MESH-ARENA-SIZED BUFFERS, END TO END.
|
||||||
|
//
|
||||||
|
// A buffer store of at least 16MiB is adopted into the backend's persistently and
|
||||||
|
// coherently mapped GPU storage the moment it is defined (BufferObject::
|
||||||
|
// TryAdoptLargeStorage): the CPU shadow is dropped and every later write lands
|
||||||
|
// directly in GPU-visible memory with no per-write driver call. Minecraft 26.3
|
||||||
|
// streams chunk meshes into 128MB vertex arenas with plain glNamedBufferSubData -
|
||||||
|
// on Mali, every driver-mediated route for that write into a busy mutable store
|
||||||
|
// either parks the calling thread or ghost-copies the whole arena on a driver
|
||||||
|
// worker (~167ms per touched arena: the recurring in-world hiccup this adoption
|
||||||
|
// removed). Every existing buffer scenario uses stores far below the threshold,
|
||||||
|
// so without this file the adopted path would have zero coverage.
|
||||||
|
//
|
||||||
|
// What is pinned, deliberately through the same API mix Minecraft uses:
|
||||||
|
// * a glBufferSubData written AFTER the arena was drawn (in flight) reaches the
|
||||||
|
// next draw - the write-visibility contract adoption must not weaken;
|
||||||
|
// * GetBufferSubData reads back the latest CPU write - the shadow IS the map;
|
||||||
|
// * a compute-shader write through an SSBO binding of the same arena is read
|
||||||
|
// back - the GPU-written path for adopted stores (glFinish + direct read).
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstring>
|
||||||
|
#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 {
|
||||||
|
|
||||||
|
// Comfortably past the 16MiB adoption threshold, and the vertex payload sits
|
||||||
|
// deep inside the store so an implementation that quietly clamped or aliased
|
||||||
|
// the adopted range would miss it.
|
||||||
|
constexpr GLsizeiptr kArenaBytes = GLsizeiptr(24) * 1024 * 1024;
|
||||||
|
constexpr GLintptr kVertexOffset = GLintptr(20) * 1024 * 1024;
|
||||||
|
|
||||||
|
constexpr const char* kVertexSource = R"(#version 430 core
|
||||||
|
layout(location = 0) in vec2 a_pos;
|
||||||
|
layout(location = 1) in vec3 a_color;
|
||||||
|
out vec3 v_color;
|
||||||
|
void main() {
|
||||||
|
v_color = a_color;
|
||||||
|
gl_Position = vec4(a_pos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kFragmentSource = R"(#version 430 core
|
||||||
|
in vec3 v_color;
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = vec4(v_color, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kMarkerComputeSource = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
layout(std430, binding = 0) buffer Arena { uint word; };
|
||||||
|
void main() { word = 0xC0FFEEu; }
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
float r, g, b;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A full-viewport quad, colored uniformly so one center readback speaks for
|
||||||
|
// the whole draw.
|
||||||
|
std::vector<Vertex> QuadVertices(float r, float g, float b) {
|
||||||
|
return {
|
||||||
|
{-1.f, -1.f, r, g, b}, {1.f, -1.f, r, g, b}, {1.f, 1.f, r, g, b},
|
||||||
|
{-1.f, -1.f, r, g, b}, {1.f, 1.f, r, g, b}, {-1.f, 1.f, r, g, b},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class LargeArenaAdoptionScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
m_program = LinkProgram(kVertexSource, kFragmentSource);
|
||||||
|
ASSERT_NE(m_program, 0u) << m_buildLog;
|
||||||
|
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glGenBuffers(1, &m_arena);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||||
|
// The NULL-data definition is the adoption point (and Minecraft's
|
||||||
|
// arena-creation idiom).
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, kArenaBytes, nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||||
|
reinterpret_cast<void*>(kVertexOffset));
|
||||||
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||||
|
reinterpret_cast<void*>(kVertexOffset + 2 * sizeof(float)));
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
if (m_arena != 0) glDeleteBuffers(1, &m_arena);
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
if (m_compute != 0) glDeleteProgram(m_compute);
|
||||||
|
m_vao = 0;
|
||||||
|
m_arena = 0;
|
||||||
|
m_program = 0;
|
||||||
|
m_compute = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int CompileStage(GLenum stage, const char* source) {
|
||||||
|
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) {
|
||||||
|
char log[2048] = {};
|
||||||
|
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||||
|
m_buildLog = std::string("shader did not compile: ") + log;
|
||||||
|
glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int LinkProgram(const char* vs, const char* fs) {
|
||||||
|
const GLuint v = CompileStage(GL_VERTEX_SHADER, vs);
|
||||||
|
if (v == 0) return 0;
|
||||||
|
const GLuint f = CompileStage(GL_FRAGMENT_SHADER, fs);
|
||||||
|
if (f == 0) {
|
||||||
|
glDeleteShader(v);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, v);
|
||||||
|
glAttachShader(program, f);
|
||||||
|
glLinkProgram(program);
|
||||||
|
glDeleteShader(v);
|
||||||
|
glDeleteShader(f);
|
||||||
|
GLint linked = 0;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||||
|
m_buildLog = std::string("program did not link: ") + log;
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UploadQuad(float r, float g, float b) {
|
||||||
|
const auto vertices = QuadVertices(r, g, b);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
|
||||||
|
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawQuad() {
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<unsigned char, 4> CenterPixel() {
|
||||||
|
std::array<unsigned char, 4> px = {0, 0, 0, 0};
|
||||||
|
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
|
px.data());
|
||||||
|
return px;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int m_program = 0;
|
||||||
|
unsigned int m_compute = 0;
|
||||||
|
unsigned int m_vao = 0;
|
||||||
|
unsigned int m_arena = 0;
|
||||||
|
std::string m_buildLog;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The Minecraft shape: the arena is drawn, the frame retires, and a
|
||||||
|
// glBufferSubData rewrites the SAME vertex bytes while the previous frame's
|
||||||
|
// draw may still be in flight. The next draw must show the NEW bytes.
|
||||||
|
TEST_F(LargeArenaAdoptionScenario, SubDataAfterAnInFlightDrawReachesTheNextDraw) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
UploadQuad(1.f, 0.f, 0.f);
|
||||||
|
DrawQuad();
|
||||||
|
auto px = CenterPixel();
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_GT(px[0], 200) << "the first draw from the adopted arena never landed";
|
||||||
|
EXPECT_LT(px[1], 50);
|
||||||
|
|
||||||
|
Gl().EndFrame();
|
||||||
|
|
||||||
|
UploadQuad(0.f, 1.f, 0.f);
|
||||||
|
DrawQuad();
|
||||||
|
px = CenterPixel();
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_GT(px[1], 200) << "the cross-frame rewrite of the adopted arena did not reach the draw; "
|
||||||
|
"the old color means the write went to bytes the draw no longer reads";
|
||||||
|
EXPECT_LT(px[0], 50) << "the draw still shows the previous frame's bytes";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The shadow IS the mapping: a readback straight after a CPU write must hand
|
||||||
|
// back exactly those bytes.
|
||||||
|
TEST_F(LargeArenaAdoptionScenario, ReadbackSeesTheLatestCpuWrite) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
const auto vertices = QuadVertices(0.25f, 0.5f, 0.75f);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
|
||||||
|
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
||||||
|
std::vector<Vertex> read(vertices.size());
|
||||||
|
glGetBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
|
||||||
|
GLsizeiptr(read.size() * sizeof(Vertex)), read.data());
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_EQ(0, std::memcmp(read.data(), vertices.data(), read.size() * sizeof(Vertex)))
|
||||||
|
<< "GetBufferSubData of the adopted arena returned different bytes than the SubData wrote";
|
||||||
|
}
|
||||||
|
|
||||||
|
// A GPU write through an SSBO binding of the adopted arena must be visible to
|
||||||
|
// a CPU readback - the path that waits out the GPU and reads the coherent
|
||||||
|
// mapping directly.
|
||||||
|
TEST_F(LargeArenaAdoptionScenario, GpuWriteIntoTheArenaIsReadBack) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
GLint maxComputeStorageBlocks = 0;
|
||||||
|
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &maxComputeStorageBlocks);
|
||||||
|
if (maxComputeStorageBlocks < 1) {
|
||||||
|
GTEST_SKIP() << "no compute shader storage blocks on this driver";
|
||||||
|
}
|
||||||
|
const GLuint compute = CompileStage(GL_COMPUTE_SHADER, kMarkerComputeSource);
|
||||||
|
ASSERT_NE(compute, 0u) << m_buildLog;
|
||||||
|
m_compute = glCreateProgram();
|
||||||
|
glAttachShader(m_compute, compute);
|
||||||
|
glLinkProgram(m_compute);
|
||||||
|
glDeleteShader(compute);
|
||||||
|
GLint linked = 0;
|
||||||
|
glGetProgramiv(m_compute, GL_LINK_STATUS, &linked);
|
||||||
|
ASSERT_EQ(linked, GL_TRUE);
|
||||||
|
|
||||||
|
const unsigned int seed = 0u;
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(seed), &seed);
|
||||||
|
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, m_arena, 0, sizeof(unsigned int));
|
||||||
|
glUseProgram(m_compute);
|
||||||
|
glDispatchCompute(1, 1, 1);
|
||||||
|
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||||
|
|
||||||
|
unsigned int marker = 0;
|
||||||
|
glGetBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(marker), &marker);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_EQ(marker, 0xC0FFEEu)
|
||||||
|
<< "the compute write into the adopted arena did not reach the CPU readback";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,522 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PointSizeDemotionScenario.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 - THE gl_PointSize DEMOTION IS CLIENT-INVISIBLE, AND IT ACTUALLY ARMS.
|
||||||
|
//
|
||||||
|
// On a device that hosts the built-in in tessellation/geometry stages (llvmpipe and
|
||||||
|
// lavapipe both do), gl_PointSize travels as itself; on one that does not (the Mali
|
||||||
|
// devices this exists for), phase B demotes it to an ordinary varying
|
||||||
|
// (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram) and the capture
|
||||||
|
// machinery follows it there. This scenario runs in BOTH configurations and must hand
|
||||||
|
// back identical bytes: the ambient registrations take the native path, and the
|
||||||
|
// PointSizeDemotion. registrations pin MOBILEGL_POINT_SIZE_DEMOTION=1 so the demotion
|
||||||
|
// runs on the same healthy drivers - CopyImagePacked16Scenario's dual-configuration
|
||||||
|
// contract, applied to a value chain instead of a storage format.
|
||||||
|
//
|
||||||
|
// The VALUE is the whole contract: every case writes gl_PointSize in one stage, reads it
|
||||||
|
// back out of gl_in[] in the next, and captures it by name under rasterizer discard, so
|
||||||
|
// one wrong link anywhere in VS -> TCS -> TES -> GS -> capture lands in the readback.
|
||||||
|
// The RASTERIZED size is deliberately not asserted anywhere: with the built-in unhosted
|
||||||
|
// it falls back to 1.0 by spec on both targets, which is exactly the honest residue the
|
||||||
|
// demotion documents (point_rendering-style bodies keep failing truthfully).
|
||||||
|
//
|
||||||
|
// 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 <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#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 {
|
||||||
|
|
||||||
|
constexpr float kPoison = -987654.0f;
|
||||||
|
|
||||||
|
const char* const kFragmentSource = R"(#version 460 core
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The full chain, with per-vertex VARIATION seeded in the vertex stage so a control
|
||||||
|
// invocation that read or wrote the wrong slot changes the sum: 2,3,4 arrive, 3,4,5
|
||||||
|
// leave, the evaluation stage sums its patch to 12, the geometry stage doubles what
|
||||||
|
// it read to 24.
|
||||||
|
const char* const kChainVertexSource = R"(#version 460 core
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
gl_PointSize = 2.0 + float(gl_VertexID);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kChainTessControlSource = R"(#version 460 core
|
||||||
|
layout(vertices = 3) out;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
|
||||||
|
gl_out[gl_InvocationID].gl_PointSize = gl_in[gl_InvocationID].gl_PointSize + 1.0;
|
||||||
|
gl_TessLevelOuter[0] = 1.0;
|
||||||
|
gl_TessLevelOuter[1] = 1.0;
|
||||||
|
gl_TessLevelOuter[2] = 1.0;
|
||||||
|
gl_TessLevelInner[0] = 1.0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kChainTessEvalSource = R"(#version 460 core
|
||||||
|
layout(triangles, equal_spacing, cw, point_mode) in;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
gl_PointSize = gl_in[0].gl_PointSize + gl_in[1].gl_PointSize + gl_in[2].gl_PointSize;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kChainGeometrySource = R"(#version 460 core
|
||||||
|
layout(points) in;
|
||||||
|
layout(points, max_vertices = 1) out;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = gl_in[0].gl_Position;
|
||||||
|
gl_PointSize = gl_in[0].gl_PointSize * 2.0;
|
||||||
|
EmitVertex();
|
||||||
|
EndPrimitive();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The geometry-only chain: no tessellation required of the stack at all.
|
||||||
|
const char* const kPointVertexSource = R"(#version 460 core
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
gl_PointSize = 7.0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kPointGeometrySource = R"(#version 460 core
|
||||||
|
layout(points) in;
|
||||||
|
layout(points, max_vertices = 1) out;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = gl_in[0].gl_Position;
|
||||||
|
gl_PointSize = gl_in[0].gl_PointSize + 1.0;
|
||||||
|
EmitVertex();
|
||||||
|
EndPrimitive();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// A capture stage that only READS the incoming point size and never writes its own.
|
||||||
|
// Legal GL, and the shape that separates "the demotion arms" from "the demotion knows
|
||||||
|
// a capture is coming": with the built-in gone, only the capture request can put a
|
||||||
|
// carrier back for a by-name capture to bind to.
|
||||||
|
const char* const kReadOnlyGeometrySource = R"(#version 460 core
|
||||||
|
layout(points) in;
|
||||||
|
layout(points, max_vertices = 1) out;
|
||||||
|
out float g_echo;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = gl_in[0].gl_Position;
|
||||||
|
g_echo = gl_in[0].gl_PointSize;
|
||||||
|
EmitVertex();
|
||||||
|
EndPrimitive();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kEchoFragmentSource = R"(#version 460 core
|
||||||
|
in float g_echo;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
fragColor = vec4(g_echo, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
class PointSizeDemotionScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (Ready()) {
|
||||||
|
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 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 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) + "\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 `vertexCount` vertices of `drawMode`, recorded as
|
||||||
|
// GL_POINTS. The buffer is poison-filled first so bytes the capture never wrote
|
||||||
|
// name themselves.
|
||||||
|
std::vector<float> RunCaptureSpan(GLuint program, GLenum drawMode, GLsizei vertexCount,
|
||||||
|
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(GL_POINTS);
|
||||||
|
glDrawArrays(drawMode, 0, vertexCount);
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The library log, for the arming case. Same machinery and same reasoning as
|
||||||
|
// UnlocatedIoBlockScenario: MOBILEGL_LOG_FILE_PATH is read at log-init, the file
|
||||||
|
// is appended to by every process in the lane, and only bytes appended after the
|
||||||
|
// snapshot may satisfy an assertion.
|
||||||
|
static std::filesystem::path LibraryLogPath() {
|
||||||
|
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||||
|
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||||
|
: std::filesystem::path();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::uintmax_t LibraryLogSize() {
|
||||||
|
std::error_code ec;
|
||||||
|
const std::filesystem::path path = LibraryLogPath();
|
||||||
|
if (path.empty()) return 0;
|
||||||
|
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||||
|
return ec ? 0 : size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string LibraryLogSince(std::uintmax_t offset) {
|
||||||
|
const std::filesystem::path path = LibraryLogPath();
|
||||||
|
if (path.empty()) return {};
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
if (!file.good()) return {};
|
||||||
|
file.seekg(static_cast<std::streamoff>(offset));
|
||||||
|
return std::string((std::istreambuf_iterator<char>(file)),
|
||||||
|
std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string m_buildLog;
|
||||||
|
|
||||||
|
private:
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
std::vector<GLuint> m_programs;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The five-stage chain. 24.0 can only arrive if the vertex mirror, both control-stage
|
||||||
|
// redirects (read AND write), the evaluation stage's three gl_in reads and the
|
||||||
|
// geometry stage's read all carried the right value - one wrong link and the sum
|
||||||
|
// moves. point_mode with every level at 1 emits three points; the first record proves
|
||||||
|
// the mechanism, exactly as TessellationXfbCaptureScenario reasons.
|
||||||
|
TEST_F(PointSizeDemotionScenario, TheValueSurvivesTheFiveStageChainIntoTheCapture) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!BackendHostsTessellation()) {
|
||||||
|
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " ("
|
||||||
|
<< Gl().RendererString() << ")";
|
||||||
|
}
|
||||||
|
glPatchParameteri(GL_PATCH_VERTICES, 3);
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kChainVertexSource},
|
||||||
|
{GL_TESS_CONTROL_SHADER, kChainTessControlSource},
|
||||||
|
{GL_TESS_EVALUATION_SHADER, kChainTessEvalSource},
|
||||||
|
{GL_GEOMETRY_SHADER, kChainGeometrySource},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||||
|
{"gl_PointSize"});
|
||||||
|
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||||
|
|
||||||
|
const std::vector<float> captured = RunCaptureSpan(program, GL_PATCHES, 3, 3);
|
||||||
|
EXPECT_TRUE(ComponentIs(captured, 0, 24.0f));
|
||||||
|
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same chain without a geometry stage: the capture then binds to the evaluation
|
||||||
|
// stage's value (the sum, 12.0) - which is also the boundary where a demoted program
|
||||||
|
// switches its capture carrier from the Io chain to the capture name.
|
||||||
|
TEST_F(PointSizeDemotionScenario, TheEvaluationStageOwnsTheCaptureWithoutAGeometryStage) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!BackendHostsTessellation()) {
|
||||||
|
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " ("
|
||||||
|
<< Gl().RendererString() << ")";
|
||||||
|
}
|
||||||
|
glPatchParameteri(GL_PATCH_VERTICES, 3);
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kChainVertexSource},
|
||||||
|
{GL_TESS_CONTROL_SHADER, kChainTessControlSource},
|
||||||
|
{GL_TESS_EVALUATION_SHADER, kChainTessEvalSource},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||||
|
{"gl_PointSize"});
|
||||||
|
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||||
|
|
||||||
|
const std::vector<float> captured = RunCaptureSpan(program, GL_PATCHES, 3, 3);
|
||||||
|
EXPECT_TRUE(ComponentIs(captured, 0, 12.0f));
|
||||||
|
|
||||||
|
// The GL query surface keeps the truthful spelling whatever the backends renamed
|
||||||
|
// underneath: reflection is a phase-A product and the demotion happens after it.
|
||||||
|
char varyingName[64] = {};
|
||||||
|
GLsizei nameLength = 0;
|
||||||
|
GLsizei varyingSize = 0;
|
||||||
|
GLenum varyingType = 0;
|
||||||
|
glGetTransformFeedbackVarying(program, 0, sizeof(varyingName), &nameLength, &varyingSize,
|
||||||
|
&varyingType, varyingName);
|
||||||
|
EXPECT_STREQ(varyingName, "gl_PointSize");
|
||||||
|
EXPECT_EQ(varyingType, static_cast<GLenum>(GL_FLOAT));
|
||||||
|
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The geometry-only chain: gl_in[0].gl_PointSize read straight off the vertex stage,
|
||||||
|
// no tessellation involved - the VS -> GS boundary of the demotion on its own.
|
||||||
|
TEST_F(PointSizeDemotionScenario, AGeometryOnlyChainCarriesTheVertexValue) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPointVertexSource},
|
||||||
|
{GL_GEOMETRY_SHADER, kPointGeometrySource},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||||
|
{"gl_PointSize"});
|
||||||
|
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||||
|
|
||||||
|
const std::vector<float> captured = RunCaptureSpan(program, GL_POINTS, 1, 1);
|
||||||
|
EXPECT_TRUE(ComponentIs(captured, 0, 8.0f));
|
||||||
|
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE CAPTURE-REQUEST PATH, END TO END - the half no unit test can reach, because the
|
||||||
|
// request travels from glTransformFeedbackVaryings through phase A's resolved capture
|
||||||
|
// set and the phase-B handoff before it reaches the demotion.
|
||||||
|
//
|
||||||
|
// The geometry stage READS gl_in[0].gl_PointSize and never writes gl_PointSize, which
|
||||||
|
// is enough to arm the demotion (glslang declares GeometryPointSize on a read) but not
|
||||||
|
// enough to create an output carrier on its own. Only the capture request can, and if
|
||||||
|
// that request never arrives the program does not merely lose the point-size column:
|
||||||
|
// DirectGLES respells the driver-side capture to a name no stage declares and the
|
||||||
|
// WHOLE capture set fails to link, while DirectVulkan mirrors a built-in the demotion
|
||||||
|
// just removed and can unwind far enough to drop the Xfb execution mode. Either way
|
||||||
|
// g_echo - an ordinary varying with nothing to do with point size - comes back poison,
|
||||||
|
// which is what this asserts. gl_PointSize itself is captured but never asserted: no
|
||||||
|
// stage writes it, so GL leaves its value undefined.
|
||||||
|
TEST_F(PointSizeDemotionScenario, ACaptureSurvivesAStageThatOnlyReadsThePointSize) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
// The NATIVE Espryt path cannot do this at all, and never could: with the built-in
|
||||||
|
// hosted, the geometry stage's ESSL simply does not declare gl_PointSize unless it
|
||||||
|
// writes it, so the driver rejects the capture request with "varying undeclared"
|
||||||
|
// and the program becomes unusable. That is a pre-existing ES limitation the
|
||||||
|
// demotion happens to REPAIR - the carrier is a real, seeded, declared varying -
|
||||||
|
// so this case has something to assert only where the demotion is armed. Magma
|
||||||
|
// consumes SPIR-V and answers on both paths, which keeps the negative control.
|
||||||
|
if (Gl().BackendName() == "DirectGLES" &&
|
||||||
|
AmbientQuirkFromEnvironment("MOBILEGL_POINT_SIZE_DEMOTION") != AmbientQuirk::On) {
|
||||||
|
GTEST_SKIP() << "Espryt cannot capture a gl_PointSize its capture stage never "
|
||||||
|
"writes without the demotion; the PointSizeDemotion. ctest entry "
|
||||||
|
"runs this same case with MOBILEGL_POINT_SIZE_DEMOTION=1";
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPointVertexSource},
|
||||||
|
{GL_GEOMETRY_SHADER, kReadOnlyGeometrySource},
|
||||||
|
{GL_FRAGMENT_SHADER, kEchoFragmentSource}},
|
||||||
|
{"g_echo", "gl_PointSize"});
|
||||||
|
ASSERT_NE(program, 0u)
|
||||||
|
<< "the capture set failed to link. On a demoting configuration this is the "
|
||||||
|
"capture request never reaching the demotion, so the point-size capture was "
|
||||||
|
"respelled to a carrier no stage declares. Build log: "
|
||||||
|
<< m_buildLog;
|
||||||
|
|
||||||
|
const std::vector<float> captured = RunCaptureSpan(program, GL_POINTS, 1, 2);
|
||||||
|
EXPECT_TRUE(ComponentIs(captured, 0, 7.0f))
|
||||||
|
<< "the unrelated varying captured alongside gl_PointSize did not survive; the "
|
||||||
|
"point-size capture took the whole set with it";
|
||||||
|
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE ONE CASE THAT CAN FAIL WHEN THE DEMOTION SILENTLY STOPS BEING ARMED.
|
||||||
|
//
|
||||||
|
// Everything above captures the right bytes on llvmpipe and lavapipe whether the
|
||||||
|
// demotion ran or not - these machines host the built-in - so those cases pin that
|
||||||
|
// the demotion does no HARM and can say nothing about whether it happened. The
|
||||||
|
// arming is where the cheap mistake lives: MOBILEGL_POINT_SIZE_DEMOTION maps onto
|
||||||
|
// the two Supports*PointSize capability bits INVERTED (forcing the demotion on
|
||||||
|
// means declaring the built-in UNHOSTED), and a swap of those arms - or a dropped
|
||||||
|
// env bit anywhere between ConfigLoader, the backend init, CompileEnv and the L1
|
||||||
|
// key - would disable the device repair with every rendering case still green.
|
||||||
|
//
|
||||||
|
// Same machinery as UnlocatedIoBlockScenario's arming case: the environment says
|
||||||
|
// the demotion is pinned on, therefore the library must SAY it demoted something.
|
||||||
|
// The observable is the latched MGLOG_I each backend emits when it first builds a
|
||||||
|
// demoted program; both spell "demoted to an ordinary varying", so this one case
|
||||||
|
// covers both pinned lanes without a backend gate.
|
||||||
|
TEST_F(PointSizeDemotionScenario, TheDemotionIsActuallyArmedWhenTheEnvironmentPinsItOn) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (AmbientQuirkFromEnvironment("MOBILEGL_POINT_SIZE_DEMOTION") != AmbientQuirk::On) {
|
||||||
|
GTEST_SKIP() << "this case needs the demotion pinned ON for the whole process, which "
|
||||||
|
"is what the PointSizeDemotion. ctest entries do with "
|
||||||
|
"MOBILEGL_POINT_SIZE_DEMOTION=1; with the variable unset the detected "
|
||||||
|
"capabilities decide, and on this machine the built-in is hosted - so "
|
||||||
|
"there would be nothing to observe";
|
||||||
|
}
|
||||||
|
if (LibraryLogPath().empty()) {
|
||||||
|
GTEST_SKIP() << "MOBILEGL_POINT_SIZE_DEMOTION is pinned on but MOBILEGL_LOG_FILE_PATH "
|
||||||
|
"is not set, so the library has nowhere to record that it demoted "
|
||||||
|
"anything; the PointSizeDemotion. ctest entries set both";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taken BEFORE the program is built, so the line this looks for can only be one
|
||||||
|
// this process wrote.
|
||||||
|
const std::uintmax_t before = LibraryLogSize();
|
||||||
|
|
||||||
|
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPointVertexSource},
|
||||||
|
{GL_GEOMETRY_SHADER, kPointGeometrySource},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||||
|
{"gl_PointSize"});
|
||||||
|
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||||
|
// Drawn as well as built, so a stack that defers its backend program to first
|
||||||
|
// use still reaches the build the latched line fires in - and the capture must
|
||||||
|
// STILL be right through the carrier.
|
||||||
|
const std::vector<float> captured = RunCaptureSpan(program, GL_POINTS, 1, 1);
|
||||||
|
EXPECT_TRUE(ComponentIs(captured, 0, 8.0f))
|
||||||
|
<< "the pinned-on lane did not even capture correctly";
|
||||||
|
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
const std::string appended = LibraryLogSince(before);
|
||||||
|
EXPECT_NE(appended.find("demoted to an ordinary varying"), std::string::npos)
|
||||||
|
<< "MOBILEGL_POINT_SIZE_DEMOTION is pinned ON, a geometry program reading and "
|
||||||
|
"writing gl_PointSize was built and captured, and no backend ever reported "
|
||||||
|
"demoting it. The demotion is not armed - check the override mapping in the "
|
||||||
|
"backend inits (it is inverted on purpose), the CompileEnv accessors, and "
|
||||||
|
"ProgramSpirvTask's verdict plumbing. Log appended by this test:\n"
|
||||||
|
<< appended;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,554 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PrimitivesGeneratedNoXfbScenario.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_PRIMITIVES_GENERATED COUNTS DRAWS MADE WITH TRANSFORM FEEDBACK
|
||||||
|
// INACTIVE.
|
||||||
|
//
|
||||||
|
// GL 4.6 core 13.4: the query counts what the last vertex processing stage emits,
|
||||||
|
// capture or no capture. The CTS leans its whole tessellation suite on that - the
|
||||||
|
// tessellator's output is MEASURED by an XFB-inactive PATCHES draw under
|
||||||
|
// rasterizer discard inside a GENERATED query, and the capture buffers of ~29
|
||||||
|
// tessellation tests are sized from the answer - so a backend that answers 0
|
||||||
|
// hands them a zero-byte buffer and an INVALID_OPERATION off its zero-length map.
|
||||||
|
//
|
||||||
|
// DirectVulkan serves the query from the transform-feedback stream query's
|
||||||
|
// primitivesNeeded, which VK_EXT_transform_feedback defines to count whether or
|
||||||
|
// not a capture span is open. Both the Mali-G1-Ultra driver AND Mesa lavapipe
|
||||||
|
// disagree with that definition: with no vkCmdBeginTransformFeedbackEXT recorded,
|
||||||
|
// the pair reads back 0. Where the bring-up probe measures that defect with a
|
||||||
|
// working control - or MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1 pins it on - the
|
||||||
|
// renderer accumulates XFB-inactive draws through the best proven substitute
|
||||||
|
// pool: VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT (which lavapipe hosts and passes,
|
||||||
|
// rasterizer discard included), else pipeline statistics over clipping-stage
|
||||||
|
// invocations (GL's CLIPPING_INPUT_PRIMITIVES). These cases assert the GL-visible
|
||||||
|
// answer, so on this machine they hold the reroute to the same numbers the
|
||||||
|
// healthy stream path must produce - the "two pools must agree" assertion - and
|
||||||
|
// on a healthy driver they pin the stream path itself.
|
||||||
|
//
|
||||||
|
// DirectVulkan only: DirectGLES has no GPU counter for an XFB-inactive draw at
|
||||||
|
// all (ES has no PRIMITIVES_GENERATED without a capture), and its CPU accounting
|
||||||
|
// is a different mechanism with its own tests.
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <functional>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <iterator>
|
||||||
|
#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 {
|
||||||
|
|
||||||
|
GLuint CompileShaderStage(GLenum type, const char* source, std::string* log) {
|
||||||
|
const GLuint shader = glCreateShader(type);
|
||||||
|
glShaderSource(shader, 1, &source, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A capture-capable vertex-only program: the varying gives glBeginTransformFeedback
|
||||||
|
// something to capture for the mixed-span case; the XFB-inactive cases draw with the
|
||||||
|
// same program and simply never begin a span.
|
||||||
|
const char* const kVertexSource = R"(#version 430 core
|
||||||
|
out vec4 vs_out_value;
|
||||||
|
void main() {
|
||||||
|
const vec2 corners[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||||
|
vs_out_value = vec4(1.0);
|
||||||
|
gl_Position = vec4(corners[gl_VertexID % 3], 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// A passthrough tessellation pipeline whose all-1 levels emit exactly one
|
||||||
|
// triangle per patch - the count the tessellation cases assert.
|
||||||
|
const char* const kTessVertexSource = R"(#version 430 core
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const char* const kTessControlSource = R"(#version 430 core
|
||||||
|
layout(vertices = 1) out;
|
||||||
|
void main() {
|
||||||
|
gl_TessLevelOuter[0] = 1.0;
|
||||||
|
gl_TessLevelOuter[1] = 1.0;
|
||||||
|
gl_TessLevelOuter[2] = 1.0;
|
||||||
|
gl_TessLevelOuter[3] = 1.0;
|
||||||
|
gl_TessLevelInner[0] = 1.0;
|
||||||
|
gl_TessLevelInner[1] = 1.0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const char* const kTessEvalSource = R"(#version 430 core
|
||||||
|
layout(triangles, equal_spacing, cw) in;
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The same tessellation pipeline with something to capture, so that
|
||||||
|
// glBeginTransformFeedback accepts it: the paused-span PATCHES case needs an
|
||||||
|
// open (but paused) capture span AND a tessellator in one program.
|
||||||
|
const char* const kTessEvalCaptureSource = R"(#version 430 core
|
||||||
|
layout(triangles, equal_spacing, cw) in;
|
||||||
|
out vec4 te_out_value;
|
||||||
|
void main() {
|
||||||
|
te_out_value = vec4(1.0);
|
||||||
|
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
class PrimitivesGeneratedNoXfbScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (Gl().BackendName() != std::string("DirectVulkan")) {
|
||||||
|
GTEST_SKIP() << "the stream-query defect and its reroute are DirectVulkan's; "
|
||||||
|
<< Gl().BackendName()
|
||||||
|
<< " answers this query from a different mechanism";
|
||||||
|
}
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glGenQueries(2, m_queries);
|
||||||
|
ASSERT_NE(m_queries[0], 0u);
|
||||||
|
ASSERT_NE(m_queries[1], 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
if (m_queries[0] != 0 || m_queries[1] != 0) glDeleteQueries(2, m_queries);
|
||||||
|
m_queries[0] = m_queries[1] = 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// captureVarying: the name to record with glTransformFeedbackVaryings, or
|
||||||
|
// nullptr for a program that can never open a capture span.
|
||||||
|
GLuint BuildProgram(std::initializer_list<std::pair<GLenum, const char*>> stages,
|
||||||
|
const char* captureVarying) {
|
||||||
|
std::vector<GLuint> shaders;
|
||||||
|
for (const auto& [type, source] : stages) {
|
||||||
|
const GLuint shader = CompileShaderStage(type, source, &m_buildLog);
|
||||||
|
if (shader == 0) {
|
||||||
|
for (const GLuint built : shaders) glDeleteShader(built);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
shaders.push_back(shader);
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
for (const GLuint shader : shaders) glAttachShader(program, shader);
|
||||||
|
if (captureVarying != nullptr) {
|
||||||
|
glTransformFeedbackVaryings(program, 1, &captureVarying, GL_INTERLEAVED_ATTRIBS);
|
||||||
|
}
|
||||||
|
glLinkProgram(program);
|
||||||
|
for (const GLuint shader : shaders) glDeleteShader(shader);
|
||||||
|
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());
|
||||||
|
m_buildLog = buffer.data();
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
m_programs.push_back(program);
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint BuildCaptureProgram() {
|
||||||
|
return BuildProgram({{GL_VERTEX_SHADER, kVertexSource}}, "vs_out_value");
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint BuildTessellationProgram(bool withCaptureVarying = false) {
|
||||||
|
GLint maxTessGenLevel = 0;
|
||||||
|
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
if (maxTessGenLevel < 1) return 0;
|
||||||
|
return BuildProgram(
|
||||||
|
{{GL_VERTEX_SHADER, kTessVertexSource},
|
||||||
|
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||||
|
{GL_TESS_EVALUATION_SHADER,
|
||||||
|
withCaptureVarying ? kTessEvalCaptureSource : kTessEvalSource}},
|
||||||
|
withCaptureVarying ? "te_out_value" : nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A capture span that is open but PAUSED. The pause closes the capture, so
|
||||||
|
// every draw inside it is XFB-inactive at the backend - the stream query's
|
||||||
|
// silent case - while the GL span stays active. `program` must be the one
|
||||||
|
// that is bound: GL requires the same program at resume.
|
||||||
|
void BeginPausedSpan() {
|
||||||
|
glGenBuffers(1, &m_captureBuffer);
|
||||||
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_captureBuffer);
|
||||||
|
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 64 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
glBeginTransformFeedback(GL_TRIANGLES);
|
||||||
|
glPauseTransformFeedback();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndPausedSpan() {
|
||||||
|
glResumeTransformFeedback();
|
||||||
|
glEndTransformFeedback();
|
||||||
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||||
|
if (m_captureBuffer != 0) glDeleteBuffers(1, &m_captureBuffer);
|
||||||
|
m_captureBuffer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GENERATED query around `record()`, answered with GL_QUERY_RESULT.
|
||||||
|
GLuint QueryGenerated(const std::function<void()>& record) {
|
||||||
|
glBeginQuery(GL_PRIMITIVES_GENERATED, m_queries[1]);
|
||||||
|
record();
|
||||||
|
glEndQuery(GL_PRIMITIVES_GENERATED);
|
||||||
|
GLuint generated = 0xFFFFFFFFu;
|
||||||
|
glGetQueryObjectuiv(m_queries[1], GL_QUERY_RESULT, &generated);
|
||||||
|
return generated;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GLenum DrainGLErrors() {
|
||||||
|
const GLenum first = glGetError();
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& BuildLog() const { return m_buildLog; }
|
||||||
|
|
||||||
|
static std::filesystem::path LibraryLogPath() {
|
||||||
|
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||||
|
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||||
|
: std::filesystem::path();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::uintmax_t LibraryLogSize() {
|
||||||
|
std::error_code ec;
|
||||||
|
const std::filesystem::path path = LibraryLogPath();
|
||||||
|
if (path.empty()) return 0;
|
||||||
|
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||||
|
return ec ? 0 : size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string LibraryLogSince(std::uintmax_t offset) {
|
||||||
|
const std::filesystem::path path = LibraryLogPath();
|
||||||
|
if (path.empty()) return {};
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
if (!file.good()) return {};
|
||||||
|
file.seekg(static_cast<std::streamoff>(offset));
|
||||||
|
return std::string((std::istreambuf_iterator<char>(file)),
|
||||||
|
std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
GLuint m_queries[2] = {0, 0}; // [0]=written, [1]=generated
|
||||||
|
GLuint m_captureBuffer = 0;
|
||||||
|
std::vector<GLuint> m_programs;
|
||||||
|
std::string m_buildLog;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The plain shape: no capture object was ever bound, no span begun, no
|
||||||
|
// rasterizer discard - just a GENERATED query around two triangles. On a
|
||||||
|
// healthy driver the stream query answers it; on an affected one the armed
|
||||||
|
// reroute must produce the same 2.
|
||||||
|
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsADrawMadeWithNoCaptureSpan) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const GLuint program = BuildCaptureProgram();
|
||||||
|
ASSERT_NE(program, 0u) << BuildLog();
|
||||||
|
glUseProgram(program);
|
||||||
|
|
||||||
|
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 6); });
|
||||||
|
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||||
|
EXPECT_EQ(generated, 2u)
|
||||||
|
<< "GL_PRIMITIVES_GENERATED must count a draw made while transform feedback is "
|
||||||
|
"inactive (GL 4.6 core 13.4)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE CTS SHAPE (esextcTessellationShaderUtils.cpp, captureTessellationData):
|
||||||
|
// rasterizer discard ON, transform feedback INACTIVE, the draw inside a
|
||||||
|
// GENERATED query. This is the exact query whose 0 sizes ~29 tessellation
|
||||||
|
// tests' capture buffers on the affected device.
|
||||||
|
//
|
||||||
|
// On lavapipe this case holds through the dedicated
|
||||||
|
// VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT reroute (its discard feature is
|
||||||
|
// what makes a discarded draw countable there - llvmpipe's clipping
|
||||||
|
// statistics AND stream query both read 0 under discard).
|
||||||
|
//
|
||||||
|
// The value-conditioned skip below is deliberate and narrow, for a stack
|
||||||
|
// with NO counter that survives discard: there this case is unfalsifiable,
|
||||||
|
// and a red would indict MobileGL for a hole the bring-up probe already
|
||||||
|
// measures and reports (StatisticsSubstitutePlainOnly / Unfixable). The
|
||||||
|
// exact-zero answer IS the capability signal - any wrong nonzero count
|
||||||
|
// still fails - and on every driver that counts discarded draws at all the
|
||||||
|
// full assertion runs. The device probe list holds this shape on the Mali.
|
||||||
|
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsUnderRasterizerDiscardWithNoCaptureSpan) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const GLuint program = BuildCaptureProgram();
|
||||||
|
ASSERT_NE(program, 0u) << BuildLog();
|
||||||
|
glUseProgram(program);
|
||||||
|
|
||||||
|
glEnable(GL_RASTERIZER_DISCARD);
|
||||||
|
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 6); });
|
||||||
|
glDisable(GL_RASTERIZER_DISCARD);
|
||||||
|
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||||
|
if (generated == 0u) {
|
||||||
|
GTEST_SKIP() << "no counter this backend can reach (stream query, dedicated "
|
||||||
|
"primitives-generated query, clipping statistics) survives "
|
||||||
|
"rasterizer discard for an XFB-inactive draw on this stack - the "
|
||||||
|
"shape is unfalsifiable here; the bring-up probe measures the same "
|
||||||
|
"hole and the POST row reports it";
|
||||||
|
}
|
||||||
|
EXPECT_EQ(generated, 2u)
|
||||||
|
<< "rasterizer discard drops primitives after clipping and must not hide them from "
|
||||||
|
"GL_PRIMITIVES_GENERATED - this is the exact shape the CTS measures the "
|
||||||
|
"tessellator with";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tessellation flavour: a PATCHES draw whose all-1 levels emit exactly
|
||||||
|
// one triangle - the count the CTS's getAmountOfVerticesGeneratedByTessellator
|
||||||
|
// protocol derives everything from. Undiscarded, so that the answer is
|
||||||
|
// holdable on this machine through whichever accounting path is armed (the
|
||||||
|
// discard interaction is the case above's business, measured separately).
|
||||||
|
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsATessellatedPatchWithNoCaptureSpan) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const GLuint program = BuildTessellationProgram();
|
||||||
|
if (program == 0) {
|
||||||
|
GTEST_SKIP() << "no tessellation stages on this stack: " << BuildLog();
|
||||||
|
}
|
||||||
|
glUseProgram(program);
|
||||||
|
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||||
|
|
||||||
|
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
|
||||||
|
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||||
|
EXPECT_EQ(generated, 1u)
|
||||||
|
<< "a triangles-domain patch with every level 1 tessellates to exactly one "
|
||||||
|
"triangle, and GL_PRIMITIVES_GENERATED must say so with no capture active";
|
||||||
|
}
|
||||||
|
|
||||||
|
// One query span holding BOTH kinds of draw: an XFB-inactive draw, then a
|
||||||
|
// captured one, then another XFB-inactive one. The GENERATED answer must
|
||||||
|
// accumulate across the two accounting paths the armed reroute splits them
|
||||||
|
// into (stream slots for the captured draw, statistics slots for the
|
||||||
|
// others), and WRITTEN must stay exactly the captured draw's count - the
|
||||||
|
// pairing the stream path exists to keep exact. Undiscarded, so the
|
||||||
|
// accumulation invariant is holdable on this machine (see the discard
|
||||||
|
// case's comment); the triangles rasterize into the harness framebuffer,
|
||||||
|
// which nothing here reads.
|
||||||
|
TEST_F(PrimitivesGeneratedNoXfbScenario, ASpanMixingActiveAndInactiveDrawsAccumulatesBoth) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const GLuint program = BuildCaptureProgram();
|
||||||
|
ASSERT_NE(program, 0u) << BuildLog();
|
||||||
|
glUseProgram(program);
|
||||||
|
|
||||||
|
GLuint captureBuffer = 0;
|
||||||
|
glGenBuffers(1, &captureBuffer);
|
||||||
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBuffer);
|
||||||
|
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 3 * 4 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
|
||||||
|
glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, m_queries[0]);
|
||||||
|
const GLuint generated = QueryGenerated([]() {
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3); // XFB inactive
|
||||||
|
glBeginTransformFeedback(GL_TRIANGLES);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3); // captured
|
||||||
|
glEndTransformFeedback();
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3); // XFB inactive again
|
||||||
|
});
|
||||||
|
glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
|
||||||
|
|
||||||
|
GLuint written = 0xFFFFFFFFu;
|
||||||
|
glGetQueryObjectuiv(m_queries[0], GL_QUERY_RESULT, &written);
|
||||||
|
glDeleteBuffers(1, &captureBuffer);
|
||||||
|
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||||
|
EXPECT_EQ(generated, 3u) << "one triangle before the span, one inside it, one after";
|
||||||
|
EXPECT_EQ(written, 1u) << "only the draw inside the span writes anything";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== DRAWS INSIDE A PAUSED SPAN =====================
|
||||||
|
//
|
||||||
|
// glPauseTransformFeedback closes the capture without closing the span, so a
|
||||||
|
// draw made while paused is XFB-INACTIVE at the backend - the stream query is
|
||||||
|
// exactly as silent for it as for a draw with no span at all - while
|
||||||
|
// GL_PRIMITIVES_GENERATED must still count what the last vertex processing
|
||||||
|
// stage emitted (GL 4.6 core 13.4; the WRITTEN query is the one the pause
|
||||||
|
// silences). The frontend does keep a CPU counter for paused draws, but it can
|
||||||
|
// price only 3 of the ~15 draw entry points and answers 0 for GL_PATCHES, so
|
||||||
|
// these draws are the reroute's business like any other - and the trap on the
|
||||||
|
// other side is counting them TWICE, once in each accounting.
|
||||||
|
//
|
||||||
|
// Each case measures the SAME draw twice: once with no span open at all (the
|
||||||
|
// capability control - what this stack can count) and once inside the paused
|
||||||
|
// span, and requires the two to agree. That differential is what makes these
|
||||||
|
// cases falsifying rather than vacuous: a stack where no counter reaches a
|
||||||
|
// capture-less draw fails the control and skips, while a stack that counts the
|
||||||
|
// unpaused draw and answers 0 for the paused one - which is what excluding
|
||||||
|
// paused draws from the reroute produced - fails, instead of skipping into
|
||||||
|
// green.
|
||||||
|
|
||||||
|
// The draw the CPU counter CAN price: if the span both reroutes it and adds the
|
||||||
|
// CPU delta, this reads 2.
|
||||||
|
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsACpuPricedDrawExactlyOnce) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (AmbientQuirkFromEnvironment("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE") == AmbientQuirk::Off) {
|
||||||
|
GTEST_SKIP() << "the negative control replays the pre-probe accounting, whose paused "
|
||||||
|
"draws are CPU-counted on top of whatever the stream query says";
|
||||||
|
}
|
||||||
|
const GLuint program = BuildCaptureProgram();
|
||||||
|
ASSERT_NE(program, 0u) << BuildLog();
|
||||||
|
glUseProgram(program);
|
||||||
|
|
||||||
|
const GLuint unpaused = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
|
||||||
|
BeginPausedSpan();
|
||||||
|
const GLuint paused = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
|
||||||
|
EndPausedSpan();
|
||||||
|
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||||
|
if (unpaused == 0u) {
|
||||||
|
GTEST_SKIP() << "no counter this backend can reach answers a capture-less draw on this "
|
||||||
|
"stack, so the paused half of the comparison proves nothing; the "
|
||||||
|
"bring-up probe measures the same hole and the POST row reports it";
|
||||||
|
}
|
||||||
|
EXPECT_EQ(unpaused, 1u) << "the control itself: one triangle is one primitive";
|
||||||
|
EXPECT_EQ(paused, unpaused)
|
||||||
|
<< "one triangle drawn while the capture span is paused is still one primitive "
|
||||||
|
"generated - counted once, by whichever accounting owns it, never by two of them "
|
||||||
|
"(a reroute slot AND the frontend's CPU paused counter reads 2)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The draw the CPU counter CANNOT price: GL_PATCHES, whose amplification is not
|
||||||
|
// knowable on the CPU (CountPrimitivesForDraw answers 0 for it by design) - and
|
||||||
|
// the CTS's tessellator-measuring shape. Excluding paused draws from the
|
||||||
|
// reroute left this counted by nothing at all on the affected device.
|
||||||
|
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsATessellatedPatchExactlyOnce) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const GLuint program = BuildTessellationProgram(/*withCaptureVarying=*/true);
|
||||||
|
if (program == 0) {
|
||||||
|
GTEST_SKIP() << "no tessellation stages on this stack: " << BuildLog();
|
||||||
|
}
|
||||||
|
glUseProgram(program);
|
||||||
|
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||||
|
|
||||||
|
const GLuint unpaused = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
|
||||||
|
BeginPausedSpan();
|
||||||
|
const GLuint paused = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
|
||||||
|
EndPausedSpan();
|
||||||
|
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||||
|
if (unpaused == 0u) {
|
||||||
|
GTEST_SKIP() << "no counter this backend can reach answers a capture-less patch draw "
|
||||||
|
"on this stack, so the paused half proves nothing; the bring-up probe "
|
||||||
|
"measures the same hole and the POST row reports it";
|
||||||
|
}
|
||||||
|
EXPECT_EQ(unpaused, 1u)
|
||||||
|
<< "the control itself: a triangles-domain patch with every level 1 tessellates to "
|
||||||
|
"exactly one triangle";
|
||||||
|
EXPECT_EQ(paused, unpaused)
|
||||||
|
<< "pausing the capture does not stop the tessellator from generating that triangle, "
|
||||||
|
"and the frontend's CPU paused counter answers 0 for GL_PATCHES - so a paused "
|
||||||
|
"patch draw left out of the reroute is counted by nothing at all";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other half of the same hole: the instanced entry points never reach the
|
||||||
|
// frontend's paused accounting either, so a paused instanced draw excluded from
|
||||||
|
// the reroute is likewise counted by nothing.
|
||||||
|
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsAnInstancedDrawExactlyOnce) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const GLuint program = BuildCaptureProgram();
|
||||||
|
ASSERT_NE(program, 0u) << BuildLog();
|
||||||
|
glUseProgram(program);
|
||||||
|
|
||||||
|
const GLuint unpaused =
|
||||||
|
QueryGenerated([]() { glDrawArraysInstanced(GL_TRIANGLES, 0, 3, 4); });
|
||||||
|
BeginPausedSpan();
|
||||||
|
const GLuint paused = QueryGenerated([]() { glDrawArraysInstanced(GL_TRIANGLES, 0, 3, 4); });
|
||||||
|
EndPausedSpan();
|
||||||
|
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||||
|
if (unpaused == 0u) {
|
||||||
|
GTEST_SKIP() << "no counter this backend can reach answers a capture-less draw on this "
|
||||||
|
"stack, so the paused half proves nothing";
|
||||||
|
}
|
||||||
|
EXPECT_EQ(unpaused, 4u) << "the control itself: four instances of one triangle";
|
||||||
|
EXPECT_EQ(paused, unpaused)
|
||||||
|
<< "four instances generate four primitives whether or not the capture span is "
|
||||||
|
"paused, and no instanced entry point reaches the frontend's paused accounting";
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE ONE CASE THAT CAN FAIL WHEN THE REROUTE SILENTLY STOPS BEING ARMED -
|
||||||
|
// the UnlocatedIoBlockScenario shape, for the same reason: every case above
|
||||||
|
// is green here whether the reroute ran or not (that is the "two pools
|
||||||
|
// agree" point), so none of them can say the pinned lane actually exercised
|
||||||
|
// a reroute pool. This one asserts a LIBRARY OBSERVABLE against the
|
||||||
|
// environment: with MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE pinned on, an
|
||||||
|
// XFB-inactive draw inside a GENERATED span must make the renderer say -
|
||||||
|
// through its latched MGLOG_I - that it engaged the reroute. It reads
|
||||||
|
// MG_Config not at all (on Android this module links the shipping library)
|
||||||
|
// and trusts only the log bytes appended after it started.
|
||||||
|
TEST_F(PrimitivesGeneratedNoXfbScenario, TheRerouteIsActuallyArmedWhenTheEnvironmentPinsItOn) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (AmbientQuirkFromEnvironment("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE") != AmbientQuirk::On) {
|
||||||
|
GTEST_SKIP() << "this case needs the reroute pinned ON for the whole process, which "
|
||||||
|
"is what the PrimGenReroute. ctest entry does with "
|
||||||
|
"MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1; unset, the bring-up probe "
|
||||||
|
"decides and this machine's verdict is its own business";
|
||||||
|
}
|
||||||
|
if (LibraryLogPath().empty()) {
|
||||||
|
GTEST_SKIP() << "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE is pinned on but "
|
||||||
|
"MOBILEGL_LOG_FILE_PATH is not set, so the library has nowhere to "
|
||||||
|
"record that it rerouted anything; the PrimGenReroute. ctest "
|
||||||
|
"entry sets both";
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint program = BuildCaptureProgram();
|
||||||
|
ASSERT_NE(program, 0u) << BuildLog();
|
||||||
|
glUseProgram(program);
|
||||||
|
|
||||||
|
// Taken BEFORE the draw, so the line this looks for can only be one this
|
||||||
|
// process wrote for this span. The latch fires on the FIRST rerouted
|
||||||
|
// draw, which is inside the query below.
|
||||||
|
const std::uintmax_t before = LibraryLogSize();
|
||||||
|
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
|
||||||
|
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||||
|
EXPECT_EQ(generated, 1u) << "the pinned-on lane did not even count correctly";
|
||||||
|
|
||||||
|
const std::string appended = LibraryLogSince(before);
|
||||||
|
EXPECT_NE(appended.find("PRIMITIVES_GENERATED reroute engaged"), std::string::npos)
|
||||||
|
<< "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE is pinned ON, an XFB-inactive draw ran inside "
|
||||||
|
"a GENERATED query, and the renderer never reported engaging the reroute. The "
|
||||||
|
"quirk is not armed - check the override mapping "
|
||||||
|
"(ChoosePrimitivesGeneratedReroute) and the arming gate in "
|
||||||
|
"VulkanRenderer::BeginXfbQueryForDraw. Log appended by this test:\n"
|
||||||
|
<< appended;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
#include "BufferObject.h"
|
#include "BufferObject.h"
|
||||||
|
|
||||||
|
#include <Config.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
@@ -126,6 +128,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// distinguishes the two cases, and it is cleared just above.
|
// distinguishes the two cases, and it is cleared just above.
|
||||||
m_storageFlags = GL_DYNAMIC_STORAGE_BIT | GL_MAP_READ_BIT | GL_MAP_WRITE_BIT;
|
m_storageFlags = GL_DYNAMIC_STORAGE_BIT | GL_MAP_READ_BIT | GL_MAP_WRITE_BIT;
|
||||||
NotifyRespecify();
|
NotifyRespecify();
|
||||||
|
TryAdoptLargeStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BufferObject::Resize(SizeT size) {
|
void BufferObject::Resize(SizeT size) {
|
||||||
@@ -144,6 +147,33 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
m_isImmutableStorage = true;
|
m_isImmutableStorage = true;
|
||||||
m_storageFlags = storageFlags;
|
m_storageFlags = storageFlags;
|
||||||
NotifyRespecify();
|
NotifyRespecify();
|
||||||
|
TryAdoptLargeStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back a LARGE store with the backend's persistently+coherently mapped GPU
|
||||||
|
// storage the moment it is (re)defined, without waiting for the app to map it.
|
||||||
|
// Minecraft 26.3 streams chunk meshes into 128MB vertex arenas with plain
|
||||||
|
// glNamedBufferSubData - the one write API that carries no synchronization
|
||||||
|
// hint - and on Mali every route that hands the driver a write into a busy
|
||||||
|
// MUTABLE store either parks the calling thread (glBufferSubData, and
|
||||||
|
// glMapBufferRange even with GL_MAP_UNSYNCHRONIZED_BIT) or ghost-copies the
|
||||||
|
// whole destination on a driver worker (staged glCopyBufferSubData, and a
|
||||||
|
// range-invalidating map: ~167ms per touched arena, the recurring in-world
|
||||||
|
// hiccup). An adopted coherent map is the one shape with NO per-write driver
|
||||||
|
// call at all: every SubData lands as a plain memcpy into GPU-visible memory,
|
||||||
|
// and the shadow copy is dropped (a 128MB arena stops costing 128MB of RAM).
|
||||||
|
// Only attempted for stores the size of mesh arenas: small buffers keep the
|
||||||
|
// shadow model whose draw-time flush already prices them correctly.
|
||||||
|
void BufferObject::TryAdoptLargeStorage() {
|
||||||
|
constexpr SizeT kLargeBufferAdoptBytes = 16u * 1024u * 1024u;
|
||||||
|
if (MG_Config::Features.DisableLargeBufferAdoption) return;
|
||||||
|
if (m_size < kLargeBufferAdoptBytes) return;
|
||||||
|
if (m_resource.IsGpuResident()) return;
|
||||||
|
if (m_isMapped) return;
|
||||||
|
if (g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) return;
|
||||||
|
if (void* base = g_bufferBackendOps->AcquirePersistentMap(*this)) {
|
||||||
|
m_resource.AdoptPersistentMap(base);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BufferObject::UploadData(DataPtr data, SizeT atOffset) {
|
void BufferObject::UploadData(DataPtr data, SizeT atOffset) {
|
||||||
@@ -253,6 +283,34 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
"UploadSubData out of bounds: atOffset (%zu) + data.size (%zu) > m_size (%zu)", atOffset,
|
"UploadSubData out of bounds: atOffset (%zu) + data.size (%zu) > m_size (%zu)", atOffset,
|
||||||
data.size, m_size);
|
data.size, m_size);
|
||||||
|
|
||||||
|
// An adopted store's Bytes() IS the memory in-flight frames are reading, and
|
||||||
|
// GL orders a glBufferSubData after those already-submitted reads. A backend
|
||||||
|
// that can land the bytes on the GPU timeline takes them here, untouched by
|
||||||
|
// the mapping - the in-place host write below tore the frames still reading
|
||||||
|
// the old bytes. The bytes are not current in the mapping until the backend's
|
||||||
|
// ordered copy executes, so reads reconcile through the same gate GPU-written
|
||||||
|
// buffers use.
|
||||||
|
if (m_resource.IsGpuResident() && data.size > 0 && g_bufferBackendOps &&
|
||||||
|
g_bufferBackendOps->ResidentSubData) {
|
||||||
|
g_bufferBackendOps->ResidentSubData(*this, atOffset, data);
|
||||||
|
m_hasDefinedContent = true;
|
||||||
|
++m_changeSerial;
|
||||||
|
m_gpuWritePending = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An adopted store's Bytes() IS the memory the GPU reads, and a backend that
|
||||||
|
// defers work (DirectVulkan's frame command buffer) may still be holding a
|
||||||
|
// recorded-but-unsubmitted dispatch that GL orders this write AFTER. Writing
|
||||||
|
// the mapping now would land the bytes underneath that dispatch - its
|
||||||
|
// increments then execute on top of the newer data and invert the call order.
|
||||||
|
// Retire the pending GPU writes first, as FillSubData already does. Shadow-
|
||||||
|
// backed stores need none of this: the Memcpy below touches only the shadow,
|
||||||
|
// and the backend's SubData op does its own ordering against in-flight work.
|
||||||
|
if (m_resource.IsGpuResident()) {
|
||||||
|
SyncGpuWrites();
|
||||||
|
}
|
||||||
|
|
||||||
Memcpy(m_resource.Bytes() + atOffset, data.data, data.size);
|
Memcpy(m_resource.Bytes() + atOffset, data.data, data.size);
|
||||||
NotifyContentWrite(atOffset, data.size);
|
NotifyContentWrite(atOffset, data.size);
|
||||||
}
|
}
|
||||||
@@ -269,6 +327,24 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
"Cannot fill data while buffer is non-persistently mapped.");
|
"Cannot fill data while buffer is non-persistently mapped.");
|
||||||
if (size == 0) return;
|
if (size == 0) return;
|
||||||
|
|
||||||
|
// An adopted store takes the same GPU-timeline landing as UploadSubData: the
|
||||||
|
// in-place write below would tear in-flight readers of the mapping.
|
||||||
|
if (m_resource.IsGpuResident() && g_bufferBackendOps && g_bufferBackendOps->ResidentSubData) {
|
||||||
|
Vector<Uint8> expanded(size);
|
||||||
|
if (pattern.size == 1) {
|
||||||
|
Memset(expanded.data(), *static_cast<const Uint8*>(pattern.data), size);
|
||||||
|
} else {
|
||||||
|
for (SizeT at = 0; at < size; at += pattern.size) {
|
||||||
|
Memcpy(expanded.data() + at, pattern.data, pattern.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_bufferBackendOps->ResidentSubData(*this, atOffset, {expanded.data(), size});
|
||||||
|
m_hasDefinedContent = true;
|
||||||
|
++m_changeSerial;
|
||||||
|
m_gpuWritePending = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// A clear is ordered after all earlier GPU writes. Partial clears additionally need the
|
// A clear is ordered after all earlier GPU writes. Partial clears additionally need the
|
||||||
// retained shadow bytes; whole-store clears need the same synchronization before writing
|
// retained shadow bytes; whole-store clears need the same synchronization before writing
|
||||||
// an adopted persistent mapping that the GPU may still be accessing.
|
// an adopted persistent mapping that the GPU may still be accessing.
|
||||||
@@ -305,6 +381,23 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
size, m_size);
|
size, m_size);
|
||||||
|
|
||||||
src->SyncGpuWrites();
|
src->SyncGpuWrites();
|
||||||
|
// An adopted DESTINATION takes the same GPU-timeline landing as UploadSubData;
|
||||||
|
// the in-place write below would tear in-flight readers of the mapping.
|
||||||
|
if (m_resource.IsGpuResident() && size > 0 && g_bufferBackendOps &&
|
||||||
|
g_bufferBackendOps->ResidentSubData) {
|
||||||
|
g_bufferBackendOps->ResidentSubData(*this, dstOffset,
|
||||||
|
{src->m_resource.Bytes() + srcOffset, size});
|
||||||
|
m_hasDefinedContent = true;
|
||||||
|
++m_changeSerial;
|
||||||
|
m_gpuWritePending = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The DESTINATION needs the same ordering as UploadSubData: an adopted store is
|
||||||
|
// written in place, so pending recorded GPU writes to it must retire before the
|
||||||
|
// copy lands or they would execute on top of it.
|
||||||
|
if (m_resource.IsGpuResident()) {
|
||||||
|
SyncGpuWrites();
|
||||||
|
}
|
||||||
Memcpy(m_resource.Bytes() + dstOffset, src->m_resource.Bytes() + srcOffset, size);
|
Memcpy(m_resource.Bytes() + dstOffset, src->m_resource.Bytes() + srcOffset, size);
|
||||||
NotifyContentWrite(dstOffset, size);
|
NotifyContentWrite(dstOffset, size);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,17 @@ namespace MobileGL {
|
|||||||
void (*Respecify)(BufferObject& bufferObject) = nullptr;
|
void (*Respecify)(BufferObject& bufferObject) = nullptr;
|
||||||
// Contents update of [offset, offset + size) from the shadow.
|
// Contents update of [offset, offset + size) from the shadow.
|
||||||
void (*SubData)(BufferObject& bufferObject, SizeT offset, SizeT size) = nullptr;
|
void (*SubData)(BufferObject& bufferObject, SizeT offset, SizeT size) = nullptr;
|
||||||
|
// Contents update of an ADOPTED (GPU-resident) store. `data` holds the app's
|
||||||
|
// bytes; the frontend has NOT touched the resident mapping. GL orders a
|
||||||
|
// glBufferSubData after already-submitted GPU reads of the store, and an
|
||||||
|
// in-place host write into the coherent mapping tears the frames still
|
||||||
|
// reading the old bytes (Minecraft patches LIVE chunk sections this way -
|
||||||
|
// the tear shows as one-frame wrong geometry/UVs during fast movement). The
|
||||||
|
// backend lands the bytes on the GPU timeline instead: after in-flight
|
||||||
|
// readers, before the next consumer. The frontend marks the buffer
|
||||||
|
// gpu-write-pending so reads reconcile through ReadbackFromGpu. Backends
|
||||||
|
// without this op keep the legacy ordered in-place host write.
|
||||||
|
void (*ResidentSubData)(BufferObject& bufferObject, SizeT offset, DataPtr data) = nullptr;
|
||||||
// Write-map flush (glUnmapBuffer / glFlushMappedBufferRange). Carries the
|
// Write-map flush (glUnmapBuffer / glFlushMappedBufferRange). Carries the
|
||||||
// app's real mapping flags so the backend can honour INVALIDATE_* /
|
// app's real mapping flags so the backend can honour INVALIDATE_* /
|
||||||
// UNSYNCHRONIZED semantics per call instead of merging them.
|
// UNSYNCHRONIZED semantics per call instead of merging them.
|
||||||
@@ -211,6 +222,10 @@ namespace MobileGL {
|
|||||||
// Sizes the store for a (re)definition, renewing an adopted GPU-resident
|
// Sizes the store for a (re)definition, renewing an adopted GPU-resident
|
||||||
// mapping across it. See the definition for why the renewal is not optional.
|
// mapping across it. See the definition for why the renewal is not optional.
|
||||||
void RedefineStorage(SizeT size);
|
void RedefineStorage(SizeT size);
|
||||||
|
// Backend-initiated coherent adoption for mesh-arena-sized stores; see the
|
||||||
|
// definition for the driver behavior that makes every other write route to
|
||||||
|
// a busy large mutable store a frame-scale stall.
|
||||||
|
void TryAdoptLargeStorage();
|
||||||
void NotifyRespecify();
|
void NotifyRespecify();
|
||||||
void NotifySubData(SizeT offset, SizeT size);
|
void NotifySubData(SizeT offset, SizeT size);
|
||||||
void NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess);
|
void NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess);
|
||||||
|
|||||||
@@ -517,7 +517,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void ProgramLinkTask::DeferLog(String line) { diagnostics.logLines.push_back(Move(line)); }
|
void ProgramLinkTask::DeferLog(String line, const Int level) {
|
||||||
|
diagnostics.logLines.push_back({level, Move(line)});
|
||||||
|
}
|
||||||
|
|
||||||
void ProgramLinkTask::SubmitAfter(const Vector<SharedPtr<ShaderCompileTask>>& deps) {
|
void ProgramLinkTask::SubmitAfter(const Vector<SharedPtr<ShaderCompileTask>>& deps) {
|
||||||
// +1 for the guard this function releases itself. Without it, a dependency that
|
// +1 for the guard this function releases itself. Without it, a dependency that
|
||||||
@@ -855,6 +857,18 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
spirvHandoff.reflection.uniformReflection = artifacts.uniformReflection;
|
spirvHandoff.reflection.uniformReflection = artifacts.uniformReflection;
|
||||||
spirvHandoff.reflection.blockReflection = artifacts.blockReflection;
|
spirvHandoff.reflection.blockReflection = artifacts.blockReflection;
|
||||||
spirvHandoff.reflection.tProgramBlockIndexToGl = artifacts.tProgramBlockIndexToGl;
|
spirvHandoff.reflection.tProgramBlockIndexToGl = artifacts.tProgramBlockIndexToGl;
|
||||||
|
// The capture set is NOT part of that slice (see the handoff's own comment), and the
|
||||||
|
// point-size demotion needs exactly one bit out of it: whether anything asked to
|
||||||
|
// capture gl_PointSize. Derived here, where ResolveTransformFeedbackVaryings has
|
||||||
|
// just filled artifacts.xfbVaryings and before the join moves them away, because a
|
||||||
|
// capture stage that only READS the built-in still has to declare the carrier the
|
||||||
|
// capture binds to - and phase B has no other way to learn that.
|
||||||
|
for (const ProgramObject::XfbVarying& varying : artifacts.xfbVaryings) {
|
||||||
|
if (varying.name == "gl_PointSize") {
|
||||||
|
spirvHandoff.captureRequestsPointSize = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Phase B pairs this with its own SpirvArtifacts to insert the completed front end.
|
// Phase B pairs this with its own SpirvArtifacts to insert the completed front end.
|
||||||
// A COPY, because the GL-thread join moves `artifacts` out of this node before phase B
|
// A COPY, because the GL-thread join moves `artifacts` out of this node before phase B
|
||||||
// runs - and with the TProgram dropped, because a memo must never hold a glslang arena.
|
// runs - and with the TProgram dropped, because a memo must never hold a glslang arena.
|
||||||
@@ -896,6 +910,11 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// env snapshot ProgramSpirvTask hands the chain, so the key and the bytes can never
|
// env snapshot ProgramSpirvTask hands the chain, so the key and the bytes can never
|
||||||
// disagree.
|
// disagree.
|
||||||
keyInputs.nativeFloat64 = env.ConsumesFloat64Natively();
|
keyInputs.nativeFloat64 = env.ConsumesFloat64Natively();
|
||||||
|
// The second and third capability bits, under exactly the same rule: each arms a
|
||||||
|
// phase-B rewrite of the cached modules (the point-size demotion), read from the
|
||||||
|
// same env snapshot that phase B will consult, so key and bytes cannot disagree.
|
||||||
|
keyInputs.demoteTessellationPointSize = env.DemotesTessellationPointSize();
|
||||||
|
keyInputs.demoteGeometryPointSize = env.DemotesGeometryPointSize();
|
||||||
keyInputs.stages.reserve(in.shaders.size());
|
keyInputs.stages.reserve(in.shaders.size());
|
||||||
for (const LinkShaderInput& shader : in.shaders) {
|
for (const LinkShaderInput& shader : in.shaders) {
|
||||||
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
|
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
|
||||||
|
|||||||
@@ -117,6 +117,19 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// for phase B after the join has moved `artifacts` away.
|
// for phase B after the join has moved `artifacts` away.
|
||||||
ProgramObject::LinkArtifacts reflection;
|
ProgramObject::LinkArtifacts reflection;
|
||||||
|
|
||||||
|
// Whether the RESOLVED transform-feedback capture set names gl_PointSize - the
|
||||||
|
// one fact about `artifacts.xfbVaryings` phase B needs, carried as a derived
|
||||||
|
// bool rather than by widening the slice above, which is deliberately the five
|
||||||
|
// (now eight) fields BuildGlobalUboRouting consumes and nothing else.
|
||||||
|
//
|
||||||
|
// It has to be here and cannot be re-derived: the point-size demotion forces the
|
||||||
|
// capture-capable stage to declare its carrier even when that stage never WRITES
|
||||||
|
// the built-in (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram's
|
||||||
|
// `captureRequestsPointSize`), and by phase B the only record of the request is
|
||||||
|
// this bit. No new L1 key material: the key already covers
|
||||||
|
// `requestedXfbVaryings`, of which this is a function.
|
||||||
|
Bool captureRequestsPointSize = false;
|
||||||
|
|
||||||
// L1 shader-translation memo key for this program's SPIR-V (see
|
// L1 shader-translation memo key for this program's SPIR-V (see
|
||||||
// MG_Util/ShaderTranspiler/TranslationCache.h). Built HERE, at the tail of phase
|
// MG_Util/ShaderTranspiler/TranslationCache.h). Built HERE, at the tail of phase
|
||||||
// A, and not by phase B - two reasons, both structural:
|
// A, and not by phase B - two reasons, both structural:
|
||||||
@@ -201,8 +214,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// Worker-side MGLOG replacement: appended to diagnostics.logLines and replayed by the
|
// Worker-side MGLOG replacement: appended to diagnostics.logLines and replayed by the
|
||||||
// join, on the GL thread, where a serial implementation would have printed it.
|
// join, on the GL thread, where a serial implementation would have printed it.
|
||||||
// Logging straight from a worker interleaves mid-line with the GL thread's output and
|
// Logging straight from a worker interleaves mid-line with the GL thread's output and
|
||||||
// lands out of order relative to the glLinkProgram that caused it.
|
// lands out of order relative to the glLinkProgram that caused it. `level` is the
|
||||||
void DeferLog(String line);
|
// severity the replay uses; DEBUG (the default) is compiled out of every shipped
|
||||||
|
// build, so a line that has to survive one names its own.
|
||||||
|
void DeferLog(String line, Int level = MOBILEGL_LOG_LEVEL_DEBUG);
|
||||||
|
|
||||||
// Counts down to zero exactly once. Starts at deps + 1: the extra guard is released
|
// Counts down to zero exactly once. Starts at deps + 1: the extra guard is released
|
||||||
// by SubmitAfter itself, so a dependency that settles while the edges are still being
|
// by SubmitAfter itself, so a dependency that settles while the edges are still being
|
||||||
|
|||||||
@@ -603,6 +603,11 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// other question about the global UBO's layout - and it is one: it decides how wide a
|
// other question about the global UBO's layout - and it is one: it decides how wide a
|
||||||
// `double` uniform's slot is.
|
// `double` uniform's slot is.
|
||||||
Bool UsesNativeFloat64() const { return Spirv().nativeFloat64; }
|
Bool UsesNativeFloat64() const { return Spirv().nativeFloat64; }
|
||||||
|
// Whether gl_PointSize was demoted out of this program's tessellation/geometry
|
||||||
|
// modules into the ordinary carrier varying. Joins phase B: it is a fact about the
|
||||||
|
// generated modules, and its readers (the backends' capture-name respelling) already
|
||||||
|
// hold the phase-B join.
|
||||||
|
Bool PointSizeDemoted() const { return Spirv().pointSizeDemoted; }
|
||||||
SizeT GetUniformStorageSpanInBytes(Uint location) const {
|
SizeT GetUniformStorageSpanInBytes(Uint location) const {
|
||||||
return UniformStorageSpanInBytes(GetUniformTypeFacts(location), GetUniformSizesInBytes(location),
|
return UniformStorageSpanInBytes(GetUniformTypeFacts(location), GetUniformSizesInBytes(location),
|
||||||
UsesNativeFloat64());
|
UsesNativeFloat64());
|
||||||
@@ -1429,6 +1434,18 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// table's offsets mean, and glUniform*d / glGetUniform*v have to write and read the
|
// table's offsets mean, and glUniform*d / glGetUniform*v have to write and read the
|
||||||
// width the shader actually declares.
|
// width the shader actually declares.
|
||||||
Bool nativeFloat64 = false;
|
Bool nativeFloat64 = false;
|
||||||
|
// Whether gl_PointSize was demoted out of THESE modules' tessellation/geometry
|
||||||
|
// stages into an ordinary varying (ShaderCompiler::
|
||||||
|
// DemoteTessellationGeometryPointSizeForProgram) because the backend cannot host
|
||||||
|
// the built-in there. Per PROGRAM by construction - a consumer whose producer
|
||||||
|
// kept the built-in would read garbage - and recorded here rather than
|
||||||
|
// re-derived because it cannot be: the rewrite's whole point is that the final
|
||||||
|
// bytes no longer declare the capability that armed it. The backends read it to
|
||||||
|
// respell a "gl_PointSize" transform-feedback capture as the carrier
|
||||||
|
// (ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME). The GL reflection surface
|
||||||
|
// deliberately keeps answering "gl_PointSize": demotion happens after phase A,
|
||||||
|
// so every query keeps the truthful GL spelling.
|
||||||
|
Bool pointSizeDemoted = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- artifacts-only helpers, shared with ProgramLinkTask ----
|
// ---- artifacts-only helpers, shared with ProgramLinkTask ----
|
||||||
|
|||||||
@@ -16,10 +16,29 @@
|
|||||||
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
void ProgramSpirvTask::DeferLog(String line) { diagnostics.logLines.push_back(Move(line)); }
|
namespace {
|
||||||
|
// The MGLOG_*_ONCE latch, moved to the SOURCE of a deferred line. It cannot live at
|
||||||
|
// the replay: Async::ApplyDeferredDiagnostics is ONE site shared by every job in the
|
||||||
|
// tree, so a latch there would silence unrelated lines. And it has to exist: a shader
|
||||||
|
// pack hands the same refusal to program after program, and a per-program WARN on a
|
||||||
|
// path like that is exactly the repeated production logging the house rule forbids.
|
||||||
|
// First occurrence at WARN - the one a bug report needs - every later one back at
|
||||||
|
// DEBUG, which shipped builds compile out.
|
||||||
|
Int FirstTimeWarnLevel(std::atomic_flag& latch) {
|
||||||
|
return latch.test_and_set(std::memory_order_relaxed) ? MOBILEGL_LOG_LEVEL_DEBUG
|
||||||
|
: MOBILEGL_LOG_LEVEL_WARN;
|
||||||
|
}
|
||||||
|
std::atomic_flag g_pointSizeDeclineReported;
|
||||||
|
std::atomic_flag g_pointSizeOptimizerFailureReported;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ProgramSpirvTask::DeferLog(String line, const Int level) {
|
||||||
|
diagnostics.logLines.push_back({level, Move(line)});
|
||||||
|
}
|
||||||
|
|
||||||
void ProgramSpirvTask::SubmitAfter(const SharedPtr<ProgramLinkTask>& phaseA) {
|
void ProgramSpirvTask::SubmitAfter(const SharedPtr<ProgramLinkTask>& phaseA) {
|
||||||
MOBILEGL_ASSERT(phaseA != nullptr, "ProgramSpirvTask::SubmitAfter: the phase-A node is missing");
|
MOBILEGL_ASSERT(phaseA != nullptr, "ProgramSpirvTask::SubmitAfter: the phase-A node is missing");
|
||||||
@@ -128,8 +147,15 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// with (ProgramLinkTask::BuildSpirvCacheKey reads the same env) or a memo written under
|
// with (ProgramLinkTask::BuildSpirvCacheKey reads the same env) or a memo written under
|
||||||
// one answer could be handed back under the other.
|
// one answer could be handed back under the other.
|
||||||
const Bool nativeFloat64 = m_phaseA->in.env != nullptr && m_phaseA->in.env->ConsumesFloat64Natively();
|
const Bool nativeFloat64 = m_phaseA->in.env != nullptr && m_phaseA->in.env->ConsumesFloat64Natively();
|
||||||
|
// The point-size demotion verdicts, read from the SAME snapshot for the same reason
|
||||||
|
// - and the same bits BuildSpirvCacheKey put in the L1 key, so a memo written under
|
||||||
|
// one answer can never be handed back under the other.
|
||||||
|
const Bool demoteTessellationPointSize =
|
||||||
|
m_phaseA->in.env != nullptr && m_phaseA->in.env->DemotesTessellationPointSize();
|
||||||
|
const Bool demoteGeometryPointSize =
|
||||||
|
m_phaseA->in.env != nullptr && m_phaseA->in.env->DemotesGeometryPointSize();
|
||||||
GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation,
|
GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation,
|
||||||
nativeFloat64);
|
nativeFloat64, demoteTessellationPointSize, demoteGeometryPointSize);
|
||||||
// GlslangToSpv was the only consumer of the parsed ASTs; everything after this point
|
// GlslangToSpv was the only consumer of the parsed ASTs; everything after this point
|
||||||
// works on the SPIR-V and on the TProgram's own self-contained reflection pool. Drop
|
// works on the SPIR-V and on the TProgram's own self-contained reflection pool. Drop
|
||||||
// them here rather than at the end of the body, which is ~87% of this node's runtime
|
// them here rather than at the end of the body, which is ~87% of this node's runtime
|
||||||
@@ -188,7 +214,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex,
|
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex,
|
||||||
const Bool deferOutputValidationForDirectVulkan,
|
const Bool deferOutputValidationForDirectVulkan,
|
||||||
const Bool enableSpirvValidation, const Bool nativeFloat64) {
|
const Bool enableSpirvValidation, const Bool nativeFloat64,
|
||||||
|
const Bool demoteTessellationPointSize,
|
||||||
|
const Bool demoteGeometryPointSize) {
|
||||||
/* As we passed first stage compilation/linking,
|
/* As we passed first stage compilation/linking,
|
||||||
* we'll assume all the operations here should
|
* we'll assume all the operations here should
|
||||||
* pass. We may be able to employ some optimizations
|
* pass. We may be able to employ some optimizations
|
||||||
@@ -267,6 +295,50 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
artifacts.spirvStatus = allOptimized;
|
artifacts.spirvStatus = allOptimized;
|
||||||
|
|
||||||
|
// The point-size demotion, program-wide and after the sanitize chain, so it works
|
||||||
|
// on the final shared bytes both backends consume and nothing downstream can trim
|
||||||
|
// the carriers it declares. Only the env half of the verdict lives here (and in the
|
||||||
|
// L1 key); whether the program actually declares the capability is probed inside,
|
||||||
|
// so the common case on an affected device - a program that never touches point
|
||||||
|
// size in those stages - pays one module parse per stage and no rewrite.
|
||||||
|
artifacts.pointSizeDemoted = false;
|
||||||
|
if (allOptimized && (demoteTessellationPointSize || demoteGeometryPointSize)) {
|
||||||
|
// Read off the HANDOFF's own derived bit, not off `handoff.reflection`: that
|
||||||
|
// field is the routing slice phase A fills with eight named members, and
|
||||||
|
// xfbVaryings is not one of them - reading it there answered "no capture ever
|
||||||
|
// asks for gl_PointSize" on every production link, which left a read-only
|
||||||
|
// capture stage without the carrier its capture binds to.
|
||||||
|
const Bool captureRequestsPointSize = handoff.captureRequestsPointSize;
|
||||||
|
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||||
|
if (!ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
artifacts.generatedSpirv, handoff.shaderTypes, demoteTessellationPointSize,
|
||||||
|
demoteGeometryPointSize, captureRequestsPointSize, outcome,
|
||||||
|
!deferOutputValidationForDirectVulkan, enableSpirvValidation)) {
|
||||||
|
// Optimizer failure: modules untouched, so the capability is still declared
|
||||||
|
// and the backends' existing refusals stay in charge - honest, just slower.
|
||||||
|
DeferLog(std::format("ProgramObject {}: point-size demotion failed in the optimizer; the "
|
||||||
|
"program keeps its built-in and the device's declines apply",
|
||||||
|
externalIndex),
|
||||||
|
FirstTimeWarnLevel(g_pointSizeOptimizerFailureReported));
|
||||||
|
} else if (outcome.demoted) {
|
||||||
|
artifacts.pointSizeDemoted = true;
|
||||||
|
DeferLog(std::format("ProgramObject {}: gl_PointSize demoted to an ordinary varying across "
|
||||||
|
"the tessellation/geometry chain (value preserved for capture and "
|
||||||
|
"gl_in reads; rasterized size falls back to 1.0)",
|
||||||
|
externalIndex));
|
||||||
|
} else if (!outcome.declineDetail.empty()) {
|
||||||
|
// THE MOST VALUABLE LINE THIS FEATURE PRODUCES: which module shape the pass
|
||||||
|
// refused, and therefore why an affected device is still about to lose the
|
||||||
|
// program. Nothing else records it - `declineDetail` has no other runtime
|
||||||
|
// surface - so at the deferred channel's DEBUG default it was formatted and
|
||||||
|
// then dropped by every INFO build, i.e. every device and every CI artifact.
|
||||||
|
DeferLog(std::format("ProgramObject {}: point-size demotion declined ({}); the program "
|
||||||
|
"keeps its built-in and the device's declines apply",
|
||||||
|
externalIndex, outcome.declineDetail),
|
||||||
|
FirstTimeWarnLevel(g_pointSizeDeclineReported));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramSpirvTask::BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff,
|
void ProgramSpirvTask::BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff,
|
||||||
|
|||||||
@@ -67,12 +67,14 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex,
|
void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex,
|
||||||
Bool deferOutputValidationForDirectVulkan, Bool enableSpirvValidation,
|
Bool deferOutputValidationForDirectVulkan, Bool enableSpirvValidation,
|
||||||
Bool nativeFloat64);
|
Bool nativeFloat64, Bool demoteTessellationPointSize,
|
||||||
|
Bool demoteGeometryPointSize);
|
||||||
void BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
|
void BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
|
||||||
|
|
||||||
// Worker-side MGLOG replacement, replayed by the join on the GL thread. Same reason as
|
// Worker-side MGLOG replacement, replayed by the join on the GL thread. Same reason as
|
||||||
// ProgramLinkTask::DeferLog.
|
// ProgramLinkTask::DeferLog, and the same severity rule: DEBUG is compiled out of
|
||||||
void DeferLog(String line);
|
// every shipped build, so a line that has to survive one names its own level.
|
||||||
|
void DeferLog(String line, Int level = MOBILEGL_LOG_LEVEL_DEBUG);
|
||||||
|
|
||||||
SharedPtr<ProgramLinkTask> m_phaseA;
|
SharedPtr<ProgramLinkTask> m_phaseA;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -384,12 +384,15 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// the log, for every failing shader. The info log is what names the offending
|
// the log, for every failing shader. The info log is what names the offending
|
||||||
// line; the source is recoverable from the application.
|
// line; the source is recoverable from the application.
|
||||||
const SizeT firstLineEnd = artifacts.infoLog.find('\n');
|
const SizeT firstLineEnd = artifacts.infoLog.find('\n');
|
||||||
diagnostics.logLines.push_back(std::format(
|
diagnostics.logLines.push_back(
|
||||||
"ShaderCompileTask: shader {} (stage {}) failed to compile; compileStatus = false. "
|
{MOBILEGL_LOG_LEVEL_DEBUG,
|
||||||
"Preprocessed source: {} bytes. First log line: {}",
|
std::format(
|
||||||
externalIndex, static_cast<Int>(stage), shared.preprocessedSource.length(),
|
"ShaderCompileTask: shader {} (stage {}) failed to compile; compileStatus = false. "
|
||||||
artifacts.infoLog.substr(0, firstLineEnd == String::npos ? artifacts.infoLog.length()
|
"Preprocessed source: {} bytes. First log line: {}",
|
||||||
: firstLineEnd)));
|
externalIndex, static_cast<Int>(stage), shared.preprocessedSource.length(),
|
||||||
|
artifacts.infoLog.substr(0, firstLineEnd == String::npos
|
||||||
|
? artifacts.infoLog.length()
|
||||||
|
: firstLineEnd))});
|
||||||
if (shouldPopulateCache) {
|
if (shouldPopulateCache) {
|
||||||
fresh->outcome = ShaderPreprocessOutcome::ParseFailed;
|
fresh->outcome = ShaderPreprocessOutcome::ParseFailed;
|
||||||
fresh->infoLog = artifacts.infoLog;
|
fresh->infoLog = artifacts.infoLog;
|
||||||
|
|||||||
@@ -30,6 +30,22 @@ target_link_libraries(DriverBugProbesTest PRIVATE
|
|||||||
${LINK_LIBRARIES}
|
${LINK_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_executable(
|
||||||
|
PrimitivesGeneratedNoXfbProbeTest
|
||||||
|
PrimitivesGeneratedNoXfbProbeTest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(PrimitivesGeneratedNoXfbProbeTest PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(PrimitivesGeneratedNoXfbProbeTest PRIVATE
|
||||||
|
GTest::gtest_main
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
include(GoogleTest)
|
include(GoogleTest)
|
||||||
gtest_discover_tests(DriverPostIterationRPWitnessTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(DriverPostIterationRPWitnessTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
gtest_discover_tests(DriverBugProbesTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(DriverBugProbesTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
gtest_discover_tests(PrimitivesGeneratedNoXfbProbeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
|||||||
@@ -0,0 +1,546 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/SelfTest/PrimitivesGeneratedNoXfbProbeTest.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
|
||||||
|
//
|
||||||
|
// The primitives-generated-without-transform-feedback probe's VERDICT and ARMING
|
||||||
|
// logic, pinned over synthetic measurements. Recording the probe for real needs a
|
||||||
|
// GPU; the two pure functions are where the cheap mistakes live - a verdict that reads a
|
||||||
|
// half-broken driver as healthy, an override arm swapped so ForceOn disarms, a
|
||||||
|
// substitute ranked below a worse one - and every driver the campaign has
|
||||||
|
// characterised is written down here as a fake measurement so the mapping cannot
|
||||||
|
// drift without a red:
|
||||||
|
// - a conforming driver (stream counts everywhere),
|
||||||
|
// - Mesa lavapipe as measured 2026-08: stream silent everywhere, the dedicated
|
||||||
|
// VK_EXT_primitives_generated_query exact everywhere (discard included), and
|
||||||
|
// the statistics control exact on the plain shape but dead under rasterizer
|
||||||
|
// discard (llvmpipe's discard short-circuit),
|
||||||
|
// - the same driver without the dedicated query - the statistics tiers,
|
||||||
|
// - a device with the defect and no working substitute,
|
||||||
|
// - a substitute that would be WORSE than the stream query on some shape (the
|
||||||
|
// never-worse rule the plain-only arm has to prove before it may arm),
|
||||||
|
// - and the refuse-to-guess shapes (half counts, missing mandatory shapes).
|
||||||
|
//
|
||||||
|
// The last section pins the probe's TEARDOWN CONTRACT instead, driving the real
|
||||||
|
// RunPrimitivesGeneratedNoXfbProbe against a fake Vulkan driver whose fence wait
|
||||||
|
// can be made to expire: no GPU is needed for that, only the entry points the
|
||||||
|
// probe is handed, and what it does on that path is what keeps a hung driver from
|
||||||
|
// hanging the POST.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
|
||||||
|
|
||||||
|
using MobileGL::Bool;
|
||||||
|
using MobileGL::Uint32;
|
||||||
|
using MobileGL::Uint64;
|
||||||
|
using MobileGL::MG_Config::QuirkOverride;
|
||||||
|
using MobileGL::MG_Util::SelfTest::EvaluatePrimitivesGeneratedNoXfbVerdict;
|
||||||
|
using MobileGL::MG_Util::SelfTest::ChoosePrimitivesGeneratedReroute;
|
||||||
|
using MobileGL::MG_Util::SelfTest::PrimGenRerouteKind;
|
||||||
|
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbMeasurement;
|
||||||
|
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbProbeContext;
|
||||||
|
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbShapeMeasurement;
|
||||||
|
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbVerdict;
|
||||||
|
using MobileGL::MG_Util::SelfTest::RunPrimitivesGeneratedNoXfbProbe;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct ShapeAnswers {
|
||||||
|
Uint64 stream = 0;
|
||||||
|
// Negative-free encoding: measured flags separate from values.
|
||||||
|
Bool pgqMeasured = false;
|
||||||
|
Uint64 pgq = 0;
|
||||||
|
Bool statMeasured = false;
|
||||||
|
Uint64 stat = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
PrimitivesGeneratedNoXfbShapeMeasurement Shape(const ShapeAnswers& answers) {
|
||||||
|
PrimitivesGeneratedNoXfbShapeMeasurement shape;
|
||||||
|
shape.drawn = true;
|
||||||
|
shape.expectedPrimitives = 1;
|
||||||
|
shape.streamGenerated = answers.stream;
|
||||||
|
shape.primitivesGeneratedExtMeasured = answers.pgqMeasured;
|
||||||
|
shape.primitivesGeneratedExt = answers.pgq;
|
||||||
|
shape.statisticsMeasured = answers.statMeasured;
|
||||||
|
shape.statisticsClippingInput = answers.stat;
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimitivesGeneratedNoXfbMeasurement Measurement(PrimitivesGeneratedNoXfbShapeMeasurement plain,
|
||||||
|
PrimitivesGeneratedNoXfbShapeMeasurement discard,
|
||||||
|
PrimitivesGeneratedNoXfbShapeMeasurement patches) {
|
||||||
|
PrimitivesGeneratedNoXfbMeasurement measurement;
|
||||||
|
measurement.ran = true;
|
||||||
|
measurement.trianglesPlain = plain;
|
||||||
|
measurement.trianglesDiscard = discard;
|
||||||
|
measurement.patchesDiscard = patches;
|
||||||
|
return measurement;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimitivesGeneratedNoXfbShapeMeasurement NotDrawn() {
|
||||||
|
return PrimitivesGeneratedNoXfbShapeMeasurement{};
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr ShapeAnswers kHealthy{1, true, 1, true, 1};
|
||||||
|
// The lavapipe measurement: stream silent, dedicated query exact, statistics
|
||||||
|
// exact only where nothing is discarded.
|
||||||
|
constexpr ShapeAnswers kLavapipePlain{0, true, 1, true, 1};
|
||||||
|
constexpr ShapeAnswers kLavapipeDiscard{0, true, 1, true, 0};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// A conforming driver: the stream query counts every capture-less shape exactly.
|
||||||
|
// Controls agreeing changes nothing - health is decided by the subject.
|
||||||
|
TEST(PrimitivesGeneratedNoXfbVerdictTest, AConformingDriverReadsStreamCounts) {
|
||||||
|
const auto measurement = Measurement(Shape(kHealthy), Shape(kHealthy), Shape(kHealthy));
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::StreamCounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...and stays healthy with no tessellation stage to draw the patches shape with,
|
||||||
|
// and with no control at all - a control is only required to QUALIFY a
|
||||||
|
// substitute, never to certify health.
|
||||||
|
TEST(PrimitivesGeneratedNoXfbVerdictTest, HealthNeedsNeitherTessellationNorAControl) {
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||||
|
Measurement(Shape(kHealthy), Shape(kHealthy), NotDrawn())),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::StreamCounts);
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||||
|
Measurement(Shape({1}), Shape({1}), Shape({1}))),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::StreamCounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mesa lavapipe as measured (2026-08): stream silent for every capture-less
|
||||||
|
// draw, the dedicated primitives-generated query exact on every shape (discard
|
||||||
|
// included), the statistics control dead under discard. The dedicated query must
|
||||||
|
// win - it is the only substitute that covers the CTS shape there.
|
||||||
|
TEST(PrimitivesGeneratedNoXfbVerdictTest, LavapipeShapedMeasurementTakesTheDedicatedQuery) {
|
||||||
|
const auto measurement =
|
||||||
|
Measurement(Shape(kLavapipePlain), Shape(kLavapipeDiscard), Shape(kLavapipeDiscard));
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The affected-device hypothesis with no dedicated query: statistics exact on
|
||||||
|
// every shape, the CTS's discarded shapes included.
|
||||||
|
TEST(PrimitivesGeneratedNoXfbVerdictTest, StatisticsExactEverywhereIsTheFullStatisticsSubstitute) {
|
||||||
|
const auto measurement = Measurement(Shape({0, false, 0, true, 1}), Shape({0, false, 0, true, 1}),
|
||||||
|
Shape({0, false, 0, true, 1}));
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A dedicated query that is silent in the same way the stream query is must not
|
||||||
|
// be armed - the statistics tier decides instead.
|
||||||
|
TEST(PrimitivesGeneratedNoXfbVerdictTest, ASilentDedicatedQueryFallsThroughToStatistics) {
|
||||||
|
const auto measurement = Measurement(Shape({0, true, 0, true, 1}), Shape({0, true, 0, true, 1}),
|
||||||
|
Shape({0, true, 0, true, 1}));
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The llvmpipe statistics hole without the dedicated query to rescue it: exact on
|
||||||
|
// the plain shape, dead under discard. Repairs undiscarded queries only, and the
|
||||||
|
// verdict must say so.
|
||||||
|
TEST(PrimitivesGeneratedNoXfbVerdictTest, StatisticsDeadUnderDiscardIsThePlainOnlySubstitute) {
|
||||||
|
const auto measurement = Measurement(Shape({0, false, 0, true, 1}), Shape({0, false, 0, true, 0}),
|
||||||
|
Shape({0, false, 0, true, 0}));
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE DOMINATION RULE. The plain-only substitute is armed for EVERY XFB-inactive
|
||||||
|
// draw, so it may only be armed where it is never worse than what it replaces:
|
||||||
|
// each shape it gets wrong must be one the stream query already answered 0 for.
|
||||||
|
// Here the discarded triangle is one the stream query answers EXACTLY (a driver
|
||||||
|
// whose silence is selective) and whose statistics read 0 - rerouting would turn
|
||||||
|
// that correct 1 into a 0, so the honest verdict is that nothing may be armed.
|
||||||
|
TEST(PrimitivesGeneratedNoXfbVerdictTest, ASubstituteWorseThanTheStreamOnAnyShapeIsRefused) {
|
||||||
|
const auto measurement = Measurement(Shape({1, false, 0, true, 1}), Shape({1, false, 0, true, 0}),
|
||||||
|
Shape({0, false, 0, true, 0}));
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::Unfixable);
|
||||||
|
// The same shape with the statistics slot MISSING on the stream-exact shape is
|
||||||
|
// the same trade: an unmeasured control cannot be assumed to answer.
|
||||||
|
const auto unmeasured = Measurement(Shape({1, false, 0, true, 1}), Shape({1, false, 0, false, 0}),
|
||||||
|
Shape({0, false, 0, true, 0}));
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(unmeasured),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::Unfixable);
|
||||||
|
// ...while the same selective silence WITH a substitute that covers the shapes
|
||||||
|
// it must still qualifies: every shape the statistics miss read 0 anyway.
|
||||||
|
const auto dominating = Measurement(Shape({1, false, 0, true, 1}), Shape({0, false, 0, true, 1}),
|
||||||
|
Shape({0, false, 0, true, 0}));
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(dominating),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The defect with no substitute: no control, controls silent, or a control that
|
||||||
|
// OVERCOUNTS the plain shape (as disqualifying as one that reads 0 - an exact
|
||||||
|
// match is what qualifies a substitute).
|
||||||
|
TEST(PrimitivesGeneratedNoXfbVerdictTest, StreamSilentWithoutAWorkingPlainControlIsUnfixable) {
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||||
|
Measurement(Shape({0}), Shape({0}), Shape({0}))),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::Unfixable);
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||||
|
Measurement(Shape({0, true, 0, true, 0}), Shape({0, true, 0, true, 0}),
|
||||||
|
Shape({0, true, 0, true, 0}))),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::Unfixable);
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||||
|
Measurement(Shape({0, true, 2, true, 2}), Shape({0, true, 1, true, 1}),
|
||||||
|
Shape({0, true, 1, true, 1}))),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::Unfixable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refuse-to-guess shapes. A nonzero-but-wrong stream answer fits neither the
|
||||||
|
// defect (exact silence) nor health (the exact count), whichever shape carries
|
||||||
|
// it; and a probe that never ran, or lost its mandatory shapes, says nothing.
|
||||||
|
TEST(PrimitivesGeneratedNoXfbVerdictTest, AnswersFittingNeitherHealthNorTheDefectAreInconclusive) {
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||||
|
Measurement(Shape({2, true, 1, true, 1}), Shape({0, true, 1, true, 1}),
|
||||||
|
Shape({0, true, 1, true, 1}))),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::Inconclusive);
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||||
|
Measurement(Shape({0, true, 1, true, 1}), Shape({3, true, 1, true, 1}),
|
||||||
|
Shape({0, true, 1, true, 1}))),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::Inconclusive);
|
||||||
|
|
||||||
|
PrimitivesGeneratedNoXfbMeasurement neverRan;
|
||||||
|
neverRan.ran = false;
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(neverRan),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::Inconclusive);
|
||||||
|
|
||||||
|
const auto missingMandatoryShape = Measurement(Shape({0, true, 1, true, 1}), NotDrawn(), NotDrawn());
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(missingMandatoryShape),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::Inconclusive);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A partial silence is still the defect: the plain shape counts but the discarded
|
||||||
|
// ones read 0 (a driver that gates the stream counter on rasterization rather
|
||||||
|
// than on the capture). With a whole control the substitute is whole.
|
||||||
|
TEST(PrimitivesGeneratedNoXfbVerdictTest, SilenceOnOnlyTheDiscardShapesIsStillTheDefect) {
|
||||||
|
const auto measurement = Measurement(Shape({1, true, 1, true, 1}), Shape({0, true, 1, true, 1}),
|
||||||
|
Shape({0, true, 1, true, 1}));
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== THE OVERRIDE MAPPING =====================
|
||||||
|
//
|
||||||
|
// The one-line swap this exists to catch: ForceOn and ForceOff exchanging arms,
|
||||||
|
// Auto arming on a verdict that never qualified a substitute, or the pool ranking
|
||||||
|
// inverting. Every cell of the (override x verdict) table is written out.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr PrimitivesGeneratedNoXfbVerdict kAllVerdicts[] = {
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::Inconclusive,
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::StreamCounts,
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute,
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute,
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly,
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::Unfixable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PrimitivesGeneratedNoXfbArmingTest, ForceOffNeverReroutes) {
|
||||||
|
for (const auto verdict : kAllVerdicts) {
|
||||||
|
for (const Bool pgqUsable : {false, true}) {
|
||||||
|
for (const Bool statsUsable : {false, true}) {
|
||||||
|
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(QuirkOverride::ForceOff, verdict, pgqUsable,
|
||||||
|
statsUsable),
|
||||||
|
PrimGenRerouteKind::None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PrimitivesGeneratedNoXfbArmingTest, ForceOnBypassesTheVerdictButNeverTheStructuralChecks) {
|
||||||
|
for (const auto verdict : kAllVerdicts) {
|
||||||
|
// The dedicated query wins where the device can host it...
|
||||||
|
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(QuirkOverride::ForceOn, verdict, true, true),
|
||||||
|
PrimGenRerouteKind::PrimitivesGeneratedExt);
|
||||||
|
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(QuirkOverride::ForceOn, verdict, true, false),
|
||||||
|
PrimGenRerouteKind::PrimitivesGeneratedExt);
|
||||||
|
// ...statistics stand in where only they exist...
|
||||||
|
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(QuirkOverride::ForceOn, verdict, false, true),
|
||||||
|
PrimGenRerouteKind::ClippingStatistics);
|
||||||
|
// ...and no pool means no reroute, forced or not.
|
||||||
|
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(QuirkOverride::ForceOn, verdict, false, false),
|
||||||
|
PrimGenRerouteKind::None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PrimitivesGeneratedNoXfbArmingTest, AutoFollowsExactlyTheSubstituteVerdicts) {
|
||||||
|
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(
|
||||||
|
QuirkOverride::Auto, PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute,
|
||||||
|
true, true),
|
||||||
|
PrimGenRerouteKind::PrimitivesGeneratedExt);
|
||||||
|
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(
|
||||||
|
QuirkOverride::Auto, PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute, false, true),
|
||||||
|
PrimGenRerouteKind::ClippingStatistics);
|
||||||
|
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(
|
||||||
|
QuirkOverride::Auto, PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly,
|
||||||
|
false, true),
|
||||||
|
PrimGenRerouteKind::ClippingStatistics);
|
||||||
|
// The statistics verdicts never take the dedicated pool: that verdict only
|
||||||
|
// exists when the dedicated query did NOT qualify.
|
||||||
|
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(
|
||||||
|
QuirkOverride::Auto, PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute, true, true),
|
||||||
|
PrimGenRerouteKind::ClippingStatistics);
|
||||||
|
for (const auto verdict :
|
||||||
|
{PrimitivesGeneratedNoXfbVerdict::Inconclusive, PrimitivesGeneratedNoXfbVerdict::StreamCounts,
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::Unfixable}) {
|
||||||
|
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(QuirkOverride::Auto, verdict, true, true),
|
||||||
|
PrimGenRerouteKind::None);
|
||||||
|
}
|
||||||
|
// The structural checks bind Auto too.
|
||||||
|
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(
|
||||||
|
QuirkOverride::Auto, PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute,
|
||||||
|
false, true),
|
||||||
|
PrimGenRerouteKind::None);
|
||||||
|
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(
|
||||||
|
QuirkOverride::Auto, PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute, false, false),
|
||||||
|
PrimGenRerouteKind::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== THE FENCE-TIMEOUT CONTRACT =====================
|
||||||
|
//
|
||||||
|
// A driver whose queue never signals the probe's fence inside 5 s is the one case
|
||||||
|
// where the probe must NOT clean up: the submission may still be executing, so
|
||||||
|
// vkDeviceWaitIdle can block forever and destroying in-flight objects is
|
||||||
|
// undefined. It therefore leaks everything it made and says so in the measurement
|
||||||
|
// (`fenceWaitTimedOut`), which is what lets its callers make the same choice for
|
||||||
|
// the object THEY own - the driver POST leaks its throwaway VkDevice instead of
|
||||||
|
// destroying it under live children (vkDestroyDevice would be the very hang the
|
||||||
|
// bound exists to prevent), and the renderer, whose device is the real one, must
|
||||||
|
// not idle-wait it either. Neither guard is reachable from a unit test - the POST
|
||||||
|
// probe lives in an anonymous namespace and the renderer needs a GPU - so this
|
||||||
|
// pins the contract they both key on, at the boundary where it is produced.
|
||||||
|
//
|
||||||
|
// The fake driver below is the whole Vulkan surface the probe touches, with a
|
||||||
|
// dialable fence-wait result and per-entry-point call counters.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct FakeDriverState {
|
||||||
|
VkResult fenceWaitResult = VK_SUCCESS;
|
||||||
|
Uint32 objectsCreated = 0;
|
||||||
|
Uint32 destroyCalls = 0;
|
||||||
|
Uint32 deviceWaitIdleCalls = 0;
|
||||||
|
Uint32 queueSubmitCalls = 0;
|
||||||
|
Uint64 streamGenerated = 1;
|
||||||
|
};
|
||||||
|
FakeDriverState g_fake;
|
||||||
|
|
||||||
|
template <typename Handle>
|
||||||
|
Handle FakeHandle() {
|
||||||
|
++g_fake.objectsCreated;
|
||||||
|
// One cast form for both handle flavours: a pointer on 64-bit builds, a
|
||||||
|
// uint64_t on 32-bit ones. The probe only ever compares against
|
||||||
|
// VK_NULL_HANDLE, so any distinct nonzero value will do.
|
||||||
|
return (Handle)(std::uintptr_t)(0x1000u + g_fake.objectsCreated * 0x10u);
|
||||||
|
}
|
||||||
|
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateCommandPool(VkDevice, const VkCommandPoolCreateInfo*,
|
||||||
|
const VkAllocationCallbacks*, VkCommandPool* out) {
|
||||||
|
*out = FakeHandle<VkCommandPool>();
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeDestroyCommandPool(VkDevice, VkCommandPool, const VkAllocationCallbacks*) {
|
||||||
|
++g_fake.destroyCalls;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL FakeAllocateCommandBuffers(VkDevice, const VkCommandBufferAllocateInfo*,
|
||||||
|
VkCommandBuffer* out) {
|
||||||
|
*out = FakeHandle<VkCommandBuffer>();
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL FakeBeginCommandBuffer(VkCommandBuffer, const VkCommandBufferBeginInfo*) {
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL FakeEndCommandBuffer(VkCommandBuffer) { return VK_SUCCESS; }
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateQueryPool(VkDevice, const VkQueryPoolCreateInfo*,
|
||||||
|
const VkAllocationCallbacks*, VkQueryPool* out) {
|
||||||
|
*out = FakeHandle<VkQueryPool>();
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeDestroyQueryPool(VkDevice, VkQueryPool, const VkAllocationCallbacks*) {
|
||||||
|
++g_fake.destroyCalls;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeCmdResetQueryPool(VkCommandBuffer, VkQueryPool, uint32_t, uint32_t) {}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeCmdBeginQuery(VkCommandBuffer, VkQueryPool, uint32_t, VkQueryControlFlags) {}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeCmdEndQuery(VkCommandBuffer, VkQueryPool, uint32_t) {}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeCmdBeginQueryIndexedEXT(VkCommandBuffer, VkQueryPool, uint32_t,
|
||||||
|
VkQueryControlFlags, uint32_t) {}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeCmdEndQueryIndexedEXT(VkCommandBuffer, VkQueryPool, uint32_t, uint32_t) {}
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateRenderPass(VkDevice, const VkRenderPassCreateInfo*,
|
||||||
|
const VkAllocationCallbacks*, VkRenderPass* out) {
|
||||||
|
*out = FakeHandle<VkRenderPass>();
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeDestroyRenderPass(VkDevice, VkRenderPass, const VkAllocationCallbacks*) {
|
||||||
|
++g_fake.destroyCalls;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateFramebuffer(VkDevice, const VkFramebufferCreateInfo*,
|
||||||
|
const VkAllocationCallbacks*, VkFramebuffer* out) {
|
||||||
|
*out = FakeHandle<VkFramebuffer>();
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeDestroyFramebuffer(VkDevice, VkFramebuffer, const VkAllocationCallbacks*) {
|
||||||
|
++g_fake.destroyCalls;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeCmdBeginRenderPass(VkCommandBuffer, const VkRenderPassBeginInfo*,
|
||||||
|
VkSubpassContents) {}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeCmdEndRenderPass(VkCommandBuffer) {}
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateShaderModule(VkDevice, const VkShaderModuleCreateInfo*,
|
||||||
|
const VkAllocationCallbacks*, VkShaderModule* out) {
|
||||||
|
*out = FakeHandle<VkShaderModule>();
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeDestroyShaderModule(VkDevice, VkShaderModule, const VkAllocationCallbacks*) {
|
||||||
|
++g_fake.destroyCalls;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL FakeCreatePipelineLayout(VkDevice, const VkPipelineLayoutCreateInfo*,
|
||||||
|
const VkAllocationCallbacks*,
|
||||||
|
VkPipelineLayout* out) {
|
||||||
|
*out = FakeHandle<VkPipelineLayout>();
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeDestroyPipelineLayout(VkDevice, VkPipelineLayout,
|
||||||
|
const VkAllocationCallbacks*) {
|
||||||
|
++g_fake.destroyCalls;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateGraphicsPipelines(VkDevice, VkPipelineCache, uint32_t count,
|
||||||
|
const VkGraphicsPipelineCreateInfo*,
|
||||||
|
const VkAllocationCallbacks*, VkPipeline* out) {
|
||||||
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
|
out[i] = FakeHandle<VkPipeline>();
|
||||||
|
}
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeDestroyPipeline(VkDevice, VkPipeline, const VkAllocationCallbacks*) {
|
||||||
|
++g_fake.destroyCalls;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeCmdBindPipeline(VkCommandBuffer, VkPipelineBindPoint, VkPipeline) {}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeCmdDraw(VkCommandBuffer, uint32_t, uint32_t, uint32_t, uint32_t) {}
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateFence(VkDevice, const VkFenceCreateInfo*,
|
||||||
|
const VkAllocationCallbacks*, VkFence* out) {
|
||||||
|
*out = FakeHandle<VkFence>();
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FakeDestroyFence(VkDevice, VkFence, const VkAllocationCallbacks*) {
|
||||||
|
++g_fake.destroyCalls;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL FakeQueueSubmit(VkQueue, uint32_t, const VkSubmitInfo*, VkFence) {
|
||||||
|
++g_fake.queueSubmitCalls;
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL FakeWaitForFences(VkDevice, uint32_t, const VkFence*, VkBool32, uint64_t) {
|
||||||
|
return g_fake.fenceWaitResult;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL FakeGetQueryPoolResults(VkDevice, VkQueryPool, uint32_t, uint32_t,
|
||||||
|
size_t dataSize, void* data, VkDeviceSize,
|
||||||
|
VkQueryResultFlags) {
|
||||||
|
// The stream pool's {primitivesWritten, primitivesNeeded} pair; the probe
|
||||||
|
// reads primitivesNeeded, and this fake device counts capture-less draws.
|
||||||
|
if (data == nullptr || dataSize < 2 * sizeof(Uint64)) {
|
||||||
|
return VK_INCOMPLETE;
|
||||||
|
}
|
||||||
|
auto* pair = static_cast<Uint64*>(data);
|
||||||
|
pair[0] = 0;
|
||||||
|
pair[1] = g_fake.streamGenerated;
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL FakeDeviceWaitIdle(VkDevice) {
|
||||||
|
++g_fake.deviceWaitIdleCalls;
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimitivesGeneratedNoXfbProbeContext FakeProbeContext() {
|
||||||
|
g_fake = FakeDriverState{};
|
||||||
|
PrimitivesGeneratedNoXfbProbeContext context;
|
||||||
|
context.device = (VkDevice)(std::uintptr_t)0xD0D0;
|
||||||
|
context.queue = (VkQueue)(std::uintptr_t)0xC0C0;
|
||||||
|
context.transformFeedbackQueriesUsable = true;
|
||||||
|
// No controls and no tessellation: this fixture is about the teardown
|
||||||
|
// contract, and the fewer optional slots the fewer moving parts.
|
||||||
|
auto& fns = context.fns;
|
||||||
|
fns.vkCreateCommandPool = FakeCreateCommandPool;
|
||||||
|
fns.vkDestroyCommandPool = FakeDestroyCommandPool;
|
||||||
|
fns.vkAllocateCommandBuffers = FakeAllocateCommandBuffers;
|
||||||
|
fns.vkBeginCommandBuffer = FakeBeginCommandBuffer;
|
||||||
|
fns.vkEndCommandBuffer = FakeEndCommandBuffer;
|
||||||
|
fns.vkCreateQueryPool = FakeCreateQueryPool;
|
||||||
|
fns.vkDestroyQueryPool = FakeDestroyQueryPool;
|
||||||
|
fns.vkCmdResetQueryPool = FakeCmdResetQueryPool;
|
||||||
|
fns.vkCmdBeginQuery = FakeCmdBeginQuery;
|
||||||
|
fns.vkCmdEndQuery = FakeCmdEndQuery;
|
||||||
|
fns.vkCmdBeginQueryIndexedEXT = FakeCmdBeginQueryIndexedEXT;
|
||||||
|
fns.vkCmdEndQueryIndexedEXT = FakeCmdEndQueryIndexedEXT;
|
||||||
|
fns.vkCreateRenderPass = FakeCreateRenderPass;
|
||||||
|
fns.vkDestroyRenderPass = FakeDestroyRenderPass;
|
||||||
|
fns.vkCreateFramebuffer = FakeCreateFramebuffer;
|
||||||
|
fns.vkDestroyFramebuffer = FakeDestroyFramebuffer;
|
||||||
|
fns.vkCmdBeginRenderPass = FakeCmdBeginRenderPass;
|
||||||
|
fns.vkCmdEndRenderPass = FakeCmdEndRenderPass;
|
||||||
|
fns.vkCreateShaderModule = FakeCreateShaderModule;
|
||||||
|
fns.vkDestroyShaderModule = FakeDestroyShaderModule;
|
||||||
|
fns.vkCreatePipelineLayout = FakeCreatePipelineLayout;
|
||||||
|
fns.vkDestroyPipelineLayout = FakeDestroyPipelineLayout;
|
||||||
|
fns.vkCreateGraphicsPipelines = FakeCreateGraphicsPipelines;
|
||||||
|
fns.vkDestroyPipeline = FakeDestroyPipeline;
|
||||||
|
fns.vkCmdBindPipeline = FakeCmdBindPipeline;
|
||||||
|
fns.vkCmdDraw = FakeCmdDraw;
|
||||||
|
fns.vkCreateFence = FakeCreateFence;
|
||||||
|
fns.vkDestroyFence = FakeDestroyFence;
|
||||||
|
fns.vkQueueSubmit = FakeQueueSubmit;
|
||||||
|
fns.vkWaitForFences = FakeWaitForFences;
|
||||||
|
fns.vkGetQueryPoolResults = FakeGetQueryPoolResults;
|
||||||
|
fns.vkDeviceWaitIdle = FakeDeviceWaitIdle;
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The hung driver. Nothing the probe created may be destroyed, the device may not
|
||||||
|
// be idle-waited, and the measurement must SAY the wait timed out - a caller that
|
||||||
|
// owns the device reads that flag to leak it too, and `ran == false` alone cannot
|
||||||
|
// tell this apart from an ordinary setup failure (where teardown already ran and
|
||||||
|
// destroying the device is correct).
|
||||||
|
TEST(PrimitivesGeneratedNoXfbProbeTeardownTest, AFenceTimeoutLeaksEverythingAndReportsItself) {
|
||||||
|
PrimitivesGeneratedNoXfbProbeContext context = FakeProbeContext();
|
||||||
|
g_fake.fenceWaitResult = VK_TIMEOUT;
|
||||||
|
|
||||||
|
const PrimitivesGeneratedNoXfbMeasurement measurement = RunPrimitivesGeneratedNoXfbProbe(context);
|
||||||
|
|
||||||
|
EXPECT_FALSE(measurement.ran);
|
||||||
|
EXPECT_TRUE(measurement.fenceWaitTimedOut)
|
||||||
|
<< "without this flag the POST destroys its throwaway VkDevice while the probe's children "
|
||||||
|
"are alive and its submission may still be executing";
|
||||||
|
EXPECT_GT(g_fake.queueSubmitCalls, 0u) << "the timeout must be the SUBMITTED probe's, not a setup failure";
|
||||||
|
EXPECT_EQ(g_fake.destroyCalls, 0u)
|
||||||
|
<< "a probe that timed out must destroy nothing: the submission may still be executing";
|
||||||
|
EXPECT_EQ(g_fake.deviceWaitIdleCalls, 0u)
|
||||||
|
<< "vkDeviceWaitIdle on a queue that missed a 5 s deadline is the hang the bound exists to "
|
||||||
|
"prevent";
|
||||||
|
// The verdict must not read a timed-out probe as anything but "no verdict".
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::Inconclusive);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The control: a driver that signals normally gets the ordinary teardown - idle
|
||||||
|
// wait, every object destroyed, no timeout flag - so the case above is testing the
|
||||||
|
// timeout branch and not a probe that never cleans up at all.
|
||||||
|
TEST(PrimitivesGeneratedNoXfbProbeTeardownTest, ASignalledFenceTearsDownNormally) {
|
||||||
|
PrimitivesGeneratedNoXfbProbeContext context = FakeProbeContext();
|
||||||
|
g_fake.fenceWaitResult = VK_SUCCESS;
|
||||||
|
g_fake.streamGenerated = 1; // healthy: the capture-less draws are counted
|
||||||
|
|
||||||
|
const PrimitivesGeneratedNoXfbMeasurement measurement = RunPrimitivesGeneratedNoXfbProbe(context);
|
||||||
|
|
||||||
|
EXPECT_TRUE(measurement.ran) << measurement.failureReason;
|
||||||
|
EXPECT_FALSE(measurement.fenceWaitTimedOut);
|
||||||
|
EXPECT_EQ(g_fake.deviceWaitIdleCalls, 1u);
|
||||||
|
EXPECT_GT(g_fake.destroyCalls, 0u);
|
||||||
|
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||||
|
PrimitivesGeneratedNoXfbVerdict::StreamCounts);
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ add_executable(
|
|||||||
FlattenAtomicCounterBlockTest.cpp
|
FlattenAtomicCounterBlockTest.cpp
|
||||||
WidenImageFormatsTest.cpp
|
WidenImageFormatsTest.cpp
|
||||||
GlslangCaptureTest.cpp
|
GlslangCaptureTest.cpp
|
||||||
|
DemotePointSizeTest.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(SpirvPassTest PRIVATE
|
target_include_directories(SpirvPassTest PRIVATE
|
||||||
|
|||||||
@@ -0,0 +1,720 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/DemotePointSizeTest.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
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include "Init.h"
|
||||||
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
|
|
||||||
|
#include "spirv-tools/libspirv.hpp"
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
|
||||||
|
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||||
|
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Compiles and LINKS a whole program, then returns one sanitized module per stage - the
|
||||||
|
// exact bytes ProgramSpirvTask hands the demotion in production, so every shape assertion
|
||||||
|
// below is made against what the backends would really receive.
|
||||||
|
Vector<Vector<Uint32>> CompileProgramToSpirv(const Vector<Pair<GLenum, const char*>>& stages) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
Vector<SharedPtr<glslang::TShader>> shaders;
|
||||||
|
Vector<GLenum> types;
|
||||||
|
for (const auto& [stage, source] : stages) {
|
||||||
|
// sourceStr is a StringView; the literals handed in are static, so the view
|
||||||
|
// stays valid for the whole compile.
|
||||||
|
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||||
|
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||||
|
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||||
|
if (!shaderResult) return {};
|
||||||
|
shaders.push_back(shaderResult.value());
|
||||||
|
types.push_back(stage);
|
||||||
|
}
|
||||||
|
ProgramAttrib programAttrib{.shaders = shaders};
|
||||||
|
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||||
|
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||||
|
if (!programResult) return {};
|
||||||
|
ProgramBinaryAttrib binaryAttrib{.shaderTypes = types, .program = *programResult.value()};
|
||||||
|
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||||
|
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||||
|
if (!binaryResult) return {};
|
||||||
|
Vector<Vector<Uint32>> modules = Move(binaryResult.value());
|
||||||
|
for (auto& module : modules) {
|
||||||
|
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true));
|
||||||
|
}
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
String Disassemble(const Vector<Uint32>& spirv) {
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
String text;
|
||||||
|
EXPECT_TRUE(tools.Disassemble(spirv, &text));
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
String Transpile(const Vector<Uint32>& spirv) {
|
||||||
|
SpvcSession session(spirv, SessionUsageBit::Transpile);
|
||||||
|
auto essl = ShaderCompiler::DecompileShader(session);
|
||||||
|
EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log);
|
||||||
|
return essl ? essl.value() : String{};
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool Validates(const Vector<Uint32>& spirv) {
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
return tools.Validate(spirv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The five-stage shape of the KHR-GL4x transform-feedback / tessellation capture bodies:
|
||||||
|
// the value is WRITTEN in the vertex stage, READ from gl_in and re-written in every stage
|
||||||
|
// after it, and the rasterized size never matters (the captures run under rasterizer
|
||||||
|
// discard). This is exactly the class the demotion exists to rescue.
|
||||||
|
const char* kVertexSource = R"(#version 460 core
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(float(gl_VertexID), 0.0, 0.0, 1.0);
|
||||||
|
gl_PointSize = 2.0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* kTessControlSource = R"(#version 460 core
|
||||||
|
layout(vertices = 3) out;
|
||||||
|
void main() {
|
||||||
|
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
|
||||||
|
gl_out[gl_InvocationID].gl_PointSize = gl_in[gl_InvocationID].gl_PointSize + 1.0;
|
||||||
|
gl_TessLevelOuter[0] = 1.0;
|
||||||
|
gl_TessLevelOuter[1] = 1.0;
|
||||||
|
gl_TessLevelOuter[2] = 1.0;
|
||||||
|
gl_TessLevelInner[0] = 1.0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* kTessEvalSource = R"(#version 460 core
|
||||||
|
layout(triangles, point_mode) in;
|
||||||
|
void main() {
|
||||||
|
gl_Position = gl_TessCoord.x * gl_in[0].gl_Position + gl_TessCoord.y * gl_in[1].gl_Position +
|
||||||
|
gl_TessCoord.z * gl_in[2].gl_Position;
|
||||||
|
gl_PointSize = gl_in[0].gl_PointSize + gl_in[1].gl_PointSize + gl_in[2].gl_PointSize;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* kGeometrySource = R"(#version 460 core
|
||||||
|
layout(points) in;
|
||||||
|
layout(points, max_vertices = 1) out;
|
||||||
|
void main() {
|
||||||
|
gl_Position = gl_in[0].gl_Position;
|
||||||
|
gl_PointSize = gl_in[0].gl_PointSize * 2.0;
|
||||||
|
EmitVertex();
|
||||||
|
EndPrimitive();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* kFragmentSource = R"(#version 460 core
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
void main() { fragColor = vec4(1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
// A control chain that never touches point size: the demotion must prove it changed
|
||||||
|
// NOTHING here, byte for byte, because this is the overwhelming majority of programs on
|
||||||
|
// an affected device.
|
||||||
|
const char* kPlainTessControlSource = R"(#version 460 core
|
||||||
|
layout(vertices = 3) out;
|
||||||
|
void main() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* kPlainTessEvalSource = R"(#version 460 core
|
||||||
|
layout(triangles, point_mode) in;
|
||||||
|
void main() {
|
||||||
|
gl_Position = gl_in[0].gl_Position;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* kPlainVertexSource = R"(#version 460 core
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(float(gl_VertexID), 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// A control stage that also carries CLIP DISTANCE. SPIRV-Cross force-redeclares the whole
|
||||||
|
// gl_PerVertex output block for exactly this stage/builtin combination, and prints its
|
||||||
|
// members from the struct's DECORATIONS rather than from what the module accesses - so a
|
||||||
|
// demoted module's untouched PointSize member would still reach the driver's ESSL.
|
||||||
|
const char* kClipDistanceTessControlSource = R"(#version 460 core
|
||||||
|
layout(vertices = 3) out;
|
||||||
|
void main() {
|
||||||
|
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
|
||||||
|
gl_out[gl_InvocationID].gl_PointSize = gl_in[gl_InvocationID].gl_PointSize + 1.0;
|
||||||
|
gl_out[gl_InvocationID].gl_ClipDistance[0] = 0.5;
|
||||||
|
gl_TessLevelOuter[0] = 1.0;
|
||||||
|
gl_TessLevelOuter[1] = 1.0;
|
||||||
|
gl_TessLevelOuter[2] = 1.0;
|
||||||
|
gl_TessLevelInner[0] = 1.0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The same clip-distance write and NO point-size access anywhere: the shape a
|
||||||
|
// successfully demoted module would have been left in. glslang emits the whole
|
||||||
|
// four-member gl_PerVertex block regardless, which is what makes it the exact
|
||||||
|
// "declared but unaccessed" state the pass header's premise is about.
|
||||||
|
const char* kClipDistanceUnusedPointSizeTessControlSource = R"(#version 460 core
|
||||||
|
layout(vertices = 3) out;
|
||||||
|
void main() {
|
||||||
|
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
|
||||||
|
gl_out[gl_InvocationID].gl_ClipDistance[0] = 0.5;
|
||||||
|
gl_TessLevelOuter[0] = 1.0;
|
||||||
|
gl_TessLevelOuter[1] = 1.0;
|
||||||
|
gl_TessLevelOuter[2] = 1.0;
|
||||||
|
gl_TessLevelInner[0] = 1.0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// A tessellation evaluation module reaching PointSize through a WHOLE-STRUCT load - the
|
||||||
|
// one shape the pass must refuse rather than half-rewrite. glslang never emits it, so it
|
||||||
|
// is assembled by hand.
|
||||||
|
const char* kWholeStructCopyTessEvalAsm = R"(
|
||||||
|
OpCapability Tessellation
|
||||||
|
OpCapability TessellationPointSize
|
||||||
|
OpMemoryModel Logical GLSL450
|
||||||
|
OpEntryPoint TessellationEvaluation %main "main" %gl_in %out_block
|
||||||
|
OpExecutionMode %main Triangles
|
||||||
|
OpExecutionMode %main SpacingEqual
|
||||||
|
OpExecutionMode %main VertexOrderCcw
|
||||||
|
OpMemberDecorate %gl_PerVertex 0 BuiltIn Position
|
||||||
|
OpMemberDecorate %gl_PerVertex 1 BuiltIn PointSize
|
||||||
|
OpDecorate %gl_PerVertex Block
|
||||||
|
%void = OpTypeVoid
|
||||||
|
%fn_ty = OpTypeFunction %void
|
||||||
|
%float = OpTypeFloat 32
|
||||||
|
%v4float = OpTypeVector %float 4
|
||||||
|
%gl_PerVertex = OpTypeStruct %v4float %float
|
||||||
|
%uint = OpTypeInt 32 0
|
||||||
|
%uint_32 = OpConstant %uint 32
|
||||||
|
%arr = OpTypeArray %gl_PerVertex %uint_32
|
||||||
|
%ptr_in_arr = OpTypePointer Input %arr
|
||||||
|
%gl_in = OpVariable %ptr_in_arr Input
|
||||||
|
%ptr_out_s = OpTypePointer Output %gl_PerVertex
|
||||||
|
%out_block = OpVariable %ptr_out_s Output
|
||||||
|
%ptr_in_s = OpTypePointer Input %gl_PerVertex
|
||||||
|
%int = OpTypeInt 32 1
|
||||||
|
%int_0 = OpConstant %int 0
|
||||||
|
%main = OpFunction %void None %fn_ty
|
||||||
|
%entry = OpLabel
|
||||||
|
%p = OpAccessChain %ptr_in_s %gl_in %int_0
|
||||||
|
%v = OpLoad %gl_PerVertex %p
|
||||||
|
OpStore %out_block %v
|
||||||
|
OpReturn
|
||||||
|
OpFunctionEnd
|
||||||
|
)";
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class DemotePointSizeTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
m_validationFailuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||||
|
}
|
||||||
|
void TearDown() override {
|
||||||
|
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresBefore)
|
||||||
|
<< "a demoted module did not survive spirv-val";
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Uint64 m_validationFailuresBefore = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(DemotePointSizeTest, DemotesAFiveStageProgramWholesale) {
|
||||||
|
Vector<Vector<Uint32>> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||||
|
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||||
|
{GL_TESS_EVALUATION_SHADER, kTessEvalSource},
|
||||||
|
{GL_GEOMETRY_SHADER, kGeometrySource},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||||
|
ASSERT_EQ(modules.size(), 5u);
|
||||||
|
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER,
|
||||||
|
GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER};
|
||||||
|
|
||||||
|
// The defect, pinned first: every tessellation/geometry stage really does declare the
|
||||||
|
// capability the device lacks - the same probe production's declines use.
|
||||||
|
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[1]));
|
||||||
|
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[2]));
|
||||||
|
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[3]));
|
||||||
|
|
||||||
|
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
modules, types, true, true, /*captureRequestsPointSize=*/true, outcome, true, true));
|
||||||
|
EXPECT_TRUE(outcome.demoted) << outcome.declineDetail;
|
||||||
|
|
||||||
|
// THE PRODUCTION GATE, as the arming guard: after demotion neither decline can arm.
|
||||||
|
// Magma's refusal and Espryt's missing-extension failure both key off exactly these.
|
||||||
|
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[1]));
|
||||||
|
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[2]));
|
||||||
|
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[3]));
|
||||||
|
for (const auto& module : modules) {
|
||||||
|
EXPECT_TRUE(Validates(module));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The carrier chain, boundary by boundary. No user varyings, so the shared location is 0.
|
||||||
|
const String vs = Disassemble(modules[0]);
|
||||||
|
EXPECT_NE(vs.find("OpName %mg_PointSizeIo0"), String::npos) << vs;
|
||||||
|
EXPECT_NE(vs.find("OpStore %mg_PointSizeIo0"), String::npos)
|
||||||
|
<< "the vertex stage must mirror its built-in into the carrier:\n"
|
||||||
|
<< vs;
|
||||||
|
EXPECT_NE(vs.find("BuiltIn PointSize"), String::npos)
|
||||||
|
<< "the vertex stage KEEPS its core built-in - only tess/geometry stages demote:\n"
|
||||||
|
<< vs;
|
||||||
|
|
||||||
|
const String tcs = Disassemble(modules[1]);
|
||||||
|
EXPECT_EQ(tcs.find("OpCapability TessellationPointSize"), String::npos) << tcs;
|
||||||
|
EXPECT_NE(tcs.find("OpName %mg_PointSizeIo0"), String::npos) << tcs;
|
||||||
|
EXPECT_NE(tcs.find("OpName %mg_PointSizeIo1"), String::npos) << tcs;
|
||||||
|
|
||||||
|
const String tes = Disassemble(modules[2]);
|
||||||
|
EXPECT_EQ(tes.find("OpCapability TessellationPointSize"), String::npos) << tes;
|
||||||
|
EXPECT_NE(tes.find("OpName %mg_PointSizeIo1"), String::npos) << tes;
|
||||||
|
EXPECT_NE(tes.find("OpName %mg_PointSizeIo2"), String::npos)
|
||||||
|
<< "with a geometry stage present the evaluation stage feeds the Io2 boundary, not the "
|
||||||
|
"capture carrier:\n"
|
||||||
|
<< tes;
|
||||||
|
|
||||||
|
const String gs = Disassemble(modules[3]);
|
||||||
|
EXPECT_EQ(gs.find("OpCapability GeometryPointSize"), String::npos) << gs;
|
||||||
|
EXPECT_NE(gs.find("OpName %mg_PointSizeIo2"), String::npos) << gs;
|
||||||
|
EXPECT_NE(gs.find("OpName %mg_PointSizeCapture"), String::npos) << gs;
|
||||||
|
EXPECT_NE(gs.find("OpDecorate %mg_PointSizeCapture Location 0"), String::npos) << gs;
|
||||||
|
EXPECT_NE(gs.find("OpStore %mg_PointSizeCapture"), String::npos) << gs;
|
||||||
|
|
||||||
|
// The struct keeps its member - declared, decorated, unaccessed - which is the shape a
|
||||||
|
// point-size-free glslang module already has on every extension-less driver.
|
||||||
|
EXPECT_NE(tes.find("BuiltIn PointSize"), String::npos) << tes;
|
||||||
|
|
||||||
|
// What SPIRV-Cross then prints: no gl_PointSize anywhere in a demoted stage's ESSL (the
|
||||||
|
// token DirectGLES's extension gate greps for), the carriers in its place. The CONTROL
|
||||||
|
// stage is transpiled too, and deliberately: it is the one stage SPIRV-Cross can be made
|
||||||
|
// to redeclare the whole output block for, which is why the clip-distance combination
|
||||||
|
// declines instead of demoting.
|
||||||
|
const String tcsEssl = Transpile(modules[1]);
|
||||||
|
EXPECT_EQ(tcsEssl.find("gl_PointSize"), String::npos) << tcsEssl;
|
||||||
|
EXPECT_NE(tcsEssl.find("mg_PointSizeIo1"), String::npos) << tcsEssl;
|
||||||
|
const String tesEssl = Transpile(modules[2]);
|
||||||
|
EXPECT_EQ(tesEssl.find("gl_PointSize"), String::npos) << tesEssl;
|
||||||
|
EXPECT_NE(tesEssl.find("mg_PointSizeIo1"), String::npos) << tesEssl;
|
||||||
|
const String gsEssl = Transpile(modules[3]);
|
||||||
|
EXPECT_EQ(gsEssl.find("gl_PointSize"), String::npos) << gsEssl;
|
||||||
|
EXPECT_NE(gsEssl.find("mg_PointSizeCapture"), String::npos) << gsEssl;
|
||||||
|
|
||||||
|
// Demotion is idempotent by construction: with the capability gone, a second pass over
|
||||||
|
// the same modules finds nothing to arm on and must not touch a byte.
|
||||||
|
Vector<Vector<Uint32>> again = modules;
|
||||||
|
ShaderCompiler::PointSizeDemotionOutcome secondOutcome;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
again, types, true, true, true, secondOutcome, true, true));
|
||||||
|
EXPECT_FALSE(secondOutcome.demoted);
|
||||||
|
EXPECT_TRUE(secondOutcome.declineDetail.empty()) << secondOutcome.declineDetail;
|
||||||
|
EXPECT_EQ(again, modules);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DemotePointSizeTest, WithoutAGeometryStageTheEvaluationStageOwnsTheCaptureCarrier) {
|
||||||
|
Vector<Vector<Uint32>> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||||
|
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||||
|
{GL_TESS_EVALUATION_SHADER, kTessEvalSource},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||||
|
ASSERT_EQ(modules.size(), 4u);
|
||||||
|
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER,
|
||||||
|
GL_FRAGMENT_SHADER};
|
||||||
|
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
modules, types, true, true, true, outcome, true, true));
|
||||||
|
EXPECT_TRUE(outcome.demoted) << outcome.declineDetail;
|
||||||
|
|
||||||
|
const String tes = Disassemble(modules[2]);
|
||||||
|
EXPECT_NE(tes.find("OpName %mg_PointSizeCapture"), String::npos) << tes;
|
||||||
|
EXPECT_NE(tes.find("OpStore %mg_PointSizeCapture"), String::npos) << tes;
|
||||||
|
EXPECT_EQ(tes.find("OpName %mg_PointSizeIo2"), String::npos)
|
||||||
|
<< "no geometry stage, no Io2 boundary:\n"
|
||||||
|
<< tes;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DemotePointSizeTest, AGeometryOnlyProgramReadsTheVertexBoundary) {
|
||||||
|
const char* geometryReadingVs = R"(#version 460 core
|
||||||
|
layout(points) in;
|
||||||
|
layout(points, max_vertices = 1) out;
|
||||||
|
void main() {
|
||||||
|
gl_Position = gl_in[0].gl_Position;
|
||||||
|
gl_PointSize = gl_in[0].gl_PointSize * 2.0;
|
||||||
|
EmitVertex();
|
||||||
|
EndPrimitive();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
Vector<Vector<Uint32>> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||||
|
{GL_GEOMETRY_SHADER, geometryReadingVs},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||||
|
ASSERT_EQ(modules.size(), 3u);
|
||||||
|
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER};
|
||||||
|
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
modules, types, /*demoteTessellation=*/false, /*demoteGeometry=*/true, false, outcome, true,
|
||||||
|
true));
|
||||||
|
EXPECT_TRUE(outcome.demoted) << outcome.declineDetail;
|
||||||
|
|
||||||
|
const String gs = Disassemble(modules[1]);
|
||||||
|
EXPECT_NE(gs.find("OpName %mg_PointSizeIo0"), String::npos)
|
||||||
|
<< "the geometry stage's input boundary is fed by the vertex stage:\n"
|
||||||
|
<< gs;
|
||||||
|
const String vs = Disassemble(modules[0]);
|
||||||
|
EXPECT_NE(vs.find("OpStore %mg_PointSizeIo0"), String::npos) << vs;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DemotePointSizeTest, TheCarrierLandsPastTheProgramsOwnVaryings) {
|
||||||
|
const char* vsWithVarying = R"(#version 460 core
|
||||||
|
out vec4 v_color;
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(1.0);
|
||||||
|
gl_PointSize = 3.0;
|
||||||
|
v_color = vec4(0.5);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const char* gsWithVarying = R"(#version 460 core
|
||||||
|
layout(points) in;
|
||||||
|
layout(points, max_vertices = 1) out;
|
||||||
|
in vec4 v_color[];
|
||||||
|
out vec4 g_color;
|
||||||
|
void main() {
|
||||||
|
gl_Position = gl_in[0].gl_Position;
|
||||||
|
gl_PointSize = gl_in[0].gl_PointSize;
|
||||||
|
g_color = v_color[0];
|
||||||
|
EmitVertex();
|
||||||
|
EndPrimitive();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const char* fsWithVarying = R"(#version 460 core
|
||||||
|
in vec4 g_color;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
void main() { fragColor = g_color; }
|
||||||
|
)";
|
||||||
|
Vector<Vector<Uint32>> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, vsWithVarying},
|
||||||
|
{GL_GEOMETRY_SHADER, gsWithVarying},
|
||||||
|
{GL_FRAGMENT_SHADER, fsWithVarying}});
|
||||||
|
ASSERT_EQ(modules.size(), 3u);
|
||||||
|
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER};
|
||||||
|
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
modules, types, false, true, true, outcome, true, true));
|
||||||
|
EXPECT_TRUE(outcome.demoted) << outcome.declineDetail;
|
||||||
|
|
||||||
|
// v_color / g_color occupy location 0, so every carrier must sit at 1 - in every stage,
|
||||||
|
// because producer and consumer match by location.
|
||||||
|
const String vs = Disassemble(modules[0]);
|
||||||
|
EXPECT_NE(vs.find("OpDecorate %mg_PointSizeIo0 Location 1"), String::npos) << vs;
|
||||||
|
const String gs = Disassemble(modules[1]);
|
||||||
|
EXPECT_NE(gs.find("OpDecorate %mg_PointSizeIo0 Location 1"), String::npos) << gs;
|
||||||
|
EXPECT_NE(gs.find("OpDecorate %mg_PointSizeCapture Location 1"), String::npos) << gs;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DemotePointSizeTest, APointSizeFreeProgramStaysByteIdentical) {
|
||||||
|
Vector<Vector<Uint32>> modules =
|
||||||
|
CompileProgramToSpirv({{GL_VERTEX_SHADER, kPlainVertexSource},
|
||||||
|
{GL_TESS_CONTROL_SHADER, kPlainTessControlSource},
|
||||||
|
{GL_TESS_EVALUATION_SHADER, kPlainTessEvalSource},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||||
|
ASSERT_EQ(modules.size(), 4u);
|
||||||
|
const Vector<Vector<Uint32>> before = modules;
|
||||||
|
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER,
|
||||||
|
GL_FRAGMENT_SHADER};
|
||||||
|
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
modules, types, true, true, false, outcome, true, true));
|
||||||
|
EXPECT_FALSE(outcome.demoted);
|
||||||
|
EXPECT_TRUE(outcome.declineDetail.empty()) << outcome.declineDetail;
|
||||||
|
EXPECT_EQ(modules, before);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DemotePointSizeTest, AHostingDeviceStaysByteIdentical) {
|
||||||
|
Vector<Vector<Uint32>> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||||
|
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||||
|
{GL_TESS_EVALUATION_SHADER, kTessEvalSource},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||||
|
ASSERT_EQ(modules.size(), 4u);
|
||||||
|
const Vector<Vector<Uint32>> before = modules;
|
||||||
|
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER,
|
||||||
|
GL_FRAGMENT_SHADER};
|
||||||
|
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||||
|
// Both verdicts say the device hosts the built-in: the un-forced lane's contract.
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
modules, types, false, false, true, outcome, true, true));
|
||||||
|
EXPECT_FALSE(outcome.demoted);
|
||||||
|
EXPECT_EQ(modules, before);
|
||||||
|
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[1]))
|
||||||
|
<< "the un-demoted module must still arm the existing declines";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DemotePointSizeTest, ACaptureRequestForcesTheCarrierOnANonWritingCaptureStage) {
|
||||||
|
// The control stage writes point size (arming the demotion); the evaluation stage never
|
||||||
|
// does - but a by-name capture must still find the carrier declared there, holding
|
||||||
|
// whatever an unwritten varying holds, exactly as the unwritten built-in would have.
|
||||||
|
Vector<Vector<Uint32>> modules =
|
||||||
|
CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||||
|
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||||
|
{GL_TESS_EVALUATION_SHADER, kPlainTessEvalSource},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||||
|
ASSERT_EQ(modules.size(), 4u);
|
||||||
|
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER,
|
||||||
|
GL_FRAGMENT_SHADER};
|
||||||
|
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
modules, types, true, true, /*captureRequestsPointSize=*/true, outcome, true, true));
|
||||||
|
EXPECT_TRUE(outcome.demoted) << outcome.declineDetail;
|
||||||
|
|
||||||
|
const String tes = Disassemble(modules[2]);
|
||||||
|
EXPECT_NE(tes.find("OpName %mg_PointSizeCapture"), String::npos) << tes;
|
||||||
|
EXPECT_TRUE(Validates(modules[2]));
|
||||||
|
|
||||||
|
// And the driver-side half of the same contract: the ESSL DirectGLES hands its driver
|
||||||
|
// has to DECLARE the carrier, because DirectGLES respells the glTransformFeedbackVaryings
|
||||||
|
// request to that name. A carrier the transpile dropped would take the whole capture set
|
||||||
|
// down with an ES link error naming a variable the application never wrote.
|
||||||
|
const String tesEssl = Transpile(modules[2]);
|
||||||
|
EXPECT_NE(tesEssl.find("mg_PointSizeCapture"), String::npos) << tesEssl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE PRODUCTION SHAPE THE FORCED CARRIER EXISTS FOR, and the one the flag's own unit test
|
||||||
|
// could not reach: the capture stage never WRITES gl_PointSize, it only reads the incoming
|
||||||
|
// one. The demotion still arms - glslang declares GeometryPointSize on a READ - so the
|
||||||
|
// built-in leaves the module, and only the capture request can put a carrier back. In
|
||||||
|
// production that request arrives as ProgramLinkTask::SpirvHandoff::captureRequestsPointSize;
|
||||||
|
// this is the same value one layer down.
|
||||||
|
TEST_F(DemotePointSizeTest, AReadOnlyCaptureStageStillDeclaresTheCaptureCarrier) {
|
||||||
|
const char* readOnlyGeometry = R"(#version 460 core
|
||||||
|
layout(points) in;
|
||||||
|
layout(points, max_vertices = 1) out;
|
||||||
|
out float g_echo;
|
||||||
|
void main() {
|
||||||
|
gl_Position = gl_in[0].gl_Position;
|
||||||
|
g_echo = gl_in[0].gl_PointSize;
|
||||||
|
EmitVertex();
|
||||||
|
EndPrimitive();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const char* echoFragment = R"(#version 460 core
|
||||||
|
in float g_echo;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
void main() { fragColor = vec4(g_echo); }
|
||||||
|
)";
|
||||||
|
Vector<Vector<Uint32>> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||||
|
{GL_GEOMETRY_SHADER, readOnlyGeometry},
|
||||||
|
{GL_FRAGMENT_SHADER, echoFragment}});
|
||||||
|
ASSERT_EQ(modules.size(), 3u);
|
||||||
|
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER};
|
||||||
|
|
||||||
|
// The premise: a stage that only READS the built-in still declares the capability, so the
|
||||||
|
// device still refuses it and the demotion still arms.
|
||||||
|
ASSERT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[1]))
|
||||||
|
<< "a geometry stage that only reads gl_in[].gl_PointSize must still declare "
|
||||||
|
"GeometryPointSize, or this whole class of program was never affected";
|
||||||
|
|
||||||
|
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
modules, types, false, true, /*captureRequestsPointSize=*/true, outcome, true, true));
|
||||||
|
EXPECT_TRUE(outcome.demoted) << outcome.declineDetail;
|
||||||
|
|
||||||
|
const String gs = Disassemble(modules[1]);
|
||||||
|
EXPECT_NE(gs.find("OpName %mg_PointSizeIo0"), String::npos)
|
||||||
|
<< "the read still has to reach the vertex stage's mirrored value:\n"
|
||||||
|
<< gs;
|
||||||
|
EXPECT_NE(gs.find("OpName %mg_PointSizeCapture"), String::npos)
|
||||||
|
<< "the capture request must force the carrier even though this stage never writes "
|
||||||
|
"the built-in; without it DirectGLES respells the capture to a name no stage "
|
||||||
|
"declares and the whole capture set fails to link:\n"
|
||||||
|
<< gs;
|
||||||
|
const String gsEssl = Transpile(modules[1]);
|
||||||
|
EXPECT_NE(gsEssl.find("mg_PointSizeCapture"), String::npos) << gsEssl;
|
||||||
|
EXPECT_EQ(gsEssl.find("gl_PointSize"), String::npos) << gsEssl;
|
||||||
|
|
||||||
|
// Without the request there is nothing to bind a by-name capture to - which is exactly
|
||||||
|
// what production did on every link while the request never reached this call.
|
||||||
|
Vector<Vector<Uint32>> unrequested = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||||
|
{GL_GEOMETRY_SHADER, readOnlyGeometry},
|
||||||
|
{GL_FRAGMENT_SHADER, echoFragment}});
|
||||||
|
ASSERT_EQ(unrequested.size(), 3u);
|
||||||
|
ShaderCompiler::PointSizeDemotionOutcome unrequestedOutcome;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
unrequested, types, false, true, /*captureRequestsPointSize=*/false, unrequestedOutcome, true,
|
||||||
|
true));
|
||||||
|
EXPECT_TRUE(unrequestedOutcome.demoted) << unrequestedOutcome.declineDetail;
|
||||||
|
EXPECT_EQ(Disassemble(unrequested[1]).find("OpName %mg_PointSizeCapture"), String::npos)
|
||||||
|
<< "with no capture asking for it, the carrier must not be declared";
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE PREMISE THE PASS HEADER USED TO STATE UNIVERSALLY: "declared but no longer accessed"
|
||||||
|
// is invisible to the ES hop. It is not, for one stage/builtin combination - and this case
|
||||||
|
// pins the mechanism with no demotion involved at all, so a future SPIRV-Cross that emitted
|
||||||
|
// by ACCESS would fail here first and the decline below could be relaxed.
|
||||||
|
TEST_F(DemotePointSizeTest, ARedeclaredControlBlockPrintsAnUnaccessedPointSizeMember) {
|
||||||
|
Vector<Vector<Uint32>> modules =
|
||||||
|
CompileProgramToSpirv({{GL_VERTEX_SHADER, kPlainVertexSource},
|
||||||
|
{GL_TESS_CONTROL_SHADER, kClipDistanceUnusedPointSizeTessControlSource},
|
||||||
|
{GL_TESS_EVALUATION_SHADER, kPlainTessEvalSource},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||||
|
ASSERT_EQ(modules.size(), 4u);
|
||||||
|
|
||||||
|
// Nothing in this control stage touches point size, so nothing declares the capability -
|
||||||
|
// it is byte-for-byte the state a demoted module would be left in.
|
||||||
|
ASSERT_FALSE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[1]));
|
||||||
|
const String tcs = Disassemble(modules[1]);
|
||||||
|
EXPECT_NE(tcs.find("BuiltIn PointSize"), String::npos)
|
||||||
|
<< "the member has to still be declared for this case to say anything:\n"
|
||||||
|
<< tcs;
|
||||||
|
|
||||||
|
const String tcsEssl = Transpile(modules[1]);
|
||||||
|
EXPECT_NE(tcsEssl.find("gl_PointSize"), String::npos)
|
||||||
|
<< "SPIRV-Cross force-redeclares a control stage's gl_PerVertex output block when its "
|
||||||
|
"clip/cull distances are live, and prints the block's members from their "
|
||||||
|
"decorations rather than from what is accessed. DirectGLES's extension gate is a "
|
||||||
|
"text search for this token over exactly this string:\n"
|
||||||
|
<< tcsEssl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... and therefore this program declines rather than demoting: a mutated module that the
|
||||||
|
// driver still rejects is strictly worse than the honest refusal, because it also flips the
|
||||||
|
// program-wide verdict and the L1 key.
|
||||||
|
TEST_F(DemotePointSizeTest, AControlStageCarryingClipDistanceDeclinesTheProgram) {
|
||||||
|
Vector<Vector<Uint32>> modules =
|
||||||
|
CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||||
|
{GL_TESS_CONTROL_SHADER, kClipDistanceTessControlSource},
|
||||||
|
{GL_TESS_EVALUATION_SHADER, kTessEvalSource},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||||
|
ASSERT_EQ(modules.size(), 4u);
|
||||||
|
const Vector<Vector<Uint32>> before = modules;
|
||||||
|
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER,
|
||||||
|
GL_FRAGMENT_SHADER};
|
||||||
|
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
modules, types, true, true, true, outcome, true, true));
|
||||||
|
EXPECT_FALSE(outcome.demoted);
|
||||||
|
EXPECT_NE(outcome.declineDetail.find("clip/cull"), String::npos) << outcome.declineDetail;
|
||||||
|
EXPECT_EQ(modules, before) << "a decline must leave every module byte-identical";
|
||||||
|
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[1]))
|
||||||
|
<< "the declined program must still arm the existing honest refusals";
|
||||||
|
}
|
||||||
|
|
||||||
|
// A legal desktop-GL shape the passthrough machinery explicitly serves: an evaluation stage
|
||||||
|
// sitting straight on the vertex stage. Both backends synthesize the missing control stage,
|
||||||
|
// and that synthesized stage forwards gl_Position and nothing else - so the input carrier the
|
||||||
|
// demotion would create has no producer, and each backend's "reads a located input" guard
|
||||||
|
// would decline the program against a varying name the application never wrote. Declining the
|
||||||
|
// demotion instead keeps the modules, and the diagnostics, honest.
|
||||||
|
TEST_F(DemotePointSizeTest, AnEvaluationStageWithNoControlStageDeclines) {
|
||||||
|
const char* readingTessEval = R"(#version 460 core
|
||||||
|
layout(triangles, point_mode) in;
|
||||||
|
void main() {
|
||||||
|
gl_Position = gl_in[0].gl_Position;
|
||||||
|
gl_PointSize = gl_in[0].gl_PointSize + 1.0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
Vector<Vector<Uint32>> modules =
|
||||||
|
CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||||
|
{GL_TESS_EVALUATION_SHADER, readingTessEval},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||||
|
ASSERT_EQ(modules.size(), 3u);
|
||||||
|
const Vector<Vector<Uint32>> before = modules;
|
||||||
|
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_EVALUATION_SHADER, GL_FRAGMENT_SHADER};
|
||||||
|
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
modules, types, true, true, true, outcome, true, true));
|
||||||
|
EXPECT_FALSE(outcome.demoted);
|
||||||
|
EXPECT_NE(outcome.declineDetail.find("control stage"), String::npos) << outcome.declineDetail;
|
||||||
|
EXPECT_EQ(modules, before) << "a decline must leave every module byte-identical";
|
||||||
|
EXPECT_FALSE(ShaderCompiler::ModuleReadsLocatedInput(modules[1]))
|
||||||
|
<< "the declined evaluation stage must not have acquired the located input carrier "
|
||||||
|
"that both backends' pass-through guard refuses";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The carrier is placed one past the highest location any stage CONSUMES, and a 64-bit
|
||||||
|
// vector consumes two of them. GL 4.6 core 11.1.2.1 says so for doubles, and
|
||||||
|
// ARB_gpu_shader_int64 - which DirectVulkan advertises unconditionally - extends the rule
|
||||||
|
// verbatim to i64/u64. An i64vec4 counted as one location would put the carrier on the
|
||||||
|
// SECOND location that varying already owns: two Output variables at one location, an
|
||||||
|
// invalid Vulkan interface and an ES link error naming a variable the application never
|
||||||
|
// wrote. This is the one direction the placement is not allowed to be wrong in.
|
||||||
|
TEST_F(DemotePointSizeTest, TheCarrierClearsA64BitIntegerVectorVarying) {
|
||||||
|
const char* wideVertex = R"(#version 460 core
|
||||||
|
#extension GL_ARB_gpu_shader_int64 : require
|
||||||
|
layout(location = 0) flat out i64vec4 v_wide;
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(1.0);
|
||||||
|
gl_PointSize = 3.0;
|
||||||
|
v_wide = i64vec4(1, 2, 3, 4);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const char* wideGeometry = R"(#version 460 core
|
||||||
|
#extension GL_ARB_gpu_shader_int64 : require
|
||||||
|
layout(points) in;
|
||||||
|
layout(points, max_vertices = 1) out;
|
||||||
|
layout(location = 0) flat in i64vec4 v_wide[];
|
||||||
|
layout(location = 0) flat out i64vec4 g_wide;
|
||||||
|
void main() {
|
||||||
|
gl_Position = gl_in[0].gl_Position;
|
||||||
|
gl_PointSize = gl_in[0].gl_PointSize;
|
||||||
|
g_wide = v_wide[0];
|
||||||
|
EmitVertex();
|
||||||
|
EndPrimitive();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const char* wideFragment = R"(#version 460 core
|
||||||
|
#extension GL_ARB_gpu_shader_int64 : require
|
||||||
|
layout(location = 0) flat in i64vec4 g_wide;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
void main() { fragColor = vec4(float(g_wide.x)); }
|
||||||
|
)";
|
||||||
|
Vector<Vector<Uint32>> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, wideVertex},
|
||||||
|
{GL_GEOMETRY_SHADER, wideGeometry},
|
||||||
|
{GL_FRAGMENT_SHADER, wideFragment}});
|
||||||
|
ASSERT_EQ(modules.size(), 3u);
|
||||||
|
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER};
|
||||||
|
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
modules, types, false, true, true, outcome, true, true));
|
||||||
|
EXPECT_TRUE(outcome.demoted) << outcome.declineDetail;
|
||||||
|
|
||||||
|
// v_wide / g_wide sit at location 0 and occupy 0 AND 1, so every carrier must clear 2.
|
||||||
|
const String vs = Disassemble(modules[0]);
|
||||||
|
EXPECT_NE(vs.find("OpDecorate %mg_PointSizeIo0 Location 2"), String::npos)
|
||||||
|
<< "the carrier landed on a location the i64vec4 varying already owns:\n"
|
||||||
|
<< vs;
|
||||||
|
const String gs = Disassemble(modules[1]);
|
||||||
|
EXPECT_NE(gs.find("OpDecorate %mg_PointSizeIo0 Location 2"), String::npos) << gs;
|
||||||
|
EXPECT_NE(gs.find("OpDecorate %mg_PointSizeCapture Location 2"), String::npos) << gs;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DemotePointSizeTest, AWholeStructCopyDeclinesTheProgramByteIdentically) {
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
Vector<Uint32> module;
|
||||||
|
ASSERT_TRUE(tools.Assemble(kWholeStructCopyTessEvalAsm, &module));
|
||||||
|
ASSERT_TRUE(tools.Validate(module));
|
||||||
|
|
||||||
|
Vector<Vector<Uint32>> modules{module};
|
||||||
|
const Vector<GLenum> types{GL_TESS_EVALUATION_SHADER};
|
||||||
|
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
modules, types, true, true, false, outcome, true, true));
|
||||||
|
EXPECT_FALSE(outcome.demoted);
|
||||||
|
EXPECT_FALSE(outcome.declineDetail.empty())
|
||||||
|
<< "a shape the pass cannot express must say so, not silently no-op";
|
||||||
|
EXPECT_EQ(modules[0], module) << "a decline must not leave a half-demoted module behind";
|
||||||
|
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[0]))
|
||||||
|
<< "the declined module must still arm the existing honest refusals";
|
||||||
|
}
|
||||||
@@ -499,6 +499,19 @@ TEST_F(TranslationCacheTest, L1KeyMovesWithEveryInputThatMovesTheSpirv) {
|
|||||||
v.nativeFloat64 = true;
|
v.nativeFloat64 = true;
|
||||||
variants.emplace_back("nativeFloat64", BuildSpirvTranslationKey(v));
|
variants.emplace_back("nativeFloat64", BuildSpirvTranslationKey(v));
|
||||||
}
|
}
|
||||||
|
{ // CompileEnv::DemotesTessellationPointSize(): phase B rewrites the cached modules
|
||||||
|
// under it (the point-size demotion), so one key shape would describe two module
|
||||||
|
// sets - built-in kept vs carried as a varying with the capability stripped.
|
||||||
|
SpirvTranslationKeyInputs v = base;
|
||||||
|
v.demoteTessellationPointSize = true;
|
||||||
|
variants.emplace_back("demoteTessellationPointSize", BuildSpirvTranslationKey(v));
|
||||||
|
}
|
||||||
|
{ // ... and its geometry twin, keyed separately because the ES loader really does
|
||||||
|
// probe the two extension families independently.
|
||||||
|
SpirvTranslationKeyInputs v = base;
|
||||||
|
v.demoteGeometryPointSize = true;
|
||||||
|
variants.emplace_back("demoteGeometryPointSize", BuildSpirvTranslationKey(v));
|
||||||
|
}
|
||||||
// ---- inputs the WIDENED payload pulled into the key ----
|
// ---- inputs the WIDENED payload pulled into the key ----
|
||||||
// They cannot move a word of the generated SPIR-V, but they do shape the reflection the
|
// They cannot move a word of the generated SPIR-V, but they do shape the reflection the
|
||||||
// payload now carries, so they have to split the key. This is the group that would go
|
// payload now carries, so they have to split the key. This is the group that would go
|
||||||
@@ -651,6 +664,48 @@ TEST_F(TranslationCacheTest, NativeFloat64IsOutOfTheFrontendFingerprintAndInside
|
|||||||
<< "one L1 entry would then describe two different module sets";
|
<< "one L1 entry would then describe two different module sets";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The second and third capability bits under the same placement rule as nativeFloat64:
|
||||||
|
// out of the front-end fingerprint (glslang produces the same thing either way), inside
|
||||||
|
// the L1 key (phase B's point-size demotion rewrites the cached modules under them). The
|
||||||
|
// accessor direction is pinned too, because it is INVERTED relative to the params field
|
||||||
|
// and a swap of the arms would disable the device repair with every rendering test green.
|
||||||
|
TEST_F(TranslationCacheTest, PointSizeDemotionBitsAreOutOfTheFrontendFingerprintAndInsideTheL1Key) {
|
||||||
|
CompileEnv none; // no backend at all: never demote, standalone compiles stay standard
|
||||||
|
CompileEnv hosting; // a backend that hosts the built-in
|
||||||
|
CompileEnv demoting; // a backend that cannot
|
||||||
|
hosting.backend = BackendType::DirectVulkan;
|
||||||
|
demoting.backend = BackendType::DirectVulkan;
|
||||||
|
demoting.params.SupportsTessellationPointSize = false;
|
||||||
|
demoting.params.SupportsGeometryPointSize = false;
|
||||||
|
|
||||||
|
EXPECT_FALSE(none.DemotesTessellationPointSize());
|
||||||
|
EXPECT_FALSE(none.DemotesGeometryPointSize());
|
||||||
|
EXPECT_FALSE(hosting.DemotesTessellationPointSize());
|
||||||
|
EXPECT_FALSE(hosting.DemotesGeometryPointSize());
|
||||||
|
EXPECT_TRUE(demoting.DemotesTessellationPointSize());
|
||||||
|
EXPECT_TRUE(demoting.DemotesGeometryPointSize());
|
||||||
|
|
||||||
|
EXPECT_EQ(ComputeFrontendCompileEnvFingerprint(hosting), ComputeFrontendCompileEnvFingerprint(demoting))
|
||||||
|
<< "the point-size capability leaked into the front-end fingerprint";
|
||||||
|
EXPECT_NE(ComputeCompileEnvFingerprint(hosting), ComputeCompileEnvFingerprint(demoting))
|
||||||
|
<< "the whole-environment fingerprint has to notice it - it is a DynamicBackendParameters "
|
||||||
|
"field, hashed by object representation";
|
||||||
|
|
||||||
|
const Vector<SpirvTranslationKeyInputs::Stage> stages{{GL_VERTEX_SHADER, kVertexSource},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}};
|
||||||
|
SpirvTranslationKeyInputs demotedKey = BaselineSpirvInputs(stages);
|
||||||
|
demotedKey.frontendFingerprint = ComputeFrontendCompileEnvFingerprint(demoting);
|
||||||
|
demotedKey.demoteTessellationPointSize = demoting.DemotesTessellationPointSize();
|
||||||
|
demotedKey.demoteGeometryPointSize = demoting.DemotesGeometryPointSize();
|
||||||
|
SpirvTranslationKeyInputs keptKey = BaselineSpirvInputs(stages);
|
||||||
|
keptKey.frontendFingerprint = ComputeFrontendCompileEnvFingerprint(hosting);
|
||||||
|
keptKey.demoteTessellationPointSize = hosting.DemotesTessellationPointSize();
|
||||||
|
keptKey.demoteGeometryPointSize = hosting.DemotesGeometryPointSize();
|
||||||
|
|
||||||
|
EXPECT_FALSE(BuildSpirvTranslationKey(demotedKey) == BuildSpirvTranslationKey(keptKey))
|
||||||
|
<< "one L1 entry would then describe two different module sets";
|
||||||
|
}
|
||||||
|
|
||||||
// The other direction, one case per input that was KEPT. Each is a limit the front end
|
// The other direction, one case per input that was KEPT. Each is a limit the front end
|
||||||
// really consumes - everything BuildTBuiltInResource copies into TBuiltInResource, plus the
|
// really consumes - everything BuildTBuiltInResource copies into TBuiltInResource, plus the
|
||||||
// two inputs to the reflection vertex-attrib limit - so each must still split the key.
|
// two inputs to the reflection vertex-attrib limit - so each must still split the key.
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ TEST(JobNodeException, AnExceptionEscapingABodyCancelsTheJobInsteadOfTerminating
|
|||||||
EXPECT_TRUE(job->IsCancelled());
|
EXPECT_TRUE(job->IsCancelled());
|
||||||
EXPECT_FALSE(job->IsComplete());
|
EXPECT_FALSE(job->IsComplete());
|
||||||
ASSERT_EQ(job->diagnostics.logLines.size(), 1u);
|
ASSERT_EQ(job->diagnostics.logLines.size(), 1u);
|
||||||
EXPECT_NE(job->diagnostics.logLines[0].find("boom"), String::npos);
|
EXPECT_NE(job->diagnostics.logLines[0].text.find("boom"), String::npos);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(JobNodeException, ANonStandardExceptionIsContainedToo) {
|
TEST(JobNodeException, ANonStandardExceptionIsContainedToo) {
|
||||||
|
|||||||
@@ -95,11 +95,13 @@ namespace MobileGL::MG_Util::Async {
|
|||||||
// which means std::terminate for the whole process. Every job boundary contains
|
// which means std::terminate for the whole process. Every job boundary contains
|
||||||
// it and reports the job as Cancelled; the joining GL thread then sees a node
|
// it and reports the job as Cancelled; the joining GL thread then sees a node
|
||||||
// that produced no result, which is the same shape as an abandoned node.
|
// that produced no result, which is the same shape as an abandoned node.
|
||||||
diagnostics.logLines.push_back(std::format("Job body threw: {}", e.what()));
|
diagnostics.logLines.push_back(
|
||||||
|
{MOBILEGL_LOG_LEVEL_DEBUG, std::format("Job body threw: {}", e.what())});
|
||||||
TryTransition(JobState::Running, JobState::Cancelled);
|
TryTransition(JobState::Running, JobState::Cancelled);
|
||||||
return;
|
return;
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
diagnostics.logLines.emplace_back("Job body threw a non-std exception");
|
diagnostics.logLines.push_back(
|
||||||
|
{MOBILEGL_LOG_LEVEL_DEBUG, String("Job body threw a non-std exception")});
|
||||||
TryTransition(JobState::Running, JobState::Cancelled);
|
TryTransition(JobState::Running, JobState::Cancelled);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -162,10 +164,27 @@ namespace MobileGL::MG_Util::Async {
|
|||||||
"being written");
|
"being written");
|
||||||
|
|
||||||
if (!node.diagnostics.logLines.empty()) {
|
if (!node.diagnostics.logLines.empty()) {
|
||||||
Vector<String> lines;
|
Vector<DeferredLogLine> lines;
|
||||||
lines.swap(node.diagnostics.logLines);
|
lines.swap(node.diagnostics.logLines);
|
||||||
for (const String& line : lines) {
|
for (const DeferredLogLine& line : lines) {
|
||||||
MGLOG_D("%s", line.c_str());
|
// Per-line severity, because a shipped build compiles MGLOG_D away entirely
|
||||||
|
// and a verdict that only this channel records would vanish with it. The
|
||||||
|
// levels are the compile-time constants, so a suppressed one costs nothing
|
||||||
|
// beyond the string the worker already built.
|
||||||
|
switch (line.level) {
|
||||||
|
case MOBILEGL_LOG_LEVEL_INFO:
|
||||||
|
MGLOG_I("%s", line.text.c_str());
|
||||||
|
break;
|
||||||
|
case MOBILEGL_LOG_LEVEL_WARN:
|
||||||
|
MGLOG_W("%s", line.text.c_str());
|
||||||
|
break;
|
||||||
|
case MOBILEGL_LOG_LEVEL_ERROR:
|
||||||
|
MGLOG_E("%s", line.text.c_str());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
MGLOG_D("%s", line.text.c_str());
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
#include <MG_Util/Types.h>
|
#include <MG_Util/Types.h>
|
||||||
|
#include <MG_Util/Debug/Log.h>
|
||||||
#include <MG_State/GLState/ErrorState/ErrorCode.h>
|
#include <MG_State/GLState/ErrorState/ErrorCode.h>
|
||||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||||
|
|
||||||
@@ -35,9 +36,25 @@ namespace MobileGL::MG_Util::Async {
|
|||||||
UniquePtr<ErrorInfo> info;
|
UniquePtr<ErrorInfo> info;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// One line of worker-side MGLOG text, with the severity the join replays it at.
|
||||||
|
//
|
||||||
|
// DEBUG is the default and stays the default: nearly every deferred line is per-program
|
||||||
|
// trace that a shipped build compiles out, which is the whole reason this channel could
|
||||||
|
// be a plain string vector for as long as it was. A line a SHIPPED build has to show -
|
||||||
|
// the reason a repair refused, which no other surface records - has to name its level
|
||||||
|
// here, or it is formatted on the worker and then thrown away at replay under the INFO
|
||||||
|
// level every device and CI build pins. Callers that sit on a repeated path latch at
|
||||||
|
// the SOURCE (a per-call-site atomic, exactly what MGLOG_*_ONCE does): the replay below
|
||||||
|
// is one shared site for every job in the tree, so a latch there would silence
|
||||||
|
// unrelated lines.
|
||||||
|
struct DeferredLogLine {
|
||||||
|
Int level = MOBILEGL_LOG_LEVEL_DEBUG;
|
||||||
|
String text;
|
||||||
|
};
|
||||||
|
|
||||||
struct JobDiagnostics {
|
struct JobDiagnostics {
|
||||||
Vector<DeferredError> errors; // replayed, in ascending `sequence`, by the join
|
Vector<DeferredError> errors; // replayed, in ascending `sequence`, by the join
|
||||||
Vector<String> logLines; // worker-side MGLOG text, flushed in order by the join
|
Vector<DeferredLogLine> logLines; // worker-side MGLOG text, flushed in order by the join
|
||||||
};
|
};
|
||||||
|
|
||||||
// The scheduling primitive every asynchronous compile and link is built on. A node owns
|
// The scheduling primitive every asynchronous compile and link is built on. A node owns
|
||||||
|
|||||||
@@ -222,6 +222,12 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
|
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
|
||||||
caps.SupportsWideLines = supportedFeatures.wideLines == VK_TRUE;
|
caps.SupportsWideLines = supportedFeatures.wideLines == VK_TRUE;
|
||||||
caps.SupportsShaderFloat64 = supportedFeatures.shaderFloat64 == VK_TRUE;
|
caps.SupportsShaderFloat64 = supportedFeatures.shaderFloat64 == VK_TRUE;
|
||||||
|
// One feature covers both stage families here, unlike the ES loader's two extension
|
||||||
|
// tiers; the renderer enables it on the device whenever advertised
|
||||||
|
// (VulkanRenderer::CreateLogicalDeviceAndQueues), so this probe and that enable can
|
||||||
|
// never disagree about the physical device.
|
||||||
|
caps.SupportsTessellationAndGeometryPointSize =
|
||||||
|
supportedFeatures.shaderTessellationAndGeometryPointSize == VK_TRUE;
|
||||||
caps.SupportsImageCubeArray = supportedFeatures.imageCubeArray == VK_TRUE;
|
caps.SupportsImageCubeArray = supportedFeatures.imageCubeArray == VK_TRUE;
|
||||||
{
|
{
|
||||||
// Probe the formats a colour render target actually uses. A driver that refuses the flag
|
// Probe the formats a colour render target actually uses. A driver that refuses the flag
|
||||||
@@ -350,6 +356,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
FillFragmentInterpolationLimits(caps, properties.limits);
|
FillFragmentInterpolationLimits(caps, properties.limits);
|
||||||
caps.SupportsWideLines = false;
|
caps.SupportsWideLines = false;
|
||||||
caps.SupportsShaderFloat64 = false;
|
caps.SupportsShaderFloat64 = false;
|
||||||
|
caps.SupportsTessellationAndGeometryPointSize = false;
|
||||||
caps.SupportsImageCubeArray = false;
|
caps.SupportsImageCubeArray = false;
|
||||||
caps.Supports2DArrayCompatible3DImages = false;
|
caps.Supports2DArrayCompatible3DImages = false;
|
||||||
// This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional
|
// This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional
|
||||||
|
|||||||
@@ -87,6 +87,13 @@ namespace MobileGL {
|
|||||||
// needs it, which includes every 64-bit vertex attribute: the attribute itself arrives
|
// needs it, which includes every 64-bit vertex attribute: the attribute itself arrives
|
||||||
// as 32-bit words, but the bitcast result and everything computed from it is Float64.
|
// as 32-bit words, but the bitcast result and everything computed from it is Float64.
|
||||||
Bool SupportsShaderFloat64 = false;
|
Bool SupportsShaderFloat64 = false;
|
||||||
|
// VkPhysicalDeviceFeatures::shaderTessellationAndGeometryPointSize. Any
|
||||||
|
// tessellation/geometry module declaring OpCapability TessellationPointSize /
|
||||||
|
// GeometryPointSize needs it (VUID-VkShaderModuleCreateInfo-pCode-08740's
|
||||||
|
// capability table); without it the shared phase-B chain demotes the built-in
|
||||||
|
// to an ordinary varying. One feature for both stage families, unlike the ES
|
||||||
|
// loader's two extension tiers.
|
||||||
|
Bool SupportsTessellationAndGeometryPointSize = false;
|
||||||
// VkPhysicalDeviceFeatures::imageCubeArray. Required before a
|
// VkPhysicalDeviceFeatures::imageCubeArray. Required before a
|
||||||
// VK_IMAGE_VIEW_TYPE_CUBE_ARRAY view may be created at all
|
// VK_IMAGE_VIEW_TYPE_CUBE_ARRAY view may be created at all
|
||||||
// (VUID-VkImageViewCreateInfo-viewType-01004), which is every cube map array texture -
|
// (VUID-VkImageViewCreateInfo-viewType-01004), which is every cube map array texture -
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "DriverPost.h"
|
#include "DriverPost.h"
|
||||||
#include "DriverPostIterationRPWitness.h"
|
#include "DriverPostIterationRPWitness.h"
|
||||||
#include "DriverPostIterationRPWitnessSpv.h"
|
#include "DriverPostIterationRPWitnessSpv.h"
|
||||||
|
#include "PrimitivesGeneratedNoXfbProbe.h"
|
||||||
#include "MG_Util/BackendLoaders/OpenGL/Loader.h"
|
#include "MG_Util/BackendLoaders/OpenGL/Loader.h"
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <MGGitHash.h>
|
#include <MGGitHash.h>
|
||||||
@@ -1555,6 +1556,377 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
disabledNote);
|
disabledNote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL_PRIMITIVES_GENERATED for draws made with transform feedback INACTIVE. GL
|
||||||
|
// defines the query to count them; the DirectVulkan backend serves it from the
|
||||||
|
// stream query's primitivesNeeded, and an affected Mali driver answers 0 there
|
||||||
|
// unless a capture span is open - the exact shape the CTS uses to measure the
|
||||||
|
// tessellator (see PrimitivesGeneratedNoXfbProbe.h). One row:
|
||||||
|
// PASS - the stream query counts the capture-less draw exactly.
|
||||||
|
// WARN - it answers 0, and the CLIPPING_INPUT_PRIMITIVES statistics control
|
||||||
|
// on the same draw answers exactly right, so the renderer substitutes
|
||||||
|
// a pipeline-statistics pool for such draws (the same probe, run at
|
||||||
|
// renderer bring-up, is what arms it).
|
||||||
|
// FAIL (optional) - it answers 0 with no working substitute, or the probe
|
||||||
|
// could not reach a verdict; applications sizing capture buffers from
|
||||||
|
// the query get 0.
|
||||||
|
// Throwaway device on purpose, like every probe here: the row reports the
|
||||||
|
// driver, not the renderer's configuration - MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE
|
||||||
|
// steers the renderer, never this row.
|
||||||
|
void ProbeVulkanPrimitivesGeneratedNoXfb(ReportBuilder& builder,
|
||||||
|
PFN_vkGetInstanceProcAddr getInstanceProcAddr,
|
||||||
|
VkInstance instance, VkPhysicalDevice physicalDevice,
|
||||||
|
Uint32 graphicsQueueFamilyIndex,
|
||||||
|
const Vector<VkExtensionProperties>& deviceExtensions,
|
||||||
|
const VkPhysicalDeviceFeatures& features,
|
||||||
|
PFN_vkGetPhysicalDeviceFeatures2 getFeatures2,
|
||||||
|
PFN_vkGetPhysicalDeviceProperties2 getProperties2) {
|
||||||
|
constexpr const char* RowName = "Primitives-generated query without capture";
|
||||||
|
const auto fail = [&](String detail) { builder.FailOptional(RowName, Move(detail)); };
|
||||||
|
|
||||||
|
if (!HasVkExtension(deviceExtensions, VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME) ||
|
||||||
|
getFeatures2 == nullptr || getProperties2 == nullptr) {
|
||||||
|
fail("VK_EXT_transform_feedback is unavailable, so the backend has no GPU counter for "
|
||||||
|
"GL_PRIMITIVES_GENERATED at all - with or without a capture");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
VkPhysicalDeviceTransformFeedbackFeaturesEXT xfbFeatures{};
|
||||||
|
xfbFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_FEATURES_EXT;
|
||||||
|
VkPhysicalDeviceFeatures2 features2{};
|
||||||
|
features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
|
||||||
|
features2.pNext = &xfbFeatures;
|
||||||
|
getFeatures2(physicalDevice, &features2);
|
||||||
|
VkPhysicalDeviceTransformFeedbackPropertiesEXT xfbProperties{};
|
||||||
|
xfbProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_PROPERTIES_EXT;
|
||||||
|
VkPhysicalDeviceProperties2 properties2{};
|
||||||
|
properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
|
||||||
|
properties2.pNext = &xfbProperties;
|
||||||
|
getProperties2(physicalDevice, &properties2);
|
||||||
|
if (xfbFeatures.transformFeedback != VK_TRUE || xfbProperties.transformFeedbackQueries != VK_TRUE) {
|
||||||
|
fail("the device has VK_EXT_transform_feedback but no usable stream queries "
|
||||||
|
"(transformFeedbackQueries = false); GL_PRIMITIVES_GENERATED and "
|
||||||
|
"GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN queries answer 0");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto vkCreateDeviceFn =
|
||||||
|
reinterpret_cast<PFN_vkCreateDevice>(getInstanceProcAddr(instance, "vkCreateDevice"));
|
||||||
|
const auto vkDestroyDeviceFn =
|
||||||
|
reinterpret_cast<PFN_vkDestroyDevice>(getInstanceProcAddr(instance, "vkDestroyDevice"));
|
||||||
|
const auto vkGetDeviceQueueFn =
|
||||||
|
reinterpret_cast<PFN_vkGetDeviceQueue>(getInstanceProcAddr(instance, "vkGetDeviceQueue"));
|
||||||
|
if (vkCreateDeviceFn == nullptr || vkDestroyDeviceFn == nullptr || vkGetDeviceQueueFn == nullptr) {
|
||||||
|
fail("vkGetInstanceProcAddr could not resolve the device-creation entry points");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Float queuePriority = 1.0f;
|
||||||
|
VkDeviceQueueCreateInfo queueInfo{};
|
||||||
|
queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
|
||||||
|
queueInfo.queueFamilyIndex = graphicsQueueFamilyIndex;
|
||||||
|
queueInfo.queueCount = 1;
|
||||||
|
queueInfo.pQueuePriorities = &queuePriority;
|
||||||
|
|
||||||
|
// Only what the probe itself needs: the transform feedback feature (a
|
||||||
|
// stream-query pool may not be created without it), the two candidate
|
||||||
|
// substitutes' features, and tessellationShader for the PATCHES shape -
|
||||||
|
// each only where the device has it. The dedicated
|
||||||
|
// primitives-generated query is taken with BOTH its bits or not at all,
|
||||||
|
// mirroring the renderer (without the discard bit two of the three
|
||||||
|
// shapes may not run inside it).
|
||||||
|
VkPhysicalDeviceFeatures enabledFeatures{};
|
||||||
|
enabledFeatures.pipelineStatisticsQuery = features.pipelineStatisticsQuery;
|
||||||
|
enabledFeatures.tessellationShader = features.tessellationShader;
|
||||||
|
VkPhysicalDeviceTransformFeedbackFeaturesEXT enabledXfbFeatures{};
|
||||||
|
enabledXfbFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_FEATURES_EXT;
|
||||||
|
enabledXfbFeatures.transformFeedback = VK_TRUE;
|
||||||
|
const char* enabledExtensions[2] = {VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME, nullptr};
|
||||||
|
Uint32 enabledExtensionCount = 1;
|
||||||
|
|
||||||
|
Bool primitivesGeneratedQueryUsable = false;
|
||||||
|
VkPhysicalDevicePrimitivesGeneratedQueryFeaturesEXT enabledPgqFeatures{};
|
||||||
|
enabledPgqFeatures.sType =
|
||||||
|
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVES_GENERATED_QUERY_FEATURES_EXT;
|
||||||
|
if (HasVkExtension(deviceExtensions, VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME)) {
|
||||||
|
VkPhysicalDevicePrimitivesGeneratedQueryFeaturesEXT pgqQuery{};
|
||||||
|
pgqQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVES_GENERATED_QUERY_FEATURES_EXT;
|
||||||
|
VkPhysicalDeviceFeatures2 pgqFeatures2{};
|
||||||
|
pgqFeatures2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
|
||||||
|
pgqFeatures2.pNext = &pgqQuery;
|
||||||
|
getFeatures2(physicalDevice, &pgqFeatures2);
|
||||||
|
if (pgqQuery.primitivesGeneratedQuery == VK_TRUE &&
|
||||||
|
pgqQuery.primitivesGeneratedQueryWithRasterizerDiscard == VK_TRUE) {
|
||||||
|
primitivesGeneratedQueryUsable = true;
|
||||||
|
enabledPgqFeatures.primitivesGeneratedQuery = VK_TRUE;
|
||||||
|
enabledPgqFeatures.primitivesGeneratedQueryWithRasterizerDiscard = VK_TRUE;
|
||||||
|
enabledPgqFeatures.pNext = &enabledXfbFeatures;
|
||||||
|
enabledExtensions[enabledExtensionCount++] =
|
||||||
|
VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VkDeviceCreateInfo deviceInfo{};
|
||||||
|
deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
|
||||||
|
deviceInfo.pNext = primitivesGeneratedQueryUsable
|
||||||
|
? static_cast<const void*>(&enabledPgqFeatures)
|
||||||
|
: static_cast<const void*>(&enabledXfbFeatures);
|
||||||
|
deviceInfo.queueCreateInfoCount = 1;
|
||||||
|
deviceInfo.pQueueCreateInfos = &queueInfo;
|
||||||
|
deviceInfo.enabledExtensionCount = enabledExtensionCount;
|
||||||
|
deviceInfo.ppEnabledExtensionNames = enabledExtensions;
|
||||||
|
deviceInfo.pEnabledFeatures = &enabledFeatures;
|
||||||
|
|
||||||
|
VkDevice device = VK_NULL_HANDLE;
|
||||||
|
const VkResult createResult = vkCreateDeviceFn(physicalDevice, &deviceInfo, nullptr, &device);
|
||||||
|
if (createResult != VK_SUCCESS || device == VK_NULL_HANDLE) {
|
||||||
|
fail(format("vkCreateDevice failed (VkResult = {})", static_cast<Int>(createResult)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The probe's own teardown destroys (and idle-waits) everything it created -
|
||||||
|
// EXCEPT when its bounded fence wait expires, where it deliberately leaks
|
||||||
|
// every child object rather than touch a possibly hung GPU. This device must
|
||||||
|
// then leak with them: vkDestroyDevice requires its children destroyed and its
|
||||||
|
// queues idle, and on the driver that just missed a 5 s deadline the realistic
|
||||||
|
// outcome is a block inside vkDestroyDevice - the POST hang the bound exists to
|
||||||
|
// prevent. Same shape as the timestamp probe's guard above and the iterationRP
|
||||||
|
// witness's below.
|
||||||
|
Bool probeFenceWaitTimedOut = false;
|
||||||
|
const ScopeGuard destroyDevice([&]() {
|
||||||
|
if (probeFenceWaitTimedOut) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vkDestroyDeviceFn(device, nullptr);
|
||||||
|
});
|
||||||
|
|
||||||
|
VkQueue queue = VK_NULL_HANDLE;
|
||||||
|
vkGetDeviceQueueFn(device, graphicsQueueFamilyIndex, 0, &queue);
|
||||||
|
if (queue == VK_NULL_HANDLE) {
|
||||||
|
fail("vkGetDeviceQueue returned a null graphics queue");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimitivesGeneratedNoXfbProbeContext probeContext;
|
||||||
|
probeContext.device = device;
|
||||||
|
probeContext.queue = queue;
|
||||||
|
probeContext.queueFamilyIndex = graphicsQueueFamilyIndex;
|
||||||
|
probeContext.transformFeedbackQueriesUsable = true;
|
||||||
|
probeContext.primitivesGeneratedQueryUsable = primitivesGeneratedQueryUsable;
|
||||||
|
probeContext.pipelineStatisticsEnabled = enabledFeatures.pipelineStatisticsQuery == VK_TRUE;
|
||||||
|
probeContext.tessellationEnabled = enabledFeatures.tessellationShader == VK_TRUE;
|
||||||
|
auto& fns = probeContext.fns;
|
||||||
|
const auto resolve = [&](const char* name) { return getInstanceProcAddr(instance, name); };
|
||||||
|
fns.vkCreateCommandPool = reinterpret_cast<PFN_vkCreateCommandPool>(resolve("vkCreateCommandPool"));
|
||||||
|
fns.vkDestroyCommandPool =
|
||||||
|
reinterpret_cast<PFN_vkDestroyCommandPool>(resolve("vkDestroyCommandPool"));
|
||||||
|
fns.vkAllocateCommandBuffers =
|
||||||
|
reinterpret_cast<PFN_vkAllocateCommandBuffers>(resolve("vkAllocateCommandBuffers"));
|
||||||
|
fns.vkBeginCommandBuffer =
|
||||||
|
reinterpret_cast<PFN_vkBeginCommandBuffer>(resolve("vkBeginCommandBuffer"));
|
||||||
|
fns.vkEndCommandBuffer = reinterpret_cast<PFN_vkEndCommandBuffer>(resolve("vkEndCommandBuffer"));
|
||||||
|
fns.vkCreateQueryPool = reinterpret_cast<PFN_vkCreateQueryPool>(resolve("vkCreateQueryPool"));
|
||||||
|
fns.vkDestroyQueryPool = reinterpret_cast<PFN_vkDestroyQueryPool>(resolve("vkDestroyQueryPool"));
|
||||||
|
fns.vkCmdResetQueryPool = reinterpret_cast<PFN_vkCmdResetQueryPool>(resolve("vkCmdResetQueryPool"));
|
||||||
|
fns.vkCmdBeginQuery = reinterpret_cast<PFN_vkCmdBeginQuery>(resolve("vkCmdBeginQuery"));
|
||||||
|
fns.vkCmdEndQuery = reinterpret_cast<PFN_vkCmdEndQuery>(resolve("vkCmdEndQuery"));
|
||||||
|
fns.vkCmdBeginQueryIndexedEXT =
|
||||||
|
reinterpret_cast<PFN_vkCmdBeginQueryIndexedEXT>(resolve("vkCmdBeginQueryIndexedEXT"));
|
||||||
|
fns.vkCmdEndQueryIndexedEXT =
|
||||||
|
reinterpret_cast<PFN_vkCmdEndQueryIndexedEXT>(resolve("vkCmdEndQueryIndexedEXT"));
|
||||||
|
fns.vkCreateRenderPass = reinterpret_cast<PFN_vkCreateRenderPass>(resolve("vkCreateRenderPass"));
|
||||||
|
fns.vkDestroyRenderPass =
|
||||||
|
reinterpret_cast<PFN_vkDestroyRenderPass>(resolve("vkDestroyRenderPass"));
|
||||||
|
fns.vkCreateFramebuffer =
|
||||||
|
reinterpret_cast<PFN_vkCreateFramebuffer>(resolve("vkCreateFramebuffer"));
|
||||||
|
fns.vkDestroyFramebuffer =
|
||||||
|
reinterpret_cast<PFN_vkDestroyFramebuffer>(resolve("vkDestroyFramebuffer"));
|
||||||
|
fns.vkCmdBeginRenderPass =
|
||||||
|
reinterpret_cast<PFN_vkCmdBeginRenderPass>(resolve("vkCmdBeginRenderPass"));
|
||||||
|
fns.vkCmdEndRenderPass = reinterpret_cast<PFN_vkCmdEndRenderPass>(resolve("vkCmdEndRenderPass"));
|
||||||
|
fns.vkCreateShaderModule =
|
||||||
|
reinterpret_cast<PFN_vkCreateShaderModule>(resolve("vkCreateShaderModule"));
|
||||||
|
fns.vkDestroyShaderModule =
|
||||||
|
reinterpret_cast<PFN_vkDestroyShaderModule>(resolve("vkDestroyShaderModule"));
|
||||||
|
fns.vkCreatePipelineLayout =
|
||||||
|
reinterpret_cast<PFN_vkCreatePipelineLayout>(resolve("vkCreatePipelineLayout"));
|
||||||
|
fns.vkDestroyPipelineLayout =
|
||||||
|
reinterpret_cast<PFN_vkDestroyPipelineLayout>(resolve("vkDestroyPipelineLayout"));
|
||||||
|
fns.vkCreateGraphicsPipelines =
|
||||||
|
reinterpret_cast<PFN_vkCreateGraphicsPipelines>(resolve("vkCreateGraphicsPipelines"));
|
||||||
|
fns.vkDestroyPipeline = reinterpret_cast<PFN_vkDestroyPipeline>(resolve("vkDestroyPipeline"));
|
||||||
|
fns.vkCmdBindPipeline = reinterpret_cast<PFN_vkCmdBindPipeline>(resolve("vkCmdBindPipeline"));
|
||||||
|
fns.vkCmdDraw = reinterpret_cast<PFN_vkCmdDraw>(resolve("vkCmdDraw"));
|
||||||
|
fns.vkCreateFence = reinterpret_cast<PFN_vkCreateFence>(resolve("vkCreateFence"));
|
||||||
|
fns.vkDestroyFence = reinterpret_cast<PFN_vkDestroyFence>(resolve("vkDestroyFence"));
|
||||||
|
fns.vkQueueSubmit = reinterpret_cast<PFN_vkQueueSubmit>(resolve("vkQueueSubmit"));
|
||||||
|
fns.vkWaitForFences = reinterpret_cast<PFN_vkWaitForFences>(resolve("vkWaitForFences"));
|
||||||
|
fns.vkGetQueryPoolResults =
|
||||||
|
reinterpret_cast<PFN_vkGetQueryPoolResults>(resolve("vkGetQueryPoolResults"));
|
||||||
|
fns.vkDeviceWaitIdle = reinterpret_cast<PFN_vkDeviceWaitIdle>(resolve("vkDeviceWaitIdle"));
|
||||||
|
|
||||||
|
const PrimitivesGeneratedNoXfbMeasurement measurement =
|
||||||
|
RunPrimitivesGeneratedNoXfbProbe(probeContext);
|
||||||
|
// Before any return below: the guard above owns the device and must know.
|
||||||
|
probeFenceWaitTimedOut = measurement.fenceWaitTimedOut;
|
||||||
|
if (!measurement.ran) {
|
||||||
|
fail(format("the probe could not run ({}); the renderer's bring-up probe decides the "
|
||||||
|
"reroute independently",
|
||||||
|
measurement.failureReason));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto shapeFacts = [](const char* name,
|
||||||
|
const PrimitivesGeneratedNoXfbShapeMeasurement& shape) {
|
||||||
|
if (!shape.drawn) {
|
||||||
|
return format("{} not drawn (no tessellationShader)", name);
|
||||||
|
}
|
||||||
|
String facts = format("{}: stream answered {} of {} expected", name, shape.streamGenerated,
|
||||||
|
shape.expectedPrimitives);
|
||||||
|
if (shape.primitivesGeneratedExtMeasured) {
|
||||||
|
facts += format(", dedicated query answered {}", shape.primitivesGeneratedExt);
|
||||||
|
}
|
||||||
|
if (shape.statisticsMeasured) {
|
||||||
|
facts += format(", statistics control answered {}", shape.statisticsClippingInput);
|
||||||
|
}
|
||||||
|
if (!shape.primitivesGeneratedExtMeasured && !shape.statisticsMeasured) {
|
||||||
|
facts += ", no control (neither VK_EXT_primitives_generated_query with its "
|
||||||
|
"discard feature nor pipelineStatisticsQuery is available)";
|
||||||
|
}
|
||||||
|
return facts;
|
||||||
|
};
|
||||||
|
const String facts = shapeFacts("triangles", measurement.trianglesPlain) + "; " +
|
||||||
|
shapeFacts("triangles under discard", measurement.trianglesDiscard) +
|
||||||
|
"; " + shapeFacts("patches under discard", measurement.patchesDiscard);
|
||||||
|
|
||||||
|
const auto statisticsExactOn = [](const PrimitivesGeneratedNoXfbShapeMeasurement& shape) {
|
||||||
|
return shape.statisticsMeasured && shape.statisticsClippingInput == shape.expectedPrimitives;
|
||||||
|
};
|
||||||
|
// What the PLAIN-ONLY verdict actually measured, named from the numbers rather
|
||||||
|
// than assumed: the shape the substitute misses may be the tessellated one
|
||||||
|
// alone, and a missed shape may read a wrong NONZERO count rather than 0. A
|
||||||
|
// row that always blamed rasterizer discard would put a false statement about
|
||||||
|
// the driver into the campaign's evidence artifact, contradicted by the facts
|
||||||
|
// string printed right after it.
|
||||||
|
const auto describeMissedStatisticsShapes = [&]() {
|
||||||
|
String missed;
|
||||||
|
const auto note = [&](const char* name,
|
||||||
|
const PrimitivesGeneratedNoXfbShapeMeasurement& shape) {
|
||||||
|
if (!shape.drawn || statisticsExactOn(shape)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!missed.empty()) {
|
||||||
|
missed += " and ";
|
||||||
|
}
|
||||||
|
missed += name;
|
||||||
|
missed += shape.statisticsMeasured
|
||||||
|
? format(" (read {} of {} expected)", shape.statisticsClippingInput,
|
||||||
|
shape.expectedPrimitives)
|
||||||
|
: String(" (its statistics slot did not read back)");
|
||||||
|
};
|
||||||
|
note("the plain draw", measurement.trianglesPlain);
|
||||||
|
note("triangles under rasterizer discard", measurement.trianglesDiscard);
|
||||||
|
note("patches under rasterizer discard", measurement.patchesDiscard);
|
||||||
|
return missed;
|
||||||
|
};
|
||||||
|
// The CTS's tessellator-measuring shape is a PATCHES draw under discard; say
|
||||||
|
// whether THIS driver's substitute covers it instead of assuming it does not.
|
||||||
|
const auto describeCtsShape = [&]() -> String {
|
||||||
|
if (!measurement.patchesDiscard.drawn) {
|
||||||
|
return "the CTS's tessellator-measuring shape (a PATCHES draw under discard) could "
|
||||||
|
"not be measured here - this device has no tessellationShader - so whether "
|
||||||
|
"the substitute covers it is unknown";
|
||||||
|
}
|
||||||
|
return statisticsExactOn(measurement.patchesDiscard)
|
||||||
|
? "the CTS's tessellator-measuring shape (a PATCHES draw under discard) is "
|
||||||
|
"NOT among them: the substitute answers it exactly, so those tests are "
|
||||||
|
"repaired"
|
||||||
|
: "the CTS's tessellator-measuring shape (a PATCHES draw under discard) is "
|
||||||
|
"among them, so those tests stay broken on this driver";
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (EvaluatePrimitivesGeneratedNoXfbVerdict(measurement)) {
|
||||||
|
case PrimitivesGeneratedNoXfbVerdict::StreamCounts:
|
||||||
|
builder.Pass(RowName,
|
||||||
|
"the stream query counts a draw made with no capture span open, as "
|
||||||
|
"VK_EXT_transform_feedback defines (" +
|
||||||
|
facts + ")");
|
||||||
|
return;
|
||||||
|
case PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute:
|
||||||
|
builder.Warn(RowName,
|
||||||
|
"the stream query answers 0 for a draw made with no capture span open - "
|
||||||
|
"the shape the CTS measures the tessellator with - while a "
|
||||||
|
"VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT query around an identical replay answers "
|
||||||
|
"exactly right, rasterizer discard included, so the renderer "
|
||||||
|
"accumulates GL_PRIMITIVES_GENERATED for such draws through the "
|
||||||
|
"dedicated query instead (one extra query slot per XFB-inactive draw "
|
||||||
|
"inside a GENERATED span; " +
|
||||||
|
facts + ")");
|
||||||
|
return;
|
||||||
|
case PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute:
|
||||||
|
builder.Warn(RowName,
|
||||||
|
"the stream query answers 0 for a draw made with no capture span open - "
|
||||||
|
"the shape the CTS measures the tessellator with - while a "
|
||||||
|
"clipping-invocations statistics query around an identical replay answers exactly "
|
||||||
|
"right, rasterizer discard included, so the renderer accumulates "
|
||||||
|
"GL_PRIMITIVES_GENERATED for such draws through a pipeline-statistics "
|
||||||
|
"pool instead (one extra query slot per XFB-inactive draw inside a "
|
||||||
|
"GENERATED span; " +
|
||||||
|
facts + ")");
|
||||||
|
return;
|
||||||
|
case PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly:
|
||||||
|
fail("the stream query answers 0 for a draw made with no capture span open, and the "
|
||||||
|
"clipping-invocations statistics substitute counts the plain draw exactly but "
|
||||||
|
"misses " +
|
||||||
|
describeMissedStatisticsShapes() +
|
||||||
|
" - each of them a shape the stream query answered 0 for as well, so the renderer "
|
||||||
|
"reroutes XFB-inactive draws (repairing every shape the substitute answers, at no "
|
||||||
|
"cost to the rest, which is what the verdict requires); " +
|
||||||
|
describeCtsShape() + " (" + facts + ")");
|
||||||
|
return;
|
||||||
|
case PrimitivesGeneratedNoXfbVerdict::Unfixable: {
|
||||||
|
// Two ways to land here, and the report must not conflate them: no
|
||||||
|
// substitute answers even the plain draw, or one does but it is WRONG on a
|
||||||
|
// shape the stream query answers EXACTLY - arming it would trade a correct
|
||||||
|
// answer for a wrong one, so MobileGL refuses (see the verdict's
|
||||||
|
// domination rule).
|
||||||
|
String downgradeShapes;
|
||||||
|
const auto noteDowngrade = [&](const char* name,
|
||||||
|
const PrimitivesGeneratedNoXfbShapeMeasurement& shape) {
|
||||||
|
if (!shape.drawn || statisticsExactOn(shape) ||
|
||||||
|
shape.streamGenerated != shape.expectedPrimitives) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!downgradeShapes.empty()) {
|
||||||
|
downgradeShapes += " and ";
|
||||||
|
}
|
||||||
|
downgradeShapes += name;
|
||||||
|
};
|
||||||
|
noteDowngrade("the plain draw", measurement.trianglesPlain);
|
||||||
|
noteDowngrade("triangles under rasterizer discard", measurement.trianglesDiscard);
|
||||||
|
noteDowngrade("patches under rasterizer discard", measurement.patchesDiscard);
|
||||||
|
if (statisticsExactOn(measurement.trianglesPlain) && !downgradeShapes.empty()) {
|
||||||
|
fail("the stream query answers 0 for a draw made with no capture span open, and the "
|
||||||
|
"clipping-invocations statistics substitute repairs the plain draw but is wrong "
|
||||||
|
"on " +
|
||||||
|
downgradeShapes +
|
||||||
|
", which the stream query answers exactly - rerouting every XFB-inactive draw "
|
||||||
|
"would trade a correct count for a wrong one, so MobileGL arms nothing and the "
|
||||||
|
"capture-less query keeps the driver's answers (" +
|
||||||
|
facts + ")");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fail("the stream query answers 0 for a draw made with no capture span open and the "
|
||||||
|
"device offers no working statistics substitute; an application sizing a capture "
|
||||||
|
"buffer from GL_PRIMITIVES_GENERATED gets 0 (" +
|
||||||
|
facts + ")");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case PrimitivesGeneratedNoXfbVerdict::Inconclusive:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
fail("the probe reached no verdict - the answers fit neither the defect nor health, and "
|
||||||
|
"MobileGL declines to repair a driver it does not understand (" +
|
||||||
|
facts + ")");
|
||||||
|
}
|
||||||
|
|
||||||
// Native iterationRP compute witness. This deliberately uses a separate
|
// Native iterationRP compute witness. This deliberately uses a separate
|
||||||
// throwaway Vulkan device rather than the real renderer's queues, and it
|
// throwaway Vulkan device rather than the real renderer's queues, and it
|
||||||
// treats MOBILEGL_MAGMA_DISABLE_SUBGROUP as irrelevant: the row reports what the
|
// treats MOBILEGL_MAGMA_DISABLE_SUBGROUP as irrelevant: the row reports what the
|
||||||
@@ -2652,6 +3024,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
ProbeVulkanIterationRPWitness(builder, getInstanceProcAddr, instance, physicalDevice, computeQueueFamilyIndex,
|
ProbeVulkanIterationRPWitness(builder, getInstanceProcAddr, instance, physicalDevice, computeQueueFamilyIndex,
|
||||||
properties, subgroupPropertiesAvailable, subgroupProperties);
|
properties, subgroupPropertiesAvailable, subgroupProperties);
|
||||||
|
|
||||||
|
ProbeVulkanPrimitivesGeneratedNoXfb(builder, getInstanceProcAddr, instance, physicalDevice,
|
||||||
|
graphicsQueueFamilyIndex, deviceExtensions, features,
|
||||||
|
vkGetPhysicalDeviceFeatures2Fn, vkGetPhysicalDeviceProperties2Fn);
|
||||||
|
|
||||||
if (HasVkExtension(deviceExtensions, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME)) {
|
if (HasVkExtension(deviceExtensions, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME)) {
|
||||||
builder.Pass("VK_KHR_draw_indirect_count",
|
builder.Pass("VK_KHR_draw_indirect_count",
|
||||||
"supported (count-buffer indirect draws run as single native "
|
"supported (count-buffer indirect draws run as single native "
|
||||||
|
|||||||
@@ -0,0 +1,556 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.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
|
||||||
|
//
|
||||||
|
// See the header for what is being measured and why. The plumbing here is shaped
|
||||||
|
// like the POST timestamp probe (DriverPost.cpp, ProbeVulkanTimerQuery): one
|
||||||
|
// throwaway command buffer, a bounded fence wait that deliberately leaks the
|
||||||
|
// device objects rather than idle-wait a hung GPU, and teardown on every path.
|
||||||
|
|
||||||
|
#include "PrimitivesGeneratedNoXfbProbe.h"
|
||||||
|
#include "PrimitivesGeneratedNoXfbProbeSpv.h"
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Util::SelfTest {
|
||||||
|
namespace {
|
||||||
|
template <typename Callable>
|
||||||
|
struct ProbeScopeGuard {
|
||||||
|
explicit ProbeScopeGuard(Callable callable) : onExit(Move(callable)) {}
|
||||||
|
ProbeScopeGuard(const ProbeScopeGuard&) = delete;
|
||||||
|
ProbeScopeGuard& operator=(const ProbeScopeGuard&) = delete;
|
||||||
|
~ProbeScopeGuard() { onExit(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
Callable onExit;
|
||||||
|
};
|
||||||
|
|
||||||
|
Bool AllRequiredFnsPresent(const PrimitivesGeneratedNoXfbProbeFns& fns) {
|
||||||
|
return fns.vkCreateCommandPool != nullptr && fns.vkDestroyCommandPool != nullptr &&
|
||||||
|
fns.vkAllocateCommandBuffers != nullptr && fns.vkBeginCommandBuffer != nullptr &&
|
||||||
|
fns.vkEndCommandBuffer != nullptr && fns.vkCreateQueryPool != nullptr &&
|
||||||
|
fns.vkDestroyQueryPool != nullptr && fns.vkCmdResetQueryPool != nullptr &&
|
||||||
|
fns.vkCmdBeginQuery != nullptr && fns.vkCmdEndQuery != nullptr &&
|
||||||
|
fns.vkCmdBeginQueryIndexedEXT != nullptr && fns.vkCmdEndQueryIndexedEXT != nullptr &&
|
||||||
|
fns.vkCreateRenderPass != nullptr && fns.vkDestroyRenderPass != nullptr &&
|
||||||
|
fns.vkCreateFramebuffer != nullptr && fns.vkDestroyFramebuffer != nullptr &&
|
||||||
|
fns.vkCmdBeginRenderPass != nullptr && fns.vkCmdEndRenderPass != nullptr &&
|
||||||
|
fns.vkCreateShaderModule != nullptr && fns.vkDestroyShaderModule != nullptr &&
|
||||||
|
fns.vkCreatePipelineLayout != nullptr && fns.vkDestroyPipelineLayout != nullptr &&
|
||||||
|
fns.vkCreateGraphicsPipelines != nullptr && fns.vkDestroyPipeline != nullptr &&
|
||||||
|
fns.vkCmdBindPipeline != nullptr && fns.vkCmdDraw != nullptr &&
|
||||||
|
fns.vkCreateFence != nullptr && fns.vkDestroyFence != nullptr &&
|
||||||
|
fns.vkQueueSubmit != nullptr && fns.vkWaitForFences != nullptr &&
|
||||||
|
fns.vkGetQueryPoolResults != nullptr && fns.vkDeviceWaitIdle != nullptr;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
PrimitivesGeneratedNoXfbMeasurement RunPrimitivesGeneratedNoXfbProbe(
|
||||||
|
const PrimitivesGeneratedNoXfbProbeContext& context) {
|
||||||
|
PrimitivesGeneratedNoXfbMeasurement measurement;
|
||||||
|
const auto fail = [&](String reason) {
|
||||||
|
measurement.ran = false;
|
||||||
|
measurement.failureReason = Move(reason);
|
||||||
|
return measurement;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!context.transformFeedbackQueriesUsable) {
|
||||||
|
return fail("transform feedback stream queries are not usable on this device, so the "
|
||||||
|
"probe has no subject");
|
||||||
|
}
|
||||||
|
if (context.device == VK_NULL_HANDLE || context.queue == VK_NULL_HANDLE) {
|
||||||
|
return fail("no device/queue was supplied");
|
||||||
|
}
|
||||||
|
const PrimitivesGeneratedNoXfbProbeFns& fns = context.fns;
|
||||||
|
if (!AllRequiredFnsPresent(fns)) {
|
||||||
|
return fail("a required Vulkan entry point was not resolved");
|
||||||
|
}
|
||||||
|
|
||||||
|
VkDevice device = context.device;
|
||||||
|
|
||||||
|
// Slot i of each pool belongs to shape i (0 = triangles plain, 1 = triangles
|
||||||
|
// under discard, 2 = patches under discard). Unused slots are reset either
|
||||||
|
// way; reset needs no feature and an unqueried reset slot is never read.
|
||||||
|
constexpr Uint32 kShapeSlots = 3;
|
||||||
|
const Bool drawPatches = context.tessellationEnabled;
|
||||||
|
const Bool measureStatistics = context.pipelineStatisticsEnabled;
|
||||||
|
// Only with BOTH feature bits: without ...WithRasterizerDiscard, a
|
||||||
|
// discarding draw inside the query is invalid usage
|
||||||
|
// (VUID-vkCmdDraw-primitivesGeneratedQueryWithRasterizerDiscard-06708),
|
||||||
|
// and two of the three shapes discard.
|
||||||
|
const Bool measurePrimitivesGeneratedExt = context.primitivesGeneratedQueryUsable;
|
||||||
|
|
||||||
|
VkCommandPool commandPool = VK_NULL_HANDLE;
|
||||||
|
VkQueryPool streamQueryPool = VK_NULL_HANDLE;
|
||||||
|
VkQueryPool primitivesGeneratedQueryPool = VK_NULL_HANDLE;
|
||||||
|
VkQueryPool statisticsQueryPool = VK_NULL_HANDLE;
|
||||||
|
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||||
|
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||||
|
VkShaderModule vertModule = VK_NULL_HANDLE;
|
||||||
|
VkShaderModule tescModule = VK_NULL_HANDLE;
|
||||||
|
VkShaderModule teseModule = VK_NULL_HANDLE;
|
||||||
|
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||||
|
VkPipeline trianglePlainPipeline = VK_NULL_HANDLE;
|
||||||
|
VkPipeline triangleDiscardPipeline = VK_NULL_HANDLE;
|
||||||
|
VkPipeline patchDiscardPipeline = VK_NULL_HANDLE;
|
||||||
|
VkFence fence = VK_NULL_HANDLE;
|
||||||
|
|
||||||
|
// Teardown on every path. When the fence wait timed out the submission may
|
||||||
|
// still be executing on a hung GPU: vkDeviceWaitIdle could block forever
|
||||||
|
// and destroying in-flight objects is undefined, so everything is
|
||||||
|
// deliberately leaked - a hung GPU must not hang the caller. The same flag
|
||||||
|
// is returned in the measurement, because a caller that OWNS the device must
|
||||||
|
// make the same choice for it (see the header): destroying a device whose
|
||||||
|
// children are alive and whose queue may still be executing is the very hang
|
||||||
|
// this bound exists to prevent.
|
||||||
|
const ProbeScopeGuard teardown([&]() {
|
||||||
|
if (measurement.fenceWaitTimedOut) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fns.vkDeviceWaitIdle(device);
|
||||||
|
if (fence != VK_NULL_HANDLE) fns.vkDestroyFence(device, fence, nullptr);
|
||||||
|
if (trianglePlainPipeline != VK_NULL_HANDLE)
|
||||||
|
fns.vkDestroyPipeline(device, trianglePlainPipeline, nullptr);
|
||||||
|
if (triangleDiscardPipeline != VK_NULL_HANDLE)
|
||||||
|
fns.vkDestroyPipeline(device, triangleDiscardPipeline, nullptr);
|
||||||
|
if (patchDiscardPipeline != VK_NULL_HANDLE)
|
||||||
|
fns.vkDestroyPipeline(device, patchDiscardPipeline, nullptr);
|
||||||
|
if (pipelineLayout != VK_NULL_HANDLE) fns.vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
|
||||||
|
if (vertModule != VK_NULL_HANDLE) fns.vkDestroyShaderModule(device, vertModule, nullptr);
|
||||||
|
if (tescModule != VK_NULL_HANDLE) fns.vkDestroyShaderModule(device, tescModule, nullptr);
|
||||||
|
if (teseModule != VK_NULL_HANDLE) fns.vkDestroyShaderModule(device, teseModule, nullptr);
|
||||||
|
if (framebuffer != VK_NULL_HANDLE) fns.vkDestroyFramebuffer(device, framebuffer, nullptr);
|
||||||
|
if (renderPass != VK_NULL_HANDLE) fns.vkDestroyRenderPass(device, renderPass, nullptr);
|
||||||
|
if (statisticsQueryPool != VK_NULL_HANDLE) fns.vkDestroyQueryPool(device, statisticsQueryPool, nullptr);
|
||||||
|
if (primitivesGeneratedQueryPool != VK_NULL_HANDLE)
|
||||||
|
fns.vkDestroyQueryPool(device, primitivesGeneratedQueryPool, nullptr);
|
||||||
|
if (streamQueryPool != VK_NULL_HANDLE) fns.vkDestroyQueryPool(device, streamQueryPool, nullptr);
|
||||||
|
if (commandPool != VK_NULL_HANDLE) fns.vkDestroyCommandPool(device, commandPool, nullptr);
|
||||||
|
});
|
||||||
|
|
||||||
|
VkCommandPoolCreateInfo poolInfo{};
|
||||||
|
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
|
||||||
|
poolInfo.queueFamilyIndex = context.queueFamilyIndex;
|
||||||
|
if (fns.vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) {
|
||||||
|
return fail("vkCreateCommandPool failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
VkCommandBufferAllocateInfo allocInfo{};
|
||||||
|
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||||
|
allocInfo.commandPool = commandPool;
|
||||||
|
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||||
|
allocInfo.commandBufferCount = 1;
|
||||||
|
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||||
|
if (fns.vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer) != VK_SUCCESS) {
|
||||||
|
return fail("vkAllocateCommandBuffers failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
VkQueryPoolCreateInfo streamPoolInfo{};
|
||||||
|
streamPoolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
|
||||||
|
streamPoolInfo.queryType = VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT;
|
||||||
|
streamPoolInfo.queryCount = kShapeSlots;
|
||||||
|
if (fns.vkCreateQueryPool(device, &streamPoolInfo, nullptr, &streamQueryPool) != VK_SUCCESS) {
|
||||||
|
return fail("vkCreateQueryPool(TRANSFORM_FEEDBACK_STREAM) failed");
|
||||||
|
}
|
||||||
|
if (measurePrimitivesGeneratedExt) {
|
||||||
|
VkQueryPoolCreateInfo pgqPoolInfo{};
|
||||||
|
pgqPoolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
|
||||||
|
pgqPoolInfo.queryType = VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT;
|
||||||
|
pgqPoolInfo.queryCount = kShapeSlots;
|
||||||
|
if (fns.vkCreateQueryPool(device, &pgqPoolInfo, nullptr, &primitivesGeneratedQueryPool) !=
|
||||||
|
VK_SUCCESS) {
|
||||||
|
return fail("vkCreateQueryPool(PRIMITIVES_GENERATED_EXT) failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (measureStatistics) {
|
||||||
|
VkQueryPoolCreateInfo statPoolInfo{};
|
||||||
|
statPoolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
|
||||||
|
statPoolInfo.queryType = VK_QUERY_TYPE_PIPELINE_STATISTICS;
|
||||||
|
statPoolInfo.queryCount = kShapeSlots;
|
||||||
|
// CLIPPING_INVOCATIONS counts the primitives PROCESSED BY (i.e. reaching)
|
||||||
|
// primitive clipping - GL's CLIPPING_INPUT_PRIMITIVES - which is the
|
||||||
|
// pre-clip, post-vertex-processing set PRIMITIVES_GENERATED is defined
|
||||||
|
// over. CLIPPING_PRIMITIVES (the stage's OUTPUT count) would be wrong:
|
||||||
|
// clipping may drop or split primitives.
|
||||||
|
statPoolInfo.pipelineStatistics = VK_QUERY_PIPELINE_STATISTIC_CLIPPING_INVOCATIONS_BIT;
|
||||||
|
if (fns.vkCreateQueryPool(device, &statPoolInfo, nullptr, &statisticsQueryPool) != VK_SUCCESS) {
|
||||||
|
return fail("vkCreateQueryPool(PIPELINE_STATISTICS) failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero-attachment render pass + 1x1 framebuffer: the draw is discarded
|
||||||
|
// before rasterization, nothing is ever written, but vkCmdDraw needs a
|
||||||
|
// render pass instance to live in.
|
||||||
|
VkSubpassDescription subpass{};
|
||||||
|
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||||
|
VkRenderPassCreateInfo renderPassInfo{};
|
||||||
|
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
|
||||||
|
renderPassInfo.subpassCount = 1;
|
||||||
|
renderPassInfo.pSubpasses = &subpass;
|
||||||
|
if (fns.vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) {
|
||||||
|
return fail("vkCreateRenderPass failed");
|
||||||
|
}
|
||||||
|
VkFramebufferCreateInfo framebufferInfo{};
|
||||||
|
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
|
||||||
|
framebufferInfo.renderPass = renderPass;
|
||||||
|
framebufferInfo.width = 1;
|
||||||
|
framebufferInfo.height = 1;
|
||||||
|
framebufferInfo.layers = 1;
|
||||||
|
if (fns.vkCreateFramebuffer(device, &framebufferInfo, nullptr, &framebuffer) != VK_SUCCESS) {
|
||||||
|
return fail("vkCreateFramebuffer failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto makeModule = [&](const std::uint32_t* words, std::size_t wordCount, VkShaderModule& out) {
|
||||||
|
VkShaderModuleCreateInfo moduleInfo{};
|
||||||
|
moduleInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
|
||||||
|
moduleInfo.codeSize = wordCount * sizeof(std::uint32_t);
|
||||||
|
moduleInfo.pCode = words;
|
||||||
|
return fns.vkCreateShaderModule(device, &moduleInfo, nullptr, &out) == VK_SUCCESS;
|
||||||
|
};
|
||||||
|
if (!makeModule(kPrimitivesGeneratedNoXfbProbeVertSpv, kPrimitivesGeneratedNoXfbProbeVertSpvWordCount,
|
||||||
|
vertModule)) {
|
||||||
|
return fail("vkCreateShaderModule(vert) failed");
|
||||||
|
}
|
||||||
|
if (drawPatches) {
|
||||||
|
if (!makeModule(kPrimitivesGeneratedNoXfbProbeTescSpv, kPrimitivesGeneratedNoXfbProbeTescSpvWordCount,
|
||||||
|
tescModule) ||
|
||||||
|
!makeModule(kPrimitivesGeneratedNoXfbProbeTeseSpv, kPrimitivesGeneratedNoXfbProbeTeseSpvWordCount,
|
||||||
|
teseModule)) {
|
||||||
|
return fail("vkCreateShaderModule(tesc/tese) failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VkPipelineLayoutCreateInfo layoutInfo{};
|
||||||
|
layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
||||||
|
if (fns.vkCreatePipelineLayout(device, &layoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
|
||||||
|
return fail("vkCreatePipelineLayout failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// With rasterizerDiscardEnable the viewport and multisample state are
|
||||||
|
// ignored by the spec, but well-formed ones are supplied anyway: the probe
|
||||||
|
// must never be the thing that trips a picky driver. The discard-off
|
||||||
|
// variant rasterizes into the zero-attachment subpass, which writes
|
||||||
|
// nothing anywhere.
|
||||||
|
const auto makePipeline = [&](Bool tessellated, Bool rasterizerDiscard, VkPipeline& out) {
|
||||||
|
VkPipelineShaderStageCreateInfo stages[3] = {};
|
||||||
|
Uint32 stageCount = 0;
|
||||||
|
stages[stageCount].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||||
|
stages[stageCount].stage = VK_SHADER_STAGE_VERTEX_BIT;
|
||||||
|
stages[stageCount].module = vertModule;
|
||||||
|
stages[stageCount].pName = "main";
|
||||||
|
++stageCount;
|
||||||
|
if (tessellated) {
|
||||||
|
stages[stageCount].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||||
|
stages[stageCount].stage = VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT;
|
||||||
|
stages[stageCount].module = tescModule;
|
||||||
|
stages[stageCount].pName = "main";
|
||||||
|
++stageCount;
|
||||||
|
stages[stageCount].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||||
|
stages[stageCount].stage = VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT;
|
||||||
|
stages[stageCount].module = teseModule;
|
||||||
|
stages[stageCount].pName = "main";
|
||||||
|
++stageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkPipelineVertexInputStateCreateInfo vertexInput{};
|
||||||
|
vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
|
||||||
|
|
||||||
|
VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
|
||||||
|
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
|
||||||
|
inputAssembly.topology =
|
||||||
|
tessellated ? VK_PRIMITIVE_TOPOLOGY_PATCH_LIST : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||||
|
|
||||||
|
VkPipelineTessellationStateCreateInfo tessellation{};
|
||||||
|
tessellation.sType = VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO;
|
||||||
|
tessellation.patchControlPoints = 1;
|
||||||
|
|
||||||
|
VkViewport viewport{};
|
||||||
|
viewport.width = 1.0f;
|
||||||
|
viewport.height = 1.0f;
|
||||||
|
viewport.maxDepth = 1.0f;
|
||||||
|
VkRect2D scissor{};
|
||||||
|
scissor.extent.width = 1;
|
||||||
|
scissor.extent.height = 1;
|
||||||
|
VkPipelineViewportStateCreateInfo viewportState{};
|
||||||
|
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
|
||||||
|
viewportState.viewportCount = 1;
|
||||||
|
viewportState.pViewports = &viewport;
|
||||||
|
viewportState.scissorCount = 1;
|
||||||
|
viewportState.pScissors = &scissor;
|
||||||
|
|
||||||
|
VkPipelineRasterizationStateCreateInfo rasterization{};
|
||||||
|
rasterization.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
|
||||||
|
rasterization.rasterizerDiscardEnable = rasterizerDiscard ? VK_TRUE : VK_FALSE;
|
||||||
|
rasterization.polygonMode = VK_POLYGON_MODE_FILL;
|
||||||
|
rasterization.cullMode = VK_CULL_MODE_NONE;
|
||||||
|
rasterization.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||||
|
rasterization.lineWidth = 1.0f;
|
||||||
|
|
||||||
|
VkPipelineMultisampleStateCreateInfo multisample{};
|
||||||
|
multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
|
||||||
|
multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
|
||||||
|
VkGraphicsPipelineCreateInfo pipelineInfo{};
|
||||||
|
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
|
||||||
|
pipelineInfo.stageCount = stageCount;
|
||||||
|
pipelineInfo.pStages = stages;
|
||||||
|
pipelineInfo.pVertexInputState = &vertexInput;
|
||||||
|
pipelineInfo.pInputAssemblyState = &inputAssembly;
|
||||||
|
pipelineInfo.pTessellationState = tessellated ? &tessellation : nullptr;
|
||||||
|
pipelineInfo.pViewportState = &viewportState;
|
||||||
|
pipelineInfo.pRasterizationState = &rasterization;
|
||||||
|
pipelineInfo.pMultisampleState = &multisample;
|
||||||
|
pipelineInfo.layout = pipelineLayout;
|
||||||
|
pipelineInfo.renderPass = renderPass;
|
||||||
|
pipelineInfo.subpass = 0;
|
||||||
|
return fns.vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &out) ==
|
||||||
|
VK_SUCCESS;
|
||||||
|
};
|
||||||
|
if (!makePipeline(false, false, trianglePlainPipeline)) {
|
||||||
|
return fail("vkCreateGraphicsPipelines(triangles) failed");
|
||||||
|
}
|
||||||
|
if (!makePipeline(false, true, triangleDiscardPipeline)) {
|
||||||
|
return fail("vkCreateGraphicsPipelines(triangles, discard) failed");
|
||||||
|
}
|
||||||
|
if (drawPatches && !makePipeline(true, true, patchDiscardPipeline)) {
|
||||||
|
return fail("vkCreateGraphicsPipelines(patches, discard) failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
VkCommandBufferBeginInfo beginInfo{};
|
||||||
|
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||||
|
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||||
|
if (fns.vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) {
|
||||||
|
return fail("vkBeginCommandBuffer failed");
|
||||||
|
}
|
||||||
|
fns.vkCmdResetQueryPool(commandBuffer, streamQueryPool, 0, kShapeSlots);
|
||||||
|
if (measurePrimitivesGeneratedExt) {
|
||||||
|
fns.vkCmdResetQueryPool(commandBuffer, primitivesGeneratedQueryPool, 0, kShapeSlots);
|
||||||
|
}
|
||||||
|
if (measureStatistics) {
|
||||||
|
fns.vkCmdResetQueryPool(commandBuffer, statisticsQueryPool, 0, kShapeSlots);
|
||||||
|
}
|
||||||
|
|
||||||
|
VkRenderPassBeginInfo renderPassBegin{};
|
||||||
|
renderPassBegin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
||||||
|
renderPassBegin.renderPass = renderPass;
|
||||||
|
renderPassBegin.framebuffer = framebuffer;
|
||||||
|
renderPassBegin.renderArea.extent.width = 1;
|
||||||
|
renderPassBegin.renderArea.extent.height = 1;
|
||||||
|
fns.vkCmdBeginRenderPass(commandBuffer, &renderPassBegin, VK_SUBPASS_CONTENTS_INLINE);
|
||||||
|
|
||||||
|
// Each query kind wraps ITS OWN replay of the shape's draw, never a shared
|
||||||
|
// one. Not pedantry - a co-active control CONTAMINATES the subject:
|
||||||
|
// measured on lavapipe, a dedicated primitives-generated query active
|
||||||
|
// around the same draw switches llvmpipe's primitive collection on, and
|
||||||
|
// the stream query on that draw then answers the exact count it answers 0
|
||||||
|
// for when it is alone - which is how the renderer actually runs it. A
|
||||||
|
// probe that measured them together certified this driver healthy and
|
||||||
|
// repaired nothing. The replays are identical recordings of a
|
||||||
|
// deterministic draw, so the per-shape comparison loses nothing.
|
||||||
|
const auto recordShape = [&](Uint32 slot, VkPipeline pipeline, Uint32 vertexCount) {
|
||||||
|
fns.vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
||||||
|
// THE SUBJECT, alone: no vkCmdBeginTransformFeedbackEXT anywhere in
|
||||||
|
// this command buffer - the stream query wraps a draw with transform
|
||||||
|
// feedback inactive, exactly the CTS's tessellator-measuring shape.
|
||||||
|
fns.vkCmdBeginQueryIndexedEXT(commandBuffer, streamQueryPool, slot, 0, 0);
|
||||||
|
fns.vkCmdDraw(commandBuffer, vertexCount, 1, 0, 0);
|
||||||
|
fns.vkCmdEndQueryIndexedEXT(commandBuffer, streamQueryPool, slot, 0);
|
||||||
|
if (measurePrimitivesGeneratedExt) {
|
||||||
|
// Plain vkCmdBeginQuery: a PRIMITIVES_GENERATED_EXT query begun
|
||||||
|
// this way counts vertex stream 0, which is where every non-GS
|
||||||
|
// (and default-stream GS) primitive goes.
|
||||||
|
fns.vkCmdBeginQuery(commandBuffer, primitivesGeneratedQueryPool, slot, 0);
|
||||||
|
fns.vkCmdDraw(commandBuffer, vertexCount, 1, 0, 0);
|
||||||
|
fns.vkCmdEndQuery(commandBuffer, primitivesGeneratedQueryPool, slot);
|
||||||
|
}
|
||||||
|
if (measureStatistics) {
|
||||||
|
fns.vkCmdBeginQuery(commandBuffer, statisticsQueryPool, slot, 0);
|
||||||
|
fns.vkCmdDraw(commandBuffer, vertexCount, 1, 0, 0);
|
||||||
|
fns.vkCmdEndQuery(commandBuffer, statisticsQueryPool, slot);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
recordShape(0, trianglePlainPipeline, 3); // one rasterized triangle
|
||||||
|
recordShape(1, triangleDiscardPipeline, 3); // one discarded triangle
|
||||||
|
if (drawPatches) {
|
||||||
|
// one 1-vertex patch -> one tessellated, discarded triangle
|
||||||
|
recordShape(2, patchDiscardPipeline, 1);
|
||||||
|
}
|
||||||
|
fns.vkCmdEndRenderPass(commandBuffer);
|
||||||
|
if (fns.vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
|
||||||
|
return fail("vkEndCommandBuffer failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
VkFenceCreateInfo fenceInfo{};
|
||||||
|
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
|
||||||
|
if (fns.vkCreateFence(device, &fenceInfo, nullptr, &fence) != VK_SUCCESS) {
|
||||||
|
return fail("vkCreateFence failed");
|
||||||
|
}
|
||||||
|
VkSubmitInfo submitInfo{};
|
||||||
|
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
||||||
|
submitInfo.commandBufferCount = 1;
|
||||||
|
submitInfo.pCommandBuffers = &commandBuffer;
|
||||||
|
if (fns.vkQueueSubmit(context.queue, 1, &submitInfo, fence) != VK_SUCCESS) {
|
||||||
|
return fail("vkQueueSubmit failed");
|
||||||
|
}
|
||||||
|
constexpr Uint64 kFenceTimeoutNs = 5'000'000'000ull; // a probe must never hang its caller
|
||||||
|
if (fns.vkWaitForFences(device, 1, &fence, VK_TRUE, kFenceTimeoutNs) != VK_SUCCESS) {
|
||||||
|
// Set BEFORE failing: the scope guard reads it to skip every destroy, and
|
||||||
|
// the caller reads it out of the measurement to skip destroying the device.
|
||||||
|
measurement.fenceWaitTimedOut = true;
|
||||||
|
return fail("the probe submission did not complete within 5 s");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto readShape = [&](Uint32 slot, Uint64 expected, PrimitivesGeneratedNoXfbShapeMeasurement& out) {
|
||||||
|
Uint64 streamPair[2] = {0, 0}; // {primitivesWritten, primitivesNeeded}
|
||||||
|
if (fns.vkGetQueryPoolResults(device, streamQueryPool, slot, 1, sizeof(streamPair), streamPair,
|
||||||
|
sizeof(streamPair),
|
||||||
|
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT) != VK_SUCCESS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.drawn = true;
|
||||||
|
out.expectedPrimitives = expected;
|
||||||
|
out.streamGenerated = streamPair[1];
|
||||||
|
if (measurePrimitivesGeneratedExt) {
|
||||||
|
Uint64 generated = 0;
|
||||||
|
if (fns.vkGetQueryPoolResults(device, primitivesGeneratedQueryPool, slot, 1, sizeof(generated),
|
||||||
|
&generated, sizeof(generated),
|
||||||
|
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT) == VK_SUCCESS) {
|
||||||
|
out.primitivesGeneratedExtMeasured = true;
|
||||||
|
out.primitivesGeneratedExt = generated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (measureStatistics) {
|
||||||
|
Uint64 clippingInput = 0;
|
||||||
|
if (fns.vkGetQueryPoolResults(device, statisticsQueryPool, slot, 1, sizeof(clippingInput),
|
||||||
|
&clippingInput, sizeof(clippingInput),
|
||||||
|
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT) == VK_SUCCESS) {
|
||||||
|
out.statisticsMeasured = true;
|
||||||
|
out.statisticsClippingInput = clippingInput;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if (!readShape(0, 1, measurement.trianglesPlain)) {
|
||||||
|
return fail("vkGetQueryPoolResults(triangles) failed");
|
||||||
|
}
|
||||||
|
if (!readShape(1, 1, measurement.trianglesDiscard)) {
|
||||||
|
return fail("vkGetQueryPoolResults(triangles, discard) failed");
|
||||||
|
}
|
||||||
|
if (drawPatches && !readShape(2, 1, measurement.patchesDiscard)) {
|
||||||
|
return fail("vkGetQueryPoolResults(patches, discard) failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
measurement.ran = true;
|
||||||
|
return measurement;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimitivesGeneratedNoXfbVerdict EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||||
|
const PrimitivesGeneratedNoXfbMeasurement& measurement) {
|
||||||
|
if (!measurement.ran || !measurement.trianglesPlain.drawn || !measurement.trianglesDiscard.drawn) {
|
||||||
|
return PrimitivesGeneratedNoXfbVerdict::Inconclusive;
|
||||||
|
}
|
||||||
|
const PrimitivesGeneratedNoXfbShapeMeasurement* shapes[3] = {&measurement.trianglesPlain,
|
||||||
|
&measurement.trianglesDiscard,
|
||||||
|
&measurement.patchesDiscard};
|
||||||
|
Bool anyStreamSilent = false;
|
||||||
|
Bool allStreamExact = true;
|
||||||
|
Bool allPrimitivesGeneratedExtExact = true;
|
||||||
|
Bool allStatisticsExact = true;
|
||||||
|
// Whether the statistics substitute DOMINATES the stream query shape by shape:
|
||||||
|
// every shape the statistics do not answer exactly must be one the stream query
|
||||||
|
// answered 0 for anyway. Without this, a plain-shape-only substitute could be
|
||||||
|
// armed on a device whose stream query was RIGHT on a shape the statistics get
|
||||||
|
// wrong - and the renderer reroutes every XFB-inactive draw, so that shape would
|
||||||
|
// be downgraded from correct to wrong. "Never worse per draw" is what makes
|
||||||
|
// arming on an uncharacterised driver defensible; it has to be measured, not
|
||||||
|
// assumed.
|
||||||
|
Bool statisticsDominateStream = true;
|
||||||
|
for (const auto* shape : shapes) {
|
||||||
|
if (!shape->drawn) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (shape->streamGenerated == 0) {
|
||||||
|
anyStreamSilent = true;
|
||||||
|
}
|
||||||
|
if (shape->streamGenerated != shape->expectedPrimitives) {
|
||||||
|
allStreamExact = false;
|
||||||
|
// A nonzero wrong answer is neither the defect nor health: refuse
|
||||||
|
// a verdict rather than repair a driver the probe does not
|
||||||
|
// understand.
|
||||||
|
if (shape->streamGenerated != 0) {
|
||||||
|
return PrimitivesGeneratedNoXfbVerdict::Inconclusive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!shape->primitivesGeneratedExtMeasured ||
|
||||||
|
shape->primitivesGeneratedExt != shape->expectedPrimitives) {
|
||||||
|
allPrimitivesGeneratedExtExact = false;
|
||||||
|
}
|
||||||
|
if (!shape->statisticsMeasured ||
|
||||||
|
shape->statisticsClippingInput != shape->expectedPrimitives) {
|
||||||
|
allStatisticsExact = false;
|
||||||
|
// Only a shape the stream query was silent on may be left behind by
|
||||||
|
// the substitute; a shape it answered exactly must not be traded away.
|
||||||
|
if (shape->streamGenerated == shape->expectedPrimitives) {
|
||||||
|
statisticsDominateStream = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allStreamExact) {
|
||||||
|
return PrimitivesGeneratedNoXfbVerdict::StreamCounts;
|
||||||
|
}
|
||||||
|
// At this point at least one drawn shape answered exactly 0.
|
||||||
|
MOBILEGL_ASSERT(anyStreamSilent, "verdict fell through with no silent shape");
|
||||||
|
if (allPrimitivesGeneratedExtExact) {
|
||||||
|
return PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute;
|
||||||
|
}
|
||||||
|
if (allStatisticsExact) {
|
||||||
|
return PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute;
|
||||||
|
}
|
||||||
|
const auto& plain = measurement.trianglesPlain;
|
||||||
|
const Bool plainStatisticsExact =
|
||||||
|
plain.statisticsMeasured && plain.statisticsClippingInput == plain.expectedPrimitives;
|
||||||
|
// Both halves are required: the substitute must repair the plain shape, AND it
|
||||||
|
// must not cost any shape an answer the stream query already had right.
|
||||||
|
return (plainStatisticsExact && statisticsDominateStream)
|
||||||
|
? PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly
|
||||||
|
: PrimitivesGeneratedNoXfbVerdict::Unfixable;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimGenRerouteKind ChoosePrimitivesGeneratedReroute(MG_Config::QuirkOverride overrideSetting,
|
||||||
|
PrimitivesGeneratedNoXfbVerdict verdict,
|
||||||
|
Bool primitivesGeneratedQueryUsable,
|
||||||
|
Bool pipelineStatisticsEnabled) {
|
||||||
|
switch (overrideSetting) {
|
||||||
|
case MG_Config::QuirkOverride::ForceOff:
|
||||||
|
return PrimGenRerouteKind::None;
|
||||||
|
case MG_Config::QuirkOverride::ForceOn:
|
||||||
|
// ForceOn bypasses the device verdict, never the structural checks:
|
||||||
|
// without a hostable pool there is nothing to route through. The
|
||||||
|
// dedicated query wins where both exist - its semantics are the GL
|
||||||
|
// target's by definition.
|
||||||
|
if (primitivesGeneratedQueryUsable) {
|
||||||
|
return PrimGenRerouteKind::PrimitivesGeneratedExt;
|
||||||
|
}
|
||||||
|
return pipelineStatisticsEnabled ? PrimGenRerouteKind::ClippingStatistics
|
||||||
|
: PrimGenRerouteKind::None;
|
||||||
|
case MG_Config::QuirkOverride::Auto:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
switch (verdict) {
|
||||||
|
case PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute:
|
||||||
|
return primitivesGeneratedQueryUsable ? PrimGenRerouteKind::PrimitivesGeneratedExt
|
||||||
|
: PrimGenRerouteKind::None;
|
||||||
|
case PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute:
|
||||||
|
case PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly:
|
||||||
|
return pipelineStatisticsEnabled ? PrimGenRerouteKind::ClippingStatistics
|
||||||
|
: PrimGenRerouteKind::None;
|
||||||
|
case PrimitivesGeneratedNoXfbVerdict::Inconclusive:
|
||||||
|
case PrimitivesGeneratedNoXfbVerdict::StreamCounts:
|
||||||
|
case PrimitivesGeneratedNoXfbVerdict::Unfixable:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return PrimGenRerouteKind::None;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Util::SelfTest
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h
|
||||||
|
// 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
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Config.h>
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Util::SelfTest {
|
||||||
|
// ============ PRIMITIVES GENERATED WITHOUT TRANSFORM FEEDBACK ============
|
||||||
|
//
|
||||||
|
// GL_PRIMITIVES_GENERATED counts what the last vertex processing stage emits
|
||||||
|
// whether or not a transform feedback capture is active (GL 4.6 core 13.4), and
|
||||||
|
// the DirectVulkan backend serves it from the second result
|
||||||
|
// (primitivesNeeded) of a VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT pool
|
||||||
|
// slot wrapped around each draw. VK_EXT_transform_feedback defines that value
|
||||||
|
// as the primitives the vertex stream produced, capture or no capture - but a
|
||||||
|
// Mali driver (G1-Ultra, observed against the gl44/gl45/gl46 CTS) answers 0
|
||||||
|
// for every draw made while no vkCmdBeginTransformFeedbackEXT span is open,
|
||||||
|
// while answering exactly right as soon as one is. The tessellation suites
|
||||||
|
// measure the tessellator by exactly that shape (rasterizer discard on,
|
||||||
|
// transform feedback INACTIVE, a PATCHES draw inside a GENERATED query;
|
||||||
|
// esextcTessellationShaderUtils.cpp, captureTessellationData), size their
|
||||||
|
// capture buffers from the answer, and die on the zero-byte buffer the 0
|
||||||
|
// produces - about 29 tessellation tests per tree plus all 13
|
||||||
|
// tessellation_shader.vertex bodies.
|
||||||
|
//
|
||||||
|
// THE PROBE draws three shapes through pipelines with no Xfb execution mode
|
||||||
|
// and no transform feedback begun, each inside its own stream-query slot:
|
||||||
|
// - one triangle, plainly (no rasterizer discard);
|
||||||
|
// - one triangle with rasterizer discard baked into the pipeline;
|
||||||
|
// - one PATCHES draw with discard, through a passthrough tessellation
|
||||||
|
// pipeline whose all-1 levels emit exactly one triangle (when the device
|
||||||
|
// has tessellationShader) - the CTS shape verbatim.
|
||||||
|
// Alongside each stream slot it measures the TWO candidate substitutes, each
|
||||||
|
// around ITS OWN identical replay of the shape's draw - never co-active with
|
||||||
|
// the subject, because a co-active control contaminates it: on lavapipe a
|
||||||
|
// dedicated primitives-generated query active around the same draw switches
|
||||||
|
// the driver's primitive collection on and the stream query then counts a
|
||||||
|
// draw it answers 0 for when alone, which is how the renderer actually runs
|
||||||
|
// it. The substitutes:
|
||||||
|
// - a VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT slot, where the device has
|
||||||
|
// VK_EXT_primitives_generated_query with BOTH primitivesGeneratedQuery and
|
||||||
|
// primitivesGeneratedQueryWithRasterizerDiscard (without the discard
|
||||||
|
// feature the spec forbids the query around a discarding draw at all -
|
||||||
|
// VUID-vkCmdDraw-...-06708 - and GL applications toggle discard freely, so
|
||||||
|
// a base-feature-only device cannot use this tier). The extension exists
|
||||||
|
// precisely because GL needs PRIMITIVES_GENERATED without a capture, so
|
||||||
|
// its semantics are exact by definition - what remains to prove is that
|
||||||
|
// the DRIVER's implementation is not silent in the same way its stream
|
||||||
|
// query is;
|
||||||
|
// - a VK_QUERY_TYPE_PIPELINE_STATISTICS slot counting CLIPPING_INVOCATIONS
|
||||||
|
// (when the device has pipelineStatisticsQuery): one invocation of the
|
||||||
|
// primitive clipping stage per primitive reaching it - GL's
|
||||||
|
// CLIPPING_INPUT_PRIMITIVES - which sits AFTER every vertex processing
|
||||||
|
// stage (post-tess, post-GS) and, per spec, BEFORE rasterizer discard, so
|
||||||
|
// for an XFB-inactive draw it is definitionally the number
|
||||||
|
// PRIMITIVES_GENERATED must answer. (A geometry stage's non-zero vertex
|
||||||
|
// streams never reach clipping, but non-indexed GL_PRIMITIVES_GENERATED
|
||||||
|
// counts stream 0 alone, so the sets still agree. The stage's OUTPUT
|
||||||
|
// count - CLIPPING_PRIMITIVES - would not: clipping drops and splits.)
|
||||||
|
//
|
||||||
|
// THE CONTROL DISCIPLINE (DriverBugProbes.h): the substitute slots are the
|
||||||
|
// probe's controls, and the DISCARD dimension is measured separately because
|
||||||
|
// it is a real fault line, not paranoia: Mesa llvmpipe short-circuits its
|
||||||
|
// clipping statistics under rasterizer discard (reading 0 there while counting
|
||||||
|
// the identical undiscarded draw exactly) while its dedicated
|
||||||
|
// primitives-generated query counts both - measured 2026-08, and the reason
|
||||||
|
// the verdict ranks the dedicated query first. A substitute qualifies only by
|
||||||
|
// answering the exact expected count on every shape it is required to cover;
|
||||||
|
// a device where no substitute qualifies even for the plain shape gets none
|
||||||
|
// (the honest verdict is the current behaviour); anything that fits neither
|
||||||
|
// the defect nor health is INCONCLUSIVE and must never arm anything. The
|
||||||
|
// expected counts are exact (1 triangle per shape), not merely nonzero, so a
|
||||||
|
// driver that half-counts cannot arm a half-right repair.
|
||||||
|
|
||||||
|
// What one drawn shape of the probe measured.
|
||||||
|
struct PrimitivesGeneratedNoXfbShapeMeasurement {
|
||||||
|
// The shape's draw was recorded and its query slots were read back.
|
||||||
|
Bool drawn = false;
|
||||||
|
// Primitives the draw is defined to emit (1 for every shape).
|
||||||
|
Uint64 expectedPrimitives = 0;
|
||||||
|
// The stream-query slot's primitivesNeeded answer - what the renderer's
|
||||||
|
// GL_PRIMITIVES_GENERATED path would have returned.
|
||||||
|
Uint64 streamGenerated = 0;
|
||||||
|
// Whether the dedicated primitives-generated slot ran (it needs the
|
||||||
|
// extension with both feature bits, see above).
|
||||||
|
Bool primitivesGeneratedExtMeasured = false;
|
||||||
|
// Its answer for the same draw.
|
||||||
|
Uint64 primitivesGeneratedExt = 0;
|
||||||
|
// Whether the statistics slot ran (it needs pipelineStatisticsQuery).
|
||||||
|
Bool statisticsMeasured = false;
|
||||||
|
// The clipping-stage invocation count for the same draw.
|
||||||
|
Uint64 statisticsClippingInput = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PrimitivesGeneratedNoXfbMeasurement {
|
||||||
|
// The probe submitted and read back at least the two triangle shapes.
|
||||||
|
// False when any setup step failed; failureReason then names the step.
|
||||||
|
Bool ran = false;
|
||||||
|
// The probe's bounded fence wait expired with the submission possibly
|
||||||
|
// still executing. The probe then deliberately LEAKED every child object
|
||||||
|
// it created (no vkDeviceWaitIdle, no destroys - a hung GPU must not hang
|
||||||
|
// the caller), so a caller that owns the device MUST NOT destroy or
|
||||||
|
// idle-wait it either: vkDestroyDevice with live children and in-flight
|
||||||
|
// work is the exact hang the bound exists to prevent. The POST leaks its
|
||||||
|
// throwaway device on this flag, mirroring its sibling probes.
|
||||||
|
Bool fenceWaitTimedOut = false;
|
||||||
|
String failureReason;
|
||||||
|
PrimitivesGeneratedNoXfbShapeMeasurement trianglesPlain;
|
||||||
|
PrimitivesGeneratedNoXfbShapeMeasurement trianglesDiscard;
|
||||||
|
// drawn = false when the device has no tessellationShader.
|
||||||
|
PrimitivesGeneratedNoXfbShapeMeasurement patchesDiscard;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The device-level entry points the probe records with. Supplied by the caller
|
||||||
|
// because the two callers resolve them differently: the renderer passes its
|
||||||
|
// statically linked symbols (and its vkGetDeviceProcAddr-resolved EXT
|
||||||
|
// pointers), the driver POST passes vkGetInstanceProcAddr trampolines.
|
||||||
|
struct PrimitivesGeneratedNoXfbProbeFns {
|
||||||
|
PFN_vkCreateCommandPool vkCreateCommandPool = nullptr;
|
||||||
|
PFN_vkDestroyCommandPool vkDestroyCommandPool = nullptr;
|
||||||
|
PFN_vkAllocateCommandBuffers vkAllocateCommandBuffers = nullptr;
|
||||||
|
PFN_vkBeginCommandBuffer vkBeginCommandBuffer = nullptr;
|
||||||
|
PFN_vkEndCommandBuffer vkEndCommandBuffer = nullptr;
|
||||||
|
PFN_vkCreateQueryPool vkCreateQueryPool = nullptr;
|
||||||
|
PFN_vkDestroyQueryPool vkDestroyQueryPool = nullptr;
|
||||||
|
PFN_vkCmdResetQueryPool vkCmdResetQueryPool = nullptr;
|
||||||
|
PFN_vkCmdBeginQuery vkCmdBeginQuery = nullptr;
|
||||||
|
PFN_vkCmdEndQuery vkCmdEndQuery = nullptr;
|
||||||
|
PFN_vkCmdBeginQueryIndexedEXT vkCmdBeginQueryIndexedEXT = nullptr;
|
||||||
|
PFN_vkCmdEndQueryIndexedEXT vkCmdEndQueryIndexedEXT = nullptr;
|
||||||
|
PFN_vkCreateRenderPass vkCreateRenderPass = nullptr;
|
||||||
|
PFN_vkDestroyRenderPass vkDestroyRenderPass = nullptr;
|
||||||
|
PFN_vkCreateFramebuffer vkCreateFramebuffer = nullptr;
|
||||||
|
PFN_vkDestroyFramebuffer vkDestroyFramebuffer = nullptr;
|
||||||
|
PFN_vkCmdBeginRenderPass vkCmdBeginRenderPass = nullptr;
|
||||||
|
PFN_vkCmdEndRenderPass vkCmdEndRenderPass = nullptr;
|
||||||
|
PFN_vkCreateShaderModule vkCreateShaderModule = nullptr;
|
||||||
|
PFN_vkDestroyShaderModule vkDestroyShaderModule = nullptr;
|
||||||
|
PFN_vkCreatePipelineLayout vkCreatePipelineLayout = nullptr;
|
||||||
|
PFN_vkDestroyPipelineLayout vkDestroyPipelineLayout = nullptr;
|
||||||
|
PFN_vkCreateGraphicsPipelines vkCreateGraphicsPipelines = nullptr;
|
||||||
|
PFN_vkDestroyPipeline vkDestroyPipeline = nullptr;
|
||||||
|
PFN_vkCmdBindPipeline vkCmdBindPipeline = nullptr;
|
||||||
|
PFN_vkCmdDraw vkCmdDraw = nullptr;
|
||||||
|
PFN_vkCreateFence vkCreateFence = nullptr;
|
||||||
|
PFN_vkDestroyFence vkDestroyFence = nullptr;
|
||||||
|
PFN_vkQueueSubmit vkQueueSubmit = nullptr;
|
||||||
|
PFN_vkWaitForFences vkWaitForFences = nullptr;
|
||||||
|
PFN_vkGetQueryPoolResults vkGetQueryPoolResults = nullptr;
|
||||||
|
PFN_vkDeviceWaitIdle vkDeviceWaitIdle = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PrimitivesGeneratedNoXfbProbeContext {
|
||||||
|
VkDevice device = VK_NULL_HANDLE;
|
||||||
|
VkQueue queue = VK_NULL_HANDLE;
|
||||||
|
Uint32 queueFamilyIndex = 0;
|
||||||
|
// The device was created with VK_EXT_transform_feedback, its
|
||||||
|
// transformFeedback feature, and advertises transformFeedbackQueries.
|
||||||
|
// Without this the probe has no subject and reports "did not run".
|
||||||
|
Bool transformFeedbackQueriesUsable = false;
|
||||||
|
// The device was created with VK_EXT_primitives_generated_query and BOTH
|
||||||
|
// its primitivesGeneratedQuery and ...WithRasterizerDiscard features;
|
||||||
|
// gates the dedicated-query control slots.
|
||||||
|
Bool primitivesGeneratedQueryUsable = false;
|
||||||
|
// The device was created with the pipelineStatisticsQuery feature; gates
|
||||||
|
// the statistics control slots. A probe with no control at all can still
|
||||||
|
// DETECT, but never qualifies a substitute.
|
||||||
|
Bool pipelineStatisticsEnabled = false;
|
||||||
|
// The device was created with the tessellationShader feature; gates the
|
||||||
|
// PATCHES shape.
|
||||||
|
Bool tessellationEnabled = false;
|
||||||
|
PrimitivesGeneratedNoXfbProbeFns fns;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Records, submits and reads back the probe. Synchronous: waits on its own
|
||||||
|
// fence (bounded; on timeout it deliberately leaks its device objects rather
|
||||||
|
// than idle-wait a possibly hung GPU, mirroring the POST timestamp probe) and
|
||||||
|
// destroys everything it created. Never touches MG_State or renderer state -
|
||||||
|
// the caller only lends it a device and an otherwise idle queue.
|
||||||
|
PrimitivesGeneratedNoXfbMeasurement RunPrimitivesGeneratedNoXfbProbe(
|
||||||
|
const PrimitivesGeneratedNoXfbProbeContext& context);
|
||||||
|
|
||||||
|
// The verdict vocabulary. Pure function of the measurement, split from the
|
||||||
|
// Vulkan plumbing so a unit test can pin every mapping with synthetic numbers.
|
||||||
|
enum class PrimitivesGeneratedNoXfbVerdict : Uint8 {
|
||||||
|
// The probe did not run, or answered something that is neither healthy nor
|
||||||
|
// the defect (a half-count, a nonzero-but-wrong stream answer). Must never
|
||||||
|
// arm the reroute and must never be reported as the bug.
|
||||||
|
Inconclusive,
|
||||||
|
// Every drawn shape's stream query answered its exact expected count: the
|
||||||
|
// driver counts XFB-inactive draws and the existing path is correct.
|
||||||
|
StreamCounts,
|
||||||
|
// The defect is present (a drawn shape's stream query answered exactly 0)
|
||||||
|
// and the dedicated primitives-generated query answered every drawn shape
|
||||||
|
// exactly, the rasterizer-discard shapes included: the substitution is
|
||||||
|
// proven whole through the query Vulkan defines for exactly this GL
|
||||||
|
// target.
|
||||||
|
PrimitivesGeneratedExtSubstitute,
|
||||||
|
// The defect is present, the dedicated query did not qualify (absent, or
|
||||||
|
// silent like the stream query), and the statistics control answered EVERY
|
||||||
|
// drawn shape exactly - discard shapes included: the substitution is
|
||||||
|
// proven whole through clipping statistics.
|
||||||
|
StatisticsSubstitute,
|
||||||
|
// The defect is present and the statistics control is exact on the PLAIN
|
||||||
|
// shape but not on every drawn shape (llvmpipe's discard short-circuit
|
||||||
|
// does this to its statistics - its dedicated query is what rescues it to
|
||||||
|
// the verdict above). This verdict additionally GUARANTEES domination:
|
||||||
|
// every shape the statistics missed measured exactly 0 through the stream
|
||||||
|
// query too, so rerouting is never worse per draw - it repairs every
|
||||||
|
// shape the statistics answer exactly and leaves the rest at the 0 they
|
||||||
|
// already read. A measurement where the stream was EXACT on a shape the
|
||||||
|
// statistics missed does not qualify (rerouting would downgrade that
|
||||||
|
// shape) and falls to Unfixable instead. The shapes the substitute
|
||||||
|
// misses - the CTS's discarded shapes among them wherever they are the
|
||||||
|
// missed ones - stay broken, and the report must say which.
|
||||||
|
StatisticsSubstitutePlainOnly,
|
||||||
|
// The defect is present and no substitute qualifies: none is exact
|
||||||
|
// everywhere, and the plain-only fallback either misses the plain shape
|
||||||
|
// or fails the domination rule above. The honest verdict is the current
|
||||||
|
// behaviour.
|
||||||
|
Unfixable,
|
||||||
|
};
|
||||||
|
|
||||||
|
PrimitivesGeneratedNoXfbVerdict EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||||
|
const PrimitivesGeneratedNoXfbMeasurement& measurement);
|
||||||
|
|
||||||
|
// Which query pool the renderer routes GL_PRIMITIVES_GENERATED accumulation
|
||||||
|
// for XFB-inactive draws through.
|
||||||
|
enum class PrimGenRerouteKind : Uint8 {
|
||||||
|
None,
|
||||||
|
// VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT (needs the extension with both
|
||||||
|
// feature bits - see the context flag).
|
||||||
|
PrimitivesGeneratedExt,
|
||||||
|
// VK_QUERY_TYPE_PIPELINE_STATISTICS over clipping invocations (needs
|
||||||
|
// pipelineStatisticsQuery).
|
||||||
|
ClippingStatistics,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The arming decision. Pure, so the override mapping is unit-pinnable:
|
||||||
|
// - ForceOff never reroutes;
|
||||||
|
// - ForceOn bypasses the verdict but never the structural checks: it takes
|
||||||
|
// the dedicated query where the device can host it, the statistics pool
|
||||||
|
// where only that exists, and nothing where neither does;
|
||||||
|
// - Auto follows the verdict: the dedicated query on
|
||||||
|
// PrimitivesGeneratedExtSubstitute, the statistics pool on
|
||||||
|
// StatisticsSubstitute and StatisticsSubstitutePlainOnly (each already
|
||||||
|
// implies its feature-backed control), and nothing otherwise.
|
||||||
|
PrimGenRerouteKind ChoosePrimitivesGeneratedReroute(MG_Config::QuirkOverride overrideSetting,
|
||||||
|
PrimitivesGeneratedNoXfbVerdict verdict,
|
||||||
|
Bool primitivesGeneratedQueryUsable,
|
||||||
|
Bool pipelineStatisticsEnabled);
|
||||||
|
} // namespace MobileGL::MG_Util::SelfTest
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.tesc
|
||||||
|
// 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
|
||||||
|
//
|
||||||
|
// Tessellation control stage of the PATCHES variant of the
|
||||||
|
// primitives-generated-without-transform-feedback probe. Every level is 1, so with
|
||||||
|
// the evaluation stage's triangles domain the tessellator emits exactly one
|
||||||
|
// triangle per patch - the expected count the probe checks the queries against.
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(vertices = 1) out;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_TessLevelOuter[0] = 1.0;
|
||||||
|
gl_TessLevelOuter[1] = 1.0;
|
||||||
|
gl_TessLevelOuter[2] = 1.0;
|
||||||
|
gl_TessLevelOuter[3] = 1.0;
|
||||||
|
gl_TessLevelInner[0] = 1.0;
|
||||||
|
gl_TessLevelInner[1] = 1.0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.tese
|
||||||
|
// 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
|
||||||
|
//
|
||||||
|
// Tessellation evaluation stage of the PATCHES variant of the
|
||||||
|
// primitives-generated-without-transform-feedback probe. Triangles domain: with the
|
||||||
|
// control stage's all-1 levels the tessellator emits exactly one triangle per
|
||||||
|
// patch. Like the vertex stage, it deliberately carries no Xfb execution mode.
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(triangles, equal_spacing, cw) in;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.vert
|
||||||
|
// 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
|
||||||
|
//
|
||||||
|
// Vertex stage of the primitives-generated-without-transform-feedback probe
|
||||||
|
// (PrimitivesGeneratedNoXfbProbe.cpp), used by both triangle shapes (with and
|
||||||
|
// without rasterizer discard) and as the tessellation shapes' vertex stage.
|
||||||
|
// Deliberately carries NO Xfb execution mode: the probe's whole subject is what
|
||||||
|
// the transform-feedback stream query answers for a pipeline that captures
|
||||||
|
// nothing. Positions are distinct (a full-viewport triangle per three vertices)
|
||||||
|
// so no driver can excuse the primitive as degenerate before it reaches
|
||||||
|
// primitive assembly.
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
const vec2 corners[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||||
|
gl_Position = vec4(corners[gl_VertexIndex % 3], 0.0, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbeSpv.h
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// Generated from PrimitivesGeneratedNoXfbProbe.{vert,tesc,tese} with:
|
||||||
|
// glslangValidator --target-env vulkan1.1 -V PrimitivesGeneratedNoXfbProbe.<stage>
|
||||||
|
// (SPIR-V words dumped little-endian, six per line.)
|
||||||
|
//
|
||||||
|
// Regenerate whenever a probe shader changes; nothing else in the probe depends on
|
||||||
|
// the exact binary. None of the modules carries an Xfb execution mode - that is the
|
||||||
|
// probe's subject, see PrimitivesGeneratedNoXfbProbe.cpp.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Util::SelfTest {
|
||||||
|
inline constexpr std::uint32_t kPrimitivesGeneratedNoXfbProbeVertSpv[] = {
|
||||||
|
0x07230203, 0x00010300, 0x0008000b, 0x0000002a, 0x00000000, 0x00020011,
|
||||||
|
0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
|
||||||
|
0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0007000f, 0x00000000,
|
||||||
|
0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x0000001a, 0x00030003,
|
||||||
|
0x00000002, 0x000001c2, 0x00040005, 0x00000004, 0x6e69616d, 0x00000000,
|
||||||
|
0x00060005, 0x0000000b, 0x505f6c67, 0x65567265, 0x78657472, 0x00000000,
|
||||||
|
0x00060006, 0x0000000b, 0x00000000, 0x505f6c67, 0x7469736f, 0x006e6f69,
|
||||||
|
0x00070006, 0x0000000b, 0x00000001, 0x505f6c67, 0x746e696f, 0x657a6953,
|
||||||
|
0x00000000, 0x00070006, 0x0000000b, 0x00000002, 0x435f6c67, 0x4470696c,
|
||||||
|
0x61747369, 0x0065636e, 0x00070006, 0x0000000b, 0x00000003, 0x435f6c67,
|
||||||
|
0x446c6c75, 0x61747369, 0x0065636e, 0x00030005, 0x0000000d, 0x00000000,
|
||||||
|
0x00060005, 0x0000001a, 0x565f6c67, 0x65747265, 0x646e4978, 0x00007865,
|
||||||
|
0x00050005, 0x0000001f, 0x65646e69, 0x6c626178, 0x00000065, 0x00030047,
|
||||||
|
0x0000000b, 0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x0000000b,
|
||||||
|
0x00000000, 0x00050048, 0x0000000b, 0x00000001, 0x0000000b, 0x00000001,
|
||||||
|
0x00050048, 0x0000000b, 0x00000002, 0x0000000b, 0x00000003, 0x00050048,
|
||||||
|
0x0000000b, 0x00000003, 0x0000000b, 0x00000004, 0x00040047, 0x0000001a,
|
||||||
|
0x0000000b, 0x0000002a, 0x00020013, 0x00000002, 0x00030021, 0x00000003,
|
||||||
|
0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007,
|
||||||
|
0x00000006, 0x00000004, 0x00040015, 0x00000008, 0x00000020, 0x00000000,
|
||||||
|
0x0004002b, 0x00000008, 0x00000009, 0x00000001, 0x0004001c, 0x0000000a,
|
||||||
|
0x00000006, 0x00000009, 0x0006001e, 0x0000000b, 0x00000007, 0x00000006,
|
||||||
|
0x0000000a, 0x0000000a, 0x00040020, 0x0000000c, 0x00000003, 0x0000000b,
|
||||||
|
0x0004003b, 0x0000000c, 0x0000000d, 0x00000003, 0x00040015, 0x0000000e,
|
||||||
|
0x00000020, 0x00000001, 0x0004002b, 0x0000000e, 0x0000000f, 0x00000000,
|
||||||
|
0x00040017, 0x00000010, 0x00000006, 0x00000002, 0x0004002b, 0x00000008,
|
||||||
|
0x00000011, 0x00000003, 0x0004001c, 0x00000012, 0x00000010, 0x00000011,
|
||||||
|
0x0004002b, 0x00000006, 0x00000013, 0xbf800000, 0x0005002c, 0x00000010,
|
||||||
|
0x00000014, 0x00000013, 0x00000013, 0x0004002b, 0x00000006, 0x00000015,
|
||||||
|
0x40400000, 0x0005002c, 0x00000010, 0x00000016, 0x00000015, 0x00000013,
|
||||||
|
0x0005002c, 0x00000010, 0x00000017, 0x00000013, 0x00000015, 0x0006002c,
|
||||||
|
0x00000012, 0x00000018, 0x00000014, 0x00000016, 0x00000017, 0x00040020,
|
||||||
|
0x00000019, 0x00000001, 0x0000000e, 0x0004003b, 0x00000019, 0x0000001a,
|
||||||
|
0x00000001, 0x0004002b, 0x0000000e, 0x0000001c, 0x00000003, 0x00040020,
|
||||||
|
0x0000001e, 0x00000007, 0x00000012, 0x00040020, 0x00000020, 0x00000007,
|
||||||
|
0x00000010, 0x0004002b, 0x00000006, 0x00000023, 0x00000000, 0x0004002b,
|
||||||
|
0x00000006, 0x00000024, 0x3f800000, 0x00040020, 0x00000028, 0x00000003,
|
||||||
|
0x00000007, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003,
|
||||||
|
0x000200f8, 0x00000005, 0x0004003b, 0x0000001e, 0x0000001f, 0x00000007,
|
||||||
|
0x0004003d, 0x0000000e, 0x0000001b, 0x0000001a, 0x0005008b, 0x0000000e,
|
||||||
|
0x0000001d, 0x0000001b, 0x0000001c, 0x0003003e, 0x0000001f, 0x00000018,
|
||||||
|
0x00050041, 0x00000020, 0x00000021, 0x0000001f, 0x0000001d, 0x0004003d,
|
||||||
|
0x00000010, 0x00000022, 0x00000021, 0x00050051, 0x00000006, 0x00000025,
|
||||||
|
0x00000022, 0x00000000, 0x00050051, 0x00000006, 0x00000026, 0x00000022,
|
||||||
|
0x00000001, 0x00070050, 0x00000007, 0x00000027, 0x00000025, 0x00000026,
|
||||||
|
0x00000023, 0x00000024, 0x00050041, 0x00000028, 0x00000029, 0x0000000d,
|
||||||
|
0x0000000f, 0x0003003e, 0x00000029, 0x00000027, 0x000100fd, 0x00010038,
|
||||||
|
};
|
||||||
|
inline constexpr std::size_t kPrimitivesGeneratedNoXfbProbeVertSpvWordCount = sizeof(kPrimitivesGeneratedNoXfbProbeVertSpv) / sizeof(kPrimitivesGeneratedNoXfbProbeVertSpv[0]);
|
||||||
|
|
||||||
|
inline constexpr std::uint32_t kPrimitivesGeneratedNoXfbProbeTescSpv[] = {
|
||||||
|
0x07230203, 0x00010300, 0x0008000b, 0x0000001d, 0x00000000, 0x00020011,
|
||||||
|
0x00000003, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
|
||||||
|
0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0007000f, 0x00000001,
|
||||||
|
0x00000004, 0x6e69616d, 0x00000000, 0x0000000b, 0x0000001a, 0x00040010,
|
||||||
|
0x00000004, 0x0000001a, 0x00000001, 0x00030003, 0x00000002, 0x000001c2,
|
||||||
|
0x00040005, 0x00000004, 0x6e69616d, 0x00000000, 0x00070005, 0x0000000b,
|
||||||
|
0x545f6c67, 0x4c737365, 0x6c657665, 0x6574754f, 0x00000072, 0x00070005,
|
||||||
|
0x0000001a, 0x545f6c67, 0x4c737365, 0x6c657665, 0x656e6e49, 0x00000072,
|
||||||
|
0x00040047, 0x0000000b, 0x0000000b, 0x0000000b, 0x00030047, 0x0000000b,
|
||||||
|
0x0000000f, 0x00040047, 0x0000001a, 0x0000000b, 0x0000000c, 0x00030047,
|
||||||
|
0x0000001a, 0x0000000f, 0x00020013, 0x00000002, 0x00030021, 0x00000003,
|
||||||
|
0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040015, 0x00000007,
|
||||||
|
0x00000020, 0x00000000, 0x0004002b, 0x00000007, 0x00000008, 0x00000004,
|
||||||
|
0x0004001c, 0x00000009, 0x00000006, 0x00000008, 0x00040020, 0x0000000a,
|
||||||
|
0x00000003, 0x00000009, 0x0004003b, 0x0000000a, 0x0000000b, 0x00000003,
|
||||||
|
0x00040015, 0x0000000c, 0x00000020, 0x00000001, 0x0004002b, 0x0000000c,
|
||||||
|
0x0000000d, 0x00000000, 0x0004002b, 0x00000006, 0x0000000e, 0x3f800000,
|
||||||
|
0x00040020, 0x0000000f, 0x00000003, 0x00000006, 0x0004002b, 0x0000000c,
|
||||||
|
0x00000011, 0x00000001, 0x0004002b, 0x0000000c, 0x00000013, 0x00000002,
|
||||||
|
0x0004002b, 0x0000000c, 0x00000015, 0x00000003, 0x0004002b, 0x00000007,
|
||||||
|
0x00000017, 0x00000002, 0x0004001c, 0x00000018, 0x00000006, 0x00000017,
|
||||||
|
0x00040020, 0x00000019, 0x00000003, 0x00000018, 0x0004003b, 0x00000019,
|
||||||
|
0x0000001a, 0x00000003, 0x00050036, 0x00000002, 0x00000004, 0x00000000,
|
||||||
|
0x00000003, 0x000200f8, 0x00000005, 0x00050041, 0x0000000f, 0x00000010,
|
||||||
|
0x0000000b, 0x0000000d, 0x0003003e, 0x00000010, 0x0000000e, 0x00050041,
|
||||||
|
0x0000000f, 0x00000012, 0x0000000b, 0x00000011, 0x0003003e, 0x00000012,
|
||||||
|
0x0000000e, 0x00050041, 0x0000000f, 0x00000014, 0x0000000b, 0x00000013,
|
||||||
|
0x0003003e, 0x00000014, 0x0000000e, 0x00050041, 0x0000000f, 0x00000016,
|
||||||
|
0x0000000b, 0x00000015, 0x0003003e, 0x00000016, 0x0000000e, 0x00050041,
|
||||||
|
0x0000000f, 0x0000001b, 0x0000001a, 0x0000000d, 0x0003003e, 0x0000001b,
|
||||||
|
0x0000000e, 0x00050041, 0x0000000f, 0x0000001c, 0x0000001a, 0x00000011,
|
||||||
|
0x0003003e, 0x0000001c, 0x0000000e, 0x000100fd, 0x00010038,
|
||||||
|
};
|
||||||
|
inline constexpr std::size_t kPrimitivesGeneratedNoXfbProbeTescSpvWordCount = sizeof(kPrimitivesGeneratedNoXfbProbeTescSpv) / sizeof(kPrimitivesGeneratedNoXfbProbeTescSpv[0]);
|
||||||
|
|
||||||
|
inline constexpr std::uint32_t kPrimitivesGeneratedNoXfbProbeTeseSpv[] = {
|
||||||
|
0x07230203, 0x00010300, 0x0008000b, 0x00000021, 0x00000000, 0x00020011,
|
||||||
|
0x00000003, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
|
||||||
|
0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0007000f, 0x00000002,
|
||||||
|
0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000012, 0x00030010,
|
||||||
|
0x00000004, 0x00000016, 0x00030010, 0x00000004, 0x00000001, 0x00030010,
|
||||||
|
0x00000004, 0x00000004, 0x00030003, 0x00000002, 0x000001c2, 0x00040005,
|
||||||
|
0x00000004, 0x6e69616d, 0x00000000, 0x00060005, 0x0000000b, 0x505f6c67,
|
||||||
|
0x65567265, 0x78657472, 0x00000000, 0x00060006, 0x0000000b, 0x00000000,
|
||||||
|
0x505f6c67, 0x7469736f, 0x006e6f69, 0x00070006, 0x0000000b, 0x00000001,
|
||||||
|
0x505f6c67, 0x746e696f, 0x657a6953, 0x00000000, 0x00070006, 0x0000000b,
|
||||||
|
0x00000002, 0x435f6c67, 0x4470696c, 0x61747369, 0x0065636e, 0x00070006,
|
||||||
|
0x0000000b, 0x00000003, 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e,
|
||||||
|
0x00030005, 0x0000000d, 0x00000000, 0x00060005, 0x00000012, 0x545f6c67,
|
||||||
|
0x43737365, 0x64726f6f, 0x00000000, 0x00030047, 0x0000000b, 0x00000002,
|
||||||
|
0x00050048, 0x0000000b, 0x00000000, 0x0000000b, 0x00000000, 0x00050048,
|
||||||
|
0x0000000b, 0x00000001, 0x0000000b, 0x00000001, 0x00050048, 0x0000000b,
|
||||||
|
0x00000002, 0x0000000b, 0x00000003, 0x00050048, 0x0000000b, 0x00000003,
|
||||||
|
0x0000000b, 0x00000004, 0x00040047, 0x00000012, 0x0000000b, 0x0000000d,
|
||||||
|
0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016,
|
||||||
|
0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004,
|
||||||
|
0x00040015, 0x00000008, 0x00000020, 0x00000000, 0x0004002b, 0x00000008,
|
||||||
|
0x00000009, 0x00000001, 0x0004001c, 0x0000000a, 0x00000006, 0x00000009,
|
||||||
|
0x0006001e, 0x0000000b, 0x00000007, 0x00000006, 0x0000000a, 0x0000000a,
|
||||||
|
0x00040020, 0x0000000c, 0x00000003, 0x0000000b, 0x0004003b, 0x0000000c,
|
||||||
|
0x0000000d, 0x00000003, 0x00040015, 0x0000000e, 0x00000020, 0x00000001,
|
||||||
|
0x0004002b, 0x0000000e, 0x0000000f, 0x00000000, 0x00040017, 0x00000010,
|
||||||
|
0x00000006, 0x00000003, 0x00040020, 0x00000011, 0x00000001, 0x00000010,
|
||||||
|
0x0004003b, 0x00000011, 0x00000012, 0x00000001, 0x00040017, 0x00000013,
|
||||||
|
0x00000006, 0x00000002, 0x0004002b, 0x00000006, 0x00000016, 0x40000000,
|
||||||
|
0x0004002b, 0x00000006, 0x00000018, 0x3f800000, 0x0004002b, 0x00000006,
|
||||||
|
0x0000001b, 0x00000000, 0x00040020, 0x0000001f, 0x00000003, 0x00000007,
|
||||||
|
0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8,
|
||||||
|
0x00000005, 0x0004003d, 0x00000010, 0x00000014, 0x00000012, 0x0007004f,
|
||||||
|
0x00000013, 0x00000015, 0x00000014, 0x00000014, 0x00000000, 0x00000001,
|
||||||
|
0x0005008e, 0x00000013, 0x00000017, 0x00000015, 0x00000016, 0x00050050,
|
||||||
|
0x00000013, 0x00000019, 0x00000018, 0x00000018, 0x00050083, 0x00000013,
|
||||||
|
0x0000001a, 0x00000017, 0x00000019, 0x00050051, 0x00000006, 0x0000001c,
|
||||||
|
0x0000001a, 0x00000000, 0x00050051, 0x00000006, 0x0000001d, 0x0000001a,
|
||||||
|
0x00000001, 0x00070050, 0x00000007, 0x0000001e, 0x0000001c, 0x0000001d,
|
||||||
|
0x0000001b, 0x00000018, 0x00050041, 0x0000001f, 0x00000020, 0x0000000d,
|
||||||
|
0x0000000f, 0x0003003e, 0x00000020, 0x0000001e, 0x000100fd, 0x00010038,
|
||||||
|
};
|
||||||
|
inline constexpr std::size_t kPrimitivesGeneratedNoXfbProbeTeseSpvWordCount = sizeof(kPrimitivesGeneratedNoXfbProbeTeseSpv) / sizeof(kPrimitivesGeneratedNoXfbProbeTeseSpv[0]);
|
||||||
|
} // namespace MobileGL::MG_Util::SelfTest
|
||||||
@@ -154,6 +154,20 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
// demoted module is the one that works everywhere, so it is what a standalone compile
|
// demoted module is the one that works everywhere, so it is what a standalone compile
|
||||||
// (an internal shader object, a unit test) gets.
|
// (an internal shader object, a unit test) gets.
|
||||||
Bool ConsumesFloat64Natively() const { return HasBackend() && params.SupportsShaderFloat64; }
|
Bool ConsumesFloat64Natively() const { return HasBackend() && params.SupportsShaderFloat64; }
|
||||||
|
// Whether the tessellation / geometry gl_PointSize demotion is ARMED for this env -
|
||||||
|
// i.e. the backend declared it cannot host the capability. Deliberately requiring a
|
||||||
|
// backend, opposite in shape to ConsumesFloat64Natively's fallback but for the same
|
||||||
|
// conservatism: the fp64 demotion is the module that works everywhere, while this
|
||||||
|
// one rewrites interfaces and capture names, so the no-backend answer (standalone
|
||||||
|
// compiles, unit tests) is the untouched module. Like nativeFloat64, each bit is L1
|
||||||
|
// key material of its own (SpirvTranslationKeyInputs), never part of the frontend
|
||||||
|
// fingerprint: glslang produces the same thing either way.
|
||||||
|
Bool DemotesTessellationPointSize() const {
|
||||||
|
return HasBackend() && !params.SupportsTessellationPointSize;
|
||||||
|
}
|
||||||
|
Bool DemotesGeometryPointSize() const {
|
||||||
|
return HasBackend() && !params.SupportsGeometryPointSize;
|
||||||
|
}
|
||||||
// Matches the historical rule exactly: with no active backend every extension counts
|
// Matches the historical rule exactly: with no active backend every extension counts
|
||||||
// as advertised, because the frontend then has nothing to gate against.
|
// as advertised, because the frontend then has nothing to gate against.
|
||||||
Bool IsExtensionAdvertised(GLExtension extension) const {
|
Bool IsExtensionAdvertised(GLExtension extension) const {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
#include "SpirvPasses/LegalizeFragmentOutputIndexPass.h"
|
#include "SpirvPasses/LegalizeFragmentOutputIndexPass.h"
|
||||||
#include "SpirvPasses/LegalizeResourceArrayIndexPass.h"
|
#include "SpirvPasses/LegalizeResourceArrayIndexPass.h"
|
||||||
#include "SpirvPasses/FlattenAtomicCounterBlockPass.h"
|
#include "SpirvPasses/FlattenAtomicCounterBlockPass.h"
|
||||||
|
#include "SpirvPasses/DemotePointSizePass.h"
|
||||||
#include "spirv-tools/libspirv.h"
|
#include "spirv-tools/libspirv.h"
|
||||||
#include "spirv-tools/optimizer.hpp"
|
#include "spirv-tools/optimizer.hpp"
|
||||||
#include "source/opt/build_module.h"
|
#include "source/opt/build_module.h"
|
||||||
@@ -816,6 +817,326 @@ namespace MobileGL {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
namespace opt_analysis = spvtools::opt::analysis;
|
||||||
|
|
||||||
|
// Locations one value of `type` consumes (GL 4.6 core 11.1.2.1). Unknown
|
||||||
|
// shapes OVERESTIMATE (4) rather than fail: this feeds the free-location
|
||||||
|
// choice for the demoted point-size carrier, where an overestimate wastes a
|
||||||
|
// couple of slots and an underestimate aliases a live varying.
|
||||||
|
Uint32 ConservativeLocationSpan(const opt_analysis::Type* type) {
|
||||||
|
constexpr Uint32 kUnknownSpan = 4;
|
||||||
|
if (type == nullptr) return kUnknownSpan;
|
||||||
|
if (type->AsFloat() != nullptr || type->AsInteger() != nullptr ||
|
||||||
|
type->AsBool() != nullptr) {
|
||||||
|
return 1u;
|
||||||
|
}
|
||||||
|
if (const auto* vector = type->AsVector()) {
|
||||||
|
// 64-bit INTEGER elements count exactly like 64-bit floats:
|
||||||
|
// ARB_gpu_shader_int64 extends 11.1.2.1's double-precision rule
|
||||||
|
// verbatim to i64/u64, and DirectVulkan advertises that extension
|
||||||
|
// unconditionally - so answering "one location" for an i64vec4 would
|
||||||
|
// place the carrier on the SECOND location that varying already owns,
|
||||||
|
// which is the underestimate this function's header forbids.
|
||||||
|
const auto* element = vector->element_type();
|
||||||
|
const auto* elementFloat = element->AsFloat();
|
||||||
|
const auto* elementInteger = element->AsInteger();
|
||||||
|
const Bool is64Bit = (elementFloat != nullptr && elementFloat->width() == 64) ||
|
||||||
|
(elementInteger != nullptr && elementInteger->width() == 64);
|
||||||
|
return (is64Bit && vector->element_count() > 2) ? 2u : 1u;
|
||||||
|
}
|
||||||
|
if (const auto* matrix = type->AsMatrix()) {
|
||||||
|
return ConservativeLocationSpan(matrix->element_type()) * matrix->element_count();
|
||||||
|
}
|
||||||
|
if (const auto* array = type->AsArray()) {
|
||||||
|
const auto& lengthWords = array->length_info().words;
|
||||||
|
if (lengthWords.size() != 2 ||
|
||||||
|
lengthWords[0] !=
|
||||||
|
static_cast<Uint32>(opt_analysis::Array::LengthInfo::kConstant)) {
|
||||||
|
return kUnknownSpan;
|
||||||
|
}
|
||||||
|
return ConservativeLocationSpan(array->element_type()) * std::max(lengthWords[1], 1u);
|
||||||
|
}
|
||||||
|
if (const auto* strct = type->AsStruct()) {
|
||||||
|
Uint32 sum = 0;
|
||||||
|
for (const auto* member : strct->element_types()) {
|
||||||
|
sum += ConservativeLocationSpan(member);
|
||||||
|
}
|
||||||
|
return std::max(sum, 1u);
|
||||||
|
}
|
||||||
|
return kUnknownSpan;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One BuildModule per module answers all three questions the program-scoped
|
||||||
|
// demotion driver asks: which point-size capability the module declares, and
|
||||||
|
// one past the highest Input/Output location slot it consumes (so the carrier
|
||||||
|
// can be placed beyond every varying of every stage).
|
||||||
|
struct PointSizeModuleProbe {
|
||||||
|
Bool parsed = false;
|
||||||
|
Bool declaresTessellationPointSize = false;
|
||||||
|
Bool declaresGeometryPointSize = false;
|
||||||
|
Uint32 locationSlotEnd = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
PointSizeModuleProbe ProbePointSizeModule(const Vector<Uint32>& spirv) {
|
||||||
|
PointSizeModuleProbe probe;
|
||||||
|
if (spirv.empty()) {
|
||||||
|
probe.parsed = true; // an absent stage constrains nothing
|
||||||
|
return probe;
|
||||||
|
}
|
||||||
|
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
|
||||||
|
SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ProbePointSizeModule"), spirv.data(),
|
||||||
|
spirv.size());
|
||||||
|
if (!context) return probe;
|
||||||
|
probe.parsed = true;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
probe.declaresTessellationPointSize = true;
|
||||||
|
} else if (declared == spv::Capability::GeometryPointSize) {
|
||||||
|
probe.declaresGeometryPointSize = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spv::ExecutionModel model = spv::ExecutionModel::Max;
|
||||||
|
for (spvtools::opt::Instruction& entryPoint : context->module()->entry_points()) {
|
||||||
|
model = static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Per-vertex interfaces are arrayed one level deeper than the locations
|
||||||
|
// they consume; peel that level, but never off a per-patch output.
|
||||||
|
const Bool peelInputs = model == spv::ExecutionModel::TessellationControl ||
|
||||||
|
model == spv::ExecutionModel::TessellationEvaluation ||
|
||||||
|
model == spv::ExecutionModel::Geometry;
|
||||||
|
const Bool peelOutputs = model == spv::ExecutionModel::TessellationControl;
|
||||||
|
|
||||||
|
std::unordered_set<Uint32> patchDecorated;
|
||||||
|
for (spvtools::opt::Instruction& annotation : context->annotations()) {
|
||||||
|
if (annotation.opcode() == spv::Op::OpDecorate && annotation.NumInOperands() >= 2 &&
|
||||||
|
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) ==
|
||||||
|
spv::Decoration::Patch) {
|
||||||
|
patchDecorated.insert(annotation.GetSingleWordInOperand(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* defUse = context->get_def_use_mgr();
|
||||||
|
auto* typeMgr = context->get_type_mgr();
|
||||||
|
for (spvtools::opt::Instruction& annotation : context->annotations()) {
|
||||||
|
if (annotation.opcode() == spv::Op::OpDecorate && annotation.NumInOperands() >= 3 &&
|
||||||
|
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) ==
|
||||||
|
spv::Decoration::Location) {
|
||||||
|
const Uint32 location = annotation.GetSingleWordInOperand(2);
|
||||||
|
Uint32 span = 1;
|
||||||
|
spvtools::opt::Instruction* var =
|
||||||
|
defUse->GetDef(annotation.GetSingleWordInOperand(0));
|
||||||
|
if (var != nullptr && var->opcode() == spv::Op::OpVariable) {
|
||||||
|
const auto storage =
|
||||||
|
static_cast<spv::StorageClass>(var->GetSingleWordInOperand(0));
|
||||||
|
// Two location namespaces are NOT varying slots and must not
|
||||||
|
// shrink the carrier budget: vertex-stage inputs (attribute
|
||||||
|
// locations) and fragment-stage outputs (draw buffers).
|
||||||
|
if ((model == spv::ExecutionModel::Vertex &&
|
||||||
|
storage == spv::StorageClass::Input) ||
|
||||||
|
(model == spv::ExecutionModel::Fragment &&
|
||||||
|
storage == spv::StorageClass::Output)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
spvtools::opt::Instruction* pointerType = defUse->GetDef(var->type_id());
|
||||||
|
if (pointerType != nullptr &&
|
||||||
|
pointerType->opcode() == spv::Op::OpTypePointer) {
|
||||||
|
const opt_analysis::Type* pointee =
|
||||||
|
typeMgr->GetType(pointerType->GetSingleWordInOperand(1));
|
||||||
|
const Bool peel =
|
||||||
|
((storage == spv::StorageClass::Input && peelInputs) ||
|
||||||
|
(storage == spv::StorageClass::Output && peelOutputs)) &&
|
||||||
|
patchDecorated.count(var->result_id()) == 0;
|
||||||
|
if (peel && pointee != nullptr && pointee->AsArray() != nullptr) {
|
||||||
|
pointee = pointee->AsArray()->element_type();
|
||||||
|
}
|
||||||
|
span = ConservativeLocationSpan(pointee);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
probe.locationSlotEnd = std::max(probe.locationSlotEnd, location + span);
|
||||||
|
} else if (annotation.opcode() == spv::Op::OpMemberDecorate &&
|
||||||
|
annotation.NumInOperands() >= 4 &&
|
||||||
|
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(2)) ==
|
||||||
|
spv::Decoration::Location) {
|
||||||
|
const Uint32 member = annotation.GetSingleWordInOperand(1);
|
||||||
|
const Uint32 location = annotation.GetSingleWordInOperand(3);
|
||||||
|
Uint32 span = 1;
|
||||||
|
spvtools::opt::Instruction* structType =
|
||||||
|
defUse->GetDef(annotation.GetSingleWordInOperand(0));
|
||||||
|
if (structType != nullptr && structType->opcode() == spv::Op::OpTypeStruct &&
|
||||||
|
member < structType->NumInOperands()) {
|
||||||
|
span = ConservativeLocationSpan(
|
||||||
|
typeMgr->GetType(structType->GetSingleWordInOperand(member)));
|
||||||
|
}
|
||||||
|
probe.locationSlotEnd = std::max(probe.locationSlotEnd, location + span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return probe;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interior boundary carrier names, spelled by the PRODUCING stage so both
|
||||||
|
// sides of one boundary agree textually as well as by location. The capture
|
||||||
|
// stage's output uses POINT_SIZE_CAPTURE_CARRIER_NAME instead. None of these
|
||||||
|
// may embed the token "gl_PointSize" - see the constant's comment.
|
||||||
|
const char* PointSizeBoundaryCarrierName(const GLenum producerStage) {
|
||||||
|
switch (producerStage) {
|
||||||
|
case GL_VERTEX_SHADER:
|
||||||
|
return "mg_PointSizeIo0";
|
||||||
|
case GL_TESS_CONTROL_SHADER:
|
||||||
|
return "mg_PointSizeIo1";
|
||||||
|
case GL_TESS_EVALUATION_SHADER:
|
||||||
|
return "mg_PointSizeIo2";
|
||||||
|
default:
|
||||||
|
return "mg_PointSizeIo0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Past this the carrier would sit above what a minimum-spec varying budget can
|
||||||
|
// address; such a program keeps its honest decline instead.
|
||||||
|
constexpr Uint32 kMaxDemotedPointSizeCarrierLocation = 30;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Bool ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
Vector<Vector<Uint32>>& modules, const Vector<GLenum>& shaderTypes,
|
||||||
|
const Bool demoteTessellation, const Bool demoteGeometry,
|
||||||
|
const Bool captureRequestsPointSize, PointSizeDemotionOutcome& outcome,
|
||||||
|
const bool validateOutput, const bool enableSpirvValidation) {
|
||||||
|
outcome = {};
|
||||||
|
if (!demoteTessellation && !demoteGeometry) return true;
|
||||||
|
|
||||||
|
// The pre-rasterization chain, in pipeline order, as indices into `modules`.
|
||||||
|
Int stageIndex[4] = {-1, -1, -1, -1}; // VS, TCS, TES, GS
|
||||||
|
for (SizeT i = 0; i < shaderTypes.size() && i < modules.size(); ++i) {
|
||||||
|
switch (shaderTypes[i]) {
|
||||||
|
case GL_VERTEX_SHADER: stageIndex[0] = static_cast<Int>(i); break;
|
||||||
|
case GL_TESS_CONTROL_SHADER: stageIndex[1] = static_cast<Int>(i); break;
|
||||||
|
case GL_TESS_EVALUATION_SHADER: stageIndex[2] = static_cast<Int>(i); break;
|
||||||
|
case GL_GEOMETRY_SHADER: stageIndex[3] = static_cast<Int>(i); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stageIndex[1] < 0 && stageIndex[2] < 0 && stageIndex[3] < 0) return true;
|
||||||
|
|
||||||
|
// One probe per module: the capability facts arm the verdict, the location
|
||||||
|
// scan places the carrier past every varying of every stage (the location is
|
||||||
|
// shared program-wide, so it has to clear all of them at once).
|
||||||
|
Bool anyTessellationUse = false;
|
||||||
|
Bool anyGeometryUse = false;
|
||||||
|
Uint32 carrierLocation = 0;
|
||||||
|
for (const auto& module : modules) {
|
||||||
|
const PointSizeModuleProbe probe = ProbePointSizeModule(module);
|
||||||
|
if (!probe.parsed) {
|
||||||
|
// Unparseable is not a verdict; the module is already broken for
|
||||||
|
// other reasons and owns its own failure.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
anyTessellationUse |= probe.declaresTessellationPointSize;
|
||||||
|
anyGeometryUse |= probe.declaresGeometryPointSize;
|
||||||
|
carrierLocation = std::max(carrierLocation, probe.locationSlotEnd);
|
||||||
|
}
|
||||||
|
if (!((anyTessellationUse && demoteTessellation) ||
|
||||||
|
(anyGeometryUse && demoteGeometry))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (carrierLocation > kMaxDemotedPointSizeCarrierLocation) {
|
||||||
|
outcome.declineDetail = std::format(
|
||||||
|
"the program's varyings already reach location {}, past the carrier budget",
|
||||||
|
carrierLocation);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL 4.6 core 13.3: capture reads the last capture-capable stage - geometry,
|
||||||
|
// else evaluation, else the vertex stage (whose built-in needs no demotion).
|
||||||
|
const Int captureStage = stageIndex[3] >= 0 ? 3 : (stageIndex[2] >= 0 ? 2 : -1);
|
||||||
|
constexpr GLenum kStageEnum[4] = {GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER,
|
||||||
|
GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER};
|
||||||
|
|
||||||
|
// Back to front, so each stage's "I now read the carrier" report can force the
|
||||||
|
// producing stage's output carrier into existence - Vulkan requires every
|
||||||
|
// consumed input to be produced (VUID-RuntimeSpirv-OpEntryPoint-08743), and an
|
||||||
|
// ES link may reject a statically read input with no producing output.
|
||||||
|
Vector<Vector<Uint32>> rewritten(modules.size());
|
||||||
|
Bool rewrote[4] = {false, false, false, false};
|
||||||
|
Bool forceOutput[4] = {false, false, false, false};
|
||||||
|
if (captureStage >= 0 && captureRequestsPointSize) {
|
||||||
|
forceOutput[captureStage] = true;
|
||||||
|
}
|
||||||
|
for (Int stage = 3; stage >= 0; --stage) {
|
||||||
|
const Int moduleIndex = stageIndex[stage];
|
||||||
|
if (moduleIndex < 0) continue;
|
||||||
|
Int producer = stage - 1;
|
||||||
|
while (producer >= 0 && stageIndex[producer] < 0) --producer;
|
||||||
|
|
||||||
|
DemotePointSizeOptions options;
|
||||||
|
options.location = carrierLocation;
|
||||||
|
options.inputCarrierName = PointSizeBoundaryCarrierName(
|
||||||
|
producer >= 0 ? kStageEnum[producer]
|
||||||
|
// A separable program whose first present stage already
|
||||||
|
// consumes the carrier: the producer lives in another
|
||||||
|
// program. Name by the conventional producer of this
|
||||||
|
// stage's boundary; matching across programs is by
|
||||||
|
// location and is documented residue either way.
|
||||||
|
: kStageEnum[stage > 0 ? stage - 1 : 0]);
|
||||||
|
options.outputCarrierName = stage == captureStage
|
||||||
|
? String(POINT_SIZE_CAPTURE_CARRIER_NAME)
|
||||||
|
: String(PointSizeBoundaryCarrierName(kStageEnum[stage]));
|
||||||
|
options.forceOutputCarrier = forceOutput[stage];
|
||||||
|
|
||||||
|
// A vertex stage with nothing downstream consuming the carrier needs no
|
||||||
|
// mirror and stays byte-identical without an optimizer round trip.
|
||||||
|
if (stage == 0 && !options.forceOutputCarrier) continue;
|
||||||
|
|
||||||
|
DemotePointSizeReport report;
|
||||||
|
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
|
optimizer.RegisterPass(
|
||||||
|
DemotePointSizePass::CreateDemotePointSizePass(options, &report));
|
||||||
|
if (!RunOptimizerChecked("DemoteTessellationGeometryPointSizeForProgram", optimizer,
|
||||||
|
modules[moduleIndex], rewritten[moduleIndex],
|
||||||
|
validateOutput, enableSpirvValidation)) {
|
||||||
|
return false; // modules untouched: nothing was committed
|
||||||
|
}
|
||||||
|
if (report.declined) {
|
||||||
|
outcome.declineDetail = Move(report.declineReason);
|
||||||
|
return true; // byte-identical decline; the existing refusals stay armed
|
||||||
|
}
|
||||||
|
// AN EVALUATION STAGE WITH NO CONTROL STAGE THAT NOW READS A LOCATED
|
||||||
|
// INPUT. GL lets the evaluation stage sit straight on the vertex stage,
|
||||||
|
// and both backends stand a SYNTHESIZED pass-through control stage in
|
||||||
|
// between - one that forwards gl_Position and nothing else. Their guard
|
||||||
|
// for that is literally "does this module read a located input"
|
||||||
|
// (ModuleReadsLocatedInput / ReflectPassthroughTessControlNeed), so the
|
||||||
|
// carrier this pass just created would turn the very program the demotion
|
||||||
|
// exists to rescue into a declined one, reported against a varying name
|
||||||
|
// the application never wrote. Declining here keeps the modules
|
||||||
|
// byte-identical and leaves the honest built-in refusal in charge; only
|
||||||
|
// teaching the synthesized stage to forward the carrier could do better.
|
||||||
|
if (stage == 2 && stageIndex[1] < 0 && report.createdInputCarrier) {
|
||||||
|
outcome.declineDetail =
|
||||||
|
"an evaluation stage reads gl_in point size with no control stage to "
|
||||||
|
"carry it; the synthesized pass-through cannot forward the carrier";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
rewrote[stage] = true;
|
||||||
|
if (report.createdInputCarrier && producer >= 0) {
|
||||||
|
forceOutput[producer] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomic commit: every stage rewritten together or none at all.
|
||||||
|
for (Int stage = 0; stage < 4; ++stage) {
|
||||||
|
if (!rewrote[stage]) continue;
|
||||||
|
modules[stageIndex[stage]] = Move(rewritten[stageIndex[stage]]);
|
||||||
|
}
|
||||||
|
outcome.demoted = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -562,6 +562,61 @@ namespace MobileGL {
|
|||||||
// the module parse costs nothing on a device that has it.
|
// the module parse costs nothing on a device that has it.
|
||||||
static Bool ModuleDeclaresTessellationOrGeometryPointSize(const Vector<Uint32>& spirv);
|
static Bool ModuleDeclaresTessellationOrGeometryPointSize(const Vector<Uint32>& spirv);
|
||||||
|
|
||||||
|
// ---- gl_PointSize demotion for devices without the capability above ----
|
||||||
|
// The name of the demoted program's LAST capture-capable stage's point-size
|
||||||
|
// carrier. It is the contract three parties meet at: the demotion pass names
|
||||||
|
// the variable, DirectVulkan's XfbCaptureDecoratePass binds a "gl_PointSize"
|
||||||
|
// capture to it instead of mirroring the (no longer accessed) built-in, and
|
||||||
|
// DirectGLES respells the driver-side glTransformFeedbackVaryings request
|
||||||
|
// with it. Deliberately NOT containing the substring "gl_PointSize":
|
||||||
|
// DirectGLES's extension-request gate is a text search for that token over
|
||||||
|
// the emitted ESSL, and a carrier name embedding it would re-arm the decline
|
||||||
|
// this demotion exists to retire.
|
||||||
|
static constexpr const char* POINT_SIZE_CAPTURE_CARRIER_NAME = "mg_PointSizeCapture";
|
||||||
|
|
||||||
|
// What the program-scoped demotion left behind. `demoted` false with an empty
|
||||||
|
// detail means the program never needed it (no tessellation/geometry stage
|
||||||
|
// accesses the built-in, or the device hosts it); false WITH a detail means a
|
||||||
|
// module shape the pass cannot express - the modules are byte-identical and
|
||||||
|
// the existing decline paths (Espryt's missing-extension compile failure,
|
||||||
|
// Magma's pointSizeCapabilityUnsupported refusal) stay in charge of it.
|
||||||
|
struct PointSizeDemotionOutcome {
|
||||||
|
Bool demoted = false;
|
||||||
|
String declineDetail;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Demotes gl_PointSize across a WHOLE program's pre-rasterization chain into
|
||||||
|
// ordinary float varyings at one shared free location, so a device that
|
||||||
|
// advertises neither ES tessellation/geometry_point_size extension nor
|
||||||
|
// Vulkan's shaderTessellationAndGeometryPointSize can still run programs
|
||||||
|
// whose tessellation/geometry stages merely CARRY the value (transform
|
||||||
|
// feedback and gl_in[].gl_PointSize reads). Runs after
|
||||||
|
// SanitizeAndOptimizeBinary, on the final shared modules both backends
|
||||||
|
// consume, and is atomic per program: every stage is rewritten or none is,
|
||||||
|
// because a consumer whose producer kept the built-in would read garbage.
|
||||||
|
// `demoteTessellation` / `demoteGeometry` are the env verdicts (the device
|
||||||
|
// LACKS that capability); the per-program half of the decision - whether any
|
||||||
|
// module actually declares TessellationPointSize / GeometryPointSize - is
|
||||||
|
// probed here. `captureRequestsPointSize` forces the capture-capable last
|
||||||
|
// stage to declare its carrier even when it never writes the built-in, so a
|
||||||
|
// by-name capture always has something to bind to. Returns false only when
|
||||||
|
// the optimizer itself failed (modules untouched); a shape decline is
|
||||||
|
// reported through `outcome` and also leaves the modules untouched. See
|
||||||
|
// DemotePointSizePass for the per-module rewrite and its honest residue.
|
||||||
|
//
|
||||||
|
// Two declines are PROGRAM-shaped and therefore live here rather than in the
|
||||||
|
// pass: a carrier that would land past the minimum-spec varying budget, and
|
||||||
|
// an evaluation stage reading gl_in point size with NO control stage - the
|
||||||
|
// synthesized pass-through control stage both backends stand in that gap
|
||||||
|
// forwards gl_Position alone, so the input carrier would strand the value and
|
||||||
|
// trip the backends' own "reads a located input" refusal against a name the
|
||||||
|
// application never wrote.
|
||||||
|
static Bool DemoteTessellationGeometryPointSizeForProgram(
|
||||||
|
Vector<Vector<Uint32>>& modules, const Vector<GLenum>& shaderTypes,
|
||||||
|
Bool demoteTessellation, Bool demoteGeometry, Bool captureRequestsPointSize,
|
||||||
|
PointSizeDemotionOutcome& outcome, bool validateOutput = true,
|
||||||
|
bool enableSpirvValidation = false);
|
||||||
|
|
||||||
// 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
|
||||||
// module (see its header for the two operations that make it decline), which is
|
// module (see its header for the two operations that make it decline), which is
|
||||||
|
|||||||
@@ -0,0 +1,805 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemotePointSizePass.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
|
||||||
|
|
||||||
|
#include "DemotePointSizePass.h"
|
||||||
|
|
||||||
|
#include "spirv.hpp"
|
||||||
|
#include "source/opt/constants.h"
|
||||||
|
#include "source/opt/def_use_manager.h"
|
||||||
|
#include "source/opt/instruction.h"
|
||||||
|
#include "source/opt/ir_context.h"
|
||||||
|
#include "source/opt/module.h"
|
||||||
|
#include "source/opt/type_manager.h"
|
||||||
|
#include "source/opt/types.h"
|
||||||
|
#include "source/util/make_unique.h"
|
||||||
|
#include "source/util/string_utils.h"
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
namespace {
|
||||||
|
using spvtools::opt::Instruction;
|
||||||
|
using spvtools::opt::IRContext;
|
||||||
|
using spvtools::opt::Operand;
|
||||||
|
namespace analysis = spvtools::opt::analysis;
|
||||||
|
|
||||||
|
spv::ExecutionModel EntryExecutionModel(IRContext* ctx) {
|
||||||
|
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||||
|
return static_cast<spv::ExecutionModel>(ep.GetSingleWordInOperand(0));
|
||||||
|
}
|
||||||
|
return spv::ExecutionModel::Max;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instruction* EntryPoint(IRContext* ctx) {
|
||||||
|
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||||
|
return &ep;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpTypePointer <storage-class> <pointee>
|
||||||
|
uint32_t VariablePointeeType(IRContext* ctx, Instruction* var) {
|
||||||
|
Instruction* ptrType = ctx->get_def_use_mgr()->GetDef(var->type_id());
|
||||||
|
if (ptrType == nullptr || ptrType->opcode() != spv::Op::OpTypePointer) return 0;
|
||||||
|
return ptrType->GetSingleWordInOperand(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsFloat32Type(IRContext* ctx, uint32_t typeId) {
|
||||||
|
Instruction* t = ctx->get_def_use_mgr()->GetDef(typeId);
|
||||||
|
return t != nullptr && t->opcode() == spv::Op::OpTypeFloat &&
|
||||||
|
t->NumInOperands() >= 1 && t->GetSingleWordInOperand(0) == 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The value of a plain 32-bit OpConstant, or false (spec constants and anything
|
||||||
|
// else make the caller decline rather than guess).
|
||||||
|
bool PlainConstantValue(IRContext* ctx, uint32_t id, uint32_t& outValue) {
|
||||||
|
Instruction* def = ctx->get_def_use_mgr()->GetDef(id);
|
||||||
|
if (def == nullptr || def->opcode() != spv::Op::OpConstant) return false;
|
||||||
|
if (def->NumInOperands() != 1) return false;
|
||||||
|
outValue = def->GetSingleWordInOperand(0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t Float32Type(IRContext* ctx) {
|
||||||
|
analysis::Float f(32);
|
||||||
|
return ctx->get_type_mgr()->GetTypeInstruction(&f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// An OpTypeArray of float32 with the given length constant, reusing an existing
|
||||||
|
// declaration when one exists.
|
||||||
|
uint32_t ArrayOfFloat32Type(IRContext* ctx, uint32_t lengthConstId, uint32_t lengthValue) {
|
||||||
|
analysis::Float f(32);
|
||||||
|
analysis::Type* floatReg = ctx->get_type_mgr()->GetRegisteredType(&f);
|
||||||
|
const analysis::Array::LengthInfo lengthInfo{
|
||||||
|
lengthConstId,
|
||||||
|
{static_cast<uint32_t>(analysis::Array::LengthInfo::kConstant), lengthValue}};
|
||||||
|
analysis::Array arr(floatReg, lengthInfo);
|
||||||
|
return ctx->get_type_mgr()->GetTypeInstruction(&arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddNameFor(IRContext* ctx, uint32_t id, const String& name) {
|
||||||
|
std::vector<Operand> operands;
|
||||||
|
operands.push_back({SPV_OPERAND_TYPE_ID, {id}});
|
||||||
|
operands.push_back(
|
||||||
|
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)});
|
||||||
|
ctx->AddDebug2Inst(
|
||||||
|
spvtools::MakeUnique<Instruction>(ctx, spv::Op::OpName, 0, 0, operands));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddLocationDecoration(IRContext* ctx, uint32_t id, uint32_t location) {
|
||||||
|
ctx->AddAnnotationInst(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpDecorate, 0, 0,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_ID, {id}},
|
||||||
|
{SPV_OPERAND_TYPE_DECORATION,
|
||||||
|
{static_cast<uint32_t>(spv::Decoration::Location)}},
|
||||||
|
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {location}}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A fresh interface variable: declared, named, located, listed on the entry
|
||||||
|
// point, and registered with the def-use manager so ReplaceAllUsesWith may name
|
||||||
|
// it before the end-of-pass invalidation.
|
||||||
|
uint32_t CreateCarrierVariable(IRContext* ctx, Instruction* entryPoint, uint32_t pointeeTypeId,
|
||||||
|
spv::StorageClass storage, const String& name,
|
||||||
|
uint32_t location) {
|
||||||
|
const uint32_t ptrTypeId = ctx->get_type_mgr()->FindPointerToType(pointeeTypeId, storage);
|
||||||
|
if (ptrTypeId == 0) return 0;
|
||||||
|
const uint32_t varId = ctx->TakeNextId();
|
||||||
|
auto var = spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpVariable, ptrTypeId, varId,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast<uint32_t>(storage)}}});
|
||||||
|
Instruction* varInst = var.get();
|
||||||
|
ctx->AddGlobalValue(std::move(var));
|
||||||
|
ctx->get_def_use_mgr()->AnalyzeInstDefUse(varInst);
|
||||||
|
AddNameFor(ctx, varId, name);
|
||||||
|
AddLocationDecoration(ctx, varId, location);
|
||||||
|
entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {varId}});
|
||||||
|
return varId;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
spvtools::opt::Pass::Status DemotePointSizePass::Process() {
|
||||||
|
auto* ctx = context();
|
||||||
|
auto* defUse = ctx->get_def_use_mgr();
|
||||||
|
const spv::ExecutionModel model = EntryExecutionModel(ctx);
|
||||||
|
Instruction* entryPoint = EntryPoint(ctx);
|
||||||
|
if (entryPoint == nullptr) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
const bool isVertex = model == spv::ExecutionModel::Vertex;
|
||||||
|
const bool isTessControl = model == spv::ExecutionModel::TessellationControl;
|
||||||
|
const bool isTessEval = model == spv::ExecutionModel::TessellationEvaluation;
|
||||||
|
const bool isGeometry = model == spv::ExecutionModel::Geometry;
|
||||||
|
if (!isVertex && !isTessControl && !isTessEval && !isGeometry) {
|
||||||
|
return Status::SuccessWithoutChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto decline = [&](String reason) {
|
||||||
|
if (m_report != nullptr) {
|
||||||
|
m_report->declined = true;
|
||||||
|
m_report->declineReason = Move(reason);
|
||||||
|
}
|
||||||
|
return Status::SuccessWithoutChange;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- discovery: where does PointSize live in this module ------------------
|
||||||
|
// Member form: every struct type with a member decorated BuiltIn PointSize.
|
||||||
|
struct MemberSite {
|
||||||
|
uint32_t structId = 0;
|
||||||
|
uint32_t memberIndex = 0;
|
||||||
|
};
|
||||||
|
std::vector<MemberSite> memberSites;
|
||||||
|
// Standalone form: a variable decorated BuiltIn PointSize directly.
|
||||||
|
std::vector<Instruction*> standaloneVars;
|
||||||
|
std::vector<Instruction*> standaloneBuiltInDecorations;
|
||||||
|
// Where clip and cull distance live, by (struct, member). DECLARATION is not
|
||||||
|
// the question - glslang emits the whole four-member gl_PerVertex block into
|
||||||
|
// every stage, touched or not - so these sites are only the starting point
|
||||||
|
// for the ACCESS scan the control-stage decline below performs, which is the
|
||||||
|
// same thing SPIRV-Cross's own clip_distance_count counts.
|
||||||
|
std::vector<MemberSite> clipCullSites;
|
||||||
|
std::vector<Instruction*> standaloneClipCullVars;
|
||||||
|
const auto isClipOrCull = [](const uint32_t builtIn) {
|
||||||
|
return static_cast<spv::BuiltIn>(builtIn) == spv::BuiltIn::ClipDistance ||
|
||||||
|
static_cast<spv::BuiltIn>(builtIn) == spv::BuiltIn::CullDistance;
|
||||||
|
};
|
||||||
|
for (Instruction& ann : ctx->annotations()) {
|
||||||
|
if (ann.opcode() == spv::Op::OpMemberDecorate && ann.NumInOperands() >= 4 &&
|
||||||
|
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) ==
|
||||||
|
spv::Decoration::BuiltIn) {
|
||||||
|
if (static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(3)) ==
|
||||||
|
spv::BuiltIn::PointSize) {
|
||||||
|
memberSites.push_back(
|
||||||
|
{ann.GetSingleWordInOperand(0), ann.GetSingleWordInOperand(1)});
|
||||||
|
} else if (isClipOrCull(ann.GetSingleWordInOperand(3))) {
|
||||||
|
clipCullSites.push_back(
|
||||||
|
{ann.GetSingleWordInOperand(0), ann.GetSingleWordInOperand(1)});
|
||||||
|
}
|
||||||
|
} else if (ann.opcode() == spv::Op::OpDecorate && ann.NumInOperands() >= 3 &&
|
||||||
|
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) ==
|
||||||
|
spv::Decoration::BuiltIn) {
|
||||||
|
if (static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(2)) ==
|
||||||
|
spv::BuiltIn::PointSize) {
|
||||||
|
Instruction* var = defUse->GetDef(ann.GetSingleWordInOperand(0));
|
||||||
|
if (var != nullptr && var->opcode() == spv::Op::OpVariable) {
|
||||||
|
standaloneVars.push_back(var);
|
||||||
|
standaloneBuiltInDecorations.push_back(&ann);
|
||||||
|
}
|
||||||
|
} else if (isClipOrCull(ann.GetSingleWordInOperand(2))) {
|
||||||
|
Instruction* var = defUse->GetDef(ann.GetSingleWordInOperand(0));
|
||||||
|
if (var != nullptr && var->opcode() == spv::Op::OpVariable) {
|
||||||
|
standaloneClipCullVars.push_back(var);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto memberIndexIn = [&](uint32_t structId, uint32_t& outMember) {
|
||||||
|
for (const MemberSite& site : memberSites) {
|
||||||
|
if (site.structId == structId) {
|
||||||
|
outMember = site.memberIndex;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The gl_PerVertex-shaped interface variables: Input/Output variables whose
|
||||||
|
// pointee is (an array of) a struct carrying a PointSize member.
|
||||||
|
struct BlockVar {
|
||||||
|
Instruction* var = nullptr;
|
||||||
|
spv::StorageClass storage = spv::StorageClass::Output;
|
||||||
|
bool arrayed = false;
|
||||||
|
uint32_t arrayLengthConstId = 0;
|
||||||
|
uint32_t arrayLengthValue = 0;
|
||||||
|
uint32_t memberIndex = 0;
|
||||||
|
};
|
||||||
|
std::vector<BlockVar> blockVars;
|
||||||
|
for (Instruction& inst : ctx->module()->types_values()) {
|
||||||
|
if (inst.opcode() != spv::Op::OpVariable) continue;
|
||||||
|
const auto storage = static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0));
|
||||||
|
if (storage != spv::StorageClass::Input && storage != spv::StorageClass::Output) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
uint32_t pointeeId = VariablePointeeType(ctx, &inst);
|
||||||
|
if (pointeeId == 0) continue;
|
||||||
|
Instruction* pointee = defUse->GetDef(pointeeId);
|
||||||
|
if (pointee == nullptr) continue;
|
||||||
|
BlockVar entry;
|
||||||
|
entry.var = &inst;
|
||||||
|
entry.storage = storage;
|
||||||
|
if (pointee->opcode() == spv::Op::OpTypeArray) {
|
||||||
|
entry.arrayed = true;
|
||||||
|
entry.arrayLengthConstId = pointee->GetSingleWordInOperand(1);
|
||||||
|
if (!PlainConstantValue(ctx, entry.arrayLengthConstId, entry.arrayLengthValue)) {
|
||||||
|
continue; // spec-constant-sized interface array: not glslang's shape
|
||||||
|
}
|
||||||
|
pointee = defUse->GetDef(pointee->GetSingleWordInOperand(0));
|
||||||
|
if (pointee == nullptr) continue;
|
||||||
|
}
|
||||||
|
if (pointee->opcode() != spv::Op::OpTypeStruct) continue;
|
||||||
|
if (!memberIndexIn(pointee->result_id(), entry.memberIndex)) continue;
|
||||||
|
blockVars.push_back(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- vertex stage: mirror, never demote -----------------------------------
|
||||||
|
if (isVertex) {
|
||||||
|
if (!m_options.forceOutputCarrier) return Status::SuccessWithoutChange;
|
||||||
|
if (m_options.outputCarrierName.empty()) {
|
||||||
|
return decline("vertex mirror requested without a carrier name");
|
||||||
|
}
|
||||||
|
const uint32_t floatTypeId = Float32Type(ctx);
|
||||||
|
// The source of the mirrored value: the output block's PointSize member,
|
||||||
|
// a standalone output variable, or - with neither declared - the constant
|
||||||
|
// 1.0 GL's default point size names.
|
||||||
|
Instruction* blockVar = nullptr;
|
||||||
|
uint32_t memberIndex = 0;
|
||||||
|
for (const BlockVar& candidate : blockVars) {
|
||||||
|
if (candidate.storage == spv::StorageClass::Output && !candidate.arrayed) {
|
||||||
|
blockVar = candidate.var;
|
||||||
|
memberIndex = candidate.memberIndex;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Instruction* standaloneOut = nullptr;
|
||||||
|
for (Instruction* candidate : standaloneVars) {
|
||||||
|
if (static_cast<spv::StorageClass>(candidate->GetSingleWordInOperand(0)) ==
|
||||||
|
spv::StorageClass::Output &&
|
||||||
|
IsFloat32Type(ctx, VariablePointeeType(ctx, candidate))) {
|
||||||
|
standaloneOut = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t carrierId =
|
||||||
|
CreateCarrierVariable(ctx, entryPoint, floatTypeId, spv::StorageClass::Output,
|
||||||
|
m_options.outputCarrierName, m_options.location);
|
||||||
|
if (carrierId == 0) return decline("could not declare the vertex mirror carrier");
|
||||||
|
|
||||||
|
uint32_t memberConstId = 0;
|
||||||
|
uint32_t ptrOutputFloatId = 0;
|
||||||
|
if (blockVar != nullptr) {
|
||||||
|
memberConstId = ctx->get_constant_mgr()->GetSIntConstId(
|
||||||
|
static_cast<int32_t>(memberIndex));
|
||||||
|
ptrOutputFloatId =
|
||||||
|
ctx->get_type_mgr()->FindPointerToType(floatTypeId, spv::StorageClass::Output);
|
||||||
|
if (ptrOutputFloatId == 0) return decline("no Output float pointer type");
|
||||||
|
}
|
||||||
|
uint32_t defaultOneId = 0;
|
||||||
|
if (blockVar == nullptr && standaloneOut == nullptr) {
|
||||||
|
defaultOneId = ctx->get_constant_mgr()->GetFloatConstId(1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t entryFunctionId = entryPoint->GetSingleWordInOperand(1);
|
||||||
|
bool mirrored = false;
|
||||||
|
for (auto funcIt = ctx->module()->begin(); funcIt != ctx->module()->end(); ++funcIt) {
|
||||||
|
if (funcIt->result_id() != entryFunctionId) continue;
|
||||||
|
funcIt->ForEachInst([&](Instruction* inst) {
|
||||||
|
if (inst->opcode() != spv::Op::OpReturn &&
|
||||||
|
inst->opcode() != spv::Op::OpReturnValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t valueId = 0;
|
||||||
|
if (blockVar != nullptr) {
|
||||||
|
const uint32_t chainId = ctx->TakeNextId();
|
||||||
|
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpAccessChain, ptrOutputFloatId, chainId,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_ID, {blockVar->result_id()}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {memberConstId}}}));
|
||||||
|
valueId = ctx->TakeNextId();
|
||||||
|
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpLoad, floatTypeId, valueId,
|
||||||
|
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {chainId}}}));
|
||||||
|
} else if (standaloneOut != nullptr) {
|
||||||
|
valueId = ctx->TakeNextId();
|
||||||
|
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpLoad, floatTypeId, valueId,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_ID, {standaloneOut->result_id()}}}));
|
||||||
|
} else {
|
||||||
|
valueId = defaultOneId;
|
||||||
|
}
|
||||||
|
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpStore, 0, 0,
|
||||||
|
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {carrierId}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {valueId}}}));
|
||||||
|
mirrored = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!mirrored) {
|
||||||
|
// An entry function with no return is not a module glslang produces;
|
||||||
|
// the carrier stays declared (the consumer's read is undefined, as an
|
||||||
|
// unwritten built-in's would have been).
|
||||||
|
}
|
||||||
|
ctx->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||||
|
return Status::SuccessWithChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- tessellation / geometry: redirect and strip --------------------------
|
||||||
|
// Phase 1: ANALYSIS ONLY. Every plan is collected before anything mutates, so
|
||||||
|
// a decline leaves the module byte-identical.
|
||||||
|
struct ArrayedRedirect {
|
||||||
|
Instruction* chain = nullptr;
|
||||||
|
bool input = false;
|
||||||
|
};
|
||||||
|
std::vector<ArrayedRedirect> arrayedRedirects; // gl_in[i].ps / gl_out[i].ps
|
||||||
|
std::vector<Instruction*> scalarOutputChains; // non-arrayed out block's member
|
||||||
|
const BlockVar* arrayedInput = nullptr;
|
||||||
|
const BlockVar* arrayedOutput = nullptr;
|
||||||
|
|
||||||
|
for (const BlockVar& blockVar : blockVars) {
|
||||||
|
if (blockVar.arrayed) {
|
||||||
|
if (blockVar.storage == spv::StorageClass::Input) {
|
||||||
|
arrayedInput = &blockVar;
|
||||||
|
} else {
|
||||||
|
arrayedOutput = &blockVar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool declined = false;
|
||||||
|
String reason;
|
||||||
|
defUse->ForEachUser(blockVar.var, [&](Instruction* user) {
|
||||||
|
if (declined) return;
|
||||||
|
switch (user->opcode()) {
|
||||||
|
case spv::Op::OpEntryPoint:
|
||||||
|
case spv::Op::OpName:
|
||||||
|
case spv::Op::OpDecorate:
|
||||||
|
return;
|
||||||
|
case spv::Op::OpAccessChain:
|
||||||
|
case spv::Op::OpInBoundsAccessChain: {
|
||||||
|
const uint32_t indexCount = user->NumInOperands() - 1;
|
||||||
|
if (!blockVar.arrayed) {
|
||||||
|
if (indexCount < 1) {
|
||||||
|
declined = true;
|
||||||
|
reason = "an index-less pointer to the whole gl_PerVertex block";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t member = 0;
|
||||||
|
if (!PlainConstantValue(ctx, user->GetSingleWordInOperand(1), member)) {
|
||||||
|
declined = true;
|
||||||
|
reason = "a non-constant gl_PerVertex member index";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (member != blockVar.memberIndex) return; // another member
|
||||||
|
if (indexCount != 1) {
|
||||||
|
declined = true;
|
||||||
|
reason = "an access chain that continues past the PointSize member";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scalarOutputChains.push_back(user);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Arrayed (gl_in / gl_out): [vertex, member, ...].
|
||||||
|
if (indexCount < 2) {
|
||||||
|
// A pointer that stops at the whole per-vertex struct can still
|
||||||
|
// reach PointSize through a second chain; following that split
|
||||||
|
// is not worth the shapes it would have to prove absent.
|
||||||
|
bool touchesPointSize = false;
|
||||||
|
defUse->ForEachUser(user, [&](Instruction* chainUser) {
|
||||||
|
if ((chainUser->opcode() == spv::Op::OpAccessChain ||
|
||||||
|
chainUser->opcode() == spv::Op::OpInBoundsAccessChain) &&
|
||||||
|
chainUser->NumInOperands() >= 2) {
|
||||||
|
uint32_t member = 0;
|
||||||
|
if (PlainConstantValue(ctx, chainUser->GetSingleWordInOperand(1),
|
||||||
|
member) &&
|
||||||
|
member == blockVar.memberIndex) {
|
||||||
|
touchesPointSize = true;
|
||||||
|
}
|
||||||
|
} else if (chainUser->opcode() == spv::Op::OpLoad ||
|
||||||
|
chainUser->opcode() == spv::Op::OpStore ||
|
||||||
|
chainUser->opcode() == spv::Op::OpCopyMemory) {
|
||||||
|
touchesPointSize = true; // whole-struct copy
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (touchesPointSize) {
|
||||||
|
declined = true;
|
||||||
|
reason = "a split access chain or whole-struct copy reaching PointSize";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t member = 0;
|
||||||
|
if (!PlainConstantValue(ctx, user->GetSingleWordInOperand(2), member)) {
|
||||||
|
declined = true;
|
||||||
|
reason = "a non-constant gl_PerVertex member index";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (member != blockVar.memberIndex) return; // another member
|
||||||
|
if (indexCount != 2) {
|
||||||
|
declined = true;
|
||||||
|
reason = "an access chain that continues past the PointSize member";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
arrayedRedirects.push_back(
|
||||||
|
{user, blockVar.storage == spv::StorageClass::Input});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case spv::Op::OpLoad:
|
||||||
|
case spv::Op::OpStore:
|
||||||
|
case spv::Op::OpCopyMemory:
|
||||||
|
declined = true;
|
||||||
|
reason = "a whole-aggregate load/store/copy of the gl_PerVertex interface";
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
declined = true;
|
||||||
|
reason = std::format("SPIR-V opcode {} reaching the gl_PerVertex interface",
|
||||||
|
static_cast<uint32_t>(user->opcode()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (declined) return decline(Move(reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standalone variables: swapping the decoration is only sound for the float /
|
||||||
|
// float-array shapes the built-in is allowed to have; mixing forms in one
|
||||||
|
// direction never comes out of glslang and declines.
|
||||||
|
struct StandaloneSwap {
|
||||||
|
Instruction* var = nullptr;
|
||||||
|
Instruction* builtInDecoration = nullptr;
|
||||||
|
bool input = false;
|
||||||
|
};
|
||||||
|
std::vector<StandaloneSwap> standaloneSwaps;
|
||||||
|
for (SizeT i = 0; i < standaloneVars.size(); ++i) {
|
||||||
|
Instruction* var = standaloneVars[i];
|
||||||
|
const auto storage = static_cast<spv::StorageClass>(var->GetSingleWordInOperand(0));
|
||||||
|
if (storage != spv::StorageClass::Input && storage != spv::StorageClass::Output) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const bool input = storage == spv::StorageClass::Input;
|
||||||
|
uint32_t pointeeId = VariablePointeeType(ctx, var);
|
||||||
|
Instruction* pointee = defUse->GetDef(pointeeId);
|
||||||
|
if (pointee != nullptr && pointee->opcode() == spv::Op::OpTypeArray) {
|
||||||
|
pointee = defUse->GetDef(pointee->GetSingleWordInOperand(0));
|
||||||
|
}
|
||||||
|
if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeFloat) {
|
||||||
|
return decline("a standalone PointSize variable of an unexpected type");
|
||||||
|
}
|
||||||
|
if (input && arrayedInput != nullptr) {
|
||||||
|
return decline("PointSize declared both as a block member and standalone (input)");
|
||||||
|
}
|
||||||
|
if (!input && (arrayedOutput != nullptr || !scalarOutputChains.empty())) {
|
||||||
|
return decline("PointSize declared both as a block member and standalone (output)");
|
||||||
|
}
|
||||||
|
standaloneSwaps.push_back({var, standaloneBuiltInDecorations[i], input});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool needsInputCarrier = false;
|
||||||
|
bool needsOutputCarrier = m_options.forceOutputCarrier;
|
||||||
|
for (const ArrayedRedirect& redirect : arrayedRedirects) {
|
||||||
|
(redirect.input ? needsInputCarrier : needsOutputCarrier) = true;
|
||||||
|
// Only a control stage has an ARRAYED output block; anywhere else this
|
||||||
|
// shape would hand a scalar carrier an extra index.
|
||||||
|
if (!redirect.input && !isTessControl) {
|
||||||
|
return decline("an arrayed PointSize output outside a control stage");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!scalarOutputChains.empty()) {
|
||||||
|
needsOutputCarrier = true;
|
||||||
|
// And only evaluation/geometry stages have the non-arrayed output block.
|
||||||
|
if (isTessControl) {
|
||||||
|
return decline("a non-arrayed PointSize output in a control stage");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool standaloneInputSwapped = false;
|
||||||
|
bool standaloneOutputSwapped = false;
|
||||||
|
for (const StandaloneSwap& swap : standaloneSwaps) {
|
||||||
|
(swap.input ? standaloneInputSwapped : standaloneOutputSwapped) = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsInputCarrier && arrayedInput == nullptr) {
|
||||||
|
return decline("a PointSize read with no arrayed input block to size the carrier by");
|
||||||
|
}
|
||||||
|
if (needsInputCarrier && m_options.inputCarrierName.empty()) {
|
||||||
|
return decline("a PointSize read with no input carrier name to bind it to");
|
||||||
|
}
|
||||||
|
if ((needsOutputCarrier && !standaloneOutputSwapped) &&
|
||||||
|
m_options.outputCarrierName.empty()) {
|
||||||
|
return decline("a PointSize write with no output carrier name to bind it to");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A FORCED carrier with nothing redirected onto it is a carrier no instruction
|
||||||
|
// ever writes - and a declared-but-unwritten output does not survive the ES
|
||||||
|
// hop: the driver's GLSL front end drops it, and a transform-feedback request
|
||||||
|
// naming it then fails the link with "varying undeclared", taking every other
|
||||||
|
// capture in the set down with it. The pass therefore SEEDS such a carrier
|
||||||
|
// (below) with GL's default point size, which is also what an unhosted
|
||||||
|
// built-in rasterizes at. GL leaves the value of an unwritten output
|
||||||
|
// undefined, so a defined 1.0 is a legal choice and a far better one than a
|
||||||
|
// varying the driver deletes.
|
||||||
|
const bool outputCarrierHasWriter = [&] {
|
||||||
|
if (!scalarOutputChains.empty()) return true;
|
||||||
|
for (const ArrayedRedirect& redirect : arrayedRedirects) {
|
||||||
|
if (!redirect.input) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}();
|
||||||
|
// The seed the control stage would need is per-INVOCATION - gl_out[
|
||||||
|
// gl_InvocationID] - and synthesizing that means inventing the InvocationId
|
||||||
|
// built-in for a stage that may not declare it. A control stage asked to
|
||||||
|
// produce a value it never computes is also a program reading undefined data
|
||||||
|
// either way, so this declines rather than growing the pass for it.
|
||||||
|
if (needsOutputCarrier && !standaloneOutputSwapped && !outputCarrierHasWriter &&
|
||||||
|
isTessControl) {
|
||||||
|
return decline("a control stage asked to produce a point-size carrier it never writes");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The TCS output carrier is arrayed per vertex; its length comes from gl_out,
|
||||||
|
// or - for a forced carrier in a control stage that never declared gl_out -
|
||||||
|
// from the OutputVertices execution mode.
|
||||||
|
uint32_t outputArrayLengthConstId = 0;
|
||||||
|
uint32_t outputArrayLengthValue = 0;
|
||||||
|
if (isTessControl && needsOutputCarrier && !standaloneOutputSwapped) {
|
||||||
|
if (arrayedOutput != nullptr) {
|
||||||
|
outputArrayLengthConstId = arrayedOutput->arrayLengthConstId;
|
||||||
|
outputArrayLengthValue = arrayedOutput->arrayLengthValue;
|
||||||
|
} else {
|
||||||
|
for (Instruction& mode : ctx->module()->execution_modes()) {
|
||||||
|
if (mode.NumInOperands() >= 3 &&
|
||||||
|
static_cast<spv::ExecutionMode>(mode.GetSingleWordInOperand(1)) ==
|
||||||
|
spv::ExecutionMode::OutputVertices) {
|
||||||
|
outputArrayLengthValue = mode.GetSingleWordInOperand(2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (outputArrayLengthValue == 0) {
|
||||||
|
return decline("a control stage with neither gl_out nor OutputVertices");
|
||||||
|
}
|
||||||
|
outputArrayLengthConstId =
|
||||||
|
ctx->get_constant_mgr()->GetUIntConstId(outputArrayLengthValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool anyWork = needsInputCarrier || needsOutputCarrier ||
|
||||||
|
!standaloneSwaps.empty();
|
||||||
|
// Even with no access left to redirect (a dead read the sanitize chain already
|
||||||
|
// removed), a declared TessellationPointSize/GeometryPointSize capability must
|
||||||
|
// still be stripped - it alone makes the module unbuildable on the device.
|
||||||
|
std::vector<Instruction*> capabilitiesToStrip;
|
||||||
|
for (Instruction& capability : ctx->module()->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) {
|
||||||
|
capabilitiesToStrip.push_back(&capability);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!anyWork && capabilitiesToStrip.empty()) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
// THE ONE SHAPE WHERE "declared but unaccessed" IS NOT ENOUGH. SPIRV-Cross
|
||||||
|
// force-emits the whole redeclared gl_PerVertex OUTPUT block for a control
|
||||||
|
// stage whose clip or cull distances are LIVE (should_force_emit_builtin_block,
|
||||||
|
// spirv_glsl.cpp), and that emission is driven by the struct's member
|
||||||
|
// DECORATIONS, never by access - so it prints "float gl_PointSize;" into a
|
||||||
|
// block no instruction touches any more. On the extension-less ES drivers this
|
||||||
|
// pass exists for, that redeclaration is exactly as illegal as the access was,
|
||||||
|
// so the demoted program would still be lost - only now with the capability
|
||||||
|
// stripped, the program-wide verdict flipped and a diagnostic naming a
|
||||||
|
// built-in the module no longer mentions. Declining keeps the honest refusal,
|
||||||
|
// and keeps the header's claim true for every shape that IS demoted.
|
||||||
|
//
|
||||||
|
// LIVE, not declared: glslang emits the whole four-member gl_PerVertex block
|
||||||
|
// into every stage whether or not it is touched, and SPIRV-Cross counts clip
|
||||||
|
// and cull distance from ACCESSES (Compiler::ActiveBuiltinHandler). Keying
|
||||||
|
// this on the decorations alone would decline every control stage there is.
|
||||||
|
// Only a block-MEMBER PointSize can be left behind to be printed; a standalone
|
||||||
|
// variable leaves with the demotion.
|
||||||
|
if (isTessControl && !blockVars.empty()) {
|
||||||
|
bool clipOrCullIsLive = false;
|
||||||
|
const auto memberIsClipOrCull = [&](const uint32_t structId,
|
||||||
|
const uint32_t member) {
|
||||||
|
for (const MemberSite& site : clipCullSites) {
|
||||||
|
if (site.structId == structId && site.memberIndex == member) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
for (Instruction& inst : ctx->module()->types_values()) {
|
||||||
|
if (clipOrCullIsLive) break;
|
||||||
|
if (inst.opcode() != spv::Op::OpVariable) continue;
|
||||||
|
const auto storage =
|
||||||
|
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0));
|
||||||
|
if (storage != spv::StorageClass::Input &&
|
||||||
|
storage != spv::StorageClass::Output) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// A standalone clip/cull variable counts the moment anything but its
|
||||||
|
// own declaration touches it.
|
||||||
|
bool standaloneClipCull = false;
|
||||||
|
for (Instruction* candidate : standaloneClipCullVars) {
|
||||||
|
if (candidate == &inst) standaloneClipCull = true;
|
||||||
|
}
|
||||||
|
Instruction* pointee = defUse->GetDef(VariablePointeeType(ctx, &inst));
|
||||||
|
bool arrayed = false;
|
||||||
|
if (pointee != nullptr && pointee->opcode() == spv::Op::OpTypeArray) {
|
||||||
|
arrayed = true;
|
||||||
|
pointee = defUse->GetDef(pointee->GetSingleWordInOperand(0));
|
||||||
|
}
|
||||||
|
const bool blockCarriesClipCull =
|
||||||
|
pointee != nullptr && pointee->opcode() == spv::Op::OpTypeStruct &&
|
||||||
|
[&] {
|
||||||
|
for (const MemberSite& site : clipCullSites) {
|
||||||
|
if (site.structId == pointee->result_id()) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}();
|
||||||
|
if (!standaloneClipCull && !blockCarriesClipCull) continue;
|
||||||
|
const uint32_t structId = blockCarriesClipCull ? pointee->result_id() : 0;
|
||||||
|
const uint32_t memberOperand = arrayed ? 2u : 1u;
|
||||||
|
defUse->ForEachUser(&inst, [&](Instruction* user) {
|
||||||
|
if (clipOrCullIsLive) return;
|
||||||
|
switch (user->opcode()) {
|
||||||
|
case spv::Op::OpEntryPoint:
|
||||||
|
case spv::Op::OpName:
|
||||||
|
case spv::Op::OpMemberName:
|
||||||
|
case spv::Op::OpDecorate:
|
||||||
|
case spv::Op::OpMemberDecorate:
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (standaloneClipCull) {
|
||||||
|
clipOrCullIsLive = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (user->opcode() != spv::Op::OpAccessChain &&
|
||||||
|
user->opcode() != spv::Op::OpInBoundsAccessChain) {
|
||||||
|
// A whole-block load, copy or anything else that cannot be
|
||||||
|
// narrowed reaches every member, clip distance included.
|
||||||
|
clipOrCullIsLive = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t member = 0;
|
||||||
|
if (user->NumInOperands() <= memberOperand ||
|
||||||
|
!PlainConstantValue(ctx, user->GetSingleWordInOperand(memberOperand),
|
||||||
|
member)) {
|
||||||
|
clipOrCullIsLive = true; // cannot prove it misses clip/cull
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (memberIsClipOrCull(structId, member)) clipOrCullIsLive = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (clipOrCullIsLive) {
|
||||||
|
for (const BlockVar& blockVar : blockVars) {
|
||||||
|
if (blockVar.storage == spv::StorageClass::Output) {
|
||||||
|
return decline(
|
||||||
|
"a control stage with live clip/cull distance, whose "
|
||||||
|
"redeclared output block would still print gl_PointSize");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: MUTATION. Nothing below may decline.
|
||||||
|
const uint32_t floatTypeId = Float32Type(ctx);
|
||||||
|
uint32_t inputCarrierId = 0;
|
||||||
|
if (needsInputCarrier) {
|
||||||
|
const uint32_t arrayTypeId = ArrayOfFloat32Type(
|
||||||
|
ctx, arrayedInput->arrayLengthConstId, arrayedInput->arrayLengthValue);
|
||||||
|
inputCarrierId =
|
||||||
|
CreateCarrierVariable(ctx, entryPoint, arrayTypeId, spv::StorageClass::Input,
|
||||||
|
m_options.inputCarrierName, m_options.location);
|
||||||
|
}
|
||||||
|
uint32_t outputCarrierId = 0;
|
||||||
|
if (needsOutputCarrier && !standaloneOutputSwapped) {
|
||||||
|
uint32_t pointeeTypeId = floatTypeId;
|
||||||
|
if (isTessControl) {
|
||||||
|
pointeeTypeId =
|
||||||
|
ArrayOfFloat32Type(ctx, outputArrayLengthConstId, outputArrayLengthValue);
|
||||||
|
}
|
||||||
|
outputCarrierId =
|
||||||
|
CreateCarrierVariable(ctx, entryPoint, pointeeTypeId, spv::StorageClass::Output,
|
||||||
|
m_options.outputCarrierName, m_options.location);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed a forced carrier nothing writes, so the ES hop keeps it (see the
|
||||||
|
// reasoning at outputCarrierHasWriter). In a GEOMETRY stage the store has to
|
||||||
|
// go before every EmitVertex, because that is when the outputs of one vertex
|
||||||
|
// are latched; anywhere else the ends of the entry function will do.
|
||||||
|
if (outputCarrierId != 0 && !outputCarrierHasWriter) {
|
||||||
|
const uint32_t defaultPointSizeId = ctx->get_constant_mgr()->GetFloatConstId(1.0f);
|
||||||
|
const uint32_t entryFunctionId = entryPoint->GetSingleWordInOperand(1);
|
||||||
|
std::vector<Instruction*> seedSites;
|
||||||
|
for (auto funcIt = ctx->module()->begin(); funcIt != ctx->module()->end();
|
||||||
|
++funcIt) {
|
||||||
|
if (funcIt->result_id() != entryFunctionId) continue;
|
||||||
|
funcIt->ForEachInst([&](Instruction* inst) {
|
||||||
|
const bool emit = inst->opcode() == spv::Op::OpEmitVertex ||
|
||||||
|
inst->opcode() == spv::Op::OpEmitStreamVertex;
|
||||||
|
const bool ret = inst->opcode() == spv::Op::OpReturn ||
|
||||||
|
inst->opcode() == spv::Op::OpReturnValue;
|
||||||
|
if (isGeometry ? emit : ret) seedSites.push_back(inst);
|
||||||
|
});
|
||||||
|
// A geometry stage with no EmitVertex emits nothing at all; seeding the
|
||||||
|
// ends of the function still keeps the varying alive for the capture.
|
||||||
|
if (isGeometry && seedSites.empty()) {
|
||||||
|
funcIt->ForEachInst([&](Instruction* inst) {
|
||||||
|
if (inst->opcode() == spv::Op::OpReturn ||
|
||||||
|
inst->opcode() == spv::Op::OpReturnValue) {
|
||||||
|
seedSites.push_back(inst);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Instruction* site : seedSites) {
|
||||||
|
site->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpStore, 0, 0,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_ID, {outputCarrierId}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {defaultPointSizeId}}}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar output chains first, while the def-use index still knows their uses.
|
||||||
|
for (Instruction* chain : scalarOutputChains) {
|
||||||
|
ctx->ReplaceAllUsesWith(chain->result_id(), outputCarrierId);
|
||||||
|
ctx->KillInst(chain);
|
||||||
|
}
|
||||||
|
// Arrayed chains are rewritten in place: same result id, same result type
|
||||||
|
// (pointer-to-float in the same storage class), one fewer index.
|
||||||
|
for (const ArrayedRedirect& redirect : arrayedRedirects) {
|
||||||
|
const uint32_t carrierId = redirect.input ? inputCarrierId : outputCarrierId;
|
||||||
|
const Operand vertexIndex = redirect.chain->GetInOperand(1);
|
||||||
|
redirect.chain->SetInOperands(Instruction::OperandList{
|
||||||
|
{SPV_OPERAND_TYPE_ID, {carrierId}}, vertexIndex});
|
||||||
|
}
|
||||||
|
// Standalone form: the variable becomes its own carrier.
|
||||||
|
for (const StandaloneSwap& swap : standaloneSwaps) {
|
||||||
|
ctx->KillInst(swap.builtInDecoration);
|
||||||
|
AddLocationDecoration(ctx, swap.var->result_id(), m_options.location);
|
||||||
|
std::vector<Instruction*> oldNames;
|
||||||
|
for (Instruction& debugInst : ctx->module()->debugs2()) {
|
||||||
|
if (debugInst.opcode() == spv::Op::OpName &&
|
||||||
|
debugInst.GetSingleWordInOperand(0) == swap.var->result_id()) {
|
||||||
|
oldNames.push_back(&debugInst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Instruction* oldName : oldNames) ctx->KillInst(oldName);
|
||||||
|
AddNameFor(ctx, swap.var->result_id(),
|
||||||
|
swap.input ? m_options.inputCarrierName : m_options.outputCarrierName);
|
||||||
|
}
|
||||||
|
for (Instruction* capability : capabilitiesToStrip) {
|
||||||
|
ctx->KillInst(capability);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_report != nullptr) {
|
||||||
|
m_report->createdInputCarrier = needsInputCarrier || standaloneInputSwapped;
|
||||||
|
}
|
||||||
|
ctx->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||||
|
return Status::SuccessWithChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
spvtools::Optimizer::PassToken DemotePointSizePass::CreateDemotePointSizePass(
|
||||||
|
DemotePointSizeOptions options, DemotePointSizeReport* report) {
|
||||||
|
return spvtools::Optimizer::PassToken(
|
||||||
|
MakeUnique<DemotePointSizePass>(Move(options), report));
|
||||||
|
}
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemotePointSizePass.h
|
||||||
|
// 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
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "source/opt/pass.h"
|
||||||
|
#include "spirv-tools/optimizer.hpp"
|
||||||
|
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
// Demotes gl_PointSize traffic in ONE tessellation or geometry module (or mirrors it
|
||||||
|
// out of a vertex module) into an ordinary inter-stage float varying, for devices
|
||||||
|
// that cannot host the built-in in those stages at all: no
|
||||||
|
// EXT/OES_tessellation_point_size / geometry_point_size on the ES driver, and
|
||||||
|
// shaderTessellationAndGeometryPointSize == VK_FALSE on the Vulkan one. Desktop GL
|
||||||
|
// treats the built-in as an ordinary per-vertex output, so the programs this rescues
|
||||||
|
// are legal GL - only the targets cannot spell them.
|
||||||
|
//
|
||||||
|
// What "demoted" means, precisely. In a tessellation/geometry stage every access
|
||||||
|
// chain that reaches the PointSize member of a gl_PerVertex block (gl_in[i]
|
||||||
|
// .gl_PointSize, gl_out[i].gl_PointSize, the non-arrayed output block's member) is
|
||||||
|
// redirected onto a plain float varying at the caller-chosen location - an arrayed
|
||||||
|
// Input for gl_in reads, an arrayed Output for TCS gl_out writes, a scalar Output
|
||||||
|
// for the TES/GS output - and the TessellationPointSize / GeometryPointSize
|
||||||
|
// capability is stripped. The gl_PerVertex STRUCT keeps its PointSize member,
|
||||||
|
// declared and decorated but no longer accessed: that is exactly the shape glslang
|
||||||
|
// produces for a program that never touches point size (it defers the capability to
|
||||||
|
// first use). A standalone PointSize VARIABLE (never glslang's shape, but legal
|
||||||
|
// SPIR-V) is demoted in place: BuiltIn swapped for the Location, and the variable
|
||||||
|
// renamed to the carrier's name.
|
||||||
|
//
|
||||||
|
// "Declared but unaccessed" is only safe while the ES hop PRINTS by access, and
|
||||||
|
// there is one shape where it does not. SPIRV-Cross redeclares the whole
|
||||||
|
// gl_PerVertex output block for a CONTROL stage whose clip or cull distances are
|
||||||
|
// live (should_force_emit_builtin_block), and that redeclaration walks the
|
||||||
|
// struct's member DECORATIONS - so it would print "float gl_PointSize;" into a
|
||||||
|
// block nothing touches any more, which an extension-less ES driver rejects
|
||||||
|
// exactly as it rejected the access. That combination therefore DECLINES, below,
|
||||||
|
// rather than shipping a module that is mutated and still lost. Every other
|
||||||
|
// demoted shape leaves the member genuinely invisible to the ES hop, which is
|
||||||
|
// what the pinned transpile assertions hold.
|
||||||
|
//
|
||||||
|
// A VERTEX module is never capability-limited (gl_PointSize is core there on both
|
||||||
|
// targets), so it keeps its built-in untouched and, when the next stage consumes the
|
||||||
|
// carrier, MIRRORS the built-in's value into the carrier at every return of the
|
||||||
|
// entry function - the VS->TCS half of the chain.
|
||||||
|
//
|
||||||
|
// The VALUE is what survives: gl_in[].gl_PointSize reads and transform-feedback
|
||||||
|
// captures see exactly what the upstream stage wrote. The RASTERIZED point size is
|
||||||
|
// what does not - with the built-in unhosted, both targets rasterize such pipelines
|
||||||
|
// at the default size 1.0 (Vulkan: the shaderTessellationAndGeometryPointSize
|
||||||
|
// feature description; ES: PointSizeRange default) - so rasterization-verified
|
||||||
|
// point_rendering tests keep failing honestly and nothing may be gated on them.
|
||||||
|
//
|
||||||
|
// Anything the pass cannot express - a whole gl_PerVertex struct load/store/copy, a
|
||||||
|
// pointer that escapes into an opcode it cannot follow, an access-chain split across
|
||||||
|
// two chains - DECLINES the module byte-identically, reported through the report
|
||||||
|
// struct, so the caller keeps the existing honest refusal paths instead of shipping
|
||||||
|
// a half-demoted program.
|
||||||
|
//
|
||||||
|
// One module per run; the PROGRAM-wide contract (every stage demoted or none, one
|
||||||
|
// shared location, matching carrier names across each boundary) is owned by
|
||||||
|
// ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram, the only caller.
|
||||||
|
struct DemotePointSizeOptions {
|
||||||
|
// The Location every carrier of this program uses; chosen by the caller past
|
||||||
|
// every location any stage of the program already consumes.
|
||||||
|
Uint32 location = 0;
|
||||||
|
// Name for the arrayed Input carrier (empty forbids creating one: a module that
|
||||||
|
// reads gl_in[].gl_PointSize with no name to give the carrier declines).
|
||||||
|
String inputCarrierName;
|
||||||
|
// Name for the Output carrier (scalar in VS/TES/GS, arrayed in TCS).
|
||||||
|
String outputCarrierName;
|
||||||
|
// Create the Output carrier even when this module never writes PointSize: the
|
||||||
|
// next stage reads it (Vulkan requires every consumed input to be produced,
|
||||||
|
// VUID-RuntimeSpirv-OpEntryPoint-08743), or a transform-feedback capture of
|
||||||
|
// gl_PointSize binds to it. Such a carrier is SEEDED with 1.0 - GL's default
|
||||||
|
// point size, and what an unhosted built-in rasterizes at - rather than left
|
||||||
|
// unwritten: GL calls the value of an unwritten output undefined, but an ES
|
||||||
|
// driver's front end DELETES a never-written output, and a capture naming a
|
||||||
|
// deleted varying fails the link and takes the whole capture set with it. A
|
||||||
|
// control stage cannot be seeded this way (the write is per-invocation) and
|
||||||
|
// declines instead.
|
||||||
|
Bool forceOutputCarrier = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DemotePointSizeReport {
|
||||||
|
Bool declined = false;
|
||||||
|
String declineReason;
|
||||||
|
// The module reads incoming PointSize, so an Input carrier now exists - which
|
||||||
|
// obliges the PREVIOUS stage to produce the matching Output carrier. The driver
|
||||||
|
// walks the stages back-to-front off exactly this bit.
|
||||||
|
Bool createdInputCarrier = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DemotePointSizePass : public spvtools::opt::Pass {
|
||||||
|
public:
|
||||||
|
DemotePointSizePass(DemotePointSizeOptions options, DemotePointSizeReport* report)
|
||||||
|
: m_options(Move(options)), m_report(report) {}
|
||||||
|
const char* name() const override { return "mobilegl-demote-point-size"; }
|
||||||
|
Status Process() override;
|
||||||
|
|
||||||
|
static spvtools::Optimizer::PassToken CreateDemotePointSizePass(
|
||||||
|
DemotePointSizeOptions options, DemotePointSizeReport* report);
|
||||||
|
|
||||||
|
private:
|
||||||
|
DemotePointSizeOptions m_options;
|
||||||
|
DemotePointSizeReport* m_report;
|
||||||
|
};
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -78,9 +78,9 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Locations one value of `type` occupies (GL 4.6 core 11.1.2.1 / 15.2): a
|
// Locations one value of `type` occupies (GL 4.6 core 11.1.2.1 / 15.2): a
|
||||||
// matrix takes one per column, a double-precision vector wider than two takes
|
// matrix takes one per column, a 64-bit vector wider than two takes two, an
|
||||||
// two, an array takes its element's span once per element. 0 means "this pass
|
// array takes its element's span once per element. 0 means "this pass cannot
|
||||||
// cannot place it", which declines the whole block rather than guessing.
|
// place it", which declines the whole block rather than guessing.
|
||||||
Uint32 LocationSpan(const analysis::Type* type) {
|
Uint32 LocationSpan(const analysis::Type* type) {
|
||||||
if (type == nullptr) return 0;
|
if (type == nullptr) return 0;
|
||||||
if (type->AsFloat() != nullptr || type->AsInteger() != nullptr ||
|
if (type->AsFloat() != nullptr || type->AsInteger() != nullptr ||
|
||||||
@@ -93,8 +93,14 @@ namespace MobileGL {
|
|||||||
element->AsBool() == nullptr) {
|
element->AsBool() == nullptr) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
// 64-bit INTEGERS span two locations exactly like doubles do:
|
||||||
|
// ARB_gpu_shader_int64 extends 11.1.2.1's double-precision rule
|
||||||
|
// verbatim to i64/u64. Answering 1 for an i64vec4 would pack the
|
||||||
|
// members after it onto locations that varying already owns.
|
||||||
const auto* elementFloat = element->AsFloat();
|
const auto* elementFloat = element->AsFloat();
|
||||||
const Bool is64Bit = elementFloat != nullptr && elementFloat->width() == 64;
|
const auto* elementInteger = element->AsInteger();
|
||||||
|
const Bool is64Bit = (elementFloat != nullptr && elementFloat->width() == 64) ||
|
||||||
|
(elementInteger != nullptr && elementInteger->width() == 64);
|
||||||
return (is64Bit && vector->element_count() > 2) ? 2u : 1u;
|
return (is64Bit && vector->element_count() > 2) ? 2u : 1u;
|
||||||
}
|
}
|
||||||
if (const auto* matrix = type->AsMatrix()) {
|
if (const auto* matrix = type->AsMatrix()) {
|
||||||
|
|||||||
@@ -34,7 +34,11 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
// gated, so one L1 key shape can describe two materially different module sets (real
|
// gated, so one L1 key shape can describe two materially different module sets (real
|
||||||
// doubles vs demoted-and-flattened) and a blob written under 4 says nothing about
|
// doubles vs demoted-and-flattened) and a blob written under 4 says nothing about
|
||||||
// which one it holds.
|
// which one it holds.
|
||||||
constexpr Uint32 kKeyLayoutVersion = 5u;
|
// 6: L1 gained the two point-size demotion bits (demoteTessellationPointSize /
|
||||||
|
// demoteGeometryPointSize). Phase B now rewrites the cached modules on a device
|
||||||
|
// that cannot host gl_PointSize in tessellation/geometry stages, so a blob
|
||||||
|
// written under 5 says nothing about whether its modules were demoted.
|
||||||
|
constexpr Uint32 kKeyLayoutVersion = 6u;
|
||||||
|
|
||||||
// The repo's existing cache epoch (MG_Config::CacheVersion, the seed
|
// The repo's existing cache epoch (MG_Config::CacheVersion, the seed
|
||||||
// ProgramFactory::ComputeHash uses). Strictly redundant for an in-memory
|
// ProgramFactory::ComputeHash uses). Strictly redundant for an in-memory
|
||||||
@@ -128,6 +132,8 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
builder.Value(inputs.shaderCompileFlags);
|
builder.Value(inputs.shaderCompileFlags);
|
||||||
builder.Value(static_cast<Uint8>(inputs.enableSpirvValidation));
|
builder.Value(static_cast<Uint8>(inputs.enableSpirvValidation));
|
||||||
builder.Value(static_cast<Uint8>(inputs.nativeFloat64));
|
builder.Value(static_cast<Uint8>(inputs.nativeFloat64));
|
||||||
|
builder.Value(static_cast<Uint8>(inputs.demoteTessellationPointSize));
|
||||||
|
builder.Value(static_cast<Uint8>(inputs.demoteGeometryPointSize));
|
||||||
builder.Value(static_cast<Uint64>(inputs.stages.size()));
|
builder.Value(static_cast<Uint64>(inputs.stages.size()));
|
||||||
for (const auto& stage : inputs.stages) {
|
for (const auto& stage : inputs.stages) {
|
||||||
builder.Value(static_cast<Uint32>(stage.type));
|
builder.Value(static_cast<Uint32>(stage.type));
|
||||||
|
|||||||
@@ -411,9 +411,17 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
const UnorderedMap<String, Uint>* explicitFragmentOutIndices = nullptr;
|
const UnorderedMap<String, Uint>* explicitFragmentOutIndices = nullptr;
|
||||||
Uint32 shaderCompileFlags = 0;
|
Uint32 shaderCompileFlags = 0;
|
||||||
Bool enableSpirvValidation = false;
|
Bool enableSpirvValidation = false;
|
||||||
// CompileEnv::ConsumesFloat64Natively() - the fp64 tail of the sanitize chain. The
|
// CompileEnv::ConsumesFloat64Natively() - the fp64 tail of the sanitize chain. See
|
||||||
// one backend capability bit in this key; see the note above for why it has to be.
|
// the note above for why it has to be here.
|
||||||
Bool nativeFloat64 = false;
|
Bool nativeFloat64 = false;
|
||||||
|
// CompileEnv::DemotesTessellationPointSize() / DemotesGeometryPointSize() - the
|
||||||
|
// second and third capability bits under the same rule as nativeFloat64: each ARMS
|
||||||
|
// a phase-B rewrite of the cached modules themselves
|
||||||
|
// (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram), so the same GLSL
|
||||||
|
// produces materially different module sets under the two answers - built-in
|
||||||
|
// point size kept, or carried as an ordinary varying with the capability stripped.
|
||||||
|
Bool demoteTessellationPointSize = false;
|
||||||
|
Bool demoteGeometryPointSize = false;
|
||||||
// ---- inputs that only matter because the PAYLOAD now carries the reflection ----
|
// ---- inputs that only matter because the PAYLOAD now carries the reflection ----
|
||||||
// When the payload was SPIR-V alone these were provably irrelevant: transform
|
// When the payload was SPIR-V alone these were provably irrelevant: transform
|
||||||
// feedback is resolved by READING the linked intermediates and never writes an XFB
|
// feedback is resolved by READING the linked intermediates and never writes an XFB
|
||||||
|
|||||||
Reference in New Issue
Block a user