[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:
2026-07-09 06:30:10 +00:00
co-authored by Claude Fable 5
parent 2395a6ded2
commit 139de76347
27 changed files with 973 additions and 130 deletions
+1
View File
@@ -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
+14
View File
@@ -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;
+294 -83
View File
@@ -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);
+66 -9
View File
@@ -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 {
+13 -3
View File
@@ -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;
+27 -2
View File
@@ -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);
}
+84 -20
View File
@@ -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