mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 12:48:32 +09:00
Merge branch "feat/fp64-demote-to-fp32" into dev
This commit is contained in:
@@ -276,6 +276,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
|
||||
|
||||
@@ -82,6 +82,14 @@ namespace MobileGL::MG_Config {
|
||||
#endif
|
||||
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
|
||||
Bool DisableSubgroup = false;
|
||||
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
||||
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
||||
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
||||
// promises 64-bit precision, and that is the one thing the narrowing cannot deliver.
|
||||
// Off by default so an application that checks the string before using doubles keeps
|
||||
// its float path; on for measuring what the conformance suite makes of the demoted
|
||||
// precision. See the DemoteFloat64Pass header and the "fp64" POST row.
|
||||
Bool AdvertiseFp64 = false;
|
||||
// MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
|
||||
Bool MagmaR11G11B10FFallback = false;
|
||||
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
||||
|
||||
@@ -167,6 +167,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
||||
#endif
|
||||
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
|
||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
features.AvoidSamplerMipmapMinFilter =
|
||||
|
||||
@@ -960,6 +960,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
||||
}
|
||||
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). Every `double` in a
|
||||
// shader compiles and runs already - it is narrowed to 32 bits before the module
|
||||
// reaches this backend - so an application that simply uses doubles needs nothing
|
||||
// advertised. What the extension additionally promises is 64-bit PRECISION, which no
|
||||
// mobile GPU has and the narrowing cannot fake, so advertising it by default would
|
||||
// make an application that checks the string take a path MobileGL cannot honour.
|
||||
if (MG_Config::Features.AdvertiseFp64) {
|
||||
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
|
||||
}
|
||||
// Only advertised when the device driver actually has usable timer queries
|
||||
// (GL_EXT_disjoint_timer_query plus its entry points) and the
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
||||
|
||||
@@ -539,6 +539,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
||||
}
|
||||
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). Every `double` in a
|
||||
// shader compiles and runs already - it is narrowed to 32 bits before the module
|
||||
// reaches this backend - so an application that simply uses doubles needs nothing
|
||||
// advertised. What the extension additionally promises is 64-bit PRECISION, which no
|
||||
// mobile GPU has and the narrowing cannot fake, so advertising it by default would
|
||||
// make an application that checks the string take a path MobileGL cannot honour.
|
||||
if (MG_Config::Features.AdvertiseFp64) {
|
||||
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
|
||||
}
|
||||
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension string);
|
||||
// only advertised when the device actually supports timestamp queries and the
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
||||
@@ -877,7 +886,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||
}
|
||||
}
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = m_vulkanCaps.SupportsShaderFloat64;
|
||||
// Never, on any device, and no longer for the reason it used to be. It used to track
|
||||
// shaderFloat64 because a `dvec3` input needed the Float64 capability to exist in the
|
||||
// module at all; a 64-bit vertex FETCH was already impossible (VK_FORMAT_R64*_SFLOAT is
|
||||
// optional and lavapipe reports zero bufferFeatures for all four), so the attribute
|
||||
// arrived as its 32-bit word pair and PackDoubleVertexInputsPass bitcast it back.
|
||||
//
|
||||
// The shader half of that is gone: every 64-bit float is narrowed before any module
|
||||
// reaches a backend (ShaderTranspiler::DemoteFloat64Pass), so there is no `double` input
|
||||
// left to bitcast INTO, and feeding a UINT-formatted attribute to what is now a `float`
|
||||
// input would be silent garbage. Reconstructing the value would mean decoding the
|
||||
// IEEE-754 double bit pattern in the shader - software fp64, which is precisely what the
|
||||
// demotion exists to avoid - and on Espryt it would additionally need the ES driver to
|
||||
// fetch 2N uint components where the application declared N doubles, which a dvec3 or
|
||||
// dvec4 cannot even express within one attribute location.
|
||||
//
|
||||
// So glVertexAttribLFormat / glVertexAttribLPointer are declined here exactly as they
|
||||
// already were on Espryt and on every real mobile device (Adreno and Mali both report
|
||||
// shaderFloat64 == VK_FALSE), and for the same visible reason. A `dvec3` INPUT still
|
||||
// compiles and draws - it is a `vec3` after demotion - as long as the application feeds
|
||||
// it with glVertexAttribPointer(GL_FLOAT) rather than 64-bit data.
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||
m_dynamicParameters.MaxShaderStorageBlockSize =
|
||||
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
||||
if (m_vulkanCaps.SupportsShaderSubgroup) {
|
||||
|
||||
@@ -850,7 +850,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// vector per column - while the value glGetUniform* must return is tightly packed
|
||||
// columns * rows floats. Only mat4 is the same either way; every other shape needs the
|
||||
// padding undone, and the readback has to undo exactly what UniformMatrixfv_Object put
|
||||
// there. Returns false when `ttype` is not a float matrix (nothing to unpack).
|
||||
// there. Returns false when there is nothing here to unpack.
|
||||
//
|
||||
// A DOUBLE matrix is declined not because it is laid out differently - it is not, the
|
||||
// demotion makes a dmat4 a mat4 in the shader and a mat4-shaped slot here - but because it
|
||||
// is ROUTED differently: the caller's component-by-component EbtDouble branch has to widen
|
||||
// each float back to the queried type, and it undoes the same padding itself.
|
||||
Bool TryGatherFloatMatrixColumns(const glslang::TType* ttype, const char* pBase, void* params) {
|
||||
if (ttype == nullptr || !ttype->isMatrix() || ttype->getBasicType() == glslang::EbtDouble) return false;
|
||||
const Int columns = ttype->getMatrixCols();
|
||||
@@ -909,7 +914,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) {
|
||||
Memcpy(params, pUBO + offset, size);
|
||||
// Never more than the uniform actually occupies. `size` is the GL type size,
|
||||
// which for a `double` uniform is twice its storage - every 64-bit float is
|
||||
// narrowed before the module reaches a backend, so the slot holds floats. The
|
||||
// typed entry points (glGetUniformdv and friends) go through
|
||||
// GetUniformScalar_State, which converts component by component; this raw
|
||||
// copy has no type to convert with, so it is bounded rather than converted.
|
||||
Memcpy(params, pUBO + offset, std::min<SizeT>(size, span));
|
||||
}
|
||||
}
|
||||
// TODO: handle 1i variant as texture unit
|
||||
@@ -960,22 +971,27 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return;
|
||||
}
|
||||
|
||||
// A double-precision uniform is the one case where the stored component type can
|
||||
// differ from the queried one for a non-opaque uniform, and the difference is not
|
||||
// just a reinterpretation: it is twice as wide, so a raw copy would overrun the
|
||||
// caller's buffer as well as return nonsense. Read component by component and let
|
||||
// GL's conversion rules (7.6: round to nearest for the integer queries) apply.
|
||||
// A double-precision uniform is the one case where the stored component type differs
|
||||
// from the DECLARED one for a non-opaque uniform: the shader's 64-bit floats are
|
||||
// narrowed to 32 bits before the module reaches a backend
|
||||
// (ShaderTranspiler::DemoteFloat64Pass), so what is in the global UBO is a float per
|
||||
// component, laid out exactly like the float-typed twin of this uniform - std140
|
||||
// 16-byte column stride for a matrix included. Reading it as a GLdouble would return
|
||||
// two components reinterpreted as one. Read component by component and let GL's
|
||||
// conversion rules (7.6: round to nearest for the integer queries) apply; the value
|
||||
// widens back to the queried type, having lost precision at the glUniform*d that
|
||||
// stored it and not here.
|
||||
if (ttype->getBasicType() == glslang::EbtDouble) {
|
||||
const Int columns = ttype->isMatrix() ? ttype->getMatrixCols() : 1;
|
||||
const Int rows = ttype->isMatrix() ? ttype->getMatrixRows()
|
||||
: (ttype->isVector() ? ttype->getVectorSize() : 1);
|
||||
// The slot the linker handed out is exactly `columns` columns wide, so it also
|
||||
// states the column stride - which for a double matrix is not a float's 16 bytes.
|
||||
const SizeT columnStride = columns > 0 ? size / static_cast<SizeT>(columns) : size;
|
||||
// std140 gives every matrix column its own 16-byte slot; a non-matrix is one
|
||||
// tightly packed run and never reaches the stride at all.
|
||||
const SizeT columnStride = 4 * sizeof(GLfloat);
|
||||
for (Int column = 0; column < columns; ++column) {
|
||||
for (Int row = 0; row < rows; ++row) {
|
||||
GLdouble component = 0.0;
|
||||
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLdouble),
|
||||
GLfloat component = 0.0f;
|
||||
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLfloat),
|
||||
sizeof(component));
|
||||
if constexpr (std::is_integral_v<T>) {
|
||||
// Rounded to the nearest integer and clamped into the queried type's
|
||||
@@ -1248,36 +1264,39 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// glUniform*d / glUniformMatrix*dv. The vector forms need nothing beyond the shared
|
||||
// upload template - it is already typed on the component - but a matrix does: the
|
||||
// column stride the linker used for a double matrix is not the 16 bytes a float one
|
||||
// gets. It is not guessed here; the slot the uniform was given is exactly `columns`
|
||||
// columns wide, so dividing states the stride the rest of the pipeline agreed on.
|
||||
template <typename Program>
|
||||
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value, Int columns, Int rows) {
|
||||
const SizeT slotSize = programObject.GetUniformSizesInBytes(location);
|
||||
const SizeT columnStride = columns > 0 ? slotSize / static_cast<SizeT>(columns) : slotSize;
|
||||
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
||||
Vector<GLdouble> column(static_cast<SizeT>(rows));
|
||||
for (GLint matrix = 0; matrix < count; ++matrix) {
|
||||
if (matrix > 0 && !programObject.UniformLocationsAliasSameUniform(location, location + matrix)) break;
|
||||
if (!programObject.IsValidUniformLocation(location + matrix)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + matrix, "the current program object");
|
||||
return;
|
||||
}
|
||||
const GLdouble* source = value + matrix * componentCount;
|
||||
for (Int c = 0; c < columns; ++c) {
|
||||
for (Int r = 0; r < rows; ++r) {
|
||||
column[r] = transpose == GL_TRUE ? source[r * columns + c] : source[c * rows + r];
|
||||
}
|
||||
Uniform_State<1>(programObject, location + matrix, column.data(), c * columnStride);
|
||||
for (Int r = 1; r < rows; ++r) {
|
||||
Uniform_State<1>(programObject, location + matrix, column.data() + r,
|
||||
c * columnStride + r * sizeof(GLdouble));
|
||||
}
|
||||
}
|
||||
// glUniform*d / glUniformMatrix*dv. Neither needs a layout of its own any more: the
|
||||
// transpile chain narrows every 64-bit float in the shader to 32 bits
|
||||
// (ShaderTranspiler::DemoteFloat64Pass) and the global UBO is laid out by reflecting that
|
||||
// demoted module, so a double uniform's storage IS a float uniform's - same offset, same
|
||||
// 4-byte components, same std140 column padding for matrices. Narrowing here, at the one
|
||||
// place the 64-bit value enters, and then handing the bytes to the ordinary float upload
|
||||
// path is what keeps the two in step; a separate double-shaped layout here would write
|
||||
// 8-byte components into 4-byte slots and silently address the wrong ones.
|
||||
//
|
||||
// The narrowing is the same static_cast the shader's own arithmetic now performs, so the
|
||||
// value the shader reads is the value glUniform*d was given, at float precision.
|
||||
template <GLsizei ItemCount>
|
||||
void UniformvNarrowed_State(GLint location, GLsizei count, const GLdouble* value) {
|
||||
if (value == nullptr || count <= 0) {
|
||||
// Same shape as the float entry points: the location validation still runs, and a
|
||||
// null pointer is left to fault exactly where glUniform*fv would.
|
||||
Uniformv_State<ItemCount>(location, count, reinterpret_cast<const GLfloat*>(value));
|
||||
return;
|
||||
}
|
||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
|
||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||
Uniformv_State<ItemCount>(location, count, narrowed.data());
|
||||
}
|
||||
|
||||
template <GLsizei ItemCount>
|
||||
void ProgramUniformvNarrowed_State(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||
if (value == nullptr || count <= 0) {
|
||||
ProgramUniformv_State<ItemCount>(program, location, count, reinterpret_cast<const GLfloat*>(value));
|
||||
return;
|
||||
}
|
||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
|
||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||
ProgramUniformv_State<ItemCount>(program, location, count, narrowed.data());
|
||||
}
|
||||
|
||||
// glUniformMatrix*fv / glProgramUniformMatrix*fv, every shape (square and non-square).
|
||||
@@ -1326,6 +1345,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// glUniformMatrix*dv / glProgramUniformMatrix*dv. Narrowed to the float form and handed
|
||||
// straight to it: after DemoteFloat64Pass a `dmat4` uniform is a `mat4` in the shader and a
|
||||
// mat4-shaped slot in the global UBO, columns padded to a vec4 and all. Everything else
|
||||
// about the call - transpose handling, the array-element walk, the opaque-uniform refusal -
|
||||
// is then the one implementation both spellings share.
|
||||
template <typename Program>
|
||||
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value, Int columns, Int rows) {
|
||||
if (value == nullptr || count <= 0) return;
|
||||
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * componentCount);
|
||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||
UniformMatrixfv_Object(programObject, "glUniformMatrixdv", location, count, transpose, narrowed.data(),
|
||||
columns, rows, "the current program object");
|
||||
}
|
||||
|
||||
// Helper function to transpose a 2x2 matrix
|
||||
void TransposeMatrix2x2(const GLfloat* input, GLfloat* output) {
|
||||
// Input matrix is in column-major order (OpenGL default)
|
||||
@@ -2089,71 +2124,71 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
void Uniform1d(GLint location, GLdouble v0) {
|
||||
const GLdouble v[] = {v0};
|
||||
Uniformv_State<1>(location, 1, v);
|
||||
UniformvNarrowed_State<1>(location, 1, v);
|
||||
}
|
||||
|
||||
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value) {
|
||||
Uniformv_State<1>(location, count, value);
|
||||
UniformvNarrowed_State<1>(location, count, value);
|
||||
}
|
||||
|
||||
void ProgramUniform1d(GLuint program, GLint location, GLdouble v0) {
|
||||
const GLdouble v[] = {v0};
|
||||
ProgramUniformv_State<1>(program, location, 1, v);
|
||||
ProgramUniformvNarrowed_State<1>(program, location, 1, v);
|
||||
}
|
||||
|
||||
void ProgramUniform1dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||
ProgramUniformv_State<1>(program, location, count, value);
|
||||
ProgramUniformvNarrowed_State<1>(program, location, count, value);
|
||||
}
|
||||
void Uniform2d(GLint location, GLdouble v0, GLdouble v1) {
|
||||
const GLdouble v[] = {v0, v1};
|
||||
Uniformv_State<2>(location, 1, v);
|
||||
UniformvNarrowed_State<2>(location, 1, v);
|
||||
}
|
||||
|
||||
void Uniform2dv(GLint location, GLsizei count, const GLdouble* value) {
|
||||
Uniformv_State<2>(location, count, value);
|
||||
UniformvNarrowed_State<2>(location, count, value);
|
||||
}
|
||||
|
||||
void ProgramUniform2d(GLuint program, GLint location, GLdouble v0, GLdouble v1) {
|
||||
const GLdouble v[] = {v0, v1};
|
||||
ProgramUniformv_State<2>(program, location, 1, v);
|
||||
ProgramUniformvNarrowed_State<2>(program, location, 1, v);
|
||||
}
|
||||
|
||||
void ProgramUniform2dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||
ProgramUniformv_State<2>(program, location, count, value);
|
||||
ProgramUniformvNarrowed_State<2>(program, location, count, value);
|
||||
}
|
||||
void Uniform3d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2) {
|
||||
const GLdouble v[] = {v0, v1, v2};
|
||||
Uniformv_State<3>(location, 1, v);
|
||||
UniformvNarrowed_State<3>(location, 1, v);
|
||||
}
|
||||
|
||||
void Uniform3dv(GLint location, GLsizei count, const GLdouble* value) {
|
||||
Uniformv_State<3>(location, count, value);
|
||||
UniformvNarrowed_State<3>(location, count, value);
|
||||
}
|
||||
|
||||
void ProgramUniform3d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2) {
|
||||
const GLdouble v[] = {v0, v1, v2};
|
||||
ProgramUniformv_State<3>(program, location, 1, v);
|
||||
ProgramUniformvNarrowed_State<3>(program, location, 1, v);
|
||||
}
|
||||
|
||||
void ProgramUniform3dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||
ProgramUniformv_State<3>(program, location, count, value);
|
||||
ProgramUniformvNarrowed_State<3>(program, location, count, value);
|
||||
}
|
||||
void Uniform4d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) {
|
||||
const GLdouble v[] = {v0, v1, v2, v3};
|
||||
Uniformv_State<4>(location, 1, v);
|
||||
UniformvNarrowed_State<4>(location, 1, v);
|
||||
}
|
||||
|
||||
void Uniform4dv(GLint location, GLsizei count, const GLdouble* value) {
|
||||
Uniformv_State<4>(location, count, value);
|
||||
UniformvNarrowed_State<4>(location, count, value);
|
||||
}
|
||||
|
||||
void ProgramUniform4d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) {
|
||||
const GLdouble v[] = {v0, v1, v2, v3};
|
||||
ProgramUniformv_State<4>(program, location, 1, v);
|
||||
ProgramUniformvNarrowed_State<4>(program, location, 1, v);
|
||||
}
|
||||
|
||||
void ProgramUniform4dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||
ProgramUniformv_State<4>(program, location, count, value);
|
||||
ProgramUniformvNarrowed_State<4>(program, location, count, value);
|
||||
}
|
||||
void UniformMatrix2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
|
||||
@@ -61,6 +61,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||
Scenarios/DepthStencilReadbackScenario.cpp
|
||||
Scenarios/SsboArrayLengthScenario.cpp
|
||||
Scenarios/DoublePrecisionScenario.cpp
|
||||
Scenarios/UniformInitializerScenario.cpp
|
||||
Scenarios/SwizzleAccessRoutineScenario.cpp
|
||||
Scenarios/ProgramPipelineScenario.cpp
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DoublePrecisionScenario.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 - GLSL DOUBLES, RUN AT SINGLE PRECISION.
|
||||
//
|
||||
// No mobile GPU has 64-bit floats. Adreno and Mali both report shaderFloat64 == VK_FALSE, so
|
||||
// Magma cannot build a module that declares the Float64 capability, and ESSL has no fp64 type
|
||||
// at all, so SPIRV-Cross refuses the module outright on Espryt ("FP64 not supported in ES
|
||||
// profile") and the program never reaches the driver. MobileGL therefore narrows every 64-bit
|
||||
// float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than declining the
|
||||
// shader: `double` compiles and runs everywhere, at float precision.
|
||||
//
|
||||
// The narrowing is only half a contract. The other half is the API side: the global UBO is
|
||||
// laid out by reflecting the DEMOTED module, so glUniform*d has to store a float where the
|
||||
// shader reads a float, glGetUniform*v has to read one back, and a dmat4's columns are now
|
||||
// std140-padded like any other matrix's. Every one of those is a byte offset that fails
|
||||
// silently - the uniform simply reads as something else - so the cases below set values
|
||||
// through the API and have the SHADER report what it saw.
|
||||
//
|
||||
// What is deliberately NOT asserted: that the values are exact to double precision. They are
|
||||
// not, and cannot be. Every expectation here is the float value of the double that was set,
|
||||
// which is the whole point.
|
||||
|
||||
#include <cmath>
|
||||
#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 {
|
||||
|
||||
// Doubles in every shape the demotion has to handle - a scalar, a vector, a matrix
|
||||
// whose column stride changes, an array whose element stride changes - all reported
|
||||
// through one float SSBO so a single readback says which one moved.
|
||||
constexpr const char* kComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
uniform double uScalar;
|
||||
uniform dvec3 uVector;
|
||||
uniform dmat4 uMatrix;
|
||||
uniform double uArray[3];
|
||||
layout(std430, binding = 0) buffer Output {
|
||||
float g_out[];
|
||||
};
|
||||
void main() {
|
||||
g_out[0] = float(uScalar);
|
||||
g_out[1] = float(uVector.x);
|
||||
g_out[2] = float(uVector.y);
|
||||
g_out[3] = float(uVector.z);
|
||||
// Column-major [column][row]. Off-diagonal entries catch a column-stride mistake that a
|
||||
// diagonal-only check reads straight past.
|
||||
g_out[4] = float(uMatrix[0][0]);
|
||||
g_out[5] = float(uMatrix[0][3]);
|
||||
g_out[6] = float(uMatrix[3][0]);
|
||||
g_out[7] = float(uMatrix[3][3]);
|
||||
g_out[8] = float(uArray[0]);
|
||||
g_out[9] = float(uArray[1]);
|
||||
g_out[10] = float(uArray[2]);
|
||||
// Arithmetic on doubles, including an implicit float->double conversion and a literal
|
||||
// with the fp64 suffix: this is what an application actually writes, and it is the part
|
||||
// that has to survive the conversion folding.
|
||||
double accumulated = uScalar * 2.0lf + 1.5;
|
||||
g_out[11] = float(accumulated);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr int kOutputSlots = 12;
|
||||
|
||||
class DoublePrecisionScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_program = CompileComputeProgram(kComputeSource);
|
||||
ASSERT_NE(m_program, 0u) << m_buildLog;
|
||||
|
||||
glGenBuffers(1, &m_output);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
||||
const std::vector<float> zeroes(kOutputSlots, 0.0f);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, kOutputSlots * sizeof(float), zeroes.data(),
|
||||
GL_DYNAMIC_DRAW);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_output);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
if (m_output != 0) glDeleteBuffers(1, &m_output);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
}
|
||||
|
||||
unsigned int CompileComputeProgram(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
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("compute shader did not compile: ") + log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
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("compute program did not link: ") + log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
std::vector<float> Dispatch() {
|
||||
glUseProgram(m_program);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
std::vector<float> values(kOutputSlots, -1.0f);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, kOutputSlots * sizeof(float), values.data());
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
glUseProgram(0);
|
||||
return values;
|
||||
}
|
||||
|
||||
unsigned int m_program = 0;
|
||||
unsigned int m_output = 0;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
TEST_F(DoublePrecisionScenario, ADoubleUniformReachesTheShaderAtFloatPrecision) {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(m_program);
|
||||
const GLint scalar = glGetUniformLocation(m_program, "uScalar");
|
||||
ASSERT_GE(scalar, 0);
|
||||
// 0.1 has no exact float (or double) representation, so this only passes if the
|
||||
// value really travelled through the demoted slot rather than being read out of
|
||||
// some other four bytes.
|
||||
glUniform1d(scalar, 0.1);
|
||||
glUseProgram(0);
|
||||
|
||||
const std::vector<float> values = Dispatch();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_FLOAT_EQ(values[0], static_cast<float>(0.1));
|
||||
EXPECT_FLOAT_EQ(values[11], static_cast<float>(static_cast<float>(0.1) * 2.0f + 1.5f))
|
||||
<< "arithmetic on the demoted value, including the folded fp64 literal";
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, EveryDoubleShapeLandsInItsOwnSlot) {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(m_program);
|
||||
const GLint scalar = glGetUniformLocation(m_program, "uScalar");
|
||||
const GLint vector = glGetUniformLocation(m_program, "uVector");
|
||||
const GLint matrix = glGetUniformLocation(m_program, "uMatrix");
|
||||
const GLint array0 = glGetUniformLocation(m_program, "uArray[0]");
|
||||
const GLint array2 = glGetUniformLocation(m_program, "uArray[2]");
|
||||
ASSERT_GE(scalar, 0);
|
||||
ASSERT_GE(vector, 0);
|
||||
ASSERT_GE(matrix, 0);
|
||||
ASSERT_GE(array0, 0);
|
||||
ASSERT_GE(array2, 0);
|
||||
|
||||
glUniform1d(scalar, 5.0);
|
||||
const GLdouble vectorValue[3] = {11.0, 12.0, 13.0};
|
||||
glUniform3dv(vector, 1, vectorValue);
|
||||
// Column-major, and every entry distinct so a transposed or mis-strided write
|
||||
// cannot land on a value that happens to match.
|
||||
GLdouble matrixValue[16] = {};
|
||||
for (int i = 0; i < 16; ++i) matrixValue[i] = 100.0 + i;
|
||||
glUniformMatrix4dv(matrix, 1, GL_FALSE, matrixValue);
|
||||
const GLdouble arrayValue[3] = {71.0, 72.0, 73.0};
|
||||
glUniform1dv(array0, 3, arrayValue);
|
||||
glUseProgram(0);
|
||||
|
||||
const std::vector<float> values = Dispatch();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_FLOAT_EQ(values[0], 5.0f) << "scalar double";
|
||||
EXPECT_FLOAT_EQ(values[1], 11.0f) << "dvec3 .x";
|
||||
EXPECT_FLOAT_EQ(values[2], 12.0f) << "dvec3 .y";
|
||||
EXPECT_FLOAT_EQ(values[3], 13.0f) << "dvec3 .z";
|
||||
EXPECT_FLOAT_EQ(values[4], 100.0f) << "dmat4 [0][0]";
|
||||
EXPECT_FLOAT_EQ(values[5], 103.0f) << "dmat4 [0][3] - within the first column";
|
||||
EXPECT_FLOAT_EQ(values[6], 112.0f) << "dmat4 [3][0] - column stride";
|
||||
EXPECT_FLOAT_EQ(values[7], 115.0f) << "dmat4 [3][3]";
|
||||
EXPECT_FLOAT_EQ(values[8], 71.0f) << "double array element 0";
|
||||
EXPECT_FLOAT_EQ(values[9], 72.0f) << "double array element 1 - element stride";
|
||||
EXPECT_FLOAT_EQ(values[10], 73.0f) << "double array element 2";
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, TheTransposeFlagStillTransposes) {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(m_program);
|
||||
const GLint matrix = glGetUniformLocation(m_program, "uMatrix");
|
||||
ASSERT_GE(matrix, 0);
|
||||
GLdouble matrixValue[16] = {};
|
||||
for (int i = 0; i < 16; ++i) matrixValue[i] = 100.0 + i;
|
||||
glUniformMatrix4dv(matrix, 1, GL_TRUE, matrixValue);
|
||||
glUseProgram(0);
|
||||
|
||||
const std::vector<float> values = Dispatch();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
// Transposed, so [column][row] now reads the source's [row][column].
|
||||
EXPECT_FLOAT_EQ(values[4], 100.0f) << "dmat4 [0][0] is on the diagonal either way";
|
||||
EXPECT_FLOAT_EQ(values[5], 112.0f) << "dmat4 [0][3] after transpose";
|
||||
EXPECT_FLOAT_EQ(values[6], 103.0f) << "dmat4 [3][0] after transpose";
|
||||
EXPECT_FLOAT_EQ(values[7], 115.0f) << "dmat4 [3][3] is on the diagonal either way";
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, TheUniformIsStillReportedAsADouble) {
|
||||
if (!Ready()) return;
|
||||
// The demotion is an implementation detail of how the value is STORED. What the
|
||||
// shader source declared is what the application asked about, so the reflection
|
||||
// keeps answering GL_DOUBLE* - an application that switches on the type and calls
|
||||
// glUniform*d has to keep working, and it is the glUniform*d path that is correct
|
||||
// for these uniforms.
|
||||
struct Expectation {
|
||||
const char* name;
|
||||
GLenum type;
|
||||
GLint size;
|
||||
};
|
||||
const Expectation expectations[] = {
|
||||
{"uScalar", GL_DOUBLE, 1},
|
||||
{"uVector", GL_DOUBLE_VEC3, 1},
|
||||
{"uMatrix", GL_DOUBLE_MAT4, 1},
|
||||
{"uArray[0]", GL_DOUBLE, 3},
|
||||
};
|
||||
|
||||
GLint activeUniforms = 0;
|
||||
glGetProgramiv(m_program, GL_ACTIVE_UNIFORMS, &activeUniforms);
|
||||
ASSERT_GT(activeUniforms, 0);
|
||||
|
||||
for (const Expectation& expectation : expectations) {
|
||||
bool found = false;
|
||||
for (GLint index = 0; index < activeUniforms; ++index) {
|
||||
char name[128] = {};
|
||||
GLsizei length = 0;
|
||||
GLint size = 0;
|
||||
GLenum type = 0;
|
||||
glGetActiveUniform(m_program, static_cast<GLuint>(index), sizeof(name) - 1, &length, &size,
|
||||
&type, name);
|
||||
if (std::string(name, static_cast<size_t>(length)) != expectation.name) continue;
|
||||
found = true;
|
||||
EXPECT_EQ(type, expectation.type) << expectation.name;
|
||||
EXPECT_EQ(size, expectation.size) << expectation.name;
|
||||
break;
|
||||
}
|
||||
EXPECT_TRUE(found) << "glGetActiveUniform never reported " << expectation.name;
|
||||
}
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, GetUniformdvReadsBackWhatWasStored) {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(m_program);
|
||||
const GLint scalar = glGetUniformLocation(m_program, "uScalar");
|
||||
const GLint vector = glGetUniformLocation(m_program, "uVector");
|
||||
const GLint matrix = glGetUniformLocation(m_program, "uMatrix");
|
||||
ASSERT_GE(scalar, 0);
|
||||
ASSERT_GE(vector, 0);
|
||||
ASSERT_GE(matrix, 0);
|
||||
glUniform1d(scalar, 0.1);
|
||||
const GLdouble vectorValue[3] = {11.5, 12.5, 13.5};
|
||||
glUniform3dv(vector, 1, vectorValue);
|
||||
GLdouble matrixValue[16] = {};
|
||||
for (int i = 0; i < 16; ++i) matrixValue[i] = 100.0 + i;
|
||||
glUniformMatrix4dv(matrix, 1, GL_FALSE, matrixValue);
|
||||
glUseProgram(0);
|
||||
|
||||
// The readback has to undo exactly what the write did - the same std140 column
|
||||
// padding, the same 4-byte components - or a dmat4 comes back with its columns
|
||||
// shifted and nothing else in the API would say so.
|
||||
GLdouble readScalar = 0.0;
|
||||
glGetUniformdv(m_program, scalar, &readScalar);
|
||||
EXPECT_DOUBLE_EQ(readScalar, static_cast<double>(static_cast<float>(0.1)))
|
||||
<< "the value is what a float can hold, not the double that was passed in";
|
||||
|
||||
GLdouble readVector[3] = {};
|
||||
glGetUniformdv(m_program, vector, readVector);
|
||||
EXPECT_DOUBLE_EQ(readVector[0], 11.5);
|
||||
EXPECT_DOUBLE_EQ(readVector[1], 12.5);
|
||||
EXPECT_DOUBLE_EQ(readVector[2], 13.5);
|
||||
|
||||
GLdouble readMatrix[16] = {};
|
||||
glGetUniformdv(m_program, matrix, readMatrix);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
EXPECT_DOUBLE_EQ(readMatrix[i], 100.0 + i) << "dmat4 component " << i;
|
||||
}
|
||||
|
||||
// The float query sees the same storage through the type it is actually stored as.
|
||||
GLfloat readFloat = 0.0f;
|
||||
glGetUniformfv(m_program, scalar, &readFloat);
|
||||
EXPECT_FLOAT_EQ(readFloat, static_cast<float>(0.1));
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, ADoubleUniformKeepsItsDeclaredInitializer) {
|
||||
if (!Ready()) return;
|
||||
// A declared initializer is seeded straight into the uniform shadow at link, and the
|
||||
// seeding used to skip 64-bit floats outright ("no 32-bit shadow encoding") - which
|
||||
// was true before the demotion and silently left every such uniform reading zero.
|
||||
const char* source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
uniform double uSeeded = 2.5lf;
|
||||
uniform dvec3 uSeededVector = dvec3(4.0lf, 5.0lf, 6.0lf);
|
||||
layout(std430, binding = 0) buffer Output {
|
||||
float g_out[];
|
||||
};
|
||||
void main() {
|
||||
g_out[0] = float(uSeeded);
|
||||
g_out[1] = float(uSeededVector.x);
|
||||
g_out[2] = float(uSeededVector.y);
|
||||
g_out[3] = float(uSeededVector.z);
|
||||
}
|
||||
)";
|
||||
const GLuint program = CompileComputeProgram(source);
|
||||
ASSERT_NE(program, 0u) << m_buildLog;
|
||||
|
||||
glUseProgram(program);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
std::vector<float> values(4, -1.0f);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, 4 * sizeof(float), values.data());
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
|
||||
EXPECT_FLOAT_EQ(values[0], 2.5f) << "scalar double initializer";
|
||||
EXPECT_FLOAT_EQ(values[1], 4.0f) << "dvec3 initializer .x";
|
||||
EXPECT_FLOAT_EQ(values[2], 5.0f) << "dvec3 initializer .y";
|
||||
EXPECT_FLOAT_EQ(values[3], 6.0f) << "dvec3 initializer .z";
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, TheFp64ExtensionIsNotAdvertised) {
|
||||
if (!Ready()) return;
|
||||
// The shader above compiled, linked and ran without the extension string, which is
|
||||
// the point: an application does not need GL_ARB_gpu_shader_fp64 advertised to USE
|
||||
// doubles here. What the string additionally promises is 64-bit precision, and that
|
||||
// is the one thing the demotion cannot deliver - so it stays off unless
|
||||
// MOBILEGL_ADVERTISE_FP64 asks for it, and an application that branches on the
|
||||
// string keeps taking its float path.
|
||||
GLint extensionCount = 0;
|
||||
glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount);
|
||||
ASSERT_GT(extensionCount, 0);
|
||||
bool advertised = false;
|
||||
for (GLint i = 0; i < extensionCount; ++i) {
|
||||
const char* name = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, static_cast<GLuint>(i)));
|
||||
if (name != nullptr && std::string(name) == "GL_ARB_gpu_shader_fp64") advertised = true;
|
||||
}
|
||||
EXPECT_FALSE(advertised);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsDeclinedOnEveryBackend) {
|
||||
if (!Ready()) return;
|
||||
// The demotion leaves no 64-bit shader input to feed, so there is nothing a 64-bit
|
||||
// vertex FETCH could be fetched into - on either backend, and no longer only on the
|
||||
// ones whose device lacks shaderFloat64. Declined loudly rather than accepted and
|
||||
// drawn as garbage; the matching POST row says the same thing at startup.
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
while (glGetError() != GL_NO_ERROR) {}
|
||||
|
||||
glVertexAttribLFormat(0, 3, GL_DOUBLE, 0);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
|
||||
|
||||
glBindVertexArray(0);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
while (glGetError() != GL_NO_ERROR) {}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -165,10 +165,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
const Int elements = init.arraySize;
|
||||
if (componentsPerElement <= 0 || elements <= 0) continue;
|
||||
|
||||
const Bool isFloat = init.basicType == glslang::EbtFloat || init.basicType == glslang::EbtFloat16;
|
||||
// EbtDouble belongs with the floats now, not with the skipped types: every 64-bit
|
||||
// float in a shader is narrowed to 32 bits before the module reaches a backend
|
||||
// (ShaderTranspiler::DemoteFloat64Pass), so a `uniform double d = 1.5;` has exactly
|
||||
// the 32-bit shadow encoding a `uniform float` does - and glslang already folded its
|
||||
// value into floatValues, which is a vector<double> either way. Leaving it out meant
|
||||
// the initializer was silently dropped and the uniform came up zero.
|
||||
const Bool isFloat = init.basicType == glslang::EbtFloat ||
|
||||
init.basicType == glslang::EbtFloat16 ||
|
||||
init.basicType == glslang::EbtDouble;
|
||||
const Bool isInt = init.basicType == glslang::EbtInt || init.basicType == glslang::EbtUint ||
|
||||
init.basicType == glslang::EbtBool;
|
||||
// Anything else (fp64, 64-bit integers) has no 32-bit shadow encoding here, and a
|
||||
// Anything else (64-bit integers) has no 32-bit shadow encoding here, and a
|
||||
// half-written uniform is worse than an untouched one.
|
||||
if (!isFloat && !isInt) continue;
|
||||
const SizeT provided = isFloat ? init.floatValues.size() : init.intValues.size();
|
||||
|
||||
@@ -326,15 +326,22 @@ namespace MobileGL::MG_State::GLState {
|
||||
: kInvalidUniformOffset;
|
||||
}
|
||||
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
|
||||
// Bytes a uniform actually occupies in the global UBO, which is not its GL type size:
|
||||
// std140 pads each column of a float matrix out to a vec4, so a mat3 spans 48 bytes
|
||||
// even though only 36 of them carry components. Anything reading or writing a whole
|
||||
// uniform's storage - a bounds check, a copy between two programs' shadows - wants
|
||||
// this rather than GetUniformSizesInBytes.
|
||||
// Bytes a uniform actually occupies in the global UBO, which is not its GL type size,
|
||||
// for two reasons. std140 pads each column of a matrix out to a vec4, so a mat3 spans
|
||||
// 48 bytes even though only 36 of them carry components. And every 64-bit float in a
|
||||
// shader is narrowed to 32 bits before the module reaches a backend
|
||||
// (ShaderTranspiler::DemoteFloat64Pass) - the global UBO is laid out by reflecting that
|
||||
// demoted module - so a `double` uniform occupies exactly what its float-typed twin
|
||||
// would, half its GL type size, and a `dmat4` is padded like any other matrix. Anything
|
||||
// reading or writing a whole uniform's storage - a bounds check, a copy between two
|
||||
// programs' shadows - wants this rather than GetUniformSizesInBytes.
|
||||
static SizeT UniformStorageSpanInBytes(const glslang::TType* type, SizeT tightSize) {
|
||||
if (type != nullptr && type->isMatrix() && type->getBasicType() != glslang::EbtDouble) {
|
||||
if (type != nullptr && type->isMatrix()) {
|
||||
return static_cast<SizeT>(type->getMatrixCols()) * 4 * sizeof(Float);
|
||||
}
|
||||
if (type != nullptr && type->getBasicType() == glslang::EbtDouble) {
|
||||
return tightSize / 2;
|
||||
}
|
||||
return tightSize;
|
||||
}
|
||||
SizeT GetUniformStorageSpanInBytes(Uint location) const {
|
||||
|
||||
@@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.14)
|
||||
add_executable(
|
||||
SpirvPassTest
|
||||
SpirvPassTest.cpp
|
||||
DemoteFloat64Test.cpp
|
||||
)
|
||||
|
||||
target_include_directories(SpirvPassTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/DemoteFloat64Test.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::ShaderCompiler;
|
||||
|
||||
namespace {
|
||||
// The test-side reference walker, deliberately independent of the production code: a bug in
|
||||
// the pass must not be able to hide behind the same helper. Counts OpTypeFloat declarations of
|
||||
// a given width and collects the Offset literal of every OpMemberDecorate, in module order.
|
||||
constexpr Uint32 kSpirvHeaderWordCount = 5;
|
||||
constexpr Uint32 kOpTypeFloat = 22;
|
||||
constexpr Uint32 kOpName = 5;
|
||||
constexpr Uint32 kOpMemberDecorate = 72;
|
||||
constexpr Uint32 kOpFConvert = 115;
|
||||
constexpr Uint32 kOpCapability = 17;
|
||||
constexpr Uint32 kDecorationOffset = 35;
|
||||
constexpr Uint32 kCapabilityFloat64 = 10;
|
||||
|
||||
template <typename Visitor>
|
||||
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
|
||||
for (SizeT i = kSpirvHeaderWordCount; i < spirv.size();) {
|
||||
const Uint32 wordCount = spirv[i] >> 16;
|
||||
const Uint32 opcode = spirv[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
||||
visit(opcode, &spirv[i], wordCount);
|
||||
i += wordCount;
|
||||
}
|
||||
}
|
||||
|
||||
Uint32 CountFloatTypesOfWidth(const Vector<Uint32>& spirv, Uint32 width) {
|
||||
Uint32 count = 0;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == kOpTypeFloat && wordCount >= 3 && words[2] == width) ++count;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
Uint32 CountFConverts(const Vector<Uint32>& spirv) {
|
||||
Uint32 count = 0;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32*, Uint32) {
|
||||
if (opcode == kOpFConvert) ++count;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
Bool DeclaresFloat64Capability(const Vector<Uint32>& spirv) {
|
||||
Bool found = false;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == kOpCapability && wordCount >= 2 && words[1] == kCapabilityFloat64) found = true;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
// Byte offset of every member of the struct named `blockName`, in member order.
|
||||
Vector<Uint32> CollectOffsetsOf(const Vector<Uint32>& spirv, const String& blockName) {
|
||||
Uint32 structId = 0;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != kOpName || wordCount < 3 || structId != 0) return;
|
||||
const char* text = reinterpret_cast<const char*>(&words[2]);
|
||||
const SizeT maxBytes = (wordCount - 2) * sizeof(Uint32);
|
||||
if (std::strncmp(text, blockName.c_str(), maxBytes) == 0) structId = words[1];
|
||||
});
|
||||
if (structId == 0) return {};
|
||||
|
||||
std::map<Uint32, Uint32> offsetByMember;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == kOpMemberDecorate && wordCount >= 5 && words[1] == structId &&
|
||||
words[3] == kDecorationOffset) {
|
||||
offsetByMember[words[2]] = words[4];
|
||||
}
|
||||
});
|
||||
|
||||
Vector<Uint32> offsets;
|
||||
for (const auto& [member, offset] : offsetByMember) offsets.push_back(offset);
|
||||
return offsets;
|
||||
}
|
||||
|
||||
// Everything the production pipeline does to a source before the pass sees it.
|
||||
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||
if (!shaderResult) return {};
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
String Disassemble(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String text;
|
||||
tools.Disassemble(spirv, &text);
|
||||
return text;
|
||||
}
|
||||
|
||||
// A vertex shader that exercises every shape the pass has to handle at once: a block with
|
||||
// double / dvec2 / dvec3 / dvec4 / dmat4 members between two floats (so a shifted offset would
|
||||
// be visible), a default-block double uniform, a 64-bit vertex input, a double-typed array, an
|
||||
// implicit float->double conversion and an explicit double->float one.
|
||||
const char* kWideVertexSource = R"(#version 460 core
|
||||
layout(std140, binding = 0) uniform Blk {
|
||||
float a;
|
||||
double d;
|
||||
dvec2 v2;
|
||||
dvec3 v3;
|
||||
dvec4 v4;
|
||||
dmat4 m4;
|
||||
double arr[3];
|
||||
float z;
|
||||
};
|
||||
layout(location = 0) uniform double uScale;
|
||||
layout(location = 0) in dvec3 inPos;
|
||||
layout(location = 1) in vec3 inNormal;
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
double s = d * uScale + a;
|
||||
dvec3 p = inPos * v3 + v2.xyx + v4.xyz + dvec3(m4[0].xyz);
|
||||
s += p.x + p.y + p.z + arr[0] + arr[1] + arr[2] + z + 0.5lf;
|
||||
vOut = float(s) + inNormal.x;
|
||||
gl_Position = vec4(float(s));
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
class DemoteFloat64Test : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
ShaderCompiler::SetSpirvValidationEnabled(true);
|
||||
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// The wrapper validates its OUTPUT on every run, so this covers every demotion the test
|
||||
// performed without any of them having to say so.
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
|
||||
<< "the demoted module did not survive spirv-val";
|
||||
}
|
||||
|
||||
Uint64 m_validationFailuresAtStart = 0;
|
||||
};
|
||||
|
||||
TEST_F(DemoteFloat64Test, DemotesEveryWidthAndDropsTheCapability) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kWideVertexSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
ASSERT_EQ(CountFloatTypesOfWidth(input, 64), 1u) << Disassemble(input);
|
||||
ASSERT_TRUE(DeclaresFloat64Capability(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
|
||||
EXPECT_EQ(CountFloatTypesOfWidth(output, 64), 0u) << Disassemble(output);
|
||||
// And exactly one 32-bit float type survives: the merge has to happen, or spirv-val rejects
|
||||
// the second declaration.
|
||||
EXPECT_EQ(CountFloatTypesOfWidth(output, 32), 1u) << Disassemble(output);
|
||||
EXPECT_FALSE(DeclaresFloat64Capability(output));
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, RederivesTheStd140LayoutOfADemotedUniformBlock) {
|
||||
const String source = R"(#version 460 core
|
||||
layout(std140, binding = 0) uniform Blk {
|
||||
float a;
|
||||
double d;
|
||||
dvec2 v2;
|
||||
dvec3 v3;
|
||||
dvec4 v4;
|
||||
dmat4 m4;
|
||||
double arr[3];
|
||||
float z;
|
||||
};
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
vOut = float(d + v2.x + v3.y + v4.z + m4[2].w + arr[1] + a + z);
|
||||
gl_Position = vec4(vOut);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
// What glslang laid out for the 64-bit members, which is what an application computing
|
||||
// std140 by hand would also get.
|
||||
EXPECT_EQ(CollectOffsetsOf(input, "Blk"), (Vector<Uint32>{0, 8, 16, 32, 64, 96, 224, 272}))
|
||||
<< Disassemble(input);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
|
||||
// std140 for the demoted members: float at 4, vec2 at 8, vec3 at 16 (aligned like a vec4),
|
||||
// vec4 at 32, mat4 at 48 with a 16-byte column stride, the array at 112 with the std140
|
||||
// 16-byte element stride, and the trailing float at 160. This is what SPIRV-Cross has to be
|
||||
// able to re-derive for GLSL ES, which has no member layout(offset=) to fall back on.
|
||||
EXPECT_EQ(CollectOffsetsOf(output, "Blk"), (Vector<Uint32>{0, 4, 8, 16, 32, 48, 112, 160}))
|
||||
<< Disassemble(output);
|
||||
const String text = Disassemble(output);
|
||||
EXPECT_NE(text.find("MatrixStride 16"), String::npos) << text;
|
||||
EXPECT_NE(text.find("ArrayStride 16"), String::npos) << text;
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, RederivesTheStd430LayoutOfADemotedStorageBlock) {
|
||||
const String source = R"(#version 460 core
|
||||
layout(std430, binding = 0) buffer Ssbo {
|
||||
double head;
|
||||
dvec4 wide;
|
||||
double tail[4];
|
||||
};
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
vOut = float(head + wide.w + tail[3]);
|
||||
gl_Position = vec4(vOut);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_EQ(CollectOffsetsOf(input, "Ssbo"), (Vector<Uint32>{0, 32, 64})) << Disassemble(input);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
|
||||
// std430, so the array packs at its element size rather than being rounded to 16: float at 0,
|
||||
// vec4 at 16, float[4] at 32 with a 4-byte stride. A storage block must NOT come out std140,
|
||||
// which is the whole reason the packing is chosen per storage class.
|
||||
EXPECT_EQ(CollectOffsetsOf(output, "Ssbo"), (Vector<Uint32>{0, 16, 32})) << Disassemble(output);
|
||||
EXPECT_NE(Disassemble(output).find("ArrayStride 4"), String::npos) << Disassemble(output);
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, LeavesTheLayoutOfABlockWithoutDoublesAlone) {
|
||||
const String source = R"(#version 460 core
|
||||
layout(std140, binding = 0) uniform Blk {
|
||||
float a;
|
||||
vec3 v3;
|
||||
mat4 m4;
|
||||
};
|
||||
layout(std140, binding = 1) uniform Wide {
|
||||
float w;
|
||||
double d;
|
||||
};
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
vOut = float(a + v3.y + m4[1].z + float(d) + w);
|
||||
gl_Position = vec4(vOut);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Vector<Uint32> before = CollectOffsetsOf(input, "Blk");
|
||||
ASSERT_FALSE(before.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
|
||||
// Only the block that actually narrowed is re-laid-out. Touching the other one would be
|
||||
// churn at best, and a disagreement with glslang's own layout at worst.
|
||||
EXPECT_EQ(CollectOffsetsOf(output, "Blk"), before) << Disassemble(output);
|
||||
EXPECT_EQ(CollectOffsetsOf(output, "Wide"), (Vector<Uint32>{0, 4})) << Disassemble(output);
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, FoldsTheConversionsThatBecameIdentities) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kWideVertexSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
ASSERT_GT(CountFConverts(input), 0u) << "the fixture no longer converts between the two widths";
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
|
||||
// SPIR-V requires the two component widths of an OpFConvert to differ, so every one of them
|
||||
// has to be gone: both sides are 32 bits now.
|
||||
EXPECT_EQ(CountFConverts(output), 0u) << Disassemble(output);
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, NarrowsDoubleConstantsToTheirFloatValue) {
|
||||
const String source = R"(#version 460 core
|
||||
layout(location = 0) out float outValue;
|
||||
void main() {
|
||||
double d = 0.5lf;
|
||||
outValue = float(d * 0.25lf);
|
||||
gl_Position = vec4(0.0);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
|
||||
// A 64-bit literal is two words wide and a 32-bit one is a single word, so a constant left
|
||||
// unconverted is not merely imprecise - it is an unparseable instruction. Disassembling both
|
||||
// values proves the re-encode produced the right number, not just the right width.
|
||||
const String text = Disassemble(output);
|
||||
EXPECT_NE(text.find("OpConstant %float 0.5"), String::npos) << text;
|
||||
EXPECT_NE(text.find("OpConstant %float 0.25"), String::npos) << text;
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, LeavesAModuleWithoutDoublesByteIdentical) {
|
||||
const String source = R"(#version 460 core
|
||||
layout(location = 0) in vec4 inPos;
|
||||
void main() { gl_Position = inPos; }
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
// The pass reports SuccessWithoutChange here, and SPIRV-Tools asserts (in assert-enabled
|
||||
// builds) that such a run round-trips byte-identically.
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, DeclinesAModuleThatBitcastsAcrossTheWidthBoundary) {
|
||||
// packDouble2x32 is defined only for a 64-bit result: there is no 32-bit answer to give, and
|
||||
// narrowing one side of the surrounding OpBitcast alone produces a module spirv-val rejects.
|
||||
// The contract is that such a module comes back untouched rather than broken.
|
||||
const String source = R"(#version 460 core
|
||||
#extension GL_ARB_gpu_shader_fp64 : require
|
||||
layout(location = 0) uniform uvec2 uPacked;
|
||||
layout(location = 0) out float outValue;
|
||||
void main() {
|
||||
double d = packDouble2x32(uPacked);
|
||||
outValue = float(d);
|
||||
gl_Position = vec4(0.0);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
ASSERT_EQ(CountFloatTypesOfWidth(input, 64), 1u) << Disassemble(input);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
EXPECT_EQ(output, input) << Disassemble(output);
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64(output));
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, ModuleDeclaresFloat64AnswersBothWays) {
|
||||
const Vector<Uint32> wide = CompileToSpirv(GL_VERTEX_SHADER, kWideVertexSource);
|
||||
ASSERT_FALSE(wide.empty());
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64(wide));
|
||||
|
||||
Vector<Uint32> demoted;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(wide, demoted));
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64(demoted));
|
||||
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64({}));
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, TheSharedChainDemotesToo) {
|
||||
// Production never calls the pass on its own: it reaches it through the one chain every
|
||||
// module goes through at link, on both backends.
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kWideVertexSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64(output)) << Disassemble(output);
|
||||
}
|
||||
|
||||
// The payoff on the Espryt path: SPIRV-Cross throws "FP64 not supported in ES profile" for every
|
||||
// one of these before demotion, so the program simply could not be transpiled at all.
|
||||
class DemoteFloat64EsslTest : public DemoteFloat64Test, public ::testing::WithParamInterface<const char*> {};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
Shapes, DemoteFloat64EsslTest,
|
||||
::testing::Values(
|
||||
// A double that never reaches an interface: locals and literals only.
|
||||
R"(#version 460 core
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
double s = 0.5lf;
|
||||
for (int i = 0; i < 3; ++i) s = s * 1.5lf + 0.25lf;
|
||||
vOut = float(s);
|
||||
gl_Position = vec4(float(s));
|
||||
}
|
||||
)",
|
||||
// A default-block double uniform: the glUniform*d path, and the block MobileGL lays out
|
||||
// itself.
|
||||
R"(#version 460 core
|
||||
layout(location = 0) uniform double uScale;
|
||||
layout(location = 1) uniform dvec3 uOffset;
|
||||
layout(location = 2) uniform dmat4 uTransform;
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
dvec3 p = uOffset * uScale + dvec3(uTransform[1].xyz);
|
||||
vOut = float(p.x + p.y + p.z);
|
||||
gl_Position = vec4(float(p.x));
|
||||
}
|
||||
)",
|
||||
// An application-declared std140 block whose members are 64-bit.
|
||||
R"(#version 460 core
|
||||
layout(std140, binding = 0) uniform Blk {
|
||||
float a;
|
||||
double d;
|
||||
dvec2 v2;
|
||||
dvec3 v3;
|
||||
dvec4 v4;
|
||||
dmat4 m4;
|
||||
double arr[3];
|
||||
float z;
|
||||
};
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
double s = d + v2.x + v3.y + v4.z + m4[2].w + arr[1] + a + z;
|
||||
vOut = float(s);
|
||||
gl_Position = vec4(float(s));
|
||||
}
|
||||
)",
|
||||
// A 64-bit vertex input, which is what glVertexAttribLFormat feeds.
|
||||
R"(#version 460 core
|
||||
layout(location = 0) in dvec3 inPos;
|
||||
layout(location = 2) in double inWeight;
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
vOut = float(inPos.x + inPos.y + inPos.z + inWeight);
|
||||
gl_Position = vec4(vOut);
|
||||
}
|
||||
)",
|
||||
// An std430 storage block, whose double members pack differently again.
|
||||
R"(#version 460 core
|
||||
layout(std430, binding = 0) buffer Ssbo {
|
||||
double head;
|
||||
dvec4 wide;
|
||||
double tail[4];
|
||||
};
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
double s = head + wide.w + tail[3];
|
||||
vOut = float(s);
|
||||
gl_Position = vec4(float(s));
|
||||
}
|
||||
)"));
|
||||
|
||||
TEST_P(DemoteFloat64EsslTest, TheDemotedModuleCanBeEmittedAsEssl) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, GetParam());
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
|
||||
|
||||
SpvcSession session(output, SessionUsageBit::Transpile);
|
||||
spvc_compiler_options options;
|
||||
ASSERT_EQ(session.CreateOptions(&options), SPVC_SUCCESS);
|
||||
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 320);
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
|
||||
ASSERT_EQ(session.SetOptions(options), SPVC_SUCCESS);
|
||||
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
ASSERT_TRUE(essl) << essl.error().log;
|
||||
EXPECT_NE(essl->find("#version 320 es"), String::npos) << *essl;
|
||||
// ESSL has no 64-bit float spelling at all, so any of these in the output is SPIRV-Cross
|
||||
// having emitted something no ES driver will compile.
|
||||
EXPECT_EQ(essl->find("double"), String::npos) << *essl;
|
||||
EXPECT_EQ(essl->find("dvec"), String::npos) << *essl;
|
||||
EXPECT_EQ(essl->find("dmat"), String::npos) << *essl;
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, RejectsGarbageInput) {
|
||||
const Vector<Uint32> notSpirv{0xdeadbeefu, 0u, 0u, 0u, 0u};
|
||||
Vector<Uint32> output;
|
||||
EXPECT_FALSE(ShaderCompiler::DemoteFloat64ToFloat32(notSpirv, output));
|
||||
}
|
||||
@@ -46,6 +46,22 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
RankMobileGLReported = 5,
|
||||
};
|
||||
|
||||
// Both backends' fp64 rows end the same way, and the sentence they end with depends on
|
||||
// a config flag rather than on anything either backend probes: the demotion is what
|
||||
// makes doubles work, but GL_ARB_gpu_shader_fp64 promises the PRECISION the demotion
|
||||
// cannot deliver, so the string is opt-in and the row has to say which way it went.
|
||||
String AppendFp64AdvertisementNote(String detail) {
|
||||
if (MG_Config::Features.AdvertiseFp64) {
|
||||
return Move(detail) +
|
||||
". GL_ARB_gpu_shader_fp64 IS advertised (MOBILEGL_ADVERTISE_FP64): an application "
|
||||
"that checks the string will believe it has 64-bit precision, and it does not";
|
||||
}
|
||||
return Move(detail) +
|
||||
". GL_ARB_gpu_shader_fp64 is not advertised, because the precision it promises is the "
|
||||
"one thing the demotion cannot provide; set MOBILEGL_ADVERTISE_FP64=1 to advertise it "
|
||||
"anyway";
|
||||
}
|
||||
|
||||
struct ReportBuilder {
|
||||
BackendPostReport report;
|
||||
Bool fatalFailed = false;
|
||||
@@ -484,14 +500,24 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Reported rather than probed: this one cannot come out any other way. OpenGL ES has no
|
||||
// double-precision vertex format and ESSL has no fp64 type, so there is no driver and no
|
||||
// extension that could make it work - the row exists so the loss is named at startup
|
||||
// instead of discovered as an unexplained GL_INVALID_OPERATION at draw setup.
|
||||
// Both rows are reported rather than probed: neither can come out any other way.
|
||||
// ESSL has no 64-bit float type at all, so no driver and no extension could change
|
||||
// either answer, and the rows exist so the two halves of the loss are named at
|
||||
// startup instead of discovered as a shader that will not compile or an
|
||||
// unexplained GL_INVALID_OPERATION at draw setup.
|
||||
builder.Pass("fp64", AppendFp64AdvertisementNote(
|
||||
"demoted to fp32 - ESSL has no 64-bit float type, so every double / "
|
||||
"dvec / dmat in a shader is narrowed to 32 bits before transpilation "
|
||||
"(DemoteFloat64Pass). Such shaders COMPILE AND RUN, at single "
|
||||
"precision; a block containing a double is re-laid-out for the "
|
||||
"narrowed members, so an application that hard-codes std140 offsets "
|
||||
"computed for doubles must query them instead"));
|
||||
builder.Warn("64-bit vertex attributes",
|
||||
"not supported on any GLES driver (ES has no GL_DOUBLE vertex format and ESSL has "
|
||||
"no fp64 type); glVertexAttribLFormat / glVertexArrayAttribLFormat report "
|
||||
"GL_INVALID_OPERATION - use the Vulkan backend if the application needs them");
|
||||
"not supported (ES has no GL_DOUBLE vertex format, and after the fp64 demotion "
|
||||
"above there is no 64-bit shader input left to feed either); "
|
||||
"glVertexAttribLFormat / glVertexArrayAttribLFormat report "
|
||||
"GL_INVALID_OPERATION - feed the attribute with glVertexAttribPointer(GL_FLOAT), "
|
||||
"which a demoted dvec input reads correctly");
|
||||
if (glesFuncs.glPatchParameteri != nullptr) {
|
||||
builder.Pass("Tessellation patch parameters",
|
||||
"glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)");
|
||||
@@ -1843,14 +1869,25 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"unsupported; a GL_TEXTURE_CUBE_MAP_ARRAY texture gets no image at all, so sampling "
|
||||
"one reads nothing and glFramebufferTextureLayer on one is declined");
|
||||
}
|
||||
if (features.shaderFloat64 == VK_TRUE) {
|
||||
builder.Pass("shaderFloat64",
|
||||
"GLSL double/dvec/dmat and 64-bit vertex attributes (glVertexAttribLFormat) supported");
|
||||
} else {
|
||||
builder.Warn("shaderFloat64",
|
||||
"unsupported; any shader declaring a double fails to create a shader module, and "
|
||||
"glVertexAttribLFormat reports GL_INVALID_OPERATION instead of feeding the attribute");
|
||||
}
|
||||
// Reported whichever way the device answers, because MobileGL no longer follows the
|
||||
// device here: every 64-bit float is narrowed to 32 bits before any module reaches this
|
||||
// backend (DemoteFloat64Pass), so the Float64 capability is never declared and a device
|
||||
// that HAS the feature gains nothing from it. The device's own answer is still worth
|
||||
// printing - it is the reason the demotion is unconditional.
|
||||
builder.Pass("fp64", AppendFp64AdvertisementNote(
|
||||
format("demoted to fp32 (device shaderFloat64 = {}) - every double / dvec / "
|
||||
"dmat in a shader is narrowed to 32 bits before pipeline creation, so "
|
||||
"such shaders BUILD AND RUN at single precision on every device "
|
||||
"instead of failing to create a shader module on the ones without the "
|
||||
"feature. A block containing a double is re-laid-out for the narrowed "
|
||||
"members, so an application that hard-codes std140 offsets computed "
|
||||
"for doubles must query them instead",
|
||||
features.shaderFloat64 == VK_TRUE ? "supported" : "unsupported")));
|
||||
builder.Warn("64-bit vertex attributes",
|
||||
"not supported; there is no 64-bit shader input left to feed after the fp64 demotion "
|
||||
"above, and no VK_FORMAT_R64*_SFLOAT vertex fetch to feed it with on most devices "
|
||||
"anyway. glVertexAttribLFormat reports GL_INVALID_OPERATION - feed the attribute with "
|
||||
"glVertexAttribPointer(GL_FLOAT), which a demoted dvec input reads correctly");
|
||||
|
||||
Bool shaderDrawParameters = false;
|
||||
if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "SpirvPasses/RenameBuiltinShadowingFunctionsPass.h"
|
||||
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
|
||||
#include "SpirvPasses/DecoratePositionInvariantPass.h"
|
||||
#include "SpirvPasses/DemoteFloat64Pass.h"
|
||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
||||
#include "SpirvPasses/SplitArrayVertexInputsPass.h"
|
||||
@@ -578,6 +579,37 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ShaderCompiler::ModuleDeclaresFloat64(const Vector<Uint32>& spirv) {
|
||||
if (spirv.empty()) {
|
||||
// Same reasoning as ModuleDeclaresBufferTextureSampler: a stage that produced
|
||||
// no SPIR-V is not a verdict about 64-bit floats, and parsing it would push a
|
||||
// spurious diagnostic through the message consumer.
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleDeclaresFloat64"), spirv.data(),
|
||||
spirv.size());
|
||||
if (!context) {
|
||||
return false;
|
||||
}
|
||||
for (const spvtools::opt::Instruction& type : context->types_values()) {
|
||||
if (type.opcode() == spv::Op::OpTypeFloat && type.NumInOperands() >= 1 &&
|
||||
type.GetSingleWordInOperand(0) == 64) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DemoteFloat64ToFloat32(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(DemoteFloat64Pass::CreateDemoteFloat64Pass());
|
||||
|
||||
return RunOptimizerChecked("DemoteFloat64ToFloat32", optimizer, inputBinary, outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
@@ -616,6 +648,17 @@ namespace MobileGL {
|
||||
RenameBuiltinShadowingFunctionsPass::CreateRenameBuiltinShadowingFunctionsPass());
|
||||
optimizer.RegisterPass(EliminateFloatEqualsZeroPass::CreateEliminateFloatEqualsZeroPass());
|
||||
optimizer.RegisterPass(DecomposeWorkgroupVec3Pass::CreateDecomposeWorkgroupVec3Pass());
|
||||
// No mobile GPU has 64-bit floats: Adreno and Mali both report shaderFloat64 ==
|
||||
// VK_FALSE, and ESSL has no fp64 type for SPIRV-Cross to emit. Demoting here - in
|
||||
// the one chain every module goes through, on both backends, at link - is what
|
||||
// makes `double` compile at all, and makes it behave the SAME everywhere, which
|
||||
// matters because the GL frontend's uniform storage cannot be per-backend: the
|
||||
// glUniform*d shadow narrows to float unconditionally to match this. Runs last so
|
||||
// no earlier pass ever has to reason about a width it will not see in the output;
|
||||
// in particular it runs before the backends' PackDoubleVertexInputsPass, whose
|
||||
// OpBitcast this one would otherwise decline on. Costs one types_values() walk on
|
||||
// the overwhelming majority of modules, which declare no 64-bit float at all.
|
||||
optimizer.RegisterPass(DemoteFloat64Pass::CreateDemoteFloat64Pass());
|
||||
|
||||
return RunOptimizerChecked("SanitizeAndOptimizeBinary", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
|
||||
@@ -100,6 +100,12 @@ namespace MobileGL {
|
||||
// reinterpretation paths (for example, R32F storage accessed as r32ui).
|
||||
static bool UseUnformattedFloatStorageImagesForVulkan(
|
||||
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary);
|
||||
// Rewrites every 64-bit float in the module to a 32-bit one, preserving every
|
||||
// block offset and stride exactly (see DemoteFloat64Pass). Already part of
|
||||
// SanitizeAndOptimizeBinary, which is where production reaches it; exposed
|
||||
// separately so a test can drive the demotion on its own.
|
||||
static bool DemoteFloat64ToFloat32(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
static Result<String> DecompileShader(SpvcSession& session);
|
||||
|
||||
// Parses one trivial shader in each configuration the production path can
|
||||
@@ -159,6 +165,12 @@ namespace MobileGL {
|
||||
// check exists so that failure can be reported as the missing capability it is,
|
||||
// naming the shader, rather than as a driver info log nobody sees.
|
||||
static Bool ModuleDeclaresBufferTextureSampler(const Vector<Uint32>& spirv);
|
||||
|
||||
// True when the module still declares a 64-bit float type. After
|
||||
// SanitizeAndOptimizeBinary that can only mean DemoteFloat64Pass declined the
|
||||
// module (see its header for the two operations that make it decline), which is
|
||||
// what the backends report: no mobile driver can build such a module.
|
||||
static Bool ModuleDeclaresFloat64(const Vector<Uint32>& spirv);
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
|
||||
@@ -0,0 +1,547 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.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 "DemoteFloat64Pass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/latest_version_glsl_std_450_header.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/util/make_unique.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
constexpr Uint32 kFloat64Width = 64;
|
||||
constexpr Uint32 kFloat32Width = 32;
|
||||
|
||||
// OpTypeFloat <id> <Width>. SPIR-V 1.6 added an optional FP Encoding operand after
|
||||
// it; the width stays operand 0 either way, and an encoded (non-IEEE) float is not
|
||||
// something glslang can emit for `double`, so it is left alone by the width guard.
|
||||
Bool IsFloatTypeOfWidth(const Instruction& type, Uint32 width) {
|
||||
return type.opcode() == spv::Op::OpTypeFloat && type.NumInOperands() >= 1 &&
|
||||
type.GetSingleWordInOperand(0) == width;
|
||||
}
|
||||
|
||||
// Exactly the types spirv-val forbids a second declaration of. Aggregates - arrays
|
||||
// and structs - are excluded on purpose: the spec permits duplicates of those, and
|
||||
// merging them would take one of the two OpNames and one of the two ArrayStride /
|
||||
// Offset decoration sets with it.
|
||||
Bool IsDuplicableType(spv::Op opcode) {
|
||||
switch (opcode) {
|
||||
case spv::Op::OpTypeFloat:
|
||||
case spv::Op::OpTypeVector:
|
||||
case spv::Op::OpTypeMatrix:
|
||||
case spv::Op::OpTypePointer:
|
||||
case spv::Op::OpTypeFunction: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
Uint32 RoundUp(Uint32 value, Uint32 alignment) {
|
||||
if (alignment == 0) return value;
|
||||
return (value + alignment - 1) / alignment * alignment;
|
||||
}
|
||||
|
||||
// Re-derives the std140 / std430 layout of a block whose members have just become
|
||||
// 32 bits wide, and writes it back as Offset / ArrayStride / MatrixStride.
|
||||
//
|
||||
// Why the layout is recomputed rather than preserved. Preserving it - leaving each
|
||||
// member at the byte offset glslang picked for the 64-bit type and letting the
|
||||
// freed 4 bytes become padding - keeps the application's byte layout intact and is
|
||||
// the obvious first choice, but it does not survive contact with the Espryt path:
|
||||
// SPIRV-Cross has to print an ESSL block, GLSL ES has no member `layout(offset=)`
|
||||
// (no ARB_enhanced_layouts), so it refuses any block whose declared offsets are not
|
||||
// exactly what std140 or std430 computes - "Buffer block cannot be expressed as any
|
||||
// of std430, std140, scalar". Every shader with a double in a block would fail to
|
||||
// transpile at all, which is the case this whole demotion exists to fix. Padding
|
||||
// members back in cannot rescue it either: a dmat4 member carries MatrixStride 32
|
||||
// and std140 demands 16 for the demoted mat4, and no amount of padding BETWEEN
|
||||
// members changes a stride INSIDE one.
|
||||
//
|
||||
// What recomputing costs: a block laid out for 64-bit members changes its
|
||||
// driver-visible byte layout, so an application that hard-codes std140 offsets
|
||||
// computed for doubles addresses the wrong bytes. Applications that query their
|
||||
// offsets are unaffected, and MobileGL's own default-uniform block is unaffected by
|
||||
// construction - the frontend builds its uniform routing by reflecting THIS module
|
||||
// (ProgramSpirvTask::BuildGlobalUboRouting), so glUniform*d writes wherever the
|
||||
// demoted shader reads.
|
||||
class BlockRelayout {
|
||||
public:
|
||||
BlockRelayout(IRContext* irContext, Bool std140)
|
||||
: m_irContext(irContext), m_std140(std140) {}
|
||||
|
||||
// Size and alignment of `typeId`, applying every stride decoration it implies
|
||||
// on the way down. Zero size means "not a type this layout knows how to
|
||||
// describe"; the caller then leaves the block alone rather than guessing.
|
||||
struct Extent {
|
||||
Uint32 size = 0;
|
||||
Uint32 alignment = 0;
|
||||
};
|
||||
|
||||
Extent Measure(Uint32 typeId) {
|
||||
const auto memo = m_extents.find(typeId);
|
||||
if (memo != m_extents.end()) return memo->second;
|
||||
|
||||
const Extent extent = MeasureUncached(typeId);
|
||||
m_extents.emplace(typeId, extent);
|
||||
return extent;
|
||||
}
|
||||
|
||||
private:
|
||||
Extent MeasureUncached(Uint32 typeId) {
|
||||
const Instruction* type = m_irContext->get_def_use_mgr()->GetDef(typeId);
|
||||
if (type == nullptr) return {};
|
||||
|
||||
switch (type->opcode()) {
|
||||
case spv::Op::OpTypeInt:
|
||||
case spv::Op::OpTypeFloat: {
|
||||
const Uint32 bytes = type->GetSingleWordInOperand(0) / 8;
|
||||
return {bytes, bytes};
|
||||
}
|
||||
case spv::Op::OpTypeBool: return {4, 4};
|
||||
case spv::Op::OpTypeVector: {
|
||||
const Extent component = Measure(type->GetSingleWordInOperand(0));
|
||||
if (component.size == 0) return {};
|
||||
const Uint32 count = type->GetSingleWordInOperand(1);
|
||||
// A three-component vector aligns like a four-component one.
|
||||
return {component.size * count,
|
||||
component.alignment * (count == 3 ? 4 : count)};
|
||||
}
|
||||
case spv::Op::OpTypeMatrix: {
|
||||
const Extent column = Measure(type->GetSingleWordInOperand(0));
|
||||
if (column.size == 0) return {};
|
||||
const Uint32 stride = MatrixOrArrayStride(column.alignment);
|
||||
return {stride * type->GetSingleWordInOperand(1), stride};
|
||||
}
|
||||
case spv::Op::OpTypeArray:
|
||||
case spv::Op::OpTypeRuntimeArray: {
|
||||
const Extent element = Measure(type->GetSingleWordInOperand(0));
|
||||
if (element.size == 0) return {};
|
||||
const Uint32 alignment = MatrixOrArrayStride(element.alignment);
|
||||
const Uint32 stride = RoundUp(element.size, alignment);
|
||||
SetTypeDecoration(typeId, spv::Decoration::ArrayStride, stride);
|
||||
if (type->opcode() == spv::Op::OpTypeRuntimeArray) {
|
||||
// An unsized array contributes its stride and nothing more; the
|
||||
// block's size is whatever the application bound.
|
||||
return {stride, alignment};
|
||||
}
|
||||
return {stride * ArrayLength(type->GetSingleWordInOperand(1)), alignment};
|
||||
}
|
||||
case spv::Op::OpTypeStruct: return MeasureStruct(*type);
|
||||
default: return {};
|
||||
}
|
||||
}
|
||||
|
||||
Extent MeasureStruct(const Instruction& structType) {
|
||||
Uint32 cursor = 0;
|
||||
Uint32 alignment = m_std140 ? 16u : 1u;
|
||||
for (Uint32 member = 0; member < structType.NumInOperands(); ++member) {
|
||||
const Uint32 memberTypeId = structType.GetSingleWordInOperand(member);
|
||||
const Extent extent = Measure(memberTypeId);
|
||||
if (extent.size == 0) return {};
|
||||
|
||||
const Uint32 offset = RoundUp(cursor, extent.alignment);
|
||||
SetMemberDecoration(structType.result_id(), member, spv::Decoration::Offset, offset);
|
||||
// A matrix member carries the stride between its columns on the MEMBER,
|
||||
// not on the type, so it has to be (re)stated here - including for a
|
||||
// matrix reached through an array.
|
||||
const Instruction* memberType =
|
||||
m_irContext->get_def_use_mgr()->GetDef(PeelArrays(memberTypeId));
|
||||
if (memberType != nullptr && memberType->opcode() == spv::Op::OpTypeMatrix) {
|
||||
const Extent column = Measure(memberType->GetSingleWordInOperand(0));
|
||||
SetMemberDecoration(structType.result_id(), member,
|
||||
spv::Decoration::MatrixStride,
|
||||
MatrixOrArrayStride(column.alignment));
|
||||
}
|
||||
|
||||
cursor = offset + extent.size;
|
||||
alignment = std::max(alignment, extent.alignment);
|
||||
}
|
||||
return {RoundUp(cursor, alignment), alignment};
|
||||
}
|
||||
|
||||
// std140 rounds every array and matrix stride up to a four-component vector.
|
||||
Uint32 MatrixOrArrayStride(Uint32 elementAlignment) const {
|
||||
return m_std140 ? RoundUp(elementAlignment, 16) : elementAlignment;
|
||||
}
|
||||
|
||||
Uint32 PeelArrays(Uint32 typeId) const {
|
||||
const Instruction* type = m_irContext->get_def_use_mgr()->GetDef(typeId);
|
||||
while (type != nullptr && (type->opcode() == spv::Op::OpTypeArray ||
|
||||
type->opcode() == spv::Op::OpTypeRuntimeArray)) {
|
||||
type = m_irContext->get_def_use_mgr()->GetDef(type->GetSingleWordInOperand(0));
|
||||
}
|
||||
return type != nullptr ? type->result_id() : 0;
|
||||
}
|
||||
|
||||
Uint32 ArrayLength(Uint32 lengthConstantId) const {
|
||||
const Instruction* length = m_irContext->get_def_use_mgr()->GetDef(lengthConstantId);
|
||||
if (length == nullptr || length->opcode() != spv::Op::OpConstant ||
|
||||
length->NumInOperands() < 1) {
|
||||
return 1;
|
||||
}
|
||||
return length->GetSingleWordInOperand(0);
|
||||
}
|
||||
|
||||
void SetTypeDecoration(Uint32 targetId, spv::Decoration decoration, Uint32 value) {
|
||||
for (Instruction& annotation : m_irContext->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate) continue;
|
||||
if (annotation.GetSingleWordInOperand(0) != targetId) continue;
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) != decoration) {
|
||||
continue;
|
||||
}
|
||||
annotation.SetInOperand(2, {value});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void SetMemberDecoration(Uint32 structId, Uint32 member, spv::Decoration decoration,
|
||||
Uint32 value) {
|
||||
for (Instruction& annotation : m_irContext->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpMemberDecorate) continue;
|
||||
if (annotation.GetSingleWordInOperand(0) != structId) continue;
|
||||
if (annotation.GetSingleWordInOperand(1) != member) continue;
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(2)) != decoration) {
|
||||
continue;
|
||||
}
|
||||
annotation.SetInOperand(3, {value});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
IRContext* m_irContext = nullptr;
|
||||
Bool m_std140 = true;
|
||||
std::unordered_map<Uint32, Extent> m_extents;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status DemoteFloat64Pass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
|
||||
// --- 1. the leaf types -------------------------------------------------------
|
||||
std::vector<Instruction*> float64Types;
|
||||
for (Instruction& type : irContext->types_values()) {
|
||||
if (IsFloatTypeOfWidth(type, kFloat64Width)) {
|
||||
float64Types.push_back(&type);
|
||||
}
|
||||
}
|
||||
if (float64Types.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
std::unordered_set<Uint32> float64TypeIds;
|
||||
for (const Instruction* type : float64Types) {
|
||||
float64TypeIds.insert(type->result_id());
|
||||
}
|
||||
|
||||
// Every type a *value* can have that is 64-bit float underneath: the scalar itself
|
||||
// plus the vectors and matrices built from it. The types-and-values section is in
|
||||
// declaration order and SPIR-V forbids a type from forward-referencing another, so
|
||||
// one forward walk is already the complete transitive closure. Aggregates and
|
||||
// pointers are deliberately not included: no operand of the instructions checked
|
||||
// below is ever an aggregate or a pointer.
|
||||
std::unordered_set<Uint32> float64ValueTypeIds = float64TypeIds;
|
||||
for (Instruction& type : irContext->types_values()) {
|
||||
if (type.opcode() != spv::Op::OpTypeVector && type.opcode() != spv::Op::OpTypeMatrix) {
|
||||
continue;
|
||||
}
|
||||
if (float64ValueTypeIds.count(type.GetSingleWordInOperand(0)) != 0) {
|
||||
float64ValueTypeIds.insert(type.result_id());
|
||||
}
|
||||
}
|
||||
|
||||
// Every type that has a 64-bit float anywhere underneath it, which is exactly the
|
||||
// set of blocks whose layout has to be re-derived once the leaves narrow. Computed
|
||||
// now, before the rewrite makes a demoted float indistinguishable from one that was
|
||||
// always 32 bits; the same single forward walk is a complete closure.
|
||||
std::unordered_set<Uint32> wideTypeIds = float64TypeIds;
|
||||
for (Instruction& type : irContext->types_values()) {
|
||||
const auto contains = [&](Uint32 operand) {
|
||||
return wideTypeIds.count(type.GetSingleWordInOperand(operand)) != 0;
|
||||
};
|
||||
switch (type.opcode()) {
|
||||
case spv::Op::OpTypeVector:
|
||||
case spv::Op::OpTypeMatrix:
|
||||
case spv::Op::OpTypeArray:
|
||||
case spv::Op::OpTypeRuntimeArray:
|
||||
if (contains(0)) wideTypeIds.insert(type.result_id());
|
||||
break;
|
||||
case spv::Op::OpTypePointer:
|
||||
if (contains(1)) wideTypeIds.insert(type.result_id());
|
||||
break;
|
||||
case spv::Op::OpTypeStruct:
|
||||
for (Uint32 member = 0; member < type.NumInOperands(); ++member) {
|
||||
if (contains(member)) {
|
||||
wideTypeIds.insert(type.result_id());
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto valueIsFloat64 = [&](Uint32 id) {
|
||||
const Instruction* def = defUseMgr->GetDef(id);
|
||||
return def != nullptr && float64ValueTypeIds.count(def->type_id()) != 0;
|
||||
};
|
||||
|
||||
// --- 2. decline before touching anything ------------------------------------
|
||||
// These are the operations that mean "the 64 bits themselves", not "a wide float".
|
||||
// Narrowing one side of them produces a module spirv-val rejects, and rebuilding
|
||||
// the value would mean emulating fp64 in software. Bail out with the module
|
||||
// byte-identical instead; the caller still has its "module declares Float64"
|
||||
// diagnostic for it.
|
||||
Uint32 glslStd450SetId = 0;
|
||||
for (const Instruction& import : irContext->ext_inst_imports()) {
|
||||
if (import.GetInOperand(0).AsString() == "GLSL.std.450") {
|
||||
glslStd450SetId = import.result_id();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const char* declineReason = nullptr;
|
||||
irContext->module()->ForEachInst(
|
||||
[&](Instruction* inst) {
|
||||
if (declineReason != nullptr) return;
|
||||
switch (inst->opcode()) {
|
||||
case spv::Op::OpBitcast: {
|
||||
// "Total bit width of Result Type and Operand must match" - true
|
||||
// today, false the moment one of the two sides halves.
|
||||
const Bool resultIs64 = float64ValueTypeIds.count(inst->type_id()) != 0;
|
||||
const Bool operandIs64 = valueIsFloat64(inst->GetSingleWordInOperand(0));
|
||||
if (resultIs64 != operandIs64) {
|
||||
declineReason = "an OpBitcast across the 64-bit boundary "
|
||||
"(doubleBitsToUint64 / uint64BitsToDouble / "
|
||||
"packDouble2x32)";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case spv::Op::OpExtInst: {
|
||||
if (glslStd450SetId == 0 ||
|
||||
inst->GetSingleWordInOperand(0) != glslStd450SetId) {
|
||||
break;
|
||||
}
|
||||
const Uint32 extOpcode = inst->GetSingleWordInOperand(1);
|
||||
if (extOpcode == GLSLstd450PackDouble2x32 ||
|
||||
extOpcode == GLSLstd450UnpackDouble2x32) {
|
||||
declineReason = "GLSL.std.450 PackDouble2x32 / UnpackDouble2x32, "
|
||||
"which are defined only for a 64-bit float";
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
},
|
||||
/*run_on_debug_line_insts=*/false);
|
||||
|
||||
if (declineReason != nullptr) {
|
||||
MGLOG_D("DemoteFloat64Pass: declined - the module uses %s; its 64-bit floats are "
|
||||
"left in place",
|
||||
declineReason);
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// --- 3. re-encode the literals ----------------------------------------------
|
||||
// A 64-bit float constant carries two literal words and a 32-bit one carries a
|
||||
// single word, so the value has to be narrowed before the type changes underneath
|
||||
// it - afterwards there is no way left to tell how wide the literal was meant to
|
||||
// be. Composite constants hold <id>s, not literals, and need nothing.
|
||||
for (Instruction& value : irContext->types_values()) {
|
||||
if (value.opcode() != spv::Op::OpConstant && value.opcode() != spv::Op::OpSpecConstant) {
|
||||
continue;
|
||||
}
|
||||
if (float64TypeIds.count(value.type_id()) == 0) continue;
|
||||
if (value.NumInOperands() < 1) continue;
|
||||
const Operand& literal = value.GetInOperand(0);
|
||||
if (literal.words.size() != 2) continue;
|
||||
|
||||
const Uint64 bits =
|
||||
(static_cast<Uint64>(literal.words[1]) << 32) | static_cast<Uint64>(literal.words[0]);
|
||||
double wide = 0.0;
|
||||
std::memcpy(&wide, &bits, sizeof(wide));
|
||||
// Deliberately the ordinary narrowing conversion: a magnitude no float can
|
||||
// hold becomes an infinity, which is the same answer the demoted arithmetic
|
||||
// around it would produce.
|
||||
const float narrow = static_cast<float>(wide);
|
||||
Uint32 narrowedBits = 0;
|
||||
std::memcpy(&narrowedBits, &narrow, sizeof(narrowedBits));
|
||||
|
||||
Operand narrowedLiteral = literal;
|
||||
narrowedLiteral.words = {narrowedBits};
|
||||
value.SetInOperands({std::move(narrowedLiteral)});
|
||||
}
|
||||
|
||||
// --- 4. the demotion itself --------------------------------------------------
|
||||
// In place, so every composite type, every pointer, every struct member offset and
|
||||
// every debug name that referred to the 64-bit type keeps referring to the same
|
||||
// <id>. This is the whole reason the pass does not build parallel types.
|
||||
for (Instruction* type : float64Types) {
|
||||
type->SetInOperand(0, {kFloat32Width});
|
||||
}
|
||||
// The cached analysis::Type objects were built against the old widths.
|
||||
irContext->InvalidateAnalyses(IRContext::kAnalysisTypes);
|
||||
|
||||
// --- 5. the conversions that just became identities --------------------------
|
||||
// `float(someDouble)` and `double(someFloat)` are both OpFConvert, and SPIR-V
|
||||
// requires the two component widths to differ. Both sides are 32 bits now, so each
|
||||
// one is replaced by its operand. Walking in module order means a chain of them
|
||||
// resolves in a single sweep: by the time the second is reached its operand has
|
||||
// already been rewritten to the ultimate source.
|
||||
const auto componentWidth = [&](Uint32 typeId) -> Uint32 {
|
||||
const Instruction* def = defUseMgr->GetDef(typeId);
|
||||
if (def == nullptr) return 0;
|
||||
if (def->opcode() == spv::Op::OpTypeVector) {
|
||||
def = defUseMgr->GetDef(def->GetSingleWordInOperand(0));
|
||||
}
|
||||
if (def == nullptr || def->opcode() != spv::Op::OpTypeFloat) return 0;
|
||||
return def->GetSingleWordInOperand(0);
|
||||
};
|
||||
|
||||
std::vector<Instruction*> identityConversions;
|
||||
irContext->module()->ForEachInst(
|
||||
[&](Instruction* inst) {
|
||||
if (inst->opcode() != spv::Op::OpFConvert) return;
|
||||
const Instruction* operandDef = defUseMgr->GetDef(inst->GetSingleWordInOperand(0));
|
||||
if (operandDef == nullptr) return;
|
||||
const Uint32 resultWidth = componentWidth(inst->type_id());
|
||||
if (resultWidth == 0 || resultWidth != componentWidth(operandDef->type_id())) {
|
||||
return;
|
||||
}
|
||||
identityConversions.push_back(inst);
|
||||
},
|
||||
/*run_on_debug_line_insts=*/false);
|
||||
|
||||
for (Instruction* conversion : identityConversions) {
|
||||
irContext->ReplaceAllUsesWith(conversion->result_id(),
|
||||
conversion->GetSingleWordInOperand(0));
|
||||
irContext->KillInst(conversion);
|
||||
}
|
||||
|
||||
// --- 6. the capability -------------------------------------------------------
|
||||
std::vector<Instruction*> deadCapabilities;
|
||||
for (Instruction& capability : irContext->capabilities()) {
|
||||
if (static_cast<spv::Capability>(capability.GetSingleWordInOperand(0)) ==
|
||||
spv::Capability::Float64) {
|
||||
deadCapabilities.push_back(&capability);
|
||||
}
|
||||
}
|
||||
for (Instruction* capability : deadCapabilities) {
|
||||
irContext->KillInst(capability);
|
||||
}
|
||||
|
||||
// --- 7. merge what the rewrite made into a duplicate -------------------------
|
||||
// `double` and `float` are now the same declaration, and so is every vector,
|
||||
// matrix, pointer and function type spelled in terms of them. Walking the section
|
||||
// in declaration order and replacing each duplicate the moment it is found means
|
||||
// the later types are already canonical by the time they are keyed: SPIR-V forbids
|
||||
// a type from forward-referencing another, so every operand of the instruction
|
||||
// being looked at has been through this loop already.
|
||||
std::map<std::vector<Uint32>, Uint32> keptTypeByShape;
|
||||
for (Instruction* type = &*irContext->types_values_begin(); type != nullptr;) {
|
||||
Instruction* next = type->NextNode();
|
||||
if (!IsDuplicableType(type->opcode())) {
|
||||
type = next;
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<Uint32> shape{static_cast<Uint32>(type->opcode())};
|
||||
for (Uint32 i = 0; i < type->NumInOperands(); ++i) {
|
||||
const Operand& operand = type->GetInOperand(i);
|
||||
shape.insert(shape.end(), operand.words.begin(), operand.words.end());
|
||||
}
|
||||
|
||||
const auto kept = keptTypeByShape.find(shape);
|
||||
if (kept == keptTypeByShape.end()) {
|
||||
keptTypeByShape.emplace(std::move(shape), type->result_id());
|
||||
type = next;
|
||||
continue;
|
||||
}
|
||||
|
||||
irContext->KillNamesAndDecorates(type->result_id());
|
||||
irContext->ReplaceAllUsesWith(type->result_id(), kept->second);
|
||||
irContext->KillInst(type);
|
||||
type = next;
|
||||
}
|
||||
|
||||
// --- 8. re-derive the layout of every block that held a 64-bit float -----------
|
||||
// See BlockRelayout for why this is a recomputation and not a preservation. Only
|
||||
// blocks that actually narrowed are touched: relaying out an untouched block would
|
||||
// be pure churn, and would risk disagreeing with glslang over a layout that was
|
||||
// already correct.
|
||||
for (Instruction& variable : irContext->types_values()) {
|
||||
if (variable.opcode() != spv::Op::OpVariable) continue;
|
||||
const auto storageClass =
|
||||
static_cast<spv::StorageClass>(variable.GetSingleWordInOperand(0));
|
||||
if (storageClass != spv::StorageClass::Uniform &&
|
||||
storageClass != spv::StorageClass::StorageBuffer &&
|
||||
storageClass != spv::StorageClass::PushConstant) {
|
||||
continue;
|
||||
}
|
||||
const Instruction* pointerType = defUseMgr->GetDef(variable.type_id());
|
||||
if (pointerType == nullptr) continue;
|
||||
|
||||
// An arrayed block (`uniform Blk { ... } blocks[4];`) is a pointer to an array
|
||||
// of the struct; the layout lives on the struct either way.
|
||||
const Instruction* blockType = defUseMgr->GetDef(pointerType->GetSingleWordInOperand(1));
|
||||
while (blockType != nullptr && (blockType->opcode() == spv::Op::OpTypeArray ||
|
||||
blockType->opcode() == spv::Op::OpTypeRuntimeArray)) {
|
||||
blockType = defUseMgr->GetDef(blockType->GetSingleWordInOperand(0));
|
||||
}
|
||||
if (blockType == nullptr || blockType->opcode() != spv::Op::OpTypeStruct) continue;
|
||||
if (wideTypeIds.count(blockType->result_id()) == 0) continue;
|
||||
|
||||
// A storage block packs std430, a uniform block std140. Before SPIR-V 1.3 a
|
||||
// storage block was a Uniform-storage variable whose struct carried
|
||||
// BufferBlock, so the decoration decides rather than the storage class alone.
|
||||
Bool isStorageBlock = storageClass == spv::StorageClass::StorageBuffer;
|
||||
for (const Instruction& annotation : irContext->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate) continue;
|
||||
if (annotation.GetSingleWordInOperand(0) != blockType->result_id()) continue;
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) ==
|
||||
spv::Decoration::BufferBlock) {
|
||||
isStorageBlock = true;
|
||||
}
|
||||
}
|
||||
|
||||
BlockRelayout relayout(irContext, /*std140=*/!isStorageBlock);
|
||||
if (relayout.Measure(blockType->result_id()).size == 0) {
|
||||
// A member shape the layout rules here do not describe. Leaving the block
|
||||
// at its 64-bit offsets keeps the module valid for Vulkan; SPIRV-Cross will
|
||||
// decline it for ESSL, which is the same outcome as before the demotion.
|
||||
MGLOG_D("DemoteFloat64Pass: block %%%u contains a member this pass cannot lay "
|
||||
"out; its 64-bit offsets are left in place",
|
||||
blockType->result_id());
|
||||
}
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken DemoteFloat64Pass::CreateDemoteFloat64Pass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<DemoteFloat64Pass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,84 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.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 {
|
||||
// Rewrites every 64-bit float in the module to a 32-bit one: `OpTypeFloat 64` becomes
|
||||
// `OpTypeFloat 32` in place, so every vector, matrix, array, struct, pointer and
|
||||
// function type that named it keeps its <id> and every decoration attached to it, and
|
||||
// only the meaning of the leaf type changes. The double literals are re-encoded, the
|
||||
// now-width-preserving OpFConvert pairs collapse to their operand, and the Float64
|
||||
// capability goes away.
|
||||
//
|
||||
// Why demote at all: no mobile GPU has it. Adreno and Mali both report
|
||||
// VkPhysicalDeviceFeatures::shaderFloat64 == VK_FALSE (the Magma POST has a row for it),
|
||||
// so a module declaring Float64 cannot become a pipeline there; and ESSL has no 64-bit
|
||||
// float type at all, so SPIRV-Cross throws "FP64 not supported in ES profile" and the
|
||||
// Espryt path never even reaches the driver. Demotion is what makes `double` in an
|
||||
// application's GLSL compile and run everywhere, at fp32 precision.
|
||||
//
|
||||
// BLOCK LAYOUT IS RE-DERIVED, NOT PRESERVED, and that was not the first choice - see
|
||||
// BlockRelayout in the .cpp for the measurement that forced it. Preserving the 64-bit
|
||||
// offsets (float + 4 bytes of padding in each slot) keeps the application's byte layout
|
||||
// intact and is what a Vulkan-only implementation would do, but GLSL ES has no member
|
||||
// `layout(offset=)`, so SPIRV-Cross recomputes std140/std430 from the declared types
|
||||
// and refuses any block whose stated offsets disagree - "Buffer block cannot be
|
||||
// expressed as any of std430, std140, scalar". Every shader with a double in a block
|
||||
// would then fail to transpile for Espryt at all, which is the case this demotion
|
||||
// exists to fix. Nor can padding members rescue it: a dmat4 member carries
|
||||
// MatrixStride 32 and std140 requires 16 for the demoted mat4, and padding BETWEEN
|
||||
// members cannot change a stride INSIDE one.
|
||||
//
|
||||
// What re-deriving costs, stated plainly: a block that held 64-bit members changes its
|
||||
// driver-visible byte layout, so an application that hard-codes std140 offsets it
|
||||
// computed for doubles addresses the wrong bytes. Applications that query their offsets
|
||||
// are unaffected. MobileGL's own default-uniform block is unaffected by construction:
|
||||
// the frontend builds its uniform routing by reflecting the module this pass produced
|
||||
// (ProgramSpirvTask::BuildGlobalUboRouting), so glUniform*d - which narrows to float
|
||||
// for the same reason - writes exactly where the demoted shader reads. Blocks with no
|
||||
// 64-bit member anywhere are never touched.
|
||||
//
|
||||
// Declines (leaves the module byte-identical, so the caller's existing "this module
|
||||
// still declares Float64" failure path reports it) when the module contains an
|
||||
// operation whose validity depends on the operand really being 64 bits wide:
|
||||
// - OpBitcast across the boundary - packDouble2x32 / doubleBitsToUint64 and friends,
|
||||
// where SPIR-V requires both sides to have the same total bit width;
|
||||
// - GLSL.std.450 PackDouble2x32 / UnpackDouble2x32, which are defined only for a
|
||||
// 64-bit float result/operand.
|
||||
//
|
||||
// ORDERING: must run before PackDoubleVertexInputsPass, which introduces exactly the
|
||||
// OpBitcast this pass declines on. After demotion no 64-bit vertex input is left, so
|
||||
// that pass becomes a no-op rather than a conflict.
|
||||
//
|
||||
// The in-place rewrite creates duplicate type declarations by construction - a module
|
||||
// that had both `double` and `float` ends up with two `OpTypeFloat 32`, and spirv-val
|
||||
// rejects that ("Duplicate non-aggregate type declarations are not allowed") - so the
|
||||
// pass merges them itself afterwards. Deliberately NOT by registering spvtools'
|
||||
// RemoveDuplicates alongside it: that pass also merges structurally identical STRUCTS
|
||||
// and calls KillNamesAndDecorates on the loser, which would silently delete the OpName
|
||||
// of one of two distinct-but-identically-shaped interface blocks - and OpName is how
|
||||
// MobileGL resolves block and varying names. Only the non-aggregate types spirv-val
|
||||
// actually forbids duplicates of are merged here; arrays and structs are left alone,
|
||||
// which also keeps a `double[]`'s ArrayStride from being merged into a `float[]`'s.
|
||||
class DemoteFloat64Pass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "mobilegl-demote-float64"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateDemoteFloat64Pass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -159,6 +159,7 @@ MobileGL supports runtime configuration via environment variables.
|
||||
| `MOBILEGL_DISABLE_TIMERQUERY` | Disable GPU timer-query exposure and use. | `0`, `1` | `0` |
|
||||
| `MOBILEGL_USE_ANGLE` | Load ANGLE EGL/GLES libraries. | `0`, `1` | `0` |
|
||||
| `MOBILEGL_DISABLE_SUBGROUP` | Disable Vulkan shader subgroup support. | `0`, `1` | `0` |
|
||||
| `MOBILEGL_ADVERTISE_FP64` | Advertise `GL_ARB_gpu_shader_fp64`. GLSL `double`/`dvec`/`dmat` compile and run either way - they are narrowed to 32 bits - so this only changes whether an application is told it has 64-bit precision, which it does not. | `0`, `1` | `0` |
|
||||
| `MOBILEGL_MAGMA_R11G11B10F_FALLBACK` | Use Magma's R11G11B10F format fallback. | `0`, `1` | `0` |
|
||||
| `MOBILEGL_MAGMA_FRAMESINFLIGHT` | Set Magma frames in flight. | Integer `1`–`64` | `3` |
|
||||
| `MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER` | Avoid sampler mipmap minification filters. | `0`, `1` | `0` |
|
||||
|
||||
Reference in New Issue
Block a user