mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 22:28:32 +09:00
Compare commits
10
Commits
5248b8b746
...
efa0345c36
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efa0345c36 | ||
|
|
ce0f18969c | ||
|
|
7ce0966e7d | ||
|
|
1c6ca2753f | ||
|
|
61b0532865 | ||
|
|
f5b8a505ed | ||
|
|
8371365db5 | ||
|
|
b219992ee3 | ||
|
|
94233ef928 | ||
|
|
0827d7a539 |
+116
-2
@@ -1,4 +1,4 @@
|
||||
name: Test
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -83,6 +83,8 @@ jobs:
|
||||
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
|
||||
-DMOBILEGL_BUILD_TEST=ON \
|
||||
-DMOBILEGL_BUILD_BENCHMARK=ON \
|
||||
-DMOBILEGL_BUILD_INTEGRATION_TEST=ON \
|
||||
-DMOBILEGL_ITEST_VK_ICD=/usr/share/vulkan/icd.d/lvp_icd.json \
|
||||
-DMOBILEGL_BUILD_TRACE_REPLAY=OFF \
|
||||
-DBENCHMARK_DOWNLOAD_DEPENDENCIES=ON \
|
||||
-DBENCHMARK_ENABLE_TESTING=OFF \
|
||||
@@ -110,6 +112,7 @@ jobs:
|
||||
"${BUILD_DIR}/CTestTestfile.cmake" \
|
||||
"${BUILD_DIR}/MobileGL/MG_Test" \
|
||||
"${BUILD_DIR}/MobileGL/MG_Benchmark" \
|
||||
"${BUILD_DIR}/MobileGL/MG_IntegrationTest" \
|
||||
"${SHARED_LIBS[@]}"
|
||||
|
||||
- name: Upload Linux runtime
|
||||
@@ -159,12 +162,102 @@ jobs:
|
||||
- name: Test
|
||||
working-directory: build-linux
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
|
||||
ctest -V -L unit --no-tests=error
|
||||
else
|
||||
ctest --output-on-failure -L unit --no-tests=error
|
||||
fi
|
||||
|
||||
- name: Upload core dumps
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: unit-core-dumps
|
||||
path: /tmp/core.*
|
||||
if-no-files-found: ignore
|
||||
|
||||
integration:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-linux
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get CMake
|
||||
uses: lukka/get-cmake@v4.3.3
|
||||
|
||||
- name: Install runtime dependencies
|
||||
# Same set as the benchmark job, for the same reason: the scenarios bring
|
||||
# up real headless EGL (llvmpipe) and Vulkan (lavapipe) contexts, and
|
||||
# libegl-mesa0 - the EGL vendor library behind glvnd's libegl1 dispatch -
|
||||
# only arrives as a Recommends.
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libvulkan1 libegl1 libegl-mesa0 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
|
||||
|
||||
- name: Download Linux runtime
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: mobilegl-linux-runtime
|
||||
path: .
|
||||
|
||||
- name: Unpack Linux runtime
|
||||
run: tar -xzf mobilegl-linux-runtime.tgz
|
||||
|
||||
- name: Normalize CTest command paths
|
||||
run: |
|
||||
python - <<'PY'
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
for path in Path('build-linux').rglob('CTestTestfile.cmake'):
|
||||
text = path.read_text()
|
||||
text = re.sub(r'"[^"]*/cmake-[^"]*/bin/cmake"', '"cmake"', text)
|
||||
path.write_text(text)
|
||||
PY
|
||||
|
||||
- name: Integration scenarios
|
||||
working-directory: build-linux
|
||||
# REQUIRE_GPU makes a driverless runner FAIL instead of skipping every
|
||||
# scenario - an all-skip run is otherwise indistinguishable from a pass,
|
||||
# which is how a five-month-old draw-dropping bug survived unseen until
|
||||
# this lane existed.
|
||||
#
|
||||
# The lavapipe ICD pin lives in the build-linux configure
|
||||
# (-DMOBILEGL_ITEST_VK_ICD), NOT here: the configure bakes it into each
|
||||
# test's ctest ENVIRONMENT property, and a property entry OVERRIDES the
|
||||
# job environment - a VK_ICD_FILENAMES exported here would be silently
|
||||
# ignored while looking like it works. This lane runs on lavapipe
|
||||
# deterministically, not on whichever of the eight Mesa ICDs a GPU-less
|
||||
# runner enumerates first.
|
||||
#
|
||||
# Cores are armed so that any crash - the harness pre-flight child's
|
||||
# included - leaves /tmp/core.*, which the failure-only step below ships
|
||||
# as an artifact. Analyzing a downloaded core against the runtime
|
||||
# artifact's binary in an ubuntu-24.04 userspace reproduces the exact
|
||||
# crash stack without burning a CI round on an in-workflow debugger.
|
||||
env:
|
||||
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
|
||||
ctest -V -L integration-gpu --no-tests=error
|
||||
else
|
||||
ctest --output-on-failure -L integration-gpu --no-tests=error
|
||||
fi
|
||||
|
||||
- name: Upload core dumps
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: integration-core-dumps
|
||||
path: /tmp/core.*
|
||||
if-no-files-found: ignore
|
||||
|
||||
benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-linux
|
||||
@@ -208,7 +301,18 @@ jobs:
|
||||
|
||||
- name: Benchmark
|
||||
working-directory: build-linux
|
||||
run: ctest -V -C Release -L benchmark --no-tests=error
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||
ctest -V -C Release -L benchmark --no-tests=error
|
||||
|
||||
- name: Upload core dumps
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: benchmark-core-dumps
|
||||
path: /tmp/core.*
|
||||
if-no-files-found: ignore
|
||||
|
||||
build-retrace:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -456,6 +560,8 @@ jobs:
|
||||
- name: Retrace and validate
|
||||
working-directory: build-retrace/tools/trace_replay
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
|
||||
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
|
||||
fi
|
||||
@@ -470,6 +576,14 @@ jobs:
|
||||
fi
|
||||
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
|
||||
|
||||
- name: Upload core dumps
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: retrace-core-dumps-${{ matrix.backend }}-${{ matrix.case }}
|
||||
path: /tmp/core.*
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Upload actual image
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
|
||||
@@ -205,6 +205,8 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||
|
||||
@@ -66,6 +66,11 @@ namespace MobileGL::MG_Config {
|
||||
// - DISPLAY: X11 session variable, not MobileGL configuration.
|
||||
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
|
||||
// (see MG_Util/Debug/Log.cpp).
|
||||
// - MOBILEGL_VALIDATE_SPIRV: test suites like SpirvPassTest exercise
|
||||
// ShaderCompiler without ever running MobileGL::Initialize(), and every
|
||||
// Initialize() re-runs MG_ConfigLoader::Init, which would clobber a
|
||||
// programmatic override stored here (see ShaderCompiler.cpp,
|
||||
// SpirvValidationEnabled).
|
||||
struct FeaturesTable {
|
||||
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
||||
Bool DisableTimerQuery = false;
|
||||
|
||||
@@ -37,6 +37,19 @@
|
||||
#define MOBILEGL_WGL_API MOBILEGL_API
|
||||
|
||||
// ====================== MobileGL configurations ======================= //
|
||||
// The numeric log levels live here, not only in Log.h: MOBILEGL_ASSERT below compares
|
||||
// MOBILEGL_LOG_ACTIVE_LEVEL against MOBILEGL_LOG_LEVEL_DEBUG, and in a translation unit
|
||||
// that includes Defines.h without Log.h both tokens would silently evaluate to 0 in the
|
||||
// preprocessor conditional - enabling the assert in exactly the INFO-level builds it is
|
||||
// documented to be compiled out of. Log.h redefines them identically, which is legal.
|
||||
#ifndef MOBILEGL_LOG_LEVEL_DEBUG
|
||||
#define MOBILEGL_LOG_LEVEL_DEBUG 0
|
||||
#define MOBILEGL_LOG_LEVEL_WARN 1
|
||||
#define MOBILEGL_LOG_LEVEL_ERROR 2
|
||||
#define MOBILEGL_LOG_LEVEL_INFO 3
|
||||
#define MOBILEGL_LOG_LEVEL_FATAL 4
|
||||
#endif
|
||||
|
||||
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL
|
||||
#define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_INFO
|
||||
#endif
|
||||
|
||||
@@ -966,6 +966,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (drawcount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// With no element-array buffer bound, every indices[i] is a client pointer into a
|
||||
// separate CPU allocation, not an offset into one shared buffer. The batched payload
|
||||
// below cannot express that: it carries ONE index-buffer view for the whole batch and
|
||||
// turns each pointer into a firstIndex relative to it. Replay the sub-draws through
|
||||
// the single-draw entry point instead - it snapshots each client range into its own
|
||||
// transient slice, which is exactly what the unrolled draws this must match do.
|
||||
// (The batch used to be built this way; the shared-view rewrite that added
|
||||
// MultiDrawIndexedCmd left the client-memory shape addressing a view whose byte
|
||||
// offset is a hardcoded 0, so UploadAndBindIndexBuffer saw a null client pointer,
|
||||
// declined the whole batch and painted nothing.)
|
||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (vao.GetIndexBufferBindingSlot().GetBoundObject() == nullptr) {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] <= 0) {
|
||||
continue;
|
||||
}
|
||||
DrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||
basevertex != nullptr ? basevertex[i] : 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
MultiDrawIndexedCmd payload{};
|
||||
payload.mode = mode;
|
||||
payload.indexBufferView.indexType = type;
|
||||
|
||||
@@ -111,6 +111,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (buffer.IsValid()) {
|
||||
// Outgrown, not dead: every BufferSlice handed out from this frame's arena so far
|
||||
// still names it, and those slices stay in service until the frame slot is rewound
|
||||
// (VkBufferResource::transientSlice, the converted-vertex-stream cache, the draw
|
||||
// memos). The release therefore has to survive every mid-frame reclaim and land on
|
||||
// the next ResetFrame of this slot - see VkBufferManager::CollectAllDeferredReleases.
|
||||
m_deferredReleases[frameIndex].push_back(std::move(buffer));
|
||||
}
|
||||
|
||||
|
||||
@@ -372,6 +372,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
spv_diagnostic diagnostic = nullptr;
|
||||
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
|
||||
if (result != SPV_SUCCESS) {
|
||||
// MGLOG_I, not E: at the INFO compile level of the CI/test lanes that arm
|
||||
// the validation switch, MGLOG_E is compiled out (Log.h orders
|
||||
// DEBUG < WARN < ERROR < INFO) and the VUID would never reach a log. The
|
||||
// latch is what a test harness asserts on.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
|
||||
MGLOG_I(
|
||||
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s",
|
||||
static_cast<Int>(shaderStage),
|
||||
programExternalIndex,
|
||||
static_cast<Int>(result),
|
||||
diagnostic != nullptr ? diagnostic->position.index : 0,
|
||||
diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<null>");
|
||||
}
|
||||
MOBILEGL_ASSERT(
|
||||
result == SPV_SUCCESS,
|
||||
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d line=%zu column=%zu index=%zu msg=%s",
|
||||
@@ -1263,8 +1277,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||
spvtools::OptimizerOptions options;
|
||||
// Matches the position-fix pass: this build of spirv-tools asserts rather than
|
||||
// reporting, so validation stays off in the shipping path.
|
||||
// Always off: the optimizer's input validator conflates "input invalid" with
|
||||
// "transform failed", and this call site fails open. Validating lanes check the
|
||||
// FINAL module via ValidateTransformedSpirv, which latches instead of rerouting
|
||||
// control flow.
|
||||
options.set_run_validator(false);
|
||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char* message) {
|
||||
@@ -1304,7 +1320,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling
|
||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char* message) {
|
||||
MGLOG_E("Vulkan: xfb capture pass: %s", message != nullptr ? message : "");
|
||||
@@ -1334,7 +1350,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling
|
||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char* message) {
|
||||
MGLOG_E("Vulkan: position fix pass: %s", message != nullptr ? message : "");
|
||||
});
|
||||
optimizer.RegisterPass(CreateGlToVulkanPositionFixPass(transformFlags));
|
||||
|
||||
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
||||
@@ -2525,6 +2545,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex());
|
||||
#else
|
||||
// Final module the driver receives; also checked in the INFO-level CI/test
|
||||
// lanes, where the DEBUG gate above is compiled out.
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) {
|
||||
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex());
|
||||
}
|
||||
#endif
|
||||
|
||||
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
|
||||
@@ -161,12 +161,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void VkBufferManager::CollectAllDeferredReleases() {
|
||||
// Per-resource releases only. Every one of them was deferred behind a BumpSliceEpoch,
|
||||
// so no memo can still name the handle, and the caller has proved the GPU is idle.
|
||||
//
|
||||
// The transient arena's releases are deliberately NOT collected here. A buffer lands
|
||||
// there when the arena outgrows it mid-frame (BufferArena::EnsureCapacity), and at
|
||||
// that moment every slice already handed out from this frame's arena still names it -
|
||||
// VkBufferResource::transientSlice above all, which AcquireStreamedSlice keeps
|
||||
// serving for the whole frame serial on the strength of transientFrameSerial alone.
|
||||
// Nothing bumps the slice epoch for those other resources, so freeing the buffer
|
||||
// here left the streamed memo handing a destroyed VkBuffer to vkCmdBindIndexBuffer
|
||||
// (llvmpipe then faulted inside the draw; the Create/Flywheel indirect retrace died
|
||||
// exactly this way). Mid-frame drains do not advance m_frameSerial, so they must not
|
||||
// free arena storage either: the arena's own ResetFrame/BeginFrame is the point where
|
||||
// the slot's slices stop being reachable, and that is where these releases land.
|
||||
for (Uint32 frameIndex = 0; frameIndex < m_deferredBufferReleases.size(); ++frameIndex) {
|
||||
CollectDeferredReleases(frameIndex);
|
||||
}
|
||||
for (Uint32 frameIndex = 0; frameIndex < m_transientUploadArena.GetFrameCount(); ++frameIndex) {
|
||||
m_transientUploadArena.CollectDeferredReleases(frameIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void VkBufferManager::NotifyDeviceIdle() {
|
||||
|
||||
@@ -102,10 +102,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Recreate all per-frame transient arenas
|
||||
Bool RecreateTransientArenas(Uint32 frameCount);
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
// Drains every frame slot's deferred buffer/resource releases (and the
|
||||
// transient arena's parked superseded blocks). Only valid when the
|
||||
// caller has proven every queue submission complete; used by the
|
||||
// present-less frame-boundary drain.
|
||||
// Drains every frame slot's deferred buffer/resource releases. Only valid when
|
||||
// the caller has proven every queue submission complete; used by the present-less
|
||||
// frame-boundary drain. Deliberately does NOT touch the transient arena's parked
|
||||
// superseded blocks: those are still named by this frame's slices (see the
|
||||
// definition), and only a frame rewind retires them.
|
||||
void CollectAllDeferredReleases();
|
||||
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
|
||||
void NotifyDeviceIdle();
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
#include "MG_State/GLState/TextureState/TextureObject.h"
|
||||
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
|
||||
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||
// Only reached from an MGLOG_W, which the shipping INFO log level compiles out - so the
|
||||
// missing include never broke a default build and did break every WARN/DEBUG-level one.
|
||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/RenderStateEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Math/HalfFloat.h"
|
||||
@@ -3009,15 +3012,10 @@ void main() {
|
||||
|
||||
#if defined(VK_USE_PLATFORM_XLIB_KHR)
|
||||
if (m_platformDisplay != nullptr) {
|
||||
if (m_ownsFallbackXlibWindow && m_window != 0 && m_platformLibrary != nullptr) {
|
||||
using XDestroyWindowFn = int (*)(Display*, Window);
|
||||
auto* destroyWindow = reinterpret_cast<XDestroyWindowFn>(dlsym(m_platformLibrary, "XDestroyWindow"));
|
||||
if (destroyWindow) {
|
||||
destroyWindow(static_cast<Display*>(m_platformDisplay), static_cast<Window>(m_window));
|
||||
}
|
||||
m_window = 0;
|
||||
m_ownsFallbackXlibWindow = false;
|
||||
}
|
||||
// No fallback window to destroy any more: the display here is only ever
|
||||
// one this renderer opened for a REAL window surface, and that window is
|
||||
// the caller's to own. The hidden-window pbuffer fallback that used to be
|
||||
// cleaned up here is gone (see CreateSurface).
|
||||
using XCloseDisplayFn = int (*)(Display*);
|
||||
auto* closeDisplay = reinterpret_cast<XCloseDisplayFn>(m_platformCloseDisplay);
|
||||
if (closeDisplay) {
|
||||
@@ -5568,6 +5566,19 @@ void main() {
|
||||
MakeXfbWritesVisible();
|
||||
VkTextureManager::DrawSyncScope drawSyncScope(*m_textureManager);
|
||||
m_textureManager->CollectGarbage();
|
||||
{
|
||||
// Mirror DirectGLES's SyncToBackend gate: a program whose phase-B job failed or
|
||||
// was cancelled has no usable optimized module - and on an in-place
|
||||
// SanitizeAndOptimizeBinary failure GetGeneratedSpirv() still holds the RAW
|
||||
// glslang words, which must never reach vkCreateShaderModule. Drop the draw.
|
||||
const auto& drawProgram = *MG_State::pGLContext->GetProgramForDraw();
|
||||
if (!drawProgram.GetLinkStatus() || !drawProgram.GetSpirvStatus()) {
|
||||
MGLOG_D("SetupDraw skipped: program=%u is linked=%d spirv=%d",
|
||||
drawProgram.GetExternalIndex(), static_cast<int>(drawProgram.GetLinkStatus()),
|
||||
static_cast<int>(drawProgram.GetSpirvStatus()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (TrySetupDrawFastPath(frame, mode, aspects, drawParams, pIndexBufferView)) {
|
||||
return true;
|
||||
}
|
||||
@@ -6018,6 +6029,11 @@ void main() {
|
||||
m_textureManager->CollectGarbage();
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
|
||||
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) {
|
||||
MGLOG_E("DispatchCompute skipped: program=%u has no optimized SPIR-V",
|
||||
program.GetExternalIndex());
|
||||
return;
|
||||
}
|
||||
ProgramFactory::CompileOptionFlags transformFlags = 0;
|
||||
const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags);
|
||||
|
||||
@@ -6058,6 +6074,11 @@ void main() {
|
||||
m_textureManager->CollectGarbage();
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
|
||||
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) {
|
||||
MGLOG_E("DispatchComputeIndirect skipped: program=%u has no optimized SPIR-V",
|
||||
program.GetExternalIndex());
|
||||
return;
|
||||
}
|
||||
ProgramFactory::CompileOptionFlags transformFlags = 0;
|
||||
const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags);
|
||||
|
||||
@@ -10904,17 +10925,24 @@ void main() {
|
||||
exts.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
|
||||
}
|
||||
#elif defined VK_USE_PLATFORM_XLIB_KHR
|
||||
// An offscreen surface has ZERO window-system dependence, by design and on
|
||||
// every machine - including ones that do have a display. There used to be a
|
||||
// fallback here that requested VK_KHR_xlib_surface and had CreateSurface()
|
||||
// open a hidden, never-mapped X window; it is gone. A pbuffer that quietly
|
||||
// needs an X server is a pbuffer that works on a workstation and dies on a
|
||||
// headless runner, which is exactly what it did: with no DISPLAY, XOpenDisplay
|
||||
// returned null and the next Xlib call segfaulted. If the loader genuinely has
|
||||
// no VK_EXT_headless_surface, that is an honest bring-up failure and is
|
||||
// reported as one below - never papered over with a window.
|
||||
m_headlessSurfaceSupported = IsExtensionSupported(m_extensions, VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
|
||||
if (m_headlessSurfaceSupported) {
|
||||
exts.push_back(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
|
||||
} else {
|
||||
// Real ICDs (e.g. NVIDIA's proprietary Linux driver) may not implement
|
||||
// VK_EXT_headless_surface at all. CreateSurface() falls back to a
|
||||
// hidden Xlib window in that case, so request that extension instead.
|
||||
MGLOG_I("%s not available; falling back to a hidden %s surface for the pbuffer context.",
|
||||
VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME, VK_KHR_XLIB_SURFACE_EXTENSION_NAME);
|
||||
exts.push_back(VK_KHR_XLIB_SURFACE_EXTENSION_NAME);
|
||||
if (!m_headlessSurfaceSupported) {
|
||||
MGLOG_F("%s is not available from this Vulkan loader, so an offscreen (pbuffer) DirectVulkan "
|
||||
"surface cannot be created. Refusing to substitute a window: offscreen surfaces must not "
|
||||
"depend on a window system. Install an ICD that implements it (lavapipe does).",
|
||||
VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
|
||||
throw RuntimeError("VK_EXT_headless_surface is unavailable for an offscreen DirectVulkan surface");
|
||||
}
|
||||
exts.push_back(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
|
||||
#else
|
||||
exts.push_back(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
|
||||
#endif
|
||||
@@ -11053,17 +11081,38 @@ void main() {
|
||||
|
||||
void VulkanRenderer::PickPhysicalDevice() {
|
||||
Uint32 deviceCount = 0;
|
||||
vkEnumeratePhysicalDevices(m_instance, &deviceCount, nullptr);
|
||||
VK_VERIFY(vkEnumeratePhysicalDevices(m_instance, &deviceCount, nullptr));
|
||||
if (deviceCount == 0) {
|
||||
MGLOG_E("No physical devices supporting Vulkan found.");
|
||||
} else {
|
||||
MGLOG_I("Found %d physical device(s).", deviceCount);
|
||||
// A real, reachable configuration, not a broken invariant: an instance can be
|
||||
// created from ICDs that load perfectly and then expose no device at all - a
|
||||
// GPU-less machine with the vendor ICDs installed (RADV/ANV/NVK on a CI runner)
|
||||
// is exactly that. It has to be a bring-up failure the caller can report.
|
||||
//
|
||||
// It used to be MGLOG_E + MOBILEGL_ASSERT, and BOTH are compiled out at the INFO
|
||||
// log level every shipping and CI build uses (Log.h orders DEBUG < WARN < ERROR
|
||||
// < INFO), so the count-zero case fell through in silence to `devices[0]` on an
|
||||
// EMPTY vector below and segfaulted in vkGetPhysicalDeviceProperties.
|
||||
MGLOG_F("No Vulkan physical devices found: the instance loaded ICDs but none of them exposes a "
|
||||
"device. Cannot bring up DirectVulkan. (A software ICD such as lavapipe provides one; "
|
||||
"pin it with VK_ICD_FILENAMES if the machine has no GPU.)");
|
||||
throw RuntimeError("No Vulkan physical devices available for DirectVulkan");
|
||||
}
|
||||
|
||||
MOBILEGL_ASSERT(deviceCount > 0, "No physical devices found.");
|
||||
MGLOG_I("Found %d physical device(s).", deviceCount);
|
||||
|
||||
Vector<VkPhysicalDevice> devices(deviceCount);
|
||||
vkEnumeratePhysicalDevices(m_instance, &deviceCount, devices.data());
|
||||
// Same truncation hazard as the instance-extension enumeration: a VK_INCOMPLETE here
|
||||
// leaves the tail of `devices` default-constructed (VK_NULL_HANDLE), and every one of
|
||||
// those is a null handle waiting to be passed to the driver. Take only what was
|
||||
// actually written.
|
||||
const VkResult enumerateResult = vkEnumeratePhysicalDevices(m_instance, &deviceCount, devices.data());
|
||||
if (enumerateResult != VK_SUCCESS && enumerateResult != VK_INCOMPLETE) {
|
||||
VK_VERIFY(enumerateResult, "vkEnumeratePhysicalDevices failed");
|
||||
}
|
||||
devices.resize(deviceCount);
|
||||
if (devices.empty()) {
|
||||
MGLOG_F("vkEnumeratePhysicalDevices reported devices and then wrote none");
|
||||
throw RuntimeError("No Vulkan physical devices available for DirectVulkan");
|
||||
}
|
||||
for (Int i = 0; i < deviceCount; i++) {
|
||||
if (GetMoreCapablePhysicalDevice(devices[i], m_surface, m_physicalDevice, m_physicalDevice))
|
||||
MGLOG_I("Picked physical device %d.", i);
|
||||
@@ -11898,26 +11947,36 @@ void main() {
|
||||
m_window = reinterpret_cast<NativeWindowType>(nativeWindow);
|
||||
}
|
||||
#elif defined VK_USE_PLATFORM_XLIB_KHR
|
||||
if (m_headlessSurfaceSupported) {
|
||||
// No fall-through to Xlib: an offscreen surface never touches a window
|
||||
// system. CreateInstance() has already refused the bring-up if the loader
|
||||
// lacks the extension, so reaching here without it is a broken invariant
|
||||
// rather than a platform limitation - report it and fail, do not continue.
|
||||
auto* createHeadlessSurface =
|
||||
reinterpret_cast<PFN_vkCreateHeadlessSurfaceEXT>(
|
||||
vkGetInstanceProcAddr(m_instance, "vkCreateHeadlessSurfaceEXT"));
|
||||
MOBILEGL_ASSERT(createHeadlessSurface != nullptr,
|
||||
"VK_EXT_headless_surface is not available for DirectVulkan pbuffer surface");
|
||||
if (!m_headlessSurfaceSupported || createHeadlessSurface == nullptr) {
|
||||
MGLOG_F("vkCreateHeadlessSurfaceEXT is unavailable (%s reported as %s) while creating an "
|
||||
"offscreen DirectVulkan surface",
|
||||
VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME,
|
||||
m_headlessSurfaceSupported ? "supported" : "unsupported");
|
||||
throw RuntimeError("vkCreateHeadlessSurfaceEXT is unavailable for an offscreen DirectVulkan surface");
|
||||
}
|
||||
VkHeadlessSurfaceCreateInfoEXT sci{VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT};
|
||||
VK_VERIFY(createHeadlessSurface(m_instance, &sci, nullptr, &m_surface),
|
||||
"vkCreateHeadlessSurfaceEXT failed");
|
||||
return;
|
||||
}
|
||||
// VK_EXT_headless_surface unavailable on this ICD: fall through to the
|
||||
// Xlib branch below, which creates a hidden window since m_window is
|
||||
// still null here.
|
||||
#else
|
||||
auto* createHeadlessSurface =
|
||||
reinterpret_cast<PFN_vkCreateHeadlessSurfaceEXT>(
|
||||
vkGetInstanceProcAddr(m_instance, "vkCreateHeadlessSurfaceEXT"));
|
||||
MOBILEGL_ASSERT(createHeadlessSurface != nullptr,
|
||||
"VK_EXT_headless_surface is not available for DirectVulkan pbuffer surface");
|
||||
if (createHeadlessSurface == nullptr) {
|
||||
// Same class as the Xlib branch above: a null entry point behind
|
||||
// MOBILEGL_ASSERT is a segv on the next line in every INFO-level build.
|
||||
MGLOG_F("vkCreateHeadlessSurfaceEXT is unavailable while creating an offscreen DirectVulkan "
|
||||
"surface (%s missing from this loader)",
|
||||
VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
|
||||
throw RuntimeError("vkCreateHeadlessSurfaceEXT is unavailable for an offscreen DirectVulkan surface");
|
||||
}
|
||||
VkHeadlessSurfaceCreateInfoEXT sci{VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT};
|
||||
VK_VERIFY(createHeadlessSurface(m_instance, &sci, nullptr, &m_surface),
|
||||
"vkCreateHeadlessSurfaceEXT failed");
|
||||
@@ -11946,59 +12005,52 @@ void main() {
|
||||
sci.pLayer = reinterpret_cast<const void*>(m_window);
|
||||
VK_VERIFY(vkCreateMetalSurfaceEXT(m_instance, &sci, nullptr, &m_surface), "vkCreateMetalSurfaceEXT failed");
|
||||
#elif defined VK_USE_PLATFORM_XLIB_KHR
|
||||
// m_window may legitimately still be null here: the pbuffer/headless-fallback
|
||||
// path below creates its own window when the caller didn't provide one.
|
||||
// Reached only for a REAL on-screen window surface (a windowed desktop app,
|
||||
// retrace in window mode). Presentation to a window legitimately needs a
|
||||
// window system; offscreen requests returned above and never come here, so
|
||||
// there is no longer any path that opens a display on a caller's behalf.
|
||||
//
|
||||
// Every failure below is a real error return, not MOBILEGL_ASSERT: that macro
|
||||
// is compiled out at the INFO log level every shipping and CI build uses, so
|
||||
// asserting here meant a null Display sailed straight into the next Xlib call
|
||||
// and segfaulted - which is exactly how this presented in CI.
|
||||
if (!m_window) {
|
||||
MGLOG_F("CreateSurface: a window surface was requested with no native window");
|
||||
throw RuntimeError("CreateSurface: no native window for the Vulkan Xlib surface");
|
||||
}
|
||||
|
||||
void* x11Lib = dlopen("libX11.so.6", RTLD_LOCAL | RTLD_NOW);
|
||||
if (!x11Lib) {
|
||||
x11Lib = dlopen("libX11.so", RTLD_LOCAL | RTLD_NOW);
|
||||
}
|
||||
MOBILEGL_ASSERT(x11Lib != nullptr, "Failed to open libX11 while creating Vulkan Xlib surface");
|
||||
if (x11Lib == nullptr) {
|
||||
MGLOG_F("Failed to open libX11 (.so.6 and .so) while creating a Vulkan Xlib window surface: %s",
|
||||
dlerror());
|
||||
throw RuntimeError("libX11 is unavailable for the Vulkan Xlib window surface");
|
||||
}
|
||||
using XOpenDisplayFn = Display* (*)(const char*);
|
||||
using XCloseDisplayFn = int (*)(Display*);
|
||||
auto* xOpenDisplay = reinterpret_cast<XOpenDisplayFn>(dlsym(x11Lib, "XOpenDisplay"));
|
||||
auto* xCloseDisplay = reinterpret_cast<XCloseDisplayFn>(dlsym(x11Lib, "XCloseDisplay"));
|
||||
MOBILEGL_ASSERT(xOpenDisplay != nullptr && xCloseDisplay != nullptr,
|
||||
"Failed to resolve XOpenDisplay/XCloseDisplay while creating Vulkan Xlib surface");
|
||||
if (xOpenDisplay == nullptr || xCloseDisplay == nullptr) {
|
||||
MGLOG_F("Failed to resolve XOpenDisplay/XCloseDisplay while creating a Vulkan Xlib window surface");
|
||||
dlclose(x11Lib);
|
||||
throw RuntimeError("libX11 is missing XOpenDisplay/XCloseDisplay");
|
||||
}
|
||||
|
||||
auto* display = xOpenDisplay(std::getenv("DISPLAY"));
|
||||
MOBILEGL_ASSERT(display != nullptr, "XOpenDisplay failed while creating Vulkan Xlib surface");
|
||||
const char* displayName = std::getenv("DISPLAY");
|
||||
auto* display = xOpenDisplay(displayName);
|
||||
if (display == nullptr) {
|
||||
MGLOG_F("XOpenDisplay(%s) failed while creating a Vulkan Xlib window surface; there is no usable X "
|
||||
"display for the requested window surface",
|
||||
displayName != nullptr ? displayName : "<DISPLAY unset>");
|
||||
dlclose(x11Lib);
|
||||
throw RuntimeError("XOpenDisplay failed for the Vulkan Xlib window surface");
|
||||
}
|
||||
m_platformDisplay = display;
|
||||
m_platformLibrary = x11Lib;
|
||||
m_platformCloseDisplay = reinterpret_cast<void*>(xCloseDisplay);
|
||||
|
||||
if (!m_window) {
|
||||
// Pbuffer/headless fallback: the ICD didn't implement
|
||||
// VK_EXT_headless_surface (e.g. NVIDIA's proprietary Linux driver), so
|
||||
// CreateInstance() requested VK_KHR_xlib_surface instead and left
|
||||
// m_window null for us to fill in here. This window is never mapped -
|
||||
// it exists only to give the WSI a valid drawable - so nothing is ever
|
||||
// shown on screen; the swapchain image backs the GL default framebuffer
|
||||
// exactly like the headless-surface path does.
|
||||
using XDefaultRootWindowFn = Window (*)(Display*);
|
||||
using XDefaultScreenFn = int (*)(Display*);
|
||||
using XBlackPixelFn = unsigned long (*)(Display*, int);
|
||||
using XCreateSimpleWindowFn = Window (*)(Display*, Window, int, int, unsigned int, unsigned int,
|
||||
unsigned int, unsigned long, unsigned long);
|
||||
auto* xDefaultRootWindow = reinterpret_cast<XDefaultRootWindowFn>(dlsym(x11Lib, "XDefaultRootWindow"));
|
||||
auto* xDefaultScreen = reinterpret_cast<XDefaultScreenFn>(dlsym(x11Lib, "XDefaultScreen"));
|
||||
auto* xBlackPixel = reinterpret_cast<XBlackPixelFn>(dlsym(x11Lib, "XBlackPixel"));
|
||||
auto* xCreateSimpleWindow =
|
||||
reinterpret_cast<XCreateSimpleWindowFn>(dlsym(x11Lib, "XCreateSimpleWindow"));
|
||||
MOBILEGL_ASSERT(xDefaultRootWindow && xDefaultScreen && xBlackPixel && xCreateSimpleWindow,
|
||||
"Failed to resolve XCreateSimpleWindow dependencies for the Xlib pbuffer fallback");
|
||||
|
||||
const int screen = xDefaultScreen(display);
|
||||
const Uint32 width = std::max<Uint32>(m_config.SurfaceWidth, 1);
|
||||
const Uint32 height = std::max<Uint32>(m_config.SurfaceHeight, 1);
|
||||
const Window fallbackWindow = xCreateSimpleWindow(display, xDefaultRootWindow(display), 0, 0, width,
|
||||
height, 0, xBlackPixel(display, screen),
|
||||
xBlackPixel(display, screen));
|
||||
MOBILEGL_ASSERT(fallbackWindow != 0, "XCreateSimpleWindow failed for the Xlib pbuffer fallback");
|
||||
m_window = static_cast<NativeWindowType>(fallbackWindow);
|
||||
m_ownsFallbackXlibWindow = true;
|
||||
}
|
||||
|
||||
VkXlibSurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR};
|
||||
sci.dpy = display;
|
||||
sci.window = static_cast<Window>(m_window);
|
||||
@@ -12048,10 +12100,40 @@ void main() {
|
||||
}
|
||||
|
||||
Vector<VkExtensionProperties> VulkanRenderer::EnumerateInstanceExtensions() {
|
||||
// The two-call idiom has a race the spec explicitly allows for: the loader
|
||||
// re-scans ICDs, so the property count can GROW between the sizing call and
|
||||
// the fill call, and the fill then returns VK_INCOMPLETE having written only
|
||||
// as many entries as the caller asked for. The result is a silently TRUNCATED
|
||||
// extension list - and which extensions fall off the end is exactly as stable
|
||||
// as the loader's scan order, i.e. not at all. That is how a headless CI
|
||||
// runner could decide VK_EXT_headless_surface did not exist on one run and
|
||||
// did on the next, sending the pbuffer path into the Xlib fallback with no
|
||||
// X server to open. The sibling EnumerateDeviceExtensions below already
|
||||
// checked its second call; this one dropped the result on the floor.
|
||||
// Loop until a fill call agrees with its own sizing call.
|
||||
Vector<VkExtensionProperties> extensions;
|
||||
for (Uint32 attempt = 0; attempt < 8; ++attempt) {
|
||||
Uint32 extensionCount = 0;
|
||||
VK_VERIFY(vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr));
|
||||
Vector<VkExtensionProperties> extensions(extensionCount);
|
||||
extensions.resize(extensionCount);
|
||||
if (extensionCount == 0) {
|
||||
return extensions;
|
||||
}
|
||||
const VkResult result =
|
||||
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data());
|
||||
if (result == VK_SUCCESS) {
|
||||
extensions.resize(extensionCount);
|
||||
return extensions;
|
||||
}
|
||||
if (result != VK_INCOMPLETE) {
|
||||
VK_VERIFY(result, "vkEnumerateInstanceExtensionProperties failed");
|
||||
return extensions;
|
||||
}
|
||||
MGLOG_I("vkEnumerateInstanceExtensionProperties returned VK_INCOMPLETE (the loader's list grew "
|
||||
"mid-enumeration); re-enumerating");
|
||||
}
|
||||
MGLOG_F("vkEnumerateInstanceExtensionProperties never settled; the instance extension list may be "
|
||||
"truncated and surface-extension selection is about to be made on incomplete information");
|
||||
return extensions;
|
||||
}
|
||||
|
||||
|
||||
@@ -445,15 +445,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void* m_platformDisplay = nullptr;
|
||||
void* m_platformLibrary = nullptr;
|
||||
void* m_platformCloseDisplay = nullptr;
|
||||
// Some real ICDs (e.g. NVIDIA's proprietary Linux driver) don't implement
|
||||
// VK_EXT_headless_surface at all. Detected once in CreateInstance() from the
|
||||
// enumerated instance extensions; when false, CreateSurface() falls back to a
|
||||
// hidden Xlib window instead of vkCreateHeadlessSurfaceEXT.
|
||||
// Whether the loader exposes VK_EXT_headless_surface, detected once in
|
||||
// CreateInstance() from the enumerated instance extensions. On desktop an
|
||||
// offscreen surface REQUIRES it: false is a clean, loud bring-up failure, never
|
||||
// a substituted window. (Android is the one exception and has its own path -
|
||||
// no Mali/Adreno driver seen so far exposes the extension, so a windowless
|
||||
// context is given an AImageReader ANativeWindow that is never displayed.)
|
||||
Bool m_headlessSurfaceSupported = true;
|
||||
// Set when CreateSurface() had to create its own Xlib window for the fallback
|
||||
// above (rather than being handed one by the caller), so Shutdown() knows it
|
||||
// owns that window and must destroy it.
|
||||
Bool m_ownsFallbackXlibWindow = false;
|
||||
// Android has the same shortfall: no Mali/Adreno driver seen so far exposes
|
||||
// VK_EXT_headless_surface, so a windowless (EGL pbuffer) context gets an
|
||||
// AImageReader's ANativeWindow to hand the WSI instead. Nothing is ever
|
||||
|
||||
@@ -169,25 +169,24 @@ endif()
|
||||
option(MOBILEGL_ITEST_REQUIRE_GPU
|
||||
"Fail (rather than skip) the integration scenarios when the headless harness is unusable" OFF)
|
||||
|
||||
# DirectGLES asks the system EGL for a pbuffer config, and on Mesa the default
|
||||
# platform is not X11 unless it is said out loud (run_driver_bench.sh sets the
|
||||
# same variable). Wrong platform here is not a soft failure: eglCreatePbuffer
|
||||
# fails and every scenario skips.
|
||||
if (UNIX AND NOT APPLE AND NOT ANDROID)
|
||||
set(MOBILEGL_ITEST_EGL_PLATFORM "x11" CACHE STRING
|
||||
"EGL_PLATFORM for the integration tests (empty: leave the loader alone)")
|
||||
else()
|
||||
set(MOBILEGL_ITEST_EGL_PLATFORM "" CACHE STRING
|
||||
"EGL_PLATFORM for the integration tests (empty: leave the loader alone)")
|
||||
endif()
|
||||
# No EGL_PLATFORM knob here on purpose. The harness pins EGL_PLATFORM=surfaceless
|
||||
# itself before its first EGL call (HeadlessGL.cpp, EnsureHeadlessPlatform) so a
|
||||
# developer's machine and a CI runner take the SAME path whether or not a window
|
||||
# system happens to be running. This used to inject "x11", which is how the lane
|
||||
# came up green on a workstation with WSLg and died on a runner with no X server.
|
||||
#
|
||||
# A build-system knob would not just be redundant, it would be a trap: `set(...
|
||||
# CACHE ...)` does not rewrite an existing cache, so every build directory
|
||||
# configured before this change would keep injecting EGL_PLATFORM=x11 and go on
|
||||
# binding to a window system - silently, and only on the machines that have one.
|
||||
# Someone reproducing a platform-specific bug sets EGL_PLATFORM in their own
|
||||
# environment, which the harness still honours.
|
||||
|
||||
set(MGL_ITEST_COMMON_ENV "")
|
||||
if (MOBILEGL_ITEST_EGL_VENDOR)
|
||||
list(APPEND MGL_ITEST_COMMON_ENV "__EGL_VENDOR_LIBRARY_FILENAMES=${MOBILEGL_ITEST_EGL_VENDOR}")
|
||||
endif()
|
||||
if (MOBILEGL_ITEST_EGL_PLATFORM)
|
||||
list(APPEND MGL_ITEST_COMMON_ENV "EGL_PLATFORM=${MOBILEGL_ITEST_EGL_PLATFORM}")
|
||||
endif()
|
||||
unset(MOBILEGL_ITEST_EGL_PLATFORM CACHE) # see above: an old cache must not resurrect x11
|
||||
if (MOBILEGL_ITEST_REQUIRE_GPU)
|
||||
list(APPEND MGL_ITEST_COMMON_ENV "MOBILEGL_ITEST_REQUIRE_GPU=1")
|
||||
endif()
|
||||
|
||||
@@ -74,6 +74,40 @@ namespace MGITest {
|
||||
std::string renderer;
|
||||
};
|
||||
|
||||
// The harness is headless BY CONSTRUCTION, on every machine: it must never
|
||||
// reach a window system, not even where one happens to be running. This is
|
||||
// not a CI accommodation - it is what keeps a developer's run and a CI run
|
||||
// the same run. The lane was wired up green on a workstation and immediately
|
||||
// died on the runner precisely because the workstation had a DISPLAY (WSLg)
|
||||
// and took Mesa's x11 platform, while the runner has none; that divergence
|
||||
// is the bug, and pinning the platform here is the fix for it.
|
||||
//
|
||||
// Mesa selects its EGL platform from EGL_PLATFORM at loader time, so this
|
||||
// has to run before the first EGL call in the process (see EnsureHeadless
|
||||
// callers). surfaceless is the platform with no window-system dependency at
|
||||
// all; the surface this file then creates is still a pbuffer, which every
|
||||
// platform supports and which the amendment to this rule requires as the
|
||||
// fallback shape. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
|
||||
// driver that consults them directly cannot reintroduce the dependency
|
||||
// behind EGL's back. Desktop-only file: MG_IntegrationTest never builds
|
||||
// for Android, so no device path is affected.
|
||||
void EnsureHeadlessPlatform() {
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
static bool done = false;
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
done = true;
|
||||
// An explicit EGL_PLATFORM from the operator still wins: pinning a
|
||||
// platform is exactly how someone reproduces a platform-specific bug.
|
||||
if (std::getenv("EGL_PLATFORM") == nullptr) {
|
||||
setenv("EGL_PLATFORM", "surfaceless", 1);
|
||||
}
|
||||
unsetenv("DISPLAY");
|
||||
unsetenv("WAYLAND_DISPLAY");
|
||||
#endif
|
||||
}
|
||||
|
||||
// THE bring-up, in one function so the pre-flight child and the parent run
|
||||
// literally the same sequence - a pre-flight that tests something narrower
|
||||
// than what the parent will do is exactly the kind of "predictive" check
|
||||
@@ -82,6 +116,9 @@ namespace MGITest {
|
||||
// Returns 0 on success, or the 1-based index of the step that failed, and
|
||||
// fills outReason either way.
|
||||
int RunEglBringUp(EglBringUp& out, std::string& outReason) {
|
||||
// Belt and braces: the pre-flight child and the parent both enter here,
|
||||
// and neither may be the first to touch EGL without this having run.
|
||||
EnsureHeadlessPlatform();
|
||||
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
if (display == EGL_NO_DISPLAY) {
|
||||
outReason = WithEglError("eglGetDisplay(EGL_DEFAULT_DISPLAY) returned EGL_NO_DISPLAY");
|
||||
@@ -199,11 +236,10 @@ namespace MGITest {
|
||||
}
|
||||
if (child == 0) {
|
||||
close(channel[0]);
|
||||
// The child is EXPECTED to die on a signal on an unusable
|
||||
// platform; that is the measurement. Do not let each such
|
||||
// measurement drop a core file next to the test binary.
|
||||
const rlimit noCore{0, 0};
|
||||
setrlimit(RLIMIT_CORE, &noCore);
|
||||
// No core suppression here, deliberately: when the child dies on a
|
||||
// signal, the core IS the diagnosis (an rlimit that used to sit here
|
||||
// made a CI-only crash undebuggable). Machines that do not want
|
||||
// cores control that with the usual ulimit/core_pattern knobs.
|
||||
std::fprintf(stderr, "[itest] pre-flight child: attempting a full EGL bring-up\n");
|
||||
EglBringUp local;
|
||||
std::string reason;
|
||||
@@ -284,10 +320,20 @@ namespace MGITest {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool RequireGpu() {
|
||||
const char* value = std::getenv("MOBILEGL_ITEST_REQUIRE_GPU");
|
||||
namespace {
|
||||
bool EnvFlag(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool RequireGpu() {
|
||||
return EnvFlag("MOBILEGL_ITEST_REQUIRE_GPU");
|
||||
}
|
||||
|
||||
bool RequireHardwareGpu() {
|
||||
return EnvFlag("MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU");
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const Rgba8& c) {
|
||||
os << "rgba(" << int(c.r) << "," << int(c.g) << "," << int(c.b) << "," << int(c.a) << ")";
|
||||
@@ -390,6 +436,10 @@ namespace MGITest {
|
||||
}
|
||||
|
||||
HeadlessGL::HeadlessGL() {
|
||||
// Before anything else in this process can reach EGL, and in particular
|
||||
// before the pre-flight forks - the child must measure the same platform
|
||||
// the parent will use.
|
||||
EnsureHeadlessPlatform();
|
||||
m_backendName = EnvOr("MOBILEGL_BACKEND_TYPE", "<unset>");
|
||||
m_usable = BringUp();
|
||||
}
|
||||
|
||||
@@ -41,6 +41,15 @@ namespace MGITest {
|
||||
// a job that ran everything.
|
||||
bool RequireGpu();
|
||||
|
||||
// True when MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU is set: additionally asserts
|
||||
// that the context did NOT land on a software rasterizer. Deliberately a
|
||||
// SEPARATE switch from RequireGpu - a GPU-less CI runner is a supported and
|
||||
// intended configuration for these scenarios (they pin backend draw logic,
|
||||
// which llvmpipe/lavapipe execute faithfully), so CI wants the falsifiability
|
||||
// of REQUIRE_GPU without the hardware demand. Use this one only where a vendor
|
||||
// pin silently degrading to software would invalidate the measurement.
|
||||
bool RequireHardwareGpu();
|
||||
|
||||
struct Rgba8 {
|
||||
std::uint8_t r = 0, g = 0, b = 0, a = 0;
|
||||
|
||||
|
||||
@@ -45,12 +45,18 @@ namespace MGITest {
|
||||
}
|
||||
GTEST_SKIP() << "no usable GPU/display/ICD for backend " << gl.BackendName() << ": " << gl.SkipReason();
|
||||
}
|
||||
if (RequireGpu() && LooksLikeSoftwareRasterizer(gl.RendererString())) {
|
||||
// "Ran on llvmpipe" must not be able to pass as "ran on the GPU":
|
||||
// a misconfigured vendor pin silently lands on the software
|
||||
// rasterizer, and REQUIRE_GPU exists precisely to make that loud.
|
||||
FAIL() << "MOBILEGL_ITEST_REQUIRE_GPU is set but the context landed on a software rasterizer: "
|
||||
<< gl.RendererString();
|
||||
if (RequireHardwareGpu() && LooksLikeSoftwareRasterizer(gl.RendererString())) {
|
||||
// Only when hardware was asked for BY NAME. REQUIRE_GPU means "an
|
||||
// unusable harness is a failure, not a silent skip" - it is the
|
||||
// falsifiability switch, and CI is exactly where it belongs. But CI
|
||||
// runners have no GPU, so folding "must not be llvmpipe" into the
|
||||
// same switch made the CI lane unpassable by construction: the
|
||||
// scenarios pin backend draw logic, which a software rasterizer
|
||||
// executes just as faithfully. Landing on llvmpipe/lavapipe there is
|
||||
// the intended configuration, not a misconfiguration. A vendor pin
|
||||
// that must not silently degrade sets REQUIRE_HARDWARE_GPU.
|
||||
FAIL() << "MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU is set but the context landed on a software "
|
||||
<< "rasterizer: " << gl.RendererString();
|
||||
}
|
||||
// A scenario starts from a clean slate but shares the context (and so
|
||||
// the renderer's memos) with every other scenario in this process -
|
||||
|
||||
@@ -26,8 +26,14 @@ namespace {
|
||||
const MGITest::HeadlessGL& gl = MGITest::HeadlessGL::Get();
|
||||
std::fprintf(stderr, "MobileGL integration scenarios: backend=%s\n", gl.BackendName().c_str());
|
||||
if (gl.Usable()) {
|
||||
std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless)\n",
|
||||
gl.RendererString().c_str(), gl.Width(), gl.Height());
|
||||
// EGL_PLATFORM is echoed because it is the invariant this harness
|
||||
// rests on: the run is headless on every machine, so a run that
|
||||
// silently bound to a workstation's window system is a different
|
||||
// run from CI's and must be visible as one in the log.
|
||||
const char* eglPlatform = std::getenv("EGL_PLATFORM");
|
||||
std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless, EGL_PLATFORM=%s)\n",
|
||||
gl.RendererString().c_str(), gl.Width(), gl.Height(),
|
||||
eglPlatform != nullptr ? eglPlatform : "<unset>");
|
||||
} else if (MGITest::RequireGpu()) {
|
||||
std::fprintf(stderr,
|
||||
" FAILING every scenario (MOBILEGL_ITEST_REQUIRE_GPU is set): %s\n",
|
||||
|
||||
@@ -2759,3 +2759,323 @@ void main() {
|
||||
}
|
||||
EXPECT_GE(checked, 7u) << "expected all seven declared inputs to be present in the raw module";
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Storage-class census of module-scope OpVariables plus an OpFunctionCall count -
|
||||
// everything the dead-interface-elimination tests need to see, nothing more.
|
||||
struct SpirvVariableCensus {
|
||||
SizeT inputCount = 0;
|
||||
SizeT outputCount = 0;
|
||||
SizeT privateCount = 0;
|
||||
SizeT functionCallCount = 0;
|
||||
};
|
||||
|
||||
SpirvVariableCensus TakeVariableCensus(const Vector<Uint32>& spirv) {
|
||||
constexpr unsigned kOpVariable = 59, kOpFunctionCall = 57;
|
||||
constexpr unsigned kStorageClassInput = 1, kStorageClassPrivate = 6, kStorageClassOutput = 3;
|
||||
SpirvVariableCensus census;
|
||||
for (SizeT i = 5; i < spirv.size();) { // 5-word header
|
||||
const unsigned wordCount = spirv[i] >> 16;
|
||||
const unsigned opcode = spirv[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
||||
if (opcode == kOpVariable && wordCount >= 4) {
|
||||
switch (spirv[i + 3]) {
|
||||
case kStorageClassInput: ++census.inputCount; break;
|
||||
case kStorageClassOutput: ++census.outputCount; break;
|
||||
case kStorageClassPrivate: ++census.privateCount; break;
|
||||
default: break;
|
||||
}
|
||||
} else if (opcode == kOpFunctionCall) {
|
||||
++census.functionCallCount;
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
return census;
|
||||
}
|
||||
|
||||
// The exact Iris shim shape that shipped an invalid module for a month: a declared
|
||||
// vertex input whose only use is the initializer of a file-scope global nothing ever
|
||||
// reads, in a shader whose main() still contains calls (which is what used to make
|
||||
// ADCE keep the whole chain alive).
|
||||
constexpr const char* kDeadPrivateChainVertexSource = R"(#version 460 core
|
||||
in vec3 a_Position;
|
||||
in vec2 mc_midTexCoord;
|
||||
out vec4 v_Color;
|
||||
vec4 iris_MidTex = vec4(mc_midTexCoord * (1.0 / 32768.0), 0.0, 1.0);
|
||||
vec4 helperTint();
|
||||
void main() {
|
||||
v_Color = helperTint();
|
||||
gl_Position = vec4(a_Position, 1.0);
|
||||
}
|
||||
vec4 helperTint() { return vec4(1.0); }
|
||||
)";
|
||||
|
||||
Vector<Uint32> CompileVertexToRawSpirv(const String& source) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
if (!shaderResult) {
|
||||
ADD_FAILURE() << shaderResult.error().log;
|
||||
return {};
|
||||
}
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
if (!programResult) {
|
||||
ADD_FAILURE() << programResult.error().log;
|
||||
return {};
|
||||
}
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_VERTEX_SHADER},
|
||||
.program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
if (!binaryResult || binaryResult->size() != 1u) {
|
||||
ADD_FAILURE() << (binaryResult ? "unexpected module count"
|
||||
: binaryResult.error().log);
|
||||
return {};
|
||||
}
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
struct SpirvValidationScope {
|
||||
bool previous;
|
||||
explicit SpirvValidationScope(bool enabled)
|
||||
: previous(MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SetSpirvValidationEnabled(enabled);
|
||||
}
|
||||
~SpirvValidationScope() {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SetSpirvValidationEnabled(previous);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_F(ProgramUtilTest, DeadPrivateChainVertexInputIsEliminatedFromOptimizedBinary) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> raw = CompileVertexToRawSpirv(kDeadPrivateChainVertexSource);
|
||||
ASSERT_FALSE(raw.empty());
|
||||
|
||||
const SpirvVariableCensus before = TakeVariableCensus(raw);
|
||||
// Preconditions that make this module exercise the ADCE conservatism gate: the dead
|
||||
// input is present, its Private sink is present, and main() still contains a call.
|
||||
// Four Inputs, not two: the frontend always emits gl_VertexIndex/gl_InstanceIndex
|
||||
// built-ins alongside a_Position and mc_midTexCoord.
|
||||
ASSERT_EQ(before.inputCount, 4u)
|
||||
<< "expected a_Position, mc_midTexCoord, gl_VertexIndex and gl_InstanceIndex in the raw module";
|
||||
ASSERT_GE(before.privateCount, 1u);
|
||||
ASSERT_GE(before.functionCallCount, 1u)
|
||||
<< "helperTint() was inlined by the frontend; this test no longer covers the "
|
||||
<< "entry-point-with-calls shape it exists for";
|
||||
|
||||
Vector<Uint32> optimized;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
|
||||
const SpirvVariableCensus after = TakeVariableCensus(optimized);
|
||||
EXPECT_EQ(after.inputCount, 1u)
|
||||
<< "mc_midTexCoord feeds only a never-read Private global and must not reach the driver";
|
||||
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String validatorMessages;
|
||||
tools.SetMessageConsumer([&validatorMessages](spv_message_level_t, const char*,
|
||||
const spv_position_t&, const char* message) {
|
||||
if (message != nullptr) validatorMessages += String(message) + "\n";
|
||||
});
|
||||
EXPECT_TRUE(tools.Validate(optimized)) << validatorMessages;
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, DeclaredButUnwrittenOutputSurvivesOptimization) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// Chocapic-class packs declare varyings some variants never write while the paired
|
||||
// fragment shader still reads them. The OpVariable (and its Location) must survive the
|
||||
// chain on both backends: Espryt's ESSL link would otherwise fail with "varying not
|
||||
// declared in vertex shader", and Magma's stage-interface contract breaks the same way.
|
||||
// ADCE guarantees this only while remove_outputs stays false - this test freezes that.
|
||||
const Vector<Uint32> raw = CompileVertexToRawSpirv(R"(#version 460 core
|
||||
in vec3 a_Position;
|
||||
out vec4 v_Written;
|
||||
out vec4 v_NeverWritten;
|
||||
void main() {
|
||||
v_Written = vec4(1.0);
|
||||
gl_Position = vec4(a_Position, 1.0);
|
||||
}
|
||||
)");
|
||||
ASSERT_FALSE(raw.empty());
|
||||
// v_Written, v_NeverWritten, and the gl_PerVertex block are all Output-storage variables.
|
||||
const SpirvVariableCensus before = TakeVariableCensus(raw);
|
||||
ASSERT_GE(before.outputCount, 3u);
|
||||
|
||||
Vector<Uint32> optimized;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
EXPECT_EQ(TakeVariableCensus(optimized).outputCount, before.outputCount)
|
||||
<< "a declared-but-unwritten output was deleted; a fragment stage reading it now "
|
||||
<< "fails to link (ES) or breaks the Vulkan stage interface";
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, ValidationLatchFlagsInvalidModuleWithoutChangingResults) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// Only LIVE inputs, so the chain cannot heal the module by deleting them: both
|
||||
// survive to the output, undecorated, and the output is invalid SPIR-V.
|
||||
Vector<Uint32> raw = CompileVertexToRawSpirv(R"(#version 460 core
|
||||
in vec3 a_Position;
|
||||
in vec4 a_Color;
|
||||
out vec4 v_Color;
|
||||
void main() {
|
||||
v_Color = a_Color;
|
||||
gl_Position = vec4(a_Position, 1.0);
|
||||
}
|
||||
)");
|
||||
ASSERT_FALSE(raw.empty());
|
||||
|
||||
// Strip every Input Location decoration - the exact defect class the
|
||||
// TMglGlslIoResolver used to ship ([VUID-StandaloneSpirv-Location-04916]).
|
||||
constexpr unsigned kOpDecorate = 71, kOpVariable = 59;
|
||||
constexpr unsigned kDecorationLocation = 30, kStorageClassInput = 1;
|
||||
std::set<unsigned> inputIds;
|
||||
for (SizeT i = 5; i < raw.size();) {
|
||||
const unsigned wordCount = raw[i] >> 16;
|
||||
const unsigned opcode = raw[i] & 0xFFFFu;
|
||||
ASSERT_GT(wordCount, 0u);
|
||||
if (i + wordCount > raw.size()) break;
|
||||
if (opcode == kOpVariable && wordCount >= 4 && raw[i + 3] == kStorageClassInput) {
|
||||
inputIds.insert(raw[i + 2]);
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
SizeT strippedCount = 0;
|
||||
for (SizeT i = 5; i < raw.size();) {
|
||||
const unsigned wordCount = raw[i] >> 16;
|
||||
const unsigned opcode = raw[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > raw.size()) break;
|
||||
if (opcode == kOpDecorate && wordCount >= 4 && raw[i + 2] == kDecorationLocation &&
|
||||
inputIds.count(raw[i + 1]) != 0) {
|
||||
raw.erase(raw.begin() + static_cast<std::ptrdiff_t>(i),
|
||||
raw.begin() + static_cast<std::ptrdiff_t>(i + wordCount));
|
||||
++strippedCount;
|
||||
continue; // do not advance: the next instruction moved into place
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
ASSERT_GE(strippedCount, 2u) << "expected to strip both live inputs' Location decorations";
|
||||
|
||||
Vector<Uint32> optimized;
|
||||
{
|
||||
// The armed lane: control flow is IDENTICAL to shipping (the wrapper still
|
||||
// succeeds - fail-open call sites downstream must not see a different world),
|
||||
// and the failure latch is the signal. This is the catch that took a device
|
||||
// bisect to find when the validator was off everywhere.
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
EXPECT_GT(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "an invalid optimized module must bump the validation-failure latch";
|
||||
}
|
||||
{
|
||||
// The shipping configuration: same result, no validation, latch untouched.
|
||||
SpirvValidationScope validationOff(false);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// OpTypeImage: result id (+1), sampled type (+2), dim (+3). Dim::Rect == 4.
|
||||
SizeT CountRectImageTypes(const Vector<Uint32>& spirv) {
|
||||
constexpr unsigned kOpTypeImage = 25, kDimRect = 4;
|
||||
SizeT count = 0;
|
||||
for (SizeT i = 5; i < spirv.size();) {
|
||||
const unsigned wordCount = spirv[i] >> 16;
|
||||
const unsigned opcode = spirv[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
||||
if (opcode == kOpTypeImage && wordCount >= 4 && spirv[i + 3] == kDimRect) {
|
||||
++count;
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// True when any OpDecorate Location targets a UniformConstant/Uniform-storage
|
||||
// variable ([VUID-StandaloneSpirv-Location-06672]).
|
||||
bool AnyLocationOnUniformStorage(const Vector<Uint32>& spirv) {
|
||||
constexpr unsigned kOpDecorate = 71, kOpVariable = 59, kDecorationLocation = 30;
|
||||
constexpr unsigned kStorageUniformConstant = 0, kStorageUniform = 2;
|
||||
std::set<unsigned> locatedIds;
|
||||
for (SizeT i = 5; i < spirv.size();) {
|
||||
const unsigned wordCount = spirv[i] >> 16;
|
||||
const unsigned opcode = spirv[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
||||
if (opcode == kOpDecorate && wordCount >= 4 && spirv[i + 2] == kDecorationLocation) {
|
||||
locatedIds.insert(spirv[i + 1]);
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
for (SizeT i = 5; i < spirv.size();) {
|
||||
const unsigned wordCount = spirv[i] >> 16;
|
||||
const unsigned opcode = spirv[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
||||
if (opcode == kOpVariable && wordCount >= 4 &&
|
||||
(spirv[i + 3] == kStorageUniformConstant || spirv[i + 3] == kStorageUniform) &&
|
||||
locatedIds.count(spirv[i + 2]) != 0) {
|
||||
return true;
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_F(ProgramUtilTest, RectangleSamplerModuleLeavesTheChainVulkanLegal) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// Dim::Rect is invalid under every Vulkan environment; the lowering used to run
|
||||
// only in the backends, i.e. AFTER the chain whose output the validating lanes
|
||||
// check. It now runs inside the chain, so the driver-bound bytes are rect-free.
|
||||
const Vector<Uint32> raw = CompileVertexToRawSpirv(R"(#version 460 core
|
||||
in vec3 a_Position;
|
||||
uniform sampler2DRect uRect;
|
||||
out vec4 v_Color;
|
||||
void main() {
|
||||
v_Color = texture(uRect, a_Position.xy);
|
||||
gl_Position = vec4(a_Position, 1.0);
|
||||
}
|
||||
)");
|
||||
ASSERT_FALSE(raw.empty());
|
||||
ASSERT_GE(CountRectImageTypes(raw), 1u) << "glslang no longer emits Dim::Rect for sampler2DRect";
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
Vector<Uint32> optimized;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
EXPECT_EQ(CountRectImageTypes(optimized), 0u);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "a rectangle module must leave the chain valid, not latched as a failure";
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, ExplicitSamplerLocationIsStrippedFromTheOptimizedBinary) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// glslang's relaxed GL path keeps layout(location=N) on the UniformConstant
|
||||
// variable, which Vulkan forbids; nothing downstream reads it (GL locations come
|
||||
// from phase-A reflection, Vulkan bindings go by name).
|
||||
const Vector<Uint32> raw = CompileVertexToRawSpirv(R"(#version 460 core
|
||||
in vec3 a_Position;
|
||||
layout(location = 5) uniform sampler2D uTex;
|
||||
out vec4 v_Color;
|
||||
void main() {
|
||||
v_Color = texture(uTex, a_Position.xy);
|
||||
gl_Position = vec4(a_Position, 1.0);
|
||||
}
|
||||
)");
|
||||
ASSERT_FALSE(raw.empty());
|
||||
ASSERT_TRUE(AnyLocationOnUniformStorage(raw))
|
||||
<< "glslang no longer keeps the explicit uniform location; the strip pass may be obsolete";
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
Vector<Uint32> optimized;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
EXPECT_FALSE(AnyLocationOnUniformStorage(optimized));
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the stripped module must validate clean";
|
||||
}
|
||||
|
||||
@@ -54,9 +54,7 @@ namespace MobileGL::MG_Util::Async {
|
||||
// calling eglTerminate, which is the norm for a test binary and legal
|
||||
// for an application. Registered here, during main, so it runs before
|
||||
// the destructors of statics constructed at load time.
|
||||
if (ShaderCompilePool* pool = g_processPool.load(std::memory_order_acquire)) {
|
||||
pool->StopAndDrain();
|
||||
}
|
||||
ShaderCompilePool::StopAndDrainProcessPoolAtExit();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -345,4 +343,10 @@ namespace MobileGL::MG_Util::Async {
|
||||
// eglInitialize to get its worker threads back, the re-arm belongs in
|
||||
// MobileGL::Initialize(), next to glslang::InitializeProcess().
|
||||
}
|
||||
|
||||
void ShaderCompilePool::StopAndDrainProcessPoolAtExit() {
|
||||
if (ShaderCompilePool* pool = g_processPool.load(std::memory_order_acquire)) {
|
||||
pool->StopAndDrain();
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Util::Async
|
||||
|
||||
@@ -109,6 +109,13 @@ namespace MobileGL::MG_Util::Async {
|
||||
// but they share glslang's process globals, which teardown is about to free.
|
||||
void StopAndDrain();
|
||||
|
||||
// StopAndDrain() on the process-wide pool if one was ever created; never creates
|
||||
// one. For extra atexit sentinels owned by other subsystems (the SPIR-V validation
|
||||
// switch registers one after forcing spirv-tools' lazy function-local tables into
|
||||
// existence, so the drain is sequenced before those tables' destructors - a worker
|
||||
// mid-Validate would otherwise touch freed memory during process exit).
|
||||
static void StopAndDrainProcessPoolAtExit();
|
||||
|
||||
Uint GetThreadCount() const;
|
||||
Uint GetMaxConcurrency() const;
|
||||
|
||||
|
||||
@@ -568,13 +568,26 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
#if defined(MOBILEGL_IOS)
|
||||
eglLib = OpenLib({"libtinygl4angle.dylib"});
|
||||
#else
|
||||
eglLib = OpenLib({"libEGL.so"});
|
||||
// Versioned SONAME first. The unversioned "libEGL.so" is a development
|
||||
// symlink: it ships in libegl-dev/mesa-libEGL-devel, NOT in the runtime
|
||||
// package, so a machine that can run GL perfectly well may not have it -
|
||||
// every stock Ubuntu/Debian runtime image, the GitHub Actions runners
|
||||
// included. Asking only for the unversioned name there makes dlopen fail,
|
||||
// which used to leave the whole EGL function table null and take the next
|
||||
// call through a null pointer (SIGSEGV inside InitDisplayAndContext).
|
||||
// Developer machines have both names, which is exactly why this only ever
|
||||
// showed up in CI.
|
||||
eglLib = OpenLib({"libEGL.so.1", "libEGL.so"});
|
||||
#endif
|
||||
}
|
||||
#endif // !_WIN32
|
||||
|
||||
if (!eglLib) {
|
||||
MGLOG_E("Failed to open EGL library");
|
||||
// MGLOG_F, not MGLOG_E: at the INFO log level every shipping and CI build
|
||||
// uses, MGLOG_E is compiled out (Log.h orders DEBUG < WARN < ERROR < INFO),
|
||||
// so this diagnosis was invisible in precisely the builds that needed it.
|
||||
MGLOG_F("Failed to open EGL library: none of libEGL.so.1 / libEGL.so could be "
|
||||
"dlopened; every EGL entry point will be null");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -595,7 +608,10 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
do { \
|
||||
funcs.name = (MG_External::EGL::name##_PTR)resolveEGLProc(#name); \
|
||||
if (!funcs.name) { \
|
||||
MGLOG_E("Failed to load EGL function: %s", #name); \
|
||||
/* MGLOG_F for the same reason as the open failure above: a null entry */ \
|
||||
/* point is a crash waiting for its first caller, and MGLOG_E is compiled */ \
|
||||
/* out at the INFO level every shipping and CI build uses. */ \
|
||||
MGLOG_F("Failed to load EGL function: %s", #name); \
|
||||
} \
|
||||
} while (0);
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||
@@ -30,9 +32,13 @@
|
||||
|
||||
#include "ShaderSourceProcessor.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
@@ -354,16 +360,215 @@ namespace MobileGL {
|
||||
return allSpirv;
|
||||
}
|
||||
|
||||
// -1 unresolved, 0 off, 1 on. Resolved once from MOBILEGL_VALIDATE_SPIRV on first
|
||||
// use. A live getenv rather than an MG_Config::Features field, for the same reason
|
||||
// Config.h already exempts MOBILEGL_LOG_FILE_PATH: suites like SpirvPassTest never
|
||||
// run MobileGL::Initialize(), and every Initialize() re-runs MG_ConfigLoader::Init,
|
||||
// which would clobber a programmatic override stored in the feature table.
|
||||
static std::atomic<int> g_validateSpirv{-1};
|
||||
// Total validation failures observed this process. This latch - not the wrappers'
|
||||
// return values - is the test-lane signal: validation must never change what a
|
||||
// wrapper returns, or the validating lanes would render differently from the
|
||||
// shipping configuration (fail-open call sites would silently substitute an
|
||||
// earlier-stage module).
|
||||
static std::atomic<Uint64> g_spirvValidationFailures{0};
|
||||
|
||||
namespace {
|
||||
// Test lanes (desktop/CI/WSL) validate by default; device builds do not -
|
||||
// validation costs real time per module, and on device the driver is the
|
||||
// final validator anyway. MOBILEGL_VALIDATE_SPIRV overrides in either
|
||||
// direction, using the ConfigLoader truthy rule.
|
||||
constexpr bool kValidateSpirvDefault =
|
||||
#if defined(__ANDROID__)
|
||||
false;
|
||||
#else
|
||||
true;
|
||||
#endif
|
||||
|
||||
bool IsTruthySpirvEnvValue(const char* value) {
|
||||
if (value == nullptr || value[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
String lowered(value);
|
||||
for (auto& c : lowered) {
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
return lowered != "0" && lowered != "false";
|
||||
}
|
||||
|
||||
// spirv-tools' validator lazily constructs function-local static tables on
|
||||
// its first run, which on this codebase happens on a ShaderCompilePool
|
||||
// worker. Function-local statics are destroyed in reverse construction
|
||||
// order, so those tables would die BEFORE the pool's own atexit sentinel
|
||||
// (registered at first pool use) gets to drain the workers - and a worker
|
||||
// mid-Validate would then read freed memory during process exit. Pin the
|
||||
// order instead: force the tables into existence now, then register a
|
||||
// second drain handler; being registered after the tables' destructors, it
|
||||
// runs before them.
|
||||
void PinValidatorTablesForProcessExit() {
|
||||
static std::once_flag pinnedOnce;
|
||||
std::call_once(pinnedOnce, [] {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
Vector<Uint32> warmup;
|
||||
// The module is shaped to reach BOTH lazily-constructed tables in
|
||||
// the vendored validate_id.cpp: a type-generating operand pins
|
||||
// InstructionCanHaveTypeOperand's allow-set, and the OpExtInst use
|
||||
// of the TYPELESS %glsl import is the one path into
|
||||
// InstructionRequiresTypeOperand's deny-set (its call site is
|
||||
// guarded on a referenced def with no result type). A straight-line
|
||||
// module without it leaves the deny-set to be built later on a pool
|
||||
// worker, re-creating the exit-order hazard for that one table.
|
||||
if (tools.Assemble("OpCapability Shader\n"
|
||||
"%glsl = OpExtInstImport \"GLSL.std.450\"\n"
|
||||
"OpMemoryModel Logical GLSL450\n"
|
||||
"OpEntryPoint GLCompute %main \"main\"\n"
|
||||
"OpExecutionMode %main LocalSize 1 1 1\n"
|
||||
"%void = OpTypeVoid\n"
|
||||
"%fn = OpTypeFunction %void\n"
|
||||
"%float = OpTypeFloat 32\n"
|
||||
"%c = OpConstant %float 1\n"
|
||||
"%main = OpFunction %void None %fn\n"
|
||||
"%entry = OpLabel\n"
|
||||
"%abs = OpExtInst %float %glsl FAbs %c\n"
|
||||
"OpReturn\n"
|
||||
"OpFunctionEnd\n",
|
||||
&warmup)) {
|
||||
tools.Validate(warmup);
|
||||
}
|
||||
std::atexit(+[] {
|
||||
// Flip validation off first: a validator table this warmup does
|
||||
// not know about (a future spirv-tools bump) would still be
|
||||
// destroyed before this handler, and workers must stop entering
|
||||
// Validate before the drain waits for them.
|
||||
g_validateSpirv.store(0, std::memory_order_release);
|
||||
Async::ShaderCompilePool::StopAndDrainProcessPoolAtExit();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
spvtools::MessageConsumer MakeSpirvMessageConsumer(const char* site) {
|
||||
return [site](spv_message_level_t level, const char* /*source*/,
|
||||
const spv_position_t& position, const char* message) {
|
||||
const char* text = message ? message : "";
|
||||
switch (level) {
|
||||
case SPV_MSG_FATAL:
|
||||
case SPV_MSG_INTERNAL_ERROR:
|
||||
case SPV_MSG_ERROR:
|
||||
// MGLOG_I, deliberately: at the INFO compile level of every
|
||||
// CI/WSL/retrace build, MGLOG_E and MGLOG_W are compiled out
|
||||
// (Log.h orders DEBUG < WARN < ERROR < INFO) and the VUID
|
||||
// would never reach a log.
|
||||
MGLOG_I("[spirv] %s: %s (word index %zu)", site, text, position.index);
|
||||
break;
|
||||
default:
|
||||
MGLOG_D("[spirv] %s: %s", site, text);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Validation is decoupled from control flow on purpose: a failure logs and
|
||||
// bumps the latch, and the caller proceeds exactly as the shipping (non-
|
||||
// validating) configuration would. Tests assert on the latch delta.
|
||||
void ValidateOrLatch(const char* site, const Vector<Uint32>& binary) {
|
||||
if (!ShaderCompiler::SpirvValidationEnabled()) {
|
||||
return;
|
||||
}
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
tools.SetMessageConsumer(MakeSpirvMessageConsumer(site));
|
||||
if (!tools.Validate(binary)) {
|
||||
MGLOG_I("[spirv] %s: produced a module that fails validation (failure #%llu)",
|
||||
site,
|
||||
static_cast<unsigned long long>(
|
||||
ShaderCompiler::NoteSpirvValidationFailure()));
|
||||
}
|
||||
}
|
||||
|
||||
// Shared tail for every Optimizer wrapper in this file. The optimizer's own
|
||||
// input validator stays off even in validating lanes, for two reasons: its
|
||||
// failure is indistinguishable from a transform failure (Optimizer::Run
|
||||
// returns false before BuildModule), and the FIRST wrapper's input is
|
||||
// glslang output that is legitimately not Vulkan-clean yet. What gets
|
||||
// validated is each wrapper's OUTPUT - the only bytes a driver can ever
|
||||
// receive. The message consumer is installed unconditionally: without one,
|
||||
// spirv-tools drops pass diagnostics on the floor.
|
||||
bool RunOptimizerChecked(const char* site, spvtools::Optimizer& optimizer,
|
||||
const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
optimizer.SetMessageConsumer(MakeSpirvMessageConsumer(site));
|
||||
if (!optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options)) {
|
||||
return false;
|
||||
}
|
||||
ValidateOrLatch(site, outputBinary);
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool ShaderCompiler::SpirvValidationEnabled() {
|
||||
int state = g_validateSpirv.load(std::memory_order_acquire);
|
||||
if (state < 0) {
|
||||
const char* env = std::getenv("MOBILEGL_VALIDATE_SPIRV");
|
||||
const bool resolved = env != nullptr ? IsTruthySpirvEnvValue(env) : kValidateSpirvDefault;
|
||||
int expected = -1;
|
||||
g_validateSpirv.compare_exchange_strong(expected, resolved ? 1 : 0,
|
||||
std::memory_order_acq_rel);
|
||||
state = g_validateSpirv.load(std::memory_order_acquire);
|
||||
if (state == 1) {
|
||||
PinValidatorTablesForProcessExit();
|
||||
}
|
||||
}
|
||||
return state == 1;
|
||||
}
|
||||
|
||||
void ShaderCompiler::SetSpirvValidationEnabled(bool enabled) {
|
||||
g_validateSpirv.store(enabled ? 1 : 0, std::memory_order_release);
|
||||
if (enabled) {
|
||||
PinValidatorTablesForProcessExit();
|
||||
}
|
||||
}
|
||||
|
||||
Uint64 ShaderCompiler::NoteSpirvValidationFailure() {
|
||||
return g_spirvValidationFailures.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
}
|
||||
|
||||
Uint64 ShaderCompiler::SpirvValidationFailureCount() {
|
||||
return g_spirvValidationFailures.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
|
||||
// ADCE refuses to treat a Private global as deletable while the entry point
|
||||
// still contains any OpFunctionCall (IsLocalVar -> IsEntryPointWithNoCalls), so
|
||||
// a dead vertex input feeding a never-read Private shim used to survive the
|
||||
// whole chain (the Chocapic13 shadow.vsh mc_midTexCoord/iris_MidTex case).
|
||||
// Rewriting entry-point-owned Private variables to Function storage first
|
||||
// satisfies ADCE without inlining: over 521 real Iris modules the rewrite
|
||||
// captured 17 of the 21 extra dead interface variables exhaustive inlining
|
||||
// would, while shrinking the corpus 8% - inlining grew it 20% with a 5.3x
|
||||
// worst-case module and no additional GPU-side benefit.
|
||||
optimizer.RegisterPass(PrivateToEntryLocalPass::CreatePrivateToEntryLocalPass());
|
||||
// Keep the one-arg overload: remove_outputs must stay false, forever. Output
|
||||
// variables on the entry-point interface are ADCE's only unconditional live
|
||||
// roots; XFB capture resolves varyings by OpName after this chain, and the
|
||||
// VS-out/FS-in interface contract on both backends depends on declared outputs
|
||||
// surviving even when never stored.
|
||||
optimizer.RegisterPass(CreateAggressiveDCEPass(false));
|
||||
// Complementary to ADCE, not redundant: ADCE can never delete or delist an
|
||||
// Output (see above), so never-written outputs are trimmed from the
|
||||
// OpEntryPoint operand list here.
|
||||
optimizer.RegisterPass(CreateRemoveUnusedInterfaceVariablesPass());
|
||||
// The two module-legality repairs, so the chain's output - the bytes every
|
||||
// consumer downstream sees - is valid Vulkan SPIR-V. Rect lowering used to
|
||||
// live only in the backends; a validating lane would flag every rectangle
|
||||
// module long before the backend got the chance to fix it, and the backend
|
||||
// calls remain as no-ops on the now rect-free modules.
|
||||
optimizer.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass());
|
||||
optimizer.RegisterPass(StripUniformLocationsPass::CreateStripUniformLocationsPass());
|
||||
optimizer.RegisterPass(FlattenInterfaceStructPass::CreateFlattenInterfaceStructPass());
|
||||
optimizer.RegisterPass(RenameSamplerFunctionParameterPass::CreateRenameSamplerFunctionParameterPass());
|
||||
optimizer.RegisterPass(
|
||||
@@ -371,104 +576,88 @@ namespace MobileGL {
|
||||
optimizer.RegisterPass(EliminateFloatEqualsZeroPass::CreateEliminateFloatEqualsZeroPass());
|
||||
optimizer.RegisterPass(DecomposeWorkgroupVec3Pass::CreateDecomposeWorkgroupVec3Pass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("SanitizeAndOptimizeBinary", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(LowerDrawParametersPass::CreateLowerDrawParametersPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("LowerDrawParametersForEssl", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(PackDoubleVertexInputsPass::CreatePackDoubleVertexInputsPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("PackDoubleVertexInputsForVulkan", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(
|
||||
StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("StripUboMemberRelaxedPrecisionForEssl", optimizer,
|
||||
inputBinary, outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::StripNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(StripNoPerspectivePass::CreateStripNoPerspectivePass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("StripNoPerspectiveForEssl", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(EmulateNoPerspectivePass::CreateEmulateNoPerspectivePass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("EmulateNoPerspectiveForEssl", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LowerRectImages(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("LowerRectImages", optimizer, inputBinary, outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(RebaseInstanceIndexPass::CreateRebaseInstanceIndexPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("RebaseInstanceIndexForVulkan", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(DecoratePositionInvariantPass::CreateDecoratePositionInvariantPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("DecoratePositionInvariantForVulkan", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
|
||||
@@ -598,6 +787,9 @@ namespace MobileGL {
|
||||
}
|
||||
outputBinary.insert(outputBinary.begin() + static_cast<std::ptrdiff_t>(capabilityInsertOffset),
|
||||
addedCapabilities.begin(), addedCapabilities.end());
|
||||
// Hand-rolled word walk, so no Optimizer wrapper ever sees this rewrite;
|
||||
// check the modified module explicitly in validating lanes.
|
||||
ValidateOrLatch("UseUnformattedFloatStorageImagesForVulkan", outputBinary);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,27 @@ namespace MobileGL {
|
||||
// it, the second eglInitialize of a process comes back up unwarmed and with
|
||||
// no way left to warm it.
|
||||
static void ResetPrewarmLatch();
|
||||
|
||||
// Test-environment SPIR-V validation. When enabled, every Optimizer wrapper
|
||||
// in this file validates its OUTPUT binary - the bytes a driver can actually
|
||||
// receive - and a failure logs the VUID (via MGLOG_I; see the consumer for
|
||||
// why not MGLOG_E) and bumps the failure latch below WITHOUT changing the
|
||||
// wrapper's return value: control flow must stay identical between the
|
||||
// validating and shipping configurations, or fail-open call sites would make
|
||||
// the two render differently. Resolved lazily from MOBILEGL_VALIDATE_SPIRV;
|
||||
// defaults on for desktop/CI/WSL builds and off for device (__ANDROID__)
|
||||
// builds. The setter wins over the environment and is safe to call from test
|
||||
// fixtures at any time.
|
||||
static bool SpirvValidationEnabled();
|
||||
static void SetSpirvValidationEnabled(bool enabled);
|
||||
|
||||
// The test-lane enforcement signal: total validation failures observed this
|
||||
// process. Tests snapshot it, run the operation under scrutiny, and assert
|
||||
// on the delta. NoteSpirvValidationFailure is for validation done outside
|
||||
// this file (ProgramFactory::ValidateTransformedSpirv); it returns the new
|
||||
// total.
|
||||
static Uint64 SpirvValidationFailureCount();
|
||||
static Uint64 NoteSpirvValidationFailure();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.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
|
||||
//
|
||||
// Derived from SPIRV-Tools' PrivateToLocalPass (source/opt/private_to_local_pass.cpp,
|
||||
// Copyright (c) 2017 Google Inc., Apache License 2.0). The one behavioral difference is
|
||||
// the entry-point restriction in FindEntryLocalFunction; see the header for why.
|
||||
|
||||
#include "PrivateToEntryLocalPass.h"
|
||||
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/spirv_constant.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::BasicBlock;
|
||||
using spvtools::opt::Function;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
constexpr uint32_t kVariableStorageClassInIdx = 0;
|
||||
constexpr uint32_t kSpvTypePointerTypeIdInIdx = 1;
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status PrivateToEntryLocalPass::Process() {
|
||||
// Private variables require the Shader capability; with Addresses the
|
||||
// rewrite below is not guaranteed sound (variable pointers may escape).
|
||||
if (context()->get_feature_mgr()->HasCapability(spv::Capability::Addresses)) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
std::vector<std::pair<Instruction*, Function*>> variablesToMove;
|
||||
std::unordered_set<uint32_t> localizedVariables;
|
||||
for (auto& inst : context()->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable) {
|
||||
continue;
|
||||
}
|
||||
if (spv::StorageClass(inst.GetSingleWordInOperand(kVariableStorageClassInIdx)) !=
|
||||
spv::StorageClass::Private) {
|
||||
continue;
|
||||
}
|
||||
Function* targetFunction = FindEntryLocalFunction(inst);
|
||||
if (targetFunction != nullptr) {
|
||||
variablesToMove.push_back({&inst, targetFunction});
|
||||
}
|
||||
}
|
||||
|
||||
const bool modified = !variablesToMove.empty();
|
||||
for (auto& p : variablesToMove) {
|
||||
if (!MoveVariable(p.first, p.second)) {
|
||||
return Status::Failure;
|
||||
}
|
||||
localizedVariables.insert(p.first->result_id());
|
||||
}
|
||||
|
||||
if (get_module()->version() >= SPV_SPIRV_VERSION_WORD(1, 4)) {
|
||||
// SPIR-V 1.4+ lists statically-used Private variables on OpEntryPoint;
|
||||
// drop the ones that just stopped being Private. Dead code for the 1.3
|
||||
// modules MobileGL emits, kept for robustness.
|
||||
for (auto& entry : get_module()->entry_points()) {
|
||||
std::vector<Operand> newOperands;
|
||||
for (uint32_t i = 0; i < entry.NumInOperands(); ++i) {
|
||||
// Execution model, function id and name are always kept.
|
||||
if (i < 3 || !localizedVariables.count(entry.GetSingleWordInOperand(i))) {
|
||||
newOperands.push_back(entry.GetInOperand(i));
|
||||
}
|
||||
}
|
||||
if (newOperands.size() != entry.NumInOperands()) {
|
||||
entry.SetInOperands(std::move(newOperands));
|
||||
context()->AnalyzeUses(&entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modified ? Status::SuccessWithChange : Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
Function* PrivateToEntryLocalPass::FindEntryLocalFunction(const Instruction& inst) const {
|
||||
bool foundFirstUse = false;
|
||||
Function* targetFunction = nullptr;
|
||||
const uint32_t variableId = inst.result_id();
|
||||
context()->get_def_use_mgr()->ForEachUser(
|
||||
variableId, [&targetFunction, &foundFirstUse, variableId, this](Instruction* use) {
|
||||
BasicBlock* currentBlock = context()->get_instr_block(use);
|
||||
if (currentBlock == nullptr) {
|
||||
// Module-scope users: OpName, decorations, the OpEntryPoint
|
||||
// interface list. None of them pins the variable to a function,
|
||||
// but a debug-info extended instruction would go stale after the
|
||||
// move, so treat it as disqualifying.
|
||||
if (use->opcode() == spv::Op::OpExtInst) {
|
||||
foundFirstUse = true;
|
||||
targetFunction = nullptr;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!IsValidUse(use, variableId)) {
|
||||
foundFirstUse = true;
|
||||
targetFunction = nullptr;
|
||||
return;
|
||||
}
|
||||
Function* currentFunction = currentBlock->GetParent();
|
||||
if (!foundFirstUse) {
|
||||
foundFirstUse = true;
|
||||
targetFunction = currentFunction;
|
||||
} else if (targetFunction != currentFunction) {
|
||||
targetFunction = nullptr;
|
||||
}
|
||||
});
|
||||
if (targetFunction != nullptr && !IsEntryPointFunction(targetFunction)) {
|
||||
// The whole point of this derivative: a helper can be called more than
|
||||
// once per invocation, and Function storage would reset the variable at
|
||||
// every call.
|
||||
return nullptr;
|
||||
}
|
||||
return targetFunction;
|
||||
}
|
||||
|
||||
bool PrivateToEntryLocalPass::IsEntryPointFunction(Function* function) const {
|
||||
for (auto& entry : get_module()->entry_points()) {
|
||||
if (entry.GetSingleWordInOperand(1) == function->result_id()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PrivateToEntryLocalPass::IsValidUse(const Instruction* inst, uint32_t variableId) const {
|
||||
// The cases here have to match the cases in UpdateUse: a use the rewrite
|
||||
// does not know how to update disqualifies the variable.
|
||||
switch (inst->opcode()) {
|
||||
case spv::Op::OpLoad:
|
||||
case spv::Op::OpImageTexelPointer: // treat like a load
|
||||
return true;
|
||||
case spv::Op::OpStore:
|
||||
// Storing the variable's ADDRESS somewhere else escapes it.
|
||||
return inst->GetOperand(1).AsId() != variableId;
|
||||
case spv::Op::OpAccessChain:
|
||||
return context()->get_def_use_mgr()->WhileEachUser(
|
||||
inst, [this, inst](const Instruction* user) {
|
||||
return IsValidUse(user, inst->result_id());
|
||||
});
|
||||
case spv::Op::OpName:
|
||||
return true;
|
||||
default:
|
||||
return spvOpcodeIsDecoration(inst->opcode());
|
||||
}
|
||||
}
|
||||
|
||||
bool PrivateToEntryLocalPass::MoveVariable(Instruction* variable, Function* function) {
|
||||
// Remove from the global section and re-insert at the head of the entry
|
||||
// function's first block, Function-storage variables' one legal position.
|
||||
variable->RemoveFromList();
|
||||
std::unique_ptr<Instruction> var(variable); // take ownership
|
||||
context()->ForgetUses(variable);
|
||||
|
||||
variable->SetInOperand(kVariableStorageClassInIdx,
|
||||
{uint32_t(spv::StorageClass::Function)});
|
||||
|
||||
const uint32_t newTypeId = GetNewType(variable->type_id());
|
||||
if (newTypeId == 0) {
|
||||
return false;
|
||||
}
|
||||
variable->SetResultType(newTypeId);
|
||||
|
||||
context()->AnalyzeUses(variable);
|
||||
context()->set_instr_block(variable, &*function->begin());
|
||||
function->begin()->begin()->InsertBefore(std::move(var));
|
||||
|
||||
return UpdateUses(variable);
|
||||
}
|
||||
|
||||
uint32_t PrivateToEntryLocalPass::GetNewType(uint32_t oldTypeId) {
|
||||
auto* typeMgr = context()->get_type_mgr();
|
||||
Instruction* oldTypeInst = get_def_use_mgr()->GetDef(oldTypeId);
|
||||
const uint32_t pointeeTypeId =
|
||||
oldTypeInst->GetSingleWordInOperand(kSpvTypePointerTypeIdInIdx);
|
||||
const uint32_t newTypeId =
|
||||
typeMgr->FindPointerToType(pointeeTypeId, spv::StorageClass::Function);
|
||||
if (newTypeId != 0) {
|
||||
context()->UpdateDefUse(context()->get_def_use_mgr()->GetDef(newTypeId));
|
||||
}
|
||||
return newTypeId;
|
||||
}
|
||||
|
||||
bool PrivateToEntryLocalPass::UpdateUse(Instruction* inst, Instruction* user) {
|
||||
// The cases here have to match the cases in IsValidUse.
|
||||
switch (inst->opcode()) {
|
||||
case spv::Op::OpLoad:
|
||||
case spv::Op::OpStore:
|
||||
case spv::Op::OpImageTexelPointer: // treat like a load
|
||||
// Fine as-is: their type is the pointed-to type, which is unchanged.
|
||||
break;
|
||||
case spv::Op::OpAccessChain: {
|
||||
context()->ForgetUses(inst);
|
||||
const uint32_t newTypeId = GetNewType(inst->type_id());
|
||||
if (newTypeId == 0) {
|
||||
return false;
|
||||
}
|
||||
inst->SetResultType(newTypeId);
|
||||
context()->AnalyzeUses(inst);
|
||||
if (!UpdateUses(inst)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case spv::Op::OpName:
|
||||
case spv::Op::OpEntryPoint: // handled separately in Process()
|
||||
break;
|
||||
default:
|
||||
assert(spvOpcodeIsDecoration(inst->opcode()) &&
|
||||
"PrivateToEntryLocalPass: unexpected use opcode");
|
||||
break;
|
||||
}
|
||||
(void)user;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrivateToEntryLocalPass::UpdateUses(Instruction* inst) {
|
||||
const uint32_t id = inst->result_id();
|
||||
std::vector<Instruction*> uses;
|
||||
context()->get_def_use_mgr()->ForEachUser(id,
|
||||
[&uses](Instruction* use) { uses.push_back(use); });
|
||||
for (Instruction* use : uses) {
|
||||
if (!UpdateUse(use, inst)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken PrivateToEntryLocalPass::CreatePrivateToEntryLocalPass() {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
spvtools::MakeUnique<PrivateToEntryLocalPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,55 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.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 "spirv-tools/optimizer.hpp"
|
||||
#include "source/opt/pass.h"
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// AggressiveDCE treats every store to a Private global as an observable side
|
||||
// effect while the entry point still contains any OpFunctionCall, so a dead
|
||||
// vertex-input -> Private-shim chain (Iris rewrites unused legacy attributes
|
||||
// into exactly this shape) survives the whole optimizer chain. Rewriting such
|
||||
// a variable to Function storage unlocks ADCE without inlining anything.
|
||||
//
|
||||
// Upstream's PrivateToLocalPass does that rewrite for a Private variable used
|
||||
// in ANY single function - which is unsound here: a Function-storage variable
|
||||
// is recreated on every call, so a Private global that carries state across
|
||||
// repeated calls of one helper (a memoized init flag, LCG rand state) would
|
||||
// silently lose it. This derivative applies the same rewrite restricted to
|
||||
// variables whose only using function is an entry point: an entry point runs
|
||||
// once per invocation, so the two lifetimes are indistinguishable there.
|
||||
//
|
||||
// Derived from SPIRV-Tools' PrivateToLocalPass
|
||||
// (source/opt/private_to_local_pass.cpp, Copyright (c) 2017 Google Inc.,
|
||||
// Apache License 2.0).
|
||||
class PrivateToEntryLocalPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "mobilegl-private-to-entry-local"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreatePrivateToEntryLocalPass();
|
||||
|
||||
private:
|
||||
// The single entry-point function every block-level use of the variable
|
||||
// lives in, or nullptr when the uses span functions, include an opcode the
|
||||
// rewrite cannot update, or belong to a non-entry function.
|
||||
spvtools::opt::Function* FindEntryLocalFunction(const spvtools::opt::Instruction& inst) const;
|
||||
bool IsEntryPointFunction(spvtools::opt::Function* function) const;
|
||||
bool IsValidUse(const spvtools::opt::Instruction* inst, uint32_t variableId) const;
|
||||
bool MoveVariable(spvtools::opt::Instruction* variable, spvtools::opt::Function* function);
|
||||
uint32_t GetNewType(uint32_t oldTypeId);
|
||||
bool UpdateUse(spvtools::opt::Instruction* inst, spvtools::opt::Instruction* user);
|
||||
bool UpdateUses(spvtools::opt::Instruction* inst);
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,59 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.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 "StripUniformLocationsPass.h"
|
||||
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
spvtools::opt::Pass::Status StripUniformLocationsPass::Process() {
|
||||
using spvtools::opt::Instruction;
|
||||
|
||||
std::vector<Instruction*> toKill;
|
||||
for (auto& annotation : get_module()->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate) {
|
||||
continue;
|
||||
}
|
||||
if (annotation.GetSingleWordInOperand(1) !=
|
||||
static_cast<uint32_t>(spv::Decoration::Location)) {
|
||||
continue;
|
||||
}
|
||||
Instruction* target =
|
||||
get_def_use_mgr()->GetDef(annotation.GetSingleWordInOperand(0));
|
||||
if (target == nullptr || target->opcode() != spv::Op::OpVariable) {
|
||||
continue;
|
||||
}
|
||||
switch (spv::StorageClass(target->GetSingleWordInOperand(0))) {
|
||||
case spv::StorageClass::UniformConstant:
|
||||
case spv::StorageClass::Uniform:
|
||||
case spv::StorageClass::StorageBuffer:
|
||||
toKill.push_back(&annotation);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (Instruction* inst : toKill) {
|
||||
context()->KillInst(inst);
|
||||
}
|
||||
return toKill.empty() ? Status::SuccessWithoutChange : Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken StripUniformLocationsPass::CreateStripUniformLocationsPass() {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
spvtools::MakeUnique<StripUniformLocationsPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,32 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.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 "spirv-tools/optimizer.hpp"
|
||||
#include "source/opt/pass.h"
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// glslang's relaxed GL path keeps `layout(location = N) uniform ...` as a
|
||||
// Location decoration on the UniformConstant/Uniform variable, which Vulkan
|
||||
// forbids ([VUID-StandaloneSpirv-Location-06672]). Nothing downstream reads
|
||||
// it: GL-side uniform locations come from the phase-A glslang reflection,
|
||||
// Vulkan binding assignment goes by (kind, name), and SPIRV-Cross's ESSL
|
||||
// resolves uniforms by name. Strip it so the driver-bound module is valid.
|
||||
class StripUniformLocationsPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "mobilegl-strip-uniform-locations"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateStripUniformLocationsPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -131,6 +131,11 @@ bool LoadMobileGL(const Request& request, std::string& error) {
|
||||
setenv("MOBILEGL_BACKEND_TYPE", request.backend.c_str(), 1);
|
||||
setenv("MOBILEGL_TRACE_LIBRARY", request.mobileGlLibrary.c_str(), 1);
|
||||
setenv("MOBILEGL_TRACE_SKIP_AUTODESTROY", "1", 1);
|
||||
// Retrace is a test lane on every platform, including the Android AVD one where
|
||||
// MobileGL's __ANDROID__ default would leave validation off. No overwrite: an outer
|
||||
// MOBILEGL_VALIDATE_SPIRV=0 must keep working as the escape hatch, and retracing the
|
||||
// exact shipping pipeline must stay possible.
|
||||
setenv("MOBILEGL_VALIDATE_SPIRV", "1", 0);
|
||||
setenv("MOBILEGL_TRACE_SURFACE", request.usePbuffer ? "pbuffer" : "window", 1);
|
||||
if (request.backend == "DirectVulkan") {
|
||||
setenv("MOBILEGL_MAGMA_R11G11B10F_FALLBACK", "1", 1);
|
||||
|
||||
Reference in New Issue
Block a user