mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
[Merge] (DirectGLES, ShaderTranspiler): land GL43 wave4 with the interface-block rename inside the L2 boundary
This commit is contained in:
@@ -831,6 +831,35 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
return includesBase;
|
||||
}
|
||||
|
||||
// GL 4.6 table 23.65 admits exactly four answers for GL_LAYER_PROVOKING_VERTEX and
|
||||
// GL_VIEWPORT_INDEX_PROVOKING_VERTEX. Anything else means the driver wrote something MobileGL
|
||||
// cannot forward as a convention, and GL_UNDEFINED_VERTEX - a legal answer, not a placeholder
|
||||
// - is the accurate thing to say about it.
|
||||
static GLenum NormalizeProvokingVertexConvention(GLint driverValue) {
|
||||
switch (static_cast<GLenum>(driverValue)) {
|
||||
case GL_FIRST_VERTEX_CONVENTION:
|
||||
case GL_LAST_VERTEX_CONVENTION:
|
||||
case GL_PROVOKING_VERTEX:
|
||||
case GL_UNDEFINED_VERTEX:
|
||||
return static_cast<GLenum>(driverValue);
|
||||
default:
|
||||
return GL_UNDEFINED_VERTEX;
|
||||
}
|
||||
}
|
||||
|
||||
static const char* ProvokingVertexConventionName(GLenum convention) {
|
||||
switch (convention) {
|
||||
case GL_FIRST_VERTEX_CONVENTION:
|
||||
return "GL_FIRST_VERTEX_CONVENTION";
|
||||
case GL_LAST_VERTEX_CONVENTION:
|
||||
return "GL_LAST_VERTEX_CONVENTION";
|
||||
case GL_PROVOKING_VERTEX:
|
||||
return "GL_PROVOKING_VERTEX";
|
||||
default:
|
||||
return "GL_UNDEFINED_VERTEX";
|
||||
}
|
||||
}
|
||||
|
||||
Bool FillInGLESCapabilities(MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& glesFuncs) {
|
||||
if (!glesFuncs.glGetString || !glesFuncs.glGetIntegerv) {
|
||||
MGLOG_E("Required GLES functions are not loaded, cannot query capabilities");
|
||||
@@ -1063,8 +1092,21 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
GLint maxComputeImageUniforms = 8;
|
||||
GLint maxDrawBuffers = 8;
|
||||
GLint maxColorAttachments = 8;
|
||||
GLint maxClipDistances = 8;
|
||||
// Zero is a legal answer, not a placeholder. GL_MAX_CLIP_DISTANCES exists in ES only as
|
||||
// GL_MAX_CLIP_DISTANCES_EXT under GL_EXT_clip_cull_distance, so on a driver without that
|
||||
// extension there is nowhere to put a clip distance at all: SPIRV-Cross emits
|
||||
// gl_ClipDistance behind an `#extension ... : require` the ESSL compiler rejects, and
|
||||
// DirectGLES has no state to forward the per-distance enables into (see the gate in
|
||||
// DirectGLES::SyncRenderState). Starting at 8 meant a probe that could never run left an
|
||||
// optimistic 8 behind, so the frontend promised eight clip planes and every draw with a
|
||||
// clipping program silently rendered nothing. The guarded probe below only ever widens it.
|
||||
GLint maxClipDistances = 0;
|
||||
GLint maxViewports = 16;
|
||||
// GL_UNDEFINED_VERTEX is what stands when the probes below cannot run, and it is a legal
|
||||
// answer rather than a placeholder: with neither geometry shaders nor a viewport array
|
||||
// there is no layered or multi-viewport draw for a convention to describe.
|
||||
GLenum layerProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
GLenum viewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
GLfloat minFragmentInterpolationOffset = -0.5f;
|
||||
GLfloat maxFragmentInterpolationOffset = 0.4375f;
|
||||
GLint fragmentInterpolationOffsetBits = 4;
|
||||
@@ -1074,11 +1116,39 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
GLint maxProgramTextureGatherOffset = 7;
|
||||
GLint maxPatchVertices = 32;
|
||||
GLint maxTessGenLevel = 64;
|
||||
// Function-scope, and used by every probe group below rather than redeclared inside each
|
||||
// one. Returns whether anything was drained, which is what lets a group tell "the driver
|
||||
// answered" from "the driver rejected the pname and left my local alone".
|
||||
const auto drainErrors = [&glesFuncs]() {
|
||||
Bool hadError = false;
|
||||
if (glesFuncs.glGetError) {
|
||||
while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true;
|
||||
}
|
||||
return hadError;
|
||||
};
|
||||
|
||||
// THE GENERATOR OF THIS WHOLE BUG FAMILY, closed here. A bare glGetIntegerv/glGetFloatv
|
||||
// of a pname the driver does not have does two damaging things at once: it leaves the
|
||||
// local at whatever the declaration initialised it to - an optimistic number the frontend
|
||||
// then advertises as a capability - and it leaves a GL_INVALID_ENUM in the queue where
|
||||
// the next unrelated probe's caller, or the application's first glGetError, gets blamed
|
||||
// for it. The per-stage storage block, fragment interpolation and buffer texture probes
|
||||
// below already drain and fall back; this unconditional run did neither, which is how
|
||||
// GL_MAX_CLIP_DISTANCES came to be advertised as 8 on a driver with no clip distances at
|
||||
// all. Every pname here that is not ES core is now either gated on the capability that
|
||||
// makes it exist or floored at the value a rejected probe would have left, and the whole
|
||||
// run is bracketed by a drain.
|
||||
drainErrors();
|
||||
glesFuncs.glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, aliasedLineWidthRange);
|
||||
// GL_SMOOTH_LINE_WIDTH_RANGE / GL_SMOOTH_LINE_WIDTH_GRANULARITY (0x0B22 / 0x0B23) are
|
||||
// desktop-only - ES has never had an antialiased line width query - so on a real GLES
|
||||
// driver these two raise GL_INVALID_ENUM. Kept as probes rather than dropped because the
|
||||
// ANGLE and desktop-GL hosts MobileGL also runs on do answer them; the initialisers are
|
||||
// the GL 4.6 table 23.55 minimum of [1, 1], which is both the honest answer for a driver
|
||||
// that cannot say and what an untouched out-param already holds.
|
||||
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_RANGE, smoothLineWidthRange);
|
||||
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_GRANULARITY, &smoothLineWidthGranularity);
|
||||
glesFuncs.glGetFloatv(GL_ALIASED_POINT_SIZE_RANGE, aliasedPointSizeRange);
|
||||
glesFuncs.glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE, viewportBoundsRange);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE, &max3DTextureSize);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &maxArrayTextureLayers);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE, &maxCubeMapTextureSize);
|
||||
@@ -1102,8 +1172,25 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
// single test case. 1 is a spec-legal value (the minimum required), so cap
|
||||
// to what is actually implemented instead of forwarding the raw driver limit.
|
||||
maxSampleMaskWords = std::min(maxSampleMaskWords, 1);
|
||||
// The multisample ceilings above are ES 3.1 state apart from GL_MAX_SAMPLES, which is ES
|
||||
// 3.0, so a 3.0 context rejects five of the six and leaves whatever the out-param held.
|
||||
// One sample is what a rejected probe leaves behind and is also the smallest legal
|
||||
// answer, so clamp rather than trust: a zero reaching GL_Getter would have the frontend
|
||||
// reject the very sample count it just advertised (see GetAdvertisedMaxSamples).
|
||||
maxColorTextureSamples = std::max(maxColorTextureSamples, 1);
|
||||
maxDepthTextureSamples = std::max(maxDepthTextureSamples, 1);
|
||||
maxFramebufferSamples = std::max(maxFramebufferSamples, 1);
|
||||
maxIntegerSamples = std::max(maxIntegerSamples, 1);
|
||||
maxSamples = std::max(maxSamples, 1);
|
||||
maxSampleMaskWords = std::max(maxSampleMaskWords, 1);
|
||||
// ES 3.2 core, or EXT_tessellation_shader on 3.1. Probed rather than version-gated so a
|
||||
// 3.1 driver that HAS the extension still gets to answer; the clamp below is what makes a
|
||||
// rejected query safe, since GL 4.6 table 23.66 and ES 3.2 table 21.45 set the same
|
||||
// minimums the initialisers carry and neither API permits less.
|
||||
glesFuncs.glGetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
maxPatchVertices = std::max(maxPatchVertices, 32);
|
||||
maxTessGenLevel = std::max(maxTessGenLevel, 64);
|
||||
glesFuncs.glGetIntegerv(GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET, &minProgramTextureGatherOffset);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET, &maxProgramTextureGatherOffset);
|
||||
// A driver that leaves the probe untouched (pre-ES 3.1, or an ignored enum) must not
|
||||
@@ -1140,6 +1227,13 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) {
|
||||
glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &maxGeometryImageUniforms);
|
||||
}
|
||||
// Closes the bracket opened before the run: every local above now holds either the
|
||||
// driver's answer or a floor, and nothing this function asked for is left in the error
|
||||
// queue for a later probe - or the application - to be blamed for.
|
||||
if (drainErrors()) {
|
||||
MGLOG_W("One or more capability queries were rejected by this driver; the affected "
|
||||
"limits keep MobileGL's spec-minimum floors");
|
||||
}
|
||||
// Per-stage storage-block counts. Deliberately NOT batched with the unconditional probes
|
||||
// above, for the reason GL_MAX_TEXTURE_BUFFER_SIZE is not: the vertex and fragment pnames
|
||||
// are ES 3.1, but the tessellation and geometry ones only exist from ES 3.2 on (or under
|
||||
@@ -1152,14 +1246,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
// stages is 0. That is the honest answer: DirectGLES emits ESSL 3.10 on an ES 3.1 context,
|
||||
// where those stages do not exist at all.
|
||||
{
|
||||
const auto drainErrors = [&glesFuncs]() {
|
||||
Bool hadError = false;
|
||||
if (glesFuncs.glGetError) {
|
||||
while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true;
|
||||
}
|
||||
return hadError;
|
||||
};
|
||||
|
||||
// Isolate from errors raised by the preceding probes so the drain below reports on
|
||||
// these queries only.
|
||||
drainErrors();
|
||||
@@ -1195,19 +1281,67 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
}
|
||||
glesFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
// GL_MAX_CLIP_DISTANCES is 0x0D32, which ES only ever spells GL_MAX_CLIP_DISTANCES_EXT and
|
||||
// only ever has under GL_EXT_clip_cull_distance. The extension was already resolved into
|
||||
// caps.SupportsClipDistance a few hundred lines above and is the same flag DirectGLES
|
||||
// gates the CLIP_DISTANCEi enable forwarding on, so ask the driver only where the pname
|
||||
// exists; everywhere else the honest 0 stands and no GL_INVALID_ENUM is left behind for an
|
||||
// unrelated query - or the application's first glGetError - to trip over.
|
||||
if (caps.SupportsClipDistance) {
|
||||
drainErrors();
|
||||
glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
|
||||
if (drainErrors()) {
|
||||
MGLOG_W("GL_EXT_clip_cull_distance is advertised but GL_MAX_CLIP_DISTANCES was "
|
||||
"rejected; reporting no clip distances");
|
||||
maxClipDistances = 0;
|
||||
}
|
||||
}
|
||||
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims);
|
||||
glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits);
|
||||
// GL_LAYER_PROVOKING_VERTEX is ES 3.2 core (it arrives with geometry shaders, which is
|
||||
// what gl_Layer needs). Ask the driver where the pname exists rather than asserting a
|
||||
// convention: it is a statement about which vertex of a primitive supplies gl_Layer, and
|
||||
// MobileGL forwards the geometry stage to the driver rather than implementing the
|
||||
// selection itself, so the driver's answer IS MobileGL's answer. Below ES 3.2 there are
|
||||
// no layered draws to have a convention for and GL_UNDEFINED_VERTEX stands, which GL 4.6
|
||||
// table 23.65 explicitly permits.
|
||||
if (esAtLeast32) {
|
||||
GLint driverLayerConvention = static_cast<GLint>(GL_UNDEFINED_VERTEX);
|
||||
drainErrors();
|
||||
glesFuncs.glGetIntegerv(GL_LAYER_PROVOKING_VERTEX, &driverLayerConvention);
|
||||
if (!drainErrors()) {
|
||||
layerProvokingVertex = NormalizeProvokingVertexConvention(driverLayerConvention);
|
||||
}
|
||||
}
|
||||
// GL_MAX_VIEWPORTS (0x825B), GL_VIEWPORT_SUBPIXEL_BITS (0x825C) and GL_VIEWPORT_BOUNDS_RANGE
|
||||
// (0x825D) all arrive with GL_OES_viewport_array and exist nowhere in ES core, so on the
|
||||
// drivers DirectGLES actually runs on all three raise GL_INVALID_ENUM. The values MobileGL
|
||||
// advertises do not change by asking: GL_Getter answers GL_MAX_VIEWPORTS from the frontend
|
||||
// state width (indexed viewport entry points validate against RenderStateParameters::
|
||||
// MAX_VIEWPORTS, so a device answer of 1 would reject indices the state can legitimately
|
||||
// hold), floors GL_SUBPIXEL_BITS at its own 4, and the bounds range is clamped to the core
|
||||
// minimum below. What changes is that the errors stop being manufactured.
|
||||
if (caps.SupportsViewportArray) {
|
||||
GLint driverViewportIndexConvention = static_cast<GLint>(GL_UNDEFINED_VERTEX);
|
||||
drainErrors();
|
||||
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits);
|
||||
glesFuncs.glGetIntegerv(GL_VIEWPORT_INDEX_PROVOKING_VERTEX, &driverViewportIndexConvention);
|
||||
if (glesFuncs.glGetFloatv) {
|
||||
glesFuncs.glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE, viewportBoundsRange);
|
||||
}
|
||||
if (drainErrors()) {
|
||||
MGLOG_W("GL_OES_viewport_array is advertised but its viewport limit queries were "
|
||||
"rejected; keeping the OpenGL core minimums");
|
||||
maxViewports = 16;
|
||||
viewportSubpixelBits = 0;
|
||||
viewportBoundsRange[0] = -32768.0f;
|
||||
viewportBoundsRange[1] = 32767.0f;
|
||||
} else {
|
||||
viewportIndexProvokingVertex =
|
||||
NormalizeProvokingVertexConvention(driverViewportIndexConvention);
|
||||
}
|
||||
}
|
||||
if (caps.SupportsShaderMultisampleInterpolation && glesFuncs.glGetFloatv) {
|
||||
const auto drainErrors = [&glesFuncs]() {
|
||||
Bool hadError = false;
|
||||
if (glesFuncs.glGetError) {
|
||||
while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true;
|
||||
}
|
||||
return hadError;
|
||||
};
|
||||
|
||||
// Isolate these optional queries from errors raised by preceding capability
|
||||
// probes, then consume any query error so initialization never leaks it into
|
||||
// the application's first glGetError call.
|
||||
@@ -1368,8 +1502,12 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.MaxComputeImageUniforms = maxComputeImageUniforms;
|
||||
caps.MaxDrawBuffers = maxDrawBuffers;
|
||||
caps.MaxColorAttachments = maxColorAttachments;
|
||||
caps.MaxClipDistances = maxClipDistances;
|
||||
// A driver is free to write nonsense into an out-param it then rejects, and without the
|
||||
// extension the probe above never ran at all - so the flag, not the local, decides.
|
||||
caps.MaxClipDistances = caps.SupportsClipDistance ? std::max(maxClipDistances, 0) : 0;
|
||||
caps.MaxViewports = maxViewports;
|
||||
caps.LayerProvokingVertex = layerProvokingVertex;
|
||||
caps.ViewportIndexProvokingVertex = viewportIndexProvokingVertex;
|
||||
caps.MaxViewportWidth = maxViewportDims[0];
|
||||
caps.MaxViewportHeight = maxViewportDims[1];
|
||||
// Only ever WIDER than the core minimum: a driver that answered the query is allowed to
|
||||
@@ -1450,12 +1588,20 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
MGLOG_I(" GL_MAX_COMPUTE_IMAGE_UNIFORMS: %d", caps.MaxComputeImageUniforms);
|
||||
MGLOG_I(" GL_MAX_DRAW_BUFFERS: %d", caps.MaxDrawBuffers);
|
||||
MGLOG_I(" GL_MAX_COLOR_ATTACHMENTS: %d", caps.MaxColorAttachments);
|
||||
MGLOG_I(" GL_MAX_CLIP_DISTANCES: %d", caps.MaxClipDistances);
|
||||
// Worth spelling the reason out for the same reason the per-stage storage block counts
|
||||
// are: a zero here is what stops an application's gl_ClipDistance from ever clipping, and
|
||||
// reading it back from an artifact is the difference between "MobileGL dropped my draw"
|
||||
// and "this driver has no clip distances".
|
||||
MGLOG_I(" GL_MAX_CLIP_DISTANCES: %d%s", caps.MaxClipDistances,
|
||||
caps.SupportsClipDistance ? "" : " (no GL_EXT_clip_cull_distance on this driver)");
|
||||
MGLOG_I(" GL_MAX_VIEWPORTS: %d", caps.MaxViewports);
|
||||
MGLOG_I(" GL_MAX_VIEWPORT_DIMS: [%d, %d]", caps.MaxViewportWidth, caps.MaxViewportHeight);
|
||||
MGLOG_I(" GL_VIEWPORT_BOUNDS_RANGE: [%.3f, %.3f]", caps.ViewportBoundsRangeMin,
|
||||
caps.ViewportBoundsRangeMax);
|
||||
MGLOG_I(" GL_VIEWPORT_SUBPIXEL_BITS: %d", caps.ViewportSubpixelBits);
|
||||
MGLOG_I(" GL_LAYER_PROVOKING_VERTEX: %s", ProvokingVertexConventionName(caps.LayerProvokingVertex));
|
||||
MGLOG_I(" GL_VIEWPORT_INDEX_PROVOKING_VERTEX: %s",
|
||||
ProvokingVertexConventionName(caps.ViewportIndexProvokingVertex));
|
||||
|
||||
caps.IndirectDrawInstanceIdIncludesBaseInstance =
|
||||
ProbeIndirectInstanceIdIncludesBaseInstance(caps, glesFuncs);
|
||||
@@ -1479,6 +1625,14 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.AvoidSamplerMipmapMinFilter ? "true" : "false");
|
||||
MGLOG_I(" Avoid explicit LOD bias: %s", caps.AvoidExplicitLodBias ? "true" : "false");
|
||||
|
||||
// Last line of defence. Capability init is the very first thing that touches the driver,
|
||||
// so anything it leaves in the error queue surfaces at the APPLICATION's first
|
||||
// glGetError and gets attributed to whatever call the app happened to make. Every group
|
||||
// above drains its own, but a probe added later must not be able to reintroduce the leak.
|
||||
if (drainErrors()) {
|
||||
MGLOG_W("Capability initialization left a GL error behind; it has been consumed so it "
|
||||
"cannot surface at the application's first glGetError");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace MobileGL::MG_Util::BackendLoader
|
||||
|
||||
@@ -1275,8 +1275,16 @@ namespace MobileGL {
|
||||
Int MaxComputeImageUniforms = 8;
|
||||
Int MaxDrawBuffers = 8;
|
||||
Int MaxColorAttachments = 8;
|
||||
Int MaxClipDistances = 8;
|
||||
// Zero is a legal answer, not a placeholder: ES reaches clip distances only through
|
||||
// GL_EXT_clip_cull_distance, so a driver without it has none. See the guarded probe
|
||||
// in FillInGLESCapabilities.
|
||||
Int MaxClipDistances = 0;
|
||||
Int MaxViewports = 16;
|
||||
// GL_LAYER_PROVOKING_VERTEX (ES 3.2 core) and GL_VIEWPORT_INDEX_PROVOKING_VERTEX
|
||||
// (GL_OES_viewport_array). GL_UNDEFINED_VERTEX is a legal answer for both and is what
|
||||
// a driver that has neither is honestly saying.
|
||||
GLenum LayerProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
GLenum ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
Int MaxViewportWidth = 16384;
|
||||
Int MaxViewportHeight = 16384;
|
||||
Float ViewportBoundsRangeMin = 0.0f;
|
||||
|
||||
@@ -237,6 +237,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
supportedFeatures.vertexPipelineStoresAndAtomics == VK_TRUE;
|
||||
caps.SupportsFragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics == VK_TRUE;
|
||||
caps.SupportsGeometryShader = supportedFeatures.geometryShader == VK_TRUE;
|
||||
caps.SupportsShaderClipDistance = supportedFeatures.shaderClipDistance == VK_TRUE;
|
||||
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(p.limits.maxStorageBufferRange);
|
||||
const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 &&
|
||||
HasUsableShaderSubgroupSupport(subgroupProps);
|
||||
@@ -331,6 +332,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.SupportsVertexPipelineStoresAndAtomics = false;
|
||||
caps.SupportsFragmentStoresAndAtomics = false;
|
||||
caps.SupportsGeometryShader = false;
|
||||
caps.SupportsShaderClipDistance = false;
|
||||
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(properties.limits.maxStorageBufferRange);
|
||||
caps.SupportsShaderSubgroup = false;
|
||||
caps.SubgroupSize = 0;
|
||||
|
||||
@@ -96,6 +96,12 @@ namespace MobileGL {
|
||||
Bool SupportsVertexPipelineStoresAndAtomics = false;
|
||||
Bool SupportsFragmentStoresAndAtomics = false;
|
||||
Bool SupportsGeometryShader = false;
|
||||
// VkPhysicalDeviceFeatures::shaderClipDistance. maxClipDistances is a LIMIT and is
|
||||
// reported whatever the feature says, so the limit alone does not mean a module may
|
||||
// declare ClipDistance - VulkanRenderer enables the feature only where the physical
|
||||
// device has it, and without it a shader writing gl_ClipDistance is invalid. Very
|
||||
// widely supported, hence read from the device features and never assumed false.
|
||||
Bool SupportsShaderClipDistance = false;
|
||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
Bool SupportsShaderSubgroup = false;
|
||||
Uint32 SubgroupSize = 0;
|
||||
|
||||
@@ -558,8 +558,9 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
} else {
|
||||
builder.Warn("GL_EXT_render_snorm",
|
||||
"not supported; signed-normalized formats are texture-only, so every SNORM "
|
||||
"render target is stored as a float (GL_RGBA8_SNORM/GL_RGB8_SNORM -> "
|
||||
"GL_RGBA16F) and its fragment outputs are clamped to [-1,1] in software");
|
||||
"render target is stored as a float (8-bit -> *16F, 16-bit -> *32F, which "
|
||||
"is the narrowest float that still holds a 16-bit SNORM channel exactly) "
|
||||
"and its fragment outputs are clamped to [-1,1] in software");
|
||||
}
|
||||
// FAIL, not WARN: ES 3.x core makes every float format texture-only, and every Iris
|
||||
// shaderpack renders into at least GL_R11F_G11F_B10F (Complementary's colortex0, BSL's
|
||||
|
||||
@@ -62,6 +62,11 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// gl_MaxComputeTextureImageUnits built-in constant, so a compute module that reads
|
||||
// that constant generates DIFFERENT SPIR-V under two backends that disagree on it.
|
||||
HashValue(state, env.params.MaxComputeTextureImageUnits);
|
||||
// Added when wave4 (4fc3531d) made this one env-derived, and the same class again:
|
||||
// glslang REJECTS gl_ClipDistance[i] for i >= maxClipDistances at parse (ParseHelper)
|
||||
// and expands gl_MaxClipDistances from the same number, so it decides both whether a
|
||||
// shader compiles at all and what a module that reads the constant generates.
|
||||
HashValue(state, env.params.MaxClipDistances);
|
||||
// The compute work-group limits, likewise added by wave3 (cb155c5b). They used to be
|
||||
// hardcoded maxima in BuildTBuiltInResource, and the L1 key comment said in so many
|
||||
// words that the day they became backend-derived they would have to move in here -
|
||||
|
||||
@@ -84,9 +84,11 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// * the DynamicBackendParameters fields BuildTBuiltInResource copies into
|
||||
// TBuiltInResource - MaxImageUnits, MaxDrawBuffers, MaxVertexImageUniforms,
|
||||
// MaxGeometryImageUniforms, MaxFragmentImageUniforms, MaxComputeImageUniforms,
|
||||
// MaxCombinedImageUniforms, MaxComputeTextureImageUnits. glslang enforces those at
|
||||
// parse, so they decide whether a shader compiles at all and can change the link
|
||||
// result.
|
||||
// MaxCombinedImageUniforms, MaxComputeTextureImageUnits, MaxClipDistances. glslang
|
||||
// enforces those at parse, so they decide whether a shader compiles at all and can
|
||||
// change the link result. MaxClipDistances moved in at the wave4 merge (4fc3531d),
|
||||
// the third time in three waves that a hardcoded TBuiltInResource field became
|
||||
// env-derived - assume the next wave does it again and re-audit.
|
||||
// * maxComputeWorkGroupSize and maxComputeWorkGroupCount, all three components each.
|
||||
// These moved IN at the dev merge that brought wave3's cb155c5b, which made
|
||||
// BuildTBuiltInResource read them from the env instead of hardcoding a permissive
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "SpirvPasses/LowerViewportIndexPass.h"
|
||||
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
||||
#include "SpirvPasses/FlattenXfbInterfaceBlocksPass.h"
|
||||
#include "SpirvPasses/UniquifyIoBlockNamesPass.h"
|
||||
#include "SpirvPasses/SplitArrayVertexInputsPass.h"
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/ZeroBaseVertexPass.h"
|
||||
@@ -82,7 +83,6 @@ namespace MobileGL {
|
||||
Resources.maxFragmentInputVectors = 15;
|
||||
Resources.minProgramTexelOffset = -8;
|
||||
Resources.maxProgramTexelOffset = 7;
|
||||
Resources.maxClipDistances = 8;
|
||||
Resources.maxComputeUniformComponents = MAX_COMPUTE_UNIFORM_COMPONENTS;
|
||||
Resources.maxComputeTextureImageUnits = 16;
|
||||
Resources.maxComputeImageUniforms = 8;
|
||||
@@ -181,6 +181,14 @@ namespace MobileGL {
|
||||
Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms;
|
||||
Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms;
|
||||
Resources.maxComputeTextureImageUnits = dynamicParameters.MaxComputeTextureImageUnits;
|
||||
// Load-bearing, not cosmetic. glslang rejects gl_ClipDistance[i] for
|
||||
// i >= maxClipDistances (ParseHelper.cpp) and expands gl_MaxClipDistances from the
|
||||
// same number, so tracking the backend limit is what turns "the program links,
|
||||
// the backend's shader compile fails somewhere the frontend never surfaces, and
|
||||
// the draw renders nothing" into an honest glCompileShader error with a log. It is
|
||||
// also what makes glGetIntegerv(GL_MAX_CLIP_DISTANCES) and gl_MaxClipDistances
|
||||
// agree, which KHR-GLxx.clip_distance.coverage compares directly.
|
||||
Resources.maxClipDistances = dynamicParameters.MaxClipDistances;
|
||||
|
||||
// The compute work-group limits are the env's, not the backend parameters': they
|
||||
// are the only ones that come from a REAL indexed driver query, which
|
||||
@@ -782,6 +790,42 @@ namespace MobileGL {
|
||||
outName);
|
||||
}
|
||||
|
||||
void ShaderCompiler::ProbeIoBlockNamesForEssl(const Vector<Uint32>& binary,
|
||||
std::set<String>& collidingBlockNames,
|
||||
std::set<String>& declaredNames) {
|
||||
if (binary.empty()) {
|
||||
// Same reasoning as ModuleDeclaresBufferTextureSampler: a stage that produced
|
||||
// no SPIR-V has no block names to report, and parsing it would push a
|
||||
// spurious diagnostic through the message consumer.
|
||||
return;
|
||||
}
|
||||
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ProbeIoBlockNamesForEssl"), binary.data(),
|
||||
binary.size());
|
||||
if (!context) {
|
||||
// Unparseable here means unusable downstream too; let the ordinary transpile
|
||||
// path produce the error rather than inventing a rename plan from it.
|
||||
return;
|
||||
}
|
||||
UniquifyIoBlockNamesPass::ProbeIoBlockNames(context.get(), collidingBlockNames, declaredNames);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::UniquifyIoBlockNamesForEssl(const Vector<Uint32>& inputBinary,
|
||||
const std::map<String, String>& inputBlockRenames,
|
||||
const std::map<String, String>& outputBlockRenames,
|
||||
std::set<String>& renamedBlockNames,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
if (inputBlockRenames.empty() && outputBlockRenames.empty()) return false;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(UniquifyIoBlockNamesPass::CreateUniquifyIoBlockNamesPass(
|
||||
inputBlockRenames, outputBlockRenames, &renamedBlockNames));
|
||||
|
||||
return RunOptimizerChecked("UniquifyIoBlockNamesForEssl", optimizer, inputBinary,
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "glslang/TVarEntryInfo.h"
|
||||
#include "glslang/TMglGlslIoResolver.h"
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
namespace MobileGL {
|
||||
@@ -101,6 +102,29 @@ namespace MobileGL {
|
||||
static bool RewriteXfbCaptureNameForFlattenedBlock(const String& captureName,
|
||||
const std::set<String>& flattenedBlockNames,
|
||||
String& outName);
|
||||
// Adds to `collidingBlockNames` every inter-stage interface block this stage
|
||||
// declares in BOTH directions at once (`in FOO {...}; out FOO {...}`, which
|
||||
// desktop GLSL allows because its input and output block namespaces are
|
||||
// separate), and to `declaredNames` every name the module spells. The gate for
|
||||
// UniquifyIoBlockNamesForEssl below, and the source of the name set a
|
||||
// replacement has to avoid. Reads the module; never rewrites it.
|
||||
static void ProbeIoBlockNamesForEssl(const Vector<Uint32>& binary,
|
||||
std::set<String>& collidingBlockNames,
|
||||
std::set<String>& declaredNames);
|
||||
// Renames inter-stage interface BLOCK types so the collision the probe above
|
||||
// found gets one spelling per producing stage. `inputBlockRenames` applies to
|
||||
// blocks this stage consumes and `outputBlockRenames` to blocks it produces,
|
||||
// both planned program-wide by the caller so a producer and its consumer keep
|
||||
// matching; `renamedBlockNames` reports the original names this stage actually
|
||||
// rewrote. SPIRV-Cross re-emits two same-named blocks verbatim and the Mali ES
|
||||
// driver then loses the output block's payload. Only for the DirectGLES
|
||||
// transpile path. See UniquifyIoBlockNamesPass.
|
||||
static bool UniquifyIoBlockNamesForEssl(const Vector<Uint32>& inputBinary,
|
||||
const std::map<String, String>& inputBlockRenames,
|
||||
const std::map<String, String>& outputBlockRenames,
|
||||
std::set<String>& renamedBlockNames,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Drops RelaxedPrecision member decorations from uniform-block structs so
|
||||
// SPIRV-Cross prints the same (highp) member precision in every stage; ES
|
||||
// drivers reject cross-stage uniform blocks whose member precisions differ.
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.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 "UniquifyIoBlockNamesPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#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/util/make_unique.h"
|
||||
#include "source/util/string_utils.h"
|
||||
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
|
||||
// Which storage classes a block struct is reachable from. A struct seen in both
|
||||
// directions inside ONE module cannot be renamed per direction (there is only
|
||||
// one name to change), so it is skipped rather than guessed at.
|
||||
constexpr Uint32 kSeenAsInput = 1u;
|
||||
constexpr Uint32 kSeenAsOutput = 2u;
|
||||
|
||||
// Every struct type carrying the Block decoration, minus the ones with a builtin
|
||||
// member (gl_PerVertex): those are named by the language, not by the shader, and
|
||||
// renaming one would invent a block no driver knows.
|
||||
std::unordered_set<uint32_t> CollectUserBlockStructIds(IRContext* irContext) {
|
||||
std::unordered_set<uint32_t> blockStructIds;
|
||||
std::unordered_set<uint32_t> builtinStructIds;
|
||||
for (Instruction& annotation : irContext->module()->annotations()) {
|
||||
if (annotation.opcode() == spv::Op::OpDecorate) {
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) ==
|
||||
spv::Decoration::Block) {
|
||||
blockStructIds.insert(annotation.GetSingleWordInOperand(0));
|
||||
}
|
||||
} else if (annotation.opcode() == spv::Op::OpMemberDecorate) {
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(2)) ==
|
||||
spv::Decoration::BuiltIn) {
|
||||
builtinStructIds.insert(annotation.GetSingleWordInOperand(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (uint32_t builtinStructId : builtinStructIds) {
|
||||
blockStructIds.erase(builtinStructId);
|
||||
}
|
||||
return blockStructIds;
|
||||
}
|
||||
|
||||
// The block struct an Input/Output variable declares, or 0 when the variable is
|
||||
// not an interface block of the kind this pass renames. Tessellation and geometry
|
||||
// interfaces are arrays of the block struct, so one array level is unwrapped -
|
||||
// the same shape StripUboMemberRelaxedPrecisionPass unwraps for instance-arrayed
|
||||
// uniform blocks.
|
||||
uint32_t GetInterfaceBlockStructId(IRContext* irContext, Instruction& variable,
|
||||
const std::unordered_set<uint32_t>& blockStructIds,
|
||||
spv::StorageClass& outStorageClass) {
|
||||
if (variable.opcode() != spv::Op::OpVariable) return 0;
|
||||
const auto storageClass =
|
||||
static_cast<spv::StorageClass>(variable.GetSingleWordInOperand(0));
|
||||
if (storageClass != spv::StorageClass::Input &&
|
||||
storageClass != spv::StorageClass::Output) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
Instruction* pointerType = defUseMgr->GetDef(variable.type_id());
|
||||
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) return 0;
|
||||
uint32_t pointeeId = pointerType->GetSingleWordInOperand(1);
|
||||
Instruction* pointee = defUseMgr->GetDef(pointeeId);
|
||||
while (pointee != nullptr && (pointee->opcode() == spv::Op::OpTypeArray ||
|
||||
pointee->opcode() == spv::Op::OpTypeRuntimeArray)) {
|
||||
pointeeId = pointee->GetSingleWordInOperand(0);
|
||||
pointee = defUseMgr->GetDef(pointeeId);
|
||||
}
|
||||
if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeStruct) return 0;
|
||||
if (blockStructIds.find(pointeeId) == blockStructIds.end()) return 0;
|
||||
|
||||
outStorageClass = storageClass;
|
||||
return pointeeId;
|
||||
}
|
||||
|
||||
String FindName(IRContext* irContext, uint32_t id) {
|
||||
for (Instruction& debugInst : irContext->debugs2()) {
|
||||
if (debugInst.opcode() != spv::Op::OpName) continue;
|
||||
if (debugInst.GetSingleWordInOperand(0) != id) continue;
|
||||
return debugInst.GetInOperand(1).AsString();
|
||||
}
|
||||
return String();
|
||||
}
|
||||
|
||||
// Replaces an EXISTING OpName only. A block struct with no name of its own is
|
||||
// one SPIRV-Cross would spell from a fallback, which the consuming stage would
|
||||
// not agree with anyway - leave it alone rather than invent a name for it.
|
||||
Bool ReplaceExistingName(IRContext* irContext, uint32_t id, const String& newName) {
|
||||
for (Instruction& debugInst : irContext->debugs2()) {
|
||||
if (debugInst.opcode() != spv::Op::OpName) continue;
|
||||
if (debugInst.GetSingleWordInOperand(0) != id) continue;
|
||||
debugInst.SetInOperand(
|
||||
1, spvtools::utils::MakeVector<spvtools::opt::Operand::OperandData>(newName));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Not a real id: "this name reached two different struct types in the same
|
||||
// direction", which is already an illegal shader (glslang refuses to reuse a
|
||||
// block name inside one interface) and which no rename could repair - two
|
||||
// structs would come out with one new name. Both the probe and the rewrite
|
||||
// decline it.
|
||||
constexpr uint32_t kAmbiguousStructId = 0xffffffffu;
|
||||
|
||||
// The module's interface blocks indexed the way both halves of this pass need
|
||||
// them: by name within each direction, plus which directions each struct type
|
||||
// is reached from.
|
||||
struct IoBlockIndex {
|
||||
std::map<String, uint32_t> inputStructByName;
|
||||
std::map<String, uint32_t> outputStructByName;
|
||||
std::unordered_map<uint32_t, Uint32> storageMaskByStructId;
|
||||
};
|
||||
|
||||
IoBlockIndex IndexIoBlocks(IRContext* irContext,
|
||||
const std::unordered_set<uint32_t>& blockStructIds) {
|
||||
IoBlockIndex index;
|
||||
for (Instruction& variable : irContext->module()->types_values()) {
|
||||
spv::StorageClass storageClass = spv::StorageClass::Input;
|
||||
const uint32_t structId =
|
||||
GetInterfaceBlockStructId(irContext, variable, blockStructIds, storageClass);
|
||||
if (structId == 0) continue;
|
||||
const Bool isInput = storageClass == spv::StorageClass::Input;
|
||||
index.storageMaskByStructId[structId] |= isInput ? kSeenAsInput : kSeenAsOutput;
|
||||
|
||||
const String blockName = FindName(irContext, structId);
|
||||
if (blockName.empty()) continue;
|
||||
std::map<String, uint32_t>& byName =
|
||||
isInput ? index.inputStructByName : index.outputStructByName;
|
||||
const auto inserted = byName.emplace(blockName, structId);
|
||||
if (!inserted.second && inserted.first->second != structId) {
|
||||
inserted.first->second = kAmbiguousStructId;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void UniquifyIoBlockNamesPass::ProbeIoBlockNames(spvtools::opt::IRContext* irContext,
|
||||
std::set<String>& outCollidingBlockNames,
|
||||
std::set<String>& outDeclaredNames) {
|
||||
if (irContext == nullptr) return;
|
||||
|
||||
for (Instruction& debugInst : irContext->debugs2()) {
|
||||
if (debugInst.opcode() != spv::Op::OpName) continue;
|
||||
outDeclaredNames.insert(debugInst.GetInOperand(1).AsString());
|
||||
}
|
||||
|
||||
const std::unordered_set<uint32_t> blockStructIds = CollectUserBlockStructIds(irContext);
|
||||
if (blockStructIds.empty()) return;
|
||||
|
||||
const IoBlockIndex index = IndexIoBlocks(irContext, blockStructIds);
|
||||
for (const auto& input : index.inputStructByName) {
|
||||
const auto output = index.outputStructByName.find(input.first);
|
||||
if (output == index.outputStructByName.end()) continue;
|
||||
if (input.second == kAmbiguousStructId || output->second == kAmbiguousStructId) continue;
|
||||
// Same struct type on both sides: there is one name to rename and two
|
||||
// directions wanting different ones, so the collision cannot be repaired.
|
||||
if (input.second == output->second) continue;
|
||||
outCollidingBlockNames.insert(input.first);
|
||||
}
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status UniquifyIoBlockNamesPass::Process() {
|
||||
if (m_inputBlockRenames.empty() && m_outputBlockRenames.empty()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
auto* irContext = context();
|
||||
const std::unordered_set<uint32_t> blockStructIds = CollectUserBlockStructIds(irContext);
|
||||
if (blockStructIds.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
// Indexed BEFORE anything is renamed, so every decline below is decided against
|
||||
// the names the module arrived with rather than against a half-renamed one.
|
||||
const IoBlockIndex index = IndexIoBlocks(irContext, blockStructIds);
|
||||
|
||||
Bool modified = false;
|
||||
for (int direction = 0; direction < 2; ++direction) {
|
||||
const Bool isInput = direction == 0;
|
||||
const std::map<String, uint32_t>& byName =
|
||||
isInput ? index.inputStructByName : index.outputStructByName;
|
||||
const std::map<String, String>& renames =
|
||||
isInput ? m_inputBlockRenames : m_outputBlockRenames;
|
||||
const Uint32 wantedMask = isInput ? kSeenAsInput : kSeenAsOutput;
|
||||
|
||||
for (const auto& block : byName) {
|
||||
if (block.second == kAmbiguousStructId) continue;
|
||||
const auto rename = renames.find(block.first);
|
||||
if (rename == renames.end()) continue;
|
||||
if (rename->second.empty() || rename->second == block.first) continue;
|
||||
// A struct type reached from BOTH directions carries one name for two
|
||||
// interfaces, so renaming it for this direction would rename it for the
|
||||
// other one too. Leave the module as it was.
|
||||
const auto mask = index.storageMaskByStructId.find(block.second);
|
||||
if (mask == index.storageMaskByStructId.end() || mask->second != wantedMask) continue;
|
||||
if (!ReplaceExistingName(irContext, block.second, rename->second)) continue;
|
||||
|
||||
if (m_renamedBlockNames != nullptr) m_renamedBlockNames->insert(block.first);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
return modified ? Status::SuccessWithChange : Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken UniquifyIoBlockNamesPass::CreateUniquifyIoBlockNamesPass(
|
||||
const std::map<String, String>& inputBlockRenames,
|
||||
const std::map<String, String>& outputBlockRenames, std::set<String>* renamedBlockNames) {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<UniquifyIoBlockNamesPass>(
|
||||
inputBlockRenames, outputBlockRenames, renamedBlockNames));
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,90 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.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>
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Renames the STRUCT of an inter-stage interface block, so a block name a stage
|
||||
// declares in both directions at once gets one spelling per producing stage.
|
||||
//
|
||||
// WHY. Desktop GLSL keeps SEPARATE name namespaces for input and output interface
|
||||
// blocks, so a single stage may legally write
|
||||
//
|
||||
// in TCSOutputBlock { ... } input_block[];
|
||||
// out TCSOutputBlock { ... } output_block;
|
||||
//
|
||||
// which is exactly what the tessellation evaluation stage of
|
||||
// KHR-GL42/43.shading_language_420pack.length_of_vector_and_matrix_* and
|
||||
// .qualifier_order_block_* does. glslang accepts it deliberately (ParseHelper
|
||||
// errors only when the two share a storage qualifier) and SPIRV-Cross re-emits
|
||||
// BOTH under the name TCSOutputBlock, because it too splits the namespace
|
||||
// (block_input_names vs block_output_names). The generated ESSL 3.20 then declares
|
||||
// two different blocks called TCSOutputBlock in one shader. Adreno's ES compiler
|
||||
// keeps them apart; Mali's does not - the stage compiles, the program links, and
|
||||
// the output block's payload never reaches the next stage, which is all 22 of
|
||||
// that group's Mali failures and none of Adreno's or DirectVulkan's.
|
||||
//
|
||||
// WHAT. The rename is planned program-wide by the CALLER and keyed on the
|
||||
// PRODUCING stage, so a producer and its consumer keep naming the same block:
|
||||
// the tessellation control stage's `out TCSOutputBlock` and the evaluation
|
||||
// stage's `in TCSOutputBlock` both become <name>_mgio<TCS>, while the evaluation
|
||||
// stage's own `out TCSOutputBlock` and the geometry stage's `in TCSOutputBlock`
|
||||
// both become <name>_mgio<TES>. Only the block TYPE name changes; instance names,
|
||||
// member names, locations and every decoration are left exactly as they were, and
|
||||
// ES matches inter-stage blocks by block name plus member sequence.
|
||||
//
|
||||
// DirectGLES only: DirectVulkan hands the module to the driver as SPIR-V, where
|
||||
// the two blocks are distinct type ids and the debug names carry no meaning.
|
||||
class UniquifyIoBlockNamesPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
// `inputBlockRenames` applies to blocks this stage CONSUMES and
|
||||
// `outputBlockRenames` to blocks it PRODUCES, both keyed by the block's
|
||||
// current name. `renamedBlockNames` receives the ORIGINAL names this stage
|
||||
// actually rewrote, so the caller can adopt the re-serialised module only
|
||||
// when there was something to rewrite.
|
||||
UniquifyIoBlockNamesPass(const std::map<String, String>& inputBlockRenames,
|
||||
const std::map<String, String>& outputBlockRenames,
|
||||
std::set<String>* renamedBlockNames)
|
||||
: m_inputBlockRenames(inputBlockRenames), m_outputBlockRenames(outputBlockRenames),
|
||||
m_renamedBlockNames(renamedBlockNames) {}
|
||||
|
||||
const char* name() const override { return "mobilegl-uniquify-io-block-names"; }
|
||||
Status Process() override;
|
||||
|
||||
// Reads a module WITHOUT rewriting it, for the caller's gate. Adds to
|
||||
// `outCollidingBlockNames` every block name this module declares in BOTH Input
|
||||
// and Output storage under two DIFFERENT struct types - the only shape the
|
||||
// rename above can repair - and to `outDeclaredNames` every name the module
|
||||
// spells, so the caller can pick a replacement that collides with none of them.
|
||||
// Builtin blocks (gl_PerVertex and friends) are never reported.
|
||||
static void ProbeIoBlockNames(spvtools::opt::IRContext* irContext,
|
||||
std::set<String>& outCollidingBlockNames,
|
||||
std::set<String>& outDeclaredNames);
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateUniquifyIoBlockNamesPass(
|
||||
const std::map<String, String>& inputBlockRenames,
|
||||
const std::map<String, String>& outputBlockRenames,
|
||||
std::set<String>* renamedBlockNames);
|
||||
|
||||
private:
|
||||
std::map<String, String> m_inputBlockRenames;
|
||||
std::map<String, String> m_outputBlockRenames;
|
||||
std::set<String>* m_renamedBlockNames = nullptr;
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -25,7 +25,8 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
//
|
||||
// 2: L2 gained atomicCounterEsslBindingTop (wave3's atomic-counter block rebinding
|
||||
// prints it into the emitted ESSL), and L1c was added.
|
||||
constexpr Uint32 kKeyLayoutVersion = 2u;
|
||||
// 3: L2 gained the two interface-block rename maps (wave4's UniquifyIoBlockNames).
|
||||
constexpr Uint32 kKeyLayoutVersion = 3u;
|
||||
|
||||
// The repo's existing cache epoch (MG_Config::CacheVersion, the seed
|
||||
// ProgramFactory::ComputeHash uses). Strictly redundant for an in-memory
|
||||
@@ -92,6 +93,14 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
for (const String& value : values) Text(value);
|
||||
}
|
||||
|
||||
void TranslationKeyBuilder::StringMap(const std::map<String, String>& map) {
|
||||
Value(static_cast<Uint64>(map.size()));
|
||||
for (const auto& [name, value] : map) {
|
||||
Text(name);
|
||||
Text(value);
|
||||
}
|
||||
}
|
||||
|
||||
void TranslationKeyBuilder::NameSet(const std::set<String>& names) {
|
||||
Value(static_cast<Uint64>(names.size()));
|
||||
for (const String& name : names) Text(name);
|
||||
@@ -167,6 +176,9 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
static const UnorderedMap<String, Int> kEmptyBindings;
|
||||
builder.NameMap(inputs.storageBlockBindingOverrides ? *inputs.storageBlockBindingOverrides
|
||||
: kEmptyBindings);
|
||||
static const std::map<String, String> kEmptyRenames;
|
||||
builder.StringMap(inputs.inputBlockRenames ? *inputs.inputBlockRenames : kEmptyRenames);
|
||||
builder.StringMap(inputs.outputBlockRenames ? *inputs.outputBlockRenames : kEmptyRenames);
|
||||
static const Vector<Uint32> kEmptyWords;
|
||||
builder.Words(inputs.spirv ? *inputs.spirv : kEmptyWords);
|
||||
return MakeTranslationCacheKey(builder);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <Includes.h>
|
||||
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
|
||||
@@ -130,6 +131,9 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
|
||||
// std::set is already ordered, but it gets the same length prefix.
|
||||
void NameSet(const std::set<String>& names);
|
||||
// std::map is ordered too, so it needs no sort - but both halves are TEXT, so each
|
||||
// gets its own length prefix and the pair cannot run into the next one.
|
||||
void StringMap(const std::map<String, String>& map);
|
||||
// ORDER-SENSITIVE, unlike NameMap: a transform-feedback capture list is a sequence,
|
||||
// and gl_NextBuffer / gl_SkipComponentsN make its order load-bearing.
|
||||
void TextList(const Vector<String>& values);
|
||||
@@ -553,6 +557,8 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// * the storage-block binding overrides handed to SPIRV-Cross;
|
||||
// * the atomic-counter binding top, which SetAtomicCounterBlockBindings turns into the
|
||||
// layout(binding=) qualifier every synthesized counter block is printed with;
|
||||
// * this stage's two interface-block rename maps, which UniquifyIoBlockNamesForEssl
|
||||
// turns into the block type names the emitted ESSL spells;
|
||||
// * the ESSL version SPIRV-Cross targets (ResolveBackendEsslVersion, i.e. the
|
||||
// driver's GLES version) - the remaining two SPIRV-Cross options are
|
||||
// compile-time constants (GLSL_ES true, VULKAN_SEMANTICS false);
|
||||
@@ -593,6 +599,14 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
const std::set<String>* xfbCaptureBlockNames = nullptr;
|
||||
const UnorderedMap<String, Uint>* glFormatByUniformName = nullptr;
|
||||
const UnorderedMap<String, Int>* storageBlockBindingOverrides = nullptr;
|
||||
// THIS STAGE's share of the program-wide interface-block rename plan - the two
|
||||
// arguments UniquifyIoBlockNamesForEssl is called with, which decide which block type
|
||||
// names the emitted ESSL spells. Empty for every program without a tessellation or
|
||||
// geometry stage that declares one block name in both directions, i.e. for all but a
|
||||
// handful. The maps rather than what they were derived from: they ARE the pass's
|
||||
// arguments, so they are exactly as fine as its behaviour and no finer.
|
||||
const std::map<String, String>* inputBlockRenames = nullptr;
|
||||
const std::map<String, String>* outputBlockRenames = nullptr;
|
||||
|
||||
// The top of the reserved storage-block window atomic-counter blocks are moved into
|
||||
// (`top - N` for GL binding N). Derived from the driver's
|
||||
|
||||
@@ -47,12 +47,18 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
// True when a packed internal format has REDUNDANT encodings, so decoding a texel and
|
||||
// re-encoding it keeps the VALUE but not the BITS. Only RGB9_E5 does: its shared exponent can
|
||||
// be lowered with the mantissas shifted up to match, and the spec's encoder always emits the
|
||||
// canonical form. RGB10_A2, RGB10_A2UI and R11F_G11F_B10F round-trip through float32
|
||||
// bit-exactly, so a GPU readback can answer for them.
|
||||
// canonical form, so no readback that goes through a decode cycle can return the stored words.
|
||||
//
|
||||
// This is what decides whether the CPU shadow has to stay authoritative for a format: a
|
||||
// readback of an RGB9_E5 level through a colour attachment cannot return the stored words, no
|
||||
// matter how well behaved the driver is.
|
||||
// Read this as "a FINITE value re-encodes to different bits", and nothing wider. This comment
|
||||
// used to assert that RGB10_A2, RGB10_A2UI and R11F_G11F_B10F "round-trip through float32
|
||||
// bit-exactly, so a GPU readback can answer for them", and that is false for
|
||||
// R11F_G11F_B10F: a field whose 5-bit exponent is all ones is an Inf or a NaN, and a NaN's
|
||||
// payload does not survive the trip (EncodeFloatToUnsignedSmallFloat re-encodes every NaN as
|
||||
// the canonical payload 1). glCopyImageSubData from an RGB9_E5 source produces exactly such a
|
||||
// word in the blue field on every texel, because the source's shared-exponent field is all
|
||||
// ones. The bit-exact answer for all four formats is the raw-word route,
|
||||
// DirectGLES::ReadPackedLevelWordsViaScratch; this predicate only picks which of the older
|
||||
// fallbacks to prefer when that route is unavailable.
|
||||
Bool HasRedundantPackedEncoding(TextureInternalFormat internalFormat);
|
||||
|
||||
// Decodes the canonical shadow-mip storage of `internalFormat` into wide RGBA texels for CPU
|
||||
|
||||
@@ -31,32 +31,41 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRgb16;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||
break;
|
||||
// The two render-target bits reach EVERY signed-normalized format, one-, two- and
|
||||
// four-channel included. They used to be granted to GL_RGB16_SNORM alone, which left the
|
||||
// other seven with no colour-renderable fallback at all on a driver without
|
||||
// EXT_render_snorm: an R8_SNORM or R16_SNORM attachment (what KHR-GL4x.texture_swizzle
|
||||
// renders into for every SNORM source format) got no substitute, so the ES framebuffer was
|
||||
// incomplete, the draw landed nowhere and the readback fell through to the never-written
|
||||
// CPU shadow.
|
||||
case GL_RGB16_SNORM:
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGB16Snorm;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
}
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
break;
|
||||
case GL_RGBA16_SNORM:
|
||||
case GL_RG16_SNORM:
|
||||
case GL_R16_SNORM:
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
break;
|
||||
case GL_RGBA8_SNORM:
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget;
|
||||
break;
|
||||
case GL_RGB8_SNORM:
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget;
|
||||
break;
|
||||
case GL_RG8_SNORM:
|
||||
case GL_R8_SNORM:
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget;
|
||||
break;
|
||||
// The rest of the three-channel formats no real ES driver renders to. They have no
|
||||
// other fallback: none of the driver/forced option bits names them, so before the
|
||||
@@ -113,9 +122,12 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
return {GL_RGBA16F, GL_RGBA, GL_FLOAT};
|
||||
case GL_RGB16_SNORM:
|
||||
// A half float loses the low bits of a 16-bit SNORM channel, so keep the
|
||||
// signed-normalized encoding whenever the driver can render to it.
|
||||
// signed-normalized encoding whenever the driver can render to it - and when it
|
||||
// cannot, widen to the 32-bit float, which is the only renderable storage that
|
||||
// still holds all 65535 channel values exactly. GL_RGBA16F here handed -23451/32767
|
||||
// back as -23457, six times the +/-1-step window KHR-GL4x.texture_swizzle allows.
|
||||
return (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)
|
||||
? ThreeChannelWidening{GL_RGBA16F, GL_RGBA, GL_FLOAT}
|
||||
? ThreeChannelWidening{GL_RGBA32F, GL_RGBA, GL_FLOAT}
|
||||
: ThreeChannelWidening{GL_RGBA16_SNORM, GL_RGBA, GL_SHORT};
|
||||
// Unsigned-normalized 16-bit (and the legacy 10/12-bit formats stored as RGB16):
|
||||
// GL_RGB32F is a legal ES texture format but is not colour-renderable either.
|
||||
@@ -203,7 +215,17 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
}
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
// NoSnorm16RenderTarget outranks the other two 16-bit fallbacks on purpose: it is the
|
||||
// only one whose substitute has to be EXACT, so it picks the 32-bit float rather than
|
||||
// the half the driver/ANGLE fallbacks settle for. The capability probe folds the
|
||||
// driver options and the render-target options into one set while the runtime storage
|
||||
// choice can see the render-target bit alone (GetRuntimeFallbackNormalizeOptions), so
|
||||
// the two would disagree on the storage format without a fixed precedence.
|
||||
case GL_RGBA16_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||
*outInternalFormat = GL_RGBA32F;
|
||||
break;
|
||||
}
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||
*outInternalFormat = GL_RGBA16F;
|
||||
@@ -212,6 +234,12 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
case GL_RGB16_SNORM:
|
||||
// The three-channel widening below replaces this whenever the target has to stay
|
||||
// renderable; GL_RGB32F keeps the precision for the targets that do not.
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||
*outInternalFormat = GL_RGB32F;
|
||||
break;
|
||||
}
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoRGB16Snorm) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||
@@ -221,6 +249,10 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
case GL_RG16_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||
*outInternalFormat = GL_RG32F;
|
||||
break;
|
||||
}
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||
*outInternalFormat = GL_RG16F;
|
||||
@@ -229,6 +261,10 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
case GL_R16_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||
*outInternalFormat = GL_R32F;
|
||||
break;
|
||||
}
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||
*outInternalFormat = GL_R16F;
|
||||
@@ -236,30 +272,36 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
}
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
// 8-bit SNORM: the half float already IS exact here, so the render-target bit lands on
|
||||
// the same storage the other two 8-bit fallbacks pick.
|
||||
case GL_RGBA8_SNORM:
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm)) {
|
||||
(options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||
*outInternalFormat = GL_RGBA16F;
|
||||
break;
|
||||
}
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
case GL_RGB8_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||
*outInternalFormat = GL_RGB16F;
|
||||
break;
|
||||
}
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
case GL_RG8_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||
*outInternalFormat = GL_RG16F;
|
||||
break;
|
||||
}
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
case GL_R8_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||
*outInternalFormat = GL_R16F;
|
||||
break;
|
||||
}
|
||||
@@ -270,10 +312,30 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
// per-channel precision (extra precision stays inside the CTS comparison epsilon, which is
|
||||
// derived from the requested format's bit widths). The upload (format, type) below matches
|
||||
// the canonical shadow layout in PixelStoreProcessor (UNorm8 / UNorm16 component arrays).
|
||||
//
|
||||
// The <=8-bit ones land on the 8-bit-per-channel storage that layout ALREADY is, rather
|
||||
// than on the narrower GL_RGB565/GL_RGBA4 they nominally fit in. Storing them narrower
|
||||
// made the driver requantize the UNorm8 shadow bytes on every upload, and that step is
|
||||
// exact only by luck: 5-bit value 2 encodes as UNorm8 16, and 16/255*31 = 1.945 sits
|
||||
// astride the 5-bit boundary, so a driver that truncates hands back 1 (all twelve
|
||||
// KHR-GL43.copy_image.functional rgb4->rgb4 cases fail on Mali, at verify()'s FIRST
|
||||
// check - a plain glTexImage/glGetTexImage round trip with no copy involved). The
|
||||
// 8-bit store removes the requantization entirely; the client word round-trips
|
||||
// exactly, because encoding an n-bit field to UNorm8 with rounding and back is the
|
||||
// identity for every n <= 8. It is also what DirectVulkan has always done with them
|
||||
// (VkTextureManager::ResolveTextureFormatInfo resolves all six legacy low-bit formats
|
||||
// to R8G8B8A8_UNORM), so the two backends now agree here.
|
||||
//
|
||||
// Only the DESKTOP-ONLY formats move. GL_RGBA4 and GL_RGB5_A1 are ES formats an
|
||||
// application can legitimately ask for - the same normalization picks the storage for
|
||||
// glRenderbufferStorage - so widening them would be a memory decision, not a
|
||||
// correctness one. Nothing about the REPORTED precision moves either way:
|
||||
// GL_TEXTURE_*_SIZE and glGetInternalformativ answer from TextureMetrics, keyed on the
|
||||
// requested format, not on the ES storage.
|
||||
case GL_R3_G3_B2:
|
||||
case GL_RGB4:
|
||||
case GL_RGB5:
|
||||
*outInternalFormat = GL_RGB565;
|
||||
*outInternalFormat = GL_RGB8;
|
||||
break;
|
||||
case GL_RGB10:
|
||||
case GL_RGB12:
|
||||
@@ -283,7 +345,7 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
: GL_RGB16;
|
||||
break;
|
||||
case GL_RGBA2:
|
||||
*outInternalFormat = GL_RGBA4;
|
||||
*outInternalFormat = GL_RGBA8;
|
||||
break;
|
||||
case GL_RGBA12:
|
||||
*outInternalFormat =
|
||||
@@ -513,7 +575,8 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||
(internalFormat == GL_RGB16_SNORM &&
|
||||
(options & PixelFormatNormalizeOptionBit::NoRGB16Snorm)) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)) {
|
||||
*outType = GL_FLOAT;
|
||||
break;
|
||||
} else {
|
||||
@@ -523,7 +586,8 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
case GL_RGB8_SNORM:
|
||||
case GL_RG8_SNORM:
|
||||
case GL_R8_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||
*outType = GL_FLOAT;
|
||||
break;
|
||||
}
|
||||
@@ -531,7 +595,8 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
break;
|
||||
case GL_RGBA8_SNORM:
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm)) {
|
||||
(options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||
*outType = GL_FLOAT;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -29,11 +29,21 @@ namespace MobileGL {
|
||||
// three-channel client data with an alpha of 1.0, and sampling/readback has to hide
|
||||
// the added alpha again (BackendTextureFormatAddsAlpha).
|
||||
NoThreeChannelRenderTarget = 1 << 7,
|
||||
// Pairs with the bit above: the widened four-channel format has to stay renderable AND
|
||||
// keep 16-bit signed-normalized precision, which needs both EXT_texture_norm16 and
|
||||
// EXT_render_snorm. Without them the only renderable widening left is a half float, whose
|
||||
// 11-bit mantissa cannot represent a 16-bit SNORM channel exactly.
|
||||
// A 16-bit signed-normalized image has to back a colour attachment, and the driver cannot
|
||||
// render to the signed-normalized encoding itself: that needs both EXT_texture_norm16 and
|
||||
// EXT_render_snorm, and without either one an R16_SNORM / RG16_SNORM / RGB16_SNORM /
|
||||
// RGBA16_SNORM attachment is texture-only, so the framebuffer is never complete and the
|
||||
// draw silently lands nowhere. The substitute is a 32-bit float, NOT the half float the
|
||||
// other SNORM fallbacks use: a half's 11-bit mantissa cannot represent a 16-bit SNORM
|
||||
// channel exactly - its spacing just below 1.0 is 2^-11, some 16 SNORM steps, so
|
||||
// -23451/32767 comes back as -23457 - while a 32-bit float round-trips every one of the
|
||||
// 65535 channel values bit for bit.
|
||||
NoSnorm16RenderTarget = 1 << 8,
|
||||
// The 8-bit twin of the bit above: without EXT_render_snorm an R8_SNORM / RG8_SNORM /
|
||||
// RGB8_SNORM / RGBA8_SNORM colour attachment is not renderable either. Here a half float
|
||||
// IS exact - every value in [-127, 127] divided by 127 round-trips through a half - so the
|
||||
// substitute matches what the always-on GL_RGBA8_SNORM fallback already picks.
|
||||
NoSnorm8RenderTarget = 1 << 9,
|
||||
None = 0,
|
||||
};
|
||||
namespace MG_Util::TextureFormatProcessor {
|
||||
|
||||
Reference in New Issue
Block a user