mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
[Fix] (MG_State/MG_Impl/MG_Backend): render Flywheel instanced+indirect on both backends
Create 6 / Flywheel 1.0.6 now renders correctly with both flywheel:instancing and flywheel:indirect on DirectGLES and DirectVulkan (verified in-game on Adreno 830: waterwheels and cogwheels solid, animated, correct pairing, no crashes across all four combinations). - MG_State/MG_Impl: sync explicitly-ranged SSBO bindings of FLUSH_EXPLICIT persistent maps to the backend before compute dispatches. Flywheel writes its scatter-copy descriptors into the staging ring's persistent map and never flushes that span (UB per spec, works on drivers whose maps alias GPU-visible memory); our maps alias the CPU shadow, so the descriptors never reached the GPU: the scatter compute copied nothing (GLES: empty draw commands) or stale garbage (Vulkan: wild indirect commands ending in VK_ERROR_DEVICE_LOST). - MG_Impl/MG_Backend: real glFenceSync objects backed by backend fences (GLES: native ES syncs guarded by context generation and owner thread; Vulkan: buffer-manager frame serials), replacing always-signaled stubs that let Flywheel reclaim staging memory the GPU still reads. - MG_Backend/DirectGLES: compute dispatches now run the same per-program resource sync as draws (uniform-block bindings and sampler units must be re-established through the API because layout(binding) is stripped from transpiled ESSL) and rebind texture units afterwards; the cull shader used to read a stale _FlwFrameUniforms binding and the depth-pyramid downsample sampled a stale unit-0 texture, zeroing the Hi-Z pyramid and occlusion-culling all Flywheel geometry. Image uniforms are excluded from glUniform1i (ES bakes their unit via layout(binding)); image-unit sync is clamped to the device limit; eliminated/SSBO-classified uniform blocks are skipped. - MG_Backend/DirectGLES: gl_BaseInstance in native indirect draws reads the GPU-written command buffer through an injected mg_IndirectParams SSBO view addressed per draw instead of the zero CPU shadow; layout(binding) is preserved for SSBO/image declarations (ES has no API rebinding for them); the ES context ownership claim moved to a global atomic owner thread with an EGL ground-truth check, and deferred buffer op state is mutex-guarded, so ops cannot silently no-op after context migration. - MG_Backend/DirectVulkan: new RebaseInstanceIndexPass rewrites vertex InstanceIndex loads to (InstanceIndex - BaseInstance). glslang's relaxed Vulkan mode aliases gl_InstanceID to InstanceIndex, which includes firstInstance, but GL's gl_InstanceID is zero-based - draws with nonzero baseInstance paired meshes with wrong instance data (cogwheel drawn as a waterwheel, another wheel collapsed invisible). Gated on the shaderDrawParameters device feature. Sampled-read barriers additionally cover the compute stage (the Hi-Z downsample samples the depth attachment from compute), and short uniform-buffer ranges keep the existing zero-padding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
#include "SpirvPasses/RenameSamplerFunctionParameterPass.h"
|
||||
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
|
||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "spirv-tools/libspirv.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
@@ -264,6 +265,18 @@ namespace MobileGL {
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) {
|
||||
spvc_compiler_options options;
|
||||
session.CreateOptions(&options);
|
||||
|
||||
@@ -27,6 +27,12 @@ namespace MobileGL {
|
||||
// Only for backends without native draw-parameter support (DirectGLES).
|
||||
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
|
||||
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
|
||||
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
|
||||
// which wrongly includes baseInstance).
|
||||
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
static Result<String> DecompileShader(SpvcSession& session);
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
|
||||
@@ -30,7 +30,10 @@ namespace MobileGL {
|
||||
case spv::BuiltIn::BaseVertex:
|
||||
return "mg_BaseVertex";
|
||||
case spv::BuiltIn::BaseInstance:
|
||||
return "mg_BaseInstance";
|
||||
// Distinct from the mg_BaseInstance uniform: the program manager
|
||||
// expands this into an expression that can read the (possibly
|
||||
// GPU-written) indirect command buffer.
|
||||
return "mg_BaseInstanceLowered";
|
||||
case spv::BuiltIn::DrawIndex:
|
||||
return "mg_DrawID";
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.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 "RebaseInstanceIndexPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
// Returns the Input OpVariable decorated with |builtin|, or nullptr if none.
|
||||
Instruction* FindBuiltinInputVariable(IRContext* context, spv::BuiltIn builtin) {
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
for (auto& annotation : context->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate || annotation.NumInOperands() < 3) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
|
||||
spv::Decoration::BuiltIn) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2)) != builtin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Instruction* variable = defUseMgr->GetDef(annotation.GetSingleWordInOperand(0));
|
||||
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable ||
|
||||
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Input) {
|
||||
continue;
|
||||
}
|
||||
return variable;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Creates an Input OpVariable decorated BuiltIn BaseInstance, reusing the
|
||||
// pointer-to-Input-int type of the existing InstanceIndex variable, and adds it
|
||||
// to every OpEntryPoint interface list. Returns the new variable's result id.
|
||||
uint32_t SynthesizeBaseInstanceVariable(IRContext* context, Instruction* instanceIndexVar) {
|
||||
// gl_BaseInstance has the same type as gl_InstanceIndex (Input pointer to int);
|
||||
// reuse the existing pointer type instead of creating a duplicate.
|
||||
const uint32_t pointerTypeId = instanceIndexVar->type_id();
|
||||
const uint32_t variableId = context->TakeNextId();
|
||||
|
||||
context->AddGlobalValue(spvtools::MakeUnique<Instruction>(
|
||||
context, spv::Op::OpVariable, pointerTypeId, variableId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||
{static_cast<uint32_t>(spv::StorageClass::Input)}}}));
|
||||
|
||||
context->AddAnnotationInst(spvtools::MakeUnique<Instruction>(
|
||||
context, spv::Op::OpDecorate, 0, 0,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {variableId}},
|
||||
{SPV_OPERAND_TYPE_DECORATION,
|
||||
{static_cast<uint32_t>(spv::Decoration::BuiltIn)}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER,
|
||||
{static_cast<uint32_t>(spv::BuiltIn::BaseInstance)}}}));
|
||||
|
||||
// Input variables must appear in the entry point interface list.
|
||||
for (Instruction& entryPoint : context->module()->entry_points()) {
|
||||
entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {variableId}});
|
||||
}
|
||||
|
||||
return variableId;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status RebaseInstanceIndexPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
|
||||
Instruction* instanceIndexVar = FindBuiltinInputVariable(irContext, spv::BuiltIn::InstanceIndex);
|
||||
if (instanceIndexVar == nullptr) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
const uint32_t instanceIndexVarId = instanceIndexVar->result_id();
|
||||
|
||||
// Collect every load of the InstanceIndex variable before mutating anything.
|
||||
std::vector<Instruction*> instanceLoads;
|
||||
defUseMgr->ForEachUser(instanceIndexVar, [&](Instruction* user) {
|
||||
if (user->opcode() == spv::Op::OpLoad &&
|
||||
user->GetSingleWordInOperand(0) == instanceIndexVarId) {
|
||||
instanceLoads.push_back(user);
|
||||
}
|
||||
});
|
||||
|
||||
if (instanceLoads.empty()) {
|
||||
// The builtin is declared but never read; nothing to rebase.
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// Referencing BaseInstance requires the DrawParameters capability. On the SPIR-V
|
||||
// 1.3 target used here it is core, so no OpExtension is needed.
|
||||
irContext->AddCapability(spv::Capability::DrawParameters);
|
||||
|
||||
Instruction* baseInstanceVar = FindBuiltinInputVariable(irContext, spv::BuiltIn::BaseInstance);
|
||||
const uint32_t baseInstanceVarId = (baseInstanceVar != nullptr)
|
||||
? baseInstanceVar->result_id()
|
||||
: SynthesizeBaseInstanceVariable(irContext, instanceIndexVar);
|
||||
|
||||
// Replace each `OpLoad %ty %res %instanceIndex` with
|
||||
// OpLoad %ty %fresh1 %instanceIndex
|
||||
// OpLoad %ty %fresh2 %baseInstance
|
||||
// OpISub %ty %res %fresh1 %fresh2
|
||||
// Reusing %res on the OpISub keeps downstream use lists intact.
|
||||
for (Instruction* loadInst : instanceLoads) {
|
||||
const uint32_t typeId = loadInst->type_id();
|
||||
const uint32_t instanceValueId = irContext->TakeNextId();
|
||||
const uint32_t baseValueId = irContext->TakeNextId();
|
||||
|
||||
loadInst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpLoad, typeId, instanceValueId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {instanceIndexVarId}}}));
|
||||
loadInst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpLoad, typeId, baseValueId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {baseInstanceVarId}}}));
|
||||
|
||||
loadInst->SetOpcode(spv::Op::OpISub);
|
||||
loadInst->SetInOperands(Instruction::OperandList{
|
||||
{SPV_OPERAND_TYPE_ID, {instanceValueId}},
|
||||
{SPV_OPERAND_TYPE_ID, {baseValueId}}});
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken RebaseInstanceIndexPass::CreateRebaseInstanceIndexPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<RebaseInstanceIndexPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,37 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// glslang's relaxed-Vulkan mode aliases GL's gl_InstanceID to Vulkan's
|
||||
// gl_InstanceIndex. That is wrong for OpenGL semantics: GL's gl_InstanceID is
|
||||
// zero-based (excludes baseInstance) while Vulkan's InstanceIndex includes the
|
||||
// draw's firstInstance. On the DirectVulkan backend any draw with baseInstance != 0
|
||||
// (Flywheel's glMultiDrawElementsIndirect, glDrawElementsInstancedBaseInstance)
|
||||
// therefore feeds shaders an InstanceID offset by baseInstance. This pass rebases
|
||||
// every load of the InstanceIndex builtin to (InstanceIndex - BaseInstance), the
|
||||
// same lowering Zink/DXVK use, so the value seen by the shader matches GL's
|
||||
// zero-based gl_InstanceID. Vulkan backend only - the DirectGLES transpile path
|
||||
// already receives a zero-based gl_InstanceID from SPIRV-Cross.
|
||||
class RebaseInstanceIndexPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "rebase-instance-index"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateRebaseInstanceIndexPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
Reference in New Issue
Block a user