[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
@@ -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);