mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +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:
@@ -188,6 +188,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
|
||||
@@ -72,6 +72,10 @@ namespace MobileGL {
|
||||
SizeT GetRenderbufferFormatCapabilityTargetIndex();
|
||||
void PrintFormatCapabilities(const FormatCapabilityCache& cache);
|
||||
|
||||
// Opaque backend fence-sync handle, created by GLFunctionsTable::FenceSync
|
||||
// and released by GLFunctionsTable::DeleteSync.
|
||||
using BackendSyncHandle = void*;
|
||||
|
||||
struct GLFunctionsTable {
|
||||
void (*DrawArrays)(GLenum mode, GLint first, GLsizei count);
|
||||
void (*DrawElements)(GLenum mode, GLsizei count, GLenum type, const void* indices);
|
||||
@@ -157,6 +161,16 @@ namespace MobileGL {
|
||||
GLint (*GetProgramResourceLocation)(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
GLint (*GetProgramResourceLocationIndex)(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
void (*ShaderStorageBlockBinding)(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
||||
// GL fence sync objects. All entries are optional (may be null); the
|
||||
// frontend then falls back to always-signaled sync semantics.
|
||||
// FenceSync may itself return null when the backend cannot create a
|
||||
// fence right now (e.g. the calling thread does not own the backend
|
||||
// context); the frontend treats such a sync as always signaled.
|
||||
BackendSyncHandle (*FenceSync)();
|
||||
GLenum (*ClientWaitSync)(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
|
||||
void (*WaitSync)(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
|
||||
void (*DeleteSync)(BackendSyncHandle sync);
|
||||
Bool (*GetSyncStatus)(BackendSyncHandle sync); // true = signaled
|
||||
};
|
||||
struct GlobalBackendFunctionsTable {
|
||||
GLFunctionsTable GL;
|
||||
|
||||
@@ -866,6 +866,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
funcsTable.GL.GenerateMipmap = GenerateMipmap;
|
||||
funcsTable.GL.ReadPixels = ReadPixels;
|
||||
funcsTable.GL.GetTexImage = GetTexImage;
|
||||
funcsTable.GL.FenceSync = FenceSync;
|
||||
funcsTable.GL.ClientWaitSync = ClientWaitSync;
|
||||
funcsTable.GL.WaitSync = WaitSync;
|
||||
funcsTable.GL.DeleteSync = DeleteSync;
|
||||
funcsTable.GL.GetSyncStatus = GetSyncStatus;
|
||||
funcsTableInitialized = true;
|
||||
}
|
||||
return funcsTable;
|
||||
|
||||
@@ -25,9 +25,11 @@
|
||||
#include <MG_Util/Converters/MGToGL/RenderStateEnumConverter.h>
|
||||
#include <MG_Util/Metrics/BufferMetrics.h>
|
||||
#include <MG_Util/Texture/PixelStoreProcessor.h>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
#if defined(__linux__) && !defined(__ANDROID__) && __has_include(<X11/Xlib.h>)
|
||||
#pragma push_macro("Bool")
|
||||
#pragma push_macro("None")
|
||||
@@ -403,7 +405,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
for (Uint unit = 0; unit < MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS; ++unit) {
|
||||
// The frontend tracks more image units than ES exposes; binding past the device
|
||||
// limit raises GL_INVALID_VALUE on every dispatch.
|
||||
const Uint unitCount = std::min<Uint>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS,
|
||||
static_cast<Uint>(std::max(g_GLESCapabilities.MaxImageUnits, 0)));
|
||||
for (Uint unit = 0; unit < unitCount; ++unit) {
|
||||
SyncImageTextureBinding(unit);
|
||||
}
|
||||
}
|
||||
@@ -818,6 +824,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
FramebufferImpl::g_fboBindVersions[(SizeT)target] = slot.GetVersion();
|
||||
}
|
||||
|
||||
static void BindCurrentProgramWithResources();
|
||||
static void BindCurrentTextures();
|
||||
|
||||
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
|
||||
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
|
||||
// assigned through glUniform1i.
|
||||
static Bool IsImageUniformType(GLenum type) {
|
||||
switch (type) {
|
||||
case 0x904D: /*GL_IMAGE_2D*/
|
||||
case 0x904E: /*GL_IMAGE_3D*/
|
||||
case 0x9050: /*GL_IMAGE_CUBE*/
|
||||
case 0x9051: /*GL_IMAGE_BUFFER*/
|
||||
case 0x9053: /*GL_IMAGE_2D_ARRAY*/
|
||||
case 0x9058: /*GL_INT_IMAGE_2D*/
|
||||
case 0x9059: /*GL_INT_IMAGE_3D*/
|
||||
case 0x905B: /*GL_INT_IMAGE_CUBE*/
|
||||
case 0x905C: /*GL_INT_IMAGE_BUFFER*/
|
||||
case 0x905E: /*GL_INT_IMAGE_2D_ARRAY*/
|
||||
case 0x9063: /*GL_UNSIGNED_INT_IMAGE_2D*/
|
||||
case 0x9064: /*GL_UNSIGNED_INT_IMAGE_3D*/
|
||||
case 0x9066: /*GL_UNSIGNED_INT_IMAGE_CUBE*/
|
||||
case 0x9067: /*GL_UNSIGNED_INT_IMAGE_BUFFER*/
|
||||
case 0x9069: /*GL_UNSIGNED_INT_IMAGE_2D_ARRAY*/
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void PrepareForDraw(DrawSyncBit syncBit) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
@@ -846,45 +881,63 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
BindCurrentTextures();
|
||||
BindCurrentProgramWithResources();
|
||||
}
|
||||
|
||||
// Rebinds every frontend texture unit's textures (and sampler objects) on the
|
||||
// backend context. Needed before draws AND compute dispatches: content syncs
|
||||
// (SyncTextureObjectToBackend) bind scratch textures on the active unit as a
|
||||
// side effect, so unit bindings must be re-established afterwards or shaders
|
||||
// sample whatever texture the last sync left behind (e.g. Flywheel's depth
|
||||
// pyramid downsample reading a stale unit-0 binding instead of the depth
|
||||
// attachment).
|
||||
static void BindCurrentTextures() {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedNC("BindCurrentTextures", TRACY_ZONECOLOR_BACKEND);
|
||||
ZoneScopedNC("BindCurrentTextures", TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
Int maxTextureUnits = MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS;
|
||||
for (Int unit = 0; unit < maxTextureUnits; ++unit) {
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
Int maxTextureUnits = MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS;
|
||||
for (Int unit = 0; unit < maxTextureUnits; ++unit) {
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
|
||||
for (const auto& bindingSlot : textureUnit.GetAllBindingSlots()) {
|
||||
const auto& textureObject = bindingSlot.GetBoundObject();
|
||||
if (!textureObject) continue;
|
||||
for (const auto& bindingSlot : textureUnit.GetAllBindingSlots()) {
|
||||
const auto& textureObject = bindingSlot.GetBoundObject();
|
||||
if (!textureObject) continue;
|
||||
|
||||
// Bind texture object
|
||||
auto target = textureObject->GetTarget();
|
||||
if (!TextureImpl::IsSupportedTextureTarget(target)) {
|
||||
MGLOG_D(" Texture target %s is not supported, skipping.",
|
||||
MG_Util::ConvertTextureTargetToString(target).c_str());
|
||||
continue;
|
||||
}
|
||||
const auto& backendTextureIt = TextureImpl::g_backendTextureObjects.find(textureObject.get());
|
||||
if (backendTextureIt == TextureImpl::g_backendTextureObjects.end()) continue;
|
||||
// Bind texture object
|
||||
auto target = textureObject->GetTarget();
|
||||
if (!TextureImpl::IsSupportedTextureTarget(target)) {
|
||||
MGLOG_D(" Texture target %s is not supported, skipping.",
|
||||
MG_Util::ConvertTextureTargetToString(target).c_str());
|
||||
continue;
|
||||
}
|
||||
const auto& backendTextureIt = TextureImpl::g_backendTextureObjects.find(textureObject.get());
|
||||
if (backendTextureIt == TextureImpl::g_backendTextureObjects.end()) continue;
|
||||
|
||||
GLenum targetGL = MG_Util::ConvertTextureTargetToGLEnum(target);
|
||||
backendTextureIt->second->Bind(targetGL, unit);
|
||||
GLenum targetGL = MG_Util::ConvertTextureTargetToGLEnum(target);
|
||||
backendTextureIt->second->Bind(targetGL, unit);
|
||||
}
|
||||
|
||||
// Bind sampler object if necessary
|
||||
const auto& samplerObject = textureUnit.GetSamplerObject();
|
||||
if (samplerObject) {
|
||||
const auto& backendSamplerIt = SamplerImpl::g_backendSamplerObjects.find(samplerObject.get());
|
||||
if (backendSamplerIt != SamplerImpl::g_backendSamplerObjects.end()) {
|
||||
backendSamplerIt->second->Bind(unit);
|
||||
}
|
||||
|
||||
// Bind sampler object if necessary
|
||||
const auto& samplerObject = textureUnit.GetSamplerObject();
|
||||
if (samplerObject) {
|
||||
const auto& backendSamplerIt = SamplerImpl::g_backendSamplerObjects.find(samplerObject.get());
|
||||
if (backendSamplerIt != SamplerImpl::g_backendSamplerObjects.end()) {
|
||||
backendSamplerIt->second->Bind(unit);
|
||||
}
|
||||
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Binds the current program's backend object and re-establishes its per-program
|
||||
// resources: global UBO contents, uniform-block bindings, and sampler uniform
|
||||
// units (layout(binding=N) qualifiers are stripped from transpiled ESSL, so the
|
||||
// association must be rebuilt through the API). Compute dispatches depend on
|
||||
// this as much as draws do — e.g. Flywheel's cull shader reads the
|
||||
// _FlwFrameUniforms block and the _flw_depthPyramid sampler.
|
||||
static void BindCurrentProgramWithResources() {
|
||||
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
|
||||
if (currentProgram && currentProgram->GetLinkStatus()) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -933,6 +986,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto binding = currentProgram->GetUniformBlockBinding(i);
|
||||
auto& name = currentProgram->GetUniformBlockName(i);
|
||||
GLuint backendBlkIdx = g_GLESFuncs.glGetUniformBlockIndex(backendProgramId, name.c_str());
|
||||
if (backendBlkIdx == GL_INVALID_INDEX) {
|
||||
// Not a uniform block in the backend program: either eliminated as
|
||||
// unused, or an SSBO block (the frontend's reflection lists those
|
||||
// among uniform blocks); SSBO bindings are baked into the ESSL.
|
||||
continue;
|
||||
}
|
||||
g_GLESFuncs.glUniformBlockBinding(backendProgramId, backendBlkIdx, lastUBOBinding);
|
||||
|
||||
// Connect buffer to backend binding point
|
||||
@@ -970,13 +1029,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (name.empty()) continue;
|
||||
auto unit = currentProgram->GetUniformSamplerOrImageUnitIndex(loc);
|
||||
if (unit == -1) continue;
|
||||
const auto uniformType = currentProgram->GetUniformType(loc);
|
||||
if (IsImageUniformType(uniformType)) {
|
||||
// ES image units come exclusively from the layout(binding=N)
|
||||
// qualifier (preserved in the transpiled ESSL); glUniform1i on an
|
||||
// image uniform is an INVALID_OPERATION.
|
||||
continue;
|
||||
}
|
||||
auto locAtBackend = g_GLESFuncs.glGetUniformLocation(
|
||||
backendProgramIt->second->GetBackendProgramId(), name.c_str());
|
||||
g_GLESFuncs.glUniform1i(locAtBackend, unit);
|
||||
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
auto& samplerObject = textureUnit.GetSamplerObject();
|
||||
const auto uniformType = currentProgram->GetUniformType(loc);
|
||||
const auto& texture2D = textureUnit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
|
||||
const SharedPtr<MG_State::GLState::SamplerObject>* rawDepthSamplerObject = &samplerObject;
|
||||
if (!*rawDepthSamplerObject && texture2D) {
|
||||
@@ -1009,25 +1074,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
void SetCurrentBaseInstance(Uint32 baseInstance) {
|
||||
static SharedPtr<PrgramImpl::BackendProgramObjectImpl> GetCurrentBackendProgram() {
|
||||
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
|
||||
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
||||
return;
|
||||
return nullptr;
|
||||
}
|
||||
const auto& backendProgramIt = PrgramImpl::g_backendProgramObjects.find(currentProgram.get());
|
||||
if (backendProgramIt != PrgramImpl::g_backendProgramObjects.end()) {
|
||||
backendProgramIt->second->SetBaseInstance(baseInstance);
|
||||
return backendProgramIt->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void SetCurrentBaseInstance(Uint32 baseInstance) {
|
||||
if (const auto program = GetCurrentBackendProgram()) {
|
||||
program->SetBaseInstance(baseInstance);
|
||||
}
|
||||
}
|
||||
|
||||
void SetCurrentDrawID(Uint32 drawId) {
|
||||
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
|
||||
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
||||
return;
|
||||
}
|
||||
const auto& backendProgramIt = PrgramImpl::g_backendProgramObjects.find(currentProgram.get());
|
||||
if (backendProgramIt != PrgramImpl::g_backendProgramObjects.end()) {
|
||||
backendProgramIt->second->SetDrawID(drawId);
|
||||
if (const auto program = GetCurrentBackendProgram()) {
|
||||
program->SetDrawID(drawId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1048,18 +1115,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// GPU-written command fields at all - so native is never worse. Only client-memory
|
||||
// commands take the CPU per-command loop.
|
||||
static void ExecuteIndexedIndirectCommands(GLenum mode, GLenum type, SizeT indexSize, const Uint8* commandBytes,
|
||||
SizeT commandOffset, Bool hasIndirectBuffer, GLsizei drawcount,
|
||||
GLsizei stride, const char* label) {
|
||||
SizeT commandOffset,
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& drawIndirectBuffer,
|
||||
GLsizei drawcount, GLsizei stride, const char* label) {
|
||||
(void)label;
|
||||
const Bool useNative = hasIndirectBuffer && SupportsNativeIndirectDraws();
|
||||
const Bool useNative = drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
|
||||
if (useNative) {
|
||||
// gl_BaseInstance must observe GPU-written command fields; expose the indirect
|
||||
// buffer to the program's mg_IndirectParams SSBO view and address it per draw.
|
||||
const auto backendProgram = GetCurrentBackendProgram();
|
||||
const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1;
|
||||
if (paramsBinding >= 0) {
|
||||
auto* resource = BufferImpl::EnsureBufferResource(drawIndirectBuffer);
|
||||
if (resource && resource->id != 0) {
|
||||
g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(paramsBinding),
|
||||
resource->id);
|
||||
}
|
||||
}
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
DrawElementsIndirectCommand cmd{};
|
||||
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
||||
const SizeT cmdByteOffset = commandOffset + static_cast<SizeT>(i) * stride;
|
||||
SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
SetCurrentBaseInstance(cmd.baseInstance);
|
||||
g_GLESFuncs.glDrawElementsIndirect(
|
||||
mode, type, reinterpret_cast<const void*>(commandOffset + static_cast<SizeT>(i) * stride));
|
||||
if (paramsBinding >= 0 && backendProgram) {
|
||||
// baseInstance is the 5th word of DrawElementsIndirectCommand.
|
||||
backendProgram->SetBaseInstanceWordIndex(static_cast<Int32>((cmdByteOffset + 16) / 4));
|
||||
} else {
|
||||
DrawElementsIndirectCommand cmd{};
|
||||
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
||||
SetCurrentBaseInstance(cmd.baseInstance);
|
||||
}
|
||||
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(cmdByteOffset));
|
||||
}
|
||||
} else {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
@@ -1081,18 +1165,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
static void ExecuteArraysIndirectCommands(GLenum mode, const Uint8* commandBytes, SizeT commandOffset,
|
||||
Bool hasIndirectBuffer, GLsizei drawcount, GLsizei stride,
|
||||
const char* label) {
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& drawIndirectBuffer,
|
||||
GLsizei drawcount, GLsizei stride, const char* label) {
|
||||
(void)label;
|
||||
const Bool useNative = hasIndirectBuffer && SupportsNativeIndirectDraws();
|
||||
const Bool useNative = drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
|
||||
if (useNative) {
|
||||
const auto backendProgram = GetCurrentBackendProgram();
|
||||
const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1;
|
||||
if (paramsBinding >= 0) {
|
||||
auto* resource = BufferImpl::EnsureBufferResource(drawIndirectBuffer);
|
||||
if (resource && resource->id != 0) {
|
||||
g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(paramsBinding),
|
||||
resource->id);
|
||||
}
|
||||
}
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
DrawArraysIndirectCommand cmd{};
|
||||
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
||||
const SizeT cmdByteOffset = commandOffset + static_cast<SizeT>(i) * stride;
|
||||
SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
SetCurrentBaseInstance(cmd.baseInstance);
|
||||
g_GLESFuncs.glDrawArraysIndirect(
|
||||
mode, reinterpret_cast<const void*>(commandOffset + static_cast<SizeT>(i) * stride));
|
||||
if (paramsBinding >= 0 && backendProgram) {
|
||||
// baseInstance is the 4th word of DrawArraysIndirectCommand.
|
||||
backendProgram->SetBaseInstanceWordIndex(static_cast<Int32>((cmdByteOffset + 12) / 4));
|
||||
} else {
|
||||
DrawArraysIndirectCommand cmd{};
|
||||
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
||||
SetCurrentBaseInstance(cmd.baseInstance);
|
||||
}
|
||||
g_GLESFuncs.glDrawArraysIndirect(mode, reinterpret_cast<const void*>(cmdByteOffset));
|
||||
}
|
||||
} else {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
@@ -1127,13 +1225,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& backendProgramIt = PrgramImpl::g_backendProgramObjects.find(currentProgram.get());
|
||||
if (backendProgramIt != PrgramImpl::g_backendProgramObjects.end()) {
|
||||
backendProgramIt->second->Use();
|
||||
} else {
|
||||
g_GLESFuncs.glUseProgram(0);
|
||||
MGLOG_E("No backend program found (maybe not synced) for current compute program.");
|
||||
}
|
||||
// Compute shaders sample textures through the same unit bindings as draws
|
||||
// (e.g. Flywheel's depth-pyramid downsample reads the depth attachment on
|
||||
// unit 0), so re-establish unit bindings after the content syncs above.
|
||||
BindCurrentTextures();
|
||||
// Compute programs need the same per-program resource sync as draws:
|
||||
// uniform-block bindings and sampler units only exist through the API
|
||||
// because layout(binding) is stripped from the transpiled ESSL.
|
||||
BindCurrentProgramWithResources();
|
||||
}
|
||||
|
||||
GLuint GetBackendProgramId(GLuint program) {
|
||||
@@ -1267,10 +1366,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
|
||||
const Bool hasIndirectBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject() != nullptr;
|
||||
const auto& drawIndirectBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast<SizeT>(indirect),
|
||||
hasIndirectBuffer, drawcount, stride, "MultiDrawElementsIndirect");
|
||||
drawIndirectBuffer, drawcount, stride, "MultiDrawElementsIndirect");
|
||||
}
|
||||
|
||||
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
||||
@@ -1331,7 +1430,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
std::memcpy(&actualDrawCount, parameterData->data() + drawcount, sizeof(actualDrawCount));
|
||||
actualDrawCount = std::min<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
|
||||
ExecuteIndexedIndirectCommands(mode, type, indexSize, drawData->data() + commandOffset, commandOffset,
|
||||
/*hasIndirectBuffer=*/true, static_cast<GLsizei>(actualDrawCount), stride,
|
||||
drawBuffer, static_cast<GLsizei>(actualDrawCount), stride,
|
||||
"MultiDrawElementsIndirectCount");
|
||||
}
|
||||
|
||||
@@ -1362,9 +1461,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
|
||||
const Bool hasIndirectBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject() != nullptr;
|
||||
ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast<SizeT>(indirect), hasIndirectBuffer,
|
||||
const auto& drawIndirectBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast<SizeT>(indirect), drawIndirectBuffer,
|
||||
drawcount, stride, "MultiDrawArraysIndirect");
|
||||
}
|
||||
|
||||
@@ -1428,10 +1527,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
|
||||
const Bool hasIndirectBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject() != nullptr;
|
||||
const auto& drawIndirectBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast<SizeT>(indirect),
|
||||
hasIndirectBuffer, 1, sizeof(DrawElementsIndirectCommand),
|
||||
drawIndirectBuffer, 1, sizeof(DrawElementsIndirectCommand),
|
||||
"DrawElementsIndirect");
|
||||
}
|
||||
|
||||
@@ -1460,9 +1559,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
|
||||
const Bool hasIndirectBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject() != nullptr;
|
||||
ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast<SizeT>(indirect), hasIndirectBuffer, 1,
|
||||
const auto& drawIndirectBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast<SizeT>(indirect), drawIndirectBuffer, 1,
|
||||
sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect");
|
||||
}
|
||||
|
||||
@@ -3526,7 +3625,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
namespace {
|
||||
thread_local Bool t_backendContextCurrent = false;
|
||||
// The single backend ES context migrates between app threads (FCL/pojav-style
|
||||
// LWJGL hands the EGL context from JVM thread to JVM thread). Ownership must
|
||||
// live in ONE global slot: a per-thread flag can never be cleared on the
|
||||
// LOSING thread when another thread takes (or destroys/releases) the context,
|
||||
// leaving a stale "current" claim behind. A stale claim makes buffer ops issue
|
||||
// GL calls that silently no-op (no context is current on that thread) while
|
||||
// still updating shadow bookkeeping (bind cache, synced serials), permanently
|
||||
// desynchronizing backend buffer state.
|
||||
std::atomic<std::thread::id> g_backendContextOwnerThread{};
|
||||
|
||||
// Bumped whenever the backend ES context is destroyed; fence handles
|
||||
// created under an older generation belong to a dead context and must
|
||||
// never be passed back to GL (mirrors BufferImpl's context tracking).
|
||||
Uint g_syncContextGeneration = 1;
|
||||
|
||||
// Backend fence handle: a native ES sync plus the ES context
|
||||
// generation it was created under.
|
||||
struct GLESSyncObject {
|
||||
GLsync esSync = nullptr;
|
||||
Uint contextGeneration = 0;
|
||||
};
|
||||
}
|
||||
|
||||
Bool MakeCurrent() {
|
||||
@@ -3540,7 +3659,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_E("DirectGLES::MakeCurrent failed: native eglMakeCurrent returned error 0x%04x", error);
|
||||
return false;
|
||||
}
|
||||
t_backendContextCurrent = true;
|
||||
g_backendContextOwnerThread.store(std::this_thread::get_id(), std::memory_order_release);
|
||||
// The ops table may have been unregistered when a previous ES context was
|
||||
// destroyed (e.g. a probe context); re-register now that GL is usable.
|
||||
BufferImpl::RegisterBufferBackendOps();
|
||||
@@ -3549,7 +3668,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
Bool ReleaseCurrent() {
|
||||
if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY) {
|
||||
t_backendContextCurrent = false;
|
||||
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
if (!g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) {
|
||||
@@ -3557,12 +3676,101 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_E("DirectGLES::ReleaseCurrent failed: native eglMakeCurrent returned error 0x%04x", error);
|
||||
return false;
|
||||
}
|
||||
t_backendContextCurrent = false;
|
||||
// Clearing the global owner works from ANY thread (a release request can
|
||||
// legally arrive on a thread other than the current owner); erring towards
|
||||
// "not current" only defers buffer ops, which is always safe.
|
||||
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool IsBackendContextCurrentOnThisThread() {
|
||||
return t_backendContextCurrent && g_Context != EGL_NO_CONTEXT;
|
||||
if (g_Context == EGL_NO_CONTEXT) {
|
||||
return false;
|
||||
}
|
||||
if (g_backendContextOwnerThread.load(std::memory_order_acquire) != std::this_thread::get_id()) {
|
||||
return false;
|
||||
}
|
||||
// Belt and braces: EGL itself is the ground truth. A migration that bypassed
|
||||
// MakeCurrent()/ReleaseCurrent() must not leave a stale ownership claim
|
||||
// standing, or GL calls would silently no-op while shadow bookkeeping (bind
|
||||
// cache, synced serials) still advances.
|
||||
if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() != g_Context) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
BackendSyncHandle FenceSync() {
|
||||
// ES fences can only be created on the thread that owns the ES context
|
||||
// (Flywheel and friends fence on the render thread, which does).
|
||||
// Returning null makes the frontend fall back to an always-signaled
|
||||
// sync object.
|
||||
if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glFenceSync) {
|
||||
return nullptr;
|
||||
}
|
||||
GLsync esSync = g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
if (esSync == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return new GLESSyncObject{esSync, g_syncContextGeneration};
|
||||
}
|
||||
|
||||
GLenum ClientWaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) {
|
||||
const auto* sync = static_cast<GLESSyncObject*>(handle);
|
||||
if (sync == nullptr) {
|
||||
return GL_ALREADY_SIGNALED;
|
||||
}
|
||||
// The creating ES context is gone: its GPU work either completed or
|
||||
// died with the context; waiting is meaningless either way.
|
||||
if (sync->contextGeneration != g_syncContextGeneration) {
|
||||
return GL_ALREADY_SIGNALED;
|
||||
}
|
||||
// Degraded path: a thread that does not own the ES context cannot
|
||||
// issue GL calls, so report signaled instead of blocking on state we
|
||||
// cannot observe. Fence waits normally arrive on the render thread,
|
||||
// which owns the context.
|
||||
if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glClientWaitSync) {
|
||||
return GL_ALREADY_SIGNALED;
|
||||
}
|
||||
return g_GLESFuncs.glClientWaitSync(sync->esSync, flags & GL_SYNC_FLUSH_COMMANDS_BIT, timeout);
|
||||
}
|
||||
|
||||
void WaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) {
|
||||
(void)flags;
|
||||
(void)timeout;
|
||||
const auto* sync = static_cast<GLESSyncObject*>(handle);
|
||||
if (sync == nullptr || sync->contextGeneration != g_syncContextGeneration ||
|
||||
!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glWaitSync) {
|
||||
return;
|
||||
}
|
||||
// ES 3.0 requires flags == 0 and timeout == GL_TIMEOUT_IGNORED.
|
||||
g_GLESFuncs.glWaitSync(sync->esSync, 0, GL_TIMEOUT_IGNORED);
|
||||
}
|
||||
|
||||
void DeleteSync(BackendSyncHandle handle) {
|
||||
auto* sync = static_cast<GLESSyncObject*>(handle);
|
||||
if (sync == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (sync->contextGeneration == g_syncContextGeneration && IsBackendContextCurrentOnThisThread() &&
|
||||
g_GLESFuncs.glDeleteSync) {
|
||||
g_GLESFuncs.glDeleteSync(sync->esSync);
|
||||
}
|
||||
// Otherwise the ES sync is abandoned; the ES context reclaims all of
|
||||
// its sync objects when it is destroyed.
|
||||
delete sync;
|
||||
}
|
||||
|
||||
Bool GetSyncStatus(BackendSyncHandle handle) {
|
||||
const auto* sync = static_cast<GLESSyncObject*>(handle);
|
||||
if (sync == nullptr || sync->contextGeneration != g_syncContextGeneration ||
|
||||
!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glGetSynciv) {
|
||||
return true;
|
||||
}
|
||||
GLint status = GL_SIGNALED;
|
||||
GLsizei length = 0;
|
||||
g_GLESFuncs.glGetSynciv(sync->esSync, GL_SYNC_STATUS, 1, &length, &status);
|
||||
return status == GL_SIGNALED;
|
||||
}
|
||||
|
||||
void Present() {
|
||||
@@ -3571,7 +3779,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
void DestroyEGLContext() {
|
||||
BufferImpl::OnBackendContextDestroyed();
|
||||
t_backendContextCurrent = false;
|
||||
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
|
||||
// Outstanding fence handles now refer to a dead context; treat them as
|
||||
// signaled from here on.
|
||||
++g_syncContextGeneration;
|
||||
if (g_Display != EGL_NO_DISPLAY) {
|
||||
g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
if (g_Context != EGL_NO_CONTEXT) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
#include <MG_State/GLState/TextureState/TextureState.h>
|
||||
#include <MG_State/GLState/SamplerState/SamplerObject.h>
|
||||
@@ -98,6 +99,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// True when the backend ES context is current on the calling thread, i.e.
|
||||
// immediate buffer ops may issue GL calls right now.
|
||||
Bool IsBackendContextCurrentOnThisThread();
|
||||
// GL fence sync objects, backed by native ES fences. FenceSync returns null
|
||||
// (the frontend then falls back to an always-signaled sync) when the calling
|
||||
// thread does not own the ES context. Waits/queries degrade to "signaled" in
|
||||
// the same situation, and handles created under a since-destroyed ES context
|
||||
// are always treated as signaled.
|
||||
BackendSyncHandle FenceSync();
|
||||
GLenum ClientWaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
|
||||
void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
|
||||
void DeleteSync(BackendSyncHandle sync);
|
||||
Bool GetSyncStatus(BackendSyncHandle sync);
|
||||
void Present();
|
||||
void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs);
|
||||
void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs);
|
||||
|
||||
@@ -34,6 +34,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
constexpr const char* BASE_INSTANCE_UNIFORM_NAME = "mg_BaseInstance";
|
||||
constexpr const char* DRAW_ID_UNIFORM_NAME = "mg_DrawID";
|
||||
constexpr const char* BASE_VERTEX_UNIFORM_NAME = "mg_BaseVertex";
|
||||
constexpr const char* BASE_INSTANCE_LOWERED_NAME = "mg_BaseInstanceLowered";
|
||||
constexpr const char* BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME = "mg_BaseInstanceWordIndex";
|
||||
constexpr const char* INDIRECT_PARAMS_BLOCK_NAME = "mg_IndirectParams";
|
||||
|
||||
static Bool IsAngleLlvmpipeRenderer() {
|
||||
return g_GLESCapabilities.GLESRendererString.find("ANGLE") != String::npos &&
|
||||
@@ -111,20 +114,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (shaderType != GL_VERTEX_SHADER || source.find("gl_BaseInstance") == String::npos) {
|
||||
return source;
|
||||
}
|
||||
source = ReplaceIdentifier(std::move(source), "gl_BaseInstance", BASE_INSTANCE_UNIFORM_NAME);
|
||||
return InjectUniformAfterVersion(std::move(source),
|
||||
String replaced = ReplaceIdentifier(source, "gl_BaseInstance", BASE_INSTANCE_UNIFORM_NAME);
|
||||
if (replaced == source) {
|
||||
// Only a substring hit (e.g. gl_BaseInstanceARB inside a SPIRV-Cross #ifdef
|
||||
// fallback); nothing was rewritten, so nothing must be declared either.
|
||||
return source;
|
||||
}
|
||||
return InjectUniformAfterVersion(std::move(replaced),
|
||||
String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";");
|
||||
}
|
||||
|
||||
// The LowerDrawParametersPass demotes gl_DrawID / gl_BaseInstance / gl_BaseVertex to plain
|
||||
// Private globals named mg_DrawID / mg_BaseInstance / mg_BaseVertex; SPIRV-Cross then emits
|
||||
// them as ordinary global declarations. Turn those declarations into uniforms so the draw
|
||||
// paths can feed real values per (sub-)draw.
|
||||
// Private globals (mg_DrawID / mg_BaseInstanceLowered / mg_BaseVertex); SPIRV-Cross then
|
||||
// emits them as ordinary global declarations. mg_DrawID / mg_BaseVertex become uniforms fed
|
||||
// per (sub-)draw. gl_BaseInstance is special: for indirect draws its value lives in the
|
||||
// (possibly GPU-written) indirect command buffer, so its declaration expands into a
|
||||
// std430 SSBO view of that buffer indexed by a CPU-computed word index, with the plain
|
||||
// mg_BaseInstance uniform as the fallback for non-indirect draws.
|
||||
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType) {
|
||||
if (shaderType != GL_VERTEX_SHADER) {
|
||||
return source;
|
||||
}
|
||||
for (const char* name : {DRAW_ID_UNIFORM_NAME, BASE_INSTANCE_UNIFORM_NAME, BASE_VERTEX_UNIFORM_NAME}) {
|
||||
for (const char* name : {DRAW_ID_UNIFORM_NAME, BASE_VERTEX_UNIFORM_NAME}) {
|
||||
for (const char* declPrefix : {"highp int ", "mediump int ", "lowp int ", "int ", "highp uint ",
|
||||
"mediump uint ", "uint "}) {
|
||||
const String declaration = String(declPrefix) + name + ";";
|
||||
@@ -143,6 +154,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const char* declPrefix : {"highp int ", "mediump int ", "lowp int ", "int "}) {
|
||||
const String declaration = String(declPrefix) + BASE_INSTANCE_LOWERED_NAME + ";";
|
||||
const SizeT pos = source.find(declaration);
|
||||
if (pos == String::npos) {
|
||||
continue;
|
||||
}
|
||||
const Int paramsBinding = g_GLESCapabilities.MaxShaderStorageBufferBindings > 0
|
||||
? g_GLESCapabilities.MaxShaderStorageBufferBindings - 1
|
||||
: 0;
|
||||
String machinery;
|
||||
if (source.find(String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";") == String::npos) {
|
||||
machinery += String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";\n";
|
||||
}
|
||||
machinery += String("uniform highp int ") + BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + ";\n";
|
||||
machinery += String("layout(std430, binding = ") + std::to_string(paramsBinding) +
|
||||
") readonly buffer " + INDIRECT_PARAMS_BLOCK_NAME +
|
||||
" { highp uint mg_indirectWords[]; };\n";
|
||||
machinery += String("#define ") + BASE_INSTANCE_LOWERED_NAME + " ((" +
|
||||
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " >= 0) ? int(mg_indirectWords[uint(" +
|
||||
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + ")]) : " + BASE_INSTANCE_UNIFORM_NAME + ")";
|
||||
source.replace(pos, declaration.size(), machinery);
|
||||
break;
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
@@ -196,6 +230,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
resource.storageSize == bufferObject.GetSize();
|
||||
}
|
||||
|
||||
|
||||
|
||||
void UploadRangeNow(GLESBufferResource& resource, BufferObject& bufferObject, SizeT start, SizeT end) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
@@ -2050,6 +2086,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_baseInstanceUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId,
|
||||
BASE_INSTANCE_UNIFORM_NAME);
|
||||
m_drawIdUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, DRAW_ID_UNIFORM_NAME);
|
||||
m_baseInstanceWordIndexUniformLocation =
|
||||
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME);
|
||||
// The mg_IndirectParams block binding is baked into the ESSL (ES cannot rebind
|
||||
// SSBO blocks after compile); record it so draws bind the indirect buffer there.
|
||||
m_indirectParamsBinding = -1;
|
||||
if (m_baseInstanceWordIndexUniformLocation >= 0 && g_GLESFuncs.glGetProgramResourceIndex) {
|
||||
const GLuint blockIndex = g_GLESFuncs.glGetProgramResourceIndex(
|
||||
m_backendProgramId, GL_SHADER_STORAGE_BLOCK, INDIRECT_PARAMS_BLOCK_NAME);
|
||||
if (blockIndex != GL_INVALID_INDEX && g_GLESCapabilities.MaxShaderStorageBufferBindings > 0) {
|
||||
m_indirectParamsBinding = g_GLESCapabilities.MaxShaderStorageBufferBindings - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Create global UBO
|
||||
if (stateProgramObject->GetUBOSize() > 0) {
|
||||
@@ -2074,10 +2122,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
void BackendProgramObjectImpl::SetBaseInstance(Uint32 baseInstance) const {
|
||||
if (m_baseInstanceUniformLocation < 0) {
|
||||
return;
|
||||
if (m_baseInstanceUniformLocation >= 0) {
|
||||
g_GLESFuncs.glUniform1i(m_baseInstanceUniformLocation, static_cast<GLint>(baseInstance));
|
||||
}
|
||||
// A direct value disables the indirect-command-buffer read.
|
||||
if (m_baseInstanceWordIndexUniformLocation >= 0) {
|
||||
g_GLESFuncs.glUniform1i(m_baseInstanceWordIndexUniformLocation, -1);
|
||||
}
|
||||
}
|
||||
|
||||
void BackendProgramObjectImpl::SetBaseInstanceWordIndex(Int32 wordIndex) const {
|
||||
if (m_baseInstanceWordIndexUniformLocation >= 0) {
|
||||
g_GLESFuncs.glUniform1i(m_baseInstanceWordIndexUniformLocation, wordIndex);
|
||||
}
|
||||
g_GLESFuncs.glUniform1i(m_baseInstanceUniformLocation, static_cast<GLint>(baseInstance));
|
||||
}
|
||||
|
||||
void BackendProgramObjectImpl::SetDrawID(Uint32 drawId) const {
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include "DirectGLES.h"
|
||||
#include "MG_State/GLState/SamplerState/SamplerObject.h"
|
||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||
@@ -132,12 +134,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint contextGeneration = 0;
|
||||
// Frontend change serial the backend storage reflects. When immediate
|
||||
// ops cannot run (ops unregistered, no current context), this lags and
|
||||
// EnsureBufferResource falls back to a full re-upload.
|
||||
Uint64 syncedChangeSerial = 0;
|
||||
// EnsureBufferResource falls back to a full re-upload. Atomic: read on
|
||||
// the context-owning thread while ops on other threads may update it.
|
||||
std::atomic<Uint64> syncedChangeSerial{0};
|
||||
// Ops that arrived while no ES context was current on the calling thread
|
||||
// (or before storage existed); replayed by EnsureBufferResource.
|
||||
// (or before storage existed); replayed by EnsureBufferResource. The ES
|
||||
// context migrates between app threads, so deferring ops can race with
|
||||
// the owning thread replaying them: guard both fields with pendingMutex.
|
||||
Bool pendingRespecify = false;
|
||||
VecRange1D pendingRanges;
|
||||
std::mutex pendingMutex;
|
||||
};
|
||||
|
||||
// Registered as the frontend's BufferBackendOps at backend init and on
|
||||
@@ -317,7 +323,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||
void Use() const;
|
||||
void SetBaseInstance(Uint32 baseInstance) const;
|
||||
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
|
||||
void SetDrawID(Uint32 drawId) const;
|
||||
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
||||
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
||||
Uint GetBackendGlobalUBOId() const { return m_backendGlobalUBOId; }
|
||||
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
|
||||
@@ -328,6 +336,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint m_backendGlobalUBOId = 0;
|
||||
Int m_baseInstanceUniformLocation = -1;
|
||||
Int m_drawIdUniformLocation = -1;
|
||||
Int m_baseInstanceWordIndexUniformLocation = -1;
|
||||
Int m_indirectParamsBinding = -1;
|
||||
Uint32 m_snormFallbackClampOutputMask = 0;
|
||||
Uint32 m_unormFallbackClampOutputMask = 0;
|
||||
Bool m_isInitialized = false;
|
||||
|
||||
@@ -307,10 +307,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// Sampler and uniform-block bindings are re-established at draw time through the
|
||||
// API, so their layout qualifiers are stripped (they may exceed ES limits). SSBO
|
||||
// blocks and image uniforms are different: ES has no glShaderStorageBlockBinding,
|
||||
// and image units cannot be set with glUniform1i, so for those declarations the
|
||||
// binding qualifier is the only binding mechanism and must be preserved.
|
||||
static std::regex bindingRegex(R"(layout\s*\(\s*binding\s*=\s*\d+\s*\)\s*)");
|
||||
String result = std::regex_replace(glslCode, bindingRegex, "");
|
||||
static std::regex bindingRegex2(R"(layout\s*\(\s*binding\s*=\s*\d+\s*,)");
|
||||
result = std::regex_replace(result, bindingRegex2, "layout(");
|
||||
static std::regex keepBindingRegex(R"(\b(buffer|[iu]?image[A-Za-z0-9]*)\b)");
|
||||
|
||||
String result;
|
||||
result.reserve(glslCode.size());
|
||||
SizeT lineStart = 0;
|
||||
while (lineStart <= glslCode.size()) {
|
||||
SizeT lineEnd = glslCode.find('\n', lineStart);
|
||||
const Bool lastLine = lineEnd == String::npos;
|
||||
String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
|
||||
|
||||
if (!std::regex_search(line, keepBindingRegex)) {
|
||||
line = std::regex_replace(line, bindingRegex, "");
|
||||
line = std::regex_replace(line, bindingRegex2, "layout(");
|
||||
}
|
||||
|
||||
result += line;
|
||||
if (lastLine) {
|
||||
break;
|
||||
}
|
||||
result += '\n';
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace PrgramImpl
|
||||
|
||||
@@ -569,6 +569,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
funcsTable.GL.GetProgramResourceLocation = GetProgramResourceLocation;
|
||||
funcsTable.GL.GetProgramResourceLocationIndex = GetProgramResourceLocationIndex;
|
||||
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
||||
funcsTable.GL.FenceSync = FenceSync;
|
||||
funcsTable.GL.ClientWaitSync = ClientWaitSync;
|
||||
funcsTable.GL.WaitSync = WaitSync;
|
||||
funcsTable.GL.DeleteSync = DeleteSync;
|
||||
funcsTable.GL.GetSyncStatus = GetSyncStatus;
|
||||
funcsTableInitialized = true;
|
||||
}
|
||||
return funcsTable;
|
||||
|
||||
@@ -1342,6 +1342,63 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
dstY0, dstX1, dstY1, mask, filter);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Backend fence handle: the VkBufferManager frame serial captured at
|
||||
// fence creation. The fence is signaled once every command recorded
|
||||
// under that serial has completed on the GPU (the same busy-tracking
|
||||
// horizon used to recycle buffer resources).
|
||||
struct VulkanSyncObject {
|
||||
Uint64 frameSerial = 0;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
BackendSyncHandle FenceSync() {
|
||||
if (!pVulkanRenderer) {
|
||||
return nullptr;
|
||||
}
|
||||
return new VulkanSyncObject{pVulkanRenderer->GetCurrentFrameSerial()};
|
||||
}
|
||||
|
||||
GLenum ClientWaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) {
|
||||
// Commands are only submitted at Present, so GL_SYNC_FLUSH_COMMANDS_BIT
|
||||
// cannot force progress mid-frame; WaitForFrameSerial reports whether
|
||||
// waiting can succeed at all.
|
||||
(void)flags;
|
||||
const auto* sync = static_cast<VulkanSyncObject*>(handle);
|
||||
if (sync == nullptr || !pVulkanRenderer) {
|
||||
return GL_ALREADY_SIGNALED;
|
||||
}
|
||||
if (pVulkanRenderer->IsFrameSerialComplete(sync->frameSerial)) {
|
||||
return GL_ALREADY_SIGNALED;
|
||||
}
|
||||
if (timeout == 0) {
|
||||
return GL_TIMEOUT_EXPIRED;
|
||||
}
|
||||
return pVulkanRenderer->WaitForFrameSerial(sync->frameSerial, timeout) ? GL_CONDITION_SATISFIED
|
||||
: GL_TIMEOUT_EXPIRED;
|
||||
}
|
||||
|
||||
void WaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) {
|
||||
// Server-side waits are implicit: the single graphics queue executes
|
||||
// submissions in order, so later GPU work already observes everything
|
||||
// recorded before the fence.
|
||||
(void)handle;
|
||||
(void)flags;
|
||||
(void)timeout;
|
||||
}
|
||||
|
||||
void DeleteSync(BackendSyncHandle handle) {
|
||||
delete static_cast<VulkanSyncObject*>(handle);
|
||||
}
|
||||
|
||||
Bool GetSyncStatus(BackendSyncHandle handle) {
|
||||
const auto* sync = static_cast<VulkanSyncObject*>(handle);
|
||||
if (sync == nullptr || !pVulkanRenderer) {
|
||||
return true;
|
||||
}
|
||||
return pVulkanRenderer->IsFrameSerialComplete(sync->frameSerial);
|
||||
}
|
||||
|
||||
void Present() {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer");
|
||||
pVulkanRenderer->Present();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include "Renderer/VulkanRenderer.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -89,5 +90,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
||||
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
|
||||
GLint level, GLenum format, GLenum type, GLsizei bufSize, GLvoid* pixels);
|
||||
// GL fence sync objects, mapped onto the renderer's frame-serial busy
|
||||
// tracking: a fence captures the frame serial current at creation and is
|
||||
// signaled once every command recorded under that serial has completed on
|
||||
// the GPU.
|
||||
BackendSyncHandle FenceSync();
|
||||
GLenum ClientWaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
|
||||
void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
|
||||
void DeleteSync(BackendSyncHandle sync);
|
||||
Bool GetSyncStatus(BackendSyncHandle sync);
|
||||
void Present();
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "ProgramFactory.h"
|
||||
|
||||
#include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h"
|
||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||
#include <cstring>
|
||||
@@ -311,6 +312,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return targetEnv;
|
||||
}
|
||||
|
||||
// Cheap raw-word scan for an `OpDecorate <id> BuiltIn InstanceIndex` decoration. Used
|
||||
// only to decide whether to warn when shaderDrawParameters is unavailable; a false
|
||||
// negative merely suppresses a diagnostic.
|
||||
Bool SpirvDeclaresInstanceIndexBuiltin(const Vector<Uint>& spirv) {
|
||||
constexpr Uint32 kSpirvMagicNumber = 0x07230203u;
|
||||
constexpr SizeT kHeaderWordCount = 5;
|
||||
if (spirv.size() <= kHeaderWordCount || spirv[0] != kSpirvMagicNumber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SizeT wordIndex = kHeaderWordCount;
|
||||
while (wordIndex < spirv.size()) {
|
||||
const Uint32 firstWord = spirv[wordIndex];
|
||||
const Uint32 wordCount = firstWord >> 16;
|
||||
const auto opcode = static_cast<spv::Op>(firstWord & 0xffffu);
|
||||
if (wordCount == 0 || wordIndex + wordCount > spirv.size()) {
|
||||
break;
|
||||
}
|
||||
if (opcode == spv::Op::OpDecorate && wordCount >= 4 &&
|
||||
static_cast<spv::Decoration>(spirv[wordIndex + 2]) == spv::Decoration::BuiltIn &&
|
||||
static_cast<spv::BuiltIn>(spirv[wordIndex + 3]) == spv::BuiltIn::InstanceIndex) {
|
||||
return true;
|
||||
}
|
||||
wordIndex += wordCount;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool IsInterfaceVariableStaticallyUsed(const Vector<Uint>& spirv, Uint32 spirvId) {
|
||||
if (spirv.empty() || spirvId == 0) {
|
||||
return false;
|
||||
@@ -1661,6 +1690,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} else {
|
||||
moduleSpirvs[i] = spv;
|
||||
}
|
||||
|
||||
// glslang's relaxed-Vulkan mode aliases GL's zero-based gl_InstanceID to Vulkan's
|
||||
// gl_InstanceIndex, which wrongly includes the draw's baseInstance. Rebase vertex-stage
|
||||
// loads to (InstanceIndex - BaseInstance) so shaders observe GL semantics. Reflection
|
||||
// below runs on the rebased words so the added BaseInstance builtin stays consistent.
|
||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) {
|
||||
if (m_shaderDrawParametersEnabled) {
|
||||
Vector<Uint> rebasedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
|
||||
rebasedSpirv)) {
|
||||
moduleSpirvs[i] = std::move(rebasedSpirv);
|
||||
} else {
|
||||
MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; "
|
||||
"instanced draws with a non-zero baseInstance may render incorrectly",
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
} else if (SpirvDeclaresInstanceIndexBuiltin(moduleSpirvs[i])) {
|
||||
static Bool s_warnedInstanceIndexUnsupported = false;
|
||||
if (!s_warnedInstanceIndexUnsupported) {
|
||||
s_warnedInstanceIndexUnsupported = true;
|
||||
MGLOG_W("ProgramFactory: shaderDrawParameters is unavailable; gl_InstanceID cannot be "
|
||||
"rebased and instanced draws with a non-zero baseInstance may render incorrectly");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs);
|
||||
|
||||
@@ -166,8 +166,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
};
|
||||
|
||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16)
|
||||
: m_device(device), m_config(config), m_maxBindings(maxBindings) {
|
||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
|
||||
Bool shaderDrawParametersEnabled = false)
|
||||
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
||||
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled) {
|
||||
VkProgramObject::s_device = device;
|
||||
}
|
||||
~ProgramFactory() = default;
|
||||
@@ -201,6 +203,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 m_maxBindings = 0;
|
||||
UnorderedMap<HashType, VkProgramObject> m_cache;
|
||||
const VulkanRendererConfig& m_config;
|
||||
// True when the device enabled shaderDrawParameters; gates the InstanceIndex rebase pass
|
||||
// (which needs the DrawParameters capability / gl_BaseInstance builtin).
|
||||
Bool m_shaderDrawParametersEnabled = false;
|
||||
mutable ProgramLookupCache m_lastLookup;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
|
||||
@@ -133,11 +133,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_copyProvider = provider;
|
||||
}
|
||||
|
||||
Bool VkBufferManager::IsResourceBusy(const VkBufferResource& resource) const {
|
||||
Uint64 VkBufferManager::GetCompletedSerial() const {
|
||||
const Uint64 frameCount = m_initInfo.frameCount > 0 ? m_initInfo.frameCount : 1;
|
||||
Uint64 completed = m_frameSerial > frameCount ? m_frameSerial - frameCount : 0;
|
||||
completed = std::max(completed, m_completedSerialFloor);
|
||||
return resource.lastUseSerial > completed;
|
||||
const Uint64 completed = m_frameSerial > frameCount ? m_frameSerial - frameCount : 0;
|
||||
return std::max(completed, m_completedSerialFloor);
|
||||
}
|
||||
|
||||
Bool VkBufferManager::IsResourceBusy(const VkBufferResource& resource) const {
|
||||
return resource.lastUseSerial > GetCompletedSerial();
|
||||
}
|
||||
|
||||
Bool VkBufferManager::UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data,
|
||||
|
||||
@@ -98,6 +98,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource);
|
||||
|
||||
Uint64 GetFrameSerial() const { return m_frameSerial; }
|
||||
// Highest frame serial whose GPU work is known complete; serials at or
|
||||
// below it may be considered signaled. Drives IsResourceBusy and the
|
||||
// backend GL fence objects.
|
||||
Uint64 GetCompletedSerial() const;
|
||||
// Busy = potentially referenced by GPU work that has not been fenced yet
|
||||
// (including commands recorded for the current, unsubmitted frame).
|
||||
Bool IsResourceBusy(const VkBufferResource& resource) const;
|
||||
|
||||
@@ -18,7 +18,12 @@
|
||||
#include <memory>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static constexpr VkPipelineStageFlags kGraphicsSampledReadStages = VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT;
|
||||
// Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule
|
||||
// only covers rendering commands; e.g. Flywheel's Hi-Z depth pyramid downsample samples the
|
||||
// depth attachment of the bound draw framebuffer), so sampled-read barriers must cover the
|
||||
// compute stage in addition to the graphics stages.
|
||||
static constexpr VkPipelineStageFlags kSampledReadStages =
|
||||
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||
|
||||
static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) {
|
||||
Int maxDimension = std::max<Int>(baseTexelSize.x(),
|
||||
@@ -145,7 +150,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
|
||||
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
|
||||
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
|
||||
outSrcStageMask = kGraphicsSampledReadStages;
|
||||
outSrcStageMask = kSampledReadStages;
|
||||
outSrcAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
return;
|
||||
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
|
||||
@@ -193,7 +198,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
|
||||
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
|
||||
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
|
||||
outDstStageMask = kGraphicsSampledReadStages;
|
||||
outDstStageMask = kSampledReadStages;
|
||||
outDstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
return;
|
||||
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
|
||||
@@ -925,7 +930,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
||||
kGraphicsSampledReadStages, srcAccessMask,
|
||||
kSampledReadStages, srcAccessMask,
|
||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
||||
resource->arrayLayers);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
||||
@@ -1532,7 +1537,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
uploadLayout,
|
||||
finalLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
kGraphicsSampledReadStages,
|
||||
kSampledReadStages,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
VK_ACCESS_SHADER_READ_BIT,
|
||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
||||
|
||||
@@ -1923,7 +1923,8 @@ void main() {
|
||||
|
||||
m_pipelineFactory = MakeUnique<PipelineFactory>(m_device, m_config);
|
||||
MOBILEGL_ASSERT(m_pipelineFactory != nullptr, "PipelineFactory creation failed.");
|
||||
m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings);
|
||||
m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings,
|
||||
m_shaderDrawParametersFeatureEnabled);
|
||||
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
|
||||
|
||||
m_samplerManager = MakeUnique<VkSamplerManager>();
|
||||
@@ -5416,6 +5417,41 @@ void main() {
|
||||
return frame.commandBuffer;
|
||||
}
|
||||
|
||||
Uint64 VulkanRenderer::GetCurrentFrameSerial() const {
|
||||
return m_bufferManager.GetFrameSerial();
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::IsFrameSerialComplete(Uint64 serial) const {
|
||||
return serial <= m_bufferManager.GetCompletedSerial();
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::WaitForFrameSerial(Uint64 serial, Uint64 timeoutNs) {
|
||||
(void)timeoutNs;
|
||||
if (IsFrameSerialComplete(serial)) {
|
||||
return true;
|
||||
}
|
||||
// Work recorded under the current serial has not been submitted yet
|
||||
// (submission happens in Present, on this same thread), so blocking
|
||||
// can never make progress; the caller reports a timeout instead.
|
||||
if (serial >= m_bufferManager.GetFrameSerial()) {
|
||||
return false;
|
||||
}
|
||||
if (m_device == VK_NULL_HANDLE || m_graphicsQueue == VK_NULL_HANDLE) {
|
||||
return true;
|
||||
}
|
||||
// The serial was submitted but has not been observed complete. Frame
|
||||
// fences are only waited on when their slot is reused, so the simplest
|
||||
// safe wait is to drain the graphics queue; this over-waits (bounded
|
||||
// by the in-flight frame count) but never deadlocks.
|
||||
const VkResult result = vkQueueWaitIdle(m_graphicsQueue);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E("WaitForFrameSerial: vkQueueWaitIdle returned %d", result);
|
||||
return false;
|
||||
}
|
||||
m_bufferManager.NotifyDeviceIdle();
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanRenderer::Present() {
|
||||
MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(),
|
||||
"Present, acquired image index out of range");
|
||||
|
||||
@@ -167,6 +167,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkInstance GetInstance() const;
|
||||
Bool IsDrawIndirectCountExtensionEnabled() const;
|
||||
|
||||
// GL fence support, expressed in VkBufferManager frame serials: a fence
|
||||
// captures GetCurrentFrameSerial() at creation and is signaled once
|
||||
// IsFrameSerialComplete() reports that serial complete (the same
|
||||
// busy-tracking horizon used to recycle buffer resources).
|
||||
Uint64 GetCurrentFrameSerial() const;
|
||||
Bool IsFrameSerialComplete(Uint64 serial) const;
|
||||
// Blocking wait for a submitted serial. Returns false when the serial
|
||||
// cannot complete without further submissions (it belongs to the
|
||||
// current, not-yet-presented frame) or when the wait failed.
|
||||
Bool WaitForFrameSerial(Uint64 serial, Uint64 timeoutNs);
|
||||
|
||||
void RequestSwapchainResize(Uint32 width, Uint32 height);
|
||||
void RecreateSwapchain();
|
||||
|
||||
|
||||
@@ -239,6 +239,26 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect);
|
||||
}
|
||||
|
||||
// Flywheel-style engines write GPU-copy descriptors into a FLUSH_EXPLICIT
|
||||
// persistent map and glBindBufferRange that span as an SSBO for a compute
|
||||
// dispatch WITHOUT ever flushing it (undefined per spec, but real drivers'
|
||||
// persistent maps alias GPU-visible memory, so it works there). MobileGL's
|
||||
// persistent maps alias the CPU shadow, so those bytes would never reach the
|
||||
// GPU: push every explicitly-ranged SSBO binding of such maps down right
|
||||
// before each dispatch. Whole-buffer (BindBufferBase) bindings are excluded
|
||||
// on purpose — ranges the app DID flush already arrived, and re-uploading a
|
||||
// 16MB staging ring per dispatch would be prohibitive.
|
||||
static void SyncUnflushedMappedSsboRangesForDispatch() {
|
||||
const auto pointCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage);
|
||||
for (SizeT i = 0; i < pointCount; ++i) {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, i);
|
||||
if (!point.HasExplicitRange()) continue;
|
||||
const auto& bufferObject = point.GetBoundObject();
|
||||
if (!bufferObject) continue;
|
||||
bufferObject->SyncMappedRangeForGpuRead(point.GetRange());
|
||||
}
|
||||
}
|
||||
|
||||
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
|
||||
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
|
||||
auto dispatchCompute = MG_Backend::gBackendFunctionsTable.GL.DispatchCompute;
|
||||
@@ -249,6 +269,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||
SyncUnflushedMappedSsboRangesForDispatch();
|
||||
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
||||
}
|
||||
|
||||
@@ -262,6 +283,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||
SyncUnflushedMappedSsboRangesForDispatch();
|
||||
dispatchComputeIndirect(indirect);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,56 +7,120 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "GL_Sync.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
int g_stubSyncObject = 0;
|
||||
}
|
||||
// Frontend sync object: wraps an optional backend fence handle. A null
|
||||
// backend handle (backend has no fence support, or could not create a
|
||||
// fence at call time) keeps the legacy always-signaled behavior.
|
||||
struct SyncObject {
|
||||
MG_Backend::BackendSyncHandle backendHandle = nullptr;
|
||||
GLenum condition = GL_SYNC_GPU_COMMANDS_COMPLETE;
|
||||
GLbitfield flags = 0;
|
||||
};
|
||||
|
||||
// glSync semantics not really needed right now, stubbing them out
|
||||
// Sync calls may arrive from any thread (launchers migrate the context
|
||||
// across JVM threads), so the live-object registry is mutex-guarded.
|
||||
// Entries left at process shutdown are simply dropped; their backend
|
||||
// handles die with the backend.
|
||||
std::mutex g_syncObjectsMutex;
|
||||
UnorderedMap<GLsync, SyncObject*> g_liveSyncObjects;
|
||||
|
||||
SyncObject* FindSyncObject(GLsync sync) {
|
||||
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||
const auto it = g_liveSyncObjects.find(sync);
|
||||
return it != g_liveSyncObjects.end() ? it->second : nullptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
GLsync FenceSync(GLenum condition, GLbitfield flags) {
|
||||
(void)condition;
|
||||
(void)flags;
|
||||
return reinterpret_cast<GLsync>(&g_stubSyncObject);
|
||||
auto* syncObject = new SyncObject;
|
||||
syncObject->condition = condition;
|
||||
syncObject->flags = flags;
|
||||
if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) {
|
||||
syncObject->backendHandle = backendFenceSync();
|
||||
}
|
||||
const GLsync handle = reinterpret_cast<GLsync>(syncObject);
|
||||
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||
g_liveSyncObjects[handle] = syncObject;
|
||||
return handle;
|
||||
}
|
||||
|
||||
GLboolean IsSync(GLsync sync) {
|
||||
return sync != nullptr ? GL_TRUE : GL_FALSE;
|
||||
return FindSyncObject(sync) != nullptr ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
|
||||
GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
|
||||
(void)sync;
|
||||
(void)flags;
|
||||
(void)timeout;
|
||||
return GL_ALREADY_SIGNALED;
|
||||
const auto* syncObject = FindSyncObject(sync);
|
||||
if (!syncObject) {
|
||||
return GL_WAIT_FAILED;
|
||||
}
|
||||
const auto backendClientWaitSync = MG_Backend::gBackendFunctionsTable.GL.ClientWaitSync;
|
||||
if (!backendClientWaitSync || !syncObject->backendHandle) {
|
||||
return GL_ALREADY_SIGNALED; // legacy always-signaled fallback
|
||||
}
|
||||
return backendClientWaitSync(syncObject->backendHandle, flags, timeout);
|
||||
}
|
||||
|
||||
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
|
||||
(void)sync;
|
||||
(void)flags;
|
||||
(void)timeout;
|
||||
const auto* syncObject = FindSyncObject(sync);
|
||||
if (!syncObject) {
|
||||
return;
|
||||
}
|
||||
const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync;
|
||||
if (backendWaitSync && syncObject->backendHandle) {
|
||||
backendWaitSync(syncObject->backendHandle, flags, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
void DeleteSync(GLsync sync) {
|
||||
(void)sync;
|
||||
if (sync == nullptr) {
|
||||
return; // glDeleteSync(0) is silently ignored
|
||||
}
|
||||
SyncObject* syncObject = nullptr;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||
const auto it = g_liveSyncObjects.find(sync);
|
||||
if (it == g_liveSyncObjects.end()) {
|
||||
return;
|
||||
}
|
||||
syncObject = it->second;
|
||||
g_liveSyncObjects.erase(it);
|
||||
}
|
||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
||||
if (backendDeleteSync && syncObject->backendHandle) {
|
||||
backendDeleteSync(syncObject->backendHandle);
|
||||
}
|
||||
delete syncObject;
|
||||
}
|
||||
|
||||
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values) {
|
||||
(void)sync;
|
||||
const auto* syncObject = FindSyncObject(sync);
|
||||
if (!syncObject) {
|
||||
if (length) {
|
||||
*length = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
GLint value = 0;
|
||||
switch (pname) {
|
||||
case GL_OBJECT_TYPE:
|
||||
value = GL_SYNC_FENCE;
|
||||
break;
|
||||
case GL_SYNC_STATUS:
|
||||
value = GL_SIGNALED;
|
||||
case GL_SYNC_STATUS: {
|
||||
const auto backendGetSyncStatus = MG_Backend::gBackendFunctionsTable.GL.GetSyncStatus;
|
||||
const Bool signaled = !backendGetSyncStatus || !syncObject->backendHandle ||
|
||||
backendGetSyncStatus(syncObject->backendHandle);
|
||||
value = signaled ? GL_SIGNALED : GL_UNSIGNALED;
|
||||
break;
|
||||
}
|
||||
case GL_SYNC_CONDITION:
|
||||
value = GL_SYNC_GPU_COMMANDS_COMPLETE;
|
||||
value = static_cast<GLint>(syncObject->condition);
|
||||
break;
|
||||
case GL_SYNC_FLAGS:
|
||||
value = 0;
|
||||
value = static_cast<GLint>(syncObject->flags);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "BufferObject.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
namespace {
|
||||
@@ -150,6 +151,21 @@ namespace MobileGL::MG_State::GLState {
|
||||
NotifySubData(m_mappedRange.start, m_mappedRange.end - m_mappedRange.start);
|
||||
}
|
||||
|
||||
void BufferObject::SyncMappedRangeForGpuRead(Range1D range) {
|
||||
if (!m_isMapped) return;
|
||||
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) return;
|
||||
if (!(m_mappingAccess & BufferMappingAccessBit::Write)) return;
|
||||
// Non-FLUSH_EXPLICIT persistent maps are already covered wholesale by
|
||||
// SyncPersistentMappedRange.
|
||||
if (!(m_mappingAccess & BufferMappingAccessBit::FlushExplicit)) return;
|
||||
|
||||
const SizeT start = std::max(range.start, m_mappedRange.start);
|
||||
const SizeT end = std::min({range.end, m_mappedRange.end, m_size});
|
||||
if (start >= end) return;
|
||||
|
||||
NotifySubData(start, end - start);
|
||||
}
|
||||
|
||||
void BufferObject::WritebackFromBackend(DataPtr data, SizeT atOffset) {
|
||||
MOBILEGL_ASSERT(atOffset + data.size <= m_size,
|
||||
"WritebackFromBackend out of bounds: atOffset (%zu) + data.size (%zu) > m_size (%zu)", atOffset,
|
||||
|
||||
@@ -127,6 +127,12 @@ namespace MobileGL {
|
||||
// Pushes the persistently-mapped write range to the backend; called by
|
||||
// backends at draw time (persistent maps mutate the shadow without API calls).
|
||||
void SyncPersistentMappedRange();
|
||||
// App-compat for FLUSH_EXPLICIT persistent maps: engines like Flywheel
|
||||
// write copy descriptors into the mapping and bind that range as an SSBO
|
||||
// without ever flushing it. Undefined per spec, but works on drivers whose
|
||||
// persistent maps alias GPU-visible memory. Ours alias the CPU shadow, so
|
||||
// callers push the GPU-read range down right before it is consumed.
|
||||
void SyncMappedRangeForGpuRead(Range1D range);
|
||||
// Shadow-only write used when the backend copies GPU results (e.g. ReadPixels
|
||||
// into a pixel-pack buffer) back into the frontend mirror. Does not issue a
|
||||
// backend op: the backend storage already holds these bytes.
|
||||
|
||||
@@ -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