mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 21:28:32 +09:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4322427e78 | ||
|
|
203d4bce5e | ||
|
|
761114d022 | ||
|
|
9caf34d5b1 | ||
|
|
5e676b338b | ||
|
|
db01bfa3e8 | ||
|
|
cc3dcfd80e | ||
|
|
d9556ff041 | ||
|
|
39f21e52ea | ||
|
|
0e933b8f2f |
@@ -25,5 +25,3 @@ MobileGL/MG*/cmake-build*
|
||||
/android-plugin/app/src/trace/jniLibs
|
||||
/android-plugin/local.properties
|
||||
tools/trace_replay/work/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
Vendored
+1
-1
Submodule 3rdparty/glslang updated: 900b29d449...26fe5ceb45
+3
-14
@@ -194,6 +194,9 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
@@ -455,21 +458,8 @@ if (ANDROID)
|
||||
endif()
|
||||
|
||||
if (APPLE AND NOT MOBILEGL_IOS)
|
||||
# MobileGL statically embeds glslang, SPIRV-Tools, and SPIRV-Cross. When
|
||||
# this dylib is injected with DYLD_INSERT_LIBRARIES, exporting those C++
|
||||
# symbols interposes incompatible copies embedded by host libraries such
|
||||
# as shaderc. Keep only the public GL/EGL/CGL loader surface globally
|
||||
# visible; GetProcAddress can still return pointers to hidden internals.
|
||||
set(MOBILEGL_MACOS_EXPORTED_SYMBOLS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/MobileGL/MG_Impl/DyldInterpose/ExportedSymbols.txt")
|
||||
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
|
||||
"LINKER:-exported_symbols_list,${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
|
||||
set_property(TARGET ${CMAKE_PROJECT_NAME} APPEND PROPERTY
|
||||
LINK_DEPENDS "${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
|
||||
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
|
||||
"-framework Cocoa"
|
||||
"-framework CoreVideo"
|
||||
"-framework QuartzCore"
|
||||
"-framework Foundation"
|
||||
"-framework OpenGL"
|
||||
@@ -477,7 +467,6 @@ if (APPLE AND NOT MOBILEGL_IOS)
|
||||
if(TARGET ${CMAKE_PROJECT_NAME}_s)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
|
||||
"-framework Cocoa"
|
||||
"-framework CoreVideo"
|
||||
"-framework QuartzCore"
|
||||
"-framework Foundation"
|
||||
"-framework OpenGL"
|
||||
|
||||
+6
-1
@@ -14,7 +14,7 @@ namespace MobileGL::MG_Config {
|
||||
inline const String ProjectName = "MobileGL";
|
||||
inline const String CoreName = "MobileGL Core";
|
||||
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
|
||||
inline const Version CoreVersion = {26, 8, 0, "-dev", VersionType::Development};
|
||||
inline const Version CoreVersion = {26, 7, 0, "-dev", VersionType::Development};
|
||||
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
|
||||
inline const Uint64 CacheVersion = 0;
|
||||
|
||||
@@ -80,6 +80,11 @@ namespace MobileGL::MG_Config {
|
||||
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
|
||||
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
|
||||
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_QUIRK_CLIP_DISTANCE: overrides the DirectGLES quirk that lowers
|
||||
// gl_ClipDistance for Adreno's ESSL compiler (shadow Private arrays with
|
||||
// constant-index builtin flushes, dynamic-index gl_in copy loop, redeclaration
|
||||
// strip, and const struct-array LUT splitting). Auto detects Qualcomm.
|
||||
QuirkOverride ClipDistanceQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
||||
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
|
||||
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
|
||||
|
||||
@@ -135,6 +135,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
||||
features.ClipDistanceQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_CLIP_DISTANCE");
|
||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
|
||||
+4
-13
@@ -14,7 +14,6 @@
|
||||
#include <MG_State/EGLState/Core.h>
|
||||
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
@@ -38,12 +37,6 @@ namespace MobileGL {
|
||||
MGLOG_I("MobileGL closing...");
|
||||
}
|
||||
glslang::FinalizeProcess();
|
||||
// GL syncs die with their contexts, and every context is gone by the
|
||||
// time full teardown runs: drain the live-sync registry while the
|
||||
// backend function table can still release the backend handles (and
|
||||
// before a re-initialized library could pair them with the wrong
|
||||
// backend's DeleteSync).
|
||||
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
||||
MG_Backend::pActiveBackendObject.reset();
|
||||
MG_State::pGLContext.reset();
|
||||
MG_State::pEGLContext.reset();
|
||||
@@ -107,11 +100,9 @@ namespace MobileGL {
|
||||
// (EGL/WGL/CGL): initialization happens lazily on the first entry point
|
||||
// via EnsureInitialized(), and full teardown happens deterministically
|
||||
// when the last EGL display is terminated with nothing current (EGLImpl
|
||||
// calls Destroy()). There is intentionally no backend-initializing static
|
||||
// constructor, no static destructor, and no DllMain: the global singletons
|
||||
// use leak-at-exit storage (see GlobalObjects.cpp), so a process that exits
|
||||
// calls Destroy()). There is intentionally no static constructor, no
|
||||
// static destructor, and no DllMain: the global singletons use
|
||||
// leak-at-exit storage (see GlobalObjects.cpp), so a process that exits
|
||||
// without eglTerminate simply leaks them to the OS instead of running
|
||||
// backend destructors during static teardown. macOS has a lightweight
|
||||
// dyld constructor that installs NSOpenGL dispatch hooks only; full backend
|
||||
// initialization still enters here from the first hooked CGL context.
|
||||
// backend destructors during static teardown.
|
||||
} // namespace MobileGL
|
||||
|
||||
+3
-4
@@ -13,10 +13,9 @@ namespace MobileGL {
|
||||
void Initialize();
|
||||
// Thread-safe, idempotent, and re-entrant wrapper around Initialize().
|
||||
// Host layers (EGL/WGL/CGL entry points) call this lazily on first use so
|
||||
// full backend initialization never depends on ELF/DLL static constructors,
|
||||
// and so a fresh init can follow a full Destroy() (e.g. after the last
|
||||
// eglTerminate). The macOS dyld bootstrap installs only lightweight
|
||||
// NSOpenGL method hooks.
|
||||
// MobileGL's lifecycle never depends on ELF/DLL static constructors, and
|
||||
// so a fresh init can follow a full Destroy() (e.g. after the last
|
||||
// eglTerminate).
|
||||
void EnsureInitialized();
|
||||
void Destroy();
|
||||
|
||||
|
||||
@@ -220,22 +220,6 @@ namespace MobileGL {
|
||||
// and leave the query readable later.
|
||||
Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
|
||||
void (*DeleteBackendQuery)(BackendQueryHandle query);
|
||||
// GL_SAMPLES_PASSED occlusion queries (optional; null = unsupported,
|
||||
// the frontend then rejects the target). Results/deletion flow through
|
||||
// GetQueryResult64 / DeleteBackendQuery like timer queries.
|
||||
BackendQueryHandle (*BeginOcclusionQuery)();
|
||||
void (*EndOcclusionQuery)(BackendQueryHandle query);
|
||||
// Transform feedback primitive queries backed by real GPU query pools
|
||||
// (optional; null = frontend falls back to CPU accounting).
|
||||
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
|
||||
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
|
||||
// Transform feedback capture spans, for backends whose own GL/ES driver
|
||||
// performs the capture (DirectGLES). Both optional; null means the backend
|
||||
// drives capture from its draw recording instead (DirectVulkan). End is
|
||||
// called while the frontend capture state is still active, so the backend
|
||||
// can still see the capture program and buffer bindings.
|
||||
void (*BeginTransformFeedback)(GLenum primitiveMode);
|
||||
void (*EndTransformFeedback)();
|
||||
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
|
||||
};
|
||||
struct GlobalBackendFunctionsTable {
|
||||
@@ -316,21 +300,7 @@ namespace MobileGL {
|
||||
Float ViewportBoundsRangeMin = 0.0f;
|
||||
Float ViewportBoundsRangeMax = 0.0f;
|
||||
Int ViewportSubpixelBits = 0;
|
||||
// GL 4.x fragment-interpolation offset limits. These defaults are the
|
||||
// core minimums and are replaced by live GLES/Vulkan device limits.
|
||||
Float MinFragmentInterpolationOffset = -0.5f;
|
||||
// For four fractional bits the greatest required legal offset is
|
||||
// 0.5 - 2^-4 = 0.4375 (GL 4.6 table 23.70).
|
||||
Float MaxFragmentInterpolationOffset = 0.4375f;
|
||||
Int FragmentInterpolationOffsetBits = 4;
|
||||
Bool SupportsWideLines = false;
|
||||
// Whether a framebuffer whose depth and stencil attachments are distinct
|
||||
// images can be rendered to. GL only requires support when both refer to the
|
||||
// same image and lets an implementation answer GL_FRAMEBUFFER_UNSUPPORTED
|
||||
// otherwise, which is what DirectVulkan (one combined attachment) and the
|
||||
// real ES drivers behind DirectGLES both do. Defaults to true so a backend
|
||||
// that never sets it keeps the permissive behaviour.
|
||||
Bool SupportsDistinctDepthStencilAttachments = true;
|
||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
Uint32 SubgroupSize = 0;
|
||||
Uint32 SubgroupSupportedStages = 0;
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
@@ -210,12 +209,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (options & PixelFormatNormalizeOptionBit::NoDepthComponent32) {
|
||||
reasons.push_back("GL_DEPTH_COMPONENT32 native probe failed on OpenGL ES");
|
||||
}
|
||||
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||
reasons.push_back("no three-channel multisample storage format on OpenGL ES");
|
||||
}
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||
reasons.push_back("EXT_render_snorm not supported");
|
||||
}
|
||||
|
||||
String reason;
|
||||
for (SizeT i = 0; i < reasons.size(); ++i) {
|
||||
@@ -363,42 +356,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return complete;
|
||||
}
|
||||
|
||||
// Whether the driver renders to a framebuffer whose depth and stencil come from
|
||||
// two different renderbuffers. GL only requires support when both attachments are
|
||||
// the same image, and ES drivers commonly answer GL_FRAMEBUFFER_UNSUPPORTED here;
|
||||
// reporting COMPLETE from the frontend and then rendering into a framebuffer the
|
||||
// driver refuses leaves the results silently empty.
|
||||
Bool ProbeDistinctDepthStencilAttachments(const MG_External::GLESFunctionsTable& gl) {
|
||||
if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glFramebufferRenderbuffer ||
|
||||
!gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers || !gl.glGenRenderbuffers ||
|
||||
!gl.glBindRenderbuffer || !gl.glRenderbufferStorage || !gl.glDeleteRenderbuffers) {
|
||||
return true;
|
||||
}
|
||||
|
||||
GLint prevFramebuffer = 0, prevRenderbuffer = 0;
|
||||
gl.glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFramebuffer);
|
||||
gl.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer);
|
||||
|
||||
GLuint framebuffer = 0;
|
||||
GLuint renderbuffers[2] = {0, 0};
|
||||
gl.glGenFramebuffers(1, &framebuffer);
|
||||
gl.glGenRenderbuffers(2, renderbuffers);
|
||||
gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[0]);
|
||||
gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, 4, 4);
|
||||
gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[1]);
|
||||
gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, 4, 4);
|
||||
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderbuffers[0]);
|
||||
gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, renderbuffers[1]);
|
||||
const Bool supported = gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
||||
|
||||
gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(prevFramebuffer));
|
||||
gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(prevRenderbuffer));
|
||||
gl.glDeleteFramebuffers(1, &framebuffer);
|
||||
gl.glDeleteRenderbuffers(2, renderbuffers);
|
||||
return supported;
|
||||
}
|
||||
|
||||
Bool ProbeFramebufferCompletenessForRenderbuffer(const MG_External::GLESFunctionsTable& gl,
|
||||
GLuint renderbuffer,
|
||||
TextureInternalFormat format) {
|
||||
@@ -562,50 +519,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
const GLESProbeFormatInfo nativeInfo = BuildNativeProbeFormatInfo(requestedInternalFormat);
|
||||
GLESProbeFormatInfo outerFallbackInfo;
|
||||
const Bool outerHasForcedFallback =
|
||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, outerFallbackInfo);
|
||||
if (!outerHasForcedFallback) {
|
||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, outerFallbackInfo);
|
||||
GLESProbeFormatInfo fallbackInfo;
|
||||
const Bool hasForcedFallback =
|
||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, fallbackInfo);
|
||||
if (!hasForcedFallback) {
|
||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, fallbackInfo);
|
||||
}
|
||||
|
||||
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
|
||||
const auto target = static_cast<TextureTarget>(targetIndex);
|
||||
// A multisample texture can only ever be rendered into, so its storage format
|
||||
// has to stay colour-renderable; the ordinary fallback for a three-channel
|
||||
// format is a three-channel one, which ES accepts as a texture but rejects as
|
||||
// multisample storage. Recompute the fallback per target so those formats get
|
||||
// widened here and nowhere else.
|
||||
Flags<PixelFormatNormalizeOptionBit> targetOptions;
|
||||
if (IsGLESProbeMultisampleTarget(target)) {
|
||||
targetOptions |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
|
||||
targetOptions |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
}
|
||||
}
|
||||
GLESProbeFormatInfo fallbackInfo = outerFallbackInfo;
|
||||
Bool hasForcedFallback = outerHasForcedFallback;
|
||||
if (targetOptions) {
|
||||
hasForcedFallback = BuildFallbackProbeFormatInfo(
|
||||
requestedInternalFormat, forcedOptions | targetOptions, true, fallbackInfo);
|
||||
if (!hasForcedFallback) {
|
||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | targetOptions,
|
||||
false, fallbackInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// 1D, 1D-array and rectangle textures live on an ES target (see
|
||||
// TextureImpl::MapToBackendTextureTarget), so they have to be probed there too -
|
||||
// probing the desktop-only target itself always failed, which left those slots
|
||||
// of the cache empty and stopped any fallback format from being selected for
|
||||
// them (a GL_DEPTH_COMPONENT32 1D texture then got no storage at all).
|
||||
const TextureTarget probeTarget = TextureImpl::MapToBackendTextureTarget(target);
|
||||
|
||||
Bool shouldProbeFallback = hasForcedFallback;
|
||||
if (!hasForcedFallback) {
|
||||
Bool nativeRenderable = false;
|
||||
const Bool nativeCreated =
|
||||
ProbeTexture(gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
|
||||
ProbeTexture(gl, target, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
|
||||
nativeInfo.ImageType, logicalFormat, &nativeRenderable);
|
||||
if (nativeCreated) {
|
||||
AddFullFormatCaps(cache, targetIndex, formatIndex,
|
||||
@@ -620,7 +547,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (shouldProbeFallback && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL) {
|
||||
Bool fallbackRenderable = false;
|
||||
const Bool fallbackCreated =
|
||||
ProbeTexture(gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
|
||||
ProbeTexture(gl, target, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
|
||||
fallbackInfo.ImageType, logicalFormat, &fallbackRenderable);
|
||||
if (fallbackCreated) {
|
||||
if (AddCaveatFormatCaps(cache, targetIndex, formatIndex,
|
||||
@@ -636,8 +563,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
|
||||
Bool shouldProbeFallbackRenderbuffer = outerHasForcedFallback;
|
||||
if (!outerHasForcedFallback) {
|
||||
Bool shouldProbeFallbackRenderbuffer = hasForcedFallback;
|
||||
if (!hasForcedFallback) {
|
||||
const Bool nativeRenderbufferComplete =
|
||||
ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1);
|
||||
if (nativeRenderbufferComplete) {
|
||||
@@ -651,16 +578,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
shouldProbeFallbackRenderbuffer = true;
|
||||
}
|
||||
}
|
||||
if (shouldProbeFallbackRenderbuffer && outerFallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
|
||||
ProbeRenderbuffer(gl, outerFallbackInfo.InternalFormat, logicalFormat, false, 1)) {
|
||||
if (shouldProbeFallbackRenderbuffer && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
|
||||
ProbeRenderbuffer(gl, fallbackInfo.InternalFormat, logicalFormat, false, 1)) {
|
||||
if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex,
|
||||
GetRenderbufferFeatureCaps(logicalFormat))) {
|
||||
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, outerFallbackInfo);
|
||||
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, fallbackInfo);
|
||||
}
|
||||
const Int maxSamples =
|
||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, outerFallbackInfo.ImageFormat);
|
||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat);
|
||||
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
|
||||
ProbeRenderbufferSampleCounts(gl, outerFallbackInfo.InternalFormat, logicalFormat, maxSamples);
|
||||
ProbeRenderbufferSampleCounts(gl, fallbackInfo.InternalFormat, logicalFormat, maxSamples);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -676,7 +603,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
.ExtraVendor = Nullopt, // Extra vendor
|
||||
.RendererGLInfo =
|
||||
{
|
||||
.TargetGLVersion = {3, 3, 0}, // GL target version
|
||||
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
|
||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
|
||||
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
@@ -904,12 +831,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
||||
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
|
||||
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
|
||||
// Both are core from GL 3.2/3.3 on and implemented here for
|
||||
// every advertised version, but an app targeting 3.0/3.1
|
||||
// only reaches them through the extension string - the CTS
|
||||
// picks a whole different shader for draw_buffers without
|
||||
// explicit_attrib_location. DirectVulkan advertises both.
|
||||
E_GL_ARB_explicit_attrib_location, E_GL_ARB_texture_multisample,
|
||||
E_GL_ARB_shader_image_size};
|
||||
// Only advertised when the device driver actually has usable timer queries
|
||||
// (GL_EXT_disjoint_timer_query plus its entry points) and the
|
||||
@@ -1013,25 +934,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
|
||||
funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
|
||||
funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
|
||||
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
|
||||
}
|
||||
// Occlusion queries are core ES3 (independent of MOBILEGL_DISABLE_TIMERQUERY)
|
||||
// and share the handle-based result/delete entries, which must exist even
|
||||
// when the timer-query group above is disabled.
|
||||
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
|
||||
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
|
||||
// Real driver primitive counters: the frontend's CPU accounting cannot see a
|
||||
// geometry shader's amplification.
|
||||
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
|
||||
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
|
||||
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||
// Transform feedback is captured by the real ES driver rather than
|
||||
// reconstructed from the draw recording, so the frontend has to hand the
|
||||
// span boundaries over.
|
||||
funcsTable.GL.BeginTransformFeedback = XfbImpl::BeginTransformFeedback;
|
||||
funcsTable.GL.EndTransformFeedback = XfbImpl::EndTransformFeedback;
|
||||
funcsTableInitialized = true;
|
||||
}
|
||||
return funcsTable;
|
||||
@@ -1118,8 +1025,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
clampStageImageUniforms(m_GLESCapabilities.MaxFragmentImageUniforms);
|
||||
m_dynamicParameters.MaxComputeImageUniforms =
|
||||
clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms);
|
||||
m_dynamicParameters.SupportsDistinctDepthStencilAttachments =
|
||||
ProbeDistinctDepthStencilAttachments(DirectGLES::g_GLESFuncs);
|
||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||
@@ -1129,24 +1034,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
|
||||
m_dynamicParameters.ViewportBoundsRangeMax = m_GLESCapabilities.ViewportBoundsRangeMax;
|
||||
m_dynamicParameters.ViewportSubpixelBits = m_GLESCapabilities.ViewportSubpixelBits;
|
||||
m_dynamicParameters.MinFragmentInterpolationOffset =
|
||||
std::isfinite(m_GLESCapabilities.MinFragmentInterpolationOffset) &&
|
||||
m_GLESCapabilities.MinFragmentInterpolationOffset <= -0.5f
|
||||
? m_GLESCapabilities.MinFragmentInterpolationOffset
|
||||
: -0.5f;
|
||||
m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
|
||||
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
|
||||
if (m_GLESCapabilities.FragmentInterpolationOffsetBits >= 4 &&
|
||||
std::isfinite(m_GLESCapabilities.MaxFragmentInterpolationOffset)) {
|
||||
const Float requiredMaxOffset =
|
||||
0.5f - std::ldexp(1.0f, -m_GLESCapabilities.FragmentInterpolationOffsetBits);
|
||||
if (m_GLESCapabilities.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
|
||||
m_dynamicParameters.MaxFragmentInterpolationOffset =
|
||||
m_GLESCapabilities.MaxFragmentInterpolationOffset;
|
||||
m_dynamicParameters.FragmentInterpolationOffsetBits =
|
||||
m_GLESCapabilities.FragmentInterpolationOffsetBits;
|
||||
}
|
||||
}
|
||||
m_dynamicParameters.SupportsWideLines =
|
||||
m_GLESCapabilities.AliasedLineWidthRangeMax > 1.0f || m_GLESCapabilities.SmoothLineWidthRangeMax > 1.0f;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -130,16 +130,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BackendQueryHandle BeginTimeElapsedQuery();
|
||||
void EndTimeElapsedQuery(BackendQueryHandle query);
|
||||
BackendQueryHandle QueryCounterTimestamp();
|
||||
// GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) occlusion queries: core ES3, independent of
|
||||
// GL_EXT_disjoint_timer_query and of MOBILEGL_DISABLE_TIMERQUERY. Results/deletion
|
||||
// flow through GetQueryResult64/DeleteBackendQuery like the timer queries above.
|
||||
BackendQueryHandle BeginOcclusionQuery();
|
||||
void EndOcclusionQuery(BackendQueryHandle query);
|
||||
// GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED, also core ES
|
||||
// (GL_PRIMITIVES_GENERATED from ES 3.2 on). Null when the target is unavailable, in
|
||||
// which case the frontend falls back to counting primitives from the draw calls.
|
||||
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
|
||||
void EndXfbPrimitivesQuery(BackendQueryHandle query);
|
||||
Bool IsQueryResultAvailable(BackendQueryHandle query);
|
||||
// Returns true when a final value landed in *outNanoseconds (a zero for
|
||||
// null or stale-generation handles IS final: the frontend may cache it
|
||||
@@ -164,18 +154,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
|
||||
void DestroyEGLContext();
|
||||
|
||||
// Transform feedback capture spans, performed by the real ES driver. The
|
||||
// capture set is declared on the backend program at link time; the driver-side
|
||||
// begin is deferred to the first draw of the span (ES needs the capturing
|
||||
// program current and the capture buffers bound), and the end also mirrors the
|
||||
// captured bytes back into the frontend buffer shadows.
|
||||
namespace XfbImpl {
|
||||
Bool AreTransformFeedbacksSupported();
|
||||
void BeginTransformFeedback(GLenum primitiveMode);
|
||||
void EndTransformFeedback();
|
||||
void OnBackendContextDestroyed();
|
||||
} // namespace XfbImpl
|
||||
|
||||
extern MG_External::EGLFunctionsTable g_EGLFuncs;
|
||||
extern MG_External::GLESFunctionsTable g_GLESFuncs;
|
||||
extern MG_External::GLESCapabilities g_GLESCapabilities;
|
||||
|
||||
@@ -1262,15 +1262,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const auto& allAttributes = stateVAOObject->GetAllAttributes();
|
||||
for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) {
|
||||
const auto& attrib = allAttributes[attribIndex];
|
||||
const Uint32 attribBit = 1u << attribIndex;
|
||||
|
||||
// An enabled attrib with neither a buffer object nor a client pointer has no
|
||||
// source; GL tolerates the state (only draws consuming it are undefined), but
|
||||
// Adreno's ES driver memcpys the "client array" from address 0 at draw time
|
||||
// (SIGSEGV). Keep such attribs disabled on the backend VAO and re-enable them
|
||||
// the moment they gain a source - the mask-vs-current compare below triggers
|
||||
// the enable even when only the Buffer/Format versions changed.
|
||||
const Bool unsourceable = attrib.Enabled && !attrib.Buffer && attrib.Offset == 0;
|
||||
const Bool wasForceDisabled = (m_forceDisabledAttribsMask & attribBit) != 0;
|
||||
|
||||
Bool needsSyncSwitch = allAttributeVersions[attribIndex].SwitchVersion !=
|
||||
m_syncedAttributeVersions[attribIndex].SwitchVersion;
|
||||
if (needsSyncSwitch) {
|
||||
if (attrib.Enabled) {
|
||||
if (needsSyncSwitch || unsourceable != wasForceDisabled) {
|
||||
if (attrib.Enabled && !unsourceable) {
|
||||
g_GLESFuncs.glEnableVertexAttribArray(attribIndex);
|
||||
} else {
|
||||
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
|
||||
}
|
||||
}
|
||||
if (unsourceable) {
|
||||
m_forceDisabledAttribsMask |= attribBit;
|
||||
} else {
|
||||
m_forceDisabledAttribsMask &= ~attribBit;
|
||||
}
|
||||
|
||||
Bool needsSyncFormat = allAttributeVersions[attribIndex].FormatVersion !=
|
||||
m_syncedAttributeVersions[attribIndex].FormatVersion;
|
||||
@@ -1278,7 +1294,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_syncedAttributeVersions[attribIndex].BufferVersion;
|
||||
if (!needsSyncFormat && !needsSyncBuffer) continue;
|
||||
|
||||
if (unsourceable) continue;
|
||||
|
||||
// Client-side array with a non-null pointer: the pointer is uploaded and applied
|
||||
// per draw by SyncClientSideAttributesForDrawArrays.
|
||||
if (!attrib.Buffer) continue;
|
||||
|
||||
if (!BindAttributeBuffer(attrib)) {
|
||||
if (attrib.Enabled) {
|
||||
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
|
||||
m_forceDisabledAttribsMask |= attribBit;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2405,19 +2431,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
||||
});
|
||||
|
||||
// A three-channel format widened to four for a multisample target (see
|
||||
// NormalizePixelFormat) gains an alpha channel the frontend format does not have, and
|
||||
// whatever the draw that filled it wrote there is not what GL would report: a format
|
||||
// without alpha reads back as 1.0. Answer the ALPHA swizzle source with ONE so the
|
||||
// promotion stays invisible, composed with the swizzle the application asked for.
|
||||
Vec4<TextureSwizzleParam> swizzleParams = stateTextureObject->GetAllSwizzleParams();
|
||||
if (TextureImpl::BackendTextureFormatAddsAlpha(stateTextureObject->GetFormat(), targetInternal)) {
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
if (swizzleParams[channel] == TextureSwizzleParam::Alpha) {
|
||||
swizzleParams[channel] = TextureSwizzleParam::One;
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto& swizzleParams = stateTextureObject->GetAllSwizzleParams();
|
||||
if (swizzleParams != m_cacheSwizzleParams) {
|
||||
#define SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(func, glEnum) \
|
||||
if (m_cacheSwizzleParams.func != swizzleParams.func) { \
|
||||
@@ -2691,31 +2705,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool IsFixedPointFallbackReadAttachment() {
|
||||
const auto& readFBO =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
if (!readFBO) {
|
||||
return false;
|
||||
}
|
||||
const auto readBuffer = readFBO->GetReadBuffer();
|
||||
if (readBuffer < FramebufferAttachmentType::Color0 || readBuffer > FramebufferAttachmentType::Color31) {
|
||||
return false;
|
||||
}
|
||||
// Any signed-normalized attachment, not just the ones currently substituted:
|
||||
// ES has no GL_CLAMP_READ_COLOR at all, so even a natively stored SNORM buffer
|
||||
// hands back the negative half that desktop GL clamps away.
|
||||
const auto& attachmentObject = readFBO->GetAttachment(readBuffer);
|
||||
if (attachmentObject.IsTexture()) {
|
||||
const auto& textureObject = attachmentObject.GetTexture();
|
||||
return textureObject && IsSnormFormat(textureObject->GetFormat());
|
||||
}
|
||||
if (attachmentObject.IsRenderbuffer()) {
|
||||
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
|
||||
return renderbufferObject && IsSnormFormat(renderbufferObject->GetInternalFormat());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BackendFramebufferObject::SyncReadBufferToBackend(
|
||||
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject) {
|
||||
if (!stateFBOObject) {
|
||||
@@ -3218,7 +3207,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
namespace PrgramImpl {
|
||||
Uint32 g_snormFallbackClampOutputMask = 0;
|
||||
Uint g_fragColorBroadcastCount = 1;
|
||||
Uint32 g_unormFallbackClampOutputMask = 0;
|
||||
Uint g_lastUsedBackendProgramId = 0;
|
||||
StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl> g_backendProgramObjects;
|
||||
@@ -3270,10 +3258,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u",
|
||||
stateProgramObject->GetExternalIndex(), m_backendProgramId);
|
||||
m_backendProgramUsable = true;
|
||||
m_snormFallbackClampOutputMask = g_snormFallbackClampOutputMask;
|
||||
m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask;
|
||||
m_fragColorBroadcastCount = g_fragColorBroadcastCount;
|
||||
|
||||
// Detach all existing shaders
|
||||
GLint attachedCount = 0;
|
||||
@@ -3305,6 +3291,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
auto& shaderSpirvs = stateProgramObject->GetGeneratedSpirv();
|
||||
|
||||
// Adreno's ESSL compiler mishandles gl_ClipDistance (rejects redeclarations,
|
||||
// miscompiles non-constant-index writes and constant-index gl_in element reads,
|
||||
// crashes on whole-array gl_in reads) and cannot dynamically index the global
|
||||
// const struct[] LUTs SPIRV-Cross likes to emit. Gate the workarounds to
|
||||
// Qualcomm; MOBILEGL_QUIRK_CLIP_DISTANCE overrides the device detection.
|
||||
const MG_Config::QuirkOverride clipDistanceQuirkOverride =
|
||||
MG_Config::Features.ClipDistanceQuirk;
|
||||
const Bool applyClipDistanceQuirk =
|
||||
clipDistanceQuirkOverride == MG_Config::QuirkOverride::ForceOn ||
|
||||
(clipDistanceQuirkOverride == MG_Config::QuirkOverride::Auto &&
|
||||
pActiveBackendObject &&
|
||||
pActiveBackendObject->GetDynamicParameters().GpuVendor == GpuVendorKind::Qualcomm);
|
||||
|
||||
for (int index = 0; index < attachedShaders.size(); ++index) {
|
||||
auto& shader = attachedShaders[index];
|
||||
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(shader->GetShaderStage());
|
||||
@@ -3327,6 +3326,63 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
effectiveSpirv = &loweredSpirv;
|
||||
}
|
||||
|
||||
// GL 3.3 only promises undefined *values* for out-of-bounds array indexing, but
|
||||
// Adreno's ESSL compiler constant-folds a provably out-of-bounds local-array
|
||||
// index into poison that corrupts the whole shader's output. Clamp every
|
||||
// access-chain index to its declared bounds before transpiling.
|
||||
Vector<unsigned int> clampedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::ClampAccessChainIndicesForEssl(*effectiveSpirv,
|
||||
clampedSpirv) &&
|
||||
!clampedSpirv.empty()) {
|
||||
effectiveSpirv = &clampedSpirv;
|
||||
} else {
|
||||
MGLOG_W("ClampAccessChainIndicesForEssl failed, continuing with unclamped SPIR-V.");
|
||||
}
|
||||
|
||||
// SPIRV-Cross emulates 1D samplers as 2D for ES: it widens texelFetch coordinates
|
||||
// to ivec2 but keeps the ConstOffset operand scalar, which is not a valid ESSL
|
||||
// texelFetchOffset overload (Adreno rejects it). Fold the constant offset into the
|
||||
// coordinate instead (texelFetchOffset(t,P,l,o) == texelFetch(t,P+o,l)).
|
||||
Vector<unsigned int> foldedOffsetSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::FoldConstOffsetFor1DFetchForEssl(
|
||||
*effectiveSpirv, foldedOffsetSpirv) &&
|
||||
!foldedOffsetSpirv.empty()) {
|
||||
effectiveSpirv = &foldedOffsetSpirv;
|
||||
} else {
|
||||
MGLOG_W("FoldConstOffsetFor1DFetchForEssl failed, continuing with unfolded SPIR-V.");
|
||||
}
|
||||
|
||||
// Adreno quirk: shadow gl_ClipDistance in Private arrays so the transpiled
|
||||
// ESSL only writes the builtin with literal constant indices (flushed before
|
||||
// EmitVertex/return) and only reads gl_in clip distances through dynamic loop
|
||||
// indices - the shapes this driver compiles correctly. Must run after the
|
||||
// access-chain clamp above so the flush indices stay literal constants.
|
||||
Vector<unsigned int> clipDistanceSpirv;
|
||||
if (applyClipDistanceQuirk &&
|
||||
(glShaderType == GL_VERTEX_SHADER || glShaderType == GL_GEOMETRY_SHADER)) {
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerClipDistanceForEssl(
|
||||
*effectiveSpirv, clipDistanceSpirv) &&
|
||||
!clipDistanceSpirv.empty()) {
|
||||
effectiveSpirv = &clipDistanceSpirv;
|
||||
} else {
|
||||
MGLOG_W("LowerClipDistanceForEssl failed, continuing with unlowered SPIR-V.");
|
||||
}
|
||||
}
|
||||
|
||||
// Adreno quirk: split single constant-composite stores of struct arrays so
|
||||
// SPIRV-Cross does not promote them to global const struct[] LUTs, which this
|
||||
// driver cannot dynamically index ("Cannot offset into the structure").
|
||||
Vector<unsigned int> structLutSpirv;
|
||||
if (applyClipDistanceQuirk) {
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::DefeatConstStructArrayLutForEssl(
|
||||
*effectiveSpirv, structLutSpirv) &&
|
||||
!structLutSpirv.empty()) {
|
||||
effectiveSpirv = &structLutSpirv;
|
||||
} else {
|
||||
MGLOG_W("DefeatConstStructArrayLutForEssl failed, continuing with unsplit SPIR-V.");
|
||||
}
|
||||
}
|
||||
|
||||
// ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints
|
||||
// a RelaxedPrecision member as explicit "mediump" in the vertex stage and as
|
||||
// UNQUALIFIED (mediump-by-default) in the fragment stage; after
|
||||
@@ -3356,17 +3412,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
effectiveSpirv = &noperspectiveSpirv;
|
||||
}
|
||||
|
||||
// ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather
|
||||
// than approximating one. Where every use takes integer texel coordinates a
|
||||
// rectangle image is indistinguishable from a 2D one, so rewrite the type and let
|
||||
// it through; the pass declines anything it cannot convert exactly.
|
||||
Vector<unsigned int> rectLoweredSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImagesForEssl(*effectiveSpirv,
|
||||
rectLoweredSpirv) &&
|
||||
!rectLoweredSpirv.empty()) {
|
||||
effectiveSpirv = &rectLoweredSpirv;
|
||||
}
|
||||
|
||||
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
|
||||
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
||||
|
||||
@@ -3389,7 +3434,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
r.log += spvcSession.GetLastErrorString();
|
||||
r.errc = -5;
|
||||
MGLOG_E("%s", r.log.c_str());
|
||||
m_backendProgramUsable = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3397,10 +3441,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
|
||||
source = RemoveLayoutBinding(source);
|
||||
if (applyClipDistanceQuirk) {
|
||||
// Adreno rejects the gl_ClipDistance redeclaration SPIRV-Cross still emits
|
||||
// ("reserved built-in name") but accepts plain usage with
|
||||
// GL_EXT_clip_cull_distance required; drop the line, keep the #extension.
|
||||
source = RemoveClipDistanceRedeclaration(source);
|
||||
}
|
||||
source = ProcessOutColorLocations(source);
|
||||
source = ForceFlatIntegerVaryings(source, glShaderType);
|
||||
source = BroadcastLegacyFragColor(std::move(source), glShaderType, m_fragColorBroadcastCount);
|
||||
source = EmulateTextureLodBias(source);
|
||||
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
|
||||
source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType);
|
||||
source = ForceSupporterOutput(source);
|
||||
@@ -3431,7 +3479,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Vector<GLchar> log(logLength);
|
||||
g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data());
|
||||
MGLOG_E("Shader compilation failed for backend ID %u: %s", backendShaderId, log.data());
|
||||
m_backendProgramUsable = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3441,33 +3488,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("Processed shader source length: %zu", source.length());
|
||||
}
|
||||
|
||||
// Transform feedback capture runs on the real driver (see XfbImpl in
|
||||
// DirectGLES.cpp), so the capture set has to be declared on the backend
|
||||
// program before it links. SPIRV-Cross keeps user output names verbatim in
|
||||
// the transpiled ESSL (`out vec4 result_0;` stays `result_0`), so the
|
||||
// frontend's requested names carry over unchanged.
|
||||
if (stateProgramObject->GetTransformFeedbackVaryingCount() > 0 &&
|
||||
g_GLESFuncs.glTransformFeedbackVaryings != nullptr) {
|
||||
const auto& xfbVaryings = stateProgramObject->GetTransformFeedbackVaryings();
|
||||
Vector<const GLchar*> xfbNames;
|
||||
xfbNames.reserve(xfbVaryings.size());
|
||||
for (const auto& xfbVarying : xfbVaryings) {
|
||||
xfbNames.push_back(xfbVarying.name.c_str());
|
||||
}
|
||||
MGLOG_D("Declaring %zu transform feedback varyings on program %u", xfbNames.size(),
|
||||
m_backendProgramId);
|
||||
g_GLESFuncs.glTransformFeedbackVaryings(m_backendProgramId, static_cast<GLsizei>(xfbNames.size()),
|
||||
xfbNames.data(),
|
||||
stateProgramObject->GetTransformFeedbackBufferMode());
|
||||
}
|
||||
|
||||
// Link program
|
||||
MGLOG_D("Linking program %u", m_backendProgramId);
|
||||
g_GLESFuncs.glLinkProgram(m_backendProgramId);
|
||||
|
||||
GLint linkStatus;
|
||||
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_LINK_STATUS, &linkStatus);
|
||||
m_backendProgramUsable = m_backendProgramUsable && linkStatus == GL_TRUE;
|
||||
if (linkStatus != GL_TRUE) {
|
||||
GLint logLength;
|
||||
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_INFO_LOG_LENGTH, &logLength);
|
||||
@@ -3580,11 +3606,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
binding.backendLocation = backendLoc;
|
||||
binding.uniformType = uniformType;
|
||||
binding.lastAssignedUnit = -1;
|
||||
// Present only for the samplers EmulateTextureLodBias actually rewrote; the
|
||||
// pass names it after the sampler, which SPIRV-Cross preserves verbatim.
|
||||
binding.lodBiasLocation =
|
||||
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, (String(LOD_BIAS_UNIFORM_PREFIX) + name).c_str());
|
||||
binding.lastAssignedLodBias = 0.0f;
|
||||
m_samplerUniformBindings.push_back(binding);
|
||||
}
|
||||
}
|
||||
@@ -3593,18 +3614,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// glUseProgram on a program that did not link is an INVALID_OPERATION and
|
||||
// leaves the *previous* program current, so the draw would silently render
|
||||
// with an unrelated shader (KHR-GL3x.texture_size_promotion read another
|
||||
// test case's alpha that way once a sampler2DRect stage failed to
|
||||
// transpile). Bind nothing instead: the draw is then a visible no-op.
|
||||
const Uint programToBind = m_backendProgramUsable ? m_backendProgramId : 0;
|
||||
if (g_lastUsedBackendProgramId == programToBind) {
|
||||
if (g_lastUsedBackendProgramId == m_backendProgramId) {
|
||||
return;
|
||||
}
|
||||
MGLOG_D("Using program %u", programToBind);
|
||||
g_GLESFuncs.glUseProgram(programToBind);
|
||||
g_lastUsedBackendProgramId = programToBind;
|
||||
MGLOG_D("Using program %u", m_backendProgramId);
|
||||
g_GLESFuncs.glUseProgram(m_backendProgramId);
|
||||
g_lastUsedBackendProgramId = m_backendProgramId;
|
||||
}
|
||||
|
||||
void BackendProgramObjectImpl::SetBaseInstance(Uint32 baseInstance) const {
|
||||
|
||||
@@ -258,6 +258,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
private:
|
||||
Uint m_backendVAOId = 0;
|
||||
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
||||
// Attribs the frontend has Enabled but that have no source at all (no buffer object
|
||||
// and NULL client pointer). GL keeps such attribs latently enabled, but Adreno's ES
|
||||
// driver treats them as client arrays and memcpys from address 0 at draw time
|
||||
// (SIGSEGV), so they are kept disabled on the backend VAO until they gain a source.
|
||||
Uint32 m_forceDisabledAttribsMask = 0;
|
||||
Bool m_isInitialized = false;
|
||||
Uint16 m_syncedIndexBufferVersion = 0;
|
||||
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
|
||||
@@ -270,21 +275,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
namespace TextureImpl {
|
||||
inline Bool IsSupportedTextureTarget(TextureTarget target) {
|
||||
// Every desktop-only target is stored on an ES one; see MapToBackendTextureTarget.
|
||||
(void)target;
|
||||
return true;
|
||||
// Rectangle textures need non-normalized sampling ES cannot express; everything else is
|
||||
// either native or emulated (1D -> 2D with height 1, 1D array -> 2D array, see
|
||||
// MapToBackendTextureTarget). SPIRV-Cross already emits the matching ESSL samplers and
|
||||
// coordinate padding for 1D/1D-array shaders.
|
||||
return target != TextureTarget::TextureRectangle;
|
||||
}
|
||||
|
||||
// ES has none of the desktop-only targets: 1D textures are stored as 2D (height 1), 1D
|
||||
// arrays as 2D arrays (height 1, layers in depth), and rectangle textures as plain 2D -
|
||||
// they are single-level and already clamp, so only the non-normalized coordinates differ.
|
||||
// Must match the shader-side emulation: SPIRV-Cross handles 1D/1D-array itself, and
|
||||
// ShaderCompiler::LowerRectImagesForEssl rewrites rectangle images (declining any module
|
||||
// whose lookups are not integer-coordinate, which SPIRV-Cross then still rejects).
|
||||
// ES has no 1D targets: 1D textures are stored as 2D (height 1) and 1D arrays as 2D arrays
|
||||
// (height 1, layers in depth). Must match SPIRV-Cross's ES 1D-as-2D shader emulation.
|
||||
inline TextureTarget MapToBackendTextureTarget(TextureTarget target) {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
case TextureTarget::TextureRectangle:
|
||||
return TextureTarget::Texture2D;
|
||||
case TextureTarget::Texture1DArray:
|
||||
return TextureTarget::Texture2DArray;
|
||||
@@ -300,7 +302,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
inline GLenum ConvertTextureUploadTargetToBackendGLEnum(TextureUploadTarget uploadTarget) {
|
||||
switch (uploadTarget) {
|
||||
case TextureUploadTarget::Texture1D:
|
||||
case TextureUploadTarget::TextureRectangle:
|
||||
return GL_TEXTURE_2D;
|
||||
case TextureUploadTarget::Texture1DArray:
|
||||
return GL_TEXTURE_2D_ARRAY;
|
||||
@@ -442,13 +443,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
||||
g_backendFramebufferObjects;
|
||||
// True when the read buffer names a fixed-point (norm/snorm) attachment that the
|
||||
// backend actually stores in a floating-point format. GL clamps a read from a
|
||||
// fixed-point colour buffer to [0,1] (GL_CLAMP_READ_COLOR defaults to
|
||||
// GL_FIXED_ONLY); the substituted float storage would not, so the readback path
|
||||
// has to apply the clamp itself.
|
||||
Bool IsFixedPointFallbackReadAttachment();
|
||||
|
||||
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboBindVersions;
|
||||
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
|
||||
// per target: re-attaching textures or changing draw buffers on an already-bound FBO
|
||||
@@ -587,12 +581,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Int backendLocation = -1;
|
||||
GLenum uniformType = 0;
|
||||
Int lastAssignedUnit = -1;
|
||||
// Location of this sampler's emulated GL_TEXTURE_LOD_BIAS uniform
|
||||
// (PrgramImpl::EmulateTextureLodBias), -1 when the shader has none.
|
||||
// lastAssignedLodBias mirrors the value the program currently holds,
|
||||
// so an unbiased shader issues no per-draw glUniform1f at all.
|
||||
Int lodBiasLocation = -1;
|
||||
Float lastAssignedLodBias = 0.0f;
|
||||
};
|
||||
|
||||
BackendProgramObjectImpl();
|
||||
@@ -604,14 +592,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void SetDrawID(Uint32 drawId) const;
|
||||
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
||||
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
||||
// False when the last SyncToBackend could not produce a usable program (a
|
||||
// shader failed to transpile or compile, or the link itself failed). Use()
|
||||
// must not leave the previously bound program current in that case.
|
||||
Bool IsBackendProgramUsable() const { return m_backendProgramUsable; }
|
||||
Uint GetBackendGlobalUBOId() const { return m_backendGlobalUBOId; }
|
||||
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
|
||||
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
|
||||
Uint GetFragColorBroadcastCount() const { return m_fragColorBroadcastCount; }
|
||||
|
||||
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
||||
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
||||
@@ -638,11 +621,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Int m_indirectParamsBinding = -1;
|
||||
Uint32 m_snormFallbackClampOutputMask = 0;
|
||||
Uint32 m_unormFallbackClampOutputMask = 0;
|
||||
// Draw buffers a legacy gl_FragColor write has to reach (see
|
||||
// PrgramImpl::BroadcastLegacyFragColor); 1 keeps the plain single-output shader.
|
||||
Uint m_fragColorBroadcastCount = 1;
|
||||
Bool m_isInitialized = false;
|
||||
Bool m_backendProgramUsable = false;
|
||||
|
||||
Int m_globalUboBackendBlockIndex = -1;
|
||||
Int m_globalUboBackendBlockSize = 0;
|
||||
@@ -655,10 +634,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
extern Uint32 g_snormFallbackClampOutputMask;
|
||||
extern Uint32 g_unormFallbackClampOutputMask;
|
||||
// Draw buffers the current draw framebuffer enables. Like the clamp masks above it
|
||||
// is framebuffer state that the shader has to be compiled against, so a program
|
||||
// whose snapshot no longer matches is relinked.
|
||||
extern Uint g_fragColorBroadcastCount;
|
||||
// Backend id of the last glUseProgram issued through this backend; lets Use()
|
||||
// skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the
|
||||
// ES context is recreated.
|
||||
|
||||
@@ -22,9 +22,6 @@
|
||||
#include <MG_Util/Math/SmallFloat.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <regex>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
namespace {
|
||||
@@ -48,40 +45,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return options;
|
||||
}
|
||||
|
||||
Flags<PixelFormatNormalizeOptionBit>
|
||||
GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat,
|
||||
Flags<PixelFormatNormalizeOptionBit> extraOptions) {
|
||||
Flags<PixelFormatNormalizeOptionBit> GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat) {
|
||||
using namespace MG_Util::TextureFormatProcessor;
|
||||
const Flags<PixelFormatNormalizeOptionBit> forcedOptions = GetApplicablePixelFormatNormalizeOptions(
|
||||
requestedInternalFormat, GetForcedPixelFormatNormalizeOptions() | extraOptions);
|
||||
const Flags<PixelFormatNormalizeOptionBit> forcedOptions =
|
||||
GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat, GetForcedPixelFormatNormalizeOptions());
|
||||
if (forcedOptions) {
|
||||
return forcedOptions;
|
||||
}
|
||||
return GetApplicablePixelFormatNormalizeOptions(
|
||||
requestedInternalFormat, GetDriverPixelFormatNormalizeOptions() | extraOptions);
|
||||
}
|
||||
|
||||
// Multisample textures can only ever be rendered into, never uploaded to, so a fallback
|
||||
// format for them has to stay colour-renderable - a three-channel float fallback is a legal
|
||||
// ES texture format but not a legal multisample storage format. Widening to four channels
|
||||
// is safe here precisely because there is no transfer path that would have to expand
|
||||
// three-channel client data, and the alpha the draw writes for a three-channel source is
|
||||
// already the 1.0 the frontend format implies.
|
||||
Bool TargetRequiresRenderableFormat(SizeT targetIndex) {
|
||||
return targetIndex == static_cast<SizeT>(TextureTarget::Texture2DMultisample) ||
|
||||
targetIndex == static_cast<SizeT>(TextureTarget::Texture2DMultisampleArray);
|
||||
}
|
||||
|
||||
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(SizeT targetIndex) {
|
||||
Flags<PixelFormatNormalizeOptionBit> options;
|
||||
if (!TargetRequiresRenderableFormat(targetIndex)) {
|
||||
return options;
|
||||
}
|
||||
options |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||
if (!g_GLESCapabilities.SupportsRenderSnorm || !g_GLESCapabilities.SupportsNorm16Texture) {
|
||||
options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
}
|
||||
return options;
|
||||
return GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat,
|
||||
GetDriverPixelFormatNormalizeOptions());
|
||||
}
|
||||
|
||||
Bool HasCachedFormatCapability(TextureInternalFormat internalFormat,
|
||||
@@ -141,8 +113,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||
Flags<PixelFormatNormalizeOptionBit> options;
|
||||
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||
options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat,
|
||||
GetRenderTargetNormalizeOptions(targetIndex));
|
||||
options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat);
|
||||
}
|
||||
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
|
||||
}
|
||||
@@ -177,22 +148,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat) {
|
||||
return ShouldUseCaveatFormat(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
||||
}
|
||||
|
||||
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target) {
|
||||
const SizeT targetIndex =
|
||||
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
|
||||
if (!TargetRequiresRenderableFormat(targetIndex)) {
|
||||
return false;
|
||||
}
|
||||
if (pActiveBackendObject && !ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||
return false;
|
||||
}
|
||||
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||
const Flags<PixelFormatNormalizeOptionBit> options =
|
||||
GetRuntimeFallbackNormalizeOptions(requestedInternalFormat,
|
||||
GetRenderTargetNormalizeOptions(targetIndex));
|
||||
return static_cast<Bool>(options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget);
|
||||
}
|
||||
} // namespace TextureImpl
|
||||
namespace PrgramImpl {
|
||||
String ProcessOutColorLocations(const String& glslCode) {
|
||||
@@ -321,55 +276,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// The name is the marker: ShaderSourceProcessor only emits it when the source
|
||||
// wrote gl_FragColor, and such a shader can have no other output.
|
||||
static const char* const kLoweredName = "mg_FragColor";
|
||||
if (shaderType != GL_FRAGMENT_SHADER || drawBufferCount <= 1) {
|
||||
return glslCode;
|
||||
}
|
||||
static const std::regex declRegex(
|
||||
R"(layout\s*\(\s*location\s*=\s*0\s*\)\s*out\s+((?:lowp|mediump|highp)\s+)?vec4\s+mg_FragColor\s*;)");
|
||||
std::smatch declMatch;
|
||||
if (!std::regex_search(glslCode, declMatch, declRegex)) {
|
||||
return glslCode;
|
||||
}
|
||||
const String precision = declMatch[1].matched ? declMatch[1].str() : String();
|
||||
|
||||
String replicaDecls;
|
||||
String replicaCopies;
|
||||
for (Uint location = 1; location < drawBufferCount; ++location) {
|
||||
const String name = String(kLoweredName) + "_" + std::to_string(location);
|
||||
replicaDecls += "\nlayout(location = " + std::to_string(location) + ") out " + precision + "vec4 " +
|
||||
name + ";";
|
||||
replicaCopies += "\n " + name + " = " + kLoweredName + ";";
|
||||
}
|
||||
|
||||
static const std::regex mainRegex(R"(void\s+main\s*\([^)]*\)\s*\{)");
|
||||
std::smatch mainMatch;
|
||||
if (!std::regex_search(glslCode, mainMatch, mainRegex)) {
|
||||
return glslCode;
|
||||
}
|
||||
SizeT bracePos = static_cast<SizeT>(mainMatch.position(0) + mainMatch.length(0) - 1);
|
||||
Int depth = 0;
|
||||
for (SizeT pos = bracePos; pos < glslCode.size(); ++pos) {
|
||||
if (glslCode[pos] == '{') {
|
||||
++depth;
|
||||
} else if (glslCode[pos] == '}') {
|
||||
--depth;
|
||||
if (depth == 0) {
|
||||
glslCode.insert(pos, replicaCopies + "\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
glslCode.insert(static_cast<SizeT>(declMatch.position(0)) + declMatch[0].str().size(), replicaDecls);
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
@@ -437,162 +343,37 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// How a lookup carries its level of detail, and how many arguments it takes
|
||||
// before the optional bias.
|
||||
struct LodLookupForm {
|
||||
const char* name;
|
||||
Int requiredArgs; // arguments before the optional bias (implicit form)
|
||||
Int explicitLodArg; // index of the explicit LOD argument, -1 for implicit
|
||||
};
|
||||
|
||||
// texelFetch* is deliberately absent: an integer fetch names its level directly
|
||||
// and takes no LOD bias. textureGather has no bias either. textureGrad* derives
|
||||
// the LOD from gradients and offers no argument to fold a bias into, so it is
|
||||
// left alone rather than rewritten incorrectly.
|
||||
constexpr LodLookupForm LOD_LOOKUP_FORMS[] = {
|
||||
{"textureProjLodOffset", 0, 2}, {"textureProjOffset", 4, -1}, {"textureProjLod", 0, 2},
|
||||
{"textureLodOffset", 0, 2}, {"textureOffset", 3, -1}, {"textureProj", 2, -1},
|
||||
{"textureLod", 0, 2}, {"texture", 2, -1},
|
||||
};
|
||||
|
||||
// Sampler types with no mip chain, or whose GLSL lookups have no bias overload
|
||||
// at all (the array-shadow forms), so nothing can or should be folded in.
|
||||
Bool IsBiasableSamplerType(const String& samplerType) {
|
||||
if (samplerType.find("MS") != String::npos) return false; // multisample
|
||||
if (samplerType.find("Buffer") != String::npos) return false; // texture buffer
|
||||
if (samplerType.find("Rect") != String::npos) return false; // rectangle: no mips
|
||||
if (samplerType == "sampler2DArrayShadow") return false;
|
||||
if (samplerType == "samplerCubeArrayShadow") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool IsIdentifierChar(char c) { return std::isalnum(static_cast<unsigned char>(c)) || c == '_'; }
|
||||
|
||||
// Byte offsets of the top-level argument separators and of the closing paren,
|
||||
// starting from the '(' at openParen. Empty when the parentheses do not balance.
|
||||
Vector<SizeT> SplitCallArguments(const String& code, SizeT openParen) {
|
||||
Vector<SizeT> marks;
|
||||
Int depth = 0;
|
||||
for (SizeT i = openParen; i < code.size(); ++i) {
|
||||
const char c = code[i];
|
||||
if (c == '(' || c == '[') {
|
||||
++depth;
|
||||
} else if (c == ']') {
|
||||
--depth;
|
||||
} else if (c == ')') {
|
||||
--depth;
|
||||
if (depth == 0) {
|
||||
marks.push_back(i);
|
||||
return marks;
|
||||
}
|
||||
} else if (c == ',' && depth == 1) {
|
||||
marks.push_back(i);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
String EmulateTextureLodBias(const String& glslCode) {
|
||||
String RemoveClipDistanceRedeclaration(const String& glslCode) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (glslCode.find("sampler") == String::npos || glslCode.find("texture") == String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
// Adreno rejects any redeclaration of gl_ClipDistance/gl_CullDistance ("reserved
|
||||
// built-in name") even with GL_EXT_clip_cull_distance required, but accepts plain
|
||||
// usage of the builtin. Drop the desktop-style redeclaration line SPIRV-Cross
|
||||
// prints; the "#extension GL_EXT_clip_cull_distance : require" line stays.
|
||||
static const std::regex redeclarationRegex(
|
||||
R"(^\s*(?:out|in)\s+(?:(?:high|medium|low)p\s+)?float\s+gl_(?:Clip|Cull)Distance\[[0-9]+\];\s*$)");
|
||||
|
||||
// Collect the mip-capable sampler uniforms this shader declares.
|
||||
static const std::regex samplerDeclRegex(
|
||||
R"(uniform\s+(?:(?:highp|mediump|lowp)\s+)?([iu]?sampler[A-Za-z0-9]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*;)");
|
||||
UnorderedMap<String, String> samplerNames; // name -> bias uniform name
|
||||
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), samplerDeclRegex), end; it != end; ++it) {
|
||||
const String samplerType = (*it)[1].str();
|
||||
if (!IsBiasableSamplerType(samplerType)) continue;
|
||||
const String name = (*it)[2].str();
|
||||
samplerNames.emplace(name, String(LOD_BIAS_UNIFORM_PREFIX) + name);
|
||||
}
|
||||
if (samplerNames.empty()) {
|
||||
return glslCode;
|
||||
}
|
||||
String result;
|
||||
result.reserve(glslCode.size());
|
||||
SizeT lineStart = 0;
|
||||
Bool firstLine = true;
|
||||
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);
|
||||
|
||||
// Rewrite the lookups. Right-to-left so earlier offsets stay valid, and only for
|
||||
// samplers named directly as the first argument (SPIRV-Cross never produces an
|
||||
// expression there for ES output, which has no separate sampler objects).
|
||||
String result = glslCode;
|
||||
Vector<String> usedSamplers;
|
||||
for (SizeT scan = result.size(); scan-- > 0;) {
|
||||
if (result[scan] != 't') continue;
|
||||
if (scan > 0 && IsIdentifierChar(result[scan - 1])) continue;
|
||||
|
||||
const LodLookupForm* form = nullptr;
|
||||
SizeT openParen = 0;
|
||||
for (const auto& candidate : LOD_LOOKUP_FORMS) {
|
||||
const SizeT nameLength = std::strlen(candidate.name);
|
||||
if (result.compare(scan, nameLength, candidate.name) != 0) continue;
|
||||
SizeT after = result.find_first_not_of(" \t", scan + nameLength);
|
||||
if (after == String::npos || result[after] != '(') continue;
|
||||
form = &candidate;
|
||||
openParen = after;
|
||||
if (!std::regex_match(line, redeclarationRegex)) {
|
||||
if (!firstLine) {
|
||||
result += '\n';
|
||||
}
|
||||
result += line;
|
||||
firstLine = false;
|
||||
}
|
||||
if (lastLine) {
|
||||
break;
|
||||
}
|
||||
if (form == nullptr) continue;
|
||||
|
||||
const Vector<SizeT> marks = SplitCallArguments(result, openParen);
|
||||
if (marks.empty()) continue;
|
||||
const SizeT argCount = marks.size();
|
||||
const SizeT closeParen = marks.back();
|
||||
|
||||
// First argument must be one of our samplers.
|
||||
const SizeT firstArgStart = result.find_first_not_of(" \t", openParen + 1);
|
||||
SizeT firstArgEnd = marks.front();
|
||||
while (firstArgEnd > firstArgStart && (result[firstArgEnd - 1] == ' ' || result[firstArgEnd - 1] == '\t')) {
|
||||
--firstArgEnd;
|
||||
}
|
||||
if (firstArgStart == String::npos || firstArgEnd <= firstArgStart) continue;
|
||||
const String samplerName = result.substr(firstArgStart, firstArgEnd - firstArgStart);
|
||||
const auto samplerIt = samplerNames.find(samplerName);
|
||||
if (samplerIt == samplerNames.end()) continue;
|
||||
|
||||
const String& biasName = samplerIt->second;
|
||||
if (form->explicitLodArg >= 0) {
|
||||
// Explicit LOD: the bias adds to it, as Vulkan does for
|
||||
// OpImageSampleExplicitLod and as the CTS reference expects.
|
||||
const SizeT lodIndex = static_cast<SizeT>(form->explicitLodArg);
|
||||
if (argCount <= lodIndex) continue;
|
||||
const SizeT lodStart = marks[lodIndex - 1] + 1;
|
||||
const SizeT lodEnd = marks[lodIndex];
|
||||
result.insert(lodEnd, String(") + ") + biasName + ")");
|
||||
result.insert(lodStart, "((");
|
||||
} else {
|
||||
const SizeT required = static_cast<SizeT>(form->requiredArgs);
|
||||
if (argCount == required) {
|
||||
result.insert(closeParen, String(", ") + biasName);
|
||||
} else if (argCount == required + 1) {
|
||||
const SizeT biasStart = marks[argCount - 2] + 1;
|
||||
result.insert(closeParen, String(") + ") + biasName + ")");
|
||||
result.insert(biasStart, "((");
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
usedSamplers.push_back(samplerName);
|
||||
}
|
||||
if (usedSamplers.empty()) {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
// Declare the bias uniforms that were actually referenced, right after the
|
||||
// sampler declaration line they belong to.
|
||||
for (const auto& samplerName : usedSamplers) {
|
||||
const String& biasName = samplerNames[samplerName];
|
||||
if (result.find(String("float ") + biasName + ";") != String::npos) continue;
|
||||
const std::regex declRegex(
|
||||
R"(uniform\s+(?:(?:highp|mediump|lowp)\s+)?[iu]?sampler[A-Za-z0-9]*\s+)" + samplerName + R"(\s*;)");
|
||||
std::smatch match;
|
||||
if (!std::regex_search(result, match, declRegex)) continue;
|
||||
const SizeT declEnd = static_cast<SizeT>(match.position(0)) + match[0].str().size();
|
||||
result.insert(declEnd, String("\nuniform highp float ") + biasName + ";");
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -40,11 +40,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void GenerateRenderbufferFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||
GLenum* outFormat, GLenum* outType);
|
||||
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);
|
||||
|
||||
// True when the format the texture is actually created with has an alpha channel the
|
||||
// frontend format does not (the three-channel multisample widening). GL reads such a
|
||||
// channel back as 1.0, so any swizzle source of ALPHA has to be answered with ONE.
|
||||
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
|
||||
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
||||
} // namespace TextureImpl
|
||||
|
||||
@@ -109,26 +104,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
|
||||
Uint32 unormOutputMask);
|
||||
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
|
||||
// Legacy GLSL's gl_FragColor is broadcast to every enabled draw buffer (GL 4.6
|
||||
// 15.2.3), but ShaderSourceProcessor lowers it to the single output mg_FragColor,
|
||||
// which only ever reaches draw buffer 0. Replicates it across `drawBufferCount`
|
||||
// outputs and copies the value into them at the end of main. A no-op for
|
||||
// drawBufferCount <= 1, i.e. for everything but a framebuffer that actually
|
||||
// enables several draw buffers, so the ordinary single-target shader is untouched.
|
||||
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
|
||||
String RemoveLayoutBinding(const String& glslCode);
|
||||
// Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
|
||||
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
|
||||
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
|
||||
// ES has no per-texture/sampler LOD bias at all (GL_TEXTURE_LOD_BIAS is desktop
|
||||
// only; Vulkan spells it VkSamplerCreateInfo::mipLodBias), so it has to reach the
|
||||
// shader as a uniform and be folded into every lookup's level of detail. Declares
|
||||
// one `uniform highp float mg_lodBias_<sampler>;` per mip-capable sampler and adds
|
||||
// it to the bias / explicit-LOD argument of every lookup that takes one. Draws push
|
||||
// the bound texture's (or sampler object's) value into it; a shader whose samplers
|
||||
// all have a zero bias is therefore unaffected. Returns the source unchanged when
|
||||
// there is nothing to rewrite.
|
||||
String EmulateTextureLodBias(const String& glslCode);
|
||||
String RemoveClipDistanceRedeclaration(const String& glslCode);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace Utils {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
||||
|
||||
#include <Config.h>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
@@ -470,9 +469,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// treat them as signaled/available with zero results from here on.
|
||||
BumpRendererGeneration();
|
||||
pVulkanRenderer.reset();
|
||||
// The reflection cache is file-scope, not renderer-owned; without this the
|
||||
// deleted programs' reflection strings survive full context teardown.
|
||||
ClearProgramResourceCaches();
|
||||
BackendObject::ReleaseEGLResources();
|
||||
}
|
||||
|
||||
@@ -482,9 +478,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// treat them as signaled/available with zero results from here on.
|
||||
BumpRendererGeneration();
|
||||
pVulkanRenderer.reset();
|
||||
// The reflection cache is file-scope, not renderer-owned; without this the
|
||||
// deleted programs' reflection strings survive full context teardown.
|
||||
ClearProgramResourceCaches();
|
||||
}
|
||||
|
||||
const RendererInfo& BackendObject_DirectVulkan::GetRendererInfo() const {
|
||||
@@ -526,11 +519,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
||||
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
||||
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
|
||||
E_GL_ARB_texture_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
||||
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
||||
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug,
|
||||
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
|
||||
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
|
||||
E_GL_ARB_explicit_attrib_location};
|
||||
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size};
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||
}
|
||||
@@ -636,15 +628,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
|
||||
}
|
||||
// Occlusion queries share the handle-based result/delete entries, which must
|
||||
// exist even when timer queries are disabled.
|
||||
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
|
||||
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
|
||||
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
|
||||
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
|
||||
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||
funcsTableInitialized = true;
|
||||
}
|
||||
return funcsTable;
|
||||
@@ -807,23 +790,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
|
||||
m_dynamicParameters.ViewportBoundsRangeMax = m_vulkanCaps.ViewportBoundsRangeMax;
|
||||
m_dynamicParameters.ViewportSubpixelBits = m_vulkanCaps.ViewportSubpixelBits;
|
||||
m_dynamicParameters.MinFragmentInterpolationOffset =
|
||||
std::isfinite(m_vulkanCaps.MinFragmentInterpolationOffset) &&
|
||||
m_vulkanCaps.MinFragmentInterpolationOffset <= -0.5f
|
||||
? m_vulkanCaps.MinFragmentInterpolationOffset
|
||||
: -0.5f;
|
||||
m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
|
||||
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
|
||||
if (m_vulkanCaps.FragmentInterpolationOffsetBits >= 4 &&
|
||||
std::isfinite(m_vulkanCaps.MaxFragmentInterpolationOffset)) {
|
||||
const Float requiredMaxOffset =
|
||||
0.5f - std::ldexp(1.0f, -m_vulkanCaps.FragmentInterpolationOffsetBits);
|
||||
if (m_vulkanCaps.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
|
||||
m_dynamicParameters.MaxFragmentInterpolationOffset = m_vulkanCaps.MaxFragmentInterpolationOffset;
|
||||
m_dynamicParameters.FragmentInterpolationOffsetBits =
|
||||
m_vulkanCaps.FragmentInterpolationOffsetBits;
|
||||
}
|
||||
}
|
||||
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
|
||||
m_dynamicParameters.MaxShaderStorageBlockSize =
|
||||
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
||||
|
||||
@@ -61,12 +61,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
|
||||
struct ProgramResourceCache {
|
||||
// Lifetime id of the program the cached reflection belongs to. GL names are
|
||||
// recycled (IndexGenerator hands freed indices straight back), and a
|
||||
// recreated program's backendStateVersion restarts at the same small values,
|
||||
// so the version alone can collide; the never-reused lifetime id makes the
|
||||
// slot's ownership unambiguous.
|
||||
Uint64 programLifetimeId = 0;
|
||||
Uint32 backendStateVersion = 0;
|
||||
Vector<StorageBlockResource> storageBlocks;
|
||||
Vector<BufferVariableResource> bufferVariables;
|
||||
@@ -88,11 +82,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 baseInstance = 0;
|
||||
};
|
||||
|
||||
// Keyed by GL program name so the freed-name reuse in IndexGenerator bounds the
|
||||
// map at the peak-simultaneous-program high-water mark; each slot's ownership is
|
||||
// checked against the program's lifetime id before it is served (see
|
||||
// GetProgramResourceCache). Cleared wholesale at EGL teardown via
|
||||
// ClearProgramResourceCaches.
|
||||
UnorderedMap<GLuint, ProgramResourceCache> g_programResourceCaches;
|
||||
|
||||
void ClearReadPixelsOutput(GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||
@@ -153,19 +142,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
ProgramResourceCache& GetProgramResourceCache(const MG_State::GLState::ProgramObject& program) {
|
||||
auto& cache = g_programResourceCaches[program.GetExternalIndex()];
|
||||
const Uint64 programLifetimeId = program.GetLifetimeId();
|
||||
const Uint32 backendStateVersion = program.GetBackendStateVersion();
|
||||
// The lifetime id must match too: a new program that reuses a deleted
|
||||
// program's name and happens to land on the same backendStateVersion (both
|
||||
// count from zero) would otherwise be served the dead program's reflection.
|
||||
if (cache.programLifetimeId == programLifetimeId &&
|
||||
cache.backendStateVersion == backendStateVersion &&
|
||||
if (cache.backendStateVersion == backendStateVersion &&
|
||||
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
cache = {};
|
||||
cache.programLifetimeId = programLifetimeId;
|
||||
cache.backendStateVersion = backendStateVersion;
|
||||
|
||||
Vector<SpvReflectShaderModule> modules;
|
||||
@@ -383,15 +366,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void ClearProgramResourceCaches() {
|
||||
// Called from EGL teardown while the backend's m_eglStateMutex is held; GL
|
||||
// calls are serialized in this codebase (contexts migrate threads but never
|
||||
// run concurrently), so no other thread can be inside the unsynchronized map.
|
||||
// Live programs in another context self-heal: their entry rebuilds from the
|
||||
// retained generated SPIR-V on the next resource query.
|
||||
g_programResourceCaches.clear();
|
||||
}
|
||||
|
||||
GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
||||
auto& cache = GetProgramResourceCache(program);
|
||||
const auto it = std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(),
|
||||
@@ -1250,76 +1224,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
pVulkanRenderer->Clear(mask);
|
||||
}
|
||||
|
||||
// Vulkan has no LINE_LOOP topology; rewrite the draw as an indexed LINE_STRIP
|
||||
// whose synthesized index list revisits the first vertex at the end.
|
||||
static void DrawLineLoopAsIndexedStrip(const Vector<Uint32>& closedIndices, GLint basevertex) {
|
||||
DrawIndexedCmd payload{};
|
||||
payload.mode = GL_LINE_STRIP;
|
||||
payload.indexBufferView.indexType = GL_UNSIGNED_INT;
|
||||
payload.indexBufferView.indexByteOffset = reinterpret_cast<SizeT>(closedIndices.data());
|
||||
payload.indexBufferView.indexByteSize = closedIndices.size() * sizeof(Uint32);
|
||||
payload.indexBufferView.forceClientMemory = true;
|
||||
payload.params.indexCount = static_cast<Uint32>(closedIndices.size());
|
||||
payload.params.instanceCount = 1;
|
||||
payload.params.vertexOffset = basevertex;
|
||||
pVulkanRenderer->DrawElements(payload);
|
||||
}
|
||||
|
||||
// Resolve a DrawElements index list (bound element-array buffer or client
|
||||
// memory) into uint32 values with the loop-closing first index appended.
|
||||
static Bool BuildClosedLineLoopIndices(GLsizei count, GLenum type, const void* indices,
|
||||
Vector<Uint32>& outIndices) {
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0 || count < 2) {
|
||||
return false;
|
||||
}
|
||||
const Uint8* indexBytes = nullptr;
|
||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& indexBufferShared = vao.GetIndexBufferBindingSlot().GetBoundObject();
|
||||
if (indexBufferShared != nullptr) {
|
||||
const SizeT offset = reinterpret_cast<SizeT>(indices);
|
||||
const SizeT bufferSize = indexBufferShared->GetSize();
|
||||
if (indexBufferShared->MappedData() == nullptr || offset > bufferSize ||
|
||||
static_cast<SizeT>(count) * indexSize > bufferSize - offset) {
|
||||
return false;
|
||||
}
|
||||
indexBufferShared->SyncPersistentMappedRange();
|
||||
indexBytes = indexBufferShared->MappedData() + offset;
|
||||
} else {
|
||||
indexBytes = static_cast<const Uint8*>(indices);
|
||||
if (indexBytes == nullptr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
outIndices.resize(static_cast<SizeT>(count) + 1);
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
switch (indexSize) {
|
||||
case 1: outIndices[i] = indexBytes[i]; break;
|
||||
case 2: outIndices[i] = reinterpret_cast<const Uint16*>(indexBytes)[i]; break;
|
||||
default: outIndices[i] = reinterpret_cast<const Uint32*>(indexBytes)[i]; break;
|
||||
}
|
||||
}
|
||||
outIndices[count] = outIndices[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArrays called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArrays called with null GL context");
|
||||
|
||||
if (mode == GL_LINE_LOOP) {
|
||||
if (count < 2) {
|
||||
return;
|
||||
}
|
||||
Vector<Uint32> closedIndices(static_cast<SizeT>(count) + 1);
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
closedIndices[i] = static_cast<Uint32>(first + i);
|
||||
}
|
||||
closedIndices[count] = static_cast<Uint32>(first);
|
||||
DrawLineLoopAsIndexedStrip(closedIndices, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawCmd payload{};
|
||||
payload.mode = mode;
|
||||
payload.params.firstVertex = first;
|
||||
@@ -1332,14 +1240,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElements called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElements called with null GL context");
|
||||
|
||||
if (mode == GL_LINE_LOOP) {
|
||||
Vector<Uint32> closedIndices;
|
||||
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
|
||||
DrawLineLoopAsIndexedStrip(closedIndices, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
DrawIndexedCmd payload{};
|
||||
payload.mode = mode;
|
||||
payload.indexBufferView.indexType = type;
|
||||
@@ -1408,13 +1308,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsBaseVertex called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsBaseVertex called with null GL context");
|
||||
if (mode == GL_LINE_LOOP) {
|
||||
Vector<Uint32> closedIndices;
|
||||
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
|
||||
DrawLineLoopAsIndexedStrip(closedIndices, basevertex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
DrawIndexedCmd payload{};
|
||||
payload.mode = mode;
|
||||
payload.indexBufferView.indexType = type;
|
||||
@@ -1564,12 +1457,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// records are shared (SharedPtr) with the owning pool's pending list,
|
||||
// so deleting the query while results are still in flight is safe.
|
||||
struct VulkanTimerQuery {
|
||||
enum class Kind : Uint8 { Timer, Occlusion, XfbWritten, XfbGenerated };
|
||||
Kind kind = Kind::Timer;
|
||||
SharedPtr<VkTimerQueryManager::TimestampRecord> begin;
|
||||
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
|
||||
// Kind::Occlusion - pool slots recorded between Begin/End; summed at result time.
|
||||
Vector<Uint32> occlusionSlots;
|
||||
// Renderer generation the records were written under (see
|
||||
// g_rendererGeneration). A stale generation resolves as available
|
||||
// with a final zero result: the records' pool indices and frame
|
||||
@@ -1659,26 +1548,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// ever be produced, so resolve with a final 0.
|
||||
return true;
|
||||
}
|
||||
if (query->kind == VulkanTimerQuery::Kind::Occlusion) {
|
||||
Uint64 samples = 0;
|
||||
if (!pVulkanRenderer->ResolveOcclusionQueryResult(query->occlusionSlots, samples)) {
|
||||
return false;
|
||||
}
|
||||
query->occlusionSlots.clear(); // slots are recycled by the resolve
|
||||
*outNanoseconds = samples;
|
||||
return true;
|
||||
}
|
||||
if (query->kind == VulkanTimerQuery::Kind::XfbWritten ||
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated) {
|
||||
Uint64 primitives = 0;
|
||||
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots,
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated,
|
||||
primitives)) {
|
||||
return false;
|
||||
}
|
||||
*outNanoseconds = primitives;
|
||||
return true;
|
||||
}
|
||||
// With wait, mirrors ClientWaitSync: a query ended this frame cannot
|
||||
// complete until Present submits the commands, so the wait refuses to
|
||||
// block on the current unsubmitted serial. Returning false keeps the
|
||||
@@ -1711,47 +1580,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
delete static_cast<VulkanTimerQuery*>(handle);
|
||||
}
|
||||
|
||||
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BeginXfbPrimitivesQuery called with null VulkanRenderer");
|
||||
if (!pVulkanRenderer->StartXfbQueryCapture(generated ? 1u : 0u)) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* query = new VulkanTimerQuery{};
|
||||
query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten;
|
||||
query->rendererGeneration = GetRendererGeneration();
|
||||
return query;
|
||||
}
|
||||
|
||||
void EndXfbPrimitivesQuery(BackendQueryHandle handle) {
|
||||
auto* query = static_cast<VulkanTimerQuery*>(handle);
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::EndXfbPrimitivesQuery called with null VulkanRenderer");
|
||||
if (query == nullptr || query->rendererGeneration != GetRendererGeneration()) {
|
||||
return;
|
||||
}
|
||||
pVulkanRenderer->StopXfbQueryCapture(
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots);
|
||||
}
|
||||
|
||||
BackendQueryHandle BeginOcclusionQuery() {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BeginOcclusionQuery called with null VulkanRenderer");
|
||||
if (!pVulkanRenderer->StartOcclusionQueryCapture()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* query = new VulkanTimerQuery{};
|
||||
query->kind = VulkanTimerQuery::Kind::Occlusion;
|
||||
query->rendererGeneration = GetRendererGeneration();
|
||||
return query;
|
||||
}
|
||||
|
||||
void EndOcclusionQuery(BackendQueryHandle handle) {
|
||||
auto* query = static_cast<VulkanTimerQuery*>(handle);
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::EndOcclusionQuery called with null VulkanRenderer");
|
||||
if (query == nullptr || query->rendererGeneration != GetRendererGeneration()) {
|
||||
return;
|
||||
}
|
||||
pVulkanRenderer->StopOcclusionQueryCapture(query->occlusionSlots);
|
||||
}
|
||||
|
||||
Int64 GetGpuTimestampNs() {
|
||||
// Vulkan cannot synchronously sample the GPU clock: timestamps only
|
||||
// exist as vkCmdWriteTimestamp results read back later, and
|
||||
|
||||
@@ -23,12 +23,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 GetRendererGeneration();
|
||||
void BumpRendererGeneration();
|
||||
|
||||
// Drops every cached program-resource reflection entry (CPU-side strings/vectors
|
||||
// only, no Vulkan handles). Called at EGL teardown next to the renderer reset;
|
||||
// safe because GL calls are serialized in this codebase, and any still-live
|
||||
// program rebuilds its entry from the retained generated SPIR-V on demand.
|
||||
void ClearProgramResourceCaches();
|
||||
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||
@@ -123,10 +117,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// only while a live renderer exists whose device can actually time.
|
||||
Bool IsTimerQuerySupported();
|
||||
BackendQueryHandle BeginTimeElapsedQuery();
|
||||
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
|
||||
void EndXfbPrimitivesQuery(BackendQueryHandle query);
|
||||
BackendQueryHandle BeginOcclusionQuery();
|
||||
void EndOcclusionQuery(BackendQueryHandle query);
|
||||
void EndTimeElapsedQuery(BackendQueryHandle query);
|
||||
BackendQueryHandle QueryCounterTimestamp();
|
||||
Bool IsQueryResultAvailable(BackendQueryHandle query);
|
||||
|
||||
@@ -16,19 +16,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_device = device;
|
||||
m_commandPool = commandPool;
|
||||
|
||||
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
|
||||
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
|
||||
VkCommandBufferAllocateInfo allocInfo{};
|
||||
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||
allocInfo.commandPool = commandPool;
|
||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocInfo.commandBufferCount = frameCount * 2;
|
||||
allocInfo.commandBufferCount = frameCount;
|
||||
VkResult result = vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data());
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
for (Uint32 i = 0; i < frameCount; ++i) {
|
||||
m_frames[i].commandBuffer = commandBuffers[i];
|
||||
m_frames[i].preCommandBuffer = commandBuffers[frameCount + i];
|
||||
}
|
||||
|
||||
VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
|
||||
@@ -48,10 +47,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void FrameContext::Destroy(VkDevice device, VkCommandPool commandPool) {
|
||||
const Uint32 frameCount = static_cast<Uint32>(m_frames.size());
|
||||
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
|
||||
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
|
||||
for (Uint32 i = 0; i < frameCount; ++i) {
|
||||
commandBuffers[i] = m_frames[i].commandBuffer;
|
||||
commandBuffers[frameCount + i] = m_frames[i].preCommandBuffer;
|
||||
}
|
||||
|
||||
for (Uint32 i = 0; i < frameCount; ++i) {
|
||||
@@ -62,7 +60,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (auto& frame : m_frames) {
|
||||
FreeRetiredCommandBuffers(frame);
|
||||
}
|
||||
vkFreeCommandBuffers(device, commandPool, frameCount * 2, commandBuffers.data());
|
||||
vkFreeCommandBuffers(device, commandPool, frameCount, commandBuffers.data());
|
||||
}
|
||||
m_frames.clear();
|
||||
currentFrameIndex = 0;
|
||||
@@ -89,8 +87,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
currentFrameIndex = (currentFrameIndex + 1) % static_cast<Uint32>(m_frames.size());
|
||||
GetCurrent().isCommandRecording = false;
|
||||
GetCurrent().hasCommandBufferRecorded = false;
|
||||
GetCurrent().isPreCommandRecording = false;
|
||||
GetCurrent().hasPreCommandBufferRecorded = false;
|
||||
}
|
||||
|
||||
VkCommandBuffer& FrameContext::BeginCommandRecording(VkCommandBufferUsageFlags flags,
|
||||
@@ -122,41 +118,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
frame.hasCommandBufferRecorded = true;
|
||||
}
|
||||
|
||||
VkCommandBuffer FrameContext::BeginPreCommandRecording() {
|
||||
auto& frame = GetCurrent();
|
||||
if (frame.isPreCommandRecording) {
|
||||
return frame.preCommandBuffer;
|
||||
}
|
||||
MOBILEGL_ASSERT(!frame.hasPreCommandBufferRecorded,
|
||||
"BeginPreCommandRecording: a recorded pre stream is still awaiting submission");
|
||||
VK_VERIFY(vkResetCommandBuffer(frame.preCommandBuffer, 0), "BeginPreCommandRecording, vkResetCommandBuffer");
|
||||
VkCommandBufferBeginInfo beginInfo{};
|
||||
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
VK_VERIFY(vkBeginCommandBuffer(frame.preCommandBuffer, &beginInfo),
|
||||
"BeginPreCommandRecording, vkBeginCommandBuffer");
|
||||
frame.isPreCommandRecording = true;
|
||||
return frame.preCommandBuffer;
|
||||
}
|
||||
|
||||
void FrameContext::EndPreCommandRecordingIfOpen() {
|
||||
auto& frame = GetCurrent();
|
||||
if (!frame.isPreCommandRecording) {
|
||||
return;
|
||||
}
|
||||
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "EndPreCommandRecordingIfOpen, vkEndCommandBuffer");
|
||||
frame.isPreCommandRecording = false;
|
||||
frame.hasPreCommandBufferRecorded = true;
|
||||
}
|
||||
|
||||
void FrameContext::AbandonPreCommandRecording() {
|
||||
auto& frame = GetCurrent();
|
||||
if (frame.isPreCommandRecording) {
|
||||
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "AbandonPreCommandRecording, vkEndCommandBuffer");
|
||||
}
|
||||
frame.isPreCommandRecording = false;
|
||||
frame.hasPreCommandBufferRecorded = false;
|
||||
}
|
||||
|
||||
VkResult FrameContext::InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount) {
|
||||
DestroySwapchainSemaphores(device);
|
||||
if (swapchainImageCount == 0) {
|
||||
@@ -189,30 +150,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Bool FrameContext::TransitionToPresent(VkImage image, VkImageLayout oldLayout, VkImageLayout presentLayout) {
|
||||
auto& frame = GetCurrent();
|
||||
if (oldLayout == presentLayout || oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
|
||||
if (frame.hasCommandBufferRecorded || frame.isCommandRecording || oldLayout == presentLayout ||
|
||||
oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The barrier belongs in the frame's own recording. Bailing out because
|
||||
// something was already recorded (the previous behaviour) dropped the
|
||||
// transition entirely for every frame that never ran a default-framebuffer
|
||||
// render pass - the only other thing that carries the image to
|
||||
// PRESENT_SRC_KHR, via that pass's finalLayout - so the swapchain image was
|
||||
// handed to the WSI still in the layout it was acquired in.
|
||||
// A closed-but-unsubmitted buffer can only come from a submit that already
|
||||
// failed (SubmitPendingCommandBuffer leaves the flag set on error), and
|
||||
// appending to it is illegal while reopening would reset the frame's own
|
||||
// commands away. The device is gone on that path anyway - stay silent-safe
|
||||
// rather than trade a lost device for a barrier into a closed buffer.
|
||||
if (frame.hasCommandBufferRecorded) {
|
||||
MGLOG_E("TransitionToPresent: command buffer already closed; skipping the present barrier");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reopening a recording here would vkResetCommandBuffer this frame's own
|
||||
// commands away, so append to the open one and let the caller close it.
|
||||
const Bool openedRecording = !frame.isCommandRecording;
|
||||
VkCommandBuffer commandBuffer = openedRecording ? BeginCommandRecording() : frame.commandBuffer;
|
||||
auto& commandBuffer = BeginCommandRecording();
|
||||
|
||||
VkImageMemoryBarrier presentBarrier{};
|
||||
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||
@@ -231,9 +174,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
|
||||
nullptr, 0, nullptr, 1, &presentBarrier);
|
||||
|
||||
if (openedRecording) {
|
||||
EndCommandRecording();
|
||||
}
|
||||
EndCommandRecording();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -241,27 +182,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 swapchainImageIndex) const {
|
||||
const auto& frame = GetCurrent();
|
||||
MOBILEGL_ASSERT(!frame.isCommandRecording, "GetSubmitInfo called while command buffer recording is still active");
|
||||
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
|
||||
"GetSubmitInfo called while the pre-pass stream is still recording");
|
||||
AssertValidSwapchainImageIndex(swapchainImageIndex);
|
||||
SubmitInfoPacket packet{};
|
||||
packet.waitSemaphore = frame.imageAvailableSemaphore;
|
||||
packet.signalSemaphore = m_swapchainImageRenderFinishedSemaphores[swapchainImageIndex];
|
||||
|
||||
Uint32 commandBufferCount = 0;
|
||||
// The pre-pass stream executes strictly before the frame's commands.
|
||||
if (frame.hasPreCommandBufferRecorded) {
|
||||
packet.commandBuffers[commandBufferCount++] = frame.preCommandBuffer;
|
||||
}
|
||||
if (shouldSubmitCommandBuffer) {
|
||||
packet.commandBuffers[commandBufferCount++] = frame.commandBuffer;
|
||||
}
|
||||
packet.commandBuffer = frame.commandBuffer;
|
||||
|
||||
packet.submitInfo.waitSemaphoreCount = frame.imageAvailableSemaphoreConsumed ? 0U : 1U;
|
||||
packet.submitInfo.pWaitSemaphores = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitSemaphore;
|
||||
packet.submitInfo.pWaitDstStageMask = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitDstStageMask;
|
||||
packet.submitInfo.commandBufferCount = commandBufferCount;
|
||||
packet.submitInfo.pCommandBuffers = commandBufferCount > 0 ? packet.commandBuffers : nullptr;
|
||||
packet.submitInfo.commandBufferCount = shouldSubmitCommandBuffer ? 1U : 0U;
|
||||
packet.submitInfo.pCommandBuffers = shouldSubmitCommandBuffer ? &packet.commandBuffer : nullptr;
|
||||
packet.submitInfo.signalSemaphoreCount = 1;
|
||||
packet.submitInfo.pSignalSemaphores = &packet.signalSemaphore;
|
||||
return packet;
|
||||
@@ -296,21 +227,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence,
|
||||
&outImageIndex);
|
||||
// VK_SUBOPTIMAL_KHR is a success code: an image *was* acquired and
|
||||
// imageAvailableSemaphore *will* be signaled. Bailing out on it skipped both
|
||||
// the consumed-flag reset (leaving a stale "already consumed", so the next
|
||||
// submit never waited on the pending signal) and the fence reset (leaving
|
||||
// the slot's fence signaled for the next submit to reuse). Only a genuine
|
||||
// failure - VK_ERROR_OUT_OF_DATE_KHR and friends, where nothing is acquired
|
||||
// and nothing is signaled - skips the bookkeeping.
|
||||
if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
|
||||
frame.imageAvailableSemaphoreConsumed = false;
|
||||
const VkResult resetResult = vkResetFences(device, 1, &frame.imageInFlightFence);
|
||||
// Hand the acquire's own code back so the caller can schedule a rebuild.
|
||||
return resetResult == VK_SUCCESS ? result : resetResult;
|
||||
return vkResetFences(device, 1, &frame.imageInFlightFence);
|
||||
}
|
||||
|
||||
Uint32 FrameContext::GetCurrentFrameIndex() const {
|
||||
@@ -325,14 +247,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_recordingObserver = observer;
|
||||
}
|
||||
|
||||
VkResult FrameContext::RetireCurrentCommandBuffer(Bool retirePreCommandBuffer) {
|
||||
VkResult FrameContext::RetireCurrentCommandBuffer() {
|
||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE,
|
||||
"RetireCurrentCommandBuffer requires an initialized FrameContext");
|
||||
auto& frame = GetCurrent();
|
||||
MOBILEGL_ASSERT(!frame.isCommandRecording,
|
||||
"RetireCurrentCommandBuffer called while the command buffer is still recording");
|
||||
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
|
||||
"RetireCurrentCommandBuffer called while the pre-pass stream is still recording");
|
||||
|
||||
VkCommandBufferAllocateInfo allocInfo{};
|
||||
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||
@@ -340,23 +260,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocInfo.commandBufferCount = 1;
|
||||
VkCommandBuffer replacement = VK_NULL_HANDLE;
|
||||
VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
|
||||
const VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
if (retirePreCommandBuffer) {
|
||||
VkCommandBuffer preReplacement = VK_NULL_HANDLE;
|
||||
result = vkAllocateCommandBuffers(m_device, &allocInfo, &preReplacement);
|
||||
if (result != VK_SUCCESS) {
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &replacement);
|
||||
return result;
|
||||
}
|
||||
frame.retiredCommandBuffers.push_back({frame.preCommandBuffer, frame.lastSubmitIndex});
|
||||
frame.preCommandBuffer = preReplacement;
|
||||
}
|
||||
// lastSubmitIndex was just written by the renderer for the submission
|
||||
// that carried this command buffer.
|
||||
frame.retiredCommandBuffers.push_back({frame.commandBuffer, frame.lastSubmitIndex});
|
||||
frame.retiredCommandBuffers.push_back(frame.commandBuffer);
|
||||
frame.commandBuffer = replacement;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
@@ -366,40 +274,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
if (m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE) {
|
||||
for (const auto& retired : frame.retiredCommandBuffers) {
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &retired.commandBuffer);
|
||||
}
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, static_cast<Uint32>(frame.retiredCommandBuffers.size()),
|
||||
frame.retiredCommandBuffers.data());
|
||||
}
|
||||
frame.retiredCommandBuffers.clear();
|
||||
}
|
||||
|
||||
void FrameContext::FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex) {
|
||||
if (m_device == VK_NULL_HANDLE || m_commandPool == VK_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
for (auto& frame : m_frames) {
|
||||
// Retired buffers are appended in submit order, so the completed
|
||||
// ones form a prefix.
|
||||
SizeT completedCount = 0;
|
||||
while (completedCount < frame.retiredCommandBuffers.size() &&
|
||||
frame.retiredCommandBuffers[completedCount].submitIndex <= completedSubmitIndex) {
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1,
|
||||
&frame.retiredCommandBuffers[completedCount].commandBuffer);
|
||||
++completedCount;
|
||||
}
|
||||
if (completedCount > 0) {
|
||||
frame.retiredCommandBuffers.erase(frame.retiredCommandBuffers.begin(),
|
||||
frame.retiredCommandBuffers.begin() + completedCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FrameContext::FreeAllRetiredCommandBuffers() {
|
||||
for (auto& frame : m_frames) {
|
||||
FreeRetiredCommandBuffers(frame);
|
||||
}
|
||||
}
|
||||
|
||||
void FrameContext::AssertValidFrameIndex(Uint32 frameIndex) const {
|
||||
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "FrameContext index out of range");
|
||||
}
|
||||
|
||||
@@ -29,9 +29,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
|
||||
VkSemaphore signalSemaphore = VK_NULL_HANDLE;
|
||||
// [0] = pre-pass command buffer (when recorded), then the frame
|
||||
// command buffer; submitInfo.pCommandBuffers points here.
|
||||
VkCommandBuffer commandBuffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
||||
};
|
||||
|
||||
@@ -42,35 +40,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPresentInfoKHR presentInfo{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
|
||||
};
|
||||
|
||||
// A command buffer submitted mid-frame (FlushPendingCommands), tagged
|
||||
// with the submit-tracker index it was submitted under so it can be
|
||||
// freed as soon as that submission is observed complete - without
|
||||
// waiting for the slot's fence to be waited again (present-less flush
|
||||
// loops never wait it).
|
||||
struct RetiredCommandBuffer {
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
Uint64 submitIndex = 0;
|
||||
};
|
||||
|
||||
struct FrameData {
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
// Pre-pass work stream: out-of-pass commands (deferred clear
|
||||
// materialization, sampled-layout transitions) for resources the
|
||||
// frame's recording has not touched yet. Submitted immediately
|
||||
// BEFORE commandBuffer in the same vkQueueSubmit, so recording
|
||||
// into it never has to split the frame's active render pass.
|
||||
VkCommandBuffer preCommandBuffer = VK_NULL_HANDLE;
|
||||
VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE;
|
||||
VkFence imageInFlightFence = VK_NULL_HANDLE;
|
||||
Bool isCommandRecording = false;
|
||||
Bool hasCommandBufferRecorded = false;
|
||||
Bool isPreCommandRecording = false;
|
||||
Bool hasPreCommandBufferRecorded = false;
|
||||
Bool imageAvailableSemaphoreConsumed = false;
|
||||
// Command buffers submitted mid-frame (FlushPendingCommands),
|
||||
// appended in submit order; freed once their submission is known
|
||||
// complete (fence wait or completion poll).
|
||||
Vector<RetiredCommandBuffer> retiredCommandBuffers;
|
||||
// Command buffers submitted mid-frame (FlushPendingCommands) whose
|
||||
// execution is only known complete once this slot's fence has been
|
||||
// waited again; freed at that point.
|
||||
Vector<VkCommandBuffer> retiredCommandBuffers;
|
||||
// Submit-tracker index of this slot's most recent queue submission
|
||||
// (written by the renderer at submit time).
|
||||
Uint64 lastSubmitIndex = 0;
|
||||
@@ -87,14 +67,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkCommandBuffer& BeginCommandRecording(VkCommandBufferUsageFlags flags = 0,
|
||||
const VkCommandBufferInheritanceInfo* pInheritanceInfo = nullptr);
|
||||
void EndCommandRecording();
|
||||
// Lazily opens the pre-pass work stream (see FrameData::preCommandBuffer).
|
||||
VkCommandBuffer BeginPreCommandRecording();
|
||||
// Closes the pre stream if open, marking it for submission ahead of the
|
||||
// frame command buffer. Safe to call when it never opened.
|
||||
void EndPreCommandRecordingIfOpen();
|
||||
// Drops an in-progress or recorded-but-unsubmitted pre stream (dropped
|
||||
// frame recordings, swapchain recreation).
|
||||
void AbandonPreCommandRecording();
|
||||
VkResult InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount);
|
||||
void DestroySwapchainSemaphores(VkDevice device);
|
||||
Bool TransitionToPresent(VkImage image, VkImageLayout oldLayout,
|
||||
@@ -107,18 +79,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Parks the current (already ended and submitted) command buffer on the
|
||||
// slot's retired list and installs a freshly allocated one, so recording
|
||||
// can restart while the submitted buffer is still executing. Retired
|
||||
// buffers are freed after the slot's fence is next waited, or as soon
|
||||
// as their submission is observed complete.
|
||||
VkResult RetireCurrentCommandBuffer(Bool retirePreCommandBuffer = false);
|
||||
|
||||
// Frees every retired command buffer whose tagged submission index is
|
||||
// known complete. Driven by the renderer's submit tracker on completion
|
||||
// events (fence waits and non-blocking polls), so present-less flush
|
||||
// loops reclaim their buffers without any extra wait.
|
||||
void FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex);
|
||||
// Frees every slot's retired command buffers. Only valid when the
|
||||
// caller has proven every queue submission complete.
|
||||
void FreeAllRetiredCommandBuffers();
|
||||
// buffers are freed after the slot's fence is next waited.
|
||||
VkResult RetireCurrentCommandBuffer();
|
||||
|
||||
Uint32 GetCurrentFrameIndex() const;
|
||||
Uint32 GetFrameCount() const;
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
#include "PipelineFactory.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
|
||||
@@ -244,108 +243,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const HashType hash = ComputeHash(payload);
|
||||
auto it = m_cache.find(hash);
|
||||
if (it != m_cache.end()) {
|
||||
it->second.lastUsedFrame = m_frameCounter;
|
||||
return it->second.pipeline;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
VkPipeline pipeline = CreatePipeline(payload);
|
||||
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass,
|
||||
m_frameCounter});
|
||||
m_cache.emplace(hash, pipeline);
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
void PipelineFactory::DestroyAll() {
|
||||
for (auto& pair : m_cache) {
|
||||
if (pair.second.pipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, pair.second.pipeline, nullptr);
|
||||
if (pair.second != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, pair.second, nullptr);
|
||||
}
|
||||
}
|
||||
m_cache.clear();
|
||||
}
|
||||
|
||||
Uint32 PipelineFactory::OnFrameBoundary() {
|
||||
++m_frameCounter;
|
||||
|
||||
// Sweep cadence and retire age mirror VkRenderPassManager::OnPresent: an entry
|
||||
// idle for more than kRetireAgeFrames frame boundaries cannot be referenced by
|
||||
// any in-flight command buffer (frames-in-flight <= MOBILEGL_MAGMA_FRAMESINFLIGHT),
|
||||
// so immediate vkDestroyPipeline is safe. The caller must drop its "last
|
||||
// pipeline" memo when this returns non-zero: the memo can return a cached
|
||||
// handle without touching this cache, so an evicted pipeline may still be
|
||||
// memoized (present-less flush loops never reset the memo per frame).
|
||||
constexpr Uint64 kSweepInterval = 256;
|
||||
constexpr Uint64 kRetireAgeFrames = 1024;
|
||||
if ((m_frameCounter % kSweepInterval) != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Uint32 evicted = 0;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
|
||||
if (it->second.pipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
||||
}
|
||||
it = m_cache.erase(it);
|
||||
++evicted;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (evicted > 0) {
|
||||
MGLOG_D("PipelineFactory::OnFrameBoundary: evicted %u idle pipelines (%zu remain)", evicted,
|
||||
m_cache.size());
|
||||
}
|
||||
return evicted;
|
||||
}
|
||||
|
||||
Uint32 PipelineFactory::EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses) {
|
||||
if (renderPasses.empty() || m_cache.empty()) {
|
||||
return 0;
|
||||
}
|
||||
// Sorted-batch membership test keeps a mass eviction (shader-pack switch,
|
||||
// dimension exit) at one O(cache * log batch) scan instead of one full scan
|
||||
// per dying pass.
|
||||
Vector<VkRenderPass> sortedPasses = renderPasses;
|
||||
std::sort(sortedPasses.begin(), sortedPasses.end());
|
||||
Uint32 evicted = 0;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (std::binary_search(sortedPasses.begin(), sortedPasses.end(), it->second.renderPass)) {
|
||||
if (it->second.pipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
||||
}
|
||||
it = m_cache.erase(it);
|
||||
++evicted;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (evicted > 0) {
|
||||
MGLOG_D("PipelineFactory::EvictByRenderPasses: evicted %u pipelines for %zu destroyed render passes",
|
||||
evicted, sortedPasses.size());
|
||||
}
|
||||
return evicted;
|
||||
}
|
||||
|
||||
Uint32 PipelineFactory::EvictByProgramHash(HashType programHash) {
|
||||
Uint32 evicted = 0;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (it->second.programHash == programHash) {
|
||||
if (it->second.pipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
||||
}
|
||||
it = m_cache.erase(it);
|
||||
++evicted;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (evicted > 0) {
|
||||
MGLOG_D("PipelineFactory::EvictByProgramHash: evicted %u pipelines for program hash 0x%llx",
|
||||
evicted, static_cast<unsigned long long>(programHash));
|
||||
}
|
||||
return evicted;
|
||||
}
|
||||
|
||||
VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
|
||||
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
|
||||
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
|
||||
|
||||
@@ -65,26 +65,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
|
||||
void DestroyAll();
|
||||
|
||||
// Frame boundary hook: ages the pipeline cache and destroys long-unused entries
|
||||
// (their command buffers retired many frames ago), mirroring
|
||||
// VkRenderPassManager::OnPresent's sweep. Returns the number of pipelines
|
||||
// destroyed so the caller can drop any memoized VkPipeline handle.
|
||||
Uint32 OnFrameBoundary();
|
||||
// Destroys every cached pipeline hashed on one of `renderPasses`. Only safe
|
||||
// when the caller guarantees GPU idleness for them - the render-pass manager
|
||||
// calls this (via the renderer) for passes its own >1024-boundary-idle sweep
|
||||
// just evicted, and a pipeline hashed on those handles is only ever bound by
|
||||
// draws that also hit the render-pass entries. Also closes the handle-recycling
|
||||
// hazard: a recycled VkRenderPass value must never serve a stale pipeline.
|
||||
// Batched: one cache scan regardless of how many passes died in the sweep.
|
||||
// Returns the number destroyed (callers invalidate memos when non-zero).
|
||||
Uint32 EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses);
|
||||
// Destroys every cached pipeline built from the program with content hash
|
||||
// `programHash`. Called from the ProgramFactory eviction path, which proves the
|
||||
// same >1024-boundary idleness (the program's pipelines are only bound by draws
|
||||
// that stamp its factory entry). Returns the number destroyed.
|
||||
Uint32 EvictByProgramHash(HashType programHash);
|
||||
|
||||
// Driver quirk: suppress depth writes on accumulation-blended pipelines. Multi-pass
|
||||
// depth-equality rendering (a blended prepass writes depth that later passes re-test
|
||||
// with an equality-inclusive compare on the re-rasterized geometry) requires
|
||||
@@ -107,26 +87,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static Bool ShouldSuppressDepthWrite(const PipelineCreatePayload& payload);
|
||||
|
||||
private:
|
||||
struct PipelineCacheEntry {
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
// The hashed inputs the eviction paths key on: programHash ties the entry to
|
||||
// its ProgramFactory entry, renderPass records the exact handle the hash
|
||||
// folded in (the hash is one-way, so targeted eviction needs them verbatim).
|
||||
HashType programHash = 0;
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
// Frame-boundary counter value of the last GetOrCreatePipeline hit; drives
|
||||
// cache eviction (see OnFrameBoundary).
|
||||
Uint64 lastUsedFrame = 0;
|
||||
};
|
||||
|
||||
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
const VulkanRendererConfig& m_config;
|
||||
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
|
||||
UnorderedMap<HashType, PipelineCacheEntry> m_cache;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameCounter = 0;
|
||||
UnorderedMap<HashType, VkPipeline> m_cache;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline Bool s_suppressBlendedDepthWrite = false;
|
||||
};
|
||||
|
||||
@@ -923,403 +923,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ProgramFactory::CompileOptionFlags m_transformFlags;
|
||||
};
|
||||
|
||||
// Decorates the module's captured varyings for VK_EXT_transform_feedback:
|
||||
// user outputs get XfbBuffer/XfbStride/Offset directly; a captured
|
||||
// gl_Position (a gl_PerVertex member) is mirrored into a dedicated output
|
||||
// variable copied before every OpReturn, BEFORE the position fixup runs,
|
||||
// so the captured value is the shader's own (pre-remap) gl_Position.
|
||||
class XfbCaptureDecoratePass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
struct CapturedVarying {
|
||||
std::string name;
|
||||
Uint32 bufferIndex = 0;
|
||||
Uint32 offsetBytes = 0;
|
||||
};
|
||||
const char* name() const override { return "mobilegl-xfb-capture-decorate"; }
|
||||
XfbCaptureDecoratePass(Vector<CapturedVarying> varyings, Vector<Uint32> strides)
|
||||
: m_varyings(Move(varyings)), m_strides(Move(strides)) {}
|
||||
|
||||
Status Process() override {
|
||||
using namespace spvtools::opt;
|
||||
if (m_varyings.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
auto entryPointIter = get_module()->entry_points().begin();
|
||||
if (entryPointIter == get_module()->entry_points().end()) return Status::SuccessWithoutChange;
|
||||
spvtools::opt::Instruction* entryPoint = &*entryPointIter;
|
||||
const Uint32 entryFunctionId = entryPoint->GetSingleWordInOperand(1);
|
||||
|
||||
// Name -> result id map from the debug section.
|
||||
std::unordered_map<std::string, Uint32> idsByName;
|
||||
for (auto& debugInst : get_module()->debugs2()) {
|
||||
if (debugInst.opcode() != spv::Op::OpName) continue;
|
||||
idsByName[debugInst.GetInOperand(1).AsString()] = debugInst.GetSingleWordInOperand(0);
|
||||
}
|
||||
|
||||
auto* decorationManager = context()->get_decoration_mgr();
|
||||
const auto decorateForXfb = [&](Uint32 targetId, Uint32 bufferIndex, Uint32 offsetBytes) {
|
||||
const Uint32 stride = bufferIndex < m_strides.size() ? m_strides[bufferIndex] : 0;
|
||||
decorationManager->AddDecorationVal(targetId, static_cast<Uint32>(spv::Decoration::XfbBuffer),
|
||||
bufferIndex);
|
||||
decorationManager->AddDecorationVal(targetId, static_cast<Uint32>(spv::Decoration::XfbStride),
|
||||
stride);
|
||||
decorationManager->AddDecorationVal(targetId, static_cast<Uint32>(spv::Decoration::Offset),
|
||||
offsetBytes);
|
||||
};
|
||||
|
||||
Bool modified = false;
|
||||
Bool needsPositionMirror = false;
|
||||
Uint32 positionBufferIndex = 0;
|
||||
Uint32 positionOffset = 0;
|
||||
for (const auto& varying : m_varyings) {
|
||||
if (varying.name == "gl_Position") {
|
||||
needsPositionMirror = true;
|
||||
positionBufferIndex = varying.bufferIndex;
|
||||
positionOffset = varying.offsetBytes;
|
||||
continue;
|
||||
}
|
||||
const auto idIt = idsByName.find(varying.name);
|
||||
if (idIt == idsByName.end()) {
|
||||
MGLOG_E("XfbCaptureDecoratePass: no SPIR-V variable named '%s'", varying.name.c_str());
|
||||
continue;
|
||||
}
|
||||
decorateForXfb(idIt->second, varying.bufferIndex, varying.offsetBytes);
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (needsPositionMirror) {
|
||||
modified |= MirrorPositionForCapture(entryFunctionId, *entryPoint, positionBufferIndex,
|
||||
positionOffset, decorateForXfb);
|
||||
}
|
||||
|
||||
if (!modified) return Status::SuccessWithoutChange;
|
||||
|
||||
context()->AddCapability(spv::Capability::TransformFeedback);
|
||||
{
|
||||
auto executionMode = MakeUnique<spvtools::opt::Instruction>(
|
||||
context(), spv::Op::OpExecutionMode, 0, 0,
|
||||
std::initializer_list<spvtools::opt::Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {entryPoint->GetSingleWordInOperand(1)}},
|
||||
{SPV_OPERAND_TYPE_EXECUTION_MODE, {static_cast<Uint32>(spv::ExecutionMode::Xfb)}}});
|
||||
get_module()->AddExecutionMode(Move(executionMode));
|
||||
}
|
||||
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename DecorateFn>
|
||||
Bool MirrorPositionForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
|
||||
Uint32 bufferIndex, Uint32 offsetBytes, const DecorateFn& decorateForXfb) {
|
||||
const Uint32 entryPointModel = entryPoint.GetSingleWordInOperand(0);
|
||||
using namespace spvtools::opt;
|
||||
PositionTargetInfo target{};
|
||||
if (!FindPositionTarget(context(), &target)) {
|
||||
MGLOG_E("XfbCaptureDecoratePass: gl_Position capture requested but no position output found");
|
||||
return false;
|
||||
}
|
||||
if (!target.isMember) {
|
||||
// Standalone gl_Position variable: decorate it directly.
|
||||
decorateForXfb(target.variableId, bufferIndex, offsetBytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto* typeManager = context()->get_type_mgr();
|
||||
const Uint32 mirrorPointerTypeId =
|
||||
typeManager->FindPointerToType(target.vectorTypeId, spv::StorageClass::Output);
|
||||
if (mirrorPointerTypeId == 0) return false;
|
||||
|
||||
const Uint32 mirrorVariableId = context()->TakeNextId();
|
||||
auto mirrorVariable = MakeUnique<Instruction>(
|
||||
context(), spv::Op::OpVariable, mirrorPointerTypeId, mirrorVariableId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast<Uint32>(spv::StorageClass::Output)}}});
|
||||
get_module()->AddGlobalValue(Move(mirrorVariable));
|
||||
|
||||
// A free output location: past every explicitly decorated output.
|
||||
Uint32 mirrorLocation = 0;
|
||||
for (auto& annotation : get_module()->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate ||
|
||||
annotation.GetSingleWordInOperand(1) != static_cast<Uint32>(spv::Decoration::Location)) {
|
||||
continue;
|
||||
}
|
||||
mirrorLocation = std::max(mirrorLocation, annotation.GetSingleWordInOperand(2) + 1);
|
||||
}
|
||||
auto* decorationManager = context()->get_decoration_mgr();
|
||||
decorationManager->AddDecorationVal(mirrorVariableId,
|
||||
static_cast<Uint32>(spv::Decoration::Location), mirrorLocation);
|
||||
decorateForXfb(mirrorVariableId, bufferIndex, offsetBytes);
|
||||
entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {mirrorVariableId}});
|
||||
|
||||
auto* function = context()->GetFunction(entryFunctionId);
|
||||
if (function == nullptr) return false;
|
||||
const auto model = static_cast<spv::ExecutionModel>(entryPointModel);
|
||||
Bool injected = false;
|
||||
for (auto& block : *function) {
|
||||
for (auto instIter = block.begin(); instIter != block.end(); ++instIter) {
|
||||
// Geometry stages capture per emitted vertex; other stages at return.
|
||||
const Bool isInjectionSite =
|
||||
model == spv::ExecutionModel::Geometry
|
||||
? instIter->opcode() == spv::Op::OpEmitVertex
|
||||
: instIter->opcode() == spv::Op::OpReturn;
|
||||
if (!isInjectionSite) continue;
|
||||
InstructionBuilder builder(context(), &*instIter, IRContext::kAnalysisNone);
|
||||
const Uint32 memberIndexId = builder.GetUintConstantId(target.memberIndex);
|
||||
auto* access =
|
||||
builder.AddAccessChain(target.vectorPtrTypeId, target.variableId, {memberIndexId});
|
||||
if (access == nullptr) return injected;
|
||||
auto* value = builder.AddLoad(target.vectorTypeId, access->result_id());
|
||||
if (value == nullptr) return injected;
|
||||
builder.AddStore(mirrorVariableId, value->result_id());
|
||||
injected = true;
|
||||
}
|
||||
}
|
||||
return injected;
|
||||
}
|
||||
|
||||
Vector<CapturedVarying> m_varyings;
|
||||
Vector<Uint32> m_strides;
|
||||
};
|
||||
|
||||
// Adreno 650 (driver 512.502) faults the GPU on an implicit-LOD sample of a full-screen
|
||||
// colour render target: the texture unit's derivative path reads outside the image's
|
||||
// allocation even though the sampler clamps LOD to 0 and the mapping is 1:1. MobileGL's
|
||||
// own default-framebuffer blit shader works around it with textureLod, but an
|
||||
// application's shader (Minecraft's blit.fsh is `texture(InSampler, texCoord)`) cannot be
|
||||
// edited - so rewrite the sample at the SPIR-V level instead.
|
||||
//
|
||||
// The rewrite is only requested for draws whose every sampler binding is clamped to one
|
||||
// mip level, where explicit LOD 0 is exactly what the implicit form must already produce:
|
||||
// lambda' = clamp(lambda + bias, minLod, maxLod) with minLod = maxLod = 0. Bias and MinLod
|
||||
// operands are therefore dropped rather than translated.
|
||||
class ForceExplicitLod0SamplePass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "force-explicit-lod0-sample"; }
|
||||
|
||||
Status Process() override {
|
||||
Bool isFragment = false;
|
||||
for (auto& entryPoint : get_module()->entry_points()) {
|
||||
if (entryPoint.opcode() != spv::Op::OpEntryPoint) continue;
|
||||
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) ==
|
||||
spv::ExecutionModel::Fragment) {
|
||||
isFragment = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isFragment) return Status::SuccessWithoutChange;
|
||||
|
||||
// Plan first, mutate second. Materializing the LOD constant is itself a module
|
||||
// change, so it must not happen unless at least one rewrite is going to follow -
|
||||
// otherwise the pass would grow the binary while reporting SuccessWithoutChange.
|
||||
Vector<RewritePlan> plans;
|
||||
for (auto& function : *get_module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& inst : block) {
|
||||
RewritePlan plan{};
|
||||
if (PlanRewrite(&inst, plan)) plans.push_back(Move(plan));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (plans.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
const Uint32 zeroId = GetFloatZeroId();
|
||||
if (zeroId == 0) return Status::SuccessWithoutChange;
|
||||
|
||||
for (auto& plan : plans) {
|
||||
plan.operands.push_back({SPV_OPERAND_TYPE_ID, {zeroId}});
|
||||
for (auto& operand : plan.trailingOperands) {
|
||||
plan.operands.push_back(operand);
|
||||
}
|
||||
plan.instruction->SetOpcode(plan.opcode);
|
||||
plan.instruction->SetInOperands(Move(plan.operands));
|
||||
}
|
||||
// Opcodes and operand lists changed underneath every cached analysis.
|
||||
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
private:
|
||||
struct RewritePlan {
|
||||
spvtools::opt::Instruction* instruction = nullptr;
|
||||
spv::Op opcode = spv::Op::OpNop;
|
||||
// Everything up to and including the Image Operands mask; the Lod id and the
|
||||
// trailing operand values are appended once the constant exists.
|
||||
Vector<spvtools::opt::Operand> operands;
|
||||
Vector<spvtools::opt::Operand> trailingOperands;
|
||||
};
|
||||
|
||||
// Image Operands bits that may accompany an implicit-LOD sample, in the canonical
|
||||
// ascending order SPIR-V requires the operand values to appear in.
|
||||
static constexpr Uint32 kBias = 0x1;
|
||||
static constexpr Uint32 kLod = 0x2;
|
||||
static constexpr Uint32 kGrad = 0x4;
|
||||
static constexpr Uint32 kConstOffset = 0x8;
|
||||
static constexpr Uint32 kOffset = 0x10;
|
||||
static constexpr Uint32 kConstOffsets = 0x20;
|
||||
static constexpr Uint32 kSample = 0x40;
|
||||
static constexpr Uint32 kMinLod = 0x80;
|
||||
static constexpr Uint32 kKnownMask = 0xFF;
|
||||
|
||||
Uint32 GetFloatZeroId() {
|
||||
// Reuse a 32-bit float type already in the module; a shader that samples always has
|
||||
// one, and looking it up avoids depending on type-creation API details.
|
||||
Uint32 floatTypeId = 0;
|
||||
for (auto& inst : get_module()->types_values()) {
|
||||
if (inst.opcode() == spv::Op::OpTypeFloat && inst.NumInOperands() >= 1 &&
|
||||
inst.GetSingleWordInOperand(0) == 32) {
|
||||
floatTypeId = inst.result_id();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (floatTypeId == 0) return 0;
|
||||
|
||||
const auto* floatType = context()->get_type_mgr()->GetType(floatTypeId);
|
||||
if (floatType == nullptr) return 0;
|
||||
const auto zeroBits = std::bit_cast<Uint32>(0.0f);
|
||||
const auto* zeroConst = context()->get_constant_mgr()->GetConstant(floatType, {zeroBits});
|
||||
if (zeroConst == nullptr) return 0;
|
||||
auto* zeroInst = context()->get_constant_mgr()->GetDefiningInstruction(zeroConst);
|
||||
return zeroInst != nullptr ? zeroInst->result_id() : 0;
|
||||
}
|
||||
|
||||
static Bool MapOpcode(spv::Op op, spv::Op& outOpcode, Uint32& outFixedOperandCount) {
|
||||
switch (op) {
|
||||
case spv::Op::OpImageSampleImplicitLod:
|
||||
outOpcode = spv::Op::OpImageSampleExplicitLod;
|
||||
outFixedOperandCount = 2; // sampled image, coordinate
|
||||
return true;
|
||||
case spv::Op::OpImageSampleProjImplicitLod:
|
||||
outOpcode = spv::Op::OpImageSampleProjExplicitLod;
|
||||
outFixedOperandCount = 2;
|
||||
return true;
|
||||
case spv::Op::OpImageSampleDrefImplicitLod:
|
||||
outOpcode = spv::Op::OpImageSampleDrefExplicitLod;
|
||||
outFixedOperandCount = 3; // sampled image, coordinate, Dref
|
||||
return true;
|
||||
case spv::Op::OpImageSampleProjDrefImplicitLod:
|
||||
outOpcode = spv::Op::OpImageSampleProjDrefExplicitLod;
|
||||
outFixedOperandCount = 3;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Bool PlanRewrite(spvtools::opt::Instruction* inst, RewritePlan& outPlan) {
|
||||
spv::Op newOpcode = spv::Op::OpNop;
|
||||
Uint32 fixedCount = 0;
|
||||
if (!MapOpcode(inst->opcode(), newOpcode, fixedCount)) return false;
|
||||
if (inst->NumInOperands() < fixedCount) return false;
|
||||
|
||||
Uint32 mask = 0;
|
||||
Uint32 next = fixedCount;
|
||||
if (inst->NumInOperands() > fixedCount) {
|
||||
mask = inst->GetSingleWordInOperand(fixedCount);
|
||||
next = fixedCount + 1;
|
||||
}
|
||||
// An operand this pass does not model would be silently reordered or dropped, and
|
||||
// Grad cannot legally accompany an implicit-LOD sample: leave such an instruction be.
|
||||
if ((mask & ~kKnownMask) != 0 || (mask & kGrad) != 0) return false;
|
||||
|
||||
Vector<spvtools::opt::Operand> fixedOperands;
|
||||
fixedOperands.reserve(fixedCount + 1);
|
||||
for (Uint32 i = 0; i < fixedCount; ++i) {
|
||||
fixedOperands.push_back(inst->GetInOperand(i));
|
||||
}
|
||||
|
||||
// Collect the surviving operand values in the same ascending-bit order they were
|
||||
// encoded in, so the rebuilt list stays canonical.
|
||||
Uint32 keptMask = kLod;
|
||||
Vector<spvtools::opt::Operand> keptOperands;
|
||||
static constexpr Uint32 kOrderedBits[] = {kBias, kLod, kGrad, kConstOffset,
|
||||
kOffset, kConstOffsets, kSample, kMinLod};
|
||||
for (const Uint32 bit : kOrderedBits) {
|
||||
if ((mask & bit) == 0) continue;
|
||||
if (next >= inst->NumInOperands()) return false;
|
||||
const spvtools::opt::Operand value = inst->GetInOperand(next++);
|
||||
// Bias and MinLod only shift a lambda that is already clamped to 0, and any
|
||||
// original Lod is replaced by the constant the caller appends.
|
||||
if (bit == kBias || bit == kMinLod || bit == kLod) continue;
|
||||
keptMask |= bit;
|
||||
keptOperands.push_back(value);
|
||||
}
|
||||
|
||||
fixedOperands.push_back({SPV_OPERAND_TYPE_IMAGE, {keptMask}});
|
||||
outPlan.instruction = inst;
|
||||
outPlan.opcode = newOpcode;
|
||||
outPlan.operands = Move(fixedOperands);
|
||||
outPlan.trailingOperands = Move(keptOperands);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
spvtools::Optimizer::PassToken CreateForceExplicitLod0SamplePass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<ForceExplicitLod0SamplePass>());
|
||||
}
|
||||
|
||||
Bool TransformSpirvForExplicitLod0Sampling(const Vector<Uint>& input, Vector<Uint>& output) {
|
||||
if (input.empty()) {
|
||||
output.clear();
|
||||
return true;
|
||||
}
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||
spvtools::OptimizerOptions options;
|
||||
// Matches the position-fix pass: this build of spirv-tools asserts rather than
|
||||
// reporting, so validation stays off in the shipping path.
|
||||
options.set_run_validator(false);
|
||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char* message) {
|
||||
MGLOG_E("Vulkan: explicit-LOD0 pass: %s", message != nullptr ? message : "");
|
||||
});
|
||||
optimizer.RegisterPass(CreateForceExplicitLod0SamplePass());
|
||||
|
||||
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
||||
if (!success) {
|
||||
MGLOG_E("Vulkan: explicit-LOD0 sampling pass failed; keeping the original module");
|
||||
output = input;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken CreateGlToVulkanPositionFixPass(
|
||||
ProgramFactory::CompileOptionFlags transformFlags) {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
|
||||
}
|
||||
|
||||
Bool TransformSpirvForXfbCapture(const Vector<Uint>& input, Vector<Uint>& output,
|
||||
const MG_State::GLState::ProgramObject& program) {
|
||||
if (input.empty()) {
|
||||
output.clear();
|
||||
return true;
|
||||
}
|
||||
Vector<XfbCaptureDecoratePass::CapturedVarying> varyings;
|
||||
varyings.reserve(program.GetTransformFeedbackVaryingCount());
|
||||
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
|
||||
varyings.push_back({varying.name, varying.bufferIndex, varying.offsetBytes});
|
||||
}
|
||||
Vector<Uint32> strides;
|
||||
strides.reserve(program.GetTransformFeedbackBufferCount());
|
||||
for (SizeT i = 0; i < program.GetTransformFeedbackBufferCount(); ++i) {
|
||||
strides.push_back(program.GetTransformFeedbackStride(static_cast<Uint32>(i)));
|
||||
}
|
||||
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char* message) {
|
||||
MGLOG_E("Vulkan: xfb capture pass: %s", message != nullptr ? message : "");
|
||||
});
|
||||
optimizer.RegisterPass(spvtools::Optimizer::PassToken(
|
||||
MakeUnique<XfbCaptureDecoratePass>(Move(varyings), Move(strides))));
|
||||
|
||||
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
||||
if (!success) {
|
||||
MGLOG_E("Vulkan: xfb capture decoration pass failed; keeping the original module");
|
||||
output = input;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
Bool TransformSpirvForVulkanPositionFix(const Vector<Uint>& input, Vector<Uint>& output,
|
||||
ProgramFactory::CompileOptionFlags transformFlags) {
|
||||
if (input.empty()) {
|
||||
@@ -2342,17 +1950,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
auto it = m_cache.find(hash);
|
||||
if (it != m_cache.end()) {
|
||||
// Every draw/dispatch funnels through this lookup (the renderer memos only
|
||||
// skip re-hashing, never the factory lookup), so an actively-used entry is
|
||||
// stamped at least once per frame boundary and can never be aged out while
|
||||
// any in-flight command buffer still references it.
|
||||
it->second.lastUsedFrame = m_frameCounter;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
auto& entry = m_cache[hash];
|
||||
entry.hash = hash;
|
||||
entry.lastUsedFrame = m_frameCounter;
|
||||
auto& shaders = program.GetAttachedShaders();
|
||||
auto& spirv = program.GetGeneratedSpirv();
|
||||
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
||||
@@ -2365,29 +1967,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// Apply position fixup if needed
|
||||
if (fixupStage != ShaderStage::Unknown && shaders[i] && shaders[i]->GetShaderStage() == fixupStage) {
|
||||
const Vector<Uint>* fixupInput = &spv;
|
||||
Vector<Uint> xfbSpirv;
|
||||
if ((flags & ProgramFactory::CompileOptionBit::XfbCapture) &&
|
||||
program.GetTransformFeedbackVaryingCount() > 0) {
|
||||
// Decorate BEFORE the position fixup so a captured gl_Position
|
||||
// mirror copies the shader's own (pre-remap) value.
|
||||
if (TransformSpirvForXfbCapture(spv, xfbSpirv, program)) {
|
||||
fixupInput = &xfbSpirv;
|
||||
}
|
||||
}
|
||||
TransformSpirvForVulkanPositionFix(*fixupInput, moduleSpirvs[i], flags);
|
||||
TransformSpirvForVulkanPositionFix(spv, moduleSpirvs[i], flags);
|
||||
} else {
|
||||
moduleSpirvs[i] = spv;
|
||||
}
|
||||
|
||||
if ((flags & ProgramFactory::CompileOptionBit::ExplicitLod0Sampling) && shaders[i] &&
|
||||
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
|
||||
Vector<Uint> explicitLodSpirv;
|
||||
if (TransformSpirvForExplicitLod0Sampling(moduleSpirvs[i], explicitLodSpirv)) {
|
||||
moduleSpirvs[i] = Move(explicitLodSpirv);
|
||||
}
|
||||
}
|
||||
|
||||
// GL apps depend on cross-program position invariance for multi-pass equality
|
||||
// depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
|
||||
// depth its own first pass wrote); decorate Position outputs Invariant so
|
||||
@@ -2484,42 +2068,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
void ProgramFactory::OnFrameBoundary() {
|
||||
++m_frameCounter;
|
||||
|
||||
// Sweep cadence and retire age mirror VkRenderPassManager::OnPresent: an entry
|
||||
// idle for more than kRetireAgeFrames frame boundaries cannot be referenced by
|
||||
// any in-flight command buffer (frames-in-flight <= MOBILEGL_MAGMA_FRAMESINFLIGHT),
|
||||
// so its shader modules and layouts are destroyed immediately - no deferred-
|
||||
// destroy machinery needed. Eviction is content-based, never tied to
|
||||
// glDeleteProgram: the cache is content-hash-shared across GL programs, so a
|
||||
// delete-driven erase could free an entry another live program still resolves.
|
||||
// An evicted entry self-heals - the frontend program keeps its generated
|
||||
// SPIR-V, so the next GetOrCreateProgram rebuilds it (this also covers the
|
||||
// renderer's internal blit/depth-mipmap programs).
|
||||
constexpr Uint64 kSweepInterval = 256;
|
||||
constexpr Uint64 kRetireAgeFrames = 1024;
|
||||
if ((m_frameCounter % kSweepInterval) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
|
||||
const HashType hash = it->first;
|
||||
const VkDescriptorSetLayout descriptorSetLayout = it->second.descriptorSetLayout;
|
||||
MGLOG_D("ProgramFactory::OnFrameBoundary: evicting idle program entry hash=0x%llx",
|
||||
static_cast<unsigned long long>(hash));
|
||||
// erase runs ~VkProgramObject (modules/layouts destroyed); notify after
|
||||
// so an observer never observes a half-destroyed entry through a lookup.
|
||||
// Observers only need the handle values to purge their keyed caches.
|
||||
it = m_cache.erase(it);
|
||||
if (m_evictionObserver != nullptr) {
|
||||
m_evictionObserver->OnProgramEvicted(hash, descriptorSetLayout);
|
||||
}
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -42,16 +42,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SurfaceRotate90 = 1 << 2,
|
||||
SurfaceRotate180 = 1 << 3,
|
||||
SurfaceRotate270 = 1 << 4,
|
||||
// Rewrites the fragment stage's implicit-LOD image samples to explicit LOD 0.
|
||||
// Only ever set for a draw whose every sampler binding is clamped to a single mip
|
||||
// level, which makes the two forms produce identical texels (the implicit lambda is
|
||||
// clamped into [minLod, maxLod] = [0, 0] regardless of derivatives or bias).
|
||||
ExplicitLod0Sampling = 1 << 5,
|
||||
// Decorates the last vertex-processing stage's captured varyings with
|
||||
// XfbBuffer/XfbStride/Offset (VK_EXT_transform_feedback). Set only for draws
|
||||
// recorded while GL transform feedback is active, so plain draws keep the
|
||||
// undecorated variant.
|
||||
XfbCapture = 1 << 6,
|
||||
};
|
||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||
using HashType = Uint64;
|
||||
@@ -98,9 +88,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
|
||||
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
||||
Bool fragmentReplacesDepth = false;
|
||||
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
||||
// cache eviction (see OnFrameBoundary).
|
||||
Uint64 lastUsedFrame = 0;
|
||||
|
||||
static inline VkDevice s_device = VK_NULL_HANDLE;
|
||||
|
||||
@@ -137,7 +124,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
@@ -149,7 +135,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.producerOutputComponentCount = 0;
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.lastUsedFrame = 0;
|
||||
}
|
||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||
if (this == &other) {
|
||||
@@ -185,7 +170,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
@@ -197,7 +181,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.producerOutputComponentCount = 0;
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.lastUsedFrame = 0;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -227,18 +210,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
};
|
||||
|
||||
// Notified when the OnFrameBoundary sweep destroys an aged-out cache entry,
|
||||
// carrying the entry's content hash and the VkDescriptorSetLayout it owned.
|
||||
// Dependent caches (compute pipelines, PipelineFactory entries, UniformManager's
|
||||
// per-layout descriptor sets) must purge in the same step: after vkDestroy the
|
||||
// layout handle value may be recycled for an unrelated layout, and the program
|
||||
// hash may be re-inserted by a later rebuild of the same content.
|
||||
class IEvictionObserver {
|
||||
public:
|
||||
virtual ~IEvictionObserver() = default;
|
||||
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
|
||||
};
|
||||
|
||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
|
||||
Bool shaderDrawParametersEnabled = false,
|
||||
Bool unformattedFloatStorageImagesEnabled = false)
|
||||
@@ -254,13 +225,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkProgramObject& GetOrCreateProgram(
|
||||
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
||||
|
||||
// Observer may be null (no notifications). Not owned.
|
||||
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
|
||||
// Frame boundary hook: ages the program cache and evicts long-unused entries
|
||||
// (their command buffers retired many frames ago), mirroring
|
||||
// VkRenderPassManager::OnPresent's sweep.
|
||||
void OnFrameBoundary();
|
||||
|
||||
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
||||
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
||||
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
||||
@@ -302,9 +266,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||
mutable ProgramLookupCache m_lastLookup;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameCounter = 0;
|
||||
IEvictionObserver* m_evictionObserver = nullptr;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -247,11 +247,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
m_surfaceFormat = {createInfo.imageFormat, createInfo.imageColorSpace};
|
||||
m_extent = createInfo.imageExtent;
|
||||
// The surface-space extent this swapchain was built from, i.e. before the
|
||||
// quarter-turn swap above. Out-of-date checks must compare in THIS space: comparing a
|
||||
// freshly queried currentExtent against the swapped m_extent flips axes every rotation
|
||||
// and makes the comparison alternate forever.
|
||||
m_surfaceExtent = defaultFramebufferExtent;
|
||||
m_preTransform = createInfo.preTransform;
|
||||
|
||||
VK_VERIFY(vkCreateSwapchainKHR(device, &createInfo, nullptr, &m_swapchain));
|
||||
@@ -262,9 +257,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_images.resize(imageCount, VK_NULL_HANDLE);
|
||||
VK_VERIFY(vkGetSwapchainImagesKHR(device, m_swapchain, &imageCount, m_images.data()));
|
||||
m_imageLayouts.assign(imageCount, VK_IMAGE_LAYOUT_UNDEFINED);
|
||||
// Fresh swapchain images hold garbage until a render pass stores into them.
|
||||
m_imageContentDefined.assign(imageCount, false);
|
||||
m_depthStencilContentDefined.assign(imageCount, false);
|
||||
|
||||
CreateImageViews(device);
|
||||
CreateDepthStencilResources(device, physicalDevice);
|
||||
@@ -436,39 +428,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
m_images.clear();
|
||||
m_imageLayouts.clear();
|
||||
m_imageContentDefined.clear();
|
||||
m_depthStencilContentDefined.clear();
|
||||
m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
}
|
||||
|
||||
Bool SwapchainObject::IsImageContentDefined(Uint32 index) const {
|
||||
MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range");
|
||||
return m_imageContentDefined[index];
|
||||
}
|
||||
|
||||
void SwapchainObject::SetImageContentDefined(Uint32 index, Bool defined) {
|
||||
MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range");
|
||||
m_imageContentDefined[index] = defined;
|
||||
}
|
||||
|
||||
Bool SwapchainObject::IsDepthStencilContentDefined(Uint32 index) const {
|
||||
MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(),
|
||||
"Swapchain depth/stencil content index out of range");
|
||||
return m_depthStencilContentDefined[index];
|
||||
}
|
||||
|
||||
void SwapchainObject::SetDepthStencilContentDefined(Uint32 index, Bool defined) {
|
||||
MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(),
|
||||
"Swapchain depth/stencil content index out of range");
|
||||
m_depthStencilContentDefined[index] = defined;
|
||||
}
|
||||
|
||||
void SwapchainObject::SetAllDepthStencilContentUndefined() {
|
||||
for (SizeT i = 0; i < m_depthStencilContentDefined.size(); ++i) {
|
||||
m_depthStencilContentDefined[i] = false;
|
||||
}
|
||||
}
|
||||
|
||||
VkImage SwapchainObject::GetImage(Uint32 index) const {
|
||||
MOBILEGL_ASSERT(index < m_images.size(), "Swapchain image index out of range");
|
||||
return m_images[index];
|
||||
|
||||
@@ -35,9 +35,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkSwapchainKHR GetHandle() const { return m_swapchain; }
|
||||
const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; }
|
||||
VkExtent2D GetExtent() const { return m_extent; }
|
||||
// Surface-space extent (before the pre-rotation quarter-turn swap) this swapchain was
|
||||
// created from - the value to compare a freshly queried currentExtent against.
|
||||
VkExtent2D GetSurfaceExtent() const { return m_surfaceExtent; }
|
||||
VkSurfaceTransformFlagBitsKHR GetPreTransform() const { return m_preTransform; }
|
||||
const Vector<VkImage>& GetImages() const { return m_images; }
|
||||
const Vector<VkImageView>& GetImageViews() const { return m_imageViews; }
|
||||
@@ -52,21 +49,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void SetImageLayout(Uint32 index, VkImageLayout layout);
|
||||
SizeT GetImageCount() const { return m_images.size(); }
|
||||
|
||||
// EGL content-validity tracking for the default framebuffer. A color
|
||||
// buffer's content is undefined once its image has been presented
|
||||
// (EGL_BUFFER_DESTROYED swap behaviour, the implementation default),
|
||||
// and every ancillary (depth/stencil) buffer's content is undefined
|
||||
// after ANY swap regardless of swap behaviour (EGL 1.5 §3.10.1). The
|
||||
// render-pass manager turns an undefined attachment's tile load into
|
||||
// LOAD_OP_DONT_CARE. Flags start false (a fresh swapchain image holds
|
||||
// garbage) and a render pass storing into an attachment sets it back
|
||||
// to defined.
|
||||
Bool IsImageContentDefined(Uint32 index) const;
|
||||
void SetImageContentDefined(Uint32 index, Bool defined);
|
||||
Bool IsDepthStencilContentDefined(Uint32 index) const;
|
||||
void SetDepthStencilContentDefined(Uint32 index, Bool defined);
|
||||
void SetAllDepthStencilContentUndefined();
|
||||
|
||||
private:
|
||||
void CreateImageViews(VkDevice device);
|
||||
void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice);
|
||||
@@ -81,7 +63,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
|
||||
VkSurfaceFormatKHR m_surfaceFormat{};
|
||||
VkExtent2D m_extent{};
|
||||
VkExtent2D m_surfaceExtent{};
|
||||
VkSurfaceTransformFlagBitsKHR m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
Vector<VkImage> m_images;
|
||||
Vector<VkImageView> m_imageViews;
|
||||
@@ -92,7 +73,5 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<VkDeviceMemory> m_depthStencilImageMemories;
|
||||
Vector<VkImageView> m_depthStencilImageViews;
|
||||
Vector<VkImageLayout> m_depthStencilImageLayouts;
|
||||
Vector<Bool> m_imageContentDefined;
|
||||
Vector<Bool> m_depthStencilContentDefined;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -205,7 +204,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The frame's descriptor sets are recycled above, so last frame's reuse target
|
||||
// is gone: start the per-draw descriptor-reuse cache fresh this frame.
|
||||
m_hasLastDescriptor = false;
|
||||
m_lastBindValid = false;
|
||||
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
|
||||
// reuse cannot outlive a single frame (see SamplerResolveMemo).
|
||||
for (auto& memo : m_samplerResolveMemo) {
|
||||
@@ -213,40 +211,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
void UniformManager::OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout) {
|
||||
SizeT purgedSets = 0;
|
||||
for (auto& frame : m_frames) {
|
||||
const auto it = frame.descriptorSetCacheByLayout.find(descriptorSetLayout);
|
||||
if (it == frame.descriptorSetCacheByLayout.end()) {
|
||||
continue;
|
||||
}
|
||||
// Free the sets back to their pools and credit the bucket accounting, so
|
||||
// program churn recycles pool capacity instead of abandoning the slots.
|
||||
// GPU-safe: the layout only dies after >1024 idle frame boundaries, so no
|
||||
// in-flight command buffer references these sets.
|
||||
for (const auto& cached : it->second.sets) {
|
||||
if (cached.set == VK_NULL_HANDLE) {
|
||||
continue;
|
||||
}
|
||||
vkFreeDescriptorSets(m_device, cached.pool, 1, &cached.set);
|
||||
const auto bucket = std::find_if(
|
||||
frame.descriptorPools.begin(), frame.descriptorPools.end(),
|
||||
[&cached](const DescriptorPoolBucket& candidate) { return candidate.handle == cached.pool; });
|
||||
if (bucket != frame.descriptorPools.end() && bucket->allocatedSets > 0) {
|
||||
--bucket->allocatedSets;
|
||||
}
|
||||
}
|
||||
purgedSets += it->second.sets.size();
|
||||
frame.descriptorSetCacheByLayout.erase(it);
|
||||
}
|
||||
if (purgedSets > 0) {
|
||||
// The per-draw reuse memo folds the layout handle into its signature; drop
|
||||
// it so a recycled handle value cannot revive a purged set mid-frame.
|
||||
m_hasLastDescriptor = false;
|
||||
MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets);
|
||||
}
|
||||
}
|
||||
|
||||
Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
@@ -386,28 +350,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Uint16 samplerVersion = samplerToUse->GetVersion();
|
||||
const Uint64 textureLifetimeId = texture->GetLifetimeId();
|
||||
const Uint16 textureParamsVersion = texture->GetTextureParamsVersion();
|
||||
// The sampler's LOD clamp depends on how many levels the sampled view exposes, and that
|
||||
// follows uploads as well as GL parameters - so it belongs in the memo key too.
|
||||
const Uint32 viewLevelCount = resource->sampledLevelCount;
|
||||
if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion &&
|
||||
memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion &&
|
||||
memo.forceNearestFiltering == forceNearestFiltering && memo.viewLevelCount == viewLevelCount) {
|
||||
memo.forceNearestFiltering == forceNearestFiltering) {
|
||||
resolvedSampler = memo.sampler;
|
||||
} else {
|
||||
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture,
|
||||
forceNearestFiltering, viewLevelCount);
|
||||
resolvedSampler =
|
||||
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
|
||||
memo.samplerLifetimeId = samplerLifetimeId;
|
||||
memo.samplerVersion = samplerVersion;
|
||||
memo.textureLifetimeId = textureLifetimeId;
|
||||
memo.textureParamsVersion = textureParamsVersion;
|
||||
memo.forceNearestFiltering = forceNearestFiltering;
|
||||
memo.viewLevelCount = viewLevelCount;
|
||||
memo.sampler = resolvedSampler;
|
||||
memo.valid = true;
|
||||
}
|
||||
} else {
|
||||
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering,
|
||||
resource->sampledLevelCount);
|
||||
resolvedSampler =
|
||||
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
|
||||
}
|
||||
outImageInfo = {
|
||||
.sampler = resolvedSampler,
|
||||
@@ -448,48 +408,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return outImageInfo.sampler != VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
Bool UniformManager::ProgramSamplesOnlySingleLevelTextures(
|
||||
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) {
|
||||
Bool sawSampler = false;
|
||||
for (Uint32 binding = 0; binding < programObj.bindingKinds.size(); ++binding) {
|
||||
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
|
||||
continue;
|
||||
}
|
||||
const auto* texture = ResolveSamplerTextureRaw(program, programObj, binding);
|
||||
if (texture == nullptr) return false;
|
||||
const auto& levelRange = texture->GetLevelRange();
|
||||
if (levelRange.x() != levelRange.y()) return false;
|
||||
|
||||
// An explicit-LOD sample is a single filtered tap, so it also gives up anisotropic
|
||||
// filtering - which a single-level view can still have. Resolve the sampler exactly
|
||||
// the way ResolveSamplerDescriptor does and bail if anisotropy would apply.
|
||||
const Int location = programObj.samplerUniformLocationByBinding[binding];
|
||||
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
||||
const auto& samplerOverride = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
|
||||
const auto* effectiveSampler =
|
||||
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
|
||||
if (effectiveSampler == nullptr) return false;
|
||||
if (effectiveSampler->GetMaxAnisotropy() > 1.0f &&
|
||||
effectiveSampler->GetMinFilter() == SamplerFilterMode::Linear &&
|
||||
effectiveSampler->GetMagFilter() == SamplerFilterMode::Linear) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// An explicit LOD 0 makes lambda exactly 0, which is the magnification side of the
|
||||
// min/mag decision. That only matches the implicit form when lambda could not have been
|
||||
// positive anyway (the LOD clamp already pins it at or below 0), or when the two
|
||||
// filters are the same and the choice cannot be observed.
|
||||
const Float effectiveMaxLod = effectiveSampler->GetMipmapMode() == SamplerMipmapMode::None
|
||||
? 0.0f
|
||||
: effectiveSampler->GetMaxLod();
|
||||
if (effectiveMaxLod > 0.0f && effectiveSampler->GetMinFilter() != effectiveSampler->GetMagFilter()) {
|
||||
return false;
|
||||
}
|
||||
sawSampler = true;
|
||||
}
|
||||
return sawSampler;
|
||||
}
|
||||
|
||||
Bool UniformManager::ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
|
||||
@@ -993,11 +911,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkDescriptorPoolCreateInfo poolInfo{};
|
||||
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
// FREE_DESCRIPTOR_SET_BIT lets a destroyed layout's cached sets be freed back
|
||||
// (OnDescriptorSetLayoutDestroyed) so program churn recycles pool capacity.
|
||||
// The cost is on set allocation only, which happens when a layout's per-frame
|
||||
// cache grows - never on the per-draw reuse path.
|
||||
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
|
||||
poolInfo.maxSets = maxSets;
|
||||
poolInfo.poolSizeCount = static_cast<Uint32>(std::size(poolSizes));
|
||||
poolInfo.pPoolSizes = poolSizes;
|
||||
@@ -1077,7 +990,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& frame = m_frames[frameIndex];
|
||||
auto& cache = frame.descriptorSetCacheByLayout[programObj.descriptorSetLayout];
|
||||
if (cache.cursor < cache.sets.size()) {
|
||||
outDescriptorSet = cache.sets[cache.cursor++].set;
|
||||
outDescriptorSet = cache.sets[cache.cursor++];
|
||||
} else {
|
||||
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
|
||||
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
|
||||
@@ -1091,9 +1004,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return allocResult;
|
||||
}
|
||||
|
||||
// The successful allocation came from the bucket the alloc helper left
|
||||
// active; record it so a layout-destroyed purge can free the set back.
|
||||
cache.sets.push_back({outDescriptorSet, frame.descriptorPools[frame.activeDescriptorPoolIndex].handle});
|
||||
cache.sets.push_back(outDescriptorSet);
|
||||
++cache.cursor;
|
||||
MGLOG_D("UniformDescriptorBinder: cached descriptor set count for frame=%u grew to %zu", frameIndex,
|
||||
cache.sets.size());
|
||||
@@ -1191,47 +1102,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
bufferInfo.range = ubo.range;
|
||||
dynOffset = static_cast<Uint32>(ubo.dynamicOffset);
|
||||
} else {
|
||||
// Global-UBO slice reuse (see GlobalUboSliceMemo): unchanged
|
||||
// uniform bytes re-use the slice already uploaded this frame.
|
||||
const Bool isGlobalUbo =
|
||||
programObj.globalUboBinding == static_cast<Int>(binding) && element == 0;
|
||||
const Uint64 uboFrameSerial = m_bufferManager->GetFrameSerial();
|
||||
const Uint64 uboProgramLifetimeId = program.GetLifetimeId();
|
||||
const Uint32 uboContentVersion = program.GetUBOContentVersion();
|
||||
Bool reusedSlice = false;
|
||||
if (isGlobalUbo) {
|
||||
for (const auto& memo : m_globalUboMemo) {
|
||||
if (memo.buffer != VK_NULL_HANDLE &&
|
||||
memo.programLifetimeId == uboProgramLifetimeId &&
|
||||
memo.frameSerial == uboFrameSerial &&
|
||||
memo.uboContentVersion == uboContentVersion &&
|
||||
memo.range == static_cast<VkDeviceSize>(ubo.payloadSize)) {
|
||||
bufferInfo.buffer = memo.buffer;
|
||||
bufferInfo.range = memo.range;
|
||||
dynOffset = static_cast<Uint32>(memo.offset);
|
||||
reusedSlice = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!reusedSlice) {
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
|
||||
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
|
||||
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u",
|
||||
binding, element);
|
||||
return false;
|
||||
}
|
||||
bufferInfo.buffer = slice.buffer;
|
||||
bufferInfo.range = ubo.payloadSize;
|
||||
dynOffset = static_cast<Uint32>(slice.offset);
|
||||
if (isGlobalUbo) {
|
||||
m_globalUboMemo[m_globalUboMemoNext] = GlobalUboSliceMemo{
|
||||
uboProgramLifetimeId, uboFrameSerial, uboContentVersion,
|
||||
slice.buffer, slice.offset, static_cast<VkDeviceSize>(ubo.payloadSize)};
|
||||
m_globalUboMemoNext = (m_globalUboMemoNext + 1) % kGlobalUboMemoSize;
|
||||
}
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
|
||||
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
|
||||
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u",
|
||||
binding, element);
|
||||
return false;
|
||||
}
|
||||
bufferInfo.buffer = slice.buffer;
|
||||
bufferInfo.range = ubo.payloadSize;
|
||||
dynOffset = static_cast<Uint32>(slice.offset);
|
||||
}
|
||||
bufferInfos.push_back(bufferInfo);
|
||||
// Dynamic offsets are consumed in binding order, then array element order,
|
||||
@@ -1370,34 +1250,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_hasLastDescriptor = cacheable;
|
||||
}
|
||||
|
||||
// Skip the driver call when this exact binding is already live on the
|
||||
// command buffer (see the bind-dedup shadow in the header).
|
||||
const Uint32 offsetCount = static_cast<Uint32>(dynamicOffsets.size());
|
||||
Bool identicalBind = m_lastBindValid && m_lastBindSet == descriptorSet &&
|
||||
m_lastBindLayout == programObj.pipelineLayout && m_lastBindPoint == bindPoint &&
|
||||
m_lastBindOffsetCount == offsetCount && offsetCount <= kMaxShadowedDynamicOffsets;
|
||||
if (identicalBind) {
|
||||
for (Uint32 i = 0; i < offsetCount; ++i) {
|
||||
if (m_lastBindOffsets[i] != dynamicOffsets[i]) {
|
||||
identicalBind = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!identicalBind) {
|
||||
vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1,
|
||||
&descriptorSet, offsetCount, dynamicOffsets.data());
|
||||
if (offsetCount <= kMaxShadowedDynamicOffsets) {
|
||||
m_lastBindValid = true;
|
||||
m_lastBindSet = descriptorSet;
|
||||
m_lastBindLayout = programObj.pipelineLayout;
|
||||
m_lastBindPoint = bindPoint;
|
||||
m_lastBindOffsetCount = offsetCount;
|
||||
std::copy_n(dynamicOffsets.data(), offsetCount, m_lastBindOffsets);
|
||||
} else {
|
||||
m_lastBindValid = false;
|
||||
}
|
||||
}
|
||||
vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1,
|
||||
&descriptorSet, static_cast<Uint32>(dynamicOffsets.size()), dynamicOffsets.data());
|
||||
return true;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -39,20 +39,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void Shutdown();
|
||||
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
// A command buffer (re)began recording: descriptor bindings recorded into
|
||||
// the previous buffer do not carry over, so drop the bind-dedup shadow.
|
||||
void OnCommandBufferBoundary() { m_lastBindValid = false; }
|
||||
// A ProgramFactory eviction just destroyed this layout: purge every frame
|
||||
// slot's cached descriptor sets for it, so a recycled handle value can never
|
||||
// stale-hit sets written for the dead layout's bindings. The sets are
|
||||
// vkFreeDescriptorSets'd back to their pools (created with
|
||||
// FREE_DESCRIPTOR_SET_BIT) and the pool accounting is credited, so program
|
||||
// churn recycles pool capacity instead of abandoning it. GPU-safe: the layout
|
||||
// only dies after >1024 idle frame boundaries, so no in-flight command buffer
|
||||
// references its sets. This is the only eviction path for the per-layout
|
||||
// caches - a live layout's entry must never be purged (its sets would be
|
||||
// unreachable pool slots), so there is deliberately no age-based sweep here.
|
||||
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
|
||||
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures);
|
||||
@@ -72,16 +58,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
|
||||
VkFormat resourceFormat, Bool useBindingFormat);
|
||||
|
||||
// True when the program reads at least one sampler and every one of them is bound to a
|
||||
// texture whose GL level range is a single level. Such a sampler resolves to
|
||||
// minLod = maxLod = 0 (see VkSamplerManager::GetOrCreateSampler), so an implicit-LOD sample
|
||||
// and an explicit LOD 0 sample must read the same texel - which is what makes the
|
||||
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
|
||||
// only GL state, so a texture that ends up single-level for another reason (one uploaded
|
||||
// level under a wide level range) merely misses the rewrite.
|
||||
static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
|
||||
private:
|
||||
struct DescriptorPoolBucket {
|
||||
VkDescriptorPool handle = VK_NULL_HANDLE;
|
||||
@@ -89,16 +65,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 allocatedSets = 0;
|
||||
};
|
||||
|
||||
// A cached descriptor set together with the pool it was allocated from, so a
|
||||
// layout-destroyed purge can vkFreeDescriptorSets it back and credit the
|
||||
// owning bucket's accounting.
|
||||
struct CachedDescriptorSet {
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
VkDescriptorPool pool = VK_NULL_HANDLE;
|
||||
};
|
||||
|
||||
struct DescriptorSetCacheEntry {
|
||||
Vector<CachedDescriptorSet> sets;
|
||||
Vector<VkDescriptorSet> sets;
|
||||
Uint32 cursor = 0;
|
||||
};
|
||||
|
||||
@@ -188,35 +156,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 m_lastDescriptorSignature = 0;
|
||||
Bool m_hasLastDescriptor = false;
|
||||
|
||||
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
|
||||
// block resolve to the same set AND the same dynamic offsets, so the
|
||||
// driver call can be skipped outright. Command-buffer-scope state; reset
|
||||
// via OnCommandBufferBoundary whenever a recording (re)begins. Keyed on
|
||||
// layout+bind point, so a pipeline-layout switch always rebinds.
|
||||
static constexpr Uint32 kMaxShadowedDynamicOffsets = 8;
|
||||
Bool m_lastBindValid = false;
|
||||
VkDescriptorSet m_lastBindSet = VK_NULL_HANDLE;
|
||||
VkPipelineLayout m_lastBindLayout = VK_NULL_HANDLE;
|
||||
VkPipelineBindPoint m_lastBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
Uint32 m_lastBindOffsetCount = 0;
|
||||
Uint32 m_lastBindOffsets[kMaxShadowedDynamicOffsets] = {};
|
||||
|
||||
// Global-UBO transient-slice reuse: MC leaves the default uniform block
|
||||
// untouched across long GUI/terrain runs, so the per-draw re-upload of
|
||||
// the same bytes can reuse the slice uploaded earlier THIS frame (frame
|
||||
// serial guards arena recycling; the content version guards writes).
|
||||
struct GlobalUboSliceMemo {
|
||||
Uint64 programLifetimeId = 0;
|
||||
Uint64 frameSerial = 0;
|
||||
Uint32 uboContentVersion = 0;
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VkDeviceSize offset = 0;
|
||||
VkDeviceSize range = 0;
|
||||
};
|
||||
static constexpr Uint32 kGlobalUboMemoSize = 4;
|
||||
GlobalUboSliceMemo m_globalUboMemo[kGlobalUboMemoSize];
|
||||
Uint32 m_globalUboMemoNext = 0;
|
||||
|
||||
// Per-binding fast path over VkSamplerManager's content-hashed sampler cache, which
|
||||
// stays the source of truth: its key hashes all sampler+texture state, so two distinct
|
||||
// sampler objects with identical state still resolve to one VkSampler. This memo only
|
||||
@@ -233,7 +172,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 samplerLifetimeId = 0;
|
||||
Uint64 textureLifetimeId = 0;
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
Uint32 viewLevelCount = 0;
|
||||
Uint16 samplerVersion = 0;
|
||||
Uint16 textureParamsVersion = 0;
|
||||
Bool forceNearestFiltering = false;
|
||||
|
||||
@@ -32,13 +32,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
|
||||
|
||||
// The buffer's heap address is an identity component of the key: a freed
|
||||
// buffer's reused address can alias an old cache entry, but only under a
|
||||
// byte-identical attribute layout - and the entry payload is a pure function
|
||||
// of the hashed inputs, with the draw path re-resolving bindingBufferKeys
|
||||
// against the live VAO attribute pointers, so an aliased hit returns exactly
|
||||
// what a rebuild would. Address drift only grows the map; the OnFrameBoundary
|
||||
// aging sweep bounds that.
|
||||
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
||||
}
|
||||
@@ -58,27 +51,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao) {
|
||||
// Per-draw fast path: the VAO carries a pointer to its resolved entry,
|
||||
// valid while its config version and the cache's eviction epoch both
|
||||
// match - no re-hash, no map lookup.
|
||||
const void* memoState = nullptr;
|
||||
Uint64 memoEpoch = 0;
|
||||
if (vao.GetBackendStateMemo(memoState, memoEpoch) && memoEpoch == m_evictionEpoch) {
|
||||
const auto* entry = static_cast<const BackendVertexInputState*>(memoState);
|
||||
entry->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return *entry;
|
||||
}
|
||||
const BackendVertexInputState& entry = GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
vao.SetBackendStateMemo(&entry, m_evictionEpoch);
|
||||
return entry;
|
||||
return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
}
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
|
||||
auto it = m_cache.find(hash);
|
||||
if (it != m_cache.end()) {
|
||||
it->second->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return *it->second;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
VertexInputStateBuilder builder;
|
||||
@@ -184,37 +164,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const auto& state = builder.Build();
|
||||
|
||||
auto& slot = m_cache[hash];
|
||||
if (!slot) {
|
||||
slot = MakeUnique<BackendVertexInputState>();
|
||||
}
|
||||
BackendVertexInputState& entry = *slot;
|
||||
auto& entry = m_cache[hash];
|
||||
entry.hash = hash;
|
||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
entry.bindings = builder.GetBindings();
|
||||
entry.attributes = builder.GetAttributes();
|
||||
// See the layoutHash declaration: hash only the resolved layout, never
|
||||
// buffer identities, so identical layouts across VAOs/buffers agree.
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, 0));
|
||||
for (const auto& binding : entry.bindings) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.binding, sizeof(binding.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.stride, sizeof(binding.stride)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.inputRate, sizeof(binding.inputRate)));
|
||||
}
|
||||
for (const auto& attribute : entry.attributes) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.location, sizeof(attribute.location)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.binding, sizeof(attribute.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
||||
entry.layoutHash = XXH64_digest(m_hashState);
|
||||
entry.attributeLocationMask = 0;
|
||||
for (const auto& attribute : entry.attributes) {
|
||||
if (attribute.location < 32u) {
|
||||
entry.attributeLocationMask |= (1u << attribute.location);
|
||||
}
|
||||
}
|
||||
entry.bindingBufferKeys = std::move(bindingBufferKeys);
|
||||
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
|
||||
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
|
||||
@@ -227,33 +180,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return entry;
|
||||
}
|
||||
|
||||
void VertexInputStateFactory::OnFrameBoundary() {
|
||||
++m_frameBoundaryCounter;
|
||||
|
||||
// Sweep occasionally; evict entries whose last hit is far in the past.
|
||||
// Erasure happens only here, never mid-frame: the draw path holds a
|
||||
// reference into the current entry across its setup, and unordered_map
|
||||
// erase would invalidate it. Entries are CPU-side only, so no GPU-idle
|
||||
// proof is needed; an evicted entry that is used again is simply rebuilt
|
||||
// from the VAO state (same hash, same content).
|
||||
constexpr Uint64 kSweepInterval = 256;
|
||||
constexpr Uint64 kRetireAgeBoundaries = 1024;
|
||||
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||
it = m_cache.erase(it);
|
||||
// Invalidate every VAO's state-pointer memo: the erased node's
|
||||
// address may be reused by a future insert.
|
||||
++m_evictionEpoch;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
|
||||
Bool isBgra) {
|
||||
if (isBgra) {
|
||||
|
||||
@@ -27,18 +27,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
struct BackendVertexInputState {
|
||||
HashType hash = 0;
|
||||
// Hash of the resolved Vulkan vertex layout only (bindings, attributes,
|
||||
// unsupported mask) - NO buffer identities. `hash` mixes buffer heap
|
||||
// addresses so per-chunk VBOs mint a fresh identity per buffer; keying
|
||||
// pipelines on that minted one VkPipeline per chunk section for an
|
||||
// identical layout, defeating pipeline reuse and the per-draw memo.
|
||||
// Pipelines depend only on the layout, so they key on this instead.
|
||||
HashType layoutHash = 0;
|
||||
// Frame boundary of the last cache hit; entries idle past the
|
||||
// OnFrameBoundary retirement age are evicted (CPU heap only).
|
||||
// Mutable: the VAO's state-pointer memo fast path stamps it through
|
||||
// a const entry reference.
|
||||
mutable Uint64 lastUsedFrameBoundary = 0;
|
||||
Vector<VkVertexInputBindingDescription> bindings;
|
||||
Vector<VkVertexInputAttributeDescription> attributes;
|
||||
Vector<SizeT> bindingBufferKeys;
|
||||
@@ -50,9 +38,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// absent from `attributes`, so without this mask the draw path cannot tell them apart from
|
||||
// a genuinely disabled array and would silently feed the shader the current attribute value.
|
||||
Uint32 unsupportedAttribMask = 0;
|
||||
// Bitmask of `attributes[i].location` - the draw path needs it up to
|
||||
// three times per draw, so it is baked once at build time.
|
||||
Uint32 attributeLocationMask = 0;
|
||||
VkPipelineVertexInputStateCreateInfo state{
|
||||
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
|
||||
};
|
||||
@@ -70,14 +55,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const BackendVertexInputState& GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
||||
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
||||
// Frame boundary hook: ages the cache and evicts entries not hit for many
|
||||
// frames. The key mixes buffer heap addresses, so buffer/VAO churn keeps
|
||||
// minting fresh keys; without eviction the map grows for the whole session.
|
||||
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
|
||||
// and the draw path's entry reference never spans a frame boundary, so
|
||||
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
|
||||
// compare except on sweep boundaries.
|
||||
void OnFrameBoundary();
|
||||
static SizeT GetComponentSize(DataType type);
|
||||
// Tightly-packed byte size of one vertex element for this attribute: componentSize * size for
|
||||
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
|
||||
@@ -92,19 +69,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VulkanRendererConfig& m_config;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
// Values are heap-allocated: FastSTL::unordered_map is open-addressing,
|
||||
// so INSERT invalidates references to stored values. The draw path (and
|
||||
// the VAOs' state-pointer memos) hold entry pointers across inserts;
|
||||
// only the unique_ptr cell moves, never the pointee.
|
||||
UnorderedMap<HashType, UniquePtr<BackendVertexInputState>> m_cache;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameBoundaryCounter = 0;
|
||||
// Bumped whenever any cache entry is erased. VAOs memo a raw pointer to
|
||||
// their heap-allocated entry (stable across map insert/rehash by
|
||||
// construction); a memo is honored only while its recorded epoch
|
||||
// matches, so an evicted entry can never be dereferenced through a
|
||||
// stale memo.
|
||||
Uint64 m_evictionEpoch = 1;
|
||||
UnorderedMap<HashType, BackendVertexInputState> m_cache;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -22,10 +22,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
||||
// Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled
|
||||
// (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled).
|
||||
constexpr VkBufferUsageFlags kTransformFeedbackUsage =
|
||||
VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT;
|
||||
// The app writes into the persistent map with no explicit flush, so its memory must
|
||||
// be host-coherent (Adreno host-visible memory is; requiring it keeps us portable).
|
||||
constexpr VkMemoryPropertyFlags kPersistentBackedRequiredFlags =
|
||||
@@ -145,15 +141,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_transientUploadArena.BeginFrame(frameIndex);
|
||||
}
|
||||
|
||||
void VkBufferManager::CollectAllDeferredReleases() {
|
||||
for (Uint32 frameIndex = 0; frameIndex < m_deferredBufferReleases.size(); ++frameIndex) {
|
||||
CollectDeferredReleases(frameIndex);
|
||||
}
|
||||
for (Uint32 frameIndex = 0; frameIndex < m_transientUploadArena.GetFrameCount(); ++frameIndex) {
|
||||
m_transientUploadArena.CollectDeferredReleases(frameIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void VkBufferManager::NotifyDeviceIdle() {
|
||||
// Everything submitted so far has completed. Work recorded for the
|
||||
// current frame has not been submitted yet, so the current serial
|
||||
@@ -469,10 +456,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// it from the current shadow - MappedData() is still the shadow here because the
|
||||
// frontend adopts (and drops) the shadow only after this returns.
|
||||
DeferRelease(std::move(resource->buffer));
|
||||
const VkBufferUsageFlags persistentUsage =
|
||||
kPersistentBackedUsage |
|
||||
(m_initInfo.transformFeedbackUsageEnabled ? kTransformFeedbackUsage : 0);
|
||||
if (!CreateResidentStorage(*resource, size, persistentUsage, kPersistentBackedRequiredFlags)) {
|
||||
if (!CreateResidentStorage(*resource, size, kPersistentBackedUsage, kPersistentBackedRequiredFlags)) {
|
||||
resource->persistentMapped = false;
|
||||
resource->storageSize = 0;
|
||||
resource->usageFlags = 0;
|
||||
|
||||
@@ -31,9 +31,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VmaMemoryUsage transientMemoryUsage = VMA_MEMORY_USAGE_AUTO;
|
||||
VmaAllocationCreateFlags transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
|
||||
Bool transientPersistentMapping = false;
|
||||
// VK_EXT_transform_feedback is enabled: persistent-map storage additionally
|
||||
// carries the transform feedback usage so capture targets can bind directly.
|
||||
Bool transformFeedbackUsageEnabled = false;
|
||||
};
|
||||
|
||||
// The DirectVulkan storage behind one frontend buffer (pipe_resource analogue).
|
||||
@@ -80,11 +77,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Recreate all per-frame transient arenas
|
||||
Bool RecreateTransientArenas(Uint32 frameCount);
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
// Drains every frame slot's deferred buffer/resource releases (and the
|
||||
// transient arena's parked superseded blocks). Only valid when the
|
||||
// caller has proven every queue submission complete; used by the
|
||||
// present-less frame-boundary drain.
|
||||
void CollectAllDeferredReleases();
|
||||
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
|
||||
void NotifyDeviceIdle();
|
||||
// A frame slot's submission fence has been waited: every serial up to
|
||||
|
||||
@@ -93,7 +93,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_pendingClears.clear();
|
||||
m_aliveObjects.clear();
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
|
||||
@@ -128,7 +127,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_pendingClears.erase(key);
|
||||
}
|
||||
m_aliveObjects.erase(identity);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
Bool VkClearManager::LockTextureIdentityLocked(const TextureIdentity& identity,
|
||||
@@ -223,7 +221,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
|
||||
@@ -241,7 +238,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
Bool VkClearManager::HasPendingClear(MG_State::GLState::ITextureObject* texture) {
|
||||
@@ -249,10 +245,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
|
||||
@@ -268,9 +260,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (key.texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_pendingClears.find(key) == m_pendingClears.end()) {
|
||||
@@ -298,9 +287,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (key.texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (!LockTextureLocked(key, outTexture)) {
|
||||
@@ -339,9 +325,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
@@ -362,9 +345,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
const TextureIdentity identity = MakeTextureIdentity(texture);
|
||||
MGLOG_D("%s: Pop all pending clears for texture %d", __func__, texture->GetExternalIndex());
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
@@ -381,7 +361,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto it = m_pendingClears.find(key);
|
||||
if (it != m_pendingClears.end()) {
|
||||
m_pendingClears.erase(it);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include "MG_Util/Math/VectorTypes.h"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <atomic>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -121,19 +120,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
||||
|
||||
Uint8 m_gcCounter = 0;
|
||||
public:
|
||||
// Lock-free probe for the consecutive-draw fast path: any pending clear
|
||||
// forces the full SetupDraw path (which materializes/consumes it).
|
||||
Bool HasAnyPendingClears() const { return m_pendingCount.load(std::memory_order_relaxed) != 0; }
|
||||
|
||||
private:
|
||||
mutable std::mutex m_mutex;
|
||||
// Lock-free mirror of m_pendingClears.size(), maintained under m_mutex
|
||||
// by every mutation. The per-draw probes (HasPendingClear/GetPending*)
|
||||
// read it before taking the lock: during draw batches the pending set
|
||||
// is almost always empty, so this turns several locked map probes per
|
||||
// draw into one relaxed load.
|
||||
std::atomic<Uint32> m_pendingCount{0};
|
||||
std::unordered_map<PendingClearKey, ClearAttachmentPayload, PendingClearKeyHash> m_pendingClears;
|
||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||
};
|
||||
|
||||
@@ -16,21 +16,31 @@
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
||||
// GL promises "at least the requested samples", so a non-power-of-two
|
||||
// request (legal in GL, e.g. 3) rounds up to the next Vulkan bit.
|
||||
if (requestedSamples <= 1) {
|
||||
switch (requestedSamples <= 0 ? 1 : requestedSamples) {
|
||||
case 1:
|
||||
outSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
return true;
|
||||
}
|
||||
if (requestedSamples > 64) {
|
||||
case 2:
|
||||
outSampleCount = VK_SAMPLE_COUNT_2_BIT;
|
||||
return true;
|
||||
case 4:
|
||||
outSampleCount = VK_SAMPLE_COUNT_4_BIT;
|
||||
return true;
|
||||
case 8:
|
||||
outSampleCount = VK_SAMPLE_COUNT_8_BIT;
|
||||
return true;
|
||||
case 16:
|
||||
outSampleCount = VK_SAMPLE_COUNT_16_BIT;
|
||||
return true;
|
||||
case 32:
|
||||
outSampleCount = VK_SAMPLE_COUNT_32_BIT;
|
||||
return true;
|
||||
case 64:
|
||||
outSampleCount = VK_SAMPLE_COUNT_64_BIT;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
Uint32 bit = 1;
|
||||
while (bit < static_cast<Uint32>(requestedSamples)) {
|
||||
bit <<= 1;
|
||||
}
|
||||
outSampleCount = static_cast<VkSampleCountFlagBits>(bit);
|
||||
return true;
|
||||
}
|
||||
|
||||
static VkImageAspectFlags ResolveImageAspectMaskForFormat(VkFormat format) {
|
||||
@@ -156,9 +166,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (view != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(device, view, nullptr);
|
||||
}
|
||||
if (unormTwinView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(device, unormTwinView, nullptr);
|
||||
}
|
||||
if (image != VK_NULL_HANDLE && allocation != nullptr) {
|
||||
vmaDestroyImage(allocator, image, allocation);
|
||||
}
|
||||
@@ -166,7 +173,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
image = VK_NULL_HANDLE;
|
||||
allocation = nullptr;
|
||||
view = VK_NULL_HANDLE;
|
||||
unormTwinView = VK_NULL_HANDLE;
|
||||
layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
format = VK_FORMAT_UNDEFINED;
|
||||
aspect = VK_IMAGE_ASPECT_NONE;
|
||||
@@ -174,7 +180,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
internalFormat = TextureInternalFormat::Unknown;
|
||||
samples = 0;
|
||||
deadSinceFrame = kNeverObservedDead;
|
||||
}
|
||||
|
||||
VkRenderPassManager::VkRenderPassManager(VkDevice device,
|
||||
@@ -201,7 +206,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.Destroy(m_device, m_allocator);
|
||||
}
|
||||
m_renderbufferResources.clear();
|
||||
CollectDeferredRenderbufferReleases(/*destroyAll=*/true); // caller guarantees device idle
|
||||
m_pendingRenderbufferClears.clear();
|
||||
RenderPassEntry::s_textureResourcesScratch.clear();
|
||||
s_activeRenderPass = {};
|
||||
@@ -209,80 +213,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rpFastValid = false;
|
||||
}
|
||||
|
||||
Uint64 VkRenderPassManager::RetireAgeFrames() const {
|
||||
// MaxFramesInFlight + 2 covers the frame ring plus one boundary for the
|
||||
// recording-to-submit gap and one because OnPresent runs ahead of Present's
|
||||
// fence wait; the floor of 8 keeps a margin over the default ring of 3 while
|
||||
// still releasing multi-MB attachment memory promptly (the render-pass cache's
|
||||
// 1024-frame retirement would pin it for no additional safety).
|
||||
return std::max<Uint64>(8, static_cast<Uint64>(m_config.MaxFramesInFlight) + 2);
|
||||
}
|
||||
|
||||
void VkRenderPassManager::DeferRenderbufferBackingRelease(RenderbufferResource& resource) {
|
||||
// The superseded backing may still be referenced by in-flight command buffers
|
||||
// (glRenderbufferStorage can respecify a renderbuffer drawn this very frame),
|
||||
// so it is parked and destroyed only after RetireAgeFrames() boundaries.
|
||||
if (resource.image == VK_NULL_HANDLE && resource.view == VK_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
m_deferredRenderbufferReleases.push_back(
|
||||
{resource.image, resource.allocation, resource.view, resource.unormTwinView, m_frameCounter});
|
||||
resource.image = VK_NULL_HANDLE;
|
||||
resource.allocation = nullptr;
|
||||
resource.view = VK_NULL_HANDLE;
|
||||
resource.unormTwinView = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
void VkRenderPassManager::CollectDeferredRenderbufferReleases(Bool destroyAll) {
|
||||
if (m_deferredRenderbufferReleases.empty()) {
|
||||
return;
|
||||
}
|
||||
const Uint64 retireAgeFrames = RetireAgeFrames();
|
||||
std::erase_if(m_deferredRenderbufferReleases, [&](DeferredRenderbufferRelease& release) {
|
||||
if (!destroyAll && m_frameCounter - release.deferredAtFrame < retireAgeFrames) {
|
||||
return false;
|
||||
}
|
||||
if (release.view != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(m_device, release.view, nullptr);
|
||||
}
|
||||
if (release.unormTwinView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(m_device, release.unormTwinView, nullptr);
|
||||
}
|
||||
if (release.image != VK_NULL_HANDLE) {
|
||||
vmaDestroyImage(m_allocator, release.image, release.allocation);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void VkRenderPassManager::CollectRenderbufferGarbage() {
|
||||
// Two-phase reclamation: a dead renderbuffer's VkImage may still be referenced by
|
||||
// command buffers submitted up to frames-in-flight frames ago (it was legally
|
||||
// attached and drawn right up to its deletion), so the first observation of an
|
||||
// expired weak reference only stamps the current frame counter; Destroy runs once
|
||||
// enough frame boundaries have passed that the stamping frame's submission fence
|
||||
// has provably been waited (see RetireAgeFrames).
|
||||
const Uint64 retireAgeFrames = RetireAgeFrames();
|
||||
for (auto it = m_renderbufferResources.begin(); it != m_renderbufferResources.end();) {
|
||||
auto& resource = it->second;
|
||||
Vector<MG_State::GLState::RenderbufferObject*> deadRenderbuffers;
|
||||
deadRenderbuffers.reserve(m_renderbufferResources.size());
|
||||
for (auto& [renderbuffer, resource] : m_renderbufferResources) {
|
||||
const auto liveRenderbuffer = resource.renderbuffer.lock();
|
||||
if (liveRenderbuffer && liveRenderbuffer.get() == it->first) {
|
||||
resource.deadSinceFrame = RenderbufferResource::kNeverObservedDead;
|
||||
++it;
|
||||
continue;
|
||||
if (!liveRenderbuffer || liveRenderbuffer.get() != renderbuffer) {
|
||||
deadRenderbuffers.emplace_back(renderbuffer);
|
||||
}
|
||||
if (resource.deadSinceFrame == RenderbufferResource::kNeverObservedDead) {
|
||||
resource.deadSinceFrame = m_frameCounter;
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
for (auto* renderbuffer : deadRenderbuffers) {
|
||||
auto resourceIt = m_renderbufferResources.find(renderbuffer);
|
||||
if (resourceIt != m_renderbufferResources.end()) {
|
||||
resourceIt->second.Destroy(m_device, m_allocator);
|
||||
m_renderbufferResources.erase(resourceIt);
|
||||
}
|
||||
if (m_frameCounter - resource.deadSinceFrame < retireAgeFrames) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
m_pendingRenderbufferClears.erase(it->first);
|
||||
resource.Destroy(m_device, m_allocator);
|
||||
it = m_renderbufferResources.erase(it);
|
||||
m_pendingRenderbufferClears.erase(renderbuffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,47 +249,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const auto internalFormat = renderbuffer->GetInternalFormat();
|
||||
// Three-channel color formats widen to their RGBA twin exactly like textures do
|
||||
// (VkTextureManager::ResolveTextureFormatInfo): blits/resolves between a
|
||||
// renderbuffer and a texture of the same GL format then see one VkFormat.
|
||||
const VkFormat format = [&]() -> VkFormat {
|
||||
switch (internalFormat) {
|
||||
case TextureInternalFormat::RGB:
|
||||
case TextureInternalFormat::RGB8:
|
||||
case TextureInternalFormat::R3G3B2:
|
||||
case TextureInternalFormat::RGB4:
|
||||
case TextureInternalFormat::RGB5:
|
||||
return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
case TextureInternalFormat::SRGB8:
|
||||
return VK_FORMAT_R8G8B8A8_SRGB;
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
return VK_FORMAT_R8G8B8A8_SNORM;
|
||||
case TextureInternalFormat::RGB10:
|
||||
case TextureInternalFormat::RGB12:
|
||||
case TextureInternalFormat::RGB16:
|
||||
return VK_FORMAT_R16G16B16A16_UNORM;
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
return VK_FORMAT_R16G16B16A16_SNORM;
|
||||
case TextureInternalFormat::RGB16F:
|
||||
return VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||
case TextureInternalFormat::RGB32F:
|
||||
return VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||
case TextureInternalFormat::RGB8I:
|
||||
return VK_FORMAT_R8G8B8A8_SINT;
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
return VK_FORMAT_R8G8B8A8_UINT;
|
||||
case TextureInternalFormat::RGB16I:
|
||||
return VK_FORMAT_R16G16B16A16_SINT;
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
return VK_FORMAT_R16G16B16A16_UINT;
|
||||
case TextureInternalFormat::RGB32I:
|
||||
return VK_FORMAT_R32G32B32A32_SINT;
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
return VK_FORMAT_R32G32B32A32_UINT;
|
||||
default:
|
||||
return MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||
}
|
||||
}();
|
||||
const VkFormat format = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
|
||||
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the
|
||||
// usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer),
|
||||
@@ -353,46 +259,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
: VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) |
|
||||
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
|
||||
// GL allows the implementation to allocate more samples than requested
|
||||
// (glRenderbufferStorageMultisample only promises "at least"), and devices
|
||||
// like llvmpipe expose 1x/4x but not 2x. Round the request up to the
|
||||
// nearest supported count for this format.
|
||||
if (renderbuffer->GetSamples() > 0) {
|
||||
auto supportedIt = m_attachmentSampleCountsByFormat.find(format);
|
||||
if (supportedIt == m_attachmentSampleCountsByFormat.end()) {
|
||||
VkImageFormatProperties formatProperties{};
|
||||
VkSampleCountFlags supported = VK_SAMPLE_COUNT_1_BIT;
|
||||
if (vkGetPhysicalDeviceImageFormatProperties(m_physicalDevice, format, VK_IMAGE_TYPE_2D,
|
||||
VK_IMAGE_TILING_OPTIMAL, imageUsage, 0,
|
||||
&formatProperties) == VK_SUCCESS) {
|
||||
supported = formatProperties.sampleCounts;
|
||||
}
|
||||
supportedIt = m_attachmentSampleCountsByFormat.emplace(format, supported).first;
|
||||
}
|
||||
const VkSampleCountFlags supported = supportedIt->second;
|
||||
if ((supported & sampleCount) == 0) {
|
||||
// Smallest supported count above the request, else the largest below it.
|
||||
Uint32 rounded = 0;
|
||||
for (Uint32 bit = static_cast<Uint32>(sampleCount) << 1; bit <= VK_SAMPLE_COUNT_64_BIT; bit <<= 1) {
|
||||
if ((supported & bit) != 0) {
|
||||
rounded = bit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rounded == 0) {
|
||||
for (Uint32 bit = static_cast<Uint32>(sampleCount) >> 1; bit != 0; bit >>= 1) {
|
||||
if ((supported & bit) != 0) {
|
||||
rounded = bit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rounded != 0) {
|
||||
sampleCount = static_cast<VkSampleCountFlagBits>(rounded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto& resource = m_renderbufferResources[renderbuffer.get()];
|
||||
const Bool needsCreate =
|
||||
resource.image == VK_NULL_HANDLE ||
|
||||
@@ -404,15 +270,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.samples != renderbuffer->GetSamples();
|
||||
if (!needsCreate) {
|
||||
resource.renderbuffer = renderbuffer;
|
||||
// A new renderbuffer at a recycled address may adopt a compatible entry that
|
||||
// was already stamped dead; it is alive again, so cancel the aging.
|
||||
resource.deadSinceFrame = RenderbufferResource::kNeverObservedDead;
|
||||
return &resource;
|
||||
}
|
||||
|
||||
// Respecify: park the old backing for aged destruction instead of destroying
|
||||
// inline - it may still be referenced by in-flight command buffers.
|
||||
DeferRenderbufferBackingRelease(resource);
|
||||
resource.Destroy(m_device, m_allocator);
|
||||
resource.renderbuffer = renderbuffer;
|
||||
|
||||
@@ -430,12 +290,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
imageInfo.usage = imageUsage;
|
||||
imageInfo.samples = sampleCount;
|
||||
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
// sRGB renderbuffers attach through their UNORM twin while GL_FRAMEBUFFER_SRGB
|
||||
// is disabled, which needs a format-reinterpreting second view.
|
||||
const Bool hasUnormTwin = ResolveSrgbAttachmentWriteFormat(format, false) != format;
|
||||
if (hasUnormTwin) {
|
||||
imageInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
|
||||
VkImageFormatProperties imageFormatProperties{};
|
||||
const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||
@@ -470,11 +324,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
viewInfo.subresourceRange.layerCount = 1;
|
||||
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.view),
|
||||
"vkCreateImageView(renderbuffer)");
|
||||
if (hasUnormTwin) {
|
||||
viewInfo.format = ResolveSrgbAttachmentWriteFormat(format, false);
|
||||
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.unormTwinView),
|
||||
"vkCreateImageView(renderbuffer unorm twin)");
|
||||
}
|
||||
|
||||
resource.layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
resource.format = format;
|
||||
@@ -571,18 +420,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
VkRenderPassManager::HashType VkRenderPassManager::ComputeHash(
|
||||
const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear,
|
||||
Bool includeDefaultFboDepthStencil) {
|
||||
const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear) {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
||||
if (isDefaultFbo) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &swapchainImageIndex, sizeof(swapchainImageIndex)));
|
||||
}
|
||||
// sRGB attachments switch between their sRGB and UNORM-twin views with this
|
||||
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
|
||||
const Bool framebufferSrgbEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled)));
|
||||
auto& drawBuffers = fbo.GetDrawBuffers();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
|
||||
auto readBuffer = fbo.GetReadBuffer();
|
||||
@@ -656,17 +499,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
attachment <= FramebufferAttachmentType::BackRight);
|
||||
if (isDefaultColorAttachment) {
|
||||
currentLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex);
|
||||
// Content validity feeds the attachment's loadOp (see the
|
||||
// creation path), so it must key the cache as well.
|
||||
if (!m_swapchainObject.IsImageContentDefined(swapchainImageIndex)) {
|
||||
currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
} else if (attachment == FramebufferAttachmentType::Depth ||
|
||||
attachment == FramebufferAttachmentType::Stencil) {
|
||||
currentLayout = m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex);
|
||||
if (!m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) {
|
||||
currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
|
||||
@@ -721,49 +556,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
combineFramebufferAttachmentObjHash(drawbuf);
|
||||
}
|
||||
|
||||
// The depth-less default-FBO flavor omits the depth/stencil attachment
|
||||
// entirely, so it must hash differently from the depth-full flavor.
|
||||
const Bool depthStencilIncluded = !isDefaultFbo || includeDefaultFboDepthStencil;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &depthStencilIncluded, sizeof(depthStencilIncluded)));
|
||||
if (depthStencilIncluded) {
|
||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
|
||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
|
||||
}
|
||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
|
||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
|
||||
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil) {
|
||||
// Resolve the default-FBO depth flavor (see the header comment): keep the
|
||||
// depth attachment when the caller needs it, when a depth/stencil clear is
|
||||
// pending, or when the active pass already carries it (escalate-only, so
|
||||
// alternating depth-less draws never split an established depth pass).
|
||||
Bool includeDefaultFboDepthStencil = true;
|
||||
if (fbo.IsDefaultFramebuffer()) {
|
||||
Bool activeDefaultHasDepthStencil = false;
|
||||
if (const auto* active = GetActiveRenderPass()) {
|
||||
Bool activeIsSwapchainPass = false;
|
||||
Bool activeHasSwapchainDepthStencil = false;
|
||||
for (const auto& tracked : active->trackedAttachmentLayouts) {
|
||||
activeIsSwapchainPass |= tracked.target == TrackedAttachmentTarget::SwapchainColor;
|
||||
activeHasSwapchainDepthStencil |=
|
||||
tracked.target == TrackedAttachmentTarget::SwapchainDepthStencil;
|
||||
}
|
||||
activeDefaultHasDepthStencil = activeIsSwapchainPass && activeHasSwapchainDepthStencil;
|
||||
}
|
||||
const auto& defaultDepthAtt = fbo.GetAttachment(FramebufferAttachmentType::Depth);
|
||||
const auto& defaultStencilAtt = fbo.GetAttachment(FramebufferAttachmentType::Stencil);
|
||||
const Bool pendingDepthStencilClear =
|
||||
(defaultDepthAtt.IsTexture() && m_clearManager.HasPendingClear(defaultDepthAtt)) ||
|
||||
HasPendingRenderbufferClear(defaultDepthAtt) ||
|
||||
(defaultStencilAtt.IsTexture() && m_clearManager.HasPendingClear(defaultStencilAtt)) ||
|
||||
HasPendingRenderbufferClear(defaultStencilAtt);
|
||||
includeDefaultFboDepthStencil =
|
||||
drawUsesDepthStencil || activeDefaultHasDepthStencil || pendingDepthStencilClear;
|
||||
}
|
||||
|
||||
Uint32 swapchainImageIndex) {
|
||||
auto hasPendingClearOnFramebuffer = [&]() -> Bool {
|
||||
const auto& drawBuffers = fbo.GetDrawBuffers();
|
||||
for (auto attachment : drawBuffers) {
|
||||
@@ -813,7 +613,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
|
||||
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
|
||||
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
|
||||
(!fbo.IsDefaultFramebuffer() || m_rpFastHadDepthStencil == includeDefaultFboDepthStencil) &&
|
||||
m_rpFastRenderPassHash == activeRenderPass->hash && !hasPendingClearOnFramebuffer()) {
|
||||
auto activeIt = m_renderPasses.find(activeRenderPass->hash);
|
||||
if (activeIt != m_renderPasses.end()) {
|
||||
@@ -822,7 +621,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
auto compatibilityHash = ComputeHash(fbo, swapchainImageIndex, false, includeDefaultFboDepthStencil);
|
||||
auto compatibilityHash = ComputeHash(fbo, swapchainImageIndex, false);
|
||||
if (activeRenderPass != nullptr &&
|
||||
activeRenderPass->CompatibleWith(compatibilityHash) &&
|
||||
!hasPendingClearOnFramebuffer()) {
|
||||
@@ -839,11 +638,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
|
||||
m_rpFastRbEpoch = m_renderbufferImageEpoch;
|
||||
m_rpFastRenderPassHash = activeRenderPass->hash;
|
||||
m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment;
|
||||
activeIt->second.lastUsedFrame = m_frameCounter;
|
||||
return activeIt->second;
|
||||
}
|
||||
auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil);
|
||||
auto hash = ComputeHash(fbo, swapchainImageIndex, true);
|
||||
auto it = m_renderPasses.find(hash);
|
||||
if (it != m_renderPasses.end()) {
|
||||
it->second.lastUsedFrame = m_frameCounter;
|
||||
@@ -930,12 +728,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const VkImageLayout trackedRbLayout = rbResource->layout;
|
||||
const Bool rbFramebufferSrgb =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
const VkFormat rbAttachmentFormat =
|
||||
ResolveSrgbAttachmentWriteFormat(rbResource->format, rbFramebufferSrgb);
|
||||
rbDesc.flags = 0;
|
||||
rbDesc.format = rbAttachmentFormat;
|
||||
rbDesc.format = rbResource->format;
|
||||
rbDesc.samples = rbResource->sampleCount;
|
||||
rbDesc.loadOp = rbHasClear ? VK_ATTACHMENT_LOAD_OP_CLEAR :
|
||||
(trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ATTACHMENT_LOAD_OP_DONT_CARE
|
||||
@@ -970,8 +764,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.finalLayout = rbDesc.finalLayout,
|
||||
});
|
||||
textureResources.emplace_back(nullptr);
|
||||
attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView
|
||||
: rbResource->view);
|
||||
attachmentViews.emplace_back(rbResource->view);
|
||||
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i);
|
||||
|
||||
@@ -1040,13 +833,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(swapchainImageIndex < swapchainViews.size(),
|
||||
"GetOrCreateRenderPass: swapchain image index out of range");
|
||||
trackedColorLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex);
|
||||
// EGL: a presented color buffer's content is undefined when its
|
||||
// image comes back around (EGL_BUFFER_DESTROYED, the default
|
||||
// swap behaviour) - skip the tile load instead of reloading
|
||||
// stale pixels nobody may rely on.
|
||||
if (!hasClear && !m_swapchainObject.IsImageContentDefined(swapchainImageIndex)) {
|
||||
trackedColorLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||
.target = TrackedAttachmentTarget::SwapchainColor,
|
||||
.swapchainImageIndex = swapchainImageIndex,
|
||||
@@ -1060,15 +846,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(textureResource,
|
||||
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i);
|
||||
textureResources.emplace_back(textureResource);
|
||||
desc.format = ResolveSrgbAttachmentWriteFormat(
|
||||
textureResource->format,
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
|
||||
desc.format = textureResource->format;
|
||||
attachmentSampleCount = textureResource->sampleCount;
|
||||
trackedColorLayout = textureResource->layout;
|
||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||
.target = TrackedAttachmentTarget::Texture,
|
||||
.texture = att.GetTexture(),
|
||||
.textureRaw = att.GetTexture().get(),
|
||||
.textureMipLevel = attachmentMipLevel,
|
||||
.finalLayout = desc.finalLayout,
|
||||
});
|
||||
@@ -1132,12 +915,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
const auto* selectedDepthStencilAttachment = isUsableDepthStencilAttachment(depthAtt) ? &depthAtt :
|
||||
(isUsableDepthStencilAttachment(stencilAtt) ? &stencilAtt : nullptr);
|
||||
// Depth-less default-FBO flavor: nothing in this pass touches depth/stencil
|
||||
// and their content is undefined anyway (EGL swap), so drop the attachment
|
||||
// and its whole tile load + store.
|
||||
if (isDefaultFbo && !includeDefaultFboDepthStencil) {
|
||||
selectedDepthStencilAttachment = nullptr;
|
||||
}
|
||||
const Bool hasDistinctDepthAndStencilAttachments =
|
||||
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
|
||||
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
|
||||
@@ -1156,12 +933,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageLayout trackedDepthLayout = isDefaultFbo ?
|
||||
m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex) :
|
||||
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
// EGL 1.5 §3.10.1: every ancillary (depth/stencil) buffer's content is
|
||||
// undefined after a swap, so the first default-FBO pass of a frame can
|
||||
// skip the depth/stencil tile load outright.
|
||||
if (isDefaultFbo && !m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) {
|
||||
trackedDepthLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
depthAttachmentDescription.flags = 0;
|
||||
VkSampleCountFlagBits depthAttachmentSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
Int depthAttachmentId = 0;
|
||||
@@ -1245,7 +1016,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||
.target = TrackedAttachmentTarget::Texture,
|
||||
.texture = selectedDepthStencilAttachment->GetTexture(),
|
||||
.textureRaw = selectedDepthStencilAttachment->GetTexture().get(),
|
||||
.textureMipLevel = attachmentMipLevel,
|
||||
.finalLayout = depthAttachmentDescription.finalLayout,
|
||||
});
|
||||
@@ -1291,22 +1061,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
const Bool hasDepthStencilAttachment = depthAttachmentRef.attachment != VK_ATTACHMENT_UNUSED;
|
||||
|
||||
// Declare only the used colour-reference span. The GL draw-buffer array
|
||||
// always spans 8 slots, so passes used to declare colorAttachmentCount=8
|
||||
// with trailing VK_ATTACHMENT_UNUSED holes - and Adreno configures its
|
||||
// per-pixel render-backend/export path from the DECLARED count, so every
|
||||
// fragment of every pass paid the 8-target export cost (measured on
|
||||
// Adreno 650 / MC 26.2: 11.9 -> 7.5 ms of GPU time per frame, with the
|
||||
// single-quad swapchain blit pass alone dropping 1.26 -> 0.40 ms).
|
||||
// Interior GL_NONE holes keep their slots so fragment-output locations
|
||||
// still line up; a fragment output at a location past the trimmed count
|
||||
// is discarded, which is exactly GL's semantic for writing to a draw
|
||||
// buffer set to GL_NONE.
|
||||
while (!colorAttachmentRefs.empty() &&
|
||||
colorAttachmentRefs.back().attachment == VK_ATTACHMENT_UNUSED) {
|
||||
colorAttachmentRefs.pop_back();
|
||||
}
|
||||
|
||||
// Subpass
|
||||
VkSubpassDescription subpassDesc;
|
||||
subpassDesc.flags = 0;
|
||||
@@ -1424,14 +1178,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void VkRenderPassManager::OnPresent() {
|
||||
++m_frameCounter;
|
||||
|
||||
// Runs every frame boundary, ahead of the render-pass sweep gate below: the walk
|
||||
// is O(#renderbuffer resources) — single digits in practice — and per-frame
|
||||
// invocation keeps dead-resource reclaim latency at the aging bound instead of
|
||||
// coupling it to renderbuffer *use* (the GetOrCreateRenderbufferResource call
|
||||
// site never runs again once an app stops using renderbuffers).
|
||||
CollectRenderbufferGarbage();
|
||||
CollectDeferredRenderbufferReleases(/*destroyAll=*/false);
|
||||
|
||||
// Sweep occasionally; evict entries whose last use is far past every
|
||||
// in-flight frame so their VkRenderPass/VkFramebuffer can be destroyed
|
||||
// safely (RenderPassEntry's destructor releases the handles).
|
||||
@@ -1441,12 +1187,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect the dying handles and notify once after the loop: pipelines hashed
|
||||
// on them share the entries' >kRetireAgeFrames idleness (they are only bound
|
||||
// by draws that hit those entries), so the observer may destroy them
|
||||
// immediately - and a single batched notification costs one pipeline-cache
|
||||
// scan instead of one per evicted pass.
|
||||
Vector<VkRenderPass> destroyedRenderPasses;
|
||||
const Uint64 activeHash = s_hasActiveRenderPass ? s_activeRenderPass.hash : 0;
|
||||
for (auto it = m_renderPasses.begin(); it != m_renderPasses.end();) {
|
||||
const Bool isActive = s_hasActiveRenderPass && it->first == activeHash;
|
||||
@@ -1454,15 +1194,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (m_rpFastValid && m_rpFastRenderPassHash == it->first) {
|
||||
m_rpFastValid = false;
|
||||
}
|
||||
destroyedRenderPasses.push_back(it->second.renderPass);
|
||||
it = m_renderPasses.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (!destroyedRenderPasses.empty() && m_evictionObserver != nullptr) {
|
||||
m_evictionObserver->OnRenderPassesDestroyed(destroyedRenderPasses);
|
||||
}
|
||||
}
|
||||
|
||||
Bool VkRenderPassManager::BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry) {
|
||||
@@ -1515,17 +1251,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
renderPassBeginInfo.pClearValues = clearValues.data();
|
||||
|
||||
vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
|
||||
// Pre-pass stream bookkeeping: this pass's attachment images are now
|
||||
// referenced by the open frame recording.
|
||||
if (s_textureManager != nullptr) {
|
||||
for (const auto& tracked : renderPassEntry.trackedAttachmentLayouts) {
|
||||
if (tracked.target == TrackedAttachmentTarget::Texture) {
|
||||
if (const auto texture = tracked.texture.lock()) {
|
||||
s_textureManager->StampTextureRecordingUse(texture.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& pending: renderPassEntry.pendingClearAttachments) {
|
||||
if (pending.hasInlinePayload) {
|
||||
if (s_renderPassManager != nullptr) {
|
||||
@@ -1578,15 +1303,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case TrackedAttachmentTarget::SwapchainColor:
|
||||
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
|
||||
s_swapchainObject->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout);
|
||||
// The pass stored into the attachment: its content is defined
|
||||
// until the image is next presented.
|
||||
s_swapchainObject->SetImageContentDefined(trackedAttachment.swapchainImageIndex, true);
|
||||
break;
|
||||
case TrackedAttachmentTarget::SwapchainDepthStencil:
|
||||
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
|
||||
s_swapchainObject->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex,
|
||||
trackedAttachment.finalLayout);
|
||||
s_swapchainObject->SetDepthStencilContentDefined(trackedAttachment.swapchainImageIndex, true);
|
||||
break;
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "EndRenderPass: unsupported tracked attachment target=%d",
|
||||
|
||||
@@ -42,11 +42,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
struct TrackedAttachmentLayoutInfo {
|
||||
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
|
||||
WeakPtr<MG_State::GLState::ITextureObject> texture;
|
||||
// Identity-compare shortcut for the per-draw "does the active pass use
|
||||
// this sampled texture" probe: comparing this against a LIVE texture's
|
||||
// address needs no weak_ptr::lock (two refcount atomics per probe).
|
||||
// May dangle once the texture dies - compare only, never dereference.
|
||||
MG_State::GLState::ITextureObject* textureRaw = nullptr;
|
||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||
Uint32 textureMipLevel = 0;
|
||||
Uint32 swapchainImageIndex = 0;
|
||||
@@ -162,53 +157,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
class VkRenderPassManager {
|
||||
public:
|
||||
using HashType = Uint64;
|
||||
|
||||
// Notified once per OnPresent sweep with every aged-out entry's VkRenderPass
|
||||
// value: pipelines are hashed on the raw handle, and once destroyed the value
|
||||
// may be recycled for an incompatible pass, so dependent caches must purge
|
||||
// everything keyed on them before any new pass can be created (the sweep and
|
||||
// the notification run back-to-back with no creation in between; observers
|
||||
// compare the values, never dereference them). Batched so a mass-idle cohort
|
||||
// (shader-pack switch, dimension exit) costs the observer one pipeline-cache
|
||||
// scan, not one per dying pass. The wholesale paths
|
||||
// (Shutdown/RecreateSwapchain) do not notify - their callers already drop
|
||||
// every pipeline outright.
|
||||
class IEvictionObserver {
|
||||
public:
|
||||
virtual ~IEvictionObserver() = default;
|
||||
virtual void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) = 0;
|
||||
};
|
||||
|
||||
VkRenderPassManager(VkDevice device,
|
||||
VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
|
||||
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject);
|
||||
~VkRenderPassManager();
|
||||
|
||||
// Observer may be null (no notifications). Not owned.
|
||||
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
|
||||
|
||||
Bool Initialize();
|
||||
void Shutdown();
|
||||
|
||||
HashType ComputeHash(
|
||||
const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool includePendingClear = true,
|
||||
Bool includeDefaultFboDepthStencil = true);
|
||||
// drawUsesDepthStencil: whether the operation about to run inside the pass
|
||||
// reads or writes the depth/stencil buffer (depth test or stencil test
|
||||
// enabled, or a depth/stencil clear). Only consulted for the DEFAULT
|
||||
// framebuffer: EGL undefines its ancillary buffers at every swap, so a
|
||||
// default-FBO pass whose draws provably never touch depth/stencil is
|
||||
// created WITHOUT the depth attachment - on a tiler that skips the whole
|
||||
// depth tile load AND store. The flavor only escalates: once a pass with
|
||||
// depth is active, later depth-less draws keep using it, and a depth-using
|
||||
// draw against a depth-less active pass resolves to a new (incompatible)
|
||||
// entry, which the caller's compatibility check turns into a pass split;
|
||||
// the new pass's depth loads DONT_CARE (content was undefined all along).
|
||||
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil = true);
|
||||
Bool includePendingClear = true);
|
||||
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex);
|
||||
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
|
||||
const MG_State::GLState::FramebufferObject& drawFbo);
|
||||
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
|
||||
@@ -231,20 +192,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
UnorderedMap<Uint64, RenderPassEntry> m_renderPasses;
|
||||
// Monotonic frame counter (bumped in OnPresent) for render-pass cache aging.
|
||||
Uint64 m_frameCounter = 0;
|
||||
IEvictionObserver* m_evictionObserver = nullptr;
|
||||
|
||||
// Bumped whenever a renderbuffer VkImage is (re)created; together with the texture
|
||||
// manager's image epoch this invalidates the render-pass fast path on any attachment
|
||||
// image recreation.
|
||||
Uint64 m_renderbufferImageEpoch = 1;
|
||||
|
||||
public:
|
||||
// Bumped whenever a renderbuffer backing is (re)created; consecutive-draw
|
||||
// snapshots include it so an attachment respecify forces a re-resolve.
|
||||
Uint64 GetRenderbufferImageEpoch() const { return m_renderbufferImageEpoch; }
|
||||
|
||||
private:
|
||||
|
||||
// Per-draw fast-path memo for GetOrCreateRenderPass (dirty-flag state tracking): when the
|
||||
// framebuffer state is provably unchanged since the last resolution, the active render pass
|
||||
// is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch /
|
||||
@@ -257,26 +210,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 m_rpFastTexEpoch = 0;
|
||||
Uint64 m_rpFastRbEpoch = 0;
|
||||
Uint64 m_rpFastRenderPassHash = 0;
|
||||
// Whether the memoized entry carries a depth/stencil attachment; a
|
||||
// default-FBO resolution whose effective depth request differs must
|
||||
// miss the memo (the depth-less/depth-full flavors hash differently).
|
||||
Bool m_rpFastHadDepthStencil = false;
|
||||
|
||||
public:
|
||||
struct RenderbufferResource {
|
||||
// deadSinceFrame sentinel: the owning weak reference has not been observed
|
||||
// expired. Dead resources age past every in-flight frame before Destroy
|
||||
// (see CollectRenderbufferGarbage); the GPU may still reference the image
|
||||
// for frames-in-flight frames after the GL object dies.
|
||||
static constexpr Uint64 kNeverObservedDead = UINT64_MAX;
|
||||
|
||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VmaAllocation allocation = nullptr;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
// UNORM reinterpretation of an sRGB image, used as the attachment view while
|
||||
// GL_FRAMEBUFFER_SRGB is disabled (raw writes). Null for non-sRGB formats.
|
||||
VkImageView unormTwinView = VK_NULL_HANDLE;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
|
||||
@@ -284,8 +224,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
||||
Int samples = 0;
|
||||
// m_frameCounter value at which the weak reference was first seen expired.
|
||||
Uint64 deadSinceFrame = kNeverObservedDead;
|
||||
|
||||
void Destroy(VkDevice device, VmaAllocator allocator);
|
||||
};
|
||||
@@ -303,32 +241,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ClearAttachmentPayload payload{};
|
||||
};
|
||||
|
||||
// A superseded renderbuffer backing (glRenderbufferStorage respecify) parked
|
||||
// until enough frame boundaries have passed that no in-flight command buffer
|
||||
// can still reference it; destroyed in OnPresent (see RetireAgeFrames).
|
||||
struct DeferredRenderbufferRelease {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VmaAllocation allocation = nullptr;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VkImageView unormTwinView = VK_NULL_HANDLE;
|
||||
Uint64 deferredAtFrame = 0;
|
||||
};
|
||||
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
||||
// Supported sample counts per attachment format, so per-draw resource lookups
|
||||
// do not repeat vkGetPhysicalDeviceImageFormatProperties.
|
||||
UnorderedMap<VkFormat, VkSampleCountFlags> m_attachmentSampleCountsByFormat;
|
||||
|
||||
Bool HasPendingRenderbufferClear(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
|
||||
void CollectRenderbufferGarbage();
|
||||
// Frame-boundary margin after which a resource last referenced by a retired
|
||||
// GL object (or superseded backing) is provably past every in-flight frame.
|
||||
Uint64 RetireAgeFrames() const;
|
||||
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
|
||||
void CollectDeferredRenderbufferReleases(Bool destroyAll);
|
||||
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline ActiveRenderPassInfo s_activeRenderPass{};
|
||||
|
||||
@@ -51,18 +51,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) {
|
||||
return std::min(sampler.GetMinLod(), effectiveMaxLod);
|
||||
}
|
||||
|
||||
// A single-level view can only ever deliver the base level, but the LOD clamp must not be
|
||||
// collapsed to exactly 0: both GL and Vulkan pick magFilter over minFilter from the
|
||||
// *clamped* lambda, so maxLod = 0 would make every fragment magnify and quietly retire the
|
||||
// min filter. 0.25 is the value VkSamplerCreateInfo's own note prescribes for emulating
|
||||
// GL's non-mipmapped minification - large enough for lambda to stay positive, small enough
|
||||
// that a NEAREST mip mode still rounds down to level 0. Clamped rather than assigned, so a
|
||||
// texture whose GL_TEXTURE_MAX_LOD really is 0 keeps magnifying as GL says it must.
|
||||
Float ResolveSingleLevelMaxLod(const MG_State::GLState::SamplerObject& sampler, Bool singleLevelView) {
|
||||
const Float maxLod = ResolveEffectiveMaxLod(sampler);
|
||||
return singleLevelView ? std::min(maxLod, 0.25f) : maxLod;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool VkSamplerManager::Initialize(const InitInfo& initInfo) {
|
||||
@@ -101,43 +89,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
m_device = VK_NULL_HANDLE;
|
||||
m_config = nullptr;
|
||||
m_frameBoundaryCounter = 0;
|
||||
}
|
||||
|
||||
void VkSamplerManager::OnFrameBoundary() {
|
||||
++m_frameBoundaryCounter;
|
||||
|
||||
// Sweep occasionally; destroy samplers whose last use is far past every
|
||||
// in-flight frame. Destroy and erase must stay atomic, or Shutdown would
|
||||
// double-free the handle; an evicted key that recurs simply re-creates
|
||||
// its sampler on the next miss.
|
||||
constexpr Uint64 kSweepInterval = 256;
|
||||
constexpr Uint64 kRetireAgeBoundaries = 1024;
|
||||
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto it = m_samplers.begin(); it != m_samplers.end();) {
|
||||
auto& entry = it->second;
|
||||
if (m_frameBoundaryCounter - entry.lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
|
||||
vkDestroySampler(m_device, entry.handle, nullptr);
|
||||
}
|
||||
it = m_samplers.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Bool singleLevelView) const {
|
||||
Bool forceNearestFiltering) const {
|
||||
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
|
||||
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &singleLevelView, sizeof(singleLevelView)));
|
||||
|
||||
const auto minFilter = sampler.GetMinFilter();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
|
||||
@@ -151,7 +111,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT)));
|
||||
const auto wrapR = sampler.GetWrapR();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR)));
|
||||
const auto maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
||||
const auto maxLod = ResolveEffectiveMaxLod(sampler);
|
||||
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
|
||||
@@ -173,20 +133,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Uint32 viewLevelCount) {
|
||||
// A view that exposes a single mip level has no second level to blend with, so GL's
|
||||
// *_MIPMAP_* minification filters degenerate to plain filtering on the base level -
|
||||
// sampling is unchanged by pinning the Vulkan sampler to NEAREST mip mode at LOD 0.
|
||||
// It is not cosmetic: MobileGL backs such a view with a fully allocated mip chain whose
|
||||
// tail is never written, and a LINEAR mip mode lets the texture unit issue the level+1
|
||||
// fetch anyway. On Adreno that fetch lands in uninitialized UBWC pages (or past the
|
||||
// allocation for a genuinely single-level image) and faults the GPU - the same failure
|
||||
// the default-framebuffer blit shader had to work around with an explicit-LOD sample.
|
||||
const Bool singleLevelView = viewLevelCount == 1;
|
||||
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering, singleLevelView);
|
||||
Bool forceNearestFiltering) {
|
||||
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering);
|
||||
auto it = m_samplers.find(key);
|
||||
if (it != m_samplers.end()) {
|
||||
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return it->second.handle;
|
||||
}
|
||||
|
||||
@@ -194,9 +144,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||
samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter());
|
||||
samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter());
|
||||
samplerInfo.mipmapMode = (forceNearestFiltering || singleLevelView)
|
||||
? VK_SAMPLER_MIPMAP_MODE_NEAREST
|
||||
: ToVkMipmapMode(sampler.GetMipmapMode());
|
||||
samplerInfo.mipmapMode = forceNearestFiltering ? VK_SAMPLER_MIPMAP_MODE_NEAREST
|
||||
: ToVkMipmapMode(sampler.GetMipmapMode());
|
||||
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
|
||||
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
|
||||
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
|
||||
@@ -208,8 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
samplerInfo.maxAnisotropy = maxAnisotropy;
|
||||
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
|
||||
samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture));
|
||||
// Must match BuildSamplerKey's resolution exactly.
|
||||
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
||||
samplerInfo.maxLod = ResolveEffectiveMaxLod(sampler);
|
||||
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
|
||||
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
|
||||
samplerInfo.unnormalizedCoordinates = VK_FALSE;
|
||||
@@ -221,7 +169,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
entry.handle = vkSampler;
|
||||
entry.externalIndex = sampler.GetExternalIndex();
|
||||
entry.version = sampler.GetVersion();
|
||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
m_samplers[key] = entry;
|
||||
return vkSampler;
|
||||
}
|
||||
|
||||
@@ -33,38 +33,20 @@ public:
|
||||
Bool Initialize(const InitInfo& initInfo);
|
||||
void Shutdown();
|
||||
|
||||
// viewLevelCount is the mip-level count of the image view this sampler will be paired
|
||||
// with; 0 means "unknown, do not narrow". See GetOrCreateSampler for why it matters.
|
||||
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering = false,
|
||||
Uint32 viewLevelCount = 0);
|
||||
// Frame boundary hook: ages the sampler cache and destroys samplers not used
|
||||
// for many frames. The key hashes continuous float state (lodBias, LOD clamps,
|
||||
// anisotropy), so an app animating those would otherwise mint an unbounded
|
||||
// stream of never-destroyed VkSamplers and eventually exhaust the device's
|
||||
// maxSamplerAllocationCount. A sampler idle for over a thousand frame
|
||||
// boundaries cannot be referenced by any in-flight command buffer (frames in
|
||||
// flight are single digits), and every descriptor set the GPU consumes is
|
||||
// written that same frame with live handles (the per-binding resolve memo and
|
||||
// descriptor-set reuse are both frame-reset), so destruction here needs no
|
||||
// fence wait. Self-gated: one counter bump and compare except on sweep
|
||||
// boundaries.
|
||||
void OnFrameBoundary();
|
||||
Bool forceNearestFiltering = false);
|
||||
|
||||
private:
|
||||
struct SamplerCacheEntry {
|
||||
VkSampler handle = VK_NULL_HANDLE;
|
||||
Uint externalIndex = 0;
|
||||
Uint16 version = 0;
|
||||
// Frame boundary of the last cache hit; entries idle past the
|
||||
// OnFrameBoundary retirement age have their VkSampler destroyed.
|
||||
Uint64 lastUsedFrameBoundary = 0;
|
||||
};
|
||||
|
||||
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Bool singleLevelView) const;
|
||||
Bool forceNearestFiltering) const;
|
||||
static VkFilter ToVkFilter(SamplerFilterMode mode);
|
||||
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
|
||||
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
|
||||
@@ -85,8 +67,6 @@ private:
|
||||
Bool m_samplerAnisotropySupported = false;
|
||||
Float m_maxSamplerAnisotropy = 1.0f;
|
||||
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameBoundaryCounter = 0;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -120,21 +120,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
||||
// GL promises "at least the requested samples", so a non-power-of-two
|
||||
// request (legal in GL, e.g. 3) rounds up to the next Vulkan bit.
|
||||
if (requestedSamples <= 1) {
|
||||
switch (requestedSamples) {
|
||||
case 1:
|
||||
outSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
return true;
|
||||
}
|
||||
if (requestedSamples > 64) {
|
||||
case 2:
|
||||
outSampleCount = VK_SAMPLE_COUNT_2_BIT;
|
||||
return true;
|
||||
case 4:
|
||||
outSampleCount = VK_SAMPLE_COUNT_4_BIT;
|
||||
return true;
|
||||
case 8:
|
||||
outSampleCount = VK_SAMPLE_COUNT_8_BIT;
|
||||
return true;
|
||||
case 16:
|
||||
outSampleCount = VK_SAMPLE_COUNT_16_BIT;
|
||||
return true;
|
||||
case 32:
|
||||
outSampleCount = VK_SAMPLE_COUNT_32_BIT;
|
||||
return true;
|
||||
case 64:
|
||||
outSampleCount = VK_SAMPLE_COUNT_64_BIT;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
Uint32 bit = 1;
|
||||
while (bit < static_cast<Uint32>(requestedSamples)) {
|
||||
bit <<= 1;
|
||||
}
|
||||
outSampleCount = static_cast<VkSampleCountFlagBits>(bit);
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
|
||||
@@ -577,7 +587,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_allocator = initInfo.allocator;
|
||||
m_commandPool = initInfo.commandPool;
|
||||
m_graphicsQueue = initInfo.graphicsQueue;
|
||||
m_imageFormatListSupported = initInfo.imageFormatListSupported;
|
||||
m_currentFrameIndex = 0;
|
||||
m_deferredReleases.clear();
|
||||
m_deferredReleases.resize(initInfo.frameCount);
|
||||
@@ -597,14 +606,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void VkTextureManager::Shutdown() {
|
||||
if (m_device != VK_NULL_HANDLE) {
|
||||
ReclaimCompletedUploads(/*waitAll=*/true);
|
||||
}
|
||||
DestroyDeferredReleases();
|
||||
++m_resourceEraseEpoch; // every memoized resource pointer dies with the map
|
||||
m_textureResources.clear();
|
||||
m_aliveObjects.clear();
|
||||
m_storageImageTextures.clear();
|
||||
|
||||
m_device = VK_NULL_HANDLE;
|
||||
m_physicalDevice = VK_NULL_HANDLE;
|
||||
@@ -623,27 +627,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
frameIndex, m_deferredViewReleases.size());
|
||||
m_currentFrameIndex = frameIndex;
|
||||
CollectDeferredReleases(frameIndex);
|
||||
ReclaimCompletedUploads();
|
||||
|
||||
// Frame-boundary GC: every 64 frame boundaries (~1 s at 60 fps) bounds the reclaim
|
||||
// latency for dead textures regardless of draw traffic — workloads that churn
|
||||
// textures through clears/readbacks alone never reach the draw-gated
|
||||
// CollectGarbage. Must run after CollectDeferredReleases above: the prune defers
|
||||
// its releases into this frame's slot, which was just drained, so they are
|
||||
// destroyed only after the slot's fence has been waited again one full frame-ring
|
||||
// cycle from now (never while an in-flight frame may still reference them).
|
||||
constexpr Uint32 kGcFrameInterval = 64;
|
||||
++m_gcFrameCounter;
|
||||
if (m_gcFrameCounter % kGcFrameInterval == 0) {
|
||||
PruneDeadTextures();
|
||||
}
|
||||
}
|
||||
|
||||
void VkTextureManager::CollectAllDeferredReleases() {
|
||||
const SizeT frameCount = std::min(m_deferredReleases.size(), m_deferredViewReleases.size());
|
||||
for (SizeT frameIndex = 0; frameIndex < frameCount; ++frameIndex) {
|
||||
CollectDeferredReleases(static_cast<Uint32>(frameIndex));
|
||||
}
|
||||
}
|
||||
|
||||
void VkTextureManager::EraseTrackedTexture(const TextureIdentity& identity) {
|
||||
@@ -653,10 +636,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_textureResources.erase(resourceIt);
|
||||
}
|
||||
m_aliveObjects.erase(identity);
|
||||
m_storageImageTextures.erase(identity);
|
||||
// Invalidate every cross-draw sampled-texture memo: the erased
|
||||
// resource's address may be reused by a future emplace.
|
||||
++m_resourceEraseEpoch;
|
||||
}
|
||||
|
||||
void VkTextureManager::PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture) {
|
||||
@@ -712,63 +691,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-draw memo probe (see SyncedTextureMemoEntry): skips both map
|
||||
// lookups and the (re)registration path for repeat-bound textures.
|
||||
TextureResource* resourcePtr = nullptr;
|
||||
for (Uint32 i = 0; i < kSyncedTextureMemoSize; ++i) {
|
||||
const SyncedTextureMemoEntry& memo = m_syncedTextureMemo[i];
|
||||
if (memo.texture == &texture && memo.lifetimeId == identity.lifetimeId &&
|
||||
memo.eraseEpoch == m_resourceEraseEpoch) {
|
||||
resourcePtr = memo.resource;
|
||||
break;
|
||||
auto aliveIt = m_aliveObjects.find(identity);
|
||||
if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) {
|
||||
EraseTrackedTexture(aliveIt->first);
|
||||
aliveIt = m_aliveObjects.end();
|
||||
}
|
||||
|
||||
// Only (re)register and prune when this (texture, lifetime) pair is new: stale
|
||||
// aliases can only come into existence through an address reuse, which by
|
||||
// construction introduces a new identity. Doing this unconditionally made every
|
||||
// sampled-texture sync scan the entire alive-texture map per draw.
|
||||
if (aliveIt == m_aliveObjects.end()) {
|
||||
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
|
||||
if (liveTexture && liveTexture.get() == &texture) {
|
||||
m_aliveObjects[identity] = WeakPtr<MG_State::GLState::ITextureObject>(liveTexture);
|
||||
PruneStaleTextureAliases(&texture);
|
||||
}
|
||||
}
|
||||
|
||||
if (resourcePtr == nullptr) {
|
||||
auto aliveIt = m_aliveObjects.find(identity);
|
||||
if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) {
|
||||
EraseTrackedTexture(aliveIt->first);
|
||||
aliveIt = m_aliveObjects.end();
|
||||
}
|
||||
|
||||
// Only (re)register and prune when this (texture, lifetime) pair is new: stale
|
||||
// aliases can only come into existence through an address reuse, which by
|
||||
// construction introduces a new identity. Doing this unconditionally made every
|
||||
// sampled-texture sync scan the entire alive-texture map per draw.
|
||||
if (aliveIt == m_aliveObjects.end()) {
|
||||
WeakPtr<MG_State::GLState::ITextureObject> aliveTexture;
|
||||
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
|
||||
if (liveTexture && liveTexture.get() == &texture) {
|
||||
aliveTexture = liveTexture;
|
||||
} else {
|
||||
// The name lookup legally fails while the object is alive: the name was
|
||||
// deleted with the texture still attached to an FBO (the attachment's
|
||||
// SharedPtr keeps it alive), or the name was reused by a new texture, or
|
||||
// this is a default texture object (name 0 lives outside the name map).
|
||||
// Register through the object's own control block so the resource created
|
||||
// below still participates in weak-expiry GC instead of becoming an
|
||||
// orphan no reclamation path can reach until Shutdown.
|
||||
aliveTexture = texture.weak_from_this();
|
||||
}
|
||||
if (!aliveTexture.expired()) {
|
||||
m_aliveObjects[identity] = Move(aliveTexture);
|
||||
PruneStaleTextureAliases(&texture);
|
||||
}
|
||||
}
|
||||
|
||||
auto it = m_textureResources.find(identity);
|
||||
if (it == m_textureResources.end()) {
|
||||
TextureResource initial{};
|
||||
auto [insertIt, _] = m_textureResources.emplace(identity, Move(initial));
|
||||
it = insertIt;
|
||||
}
|
||||
resourcePtr = &(it->second);
|
||||
m_syncedTextureMemo[m_syncedTextureMemoNext] =
|
||||
SyncedTextureMemoEntry{&texture, identity.lifetimeId, m_resourceEraseEpoch, resourcePtr};
|
||||
m_syncedTextureMemoNext = (m_syncedTextureMemoNext + 1) % kSyncedTextureMemoSize;
|
||||
auto it = m_textureResources.find(identity);
|
||||
if (it == m_textureResources.end()) {
|
||||
TextureResource initial{};
|
||||
auto [insertIt, _] = m_textureResources.emplace(identity, Move(initial));
|
||||
it = insertIt;
|
||||
}
|
||||
|
||||
if (!SyncTexture(texture, *resourcePtr)) {
|
||||
if (!SyncTexture(texture, it->second)) {
|
||||
MGLOG_D("%s: Syncing texture %d failed", __func__, texture.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
@@ -782,11 +730,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
if (!recorded) {
|
||||
m_drawSyncedThisDraw.push_back({identity, resourcePtr});
|
||||
m_drawSyncedThisDraw.push_back({identity, &(it->second)});
|
||||
}
|
||||
}
|
||||
|
||||
return resourcePtr;
|
||||
return &(it->second);
|
||||
}
|
||||
|
||||
VkImageView VkTextureManager::GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel) {
|
||||
@@ -830,12 +778,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
const Bool framebufferSrgbEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
const VkFormat attachmentFormat = ResolveSrgbAttachmentWriteFormat(resource->format, framebufferSrgbEnabled);
|
||||
|
||||
if (attachmentFormat == resource->format && baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
|
||||
viewType == resource->viewType) {
|
||||
if (baseArrayLayer == 0 && layerCount == resource->arrayLayers && viewType == resource->viewType) {
|
||||
return GetOrCreateViewAtMipLevel(texture, mipLevel);
|
||||
}
|
||||
|
||||
@@ -844,7 +787,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.baseArrayLayer = baseArrayLayer,
|
||||
.layerCount = layerCount,
|
||||
.viewType = viewType,
|
||||
.viewFormat = attachmentFormat,
|
||||
};
|
||||
auto it = resource->attachmentViews.find(key);
|
||||
if (it == resource->attachmentViews.end()) {
|
||||
@@ -855,7 +797,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return attachmentView;
|
||||
}
|
||||
|
||||
attachmentView = CreateImageView(resource->image, attachmentFormat, resource->aspect, viewType,
|
||||
attachmentView = CreateImageView(resource->image, resource->format, resource->aspect, viewType,
|
||||
mipLevel, 1, baseArrayLayer, layerCount);
|
||||
if (attachmentView == VK_NULL_HANDLE) {
|
||||
MGLOG_D("%s: CreateImageView failed for textureId=%d mipLevel=%u baseArrayLayer=%u layerCount=%u viewType=%d",
|
||||
@@ -1071,16 +1013,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return view;
|
||||
}
|
||||
|
||||
void VkTextureManager::StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture) {
|
||||
if (texture == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(texture));
|
||||
if (it != m_textureResources.end()) {
|
||||
it->second.lastRecordingGeneration = m_recordingGeneration;
|
||||
}
|
||||
}
|
||||
|
||||
void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) {
|
||||
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(texture));
|
||||
@@ -1108,8 +1040,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(writtenMipLevel < resource.mipLevels,
|
||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d mipLevel=%u out of range %u",
|
||||
texture->GetExternalIndex(), writtenMipLevel, resource.mipLevels);
|
||||
// Pre-pass stream bookkeeping: the render pass that just ended wrote this image.
|
||||
StampResourceRecordingUse(resource);
|
||||
|
||||
if (resource.layout != newLayout && resource.mipLevels > 1) {
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
@@ -1194,8 +1124,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
||||
resource->arrayLayers);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
StampResourceRecordingUse(*resource);
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -1225,30 +1153,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
|
||||
texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
StampResourceRecordingUse(*resource);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) {
|
||||
m_storageImageTextures.insert(MakeTextureIdentity(&texture));
|
||||
}
|
||||
|
||||
Bool VkTextureManager::NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const {
|
||||
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
||||
if (m_storageImageTextures.find(identity) == m_storageImageTextures.end()) {
|
||||
return false;
|
||||
}
|
||||
const auto it = m_textureResources.find(identity);
|
||||
// No image yet: the first sync creates it with STORAGE straight away, so there is nothing
|
||||
// to preserve and nothing to order against.
|
||||
return it != m_textureResources.end() && it->second.image != VK_NULL_HANDLE &&
|
||||
!it->second.storageUsageResolved;
|
||||
}
|
||||
|
||||
Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const {
|
||||
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
||||
const auto it = m_textureResources.find(identity);
|
||||
const auto it = m_textureResources.find(MakeTextureIdentity(&texture));
|
||||
if (it == m_textureResources.end()) {
|
||||
return true;
|
||||
}
|
||||
@@ -1256,12 +1165,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (resource.image == VK_NULL_HANDLE || resource.layout != VK_IMAGE_LAYOUT_GENERAL) {
|
||||
return true;
|
||||
}
|
||||
// The image predates this texture's first image-unit binding, so it was created without
|
||||
// STORAGE usage and has to be recreated - which is illegal inside a render pass.
|
||||
if (!resource.storageUsageResolved &&
|
||||
m_storageImageTextures.find(identity) != m_storageImageTextures.end()) {
|
||||
return true;
|
||||
}
|
||||
// Mirror SyncTexture's cross-draw skip condition: any version drift means the sync
|
||||
// path may upload or rebuild, both of which need the render pass ended first.
|
||||
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
||||
@@ -1309,22 +1212,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
SizeT VkTextureManager::CollectGarbage() {
|
||||
// Draw-gated stagger (1 in 256 calls): keeps the per-draw cost at one counter
|
||||
// bump. The guaranteed reclaim path is the frame-boundary prune in BeginFrame;
|
||||
// this remains as a cheap assist so draw-heavy workloads reclaim sooner.
|
||||
m_gcCounter++;
|
||||
if (m_gcCounter != 0) {
|
||||
return 0;
|
||||
}
|
||||
return PruneDeadTextures();
|
||||
}
|
||||
|
||||
SizeT VkTextureManager::PruneDeadTextures() {
|
||||
// Erasing entries would dangle the raw TextureResource pointers memoized for the
|
||||
// current draw; every call path (BeginFrame, and CollectGarbage at the top of a
|
||||
// freshly opened draw-sync scope) runs before any memo entry is recorded.
|
||||
MOBILEGL_ASSERT(m_drawSyncedThisDraw.empty(),
|
||||
"PruneDeadTextures: draw-sync memo holds raw resource pointers an erase would dangle");
|
||||
|
||||
Vector<MG_State::GLState::ITextureObject*> expiredTextures;
|
||||
expiredTextures.reserve(m_aliveObjects.size());
|
||||
@@ -1336,25 +1227,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (auto* texture : expiredTextures) {
|
||||
PruneStaleTextureAliases(texture);
|
||||
}
|
||||
SizeT prunedCount = expiredTextures.size();
|
||||
|
||||
// Orphan sweep: after the pass above, m_aliveObjects holds only live entries.
|
||||
// Registration in SyncTextureAndGetDescriptor cannot fail for a SharedPtr-owned
|
||||
// texture (weak_from_this fallback), so a resource whose identity has no alive
|
||||
// entry has no trackable owner: its GL-side object is gone, or was never
|
||||
// shared-owned, in which case recreation on a later sync is the safe fallback.
|
||||
// Destruction goes through the per-frame deferred queues, never immediate.
|
||||
Vector<TextureIdentity> orphanIdentities;
|
||||
for (auto it = m_textureResources.begin(); it != m_textureResources.end(); ++it) {
|
||||
if (m_aliveObjects.find(it->first) == m_aliveObjects.end()) {
|
||||
orphanIdentities.emplace_back(it->first);
|
||||
}
|
||||
}
|
||||
for (const auto& identity : orphanIdentities) {
|
||||
EraseTrackedTexture(identity);
|
||||
}
|
||||
prunedCount += orphanIdentities.size();
|
||||
return prunedCount;
|
||||
return expiredTextures.size();
|
||||
}
|
||||
|
||||
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
|
||||
@@ -1368,13 +1241,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
||||
const Uint32 syncingMipLevelCount =
|
||||
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u;
|
||||
// A pending storage-usage upgrade also has to bust the skip: nothing about the texture's
|
||||
// content or params changed, but the image itself must be recreated with STORAGE usage
|
||||
// before it can back an image-unit descriptor.
|
||||
const Bool storageUpgradePending =
|
||||
!outResource.storageUsageResolved &&
|
||||
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
|
||||
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
|
||||
if (outResource.image != VK_NULL_HANDLE &&
|
||||
outResource.syncedContentVersion == syncingContentVersion &&
|
||||
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
|
||||
outResource.syncedMipLevelCount == syncingMipLevelCount) {
|
||||
@@ -1444,23 +1311,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const IntVec3 &texelSize, SizeT byteSize, Uint32 mipLevels,
|
||||
TextureResource &resource) {
|
||||
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||
VkFormat format = formatInfo.format;
|
||||
const VkFormat format = formatInfo.format;
|
||||
if (format == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_D("%s: format == VK_FORMAT_UNDEFINED", __func__);
|
||||
return false;
|
||||
}
|
||||
// X8_D24 lacks optimal-tiling support on several drivers (lavapipe included);
|
||||
// D32_SFLOAT holds every 24-bit depth value exactly, and the upload path
|
||||
// converts the shadow words to float (see the pure-depth branch below).
|
||||
if (format == VK_FORMAT_X8_D24_UNORM_PACK32) {
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||
constexpr VkFormatFeatureFlags kDepthAttachmentAndSample =
|
||||
VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT;
|
||||
if ((formatProperties.optimalTilingFeatures & kDepthAttachmentAndSample) != kDepthAttachmentAndSample) {
|
||||
format = VK_FORMAT_D32_SFLOAT;
|
||||
}
|
||||
}
|
||||
if (texelSize.x() <= 0 || texelSize.y() <= 0 /*|| byteSize == 0*/) {
|
||||
MGLOG_D("%s: texelSize or byteSize is zero", __func__);
|
||||
return false;
|
||||
@@ -1470,17 +1325,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
const Bool isMultisampleTexture = IsMultisampleTextureUploadTarget(uploadTarget);
|
||||
// A texture that has only ever defined level 0 gets a single-level backing
|
||||
// (ANGLE's model). Preallocating the full chain put every render target
|
||||
// onto Adreno's multi-mip image layout and grew each texture by a third
|
||||
// for levels most textures never define. Once a second level is defined
|
||||
// the backing is recreated ONE time with the full chain (the
|
||||
// preserve-copy path below carries the pixels over), so sequentially-
|
||||
// defined atlas mips do not recreate per level, and glGenerateMipmap -
|
||||
// which defines every level before syncing - works unchanged.
|
||||
const Uint32 backingMipLevels =
|
||||
isMultisampleTexture ? 1u
|
||||
: (mipLevels > 1 ? std::max(mipLevels, ComputeFullMipLevelCount(texelSize)) : 1u);
|
||||
isMultisampleTexture ? 1u : std::max(mipLevels, ComputeFullMipLevelCount(texelSize));
|
||||
TextureShapeInfo shapeInfo{};
|
||||
const Bool supportedShape = TryResolveTextureShapeInfo(texture, uploadTarget, texelSize, shapeInfo);
|
||||
MOBILEGL_ASSERT(supportedShape,
|
||||
@@ -1506,89 +1352,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkImageAspectFlags aspect = GetAspectMaskForFormat(format);
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||
// Only textures that have actually been bound to a GL image unit get STORAGE usage (and
|
||||
// the MUTABLE_FORMAT it drags in for format-reinterpreting image views). Requesting it
|
||||
// for every storage-capable colour texture costs real bandwidth: Adreno cannot keep UBWC
|
||||
// compression on an image that may be written through a storage descriptor, so the whole
|
||||
// render target - MC's included - runs uncompressed. MarkStorageImageTexture upgrades a
|
||||
// texture before its first image-unit draw, and the usage below feeds the compatibility
|
||||
// check so the upgrade recreates the image.
|
||||
const Bool markedAsStorageImage =
|
||||
m_storageImageTextures.find(MakeTextureIdentity(
|
||||
const_cast<MG_State::GLState::ITextureObject*>(&texture))) != m_storageImageTextures.end();
|
||||
// Storage-image CAPABILITY (does the format allow it at all) is deliberately separate from
|
||||
// whether this texture actually needs the usage. MUTABLE_FORMAT keys off capability, as
|
||||
// before: format-reinterpreting views are not a storage-only concern - the SAMPLED path
|
||||
// needs them too (GetOrCreateSampledImageView bails out without it, see ~line 892), so
|
||||
// tying MUTABLE_FORMAT to the image-unit mark would break sampled format reinterpretation
|
||||
// for every texture that never becomes a storage image.
|
||||
const Bool storageImageCapable =
|
||||
const Bool supportsStorageImage =
|
||||
!isMultisampleTexture &&
|
||||
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
|
||||
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
|
||||
const Bool supportsStorageImage = storageImageCapable && markedAsStorageImage;
|
||||
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
|
||||
if (storageImageCapable && IsMutableStorageImageFormat(format) &&
|
||||
if (supportsStorageImage && IsMutableStorageImageFormat(format) &&
|
||||
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
||||
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
// sRGB color images attach through their UNORM twin while GL_FRAMEBUFFER_SRGB is
|
||||
// disabled (see ResolveSrgbAttachmentWriteFormat), which needs format-reinterpreting
|
||||
// views - multisample sRGB render targets included.
|
||||
if (ResolveSrgbAttachmentWriteFormat(format, false) != format &&
|
||||
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
|
||||
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
||||
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
|
||||
VkImageUsageFlags desiredUsage =
|
||||
VK_IMAGE_USAGE_SAMPLED_BIT |
|
||||
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
|
||||
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
|
||||
(((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) ?
|
||||
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT :
|
||||
0);
|
||||
if (!isMultisampleTexture) {
|
||||
desiredUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
}
|
||||
|
||||
// Round a multisample request up to a count the device supports for this
|
||||
// format (GL only promises "at least"), mirroring the renderbuffer path.
|
||||
if (isMultisampleTexture && resolvedSampleCount != VK_SAMPLE_COUNT_1_BIT) {
|
||||
auto supportedIt = m_multisampleCountsByFormat.find(format);
|
||||
if (supportedIt == m_multisampleCountsByFormat.end()) {
|
||||
VkImageFormatProperties imageFormatProperties{};
|
||||
VkSampleCountFlags supported = VK_SAMPLE_COUNT_1_BIT;
|
||||
if (vkGetPhysicalDeviceImageFormatProperties(m_physicalDevice, format, shapeInfo.imageType,
|
||||
VK_IMAGE_TILING_OPTIMAL, desiredUsage, imageCreateFlags,
|
||||
&imageFormatProperties) == VK_SUCCESS) {
|
||||
supported = imageFormatProperties.sampleCounts;
|
||||
}
|
||||
supportedIt = m_multisampleCountsByFormat.emplace(format, supported).first;
|
||||
}
|
||||
const VkSampleCountFlags supported = supportedIt->second;
|
||||
if ((supported & resolvedSampleCount) == 0) {
|
||||
Uint32 rounded = 0;
|
||||
for (Uint32 bit = static_cast<Uint32>(resolvedSampleCount) << 1; bit <= VK_SAMPLE_COUNT_64_BIT;
|
||||
bit <<= 1) {
|
||||
if ((supported & bit) != 0) {
|
||||
rounded = bit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rounded == 0) {
|
||||
for (Uint32 bit = static_cast<Uint32>(resolvedSampleCount) >> 1; bit != 0; bit >>= 1) {
|
||||
if ((supported & bit) != 0) {
|
||||
rounded = bit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rounded != 0) {
|
||||
resolvedSampleCount = static_cast<VkSampleCountFlagBits>(rounded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
|
||||
resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
|
||||
@@ -1598,7 +1370,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.viewType == shapeInfo.viewType &&
|
||||
resource.sampleCount == resolvedSampleCount &&
|
||||
resource.imageCreateFlags == imageCreateFlags &&
|
||||
resource.usageFlags == desiredUsage &&
|
||||
resource.mipLevels == backingMipLevels;
|
||||
if (compatible) {
|
||||
if (resource.perMipViews.size() != backingMipLevels) {
|
||||
@@ -1607,10 +1378,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (resource.perMipSampledViews.size() != backingMipLevels) {
|
||||
resource.perMipSampledViews.resize(backingMipLevels, VK_NULL_HANDLE);
|
||||
}
|
||||
// Keeping the image is itself the answer to the mark: either it already carries
|
||||
// STORAGE, or this format can never carry it. Either way there is nothing left to
|
||||
// recreate, so stop reporting the texture as needing preparation.
|
||||
resource.storageUsageResolved = markedAsStorageImage;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1625,10 +1392,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.sampleCount == resolvedSampleCount &&
|
||||
resource.imageCreateFlags == imageCreateFlags &&
|
||||
resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT &&
|
||||
// '<=' rather than '<': a storage-usage upgrade recreates the image with an
|
||||
// unchanged mip count, and its contents (a render target's pixels live only on the
|
||||
// GPU) still have to survive. The vkCmdCopyImage below copies min(mipLevels).
|
||||
resource.mipLevels <= backingMipLevels &&
|
||||
resource.mipLevels < backingMipLevels &&
|
||||
resource.layout != VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
|
||||
std::unique_ptr<TextureResource> preservedResource;
|
||||
@@ -1650,37 +1414,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
imageInfo.format = format;
|
||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
imageInfo.usage = desiredUsage;
|
||||
imageInfo.samples = resolvedSampleCount;
|
||||
|
||||
// Bound the mutability. A blindly-mutable image has to be laid out so that ANY format in
|
||||
// its compatibility class can be viewed, which costs bandwidth compression on tilers;
|
||||
// naming the exact set instead lets the driver keep it. Only safe when that set really is
|
||||
// exhaustive, so it is restricted to textures that are not image-unit bound: sampled views
|
||||
// can only ever ask for ResolveSampledImageViewFormat's output, whereas glBindImageTexture
|
||||
// may name any compatible format, which nothing here can enumerate ahead of time.
|
||||
Vector<VkFormat> viewFormats;
|
||||
VkImageFormatListCreateInfo formatListInfo{};
|
||||
if (m_imageFormatListSupported && !supportsStorageImage &&
|
||||
(imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
||||
viewFormats.push_back(format);
|
||||
for (const SamplerNumericDomain domain : {SamplerNumericDomain::Float,
|
||||
SamplerNumericDomain::SignedInteger,
|
||||
SamplerNumericDomain::UnsignedInteger}) {
|
||||
const VkFormat viewFormat = ResolveSampledImageViewFormat(format, domain);
|
||||
if (viewFormat == VK_FORMAT_UNDEFINED) {
|
||||
continue;
|
||||
}
|
||||
if (std::find(viewFormats.begin(), viewFormats.end(), viewFormat) == viewFormats.end()) {
|
||||
viewFormats.push_back(viewFormat);
|
||||
}
|
||||
}
|
||||
formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
|
||||
formatListInfo.viewFormatCount = static_cast<Uint32>(viewFormats.size());
|
||||
formatListInfo.pViewFormats = viewFormats.data();
|
||||
imageInfo.pNext = &formatListInfo;
|
||||
imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT |
|
||||
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
|
||||
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
|
||||
(((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) ?
|
||||
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT :
|
||||
0);
|
||||
if (!isMultisampleTexture) {
|
||||
imageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
}
|
||||
|
||||
imageInfo.samples = resolvedSampleCount;
|
||||
if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
||||
VkImageFormatProperties imageFormatProperties{};
|
||||
VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||
@@ -1719,21 +1462,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VmaAllocationCreateInfo allocationInfo{};
|
||||
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
||||
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
||||
// Soft failure like the unsupported-sample-count path above: a driver can pass the
|
||||
// vkGetPhysicalDeviceImageFormatProperties pre-check yet still refuse the creation
|
||||
// (e.g. multisampled depth on lavapipe); the texture simply stays unbacked.
|
||||
const VkResult createImageResult =
|
||||
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr);
|
||||
if (createImageResult != VK_SUCCESS) {
|
||||
MGLOG_F("SyncTextureResource: vmaCreateImage failed (%d) textureId=%d extent=%ux%u depth=%u layers=%u "
|
||||
"mips=%u samples=%d format=%d",
|
||||
createImageResult, texture.GetExternalIndex(), imageInfo.extent.width, imageInfo.extent.height,
|
||||
imageInfo.extent.depth, imageInfo.arrayLayers, imageInfo.mipLevels,
|
||||
static_cast<Int>(imageInfo.samples), static_cast<Int>(imageInfo.format));
|
||||
resource.image = VK_NULL_HANDLE;
|
||||
resource.allocation = nullptr;
|
||||
return false;
|
||||
}
|
||||
VK_VERIFY(vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr),
|
||||
"vmaCreateImage(texture)");
|
||||
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
|
||||
|
||||
resource.layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
@@ -1750,8 +1480,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.viewType = shapeInfo.viewType;
|
||||
resource.sampleCount = resolvedSampleCount;
|
||||
resource.imageCreateFlags = imageCreateFlags;
|
||||
resource.usageFlags = imageInfo.usage;
|
||||
resource.storageUsageResolved = markedAsStorageImage;
|
||||
resource.syncedTextureParamsVersion = 0;
|
||||
|
||||
if (preservedResource) {
|
||||
@@ -1801,28 +1529,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_deferredViewReleases[frameIndex].clear();
|
||||
}
|
||||
|
||||
void VkTextureManager::ReclaimCompletedUploads(Bool waitAll) {
|
||||
if (m_pendingUploadReclaims.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SizeT completed = 0;
|
||||
for (; completed < m_pendingUploadReclaims.size(); ++completed) {
|
||||
PendingUploadReclaim& entry = m_pendingUploadReclaims[completed];
|
||||
if (waitAll) {
|
||||
VK_VERIFY(vkWaitForFences(m_device, 1, &entry.fence, VK_TRUE, UINT64_MAX),
|
||||
"vkWaitForFences(texture upload reclaim)");
|
||||
} else if (vkGetFenceStatus(m_device, entry.fence) != VK_SUCCESS) {
|
||||
break;
|
||||
}
|
||||
vkDestroyFence(m_device, entry.fence, nullptr);
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &entry.commandBuffer);
|
||||
vmaDestroyBuffer(m_allocator, entry.stagingBuffer, entry.stagingAllocation);
|
||||
}
|
||||
m_pendingUploadReclaims.erase(m_pendingUploadReclaims.begin(),
|
||||
m_pendingUploadReclaims.begin() + static_cast<std::ptrdiff_t>(completed));
|
||||
}
|
||||
|
||||
void VkTextureManager::DestroyDeferredReleases() {
|
||||
for (auto& deferredReleases : m_deferredReleases) {
|
||||
deferredReleases.clear();
|
||||
@@ -2024,119 +1730,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Combined depth-stencil images need per-aspect copies (VkBufferImageCopy aspectMask
|
||||
// must have exactly one bit set), so de-interleave the shadow's GL wire format into
|
||||
// a depth plane followed by a stencil plane per upload item.
|
||||
// Combined depth-stencil images need per-aspect de-interleaved copies (VkBufferImageCopy
|
||||
// aspectMask must have exactly one bit set). Until that is implemented, skip the upload
|
||||
// instead of recording an invalid command buffer that kills the process.
|
||||
const VkImageAspectFlags uploadAspectMask = GetAspectMaskForFormat(outResource.format);
|
||||
const Bool isCombinedDepthStencil =
|
||||
(uploadAspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) && (uploadAspectMask & VK_IMAGE_ASPECT_STENCIL_BIT);
|
||||
if (isCombinedDepthStencil) {
|
||||
const Bool srcIsD24S8 = outResource.format == VK_FORMAT_D24_UNORM_S8_UINT;
|
||||
const Bool srcIsD32FS8 = outResource.format == VK_FORMAT_D32_SFLOAT_S8_UINT;
|
||||
if (!srcIsD24S8 && !srcIsD32FS8) {
|
||||
MGLOG_E("UploadDirtyMipLevels: unsupported combined depth-stencil format %d for textureId=%d",
|
||||
static_cast<Int>(outResource.format), mipmapTexture.GetExternalIndex());
|
||||
for (const auto& item : uploadItems) {
|
||||
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
stagingSize = 0;
|
||||
for (auto& item : uploadItems) {
|
||||
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
|
||||
static_cast<SizeT>(item.texelSize.y()) *
|
||||
static_cast<SizeT>(std::max(item.texelSize.z(), 1));
|
||||
const SizeT shadowTexelSize = item.uploadByteSize / std::max<SizeT>(texelCount, 1);
|
||||
MOBILEGL_ASSERT(shadowTexelSize == 4 || shadowTexelSize == 8,
|
||||
"UploadDirtyMipLevels: unexpected depth-stencil shadow texel size %zu for textureId=%d",
|
||||
shadowTexelSize, mipmapTexture.GetExternalIndex());
|
||||
// Depth plane as the aspect's buffer-copy format (32-bit word for
|
||||
// D24: low 24 bits; float for D32F), then one stencil byte per texel.
|
||||
Vector<Uint8> deinterleaved(texelCount * 4 + texelCount);
|
||||
Uint8* depthPlane = deinterleaved.data();
|
||||
Uint8* stencilPlane = deinterleaved.data() + texelCount * 4;
|
||||
const Uint8* shadow = static_cast<const Uint8*>(item.source);
|
||||
for (SizeT t = 0; t < texelCount; ++t) {
|
||||
if (shadowTexelSize == 8) {
|
||||
// GL_FLOAT_32_UNSIGNED_INT_24_8_REV: float depth, then a word
|
||||
// with stencil in its low 8 bits.
|
||||
float depthValue;
|
||||
Uint32 stencilWord;
|
||||
std::memcpy(&depthValue, shadow + t * 8, sizeof(depthValue));
|
||||
std::memcpy(&stencilWord, shadow + t * 8 + 4, sizeof(stencilWord));
|
||||
if (srcIsD32FS8) {
|
||||
std::memcpy(depthPlane + t * 4, &depthValue, sizeof(depthValue));
|
||||
} else {
|
||||
const float clamped = std::min(std::max(depthValue, 0.0f), 1.0f);
|
||||
const Uint32 depthWord = static_cast<Uint32>(clamped * 16777215.0f + 0.5f);
|
||||
std::memcpy(depthPlane + t * 4, &depthWord, sizeof(depthWord));
|
||||
}
|
||||
stencilPlane[t] = static_cast<Uint8>(stencilWord & 0xFFu);
|
||||
} else {
|
||||
// GL_UNSIGNED_INT_24_8: depth in the high 24 bits, stencil low 8.
|
||||
Uint32 packed;
|
||||
std::memcpy(&packed, shadow + t * 4, sizeof(packed));
|
||||
if (srcIsD24S8) {
|
||||
const Uint32 depthWord = packed >> 8;
|
||||
std::memcpy(depthPlane + t * 4, &depthWord, sizeof(depthWord));
|
||||
} else {
|
||||
const float depthValue = static_cast<float>(packed >> 8) / 16777215.0f;
|
||||
std::memcpy(depthPlane + t * 4, &depthValue, sizeof(depthValue));
|
||||
}
|
||||
stencilPlane[t] = static_cast<Uint8>(packed & 0xFFu);
|
||||
}
|
||||
}
|
||||
item.expandedData = Move(deinterleaved);
|
||||
item.source = item.expandedData.data();
|
||||
item.uploadByteSize = item.expandedData.size();
|
||||
item.offset = stagingSize;
|
||||
stagingSize += static_cast<VkDeviceSize>(item.uploadByteSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Pure-depth images whose canonical shadow layout differs from the image texel
|
||||
// layout (the shadow keeps a full-scale 16/32-bit unorm word or a float; the
|
||||
// image may be X8_D24 or a D32_SFLOAT fallback) convert per texel here.
|
||||
if (uploadAspectMask == VK_IMAGE_ASPECT_DEPTH_BIT) {
|
||||
const TextureInternalFormat depthInternal = mipmapTexture.GetFormat();
|
||||
const Bool shadowIsFloat = depthInternal == TextureInternalFormat::DepthComponent32F;
|
||||
const Bool dstIsFloat = outResource.format == VK_FORMAT_D32_SFLOAT;
|
||||
const Bool dstIsD24Word = outResource.format == VK_FORMAT_X8_D24_UNORM_PACK32;
|
||||
stagingSize = 0;
|
||||
for (auto& item : uploadItems) {
|
||||
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
|
||||
static_cast<SizeT>(item.texelSize.y()) *
|
||||
static_cast<SizeT>(std::max(item.texelSize.z(), 1));
|
||||
const SizeT shadowTexelSize = item.uploadByteSize / std::max<SizeT>(texelCount, 1);
|
||||
const Bool needsConversion =
|
||||
(dstIsFloat && !shadowIsFloat) || (dstIsD24Word && shadowTexelSize == 4 && !shadowIsFloat);
|
||||
if (needsConversion) {
|
||||
Vector<Uint8> converted(texelCount * 4);
|
||||
const Uint8* shadow = static_cast<const Uint8*>(item.source);
|
||||
for (SizeT t = 0; t < texelCount; ++t) {
|
||||
Uint32 wide = 0;
|
||||
if (shadowTexelSize == 2) {
|
||||
Uint16 raw = 0;
|
||||
std::memcpy(&raw, shadow + t * 2, sizeof(raw));
|
||||
wide = (static_cast<Uint32>(raw) << 16) | raw;
|
||||
} else {
|
||||
std::memcpy(&wide, shadow + t * 4, sizeof(wide));
|
||||
}
|
||||
if (dstIsFloat) {
|
||||
const float value = static_cast<float>(static_cast<double>(wide) / 4294967295.0);
|
||||
std::memcpy(converted.data() + t * 4, &value, sizeof(value));
|
||||
} else { // X8_D24: depth in the low 24 bits of a 32-bit word
|
||||
const Uint32 word = wide >> 8;
|
||||
std::memcpy(converted.data() + t * 4, &word, sizeof(word));
|
||||
}
|
||||
}
|
||||
item.expandedData = Move(converted);
|
||||
item.source = item.expandedData.data();
|
||||
item.uploadByteSize = item.expandedData.size();
|
||||
}
|
||||
item.offset = stagingSize;
|
||||
stagingSize += static_cast<VkDeviceSize>(item.uploadByteSize);
|
||||
if ((uploadAspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) && (uploadAspectMask & VK_IMAGE_ASPECT_STENCIL_BIT)) {
|
||||
MGLOG_E("UploadDirtyMipLevels: skipping unimplemented depth-stencil data upload for textureId=%d",
|
||||
mipmapTexture.GetExternalIndex());
|
||||
for (const auto& item : uploadItems) {
|
||||
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
VkBuffer stagingBuffer = VK_NULL_HANDLE;
|
||||
@@ -2189,14 +1793,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
||||
MOBILEGL_ASSERT(ok, "TransitionImageLayout to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL failed");
|
||||
|
||||
// Array textures keep their GL "depth" in VkImage array layers, so the
|
||||
// copy must address layerCount, not imageExtent.depth (which is invalid
|
||||
// for 2D images and silently dropped every layer past the first).
|
||||
const Bool depthSelectsArrayLayer = outResource.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ||
|
||||
outResource.viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY ||
|
||||
outResource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
|
||||
for (const auto& item : uploadItems) {
|
||||
const Uint32 depthOrLayers = item.texelSize.z() > 0 ? static_cast<Uint32>(item.texelSize.z()) : 1u;
|
||||
VkBufferImageCopy copy{};
|
||||
copy.bufferOffset = item.offset;
|
||||
copy.bufferRowLength = 0;
|
||||
@@ -2204,24 +1801,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
copy.imageSubresource.aspectMask = aspectMask;
|
||||
copy.imageSubresource.mipLevel = item.level;
|
||||
copy.imageSubresource.baseArrayLayer = item.baseArrayLayer;
|
||||
copy.imageSubresource.layerCount = depthSelectsArrayLayer ? depthOrLayers : 1;
|
||||
copy.imageSubresource.layerCount = 1;
|
||||
copy.imageOffset = {0, 0, 0};
|
||||
copy.imageExtent = {static_cast<Uint32>(item.texelSize.x()), static_cast<Uint32>(item.texelSize.y()),
|
||||
depthSelectsArrayLayer ? 1u : depthOrLayers};
|
||||
if (isCombinedDepthStencil) {
|
||||
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
|
||||
static_cast<SizeT>(item.texelSize.y()) *
|
||||
static_cast<SizeT>(std::max(item.texelSize.z(), 1));
|
||||
VkBufferImageCopy depthCopy = copy;
|
||||
depthCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
VkBufferImageCopy stencilCopy = copy;
|
||||
stencilCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
stencilCopy.bufferOffset = item.offset + static_cast<VkDeviceSize>(texelCount) * 4;
|
||||
const VkBufferImageCopy copies[2] = {depthCopy, stencilCopy};
|
||||
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 2, copies);
|
||||
continue;
|
||||
}
|
||||
item.texelSize.z() > 0 ? static_cast<Uint32>(item.texelSize.z()) : 1u};
|
||||
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
1, ©);
|
||||
}
|
||||
@@ -2252,23 +1835,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_VERIFY(vkCreateFence(m_device, &fenceInfo, nullptr, &uploadFence), "vkCreateFence(texture upload)");
|
||||
|
||||
VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, uploadFence), "vkQueueSubmit(texture)");
|
||||
// Do NOT wait the fence here: this submit sits behind the previous
|
||||
// frame's rendering on the queue, so a synchronous wait stalls the CPU
|
||||
// until the GPU drains - a per-frame vkQueueWaitIdle for any workload
|
||||
// with animated textures. Ordering against the current frame's draws is
|
||||
// already guaranteed (its command buffer is submitted later, at
|
||||
// present), so only the transient objects need to survive execution;
|
||||
// park them until the fence signals.
|
||||
m_pendingUploadReclaims.push_back({uploadFence, commandBuffer, stagingBuffer, stagingAllocation});
|
||||
ReclaimCompletedUploads();
|
||||
// Backstop for pathological upload storms: bound in-flight staging
|
||||
// memory by blocking on the oldest upload only once the list is deep.
|
||||
constexpr SizeT kMaxPendingTextureUploads = 16;
|
||||
if (m_pendingUploadReclaims.size() > kMaxPendingTextureUploads) {
|
||||
VK_VERIFY(vkWaitForFences(m_device, 1, &m_pendingUploadReclaims.front().fence, VK_TRUE, UINT64_MAX),
|
||||
"vkWaitForFences(texture upload backstop)");
|
||||
ReclaimCompletedUploads();
|
||||
}
|
||||
VK_VERIFY(vkWaitForFences(m_device, 1, &uploadFence, VK_TRUE, UINT64_MAX), "vkWaitForFences(texture upload)");
|
||||
vkDestroyFence(m_device, uploadFence, nullptr);
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &commandBuffer);
|
||||
|
||||
vmaDestroyBuffer(m_allocator, stagingBuffer, stagingAllocation);
|
||||
|
||||
if (!ok) {
|
||||
MGLOG_D("%s: texture upload cmd failed", __func__);
|
||||
|
||||
@@ -28,9 +28,6 @@ public:
|
||||
// manager keys its per-draw fast path on this so an attachment's image recreation
|
||||
// invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1).
|
||||
Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; }
|
||||
// Bumped whenever any tracked texture resource is erased; cached
|
||||
// TextureResource pointers are valid only while this is unchanged.
|
||||
Uint64 GetResourceEraseEpoch() const { return m_resourceEraseEpoch; }
|
||||
|
||||
struct TextureIdentity {
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
@@ -56,9 +53,6 @@ public:
|
||||
VkCommandPool commandPool = VK_NULL_HANDLE;
|
||||
VkQueue graphicsQueue = VK_NULL_HANDLE;
|
||||
Uint32 frameCount = 0;
|
||||
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
|
||||
// formats they will be viewed as, which is what lets a tiler keep them compressed.
|
||||
Bool imageFormatListSupported = false;
|
||||
};
|
||||
|
||||
struct TextureResource {
|
||||
@@ -67,16 +61,12 @@ public:
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = 1;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
// May differ from the image format: sRGB images attach through their UNORM
|
||||
// twin while GL_FRAMEBUFFER_SRGB is disabled.
|
||||
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
||||
|
||||
Bool operator==(const AttachmentViewKey& other) const {
|
||||
return mipLevel == other.mipLevel &&
|
||||
baseArrayLayer == other.baseArrayLayer &&
|
||||
layerCount == other.layerCount &&
|
||||
viewType == other.viewType &&
|
||||
viewFormat == other.viewFormat;
|
||||
viewType == other.viewType;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,8 +77,6 @@ public:
|
||||
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewFormat)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
@@ -169,25 +157,7 @@ public:
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkImageCreateFlags imageCreateFlags = 0;
|
||||
// Usage the live image was created with. STORAGE is only requested for textures that
|
||||
// have actually been bound to a GL image unit, because on Adreno a storage-capable
|
||||
// image loses UBWC bandwidth compression; a later image binding upgrades the usage
|
||||
// and recreates the image, so the resolved usage has to be part of the compatibility
|
||||
// check that decides whether the existing image can be kept.
|
||||
VkImageUsageFlags usageFlags = 0;
|
||||
// True once this image was (re)resolved while the texture was already marked as an
|
||||
// image-unit texture. Distinguishes "not upgraded yet" from "cannot be upgraded"
|
||||
// (a format whose optimalTilingFeatures lack STORAGE_IMAGE never gains the bit), so
|
||||
// NeedsStorageImagePreparation cannot ask for a recreate that will never happen.
|
||||
Bool storageUsageResolved = false;
|
||||
Uint16 syncedTextureParamsVersion = 0;
|
||||
// Recording generation (VkTextureManager::GetRecordingGeneration) of the last
|
||||
// command referencing this image that was recorded into the CURRENT frame
|
||||
// command buffer. An image untouched by the open recording may have its
|
||||
// out-of-pass work (deferred clears, sampled-layout transitions) recorded
|
||||
// into the frame's PRE command buffer - which executes strictly before the
|
||||
// frame's commands - instead of splitting the active render pass.
|
||||
Uint64 lastRecordingGeneration = 0;
|
||||
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
|
||||
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
|
||||
Uint64 syncedContentVersion = 0;
|
||||
@@ -220,10 +190,7 @@ public:
|
||||
std::swap(this->viewType, that.viewType);
|
||||
std::swap(this->sampleCount, that.sampleCount);
|
||||
std::swap(this->imageCreateFlags, that.imageCreateFlags);
|
||||
std::swap(this->usageFlags, that.usageFlags);
|
||||
std::swap(this->storageUsageResolved, that.storageUsageResolved);
|
||||
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
||||
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
|
||||
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
||||
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
||||
}
|
||||
@@ -284,8 +251,6 @@ public:
|
||||
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
imageCreateFlags = 0;
|
||||
usageFlags = 0;
|
||||
storageUsageResolved = false;
|
||||
syncedTextureParamsVersion = 0;
|
||||
syncedContentVersion = 0;
|
||||
syncedMipLevelCount = 0;
|
||||
@@ -302,10 +267,6 @@ public:
|
||||
Bool Initialize(const InitInfo& initInfo);
|
||||
void Shutdown();
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
// Drains every frame slot's deferred image/view releases. Only valid when
|
||||
// the caller has proven every queue submission complete; used by the
|
||||
// present-less frame-boundary drain.
|
||||
void CollectAllDeferredReleases();
|
||||
|
||||
TextureResource* SyncTextureAndGetDescriptor(
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
@@ -324,32 +285,6 @@ public:
|
||||
VkImageLayout newLayout);
|
||||
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
|
||||
// Recording-generation bookkeeping for the pre-pass command stream. The
|
||||
// generation advances every time the frame command buffer (re)begins
|
||||
// recording; a resource whose stamp does not match was not referenced by
|
||||
// any command in the open recording, so its out-of-pass work may safely
|
||||
// execute ahead of the whole recording (in the pre command buffer).
|
||||
void AdvanceRecordingGeneration() { ++m_recordingGeneration; }
|
||||
void StampResourceRecordingUse(TextureResource& resource) const {
|
||||
resource.lastRecordingGeneration = m_recordingGeneration;
|
||||
}
|
||||
// Map-lookup variant for callers that only hold the GL texture object.
|
||||
void StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture);
|
||||
Bool WasTouchedThisRecording(const TextureResource& resource) const {
|
||||
return resource.lastRecordingGeneration == m_recordingGeneration;
|
||||
}
|
||||
// Records that this texture is bound to a GL image unit, so its image must carry
|
||||
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
|
||||
// therefore before the render pass is committed: an image that has to be upgraded is
|
||||
// recreated, which is illegal inside a render pass. Sticky for the texture's lifetime -
|
||||
// GL lets an image binding come and go, and re-creating the image every time it does
|
||||
// would cost far more than the compression it wins back.
|
||||
void MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture);
|
||||
// True when this texture is marked but its live image predates the mark, i.e. the next sync
|
||||
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
|
||||
// submit their pending recording first, so that copy cannot read pre-flush content.
|
||||
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
|
||||
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
|
||||
// texture as a storage image may need work that is illegal inside a render pass (resource
|
||||
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
|
||||
@@ -396,9 +331,6 @@ public:
|
||||
private:
|
||||
// Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch().
|
||||
Uint64 m_textureImageEpoch = 1;
|
||||
// See AdvanceRecordingGeneration. Starts above every resource's default
|
||||
// stamp of 0 so a fresh resource counts as untouched.
|
||||
Uint64 m_recordingGeneration = 1;
|
||||
|
||||
Bool SyncTexture(MG_State::GLState::ITextureObject &texture,
|
||||
TextureResource &outResource);
|
||||
@@ -429,28 +361,18 @@ private:
|
||||
void DeferViewRelease(VkImageView view);
|
||||
void CollectDeferredReleases(Uint32 frameIndex);
|
||||
void DestroyDeferredReleases();
|
||||
// Frees the fence/command buffer/staging buffer of every in-flight texture
|
||||
// upload whose fence has signaled (submission order = completion order on
|
||||
// the single queue, so the scan stops at the first still-pending entry).
|
||||
// waitAll blocks on every entry - Shutdown's drain.
|
||||
void ReclaimCompletedUploads(Bool waitAll = false);
|
||||
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
|
||||
void EraseTrackedTexture(const TextureIdentity& identity);
|
||||
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
|
||||
SizeT PruneDeadTextures();
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
VmaAllocator m_allocator = nullptr;
|
||||
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
|
||||
Bool m_imageFormatListSupported = false;
|
||||
Uint32 m_currentFrameIndex = 0;
|
||||
|
||||
Uint8 m_gcCounter = 0;
|
||||
// Frame-boundary GC gate: counts BeginFrame calls, not draws, so texture churn
|
||||
// through non-draw paths (FBO clears, readbacks) still reaches the prune.
|
||||
Uint32 m_gcFrameCounter = 0;
|
||||
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
|
||||
// textures already fully synced in the current draw (small N -> flat scan).
|
||||
Bool m_drawSyncScopeActive = false;
|
||||
@@ -463,45 +385,12 @@ private:
|
||||
TextureResource* resource = nullptr;
|
||||
};
|
||||
Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
|
||||
// Cross-draw sampled-texture memo: the same few textures (atlas, lightmap)
|
||||
// are resolved on every draw, so cache their resource pointers and skip the
|
||||
// alive/resource map lookups. Node-based std::unordered_map keeps the
|
||||
// pointees stable across inserts; erases bump m_resourceEraseEpoch, which
|
||||
// every memo entry must match. SyncTexture still runs on memo hits, so
|
||||
// content/param freshness is unaffected. A dead-then-reused texture address
|
||||
// cannot false-hit: the new object carries a new lifetime id.
|
||||
struct SyncedTextureMemoEntry {
|
||||
const MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
Uint64 lifetimeId = 0;
|
||||
Uint64 eraseEpoch = 0;
|
||||
TextureResource* resource = nullptr;
|
||||
};
|
||||
static constexpr Uint32 kSyncedTextureMemoSize = 8;
|
||||
SyncedTextureMemoEntry m_syncedTextureMemo[kSyncedTextureMemoSize];
|
||||
Uint32 m_syncedTextureMemoNext = 0;
|
||||
Uint64 m_resourceEraseEpoch = 1;
|
||||
// Formats whose mutable-image probe failed on this device; their images are created
|
||||
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
|
||||
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
|
||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
||||
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
|
||||
// Supported multisample counts per format, so repeat texture syncs do not
|
||||
// re-query vkGetPhysicalDeviceImageFormatProperties.
|
||||
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
|
||||
Vector<Vector<TextureResource>> m_deferredReleases;
|
||||
Vector<Vector<VkImageView>> m_deferredViewReleases;
|
||||
// Texture uploads are submitted out-of-band but NOT waited on (waiting
|
||||
// behind the queue serialized the CPU against the previous frame's GPU
|
||||
// work every time an animated atlas re-uploaded). Their transient objects
|
||||
// are parked here and reclaimed once the upload fence signals.
|
||||
struct PendingUploadReclaim {
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
VkBuffer stagingBuffer = VK_NULL_HANDLE;
|
||||
VmaAllocation stagingAllocation = nullptr;
|
||||
};
|
||||
Vector<PendingUploadReclaim> m_pendingUploadReclaims;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -76,10 +76,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLenum indexType = GL_UNSIGNED_SHORT;
|
||||
SizeT indexByteOffset = 0;
|
||||
SizeT indexByteSize = 0;
|
||||
// Interpret indexByteOffset as a raw client pointer even when an element
|
||||
// array buffer is bound (backend-synthesized index lists, e.g. the
|
||||
// GL_LINE_LOOP -> LINE_STRIP rewrite).
|
||||
Bool forceClientMemory = false;
|
||||
};
|
||||
|
||||
struct DrawIndexedCmd {
|
||||
@@ -118,10 +114,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
};
|
||||
|
||||
class VulkanRenderer : public IBufferCopyCommandProvider,
|
||||
public FrameContext::IRecordingObserver,
|
||||
public VkRenderPassManager::IEvictionObserver,
|
||||
public ProgramFactory::IEvictionObserver {
|
||||
class VulkanRenderer : public IBufferCopyCommandProvider, public FrameContext::IRecordingObserver {
|
||||
public:
|
||||
VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {});
|
||||
~VulkanRenderer();
|
||||
@@ -138,31 +131,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// recording, before any render pass.
|
||||
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) override;
|
||||
|
||||
// VkRenderPassManager::IEvictionObserver: the render-pass aging sweep just
|
||||
// destroyed these VkRenderPasses; evict every graphics pipeline hashed on a
|
||||
// dying handle (they share its >1024-boundary idleness, so immediate
|
||||
// destruction is safe) and drop the last-pipeline memo if any went.
|
||||
void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) override;
|
||||
|
||||
// ProgramFactory::IEvictionObserver: an aged-out program entry was
|
||||
// destroyed; evict its compute pipeline and graphics pipelines (same
|
||||
// idleness guarantee - they are only bound through draws/dispatches that
|
||||
// stamp the program entry) and purge the descriptor-set cache entries
|
||||
// keyed by its now-recyclable VkDescriptorSetLayout handle.
|
||||
void OnProgramEvicted(ProgramFactory::HashType programHash,
|
||||
VkDescriptorSetLayout descriptorSetLayout) override;
|
||||
|
||||
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
||||
const DrawCmdParam& drawParams,
|
||||
const IndexBufferView* pIndexBufferView = nullptr);
|
||||
// ANGLE-style consecutive-draw fast path: SetupDraw snapshots the fully
|
||||
// resolved draw configuration; the next draw whose cheap version/identity
|
||||
// checks all match skips the resolution half (LOD probe, sampled-set
|
||||
// walk, render-pass and pipeline resolution) and jumps straight to the
|
||||
// per-draw tail. Returns false (leaving no side effects that the full
|
||||
// path cannot redo idempotently) whenever anything might have changed.
|
||||
Bool TrySetupDrawFastPath(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
||||
const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView);
|
||||
void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer,
|
||||
const RenderPassEntry& compatibleRenderPassEntry);
|
||||
|
||||
@@ -200,25 +171,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void GenerateMipmap(GLenum target);
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
// GL_DEPTH_COMPONENT / GL_DEPTH_STENCIL / GL_STENCIL_INDEX readback from the
|
||||
// read framebuffer's depth/stencil attachment (per-aspect buffer copies with
|
||||
// CPU repacking into the requested client layout).
|
||||
void ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
|
||||
// expects command recording to be active and any render pass already ended.
|
||||
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
||||
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
||||
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||
void* pixels);
|
||||
// Same-extent depth blit between images of different depth formats: host
|
||||
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
||||
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
||||
VkImageLayout* srcTrackedLayout, Uint32 srcMipLevel, Uint32 srcBaseArrayLayer,
|
||||
VkImage dstImage, VkFormat dstFormat, VkImageLayout* dstTrackedLayout,
|
||||
Uint32 dstMipLevel, Uint32 dstBaseArrayLayer, GLint srcX, GLint srcY, GLint dstX,
|
||||
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
|
||||
VkImageLayout dstRestoreLayout, Bool stencilAspect);
|
||||
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
||||
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
||||
GLsizei width, GLsizei height, GLenum destinationFormat,
|
||||
@@ -304,20 +256,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkTimerQueryManager::TimestampRecord& end) const;
|
||||
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
|
||||
|
||||
// GL_SAMPLES_PASSED occlusion queries: every app draw between Start and Stop is
|
||||
// wrapped in a Vulkan occlusion query slot; the result is the slot sum. Requires
|
||||
// hostQueryReset for slot recycling - Start fails (frontend keeps the query
|
||||
// unsupported) when the device lacks it.
|
||||
Bool StartOcclusionQueryCapture();
|
||||
void StopOcclusionQueryCapture(Vector<Uint32>& outSlots);
|
||||
// Flushes pending commands, waits, sums the slots, and recycles them.
|
||||
Bool ResolveOcclusionQueryResult(const Vector<Uint32>& slots, Uint64& outSamples);
|
||||
|
||||
void RequestSwapchainResize(Uint32 width, Uint32 height);
|
||||
// Re-query the surface and report whether the live swapchain no longer matches it
|
||||
// (size or orientation). This - not a VK_SUBOPTIMAL_KHR result - is what decides a
|
||||
// rebuild, so a surface the driver merely considers suboptimal cannot thrash.
|
||||
Bool SwapchainIsOutOfDate();
|
||||
// Returns false when the surface is zero-area (minimized/hidden window):
|
||||
// no new swapchain is installed and presentation must stay suspended.
|
||||
Bool RecreateSwapchain();
|
||||
@@ -406,40 +345,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkFence AcquirePooledSubmitFence();
|
||||
void DestroySubmitFencePool();
|
||||
Bool HasPendingRecordedWork() const;
|
||||
// Frame-boundary housekeeping for paths that never reach Present's
|
||||
// tail (present-less readback loops, suspended presentation, blocking
|
||||
// sync waits): runs the same per-frame drains Present performs, but
|
||||
// only when every queue submission has been observed complete AND no
|
||||
// recorded-but-unsubmitted commands exist - i.e. when CPU-GPU overlap
|
||||
// is provably already zero. Never blocks (non-blocking fence poll
|
||||
// only), so the presenting path's frames-in-flight pipelining is
|
||||
// untouched. Returns true when the drain ran.
|
||||
Bool TryDrainFrameTransients();
|
||||
|
||||
Vector<SubmitRecord> m_inFlightSubmits;
|
||||
Vector<VkFence> m_freeSubmitFences;
|
||||
Uint64 m_submitCounter = 0;
|
||||
Uint64 m_completedSubmitCounter = 0;
|
||||
// Drains since the last Present, gating the drain's frame-boundary-equivalent
|
||||
// work (arena rewind + cache aging): a presenting app's mid-frame
|
||||
// readbacks/waits must neither churn the transient caches nor accelerate the
|
||||
// aging clocks, while present-less loops still cross a boundary every few
|
||||
// iterations. Reset in Present.
|
||||
Uint32 m_drainsSinceLastPresent = 0;
|
||||
|
||||
NativeWindowType m_window = 0;
|
||||
void* m_platformDisplay = nullptr;
|
||||
void* m_platformLibrary = nullptr;
|
||||
void* m_platformCloseDisplay = nullptr;
|
||||
// Some real ICDs (e.g. NVIDIA's proprietary Linux driver) don't implement
|
||||
// VK_EXT_headless_surface at all. Detected once in CreateInstance() from the
|
||||
// enumerated instance extensions; when false, CreateSurface() falls back to a
|
||||
// hidden Xlib window instead of vkCreateHeadlessSurfaceEXT.
|
||||
Bool m_headlessSurfaceSupported = true;
|
||||
// Set when CreateSurface() had to create its own Xlib window for the fallback
|
||||
// above (rather than being handed one by the caller), so Shutdown() knows it
|
||||
// owns that window and must destroy it.
|
||||
Bool m_ownsFallbackXlibWindow = false;
|
||||
VulkanRendererConfig m_config;
|
||||
Bool m_swapchainResizeRequested = false;
|
||||
// Presentation is suspended while the window is zero-area (minimized): the
|
||||
@@ -452,9 +367,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<VkExtensionProperties> m_extensions;
|
||||
VkInstance m_instance = VK_NULL_HANDLE;
|
||||
VkDebugUtilsMessengerEXT m_debugMessenger = VK_NULL_HANDLE;
|
||||
// Fallback reporting channel for drivers that ship the validation layers but
|
||||
// only expose the older VK_EXT_debug_report (Adreno 650 / Vulkan 1.1.128).
|
||||
VkDebugReportCallbackEXT m_debugReportCallback = VK_NULL_HANDLE;
|
||||
PhysicalDevice m_physicalDevice;
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
VmaAllocator m_allocator = nullptr;
|
||||
@@ -492,59 +404,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 stride);
|
||||
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
|
||||
|
||||
// VK_EXT_transform_feedback (GL transform feedback capture)
|
||||
Bool m_transformFeedbackFeatureEnabled = false;
|
||||
static inline PFN_vkCmdBindTransformFeedbackBuffersEXT s_vkCmdBindTransformFeedbackBuffersEXT = nullptr;
|
||||
static inline PFN_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr;
|
||||
static inline PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr;
|
||||
// Counter buffers (one 4-byte slot per capture binding) let consecutive
|
||||
// draws within one glBeginTransformFeedback append GL-style.
|
||||
VkBufferObject m_xfbCounterBuffer;
|
||||
// Non-zero while inside a GL Begin/End with at least one captured draw
|
||||
// recorded; selects counter-buffer resume on the next captured draw.
|
||||
Bool m_xfbCountersValid = false;
|
||||
Uint64 m_xfbLastSeenGeneration = 0;
|
||||
// Wraps a recorded draw with BeginTransformFeedbackEXT/EndTransformFeedbackEXT
|
||||
// when GL transform feedback is active; binds capture buffers on demand.
|
||||
Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame);
|
||||
void EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began);
|
||||
// Wrap one app draw in an occlusion-query slot while a GL_SAMPLES_PASSED
|
||||
// query is active. Returns whether a slot was begun (End must mirror it).
|
||||
Bool BeginOcclusionForDraw(VkCommandBuffer commandBuffer);
|
||||
void EndOcclusionForDraw(VkCommandBuffer commandBuffer, Bool began);
|
||||
Bool m_occlusionQueryPreciseEnabled = false;
|
||||
Bool m_hostQueryResetEnabled = false;
|
||||
PFN_vkResetQueryPool s_vkResetQueryPool = nullptr;
|
||||
VkQueryPool m_occlusionQueryPool = VK_NULL_HANDLE;
|
||||
static constexpr Uint32 kOcclusionQuerySlots = 8192;
|
||||
Uint32 m_occlusionSlotCursor = 0;
|
||||
Bool m_occlusionCaptureActive = false;
|
||||
Vector<Uint32> m_occlusionActiveSlots;
|
||||
// Transform feedback primitive queries: one pool slot per captured draw yields
|
||||
// the (written, needed) pair; GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN sums the
|
||||
// first, GL_PRIMITIVES_GENERATED the second - exact with geometry shaders,
|
||||
// unlike the CPU fallback accounting.
|
||||
Bool m_xfbQueriesSupported = false;
|
||||
PFN_vkCmdBeginQueryIndexedEXT s_vkCmdBeginQueryIndexedEXT = nullptr;
|
||||
PFN_vkCmdEndQueryIndexedEXT s_vkCmdEndQueryIndexedEXT = nullptr;
|
||||
VkQueryPool m_xfbQueryPool = VK_NULL_HANDLE;
|
||||
static constexpr Uint32 kXfbQuerySlots = 8192;
|
||||
Uint32 m_xfbQuerySlotCursor = 0;
|
||||
Bool m_xfbQueryCaptureActive[2] = {false, false}; // [0]=written, [1]=generated
|
||||
Vector<Uint32> m_xfbQueryActiveSlots[2];
|
||||
Bool m_xfbQuerySlotOpen = false;
|
||||
Uint32 m_xfbQueryOpenSlot = 0;
|
||||
|
||||
public:
|
||||
// kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED.
|
||||
Bool StartXfbQueryCapture(Uint32 kind);
|
||||
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots);
|
||||
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives);
|
||||
|
||||
private:
|
||||
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
||||
void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
||||
|
||||
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||
|
||||
VkBufferManager m_bufferManager;
|
||||
@@ -557,30 +416,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// gather + synthetic vertex-input rebuild + payload hash + lookup) when the full pipeline
|
||||
// state is unchanged from the previous draw. The key provably covers every pipeline field.
|
||||
// Reset per-frame and on pipeline destruction so the cached handle can never dangle.
|
||||
// Small N-way pipeline-resolution memo (round-robin replacement). A
|
||||
// single-entry memo thrashed on draw sequences that alternate a few
|
||||
// pipelines (GUI text/quad program ping-pong), paying the full
|
||||
// payload-hash lookup per draw; eight entries cover such working sets
|
||||
// while keeping the hit path a trivial linear scan.
|
||||
struct PipelineMemoEntry {
|
||||
GLenum mode = 0;
|
||||
Uint64 programHash = 0;
|
||||
Uint64 vertexInputHash = 0;
|
||||
Uint64 renderPassHash = 0;
|
||||
Uint renderStateVersion = 0;
|
||||
ProgramFactory::CompileOptionFlags transformFlags = {};
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
static constexpr Uint32 kPipelineMemoSize = 8;
|
||||
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
|
||||
Uint32 m_pipelineMemoCount = 0;
|
||||
Uint32 m_pipelineMemoNext = 0;
|
||||
// Drops every memoized pipeline handle. Required at command-buffer
|
||||
// boundaries and whenever any pipeline may have been destroyed.
|
||||
void InvalidatePipelineMemo() {
|
||||
m_pipelineMemoCount = 0;
|
||||
m_pipelineMemoNext = 0;
|
||||
}
|
||||
Bool m_lastPipelineValid = false;
|
||||
GLenum m_lastPipelineMode = 0;
|
||||
Uint64 m_lastPipelineProgramHash = 0;
|
||||
Uint64 m_lastPipelineVertexInputHash = 0;
|
||||
Uint64 m_lastPipelineRenderPassHash = 0;
|
||||
Uint m_lastPipelineRenderStateVersion = 0;
|
||||
ProgramFactory::CompileOptionFlags m_lastPipelineTransformFlags = {};
|
||||
VkPipeline m_lastPipelineResult = VK_NULL_HANDLE;
|
||||
UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
|
||||
UniquePtr<ProgramFactory> m_programFactory;
|
||||
UniquePtr<UniformManager> m_uniformManager;
|
||||
@@ -609,61 +452,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
|
||||
Uint64 m_lastSampledSetBindGeneration = 0;
|
||||
|
||||
// Memo for the per-draw explicit-LOD-0 eligibility probe
|
||||
// (ProgramSamplesOnlySingleLevelTextures): same key family as the
|
||||
// sampled-set memo, plus the sampled textures' params-version sum so a
|
||||
// level-range or filter change re-probes. On a hit the resolved
|
||||
// transform flags are reused, which also collapses the two
|
||||
// GetOrCreateProgram lookups into one.
|
||||
Bool m_lastLodDecisionValid = false;
|
||||
Uint64 m_lastLodProgramLifetimeId = 0;
|
||||
Uint32 m_lastLodProgramVersion = 0;
|
||||
Uint64 m_lastLodBindGeneration = 0;
|
||||
Uint64 m_lastLodParamsSum = 0;
|
||||
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
|
||||
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
|
||||
|
||||
// Snapshot behind TrySetupDrawFastPath. Values only: the program and
|
||||
// render-pass caches are open-addressing maps whose entries move on
|
||||
// insert, so no pointers into them are cached; the pipeline handle is
|
||||
// protected by the command-buffer-boundary reset plus the mid-frame
|
||||
// pipeline-destruction resets, and monotonic epochs guard everything
|
||||
// that can be destroyed or recreated between draws.
|
||||
struct SetupDrawSnapshot {
|
||||
Bool valid = false;
|
||||
Uint8 aspects = 0;
|
||||
GLenum mode = 0;
|
||||
Uint64 programLifetimeId = 0;
|
||||
Uint32 programVersion = 0;
|
||||
const void* vao = nullptr;
|
||||
Uint32 vaoConfigVersion = 0;
|
||||
const void* drawFbo = nullptr;
|
||||
Uint16 fboVersion = 0;
|
||||
Bool drawFboIsDefault = false;
|
||||
Uint renderStateVersion = 0;
|
||||
Uint64 bindGeneration = 0;
|
||||
Uint32 baseTransformFlags = 0;
|
||||
Uint32 resolvedTransformFlags = 0;
|
||||
Uint64 renderPassHash = 0;
|
||||
Uint32 imageIndex = 0;
|
||||
Uint64 textureEraseEpoch = 0;
|
||||
Uint64 textureImageEpoch = 0;
|
||||
Uint64 renderbufferImageEpoch = 0;
|
||||
Uint64 sampledContentSum = 0;
|
||||
Uint64 sampledParamsSum = 0;
|
||||
IntVec2 renderPassExtent = {0, 0};
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
SetupDrawSnapshot m_setupDrawSnapshot;
|
||||
|
||||
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
|
||||
// draw call and must not allocate.
|
||||
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
|
||||
// Parallel to m_sampledTexturesScratch, refilled by every SetupDraw's
|
||||
// first sampled-texture loop: the resolved backend resources, so the
|
||||
// post-transition loop can skip re-resolving textures whose layout is
|
||||
// already sampleable.
|
||||
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
|
||||
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
||||
Vector<VkBuffer> m_vertexBuffersScratch;
|
||||
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
||||
@@ -726,8 +517,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void CreateInstance();
|
||||
VkResult SetupDebugMessenger();
|
||||
VkResult DestroyDebugMessenger();
|
||||
VkResult SetupDebugReportCallback();
|
||||
void DestroyDebugReportCallback();
|
||||
VkDebugUtilsMessengerCreateInfoEXT PopulateDebugMessengerCreateInfo();
|
||||
void CreateSurface();
|
||||
void PickPhysicalDevice();
|
||||
@@ -746,10 +535,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const RenderPassEntry& renderPassEntry);
|
||||
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
|
||||
void DestroyComputePipelines();
|
||||
// Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to
|
||||
// flush the pending recording (see the body), which retires the current command buffer.
|
||||
Bool PrepareStorageImageTextures(
|
||||
FrameContext::FrameData& frame,
|
||||
VkCommandBuffer commandBuffer,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
|
||||
@@ -811,10 +598,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const PhysicalDevice& compareWithDevice,
|
||||
PhysicalDevice& outBetterDevice);
|
||||
static constexpr const char* s_validationLayerNames[] = {"VK_LAYER_KHRONOS_validation"};
|
||||
// VK_KHR_image_format_list: lets MUTABLE_FORMAT images declare their exact view-format
|
||||
// set so the driver can keep bandwidth compression (see CreateLogicalDeviceAndQueues).
|
||||
Bool m_imageFormatListExtensionEnabled = false;
|
||||
|
||||
static constexpr const char* s_deviceExtensionNames[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
|
||||
static Bool CheckValidationLayerSupport();
|
||||
|
||||
|
||||
@@ -52,48 +52,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// GL renders into sRGB color attachments RAW while GL_FRAMEBUFFER_SRGB is disabled
|
||||
// (the core-profile default); Vulkan sRGB attachments always encode on write. The
|
||||
// attachment view (and render pass format) therefore drops to the UNORM twin
|
||||
// whenever the capability is off. Sampled views keep the sRGB format (decode on
|
||||
// sample is unconditional in GL).
|
||||
inline VkFormat ResolveSrgbAttachmentWriteFormat(VkFormat format, bool framebufferSrgbEnabled) {
|
||||
if (framebufferSrgbEnabled) return format;
|
||||
switch (format) {
|
||||
case VK_FORMAT_R8G8B8A8_SRGB:
|
||||
return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
case VK_FORMAT_B8G8R8A8_SRGB:
|
||||
return VK_FORMAT_B8G8R8A8_UNORM;
|
||||
default:
|
||||
return format;
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
// The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log
|
||||
// call: appending its format to the base format while its arguments precede the base
|
||||
// arguments makes every conversion read the wrong slot (a %s pulling an int crashes).
|
||||
#define VK_VERIFY(expr, ...) \
|
||||
do { \
|
||||
VkResult _vk_verify_result = (expr); \
|
||||
if (_vk_verify_result != VK_SUCCESS) { \
|
||||
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \
|
||||
MGLOG_F("Vulkan error %s (%d) at %s:%d", \
|
||||
MGLOG_F("Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, \
|
||||
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
|
||||
_vk_verify_result, __FILE__, __LINE__); \
|
||||
} \
|
||||
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d", \
|
||||
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
|
||||
_vk_verify_result, __FILE__, __LINE__); \
|
||||
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), _vk_verify_result, __FILE__, __LINE__); \
|
||||
} while (0)
|
||||
|
||||
#define XXHASH_VERIFY(expr, ...) \
|
||||
do { \
|
||||
XXH_errorcode _xxh_verify_result = (expr); \
|
||||
if (_xxh_verify_result != XXH_OK) { \
|
||||
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \
|
||||
} \
|
||||
MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d", _xxh_verify_result, __FILE__, \
|
||||
__LINE__); \
|
||||
MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, _xxh_verify_result, __FILE__, __LINE__); \
|
||||
} while (0)
|
||||
|
||||
@@ -24,7 +24,6 @@ namespace MobileGL::MG_Impl::CGLImpl {
|
||||
GLint Samples = 0;
|
||||
GLint Profile = kCGLOGLPVersion_3_2_Core;
|
||||
GLint RendererId = 0x4d474c;
|
||||
GLint DisplayMask = 0;
|
||||
};
|
||||
|
||||
struct ContextObject {
|
||||
@@ -135,9 +134,6 @@ namespace MobileGL::MG_Impl::CGLImpl {
|
||||
case kCGLPFARendererID:
|
||||
pixelFormat.RendererId = value;
|
||||
break;
|
||||
case kCGLPFADisplayMask:
|
||||
pixelFormat.DisplayMask = value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -347,9 +343,6 @@ namespace MobileGL::MG_Impl::CGLImpl {
|
||||
case kCGLPFARendererID:
|
||||
*value = pixelFormat->RendererId;
|
||||
return kCGLNoError;
|
||||
case kCGLPFADisplayMask:
|
||||
*value = pixelFormat->DisplayMask;
|
||||
return kCGLNoError;
|
||||
case kCGLPFAOpenGLProfile:
|
||||
*value = pixelFormat->Profile;
|
||||
return kCGLNoError;
|
||||
@@ -488,32 +481,6 @@ namespace MobileGL::MG_Impl::CGLImpl {
|
||||
return it == currentContexts.end() ? nullptr : it->second;
|
||||
}
|
||||
|
||||
CGLError SetVirtualScreen(CGLContextObj ctx, GLint screen) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
auto* object = TryGetContext(ctx);
|
||||
if (!object) {
|
||||
return kCGLBadContext;
|
||||
}
|
||||
if (screen != 0) {
|
||||
return kCGLBadValue;
|
||||
}
|
||||
object->VirtualScreen = screen;
|
||||
return kCGLNoError;
|
||||
}
|
||||
|
||||
CGLError GetVirtualScreen(CGLContextObj ctx, GLint* screen) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
auto* object = TryGetContext(ctx);
|
||||
if (!object) {
|
||||
return kCGLBadContext;
|
||||
}
|
||||
if (!screen) {
|
||||
return kCGLBadAddress;
|
||||
}
|
||||
*screen = object->VirtualScreen;
|
||||
return kCGLNoError;
|
||||
}
|
||||
|
||||
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
auto* object = TryGetContext(ctx);
|
||||
|
||||
@@ -32,8 +32,6 @@ namespace MobileGL::MG_Impl::CGLImpl {
|
||||
|
||||
CGLError SetCurrentContext(CGLContextObj ctx);
|
||||
CGLContextObj GetCurrentContext();
|
||||
CGLError SetVirtualScreen(CGLContextObj ctx, GLint screen);
|
||||
CGLError GetVirtualScreen(CGLContextObj ctx, GLint* screen);
|
||||
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params);
|
||||
CGLError GetParameter(CGLContextObj ctx, CGLContextParameter pname, GLint* params);
|
||||
CGLError UpdateContext(CGLContextObj ctx);
|
||||
|
||||
@@ -71,14 +71,6 @@ MOBILEGL_CGL_API CGLContextObj CGLGetCurrentContext(void) {
|
||||
return MobileGL::MG_Impl::CGLImpl::GetCurrentContext();
|
||||
}
|
||||
|
||||
MOBILEGL_CGL_API CGLError CGLSetVirtualScreen(CGLContextObj ctx, GLint screen) {
|
||||
return MobileGL::MG_Impl::CGLImpl::SetVirtualScreen(ctx, screen);
|
||||
}
|
||||
|
||||
MOBILEGL_CGL_API CGLError CGLGetVirtualScreen(CGLContextObj ctx, GLint* screen) {
|
||||
return MobileGL::MG_Impl::CGLImpl::GetVirtualScreen(ctx, screen);
|
||||
}
|
||||
|
||||
MOBILEGL_CGL_API CGLError CGLSetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
|
||||
return MobileGL::MG_Impl::CGLImpl::SetParameter(ctx, pname, params);
|
||||
}
|
||||
|
||||
@@ -10,12 +10,8 @@
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
#include "MG_Impl/CGLImpl/CGLImpl.h"
|
||||
#include "MG_Impl/GetProcAddress.h"
|
||||
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
#include <CoreVideo/CVDisplayLink.h>
|
||||
#include <cstdint>
|
||||
#include <dlfcn.h>
|
||||
|
||||
namespace {
|
||||
@@ -51,52 +47,10 @@ namespace {
|
||||
return dlsym(handle, symbol);
|
||||
}
|
||||
|
||||
CGDirectDisplayID DisplayForMask(GLint displayMask) {
|
||||
constexpr std::uint32_t MaxDisplays = sizeof(CGOpenGLDisplayMask) * 8;
|
||||
CGDirectDisplayID displays[MaxDisplays] = {};
|
||||
std::uint32_t displayCount = 0;
|
||||
if (displayMask != 0 &&
|
||||
CGGetActiveDisplayList(MaxDisplays, displays, &displayCount) == kCGErrorSuccess) {
|
||||
const auto mask = static_cast<CGOpenGLDisplayMask>(displayMask);
|
||||
for (std::uint32_t i = 0; i < displayCount; ++i) {
|
||||
if ((CGDisplayIDToOpenGLDisplayMask(displays[i]) & mask) != 0) {
|
||||
return displays[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return CGMainDisplayID();
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
CVReturn MobileGLCVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(
|
||||
CVDisplayLinkRef displayLink,
|
||||
CGLContextObj context,
|
||||
CGLPixelFormatObj pixelFormat) {
|
||||
GLint virtualScreen = 0;
|
||||
if (MobileGL::MG_Impl::CGLImpl::GetVirtualScreen(context, &virtualScreen) == kCGLNoError) {
|
||||
GLint displayMask = 0;
|
||||
if (!displayLink ||
|
||||
MobileGL::MG_Impl::CGLImpl::DescribePixelFormat(
|
||||
pixelFormat, virtualScreen, kCGLPFADisplayMask, &displayMask) != kCGLNoError) {
|
||||
return kCVReturnInvalidArgument;
|
||||
}
|
||||
return CVDisplayLinkSetCurrentCGDisplay(displayLink, DisplayForMask(displayMask));
|
||||
}
|
||||
|
||||
using OriginalFunction = CVReturn (*)(CVDisplayLinkRef, CGLContextObj, CGLPixelFormatObj);
|
||||
static const auto original = reinterpret_cast<OriginalFunction>(
|
||||
dlsym(RTLD_NEXT, "CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext"));
|
||||
return original ? original(displayLink, context, pixelFormat) : kCVReturnError;
|
||||
}
|
||||
|
||||
__attribute__((used)) static const DyldInterposeEntry kMobileGLDyldInterpose[]
|
||||
__attribute__((section("__DATA,__interpose"))) = {
|
||||
{reinterpret_cast<const void*>(MobileGLDlsym), reinterpret_cast<const void*>(dlsym)},
|
||||
{reinterpret_cast<const void*>(MobileGLCVDisplayLinkSetCurrentCGDisplayFromOpenGLContext),
|
||||
reinterpret_cast<const void*>(CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext)},
|
||||
};
|
||||
#pragma clang diagnostic pop
|
||||
} // namespace
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# Public CGL entry points.
|
||||
_CGL*
|
||||
|
||||
# Public EGL entry points.
|
||||
_egl*
|
||||
|
||||
# Public OpenGL and GLX entry points. OpenGL function names always use an
|
||||
# uppercase letter or digit after the "gl" prefix; excluding lowercase here
|
||||
# deliberately prevents glslang_* from matching this pattern.
|
||||
_gl[A-Z0-9]*
|
||||
@@ -1351,14 +1351,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
||||
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, pointIndex)) return;
|
||||
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Transform feedback buffer bindings cannot change while transform "
|
||||
"feedback is active."));
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, pointIndex);
|
||||
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, pointIndex);
|
||||
@@ -1392,14 +1384,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
||||
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
|
||||
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Transform feedback buffer bindings cannot change while transform "
|
||||
"feedback is active."));
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, index);
|
||||
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, index);
|
||||
|
||||
@@ -60,11 +60,6 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
||||
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
||||
pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0)));
|
||||
}
|
||||
if (target == BufferTarget::TransformFeedback) {
|
||||
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS bounds the indexed capture
|
||||
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
||||
pointCount = std::min<SizeT>(pointCount, 4);
|
||||
}
|
||||
|
||||
if (index < pointCount) {
|
||||
return true;
|
||||
|
||||
@@ -48,73 +48,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Primitives a draw of `count` vertices in `mode` assembles (0 for
|
||||
// incomplete primitives). Used for the CPU-side transform feedback
|
||||
// primitive accounting.
|
||||
static Uint64 CountPrimitivesForDraw(GLenum mode, GLsizei count) {
|
||||
if (count <= 0) return 0;
|
||||
switch (mode) {
|
||||
case GL_POINTS: return static_cast<Uint64>(count);
|
||||
case GL_LINES: return static_cast<Uint64>(count / 2);
|
||||
case GL_LINE_STRIP: return count >= 2 ? static_cast<Uint64>(count - 1) : 0;
|
||||
case GL_LINE_LOOP: return count >= 2 ? static_cast<Uint64>(count) : 0;
|
||||
case GL_TRIANGLES: return static_cast<Uint64>(count / 3);
|
||||
case GL_TRIANGLE_STRIP:
|
||||
case GL_TRIANGLE_FAN: return count >= 3 ? static_cast<Uint64>(count - 2) : 0;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Accumulate the transform feedback primitive counter for a captured draw.
|
||||
// Draws without a geometry stage write exactly the primitives they assemble,
|
||||
// clamped by the capture buffers' remaining capacity (a full buffer stops
|
||||
// recording whole primitives, which is what PRIMITIVES_WRITTEN reports).
|
||||
// Geometry amplification is not modelled here.
|
||||
static void AccountTransformFeedbackPrimitives(GLenum mode, GLsizei count) {
|
||||
if (!MG_State::pGLContext->IsTransformFeedbackActive()) return;
|
||||
Uint64 primitives = CountPrimitivesForDraw(mode, count);
|
||||
if (primitives == 0) return;
|
||||
MG_State::pGLContext->AddTransformFeedbackInputPrimitives(primitives);
|
||||
|
||||
Uint64 verticesPerPrimitive = 1;
|
||||
switch (mode) {
|
||||
case GL_LINES:
|
||||
case GL_LINE_STRIP:
|
||||
case GL_LINE_LOOP:
|
||||
verticesPerPrimitive = 2;
|
||||
break;
|
||||
case GL_TRIANGLES:
|
||||
case GL_TRIANGLE_STRIP:
|
||||
case GL_TRIANGLE_FAN:
|
||||
verticesPerPrimitive = 3;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
|
||||
if (program != nullptr) {
|
||||
// Capacity in captured vertices = the tightest bound buffer.
|
||||
Uint64 capacityVertices = ~0ull;
|
||||
for (SizeT i = 0; i < program->GetTransformFeedbackBufferCount(); ++i) {
|
||||
const Uint32 stride = program->GetTransformFeedbackStride(static_cast<Uint32>(i));
|
||||
if (stride == 0) continue;
|
||||
const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
|
||||
static_cast<Uint>(i));
|
||||
const Range1D range = point.GetRange();
|
||||
const Uint64 bytes = range.end > range.start ? static_cast<Uint64>(range.end - range.start) : 0;
|
||||
capacityVertices = std::min<Uint64>(capacityVertices, bytes / stride);
|
||||
}
|
||||
if (capacityVertices != ~0ull) {
|
||||
const Uint64 usedVertices = MG_State::pGLContext->GetTransformFeedbackCapturedVertices();
|
||||
const Uint64 remainingVertices = capacityVertices > usedVertices ? capacityVertices - usedVertices : 0;
|
||||
primitives = std::min<Uint64>(primitives, remainingVertices / verticesPerPrimitive);
|
||||
}
|
||||
}
|
||||
MG_State::pGLContext->AddTransformFeedbackPrimitives(primitives);
|
||||
MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive);
|
||||
}
|
||||
|
||||
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) {
|
||||
@@ -124,6 +57,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activeBackendObject->GetBackendType() == BackendType::DirectVulkan && mode == GL_LINE_LOOP) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"Primitive mode GL_LINE_LOOP is not supported by the DirectVulkan backend."));
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (vao && vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -133,38 +75,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// While transform feedback is active the draw's primitive type must match
|
||||
// the feedback primitive mode (GL 3.3 core 13.2.2). With a geometry shader
|
||||
// the constraint moves to the shader's output primitive type instead, so
|
||||
// the draw mode itself is unconstrained here.
|
||||
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||
!(MG_State::pGLContext->GetTransformFeedbackProgram() &&
|
||||
MG_State::pGLContext->GetTransformFeedbackProgram()->GetShaderIndexByStage(ShaderStage::Geometry) >= 0)) {
|
||||
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
|
||||
Bool compatible = false;
|
||||
switch (feedbackMode) {
|
||||
case GL_POINTS:
|
||||
compatible = mode == GL_POINTS;
|
||||
break;
|
||||
case GL_LINES:
|
||||
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP;
|
||||
break;
|
||||
case GL_TRIANGLES:
|
||||
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (!compatible) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"Primitive mode is incompatible with the active transform feedback primitive mode."));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -492,14 +402,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawElementsBaseVertex_Backend(mode, count, type, indices, basevertex);
|
||||
}
|
||||
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawArrays_Backend(mode, first, count);
|
||||
}
|
||||
|
||||
@@ -536,147 +444,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawElements_Backend(mode, count, type, indices);
|
||||
}
|
||||
|
||||
void BeginTransformFeedback(GLenum primitiveMode) {
|
||||
if (primitiveMode != GL_POINTS && primitiveMode != GL_LINES && primitiveMode != GL_TRIANGLES) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"primitiveMode must be GL_POINTS, GL_LINES or GL_TRIANGLES."));
|
||||
return;
|
||||
}
|
||||
if (MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is already active."));
|
||||
return;
|
||||
}
|
||||
const auto& program = MG_State::pGLContext->GetCurrentProgram();
|
||||
if (!program || !program->GetLinkStatus() || program->GetTransformFeedbackVaryingCount() == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"No program with transform feedback varyings is active."));
|
||||
return;
|
||||
}
|
||||
// Every capture buffer slot the program's mode uses must have a buffer bound.
|
||||
const SizeT usedBufferCount = program->GetTransformFeedbackBufferCount();
|
||||
for (SizeT i = 0; i < usedBufferCount; ++i) {
|
||||
const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
|
||||
static_cast<Uint>(i));
|
||||
if (point.GetBoundObject() == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"Transform feedback buffer binding point " + std::to_string(i) + " has no buffer bound."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
MG_State::pGLContext->BeginTransformFeedback(primitiveMode, program);
|
||||
if (const auto beginXfb = MG_Backend::gBackendFunctionsTable.GL.BeginTransformFeedback) {
|
||||
beginXfb(primitiveMode);
|
||||
}
|
||||
}
|
||||
|
||||
// Vulkan transform feedback captures triangle strips in plain (i, i+1, i+2)
|
||||
// vertex order, but GL decomposes odd strip triangles as (i+1, i, i+2)
|
||||
// (GL 4.6 table 10.1). With the geometry stage's statically-known strip
|
||||
// lengths the captured records are reordered in place: swap the first two
|
||||
// vertex records of every odd triangle within each emitted strip.
|
||||
static void FixupGsStripCaptureOrder(const SharedPtr<MG_State::GLState::ProgramObject>& program,
|
||||
Uint64 inputPrimitives) {
|
||||
// Only Vulkan-order captures need this. A backend that runs the capture on its
|
||||
// own GL/ES driver (it owns the span, hence the EndTransformFeedback entry) has
|
||||
// already produced GL's vertex order, and reordering it again would corrupt it.
|
||||
if (MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback != nullptr) {
|
||||
return;
|
||||
}
|
||||
if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) {
|
||||
return;
|
||||
}
|
||||
const auto& stripTriangles = program->GetGsStripTriangles();
|
||||
|
||||
// Global triangle indices whose leading vertex pair must swap.
|
||||
Vector<Uint64> swapTriangles;
|
||||
Uint64 triangleBase = 0;
|
||||
for (Uint64 input = 0; input < inputPrimitives; ++input) {
|
||||
for (const Uint32 stripLength : stripTriangles) {
|
||||
for (Uint32 t = 1; t < stripLength; t += 2) {
|
||||
swapTriangles.push_back(triangleBase + t);
|
||||
}
|
||||
triangleBase += stripLength;
|
||||
}
|
||||
}
|
||||
if (swapTriangles.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (SizeT bufferIndex = 0; bufferIndex < program->GetTransformFeedbackBufferCount(); ++bufferIndex) {
|
||||
const Uint32 stride = program->GetTransformFeedbackStride(static_cast<Uint32>(bufferIndex));
|
||||
if (stride == 0) continue;
|
||||
const auto& bindingPoint =
|
||||
MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
|
||||
static_cast<Uint>(bufferIndex));
|
||||
const auto& buffer = bindingPoint.GetBoundObject();
|
||||
if (buffer == nullptr) continue;
|
||||
const Range1D range = bindingPoint.GetRange();
|
||||
const Uint8* mapped = buffer->MappedData();
|
||||
if (mapped == nullptr) continue;
|
||||
// The geometry stage amplifies, so the CPU vertex counter does not bound
|
||||
// the capture; the binding range's whole-triangle capacity does.
|
||||
const Uint64 rangeBytes = range.end > range.start ? static_cast<Uint64>(range.end - range.start) : 0;
|
||||
const Uint64 capturedTriangles = std::min<Uint64>(triangleBase, (rangeBytes / stride) / 3);
|
||||
|
||||
// Observed Vulkan capture order for odd strip triangles is (i, i+2, i+1)
|
||||
// (winding preserved by swapping the trailing pair); GL wants
|
||||
// (i+1, i, i+2), which is one rotation away: (a,b,c) -> (c,a,b).
|
||||
Vector<Uint8> scratch(stride);
|
||||
for (const Uint64 triangle : swapTriangles) {
|
||||
if (triangle >= capturedTriangles) break;
|
||||
const SizeT v0Offset = static_cast<SizeT>(range.start) + static_cast<SizeT>(triangle * 3) * stride;
|
||||
const SizeT v1Offset = v0Offset + stride;
|
||||
const SizeT v2Offset = v1Offset + stride;
|
||||
Memcpy(scratch.data(), mapped + v2Offset, stride);
|
||||
buffer->WritebackFromBackend({const_cast<Uint8*>(mapped) + v1Offset, stride}, v2Offset);
|
||||
buffer->WritebackFromBackend({const_cast<Uint8*>(mapped) + v0Offset, stride}, v1Offset);
|
||||
buffer->WritebackFromBackend({scratch.data(), stride}, v0Offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EndTransformFeedback(void) {
|
||||
if (!MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is not active."));
|
||||
return;
|
||||
}
|
||||
const auto capturedProgram = MG_State::pGLContext->GetTransformFeedbackProgram();
|
||||
const Uint64 inputPrimitives = MG_State::pGLContext->GetTransformFeedbackInputPrimitives();
|
||||
// Closed while the capture state is still active: a backend that captures
|
||||
// through its own driver reads the capture program and buffer bindings here.
|
||||
if (const auto endXfb = MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback) {
|
||||
endXfb();
|
||||
}
|
||||
MG_State::pGLContext->EndTransformFeedback();
|
||||
// Captured results must be visible to MapBuffer/GetBufferSubData after
|
||||
// End; the capture targets are host-coherent GPU memory, so completing
|
||||
// the GPU work is all that is required.
|
||||
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
|
||||
if (backendGL.FenceSync && backendGL.ClientWaitSync) {
|
||||
if (auto sync = backendGL.FenceSync()) {
|
||||
backendGL.ClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, ~0ull);
|
||||
if (backendGL.DeleteSync) {
|
||||
backendGL.DeleteSync(sync);
|
||||
}
|
||||
}
|
||||
}
|
||||
FixupGsStripCaptureOrder(capturedProgram, inputPrimitives);
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
||||
void BeginTransformFeedback(GLenum primitiveMode);
|
||||
void EndTransformFeedback(void);
|
||||
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
|
||||
void DispatchComputeIndirect(GLintptr indirect);
|
||||
void MemoryBarrier(GLbitfield barriers);
|
||||
|
||||
@@ -236,12 +236,12 @@ DECLARE_GL_FUNCTION_HEAD(void, DeleteVertexArrays, GLsizei n, const GLuint* arra
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GenVertexArrays, GLsizei n, GLuint* arrays) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenVertexArrays, n, arrays)
|
||||
DECLARE_GL_FUNCTION_HEAD(GLboolean, IsVertexArray, GLuint array) DECLARE_GL_FUNCTION_END(GLboolean, IsVertexArray, array)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetIntegeri_v, GLenum target, GLuint index, GLint* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetIntegeri_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BeginTransformFeedback, GLenum primitiveMode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginTransformFeedback, primitiveMode)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, EndTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndTransformFeedback)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginTransformFeedback, GLenum primitiveMode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginTransformFeedback, primitiveMode)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndTransformFeedback)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindBufferRange, GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBufferRange, target, index, buffer, offset, size)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindBufferBase, GLenum target, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBufferBase, target, index, buffer)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackVaryings, GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackVaryings, program, count, varyings, bufferMode)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbackVarying, GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size, GLenum* type, GLchar* name) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbackVarying, program, index, bufSize, length, size, type, name)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackVaryings, GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TransformFeedbackVaryings, program, count, varyings, bufferMode)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbackVarying, GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size, GLenum* type, GLchar* name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbackVarying, program, index, bufSize, length, size, type, name)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribIPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribIPointer, index, size, type, stride, pointer)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIiv, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIiv, index, pname, params)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIuiv, GLuint index, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIuiv, index, pname, params)
|
||||
|
||||
@@ -22,21 +22,9 @@
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
// GL only requires support for framebuffers whose depth and stencil attachments
|
||||
// are the same image; anything else may be reported GL_FRAMEBUFFER_UNSUPPORTED.
|
||||
// DirectVulkan cannot form two separate attachments at all, and the real ES
|
||||
// drivers behind DirectGLES answer UNSUPPORTED for it too - so saying COMPLETE
|
||||
// and then rendering into a framebuffer the driver refuses produced silently
|
||||
// empty results (KHR-GL3x.packed_depth_stencil.verify_mixed_attachments).
|
||||
Bool ActiveBackendRejectsDistinctDepthStencil() {
|
||||
Bool IsActiveBackendDirectVulkan() {
|
||||
auto* activeBackend = MG_Backend::pActiveBackendObject.get();
|
||||
if (activeBackend == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (activeBackend->GetBackendType() == BackendType::DirectVulkan) {
|
||||
return true;
|
||||
}
|
||||
return !activeBackend->GetDynamicParameters().SupportsDistinctDepthStencilAttachments;
|
||||
return activeBackend != nullptr && activeBackend->GetBackendType() == BackendType::DirectVulkan;
|
||||
}
|
||||
|
||||
Bool HasDistinctCompleteDepthStencilTextureAttachments(
|
||||
@@ -57,32 +45,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
depthAttachment.GetTextureLevel() != stencilAttachment.GetTextureLevel();
|
||||
}
|
||||
|
||||
// Mirrors the renderer-side gate: distinct depth/stencil renderbuffers (or a
|
||||
// renderbuffer paired with a texture) cannot form one Vulkan depth-stencil
|
||||
// attachment, and GL permits reporting such framebuffers as UNSUPPORTED.
|
||||
Bool HasDistinctCompleteDepthStencilRenderbufferAttachments(
|
||||
const MG_State::GLState::FramebufferObject& framebufferObject) {
|
||||
if (framebufferObject.GetExternalIndex() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& depthAttachment = framebufferObject.GetAttachment(FramebufferAttachmentType::Depth);
|
||||
const auto& stencilAttachment = framebufferObject.GetAttachment(FramebufferAttachmentType::Stencil);
|
||||
if (!depthAttachment.IsComplete() || !stencilAttachment.IsComplete()) {
|
||||
return false;
|
||||
}
|
||||
if (depthAttachment.IsRenderbuffer() && stencilAttachment.IsRenderbuffer()) {
|
||||
return depthAttachment.GetRenderbuffer().get() != stencilAttachment.GetRenderbuffer().get();
|
||||
}
|
||||
return (depthAttachment.IsRenderbuffer() || stencilAttachment.IsRenderbuffer()) &&
|
||||
(depthAttachment.IsTexture() || stencilAttachment.IsTexture());
|
||||
}
|
||||
|
||||
Bool HasUnsupportedDistinctDepthStencilAttachments(
|
||||
Bool IsUnsupportedFramebufferForDirectVulkan(
|
||||
const MG_State::GLState::FramebufferObject& framebufferObject) {
|
||||
// TODO: Keep this in sync with DirectVulkan renderbuffer support as color renderbuffer rendering lands.
|
||||
return HasDistinctCompleteDepthStencilTextureAttachments(framebufferObject) ||
|
||||
HasDistinctCompleteDepthStencilRenderbufferAttachments(framebufferObject);
|
||||
return HasDistinctCompleteDepthStencilTextureAttachments(framebufferObject);
|
||||
}
|
||||
|
||||
Bool HasDefinedAttachment(const MG_State::GLState::FramebufferObject& framebufferObject) {
|
||||
@@ -101,14 +67,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// shared-exponent, SNORM, three-channel norm16/float32/sRGB and three-channel integer formats.
|
||||
// Desktop GL treats those as texture-only too (not in the GL 3.3 required-renderable list), so
|
||||
// reporting GL_FRAMEBUFFER_UNSUPPORTED for them is legal.
|
||||
//
|
||||
// `capabilityTargetIndex` is the row of the cache the attachment actually lives in;
|
||||
// kFormatCapabilityTargetCount asks about the format in general. Asking per target matters
|
||||
// because a capability recorded for one of them says nothing about the others: DirectGLES
|
||||
// widens three-channel formats to four channels to keep them renderable as *multisample*
|
||||
// storage, and a format that survives only through that substitution is still texture-only
|
||||
// on every ordinary target.
|
||||
Bool IsColorInternalFormatRenderable(TextureInternalFormat format, SizeT capabilityTargetIndex) {
|
||||
Bool IsColorInternalFormatRenderable(TextureInternalFormat format) {
|
||||
const SizeT formatIndex = static_cast<SizeT>(format);
|
||||
if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) {
|
||||
const auto& cache = MG_Backend::pActiveBackendObject->GetFormatCapabilities();
|
||||
@@ -120,11 +79,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_Backend::FormatCapability::Creatable);
|
||||
}
|
||||
if (cachePopulated) {
|
||||
const Bool singleTarget = capabilityTargetIndex < MG_Backend::kFormatCapabilityTargetCount;
|
||||
const SizeT firstTarget = singleTarget ? capabilityTargetIndex : 0;
|
||||
const SizeT lastTarget =
|
||||
singleTarget ? capabilityTargetIndex + 1 : MG_Backend::kFormatCapabilityTargetCount;
|
||||
for (SizeT targetIndex = firstTarget; targetIndex < lastTarget; ++targetIndex) {
|
||||
for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount;
|
||||
++targetIndex) {
|
||||
if (MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][formatIndex],
|
||||
MG_Backend::FormatCapability::FramebufferRenderable) ||
|
||||
MG_Backend::HasFormatCapability(cache.CaveatCaps[targetIndex][formatIndex],
|
||||
@@ -173,17 +129,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto& attachment = attachments[i];
|
||||
if (!attachment.IsValid()) continue;
|
||||
TextureInternalFormat format = TextureInternalFormat::Unknown;
|
||||
SizeT capabilityTargetIndex = MG_Backend::kFormatCapabilityTargetCount;
|
||||
if (attachment.IsTexture() && attachment.GetTexture()) {
|
||||
format = attachment.GetTexture()->GetFormat();
|
||||
capabilityTargetIndex =
|
||||
MG_Backend::GetFormatCapabilityTargetIndex(attachment.GetTexture()->GetTarget());
|
||||
} else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
||||
format = attachment.GetRenderbuffer()->GetInternalFormat();
|
||||
capabilityTargetIndex = MG_Backend::GetRenderbufferFormatCapabilityTargetIndex();
|
||||
}
|
||||
if (format != TextureInternalFormat::Unknown &&
|
||||
!IsColorInternalFormatRenderable(format, capabilityTargetIndex)) {
|
||||
if (format != TextureInternalFormat::Unknown && !IsColorInternalFormatRenderable(format)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -196,148 +147,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, detail));
|
||||
}
|
||||
|
||||
GLint ClassifyAttachmentComponentType(TextureInternalFormat internalFormat,
|
||||
FramebufferAttachmentType attachmentType) {
|
||||
// A stencil value is an unsigned integer index regardless of the depth half
|
||||
// of a packed format.
|
||||
if (attachmentType == FramebufferAttachmentType::Stencil) return GL_UNSIGNED_INT;
|
||||
switch (internalFormat) {
|
||||
case TextureInternalFormat::R16F:
|
||||
case TextureInternalFormat::RG16F:
|
||||
case TextureInternalFormat::RGB16F:
|
||||
case TextureInternalFormat::RGBA16F:
|
||||
case TextureInternalFormat::R32F:
|
||||
case TextureInternalFormat::RG32F:
|
||||
case TextureInternalFormat::RGB32F:
|
||||
case TextureInternalFormat::RGBA32F:
|
||||
case TextureInternalFormat::R11FG11FB10F:
|
||||
case TextureInternalFormat::RGB9E5:
|
||||
case TextureInternalFormat::DepthComponent32F:
|
||||
case TextureInternalFormat::Depth32FStencil8:
|
||||
return GL_FLOAT;
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::R16I:
|
||||
case TextureInternalFormat::R32I:
|
||||
case TextureInternalFormat::RG8I:
|
||||
case TextureInternalFormat::RG16I:
|
||||
case TextureInternalFormat::RG32I:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGB16I:
|
||||
case TextureInternalFormat::RGB32I:
|
||||
case TextureInternalFormat::RGBA8I:
|
||||
case TextureInternalFormat::RGBA16I:
|
||||
case TextureInternalFormat::RGBA32I:
|
||||
return GL_INT;
|
||||
case TextureInternalFormat::R8UI:
|
||||
case TextureInternalFormat::R16UI:
|
||||
case TextureInternalFormat::R32UI:
|
||||
case TextureInternalFormat::RG8UI:
|
||||
case TextureInternalFormat::RG16UI:
|
||||
case TextureInternalFormat::RG32UI:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
case TextureInternalFormat::RGBA8UI:
|
||||
case TextureInternalFormat::RGBA16UI:
|
||||
case TextureInternalFormat::RGBA32UI:
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
return GL_UNSIGNED_INT;
|
||||
case TextureInternalFormat::R8Snorm:
|
||||
case TextureInternalFormat::R16Snorm:
|
||||
case TextureInternalFormat::RG8Snorm:
|
||||
case TextureInternalFormat::RG16Snorm:
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
case TextureInternalFormat::RGBA8Snorm:
|
||||
case TextureInternalFormat::RGBA16Snorm:
|
||||
return GL_SIGNED_NORMALIZED;
|
||||
case TextureInternalFormat::Unknown:
|
||||
return GL_NONE;
|
||||
default:
|
||||
return GL_UNSIGNED_NORMALIZED;
|
||||
}
|
||||
}
|
||||
|
||||
// Handles the format-derived pnames shared by GetFramebufferAttachmentParameteriv
|
||||
// and its DSA variant. Returns true when pname was one of them.
|
||||
Bool TryAnswerAttachmentFormatQuery(const MG_State::GLState::FramebufferAttachmentObject* attachmentObject,
|
||||
FramebufferAttachmentType attachmentType, Bool depthStencilAlias,
|
||||
GLenum pname, GLint* params, const char* caller) {
|
||||
switch (pname) {
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pname == GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE && depthStencilAlias) {
|
||||
// The depth and stencil components have different types, so the combined
|
||||
// attachment name has no single answer.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", caller,
|
||||
"GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE cannot be queried on "
|
||||
"GL_DEPTH_STENCIL_ATTACHMENT."));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
|
||||
// With OBJECT_TYPE == GL_NONE only OBJECT_TYPE and OBJECT_NAME may be queried.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"No image is attached to the queried attachment point."));
|
||||
return true;
|
||||
}
|
||||
|
||||
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
||||
if (attachmentObject->IsTexture() && attachmentObject->GetTexture()) {
|
||||
internalFormat = attachmentObject->GetTexture()->GetFormat();
|
||||
} else if (attachmentObject->IsRenderbuffer() && attachmentObject->GetRenderbuffer()) {
|
||||
internalFormat = attachmentObject->GetRenderbuffer()->GetInternalFormat();
|
||||
}
|
||||
const auto sizes = MG_Util::GetComponentSizesForInternalFormat(internalFormat);
|
||||
|
||||
switch (pname) {
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE:
|
||||
*params = sizes.Red;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE:
|
||||
*params = sizes.Green;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE:
|
||||
*params = sizes.Blue;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE:
|
||||
*params = sizes.Alpha;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE:
|
||||
*params = sizes.Depth;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE:
|
||||
*params = sizes.Stencil;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING:
|
||||
*params = (internalFormat == TextureInternalFormat::SRGB8 ||
|
||||
internalFormat == TextureInternalFormat::SRGB8Alpha8)
|
||||
? GL_SRGB
|
||||
: GL_LINEAR;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE:
|
||||
*params = ClassifyAttachmentComponentType(internalFormat, attachmentType);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ResolveRepresentableFramebufferTextureUploadTarget(const MG_State::GLState::ITextureObject& textureObject,
|
||||
TextureUploadTarget& outUploadTarget,
|
||||
Bool& outLayered) {
|
||||
@@ -603,27 +412,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target);
|
||||
if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return;
|
||||
|
||||
// Default-framebuffer attachment names (GL_DEPTH, GL_STENCIL, GL_FRONT/GL_BACK
|
||||
// variants) alias onto the equivalent attachment points.
|
||||
switch (attachment) {
|
||||
case GL_DEPTH:
|
||||
attachment = GL_DEPTH_ATTACHMENT;
|
||||
break;
|
||||
case GL_STENCIL:
|
||||
attachment = GL_STENCIL_ATTACHMENT;
|
||||
break;
|
||||
case GL_FRONT:
|
||||
case GL_FRONT_LEFT:
|
||||
case GL_FRONT_RIGHT:
|
||||
case GL_BACK:
|
||||
case GL_BACK_LEFT:
|
||||
case GL_BACK_RIGHT:
|
||||
attachment = GL_COLOR_ATTACHMENT0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const Bool depthStencilAlias = attachment == GL_DEPTH_STENCIL_ATTACHMENT;
|
||||
FramebufferAttachmentType attachmentType = depthStencilAlias
|
||||
? FramebufferAttachmentType::Depth
|
||||
@@ -640,40 +428,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
Bool depthStencilMismatch = false;
|
||||
const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* {
|
||||
if (!depthStencilAlias) {
|
||||
return &framebufferObject->GetAttachment(attachmentType);
|
||||
}
|
||||
|
||||
const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth);
|
||||
if (depthAttachment.IsValid() && !depthAttachment.IsEmpty()) return &depthAttachment;
|
||||
|
||||
const auto& stencilAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Stencil);
|
||||
const Bool depthLive = depthAttachment.IsValid() && !depthAttachment.IsEmpty();
|
||||
const Bool stencilLive = stencilAttachment.IsValid() && !stencilAttachment.IsEmpty();
|
||||
if (depthLive && stencilLive) {
|
||||
const Bool sameObject = depthAttachment.IsTexture() == stencilAttachment.IsTexture() &&
|
||||
(!depthAttachment.IsTexture() || depthAttachment.GetTexture() == stencilAttachment.GetTexture()) &&
|
||||
(!depthAttachment.IsRenderbuffer() ||
|
||||
depthAttachment.GetRenderbuffer() == stencilAttachment.GetRenderbuffer());
|
||||
depthStencilMismatch = !sameObject;
|
||||
} else {
|
||||
// GL_DEPTH_STENCIL_ATTACHMENT means "both halves"; a lone half does not answer it.
|
||||
depthStencilMismatch = depthLive != stencilLive;
|
||||
}
|
||||
if (depthLive) return &depthAttachment;
|
||||
if (stencilLive) return &stencilAttachment;
|
||||
if (stencilAttachment.IsValid() && !stencilAttachment.IsEmpty()) return &stencilAttachment;
|
||||
|
||||
return nullptr;
|
||||
}();
|
||||
|
||||
if (depthStencilMismatch) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetFramebufferAttachmentParameteriv_State",
|
||||
"GL_DEPTH_STENCIL_ATTACHMENT query with different depth and stencil "
|
||||
"attachment images."));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (pname) {
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
|
||||
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
|
||||
@@ -737,10 +505,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: GL_FALSE;
|
||||
break;
|
||||
default:
|
||||
if (TryAnswerAttachmentFormatQuery(attachmentObject, attachmentType, depthStencilAlias, pname, params,
|
||||
"GetFramebufferAttachmentParameteriv_State")) {
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
@@ -1658,8 +1422,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (HasNonRenderableColorAttachment(*framebufferObject)) {
|
||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||
}
|
||||
if (ActiveBackendRejectsDistinctDepthStencil() &&
|
||||
HasUnsupportedDistinctDepthStencilAttachments(*framebufferObject)) {
|
||||
if (IsActiveBackendDirectVulkan() &&
|
||||
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
|
||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||
}
|
||||
return GL_FRAMEBUFFER_COMPLETE;
|
||||
@@ -1686,8 +1450,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (HasNonRenderableColorAttachment(*framebufferObject)) {
|
||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||
}
|
||||
if (ActiveBackendRejectsDistinctDepthStencil() &&
|
||||
HasUnsupportedDistinctDepthStencilAttachments(*framebufferObject)) {
|
||||
if (IsActiveBackendDirectVulkan() &&
|
||||
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
|
||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||
}
|
||||
return GL_FRAMEBUFFER_COMPLETE;
|
||||
@@ -1704,40 +1468,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
|
||||
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
|
||||
|
||||
Bool depthStencilMismatch = false;
|
||||
const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* {
|
||||
if (!depthStencilAlias) {
|
||||
return &framebufferObject->GetAttachment(attachmentType);
|
||||
}
|
||||
|
||||
const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth);
|
||||
if (depthAttachment.IsValid() && !depthAttachment.IsEmpty()) return &depthAttachment;
|
||||
|
||||
const auto& stencilAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Stencil);
|
||||
const Bool depthLive = depthAttachment.IsValid() && !depthAttachment.IsEmpty();
|
||||
const Bool stencilLive = stencilAttachment.IsValid() && !stencilAttachment.IsEmpty();
|
||||
if (depthLive && stencilLive) {
|
||||
const Bool sameObject = depthAttachment.IsTexture() == stencilAttachment.IsTexture() &&
|
||||
(!depthAttachment.IsTexture() || depthAttachment.GetTexture() == stencilAttachment.GetTexture()) &&
|
||||
(!depthAttachment.IsRenderbuffer() ||
|
||||
depthAttachment.GetRenderbuffer() == stencilAttachment.GetRenderbuffer());
|
||||
depthStencilMismatch = !sameObject;
|
||||
} else {
|
||||
// GL_DEPTH_STENCIL_ATTACHMENT means "both halves"; a lone half does not answer it.
|
||||
depthStencilMismatch = depthLive != stencilLive;
|
||||
}
|
||||
if (depthLive) return &depthAttachment;
|
||||
if (stencilLive) return &stencilAttachment;
|
||||
if (stencilAttachment.IsValid() && !stencilAttachment.IsEmpty()) return &stencilAttachment;
|
||||
|
||||
return nullptr;
|
||||
}();
|
||||
|
||||
if (depthStencilMismatch) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"GL_DEPTH_STENCIL_ATTACHMENT query with different depth and stencil "
|
||||
"attachment images."));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (pname) {
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
|
||||
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
|
||||
@@ -1783,10 +1527,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: GL_FALSE;
|
||||
break;
|
||||
default:
|
||||
if (TryAnswerAttachmentFormatQuery(attachmentObject, attachmentType, depthStencilAlias, pname, params,
|
||||
caller)) {
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
|
||||
@@ -213,13 +213,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
GLint maxSamples = 0;
|
||||
for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
|
||||
if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
|
||||
} else if (attachment.IsTexture() && attachment.GetTexture()) {
|
||||
// Multisample texture attachments count too (GL_SAMPLE_BUFFERS must
|
||||
// report 1 for any multisampled draw framebuffer).
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetTexture()->GetSamples()));
|
||||
}
|
||||
if (!attachment.IsRenderbuffer() || !attachment.GetRenderbuffer()) continue;
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
|
||||
}
|
||||
return maxSamples;
|
||||
}
|
||||
@@ -470,14 +465,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_STENCIL_TEST:
|
||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest) ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||
GLfloat value = 0.0f;
|
||||
GetFloatv(pname, &value);
|
||||
*params = value != 0.0f ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -533,19 +520,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = dynamicParameters.ViewportBoundsRangeMax;
|
||||
return;
|
||||
}
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
if (pname == GL_MIN_FRAGMENT_INTERPOLATION_OFFSET) {
|
||||
params[0] = dynamicParameters.MinFragmentInterpolationOffset;
|
||||
} else if (pname == GL_MAX_FRAGMENT_INTERPOLATION_OFFSET) {
|
||||
params[0] = dynamicParameters.MaxFragmentInterpolationOffset;
|
||||
} else {
|
||||
params[0] = static_cast<GLfloat>(dynamicParameters.FragmentInterpolationOffsetBits);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case GL_DEPTH_CLEAR_VALUE:
|
||||
params[0] = MG_State::pGLContext->GetClearDepth();
|
||||
return;
|
||||
@@ -1927,22 +1901,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS:
|
||||
*params = kFrontendMaxTransformFeedbackSeparateComponents;
|
||||
break;
|
||||
// ARB_transform_feedback3 limits. The GL CTS queries these before checking
|
||||
// whether the extension is advertised and requires no GL error; desktop
|
||||
// drivers all accept them, so answer with the separate-attrib capacity and
|
||||
// the single vertex stream the backends provide.
|
||||
case GL_MAX_TRANSFORM_FEEDBACK_BUFFERS:
|
||||
*params = kFrontendMaxTransformFeedbackSeparateAttribs;
|
||||
break;
|
||||
case GL_MAX_VERTEX_STREAMS:
|
||||
*params = 1;
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_ACTIVE:
|
||||
*params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0;
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_PAUSED:
|
||||
*params = 0;
|
||||
break;
|
||||
case GL_MAX_TEXTURE_IMAGE_UNITS:
|
||||
*params = dynamicParameters.MaxTextureImageUnits;
|
||||
break;
|
||||
@@ -1999,15 +1957,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_SUBPIXEL_BITS:
|
||||
*params = std::max(dynamicParameters.ViewportSubpixelBits, kFrontendSubpixelBits);
|
||||
break;
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
*params = static_cast<GLint>(std::lround(dynamicParameters.MinFragmentInterpolationOffset));
|
||||
break;
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxFragmentInterpolationOffset));
|
||||
break;
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
|
||||
*params = dynamicParameters.FragmentInterpolationOffsetBits;
|
||||
break;
|
||||
case GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT:
|
||||
*params = static_cast<Int>(dynamicParameters.UniformBufferOffsetAlignment);
|
||||
break;
|
||||
|
||||
@@ -49,18 +49,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
static bool CheckProgramNameValidity(GLuint program) {
|
||||
if (!MG_State::pGLContext->ValidateProgramName(program)) {
|
||||
// Programs and shaders share one name space: a name that exists but
|
||||
// belongs to a shader is INVALID_OPERATION, a name GL never handed
|
||||
// out is INVALID_VALUE (GL 3.3 core 2.11.x).
|
||||
const ErrorCode error = MG_State::pGLContext->ValidateShaderName(program)
|
||||
? ErrorCode::InvalidOperation
|
||||
: ErrorCode::InvalidValue;
|
||||
MG_State::pGLContext->RecordError(
|
||||
error,
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(program) +
|
||||
(error == ErrorCode::InvalidOperation ? " is not a program object."
|
||||
: " is not a valid name.")));
|
||||
std::to_string(program) + " is not a valid name."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -613,18 +605,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = programObject->GetActiveUniformBlocksMaxNameLength() + 1;
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_VARYINGS:
|
||||
*params = static_cast<GLint>(programObject->GetTransformFeedbackVaryingCount());
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_BUFFER_MODE:
|
||||
*params = static_cast<GLint>(programObject->GetTransformFeedbackBufferMode());
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH:
|
||||
*params = programObject->GetTransformFeedbackVaryingMaxLength();
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
case GL_COMPUTE_WORK_GROUP_SIZE: { // GL >= 4.3
|
||||
if (!programObject->GetLinkStatus() || programObject->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -644,6 +624,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
case GL_PROGRAM_BINARY_LENGTH:
|
||||
|
||||
case GL_TRANSFORM_FEEDBACK_BUFFER_MODE:
|
||||
case GL_TRANSFORM_FEEDBACK_VARYINGS:
|
||||
case GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH:
|
||||
case GL_GEOMETRY_VERTICES_OUT:
|
||||
case GL_GEOMETRY_INPUT_TYPE:
|
||||
case GL_GEOMETRY_OUTPUT_TYPE:
|
||||
@@ -844,13 +827,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
GLboolean IsProgram_State(GLuint program) {
|
||||
// Deletion-flagged names stay valid while the object is still GL-visible (program in
|
||||
// use, shader attached), so name validity is exactly the Is* answer.
|
||||
/* FIXME: Handle situations that:
|
||||
* A program object marked for deletion with glDeleteProgram but still in use as part of current
|
||||
* rendering state is still considered a program object and glIsProgram will return GL_TRUE.
|
||||
*/
|
||||
if (program == 0) return GL_FALSE;
|
||||
return MG_State::pGLContext->ValidateProgramName(program) ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
|
||||
GLboolean IsShader_State(GLuint shader) {
|
||||
/* FIXME: Handle situations that:
|
||||
* A shader object marked for deletion with glDeleteShader but still attached to a program object is still
|
||||
* considered a shader object and glIsShader will return GL_TRUE.
|
||||
*/
|
||||
if (shader == 0) return GL_FALSE;
|
||||
return MG_State::pGLContext->ValidateShaderName(shader) ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
@@ -860,18 +849,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!programObject) return;
|
||||
MGLOG_D("%s: linking program %d", __func__, program);
|
||||
|
||||
// Relinking the program an active transform feedback captures from would
|
||||
// invalidate its varyings mid-capture (GL 3.3 core 2.11.3).
|
||||
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||
MG_State::pGLContext->GetTransformFeedbackProgram().get() == programObject.get()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"The program used by active transform feedback cannot be relinked."));
|
||||
return;
|
||||
}
|
||||
|
||||
static Bool allowVSOnlyPrograms;
|
||||
static Bool initialized = false;
|
||||
if (!initialized) {
|
||||
@@ -915,16 +892,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void UseProgram_State(GLuint program) {
|
||||
MGLOG_D("UseProgram_State: program=%u", program);
|
||||
|
||||
// GL 3.3 core 2.11.3: the program in use may not change while transform
|
||||
// feedback is active (there is no pause in 3.3).
|
||||
if (MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"The current program cannot change while transform feedback is active."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (program == 0) {
|
||||
MG_State::pGLContext->UseProgram(0);
|
||||
return;
|
||||
@@ -2250,66 +2217,4 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void ValidateProgram(GLuint program) {
|
||||
ValidateProgram_State(program);
|
||||
}
|
||||
|
||||
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) {
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (bufferMode != GL_INTERLEAVED_ATTRIBS && bufferMode != GL_SEPARATE_ATTRIBS) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufferMode is not a valid capture mode."));
|
||||
return;
|
||||
}
|
||||
if (count < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "count must be non-negative."));
|
||||
return;
|
||||
}
|
||||
// GL 3.3 core: SEPARATE_ATTRIBS count may not exceed the separate-attrib limit.
|
||||
if (bufferMode == GL_SEPARATE_ATTRIBS && count > 4) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"count exceeds GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS."));
|
||||
return;
|
||||
}
|
||||
Vector<String> names;
|
||||
names.reserve(static_cast<SizeT>(count));
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
names.emplace_back(varyings != nullptr && varyings[i] != nullptr ? varyings[i] : "");
|
||||
}
|
||||
programObject->SetTransformFeedbackVaryings(Move(names), bufferMode);
|
||||
}
|
||||
|
||||
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
|
||||
GLenum* type, GLchar* name) {
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(program) + " has not been successfully linked."));
|
||||
return;
|
||||
}
|
||||
const auto* varying = programObject->GetTransformFeedbackVarying(index);
|
||||
if (varying == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"index is not an active transform feedback varying of the program."));
|
||||
return;
|
||||
}
|
||||
if (size != nullptr) *size = varying->size;
|
||||
if (type != nullptr) *type = varying->type;
|
||||
GLsizei written = 0;
|
||||
if (name != nullptr && bufSize > 0) {
|
||||
written = std::min<GLsizei>(bufSize - 1, static_cast<GLsizei>(varying->name.size()));
|
||||
Memcpy(name, varying->name.data(), static_cast<SizeT>(written));
|
||||
name[written] = '\0';
|
||||
}
|
||||
if (length != nullptr) *length = written;
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -138,7 +138,4 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
||||
void ValidateProgram(GLuint program);
|
||||
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode);
|
||||
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
|
||||
GLenum* type, GLchar* name);
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -27,8 +27,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Bool ended = false;
|
||||
Bool resultCached = false;
|
||||
Uint64 cachedResult = 0;
|
||||
// Transform feedback primitive counter at BeginQuery time.
|
||||
Uint64 counterSnapshot = 0;
|
||||
};
|
||||
|
||||
// Query calls may arrive from any thread (launchers migrate the context
|
||||
@@ -43,11 +41,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLuint g_nextQueryId = 1;
|
||||
// Id of the query currently active on GL_TIME_ELAPSED (0 = none).
|
||||
GLuint g_activeTimeElapsedQueryId = 0;
|
||||
// Ids of the queries active on the transform feedback targets (0 = none).
|
||||
GLuint g_activePrimitivesWrittenQueryId = 0;
|
||||
GLuint g_activePrimitivesGeneratedQueryId = 0;
|
||||
// Id of the query active on GL_SAMPLES_PASSED (0 = none).
|
||||
GLuint g_activeSamplesPassedQueryId = 0;
|
||||
|
||||
Bool TimerQueryDisabled() {
|
||||
return MG_Config::Features.DisableTimerQuery;
|
||||
@@ -133,11 +126,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
outValue = 0;
|
||||
return true;
|
||||
}
|
||||
// ANY_SAMPLES_PASSED* report a boolean.
|
||||
if (queryObject->target == GL_ANY_SAMPLES_PASSED ||
|
||||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
result = result != 0 ? 1 : 0;
|
||||
}
|
||||
// Final value produced (or no GetQueryResult64 hook: the
|
||||
// query degrades to a zero result); the backend handle is
|
||||
// consumed and the value cached for later reads.
|
||||
@@ -192,24 +180,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
QueryObject* queryObject = it->second;
|
||||
if (queryObject->active) {
|
||||
// Implicitly end before deletion, releasing the matching active slot.
|
||||
if (queryObject->target == GL_SAMPLES_PASSED || queryObject->target == GL_ANY_SAMPLES_PASSED ||
|
||||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
|
||||
endOcclusionQuery && queryObject->backendHandle) {
|
||||
endOcclusionQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->active = false;
|
||||
g_activeSamplesPassedQueryId = 0;
|
||||
} else if (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ||
|
||||
queryObject->target == GL_PRIMITIVES_GENERATED) {
|
||||
queryObject->active = false;
|
||||
(queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
|
||||
? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId) = 0;
|
||||
} else {
|
||||
EndTimeElapsedQueryLocked(queryObject);
|
||||
}
|
||||
EndTimeElapsedQueryLocked(queryObject); // implicitly end before deletion
|
||||
}
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
@@ -233,15 +204,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void BeginQuery(GLenum target, GLuint id) {
|
||||
const Bool isTransformFeedbackQuery =
|
||||
target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED;
|
||||
const Bool isOcclusionQuery =
|
||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
|
||||
// GL_TIMESTAMP is not a valid BeginQuery target; the occlusion targets
|
||||
// need backend support.
|
||||
if (target != GL_TIME_ELAPSED) {
|
||||
// Only GL_TIME_ELAPSED timer queries are implemented (occlusion and
|
||||
// primitive queries remain stubs); GL_TIMESTAMP is not a valid
|
||||
// BeginQuery target either.
|
||||
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
|
||||
return;
|
||||
}
|
||||
@@ -255,13 +221,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
|
||||
return;
|
||||
}
|
||||
GLuint& activeQueryId = isTransformFeedbackQuery
|
||||
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId)
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
|
||||
if (activeQueryId != 0) {
|
||||
if (g_activeTimeElapsedQueryId != 0) {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
|
||||
"A query is already active on this target.");
|
||||
"A query is already active on GL_TIME_ELAPSED.");
|
||||
return;
|
||||
}
|
||||
if (queryObject->active) {
|
||||
@@ -277,72 +239,25 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ResetQueryObjectLocked(queryObject); // discard any previous result
|
||||
queryObject->target = target;
|
||||
queryObject->active = true;
|
||||
if (isTransformFeedbackQuery) {
|
||||
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
|
||||
// the CPU accounting delta stays as the fallback when the backend lacks them.
|
||||
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
|
||||
queryObject->backendHandle =
|
||||
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
|
||||
queryObject->counterSnapshot = MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
|
||||
} else if (isOcclusionQuery) {
|
||||
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
|
||||
} else {
|
||||
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
|
||||
queryObject->backendHandle =
|
||||
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
|
||||
}
|
||||
activeQueryId = id;
|
||||
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
|
||||
queryObject->backendHandle =
|
||||
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
|
||||
g_activeTimeElapsedQueryId = id;
|
||||
}
|
||||
|
||||
void EndQuery(GLenum target) {
|
||||
const Bool isTransformFeedbackQuery =
|
||||
target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED;
|
||||
const Bool isOcclusionQuery =
|
||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
|
||||
if (target != GL_TIME_ELAPSED) {
|
||||
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
|
||||
return;
|
||||
}
|
||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||
GLuint& activeQueryId = isTransformFeedbackQuery
|
||||
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId)
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
|
||||
if (activeQueryId == 0) {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target.");
|
||||
if (g_activeTimeElapsedQueryId == 0) {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on GL_TIME_ELAPSED.");
|
||||
return;
|
||||
}
|
||||
auto* queryObject = FindQueryObjectLocked(activeQueryId);
|
||||
auto* queryObject = FindQueryObjectLocked(g_activeTimeElapsedQueryId);
|
||||
if (!queryObject) {
|
||||
activeQueryId = 0; // should not happen; keep state consistent
|
||||
return;
|
||||
}
|
||||
if (isTransformFeedbackQuery) {
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
|
||||
endXfbPrimitivesQuery(queryObject->backendHandle);
|
||||
}
|
||||
// Result comes from the GPU query at read time.
|
||||
} else {
|
||||
queryObject->cachedResult =
|
||||
MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter() - queryObject->counterSnapshot;
|
||||
queryObject->resultCached = true;
|
||||
}
|
||||
queryObject->active = false;
|
||||
queryObject->ended = true;
|
||||
activeQueryId = 0;
|
||||
return;
|
||||
}
|
||||
if (isOcclusionQuery) {
|
||||
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
|
||||
endOcclusionQuery && queryObject->backendHandle) {
|
||||
endOcclusionQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->active = false;
|
||||
queryObject->ended = true;
|
||||
activeQueryId = 0;
|
||||
g_activeTimeElapsedQueryId = 0; // should not happen; keep state consistent
|
||||
return;
|
||||
}
|
||||
EndTimeElapsedQueryLocked(queryObject);
|
||||
@@ -388,25 +303,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
switch (pname) {
|
||||
case GL_CURRENT_QUERY: {
|
||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||
switch (target) {
|
||||
case GL_TIME_ELAPSED:
|
||||
*params = static_cast<GLint>(g_activeTimeElapsedQueryId);
|
||||
break;
|
||||
case GL_SAMPLES_PASSED:
|
||||
case GL_ANY_SAMPLES_PASSED:
|
||||
case GL_ANY_SAMPLES_PASSED_CONSERVATIVE:
|
||||
*params = static_cast<GLint>(g_activeSamplesPassedQueryId);
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN:
|
||||
*params = static_cast<GLint>(g_activePrimitivesWrittenQueryId);
|
||||
break;
|
||||
case GL_PRIMITIVES_GENERATED:
|
||||
*params = static_cast<GLint>(g_activePrimitivesGeneratedQueryId);
|
||||
break;
|
||||
default:
|
||||
*params = 0;
|
||||
break;
|
||||
}
|
||||
// Only GL_TIME_ELAPSED queries can be active; GL_TIMESTAMP queries
|
||||
// never are, and other targets remain unimplemented.
|
||||
*params = target == GL_TIME_ELAPSED ? static_cast<GLint>(g_activeTimeElapsedQueryId) : 0;
|
||||
return;
|
||||
}
|
||||
case GL_QUERY_COUNTER_BITS: {
|
||||
@@ -414,13 +313,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// time: IsTimerQuerySupported is the dynamic truth (extension /
|
||||
// entry points / timestamp valid bits at call time, not at table
|
||||
// init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always
|
||||
// wins.
|
||||
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
*params = occlusionSupported ? (target == GL_SAMPLES_PASSED ? 32 : 1) : 0;
|
||||
return;
|
||||
}
|
||||
// wins. Non-timer targets remain unimplemented and report 0.
|
||||
const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP;
|
||||
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
|
||||
const Bool supported =
|
||||
|
||||
@@ -185,11 +185,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
static thread_local Vector<GLuint> names;
|
||||
MG_State::pGLContext->GenSamplerNames(count, names);
|
||||
Memcpy(samplers, names.data(), count * sizeof(GLuint));
|
||||
// Unlike textures/buffers, glGenSamplers CREATES the sampler objects: each name
|
||||
// is immediately a sampler (glIsSampler == GL_TRUE before any bind).
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->CreateSamplerObject(names[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void DeleteSamplers_State(GLsizei count, const GLuint* samplers) {
|
||||
|
||||
@@ -133,33 +133,4 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
values[0] = value;
|
||||
}
|
||||
}
|
||||
|
||||
void DestroyAllSyncObjects() {
|
||||
// Detach the registry under the lock, release outside it. Entries the app
|
||||
// already deleted were erased by DeleteSync, so nothing here double-frees;
|
||||
// a DeleteSync racing this sweep finds an empty registry and returns. A
|
||||
// thread still blocked inside ClientWaitSync/GetSynciv during teardown
|
||||
// holds a raw SyncObject* these deletes invalidate - the same undefined
|
||||
// race an app-driven DeleteSync already has.
|
||||
UnorderedMap<GLsync, SyncObject*> orphans;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||
orphans.swap(g_liveSyncObjects);
|
||||
}
|
||||
if (orphans.empty()) {
|
||||
return;
|
||||
}
|
||||
// Both backends' DeleteSync only free the heap wrapper once their GL
|
||||
// context/renderer is gone (generation/current-thread guards), so this is
|
||||
// safe after the backend has released its EGL resources - but not after
|
||||
// the function table itself is cleared.
|
||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
||||
for (const auto& [_, syncObject] : orphans) {
|
||||
if (backendDeleteSync && syncObject->backendHandle) {
|
||||
backendDeleteSync(syncObject->backendHandle);
|
||||
}
|
||||
delete syncObject;
|
||||
}
|
||||
MGLOG_D("DestroyAllSyncObjects: reclaimed %zu sync object(s) the app left undeleted", orphans.size());
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -16,12 +16,4 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout);
|
||||
void DeleteSync(GLsync sync);
|
||||
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values);
|
||||
// Destroys every still-registered sync object exactly as DeleteSync would.
|
||||
// GL requires syncs to die with their context; called only from full library
|
||||
// teardown (DestroyImpl), where no context survives on any thread, so the
|
||||
// process-global registry can be drained wholesale. Must run while the
|
||||
// backend function table is still populated: each backend handle has to be
|
||||
// released by the backend that created it, never by a later re-initialized
|
||||
// one.
|
||||
void DestroyAllSyncObjects();
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -2820,22 +2820,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"The attachment specified by the read buffer is incomplete.")); \
|
||||
return false; \
|
||||
}
|
||||
if (isDepth && isStencil) {
|
||||
// A combined internalformat copies both halves, so the read framebuffer
|
||||
// must populate both attachment points.
|
||||
const auto& stencilAttachment = currentReadFBO->GetAttachment(FramebufferAttachmentType::Stencil);
|
||||
const auto& depthAttachment = currentReadFBO->GetAttachment(FramebufferAttachmentType::Depth);
|
||||
if (!depthAttachment.IsValid() || depthAttachment.IsEmpty() || !stencilAttachment.IsValid() ||
|
||||
stencilAttachment.IsEmpty()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "CopyTexImage2D_State",
|
||||
"DEPTH_STENCIL copy requires both depth and stencil attachments in the read framebuffer."));
|
||||
return false;
|
||||
}
|
||||
GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Depth);
|
||||
} else if (isDepth) {
|
||||
if (isDepth) {
|
||||
GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Depth);
|
||||
} else if (isStencil) {
|
||||
GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Stencil);
|
||||
|
||||
@@ -85,8 +85,6 @@ namespace MobileGL::MG_Impl {
|
||||
GETPROC(CGLGetPixelFormat, name);
|
||||
GETPROC(CGLSetCurrentContext, name);
|
||||
GETPROC(CGLGetCurrentContext, name);
|
||||
GETPROC(CGLSetVirtualScreen, name);
|
||||
GETPROC(CGLGetVirtualScreen, name);
|
||||
GETPROC(CGLSetParameter, name);
|
||||
GETPROC(CGLGetParameter, name);
|
||||
GETPROC(CGLUpdateContext, name);
|
||||
|
||||
@@ -29,19 +29,10 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
char kContextViewKey;
|
||||
char kContextLayerKey;
|
||||
|
||||
std::once_flag g_installOnce;
|
||||
IMP g_pixelFormatDealloc = nullptr;
|
||||
IMP g_contextDealloc = nullptr;
|
||||
|
||||
std::mutex& HookInstallMutex() {
|
||||
static auto* mutex = new std::mutex();
|
||||
return *mutex;
|
||||
}
|
||||
|
||||
Bool& HooksInstalled() {
|
||||
static auto* installed = new Bool(false);
|
||||
return *installed;
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
Fn ObjcMsgSend() {
|
||||
return reinterpret_cast<Fn>(objc_msgSend);
|
||||
@@ -440,12 +431,12 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
method_setImplementation(method, replacement);
|
||||
}
|
||||
|
||||
Bool InstallHooksOnce() {
|
||||
void InstallHooksOnce() {
|
||||
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
|
||||
Class contextClass = objc_getClass("NSOpenGLContext");
|
||||
if (!pixelFormatClass || !contextClass) {
|
||||
MGLOG_W("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
ReplaceInstanceMethod(pixelFormatClass, "initWithAttributes:",
|
||||
@@ -480,34 +471,11 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
ReplaceInstanceMethod(contextClass, "dealloc", reinterpret_cast<IMP>(ContextDealloc), &g_contextDealloc);
|
||||
|
||||
MGLOG_I("NSOpenGLImpl hooks installed");
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void InstallHooks() {
|
||||
const std::lock_guard<std::mutex> lock(HookInstallMutex());
|
||||
if (!HooksInstalled()) {
|
||||
// Do not permanently consume the install attempt when the OpenGL
|
||||
// framework has not registered its Objective-C classes yet. The
|
||||
// dyld bootstrap normally runs after framework dependencies, but
|
||||
// an explicitly loaded/static-linked MobileGL can arrive earlier.
|
||||
HooksInstalled() = InstallHooksOnce();
|
||||
}
|
||||
std::call_once(g_installOnce, InstallHooksOnce);
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::NSOpenGLImpl
|
||||
|
||||
namespace {
|
||||
// SDL's Cocoa backend creates NSOpenGLPixelFormat/NSOpenGLContext before
|
||||
// its first dlsym("glGetString") or other MobileGL host-API call. Install
|
||||
// only the lightweight Objective-C dispatch hooks while the injected dylib
|
||||
// is loading so those first Cocoa objects are routed through CGLImpl. The
|
||||
// hooked context constructor reaches EGLImpl::GetDisplay(), which performs
|
||||
// the full, thread-safe MobileGL initialization outside this bootstrap.
|
||||
//
|
||||
// There is intentionally no matching destructor: backend teardown remains
|
||||
// owned by the EGL lifecycle and process-exit globals remain leak-at-exit.
|
||||
__attribute__((constructor)) void BootstrapNSOpenGLHooks() {
|
||||
MobileGL::MG_Impl::NSOpenGLImpl::InstallHooks();
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
@@ -231,21 +231,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
return m_resource.Bytes();
|
||||
}
|
||||
|
||||
Bool BufferObject::EnsureGpuResidentStorage() {
|
||||
if (m_resource.IsGpuResident()) {
|
||||
return true;
|
||||
}
|
||||
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) {
|
||||
return false;
|
||||
}
|
||||
void* base = g_bufferBackendOps->AcquirePersistentMap(*this);
|
||||
if (base == nullptr) {
|
||||
return false;
|
||||
}
|
||||
m_resource.AdoptPersistentMap(base);
|
||||
return true;
|
||||
}
|
||||
|
||||
void* BufferObject::AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access) {
|
||||
MOBILEGL_ASSERT(range.end <= m_size && range.start <= range.end,
|
||||
"AcquireMemoryRange out of bounds: range (%zu, %zu) exceeds m_size (%zu)", range.start,
|
||||
|
||||
@@ -133,11 +133,6 @@ namespace MobileGL {
|
||||
|
||||
void* AcquireMemory(Bool markMapped, Bool read, Bool write);
|
||||
void* AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access);
|
||||
// Adopt backend host-visible coherent GPU storage as the source of truth
|
||||
// (used for GPU-written targets like transform feedback capture, so
|
||||
// MapBuffer/GetBufferSubData read real GPU results). No-op when already
|
||||
// resident or when the backend declines.
|
||||
Bool EnsureGpuResidentStorage();
|
||||
void ReleaseMemory();
|
||||
void FlushMemoryRange(SizeT offset, SizeT length);
|
||||
|
||||
|
||||
@@ -211,47 +211,6 @@ namespace MobileGL {
|
||||
void SetScissorBox(IntVec4 box); // x, y, width, height
|
||||
const IntVec4& GetScissorBox() const; // x, y, width, height
|
||||
|
||||
// Transform feedback (GL 3.0 core Begin/End; no feedback objects yet)
|
||||
void BeginTransformFeedback(GLenum primitiveMode, const SharedPtr<ProgramObject>& program) {
|
||||
m_transformFeedbackActive = true;
|
||||
m_transformFeedbackPrimitiveMode = primitiveMode;
|
||||
m_transformFeedbackProgram = program;
|
||||
++m_transformFeedbackGeneration;
|
||||
m_transformFeedbackCapturedVertices = 0;
|
||||
m_transformFeedbackInputPrimitives = 0;
|
||||
}
|
||||
void EndTransformFeedback() {
|
||||
m_transformFeedbackActive = false;
|
||||
m_transformFeedbackProgram.reset();
|
||||
}
|
||||
Bool IsTransformFeedbackActive() const { return m_transformFeedbackActive; }
|
||||
GLenum GetTransformFeedbackPrimitiveMode() const { return m_transformFeedbackPrimitiveMode; }
|
||||
const SharedPtr<ProgramObject>& GetTransformFeedbackProgram() const {
|
||||
return m_transformFeedbackProgram;
|
||||
}
|
||||
// Bumped on every BeginTransformFeedback; the backend uses it to
|
||||
// distinguish "resume appending" from "fresh capture".
|
||||
Uint64 GetTransformFeedbackGeneration() const { return m_transformFeedbackGeneration; }
|
||||
// CPU-side primitive accounting for the transform feedback queries:
|
||||
// every captured draw adds its primitive count (draws without a
|
||||
// geometry stage write exactly what they generate).
|
||||
void AddTransformFeedbackPrimitives(Uint64 primitives) {
|
||||
m_transformFeedbackPrimitiveCounter += primitives;
|
||||
}
|
||||
Uint64 GetTransformFeedbackPrimitiveCounter() const { return m_transformFeedbackPrimitiveCounter; }
|
||||
// Vertices already captured since BeginTransformFeedback (drives the
|
||||
// buffer-capacity clamp on the primitives-written accounting).
|
||||
void AddTransformFeedbackCapturedVertices(Uint64 vertices) {
|
||||
m_transformFeedbackCapturedVertices += vertices;
|
||||
}
|
||||
Uint64 GetTransformFeedbackCapturedVertices() const { return m_transformFeedbackCapturedVertices; }
|
||||
// Raw assembled input primitives fed to the capture stage since Begin
|
||||
// (pre-clamp; drives the GS strip capture-order fixup at EndTF).
|
||||
void AddTransformFeedbackInputPrimitives(Uint64 primitives) {
|
||||
m_transformFeedbackInputPrimitives += primitives;
|
||||
}
|
||||
Uint64 GetTransformFeedbackInputPrimitives() const { return m_transformFeedbackInputPrimitives; }
|
||||
|
||||
// Framebuffer
|
||||
void GenFramebufferNames(Uint number, Vector<Uint>& framebuffers);
|
||||
const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index);
|
||||
@@ -284,13 +243,6 @@ namespace MobileGL {
|
||||
BufferState m_bufferState;
|
||||
VertexArrayState m_vertexArrayState;
|
||||
Array<CurrentVertexAttributeValue, VertexArrayObject::MAX_VERTEX_ATTRIBS> m_currentVertexAttributes{};
|
||||
Bool m_transformFeedbackActive = false;
|
||||
GLenum m_transformFeedbackPrimitiveMode = GL_POINTS;
|
||||
SharedPtr<ProgramObject> m_transformFeedbackProgram;
|
||||
Uint64 m_transformFeedbackGeneration = 0;
|
||||
Uint64 m_transformFeedbackPrimitiveCounter = 0;
|
||||
Uint64 m_transformFeedbackCapturedVertices = 0;
|
||||
Uint64 m_transformFeedbackInputPrimitives = 0;
|
||||
TextureState m_textureState;
|
||||
ProgramState m_programState;
|
||||
RenderState m_renderState;
|
||||
|
||||
@@ -170,230 +170,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_uniformNameMaxLength = 0;
|
||||
m_attribInNameMaxLength = 0;
|
||||
m_uniformBlockNameMaxLength = 0;
|
||||
m_xfbVaryings.clear();
|
||||
m_xfbStrides.clear();
|
||||
m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
m_xfbVaryingNameMaxLength = 0;
|
||||
m_linkStatus = false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// GL type enum for a vertex-stage output symbol captured by transform
|
||||
// feedback. Covers the scalar/vector/matrix float+integer types transform
|
||||
// feedback may legally capture in GL 3.3.
|
||||
Bool ResolveXfbSymbolType(const glslang::TType& type, GLenum& outType, GLint& outArraySize,
|
||||
Uint32& outBytesPerElement) {
|
||||
outArraySize = type.isArray() ? type.getOuterArraySize() : 1;
|
||||
const Int columns = type.isMatrix() ? type.getMatrixCols() : 1;
|
||||
const Int components = type.isMatrix() ? type.getMatrixRows()
|
||||
: (type.isVector() ? type.getVectorSize() : 1);
|
||||
const glslang::TBasicType basic = type.getBasicType();
|
||||
static constexpr GLenum kFloatTypes[5] = {0, GL_FLOAT, GL_FLOAT_VEC2, GL_FLOAT_VEC3, GL_FLOAT_VEC4};
|
||||
static constexpr GLenum kIntTypes[5] = {0, GL_INT, GL_INT_VEC2, GL_INT_VEC3, GL_INT_VEC4};
|
||||
static constexpr GLenum kUintTypes[5] = {0, GL_UNSIGNED_INT, GL_UNSIGNED_INT_VEC2, GL_UNSIGNED_INT_VEC3,
|
||||
GL_UNSIGNED_INT_VEC4};
|
||||
if (type.isMatrix()) {
|
||||
if (basic != glslang::EbtFloat) return false;
|
||||
static constexpr GLenum kMatTypes[5][5] = {
|
||||
{}, {},
|
||||
{0, 0, GL_FLOAT_MAT2, GL_FLOAT_MAT2x3, GL_FLOAT_MAT2x4},
|
||||
{0, 0, GL_FLOAT_MAT3x2, GL_FLOAT_MAT3, GL_FLOAT_MAT3x4},
|
||||
{0, 0, GL_FLOAT_MAT4x2, GL_FLOAT_MAT4x3, GL_FLOAT_MAT4},
|
||||
};
|
||||
if (columns < 2 || columns > 4 || components < 2 || components > 4) return false;
|
||||
outType = kMatTypes[columns][components];
|
||||
} else if (components >= 1 && components <= 4) {
|
||||
switch (basic) {
|
||||
case glslang::EbtFloat: outType = kFloatTypes[components]; break;
|
||||
case glslang::EbtInt: outType = kIntTypes[components]; break;
|
||||
case glslang::EbtUint: outType = kUintTypes[components]; break;
|
||||
default: return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
outBytesPerElement = static_cast<Uint32>(columns * components) * 4u;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ProgramObject::ResolveTransformFeedbackVaryings() {
|
||||
m_xfbVaryings.clear();
|
||||
m_xfbStrides.clear();
|
||||
m_xfbBufferMode = m_requestedXfbBufferMode;
|
||||
m_xfbVaryingNameMaxLength = 0;
|
||||
if (m_requestedXfbVaryings.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Capture happens at the last vertex-processing stage (geometry, then
|
||||
// tessellation evaluation, then vertex).
|
||||
const glslang::TIntermediate* captureIntermediate = nullptr;
|
||||
for (EShLanguage stage : {EShLangGeometry, EShLangTessEvaluation, EShLangVertex}) {
|
||||
captureIntermediate = m_program->getIntermediate(stage);
|
||||
if (captureIntermediate != nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (captureIntermediate == nullptr) {
|
||||
m_infoLog = "Transform feedback varyings requested but the program has no vertex-processing stage.";
|
||||
return false;
|
||||
}
|
||||
const glslang::TIntermAggregate* linkerObjects = captureIntermediate->findLinkerObjects();
|
||||
|
||||
const Bool interleaved = m_xfbBufferMode == GL_INTERLEAVED_ATTRIBS;
|
||||
Uint32 interleavedOffset = 0;
|
||||
for (SizeT i = 0; i < m_requestedXfbVaryings.size(); ++i) {
|
||||
const String& name = m_requestedXfbVaryings[i];
|
||||
for (SizeT j = 0; j < i; ++j) {
|
||||
if (m_requestedXfbVaryings[j] == name) {
|
||||
m_infoLog = "Transform feedback varying '" + name + "' is specified more than once.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
XfbVarying varying;
|
||||
varying.name = name;
|
||||
Uint32 bytesPerElement = 0;
|
||||
Bool resolved = false;
|
||||
if (name == "gl_Position") {
|
||||
varying.type = GL_FLOAT_VEC4;
|
||||
varying.size = 1;
|
||||
bytesPerElement = 16;
|
||||
resolved = true;
|
||||
} else if (name == "gl_PointSize") {
|
||||
varying.type = GL_FLOAT;
|
||||
varying.size = 1;
|
||||
bytesPerElement = 4;
|
||||
resolved = true;
|
||||
} else if (linkerObjects != nullptr) {
|
||||
for (const auto* node : linkerObjects->getSequence()) {
|
||||
const glslang::TIntermSymbol* symbol = node->getAsSymbolNode();
|
||||
if (symbol == nullptr || symbol->getType().getQualifier().storage != glslang::EvqVaryingOut) {
|
||||
continue;
|
||||
}
|
||||
if (symbol->getName() != name.c_str()) {
|
||||
continue;
|
||||
}
|
||||
resolved = ResolveXfbSymbolType(symbol->getType(), varying.type, varying.size, bytesPerElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!resolved) {
|
||||
m_infoLog = "Transform feedback varying '" + name + "' is not an output of the vertex stage.";
|
||||
return false;
|
||||
}
|
||||
|
||||
varying.byteSize = bytesPerElement * static_cast<Uint32>(varying.size);
|
||||
if (interleaved) {
|
||||
varying.bufferIndex = 0;
|
||||
varying.offsetBytes = interleavedOffset;
|
||||
interleavedOffset += varying.byteSize;
|
||||
} else {
|
||||
varying.bufferIndex = static_cast<Uint32>(i);
|
||||
varying.offsetBytes = 0;
|
||||
}
|
||||
m_xfbVaryingNameMaxLength =
|
||||
std::max(m_xfbVaryingNameMaxLength, static_cast<Int>(name.size()) + 1);
|
||||
m_xfbVaryings.push_back(Move(varying));
|
||||
}
|
||||
|
||||
constexpr Uint32 kMaxSeparateAttribs = 4;
|
||||
constexpr Uint32 kMaxSeparateComponents = 4;
|
||||
constexpr Uint32 kMaxInterleavedComponents = 64;
|
||||
if (interleaved) {
|
||||
if (interleavedOffset > kMaxInterleavedComponents * 4) {
|
||||
m_infoLog = "Transform feedback interleaved capture exceeds "
|
||||
"GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS.";
|
||||
return false;
|
||||
}
|
||||
m_xfbStrides.assign(1, interleavedOffset);
|
||||
} else {
|
||||
if (m_xfbVaryings.size() > kMaxSeparateAttribs) {
|
||||
m_infoLog = "Transform feedback separate capture exceeds "
|
||||
"GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS.";
|
||||
return false;
|
||||
}
|
||||
m_xfbStrides.resize(m_xfbVaryings.size());
|
||||
for (SizeT i = 0; i < m_xfbVaryings.size(); ++i) {
|
||||
if (m_xfbVaryings[i].byteSize > kMaxSeparateComponents * 4) {
|
||||
m_infoLog = "Transform feedback varying '" + m_xfbVaryings[i].name +
|
||||
"' exceeds GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS.";
|
||||
return false;
|
||||
}
|
||||
m_xfbStrides[i] = m_xfbVaryings[i].byteSize;
|
||||
}
|
||||
}
|
||||
|
||||
ResolveGsTriangleStripCapture(captureIntermediate);
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Extracts a geometry shader's per-invocation EmitVertex/EndPrimitive sequence
|
||||
// when it is statically knowable (no emit inside selection/loop/switch). Vulkan
|
||||
// transform feedback captures triangle strips in plain (i, i+1, i+2) order while
|
||||
// GL decomposes odd strip triangles as (i+1, i, i+2) (GL 4.6 table 10.1); with
|
||||
// the static strip lengths the capture buffer can be reordered after EndTF.
|
||||
class GsEmitSequenceTraverser final : public glslang::TIntermTraverser {
|
||||
public:
|
||||
bool visitAggregate(glslang::TVisit, glslang::TIntermAggregate* node) override {
|
||||
if (node->getOp() == glslang::EOpEmitVertex) {
|
||||
++emitCount;
|
||||
hasEmit = true;
|
||||
} else if (node->getOp() == glslang::EOpEndPrimitive) {
|
||||
FlushStrip();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool visitSelection(glslang::TVisit, glslang::TIntermSelection*) override {
|
||||
inControlFlow = true;
|
||||
return true;
|
||||
}
|
||||
bool visitLoop(glslang::TVisit, glslang::TIntermLoop*) override {
|
||||
inControlFlow = true;
|
||||
return true;
|
||||
}
|
||||
bool visitSwitch(glslang::TVisit, glslang::TIntermSwitch*) override {
|
||||
inControlFlow = true;
|
||||
return true;
|
||||
}
|
||||
void FlushStrip() {
|
||||
if (emitCount >= 3) {
|
||||
stripTriangles.push_back(static_cast<Uint32>(emitCount - 2));
|
||||
}
|
||||
emitCount = 0;
|
||||
}
|
||||
|
||||
Vector<Uint32> stripTriangles;
|
||||
Uint32 emitCount = 0;
|
||||
Bool hasEmit = false;
|
||||
Bool inControlFlow = false;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
void ProgramObject::ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate) {
|
||||
m_gsStripTriangles.clear();
|
||||
m_gsStripCaptureFixup = false;
|
||||
if (captureIntermediate == nullptr || m_program == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (m_program->getIntermediate(EShLangGeometry) != captureIntermediate) {
|
||||
return;
|
||||
}
|
||||
if (captureIntermediate->getOutputPrimitive() != glslang::ElgTriangleStrip) {
|
||||
return;
|
||||
}
|
||||
GsEmitSequenceTraverser traverser;
|
||||
const_cast<glslang::TIntermediate*>(captureIntermediate)->getTreeRoot()->traverse(&traverser);
|
||||
traverser.FlushStrip(); // the invocation end acts as an implicit EndPrimitive
|
||||
if (!traverser.hasEmit || traverser.inControlFlow || traverser.stripTriangles.empty()) {
|
||||
return;
|
||||
}
|
||||
m_gsStripTriangles = Move(traverser.stripTriangles);
|
||||
m_gsStripCaptureFixup = true;
|
||||
}
|
||||
|
||||
bool ProgramObject::ShaderIsAttached(const SharedPtr<ShaderObject>& shader) {
|
||||
MGLOG_D("ProgramObject %u: ShaderIsAttached check for shader %p", m_externalIndex, shader.get());
|
||||
auto it = std::find_if(m_shaders.begin(), m_shaders.end(),
|
||||
@@ -548,12 +327,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (!ValidateFragmentOutputLocations()) {
|
||||
return;
|
||||
}
|
||||
if (!ResolveTransformFeedbackVaryings()) {
|
||||
m_linkStatus = false;
|
||||
MGLOG_E("ProgramObject %u: transform feedback varying resolution failed: %s", m_externalIndex,
|
||||
m_infoLog.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
MGLOG_D("ProgramObject %u: Starting binary generation", m_externalIndex);
|
||||
GenerateBinary();
|
||||
|
||||
@@ -334,32 +334,16 @@ namespace MobileGL::MG_State::GLState {
|
||||
// draw. The memo is keyed by (backendStateVersion, flags); ResetLinkArtifacts and
|
||||
// the binding setters below invalidate it by bumping m_backendStateVersion.
|
||||
Bool GetBackendHashMemo(Uint flags, Uint64& outHash) const {
|
||||
if (m_backendHashMemoVersion != m_backendStateVersion) return false;
|
||||
for (const auto& slot : m_backendHashMemoSlots) {
|
||||
if (slot.valid && slot.flags == flags) {
|
||||
outHash = slot.hash;
|
||||
return true;
|
||||
}
|
||||
if (m_backendHashMemoVersion != m_backendStateVersion || m_backendHashMemoFlags != flags) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
outHash = m_backendHashMemo;
|
||||
return true;
|
||||
}
|
||||
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
|
||||
if (m_backendHashMemoVersion != m_backendStateVersion) {
|
||||
for (auto& slot : m_backendHashMemoSlots) slot.valid = false;
|
||||
m_backendHashMemoVersion = m_backendStateVersion;
|
||||
m_backendHashMemoNextSlot = 0;
|
||||
}
|
||||
for (auto& slot : m_backendHashMemoSlots) {
|
||||
if (slot.valid && slot.flags == flags) {
|
||||
slot.hash = hash;
|
||||
return;
|
||||
}
|
||||
}
|
||||
auto& slot = m_backendHashMemoSlots[m_backendHashMemoNextSlot];
|
||||
slot.flags = flags;
|
||||
slot.hash = hash;
|
||||
slot.valid = true;
|
||||
m_backendHashMemoNextSlot = (m_backendHashMemoNextSlot + 1) % kBackendHashMemoSlotCount;
|
||||
m_backendHashMemo = hash;
|
||||
m_backendHashMemoVersion = m_backendStateVersion;
|
||||
m_backendHashMemoFlags = flags;
|
||||
}
|
||||
|
||||
void SetUniformSamplerOrImageUnitIndex(Uint location, Int unit) {
|
||||
@@ -465,39 +449,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
return it == m_shaders.end() ? -1 : (Int)std::distance(m_shaders.begin(), it);
|
||||
}
|
||||
|
||||
// Transform feedback (GL 3.0 core: glTransformFeedbackVaryings applies on
|
||||
// the NEXT link; the linked snapshot below is what draws and queries see).
|
||||
struct XfbVarying {
|
||||
String name;
|
||||
GLenum type = GL_FLOAT;
|
||||
GLint size = 1; // array element count
|
||||
Uint32 bufferIndex = 0; // capture buffer slot
|
||||
Uint32 offsetBytes = 0; // offset within the capture buffer
|
||||
Uint32 byteSize = 0; // bytes captured per vertex for this varying
|
||||
};
|
||||
void SetTransformFeedbackVaryings(Vector<String>&& names, GLenum bufferMode) {
|
||||
m_requestedXfbVaryings = Move(names);
|
||||
m_requestedXfbBufferMode = bufferMode;
|
||||
}
|
||||
GLenum GetTransformFeedbackBufferMode() const { return m_xfbBufferMode; }
|
||||
SizeT GetTransformFeedbackVaryingCount() const { return m_xfbVaryings.size(); }
|
||||
const XfbVarying* GetTransformFeedbackVarying(SizeT index) const {
|
||||
return index < m_xfbVaryings.size() ? &m_xfbVaryings[index] : nullptr;
|
||||
}
|
||||
const Vector<XfbVarying>& GetTransformFeedbackVaryings() const { return m_xfbVaryings; }
|
||||
// Stride of one captured vertex in the given capture buffer slot.
|
||||
Uint32 GetTransformFeedbackStride(Uint32 bufferIndex) const {
|
||||
return bufferIndex < m_xfbStrides.size() ? m_xfbStrides[bufferIndex] : 0;
|
||||
}
|
||||
SizeT GetTransformFeedbackBufferCount() const { return m_xfbStrides.size(); }
|
||||
Int GetTransformFeedbackVaryingMaxLength() const { return m_xfbVaryingNameMaxLength; }
|
||||
// True when the capture stage is a triangle-strip geometry shader with a
|
||||
// statically-known emit sequence: the Vulkan capture order then needs the GL
|
||||
// odd-triangle vertex swap after EndTransformFeedback.
|
||||
Bool HasGsTriangleStripCaptureFixup() const { return m_gsStripCaptureFixup; }
|
||||
// Triangles per strip, in emission order, for ONE geometry invocation.
|
||||
const Vector<Uint32>& GetGsStripTriangles() const { return m_gsStripTriangles; }
|
||||
|
||||
Uint GetExternalIndex() const { return m_externalIndex; }
|
||||
// Globally-unique, never-reused id for this program object's lifetime. Unlike the GL
|
||||
// name (external index), which is freed to a LIFO list and immediately handed back by
|
||||
@@ -508,11 +459,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
private:
|
||||
void ResetLinkArtifacts();
|
||||
void DoReflection();
|
||||
// Resolves the requested transform feedback varyings against the linked
|
||||
// vertex stage; fails the link (GL semantics) on unknown or duplicate
|
||||
// names or exceeded capture limits.
|
||||
Bool ResolveTransformFeedbackVaryings();
|
||||
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
|
||||
void GenerateBinary();
|
||||
void WaitUntilGenerationCompleted() const;
|
||||
void AddDefaultFragmentShaderIfMissing();
|
||||
@@ -581,30 +527,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
Uint32 m_backendStateVersion = 0;
|
||||
|
||||
// Backend-owned content-hash memo (see GetBackendHashMemo): valid only while
|
||||
// m_backendStateVersion matches. Several slots, not one: a backend may resolve the same
|
||||
// program under more than one compile-flag set within a frame (surface rotation, and the
|
||||
// explicit-LOD sampling variant), and a single slot would then miss on every lookup and
|
||||
// re-hash the program's whole SPIR-V once per draw.
|
||||
static constexpr SizeT kBackendHashMemoSlotCount = 4;
|
||||
struct BackendHashMemoSlot {
|
||||
Uint64 hash = 0;
|
||||
Uint flags = 0;
|
||||
Bool valid = false;
|
||||
};
|
||||
mutable Array<BackendHashMemoSlot, kBackendHashMemoSlotCount> m_backendHashMemoSlots{};
|
||||
mutable SizeT m_backendHashMemoNextSlot = 0;
|
||||
// m_backendStateVersion and the compile flags match the recorded values.
|
||||
mutable Uint64 m_backendHashMemo = 0;
|
||||
mutable Uint32 m_backendHashMemoVersion = ~0u;
|
||||
mutable Uint m_backendHashMemoFlags = 0;
|
||||
Uint32 m_uboContentVersion = 0;
|
||||
Uint32 m_linkVersion = 0;
|
||||
|
||||
// Transform feedback: request (applies at next link) and linked snapshot.
|
||||
Vector<String> m_requestedXfbVaryings;
|
||||
GLenum m_requestedXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
Vector<XfbVarying> m_xfbVaryings;
|
||||
Vector<Uint32> m_xfbStrides;
|
||||
Vector<Uint32> m_gsStripTriangles;
|
||||
Bool m_gsStripCaptureFixup = false;
|
||||
GLenum m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
Int m_xfbVaryingNameMaxLength = 0;
|
||||
};
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
|
||||
@@ -29,25 +29,17 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (!CheckIndexAvail(program, m_programObjects)) return; // FIXME: add error reporting here
|
||||
auto& programObject = m_programObjects[program];
|
||||
if (programObject != nullptr) {
|
||||
// Snapshot the attachments: deleting the program is a detach point for shaders
|
||||
// that were flagged with glDeleteShader while still attached.
|
||||
const Vector<SharedPtr<ShaderObject>> attachedShaders = programObject->GetAttachedShaders();
|
||||
programObject->MarkAsDeleted();
|
||||
// A program in use is only FLAGGED: its name (and every program query) stays
|
||||
// valid until it stops being current, at which point UseProgram finishes the job.
|
||||
if (programObject == m_currentProgram) return;
|
||||
DestroyProgramSlot(program);
|
||||
}
|
||||
}
|
||||
|
||||
void ProgramState::DestroyProgramSlot(const Uint program) {
|
||||
auto& programObject = m_programObjects[program];
|
||||
// Snapshot the attachments: deleting the program is a detach point for shaders
|
||||
// that were flagged with glDeleteShader while still attached.
|
||||
const Vector<SharedPtr<ShaderObject>> attachedShaders = programObject->GetAttachedShaders();
|
||||
programObject.reset();
|
||||
m_programIndexGenerator.Delete(program);
|
||||
for (const auto& shader : attachedShaders) {
|
||||
const Uint shaderName = shader->GetExternalIndex();
|
||||
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
|
||||
ReleaseShaderNameIfOrphaned(shaderName);
|
||||
programObject.reset();
|
||||
m_programIndexGenerator.Delete(program);
|
||||
for (const auto& shader : attachedShaders) {
|
||||
const Uint shaderName = shader->GetExternalIndex();
|
||||
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
|
||||
ReleaseShaderNameIfOrphaned(shaderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,22 +49,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
void ProgramState::UseProgram(Uint program) {
|
||||
const SharedPtr<ProgramObject> previous = m_currentProgram;
|
||||
|
||||
if (program == 0) m_currentProgram.reset();
|
||||
|
||||
if (CheckIndexAvail(program, m_programObjects)) {
|
||||
m_currentProgram = m_programObjects[program];
|
||||
}
|
||||
|
||||
// A deletion flagged while the program was current takes effect the moment it
|
||||
// stops being current.
|
||||
if (previous != nullptr && previous != m_currentProgram && previous->GetDeleteStatus()) {
|
||||
const Uint previousName = previous->GetExternalIndex();
|
||||
if (CheckIndexAvail(previousName, m_programObjects) && m_programObjects[previousName] == previous) {
|
||||
DestroyProgramSlot(previousName);
|
||||
}
|
||||
}
|
||||
if (!CheckIndexAvail(program, m_programObjects)) return;
|
||||
m_currentProgram = m_programObjects[program];
|
||||
}
|
||||
|
||||
Uint ProgramState::CreateShader(ShaderStage stage) {
|
||||
|
||||
@@ -35,9 +35,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
private:
|
||||
Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const;
|
||||
// Frees the name slot and releases orphaned attached shaders; the immediate half
|
||||
// of glDeleteProgram (deferred while the program is current).
|
||||
void DestroyProgramSlot(Uint program);
|
||||
|
||||
template <typename T>
|
||||
static Bool CheckIndexAvail(const SizeT idx, const Vector<T>& vec) {
|
||||
|
||||
@@ -169,15 +169,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
}
|
||||
|
||||
const std::optional<String> reservedError =
|
||||
MG_Util::ShaderTranspiler::FindReservedIdentifierViolation(compileSource);
|
||||
if (reservedError) {
|
||||
m_compileStatus = false;
|
||||
m_shader.reset();
|
||||
m_infoLog = *reservedError;
|
||||
return;
|
||||
}
|
||||
|
||||
// Compile for OpenGL here, so that we can do validation and link
|
||||
// like a real OpenGL driver at linking stage
|
||||
// Will compile for other backends later.
|
||||
|
||||
@@ -162,7 +162,6 @@ namespace MobileGL {
|
||||
DepthComponent32F,
|
||||
Depth24Stencil8,
|
||||
Depth32FStencil8,
|
||||
StencilIndex8,
|
||||
|
||||
DepthComponent,
|
||||
DepthStencil,
|
||||
|
||||
@@ -15,11 +15,7 @@
|
||||
#include <MG_Util/Math/VectorTypes.h>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
// Texture objects are always SharedPtr-owned (TextureState creates every instance via
|
||||
// MakeShared, including the per-target default objects). enable_shared_from_this lets
|
||||
// backends that only receive a reference (e.g. syncing a name-deleted texture kept
|
||||
// alive by an FBO attachment) still register a weak liveness reference for GC.
|
||||
class ITextureObject : public std::enable_shared_from_this<ITextureObject> {
|
||||
class ITextureObject {
|
||||
public:
|
||||
using TargetEnum = TextureTarget;
|
||||
virtual ~ITextureObject() = default;
|
||||
|
||||
@@ -104,23 +104,6 @@ namespace MobileGL {
|
||||
m_backendHashMemoVersion = m_configVersion;
|
||||
}
|
||||
|
||||
// Backend-owned resolved-state memo: an opaque pointer into the
|
||||
// backend's vertex-input-state cache plus the cache's eviction
|
||||
// epoch, valid while the config version matches. Lets the
|
||||
// per-draw path skip the content hash AND the cache lookup; the
|
||||
// epoch guards against the cache evicting the pointee.
|
||||
Bool GetBackendStateMemo(const void*& outState, Uint64& outEpoch) const {
|
||||
if (m_backendStateMemoVersion != m_configVersion) return false;
|
||||
outState = m_backendStateMemo;
|
||||
outEpoch = m_backendStateMemoEpoch;
|
||||
return true;
|
||||
}
|
||||
void SetBackendStateMemo(const void* state, Uint64 epoch) const {
|
||||
m_backendStateMemo = state;
|
||||
m_backendStateMemoEpoch = epoch;
|
||||
m_backendStateMemoVersion = m_configVersion;
|
||||
}
|
||||
|
||||
private:
|
||||
void BumpAttributeFormatVersion(Uint index);
|
||||
void BumpAttributeBufferVersion(Uint index);
|
||||
@@ -154,9 +137,6 @@ namespace MobileGL {
|
||||
Uint32 m_configVersion = 0;
|
||||
mutable Uint64 m_backendHashMemo = 0;
|
||||
mutable Uint32 m_backendHashMemoVersion = ~0u;
|
||||
mutable const void* m_backendStateMemo = nullptr;
|
||||
mutable Uint64 m_backendStateMemoEpoch = 0;
|
||||
mutable Uint32 m_backendStateMemoVersion = ~0u;
|
||||
};
|
||||
} // namespace GLState
|
||||
} // namespace MG_State
|
||||
|
||||
@@ -34,11 +34,6 @@ namespace {
|
||||
GLint maxFragmentImageUniforms = 4;
|
||||
GLint maxComputeImageUniforms = 5;
|
||||
bool maxGeometryImageUniformsQueried = false;
|
||||
GLfloat minFragmentInterpolationOffset = -0.75f;
|
||||
GLfloat maxFragmentInterpolationOffset = 0.625f;
|
||||
GLint fragmentInterpolationOffsetBits = 6;
|
||||
bool fragmentInterpolationLimitsQueried = false;
|
||||
bool fragmentInterpolationQueryRaisesError = false;
|
||||
// Emulates ANGLE-on-Vulkan: the draw reads the indirect command's
|
||||
// baseInstance word and exposes it through gl_InstanceID.
|
||||
bool drawLeaksBaseInstanceWord = false;
|
||||
@@ -113,14 +108,6 @@ namespace {
|
||||
case GL_MAX_COMPUTE_IMAGE_UNIFORMS:
|
||||
*data = g_fake.maxComputeImageUniforms;
|
||||
break;
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
|
||||
g_fake.fragmentInterpolationLimitsQueried = true;
|
||||
if (g_fake.fragmentInterpolationQueryRaisesError) {
|
||||
g_fake.pendingError = GL_INVALID_ENUM;
|
||||
} else {
|
||||
*data = g_fake.fragmentInterpolationOffsetBits;
|
||||
}
|
||||
break;
|
||||
// FillInGLESCapabilities reads the context version before running the
|
||||
// baseInstance probe, which requires ES >= 3.1.
|
||||
case GL_MAJOR_VERSION:
|
||||
@@ -168,22 +155,6 @@ namespace {
|
||||
g_fake.maxTextureMaxAnisotropyQueried = true;
|
||||
data[0] = g_fake.maxTextureMaxAnisotropy;
|
||||
break;
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
g_fake.fragmentInterpolationLimitsQueried = true;
|
||||
if (g_fake.fragmentInterpolationQueryRaisesError) {
|
||||
g_fake.pendingError = GL_INVALID_ENUM;
|
||||
} else {
|
||||
data[0] = g_fake.minFragmentInterpolationOffset;
|
||||
}
|
||||
break;
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
g_fake.fragmentInterpolationLimitsQueried = true;
|
||||
if (g_fake.fragmentInterpolationQueryRaisesError) {
|
||||
g_fake.pendingError = GL_INVALID_ENUM;
|
||||
} else {
|
||||
data[0] = g_fake.maxFragmentInterpolationOffset;
|
||||
}
|
||||
break;
|
||||
// Two-component range queries.
|
||||
case GL_ALIASED_LINE_WIDTH_RANGE:
|
||||
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
||||
@@ -491,52 +462,6 @@ TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGe
|
||||
EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried);
|
||||
}
|
||||
|
||||
TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) {
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
MobileGL::MG_External::GLESCapabilities unsupportedCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(unsupportedCaps, funcs));
|
||||
EXPECT_FALSE(unsupportedCaps.SupportsShaderMultisampleInterpolation);
|
||||
EXPECT_FALSE(g_fake.fragmentInterpolationLimitsQueried);
|
||||
EXPECT_FLOAT_EQ(unsupportedCaps.MinFragmentInterpolationOffset, -0.5f);
|
||||
EXPECT_FLOAT_EQ(unsupportedCaps.MaxFragmentInterpolationOffset, 0.4375f);
|
||||
EXPECT_EQ(unsupportedCaps.FragmentInterpolationOffsetBits, 4);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_OES_shader_multisample_interpolation");
|
||||
// A stale error from an earlier capability probe must not make the optional
|
||||
// interpolation query look like it failed.
|
||||
g_fake.pendingError = GL_INVALID_OPERATION;
|
||||
MobileGL::MG_External::GLESCapabilities supportedCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(supportedCaps, funcs));
|
||||
EXPECT_TRUE(supportedCaps.SupportsShaderMultisampleInterpolation);
|
||||
EXPECT_TRUE(g_fake.fragmentInterpolationLimitsQueried);
|
||||
EXPECT_FLOAT_EQ(supportedCaps.MinFragmentInterpolationOffset, g_fake.minFragmentInterpolationOffset);
|
||||
EXPECT_FLOAT_EQ(supportedCaps.MaxFragmentInterpolationOffset, g_fake.maxFragmentInterpolationOffset);
|
||||
EXPECT_EQ(supportedCaps.FragmentInterpolationOffsetBits, g_fake.fragmentInterpolationOffsetBits);
|
||||
EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST(FragmentInterpolationCapabilities, QueryErrorIsDrainedAndFallsBackToCoreMinimums) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_OES_shader_multisample_interpolation");
|
||||
g_fake.fragmentInterpolationQueryRaisesError = true;
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
||||
|
||||
EXPECT_TRUE(g_fake.fragmentInterpolationLimitsQueried);
|
||||
EXPECT_FLOAT_EQ(caps.MinFragmentInterpolationOffset, -0.5f);
|
||||
EXPECT_FLOAT_EQ(caps.MaxFragmentInterpolationOffset, 0.4375f);
|
||||
EXPECT_EQ(caps.FragmentInterpolationOffsetBits, 4);
|
||||
EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The extension string is what apps gate on (LWJGL builds GLCapabilities from it), so advertising
|
||||
// it on a driver that cannot filter anisotropically would leave them silently on trilinear.
|
||||
TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) {
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <FastSTL/UnorderedMap.h>
|
||||
|
||||
namespace {
|
||||
class DynamicParameterBackend final : public MobileGL::MG_Backend::BackendObject {
|
||||
@@ -197,13 +196,13 @@ TEST(DirectGLESSanity, AdvertisesDepthTextureForGlmarkShadowScenes) {
|
||||
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_depth_texture), extensions.end());
|
||||
}
|
||||
|
||||
TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensionsAtExperimentalCTSVersion) {
|
||||
TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGLVersion) {
|
||||
MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
|
||||
const auto& rendererInfo = backend.GetRendererInfo().RendererGLInfo;
|
||||
const auto& extensions = rendererInfo.Extensions;
|
||||
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 4);
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 6);
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 3);
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 3);
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Patch, 0);
|
||||
|
||||
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_compute_shader),
|
||||
@@ -397,13 +396,13 @@ TEST(DirectVulkanSanity, RenderPassExtentUsesSwapchainSizeOnlyForDefaultFramebuf
|
||||
MobileGL::IntVec2(512, 512));
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, AdvertisesVoxyRequiredRenderingExtensionsAtExperimentalCTSVersion) {
|
||||
TEST(DirectVulkanSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGLVersion) {
|
||||
MobileGL::MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
|
||||
const auto& rendererInfo = backend.GetRendererInfo().RendererGLInfo;
|
||||
const auto& extensions = rendererInfo.Extensions;
|
||||
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 4);
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 6);
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 3);
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 3);
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Patch, 0);
|
||||
|
||||
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_compute_shader),
|
||||
@@ -516,50 +515,6 @@ TEST(DirectGLESSanity, PreservesHostPerStageImageUniformLimits) {
|
||||
EXPECT_EQ(params.MaxComputeImageUniforms, 5);
|
||||
}
|
||||
|
||||
TEST(FragmentInterpolationCapabilities, PlumbsGLESAndBothVulkanPropertyPaths) {
|
||||
using namespace MobileGL;
|
||||
|
||||
MG_External::GLESCapabilities glesCaps;
|
||||
glesCaps.MinFragmentInterpolationOffset = -0.75f;
|
||||
glesCaps.MaxFragmentInterpolationOffset = 0.625f;
|
||||
glesCaps.FragmentInterpolationOffsetBits = 6;
|
||||
MG_Backend::DirectGLES::BackendObject_DirectGLES glesBackend;
|
||||
glesBackend.ApplyGLESCapabilitiesForTesting(glesCaps);
|
||||
EXPECT_FLOAT_EQ(glesBackend.GetDynamicParameters().MinFragmentInterpolationOffset, -0.75f);
|
||||
EXPECT_FLOAT_EQ(glesBackend.GetDynamicParameters().MaxFragmentInterpolationOffset, 0.625f);
|
||||
EXPECT_EQ(glesBackend.GetDynamicParameters().FragmentInterpolationOffsetBits, 6);
|
||||
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
// A common Vulkan limit pair: max is one representable 4-bit step below 0.5.
|
||||
properties.limits.minInterpolationOffset = -0.5f;
|
||||
properties.limits.maxInterpolationOffset = 0.4375f;
|
||||
properties.limits.subPixelInterpolationOffsetBits = 4;
|
||||
MG_External::VulkanCapabilities vkCaps;
|
||||
MG_Util::BackendLoader::FillInVulkanCapabilities(vkCaps, properties);
|
||||
EXPECT_FLOAT_EQ(vkCaps.MinFragmentInterpolationOffset, -0.5f);
|
||||
EXPECT_FLOAT_EQ(vkCaps.MaxFragmentInterpolationOffset, 0.4375f);
|
||||
EXPECT_EQ(vkCaps.FragmentInterpolationOffsetBits, 4);
|
||||
|
||||
vkCaps.MinFragmentInterpolationOffset = -0.875f;
|
||||
vkCaps.MaxFragmentInterpolationOffset = 0.75f;
|
||||
vkCaps.FragmentInterpolationOffsetBits = 7;
|
||||
MG_Backend::DirectVulkan::BackendObject_DirectVulkan vkBackend;
|
||||
vkBackend.ApplyVulkanCapabilitiesForTesting(vkCaps);
|
||||
EXPECT_FLOAT_EQ(vkBackend.GetDynamicParameters().MinFragmentInterpolationOffset, -0.875f);
|
||||
EXPECT_FLOAT_EQ(vkBackend.GetDynamicParameters().MaxFragmentInterpolationOffset, 0.75f);
|
||||
EXPECT_EQ(vkBackend.GetDynamicParameters().FragmentInterpolationOffsetBits, 7);
|
||||
|
||||
// Invalid/zero host data cannot under-advertise the OpenGL 4 minimums.
|
||||
MG_External::VulkanCapabilities invalidCaps;
|
||||
invalidCaps.MinFragmentInterpolationOffset = 0.0f;
|
||||
invalidCaps.MaxFragmentInterpolationOffset = 0.0f;
|
||||
invalidCaps.FragmentInterpolationOffsetBits = 0;
|
||||
vkBackend.ApplyVulkanCapabilitiesForTesting(invalidCaps);
|
||||
EXPECT_LE(vkBackend.GetDynamicParameters().MinFragmentInterpolationOffset, -0.5f);
|
||||
EXPECT_FLOAT_EQ(vkBackend.GetDynamicParameters().MaxFragmentInterpolationOffset, 0.4375f);
|
||||
EXPECT_EQ(vkBackend.GetDynamicParameters().FragmentInterpolationOffsetBits, 4);
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) {
|
||||
using namespace MobileGL;
|
||||
|
||||
@@ -682,48 +637,6 @@ TEST(GetterSanity, ClampsMaxVertexAttribsToCurrentValueStorageCapacity) {
|
||||
MG_State::pGLContext.reset();
|
||||
}
|
||||
|
||||
TEST(GetterSanity, ReportsFragmentInterpolationLimitsForFloatAndIntegerQueries) {
|
||||
using namespace MobileGL;
|
||||
|
||||
auto previousContext = Move(MG_State::pGLContext);
|
||||
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
|
||||
MG_Backend::DynamicBackendParameters params;
|
||||
params.MinFragmentInterpolationOffset = -0.75f;
|
||||
params.MaxFragmentInterpolationOffset = 0.4375f;
|
||||
params.FragmentInterpolationOffsetBits = 6;
|
||||
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
|
||||
|
||||
GLfloat floatValue = 0.0f;
|
||||
MG_Impl::GLImpl::GetFloatv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &floatValue);
|
||||
EXPECT_FLOAT_EQ(floatValue, -0.75f);
|
||||
MG_Impl::GLImpl::GetFloatv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &floatValue);
|
||||
EXPECT_FLOAT_EQ(floatValue, 0.4375f);
|
||||
MG_Impl::GLImpl::GetFloatv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &floatValue);
|
||||
EXPECT_FLOAT_EQ(floatValue, 6.0f);
|
||||
|
||||
GLint intValue = 0;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &intValue);
|
||||
EXPECT_EQ(intValue, -1);
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &intValue);
|
||||
EXPECT_EQ(intValue, 0);
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &intValue);
|
||||
EXPECT_EQ(intValue, 6);
|
||||
|
||||
GLboolean boolValue = GL_FALSE;
|
||||
MG_Impl::GLImpl::GetBooleanv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &boolValue);
|
||||
EXPECT_EQ(boolValue, GL_TRUE);
|
||||
MG_Impl::GLImpl::GetBooleanv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &boolValue);
|
||||
EXPECT_EQ(boolValue, GL_TRUE);
|
||||
MG_Impl::GLImpl::GetBooleanv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &boolValue);
|
||||
EXPECT_EQ(boolValue, GL_TRUE);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
TEST(GetterSanity, PerStageImageUniformQueriesMatchShaderCompilerLimits) {
|
||||
using namespace MobileGL;
|
||||
|
||||
@@ -1823,61 +1736,3 @@ TEST(DirectGLESStateGuards, DefaultFramebufferBindGoesThroughShadow) {
|
||||
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, 7); // must reach the driver again
|
||||
EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 3u);
|
||||
}
|
||||
|
||||
// FastSTL::unordered_map::erase(iterator) regression coverage. The open-addressing
|
||||
// iterator constructor snaps forward from a tombstoned slot to the successor, so
|
||||
// erase must NOT advance the rebuilt iterator again: the old double-advance skipped
|
||||
// one live element per erase, and erasing the element in the highest occupied
|
||||
// bucket pushed the returned index past bucket_count where it never compared equal
|
||||
// to end() again - erase-while-iterating sweeps (pipeline/program cache eviction)
|
||||
// then ran off the bucket array and fed garbage handles to vkDestroyPipeline
|
||||
// (device crash on first mass eviction during world load).
|
||||
TEST(FastSTLSanity, EraseWhileIteratingVisitsEveryElementExactlyOnce) {
|
||||
FastSTL::unordered_map<MobileGL::Uint64, MobileGL::Uint64> map;
|
||||
constexpr MobileGL::Uint64 kCount = 1000;
|
||||
for (MobileGL::Uint64 key = 0; key < kCount; ++key) {
|
||||
map.emplace(key * 0x9e3779b97f4a7c15ull, key);
|
||||
}
|
||||
ASSERT_EQ(map.size(), kCount);
|
||||
|
||||
MobileGL::SizeT visited = 0;
|
||||
for (auto it = map.begin(); it != map.end();) {
|
||||
it = map.erase(it);
|
||||
++visited;
|
||||
ASSERT_LE(visited, kCount); // old code: runaway past end / skipped entries
|
||||
}
|
||||
EXPECT_EQ(visited, kCount);
|
||||
EXPECT_EQ(map.size(), 0u);
|
||||
}
|
||||
|
||||
TEST(FastSTLSanity, EraseReturnsTheSuccessorElement) {
|
||||
FastSTL::unordered_map<MobileGL::Uint32, MobileGL::Uint32> map;
|
||||
for (MobileGL::Uint32 key = 1; key <= 64; ++key) {
|
||||
map.emplace(key, key);
|
||||
}
|
||||
|
||||
// Erasing every other visited element must still visit all 64 exactly once:
|
||||
// the iterator returned by erase names the very next element, not one past it.
|
||||
MobileGL::SizeT visited = 0;
|
||||
MobileGL::SizeT erased = 0;
|
||||
for (auto it = map.begin(); it != map.end();) {
|
||||
++visited;
|
||||
if ((visited & 1) != 0) {
|
||||
it = map.erase(it);
|
||||
++erased;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
ASSERT_LE(visited, 64u);
|
||||
}
|
||||
EXPECT_EQ(visited, 64u);
|
||||
EXPECT_EQ(map.size(), 64u - erased);
|
||||
}
|
||||
|
||||
TEST(FastSTLSanity, ErasingTheOnlyElementReturnsEnd) {
|
||||
FastSTL::unordered_map<MobileGL::Uint32, MobileGL::Uint32> map;
|
||||
map.emplace(42u, 1u);
|
||||
auto next = map.erase(map.begin());
|
||||
EXPECT_EQ(next, map.end());
|
||||
EXPECT_TRUE(map.empty());
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "Loader.h"
|
||||
#include "MG_Util/Types.h"
|
||||
#include <Config.h>
|
||||
#include <cmath>
|
||||
#if defined(_WIN32)
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN 1
|
||||
@@ -824,12 +823,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
if (std::strcmp(extension, "GL_EXT_texture_norm16") == 0) {
|
||||
caps.SupportsNorm16Texture = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_EXT_render_snorm") == 0) {
|
||||
caps.SupportsRenderSnorm = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_EXT_sRGB_write_control") == 0) {
|
||||
caps.SupportsSrgbWriteControl = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_EXT_texture_filter_anisotropic") == 0) {
|
||||
caps.SupportsTextureFilterAnisotropy = true;
|
||||
}
|
||||
@@ -845,14 +838,8 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
if (std::strcmp(extension, "GL_NV_shader_noperspective_interpolation") == 0) {
|
||||
caps.SupportsNoperspectiveInterpolation = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_OES_shader_multisample_interpolation") == 0) {
|
||||
caps.SupportsShaderMultisampleInterpolation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
caps.SupportsShaderMultisampleInterpolation =
|
||||
caps.SupportsShaderMultisampleInterpolation || caps.GLESVersion.Major > 3 ||
|
||||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);
|
||||
|
||||
// Detect optional raster/color-mask entry points by whether they loaded. glColorMaski is GLES
|
||||
// 3.2 core (no extension string), so pointer presence is the reliable signal for all of these.
|
||||
@@ -913,9 +900,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
GLint maxColorAttachments = 8;
|
||||
GLint maxClipDistances = 8;
|
||||
GLint maxViewports = 16;
|
||||
GLfloat minFragmentInterpolationOffset = -0.5f;
|
||||
GLfloat maxFragmentInterpolationOffset = 0.4375f;
|
||||
GLint fragmentInterpolationOffsetBits = 4;
|
||||
glesFuncs.glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, aliasedLineWidthRange);
|
||||
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_RANGE, smoothLineWidthRange);
|
||||
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_GRANULARITY, &smoothLineWidthGranularity);
|
||||
@@ -935,15 +919,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
glesFuncs.glGetIntegerv(GL_MAX_INTEGER_SAMPLES, &maxIntegerSamples);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_SAMPLE_MASK_WORDS, &maxSampleMaskWords);
|
||||
// MobileGL's sample-mask state only stores a single 32-bit word (see
|
||||
// RenderState::SampleMaskValue) and SampleMaski_State() rejects any
|
||||
// maskNumber other than 0. Advertising the real driver's value here (e.g.
|
||||
// NVIDIA's GLES driver reports 2) makes glSampleMaski(1, ...) - which
|
||||
// dEQP's per-case gluStateReset always issues up to GL_MAX_SAMPLE_MASK_WORDS -
|
||||
// raise GL_INVALID_VALUE and aborts the whole glcts process after every
|
||||
// single test case. 1 is a spec-legal value (the minimum required), so cap
|
||||
// to what is actually implemented instead of forwarding the raw driver limit.
|
||||
maxSampleMaskWords = std::min(maxSampleMaskWords, 1);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTextureImageUnits);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, &maxVertexTextureImageUnits);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS, &maxComputeTextureImageUnits);
|
||||
@@ -975,29 +950,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims);
|
||||
glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits);
|
||||
if (caps.SupportsShaderMultisampleInterpolation && glesFuncs.glGetFloatv) {
|
||||
const auto drainErrors = [&glesFuncs]() {
|
||||
Bool hadError = false;
|
||||
if (glesFuncs.glGetError) {
|
||||
while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true;
|
||||
}
|
||||
return hadError;
|
||||
};
|
||||
|
||||
// Isolate these optional queries from errors raised by preceding capability
|
||||
// probes, then consume any query error so initialization never leaks it into
|
||||
// the application's first glGetError call.
|
||||
drainErrors();
|
||||
glesFuncs.glGetFloatv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &minFragmentInterpolationOffset);
|
||||
glesFuncs.glGetFloatv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &maxFragmentInterpolationOffset);
|
||||
glesFuncs.glGetIntegerv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &fragmentInterpolationOffsetBits);
|
||||
if (drainErrors()) {
|
||||
MGLOG_W("Fragment interpolation limit query failed; using OpenGL minimums");
|
||||
minFragmentInterpolationOffset = -0.5f;
|
||||
maxFragmentInterpolationOffset = 0.4375f;
|
||||
fragmentInterpolationOffsetBits = 4;
|
||||
}
|
||||
}
|
||||
// Only legal to query once the extension has been seen in the loop above, hence not batched
|
||||
// with the unconditional probes: on a driver without it this raises GL_INVALID_ENUM.
|
||||
if (caps.SupportsTextureFilterAnisotropy) {
|
||||
@@ -1055,19 +1007,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.ViewportBoundsRangeMin = viewportBoundsRange[0];
|
||||
caps.ViewportBoundsRangeMax = viewportBoundsRange[1];
|
||||
caps.ViewportSubpixelBits = viewportSubpixelBits;
|
||||
caps.MinFragmentInterpolationOffset =
|
||||
std::isfinite(minFragmentInterpolationOffset) && minFragmentInterpolationOffset <= -0.5f
|
||||
? minFragmentInterpolationOffset
|
||||
: -0.5f;
|
||||
caps.MaxFragmentInterpolationOffset = 0.4375f;
|
||||
caps.FragmentInterpolationOffsetBits = 4;
|
||||
if (fragmentInterpolationOffsetBits >= 4 && std::isfinite(maxFragmentInterpolationOffset)) {
|
||||
const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -fragmentInterpolationOffsetBits);
|
||||
if (maxFragmentInterpolationOffset >= requiredMaxOffset) {
|
||||
caps.MaxFragmentInterpolationOffset = maxFragmentInterpolationOffset;
|
||||
caps.FragmentInterpolationOffsetBits = fragmentInterpolationOffsetBits;
|
||||
}
|
||||
}
|
||||
MGLOG_I(" GL_ALIASED_LINE_WIDTH_RANGE: [%.3f, %.3f]", caps.AliasedLineWidthRangeMin,
|
||||
caps.AliasedLineWidthRangeMax);
|
||||
MGLOG_I(" GL_SMOOTH_LINE_WIDTH_RANGE: [%.3f, %.3f]", caps.SmoothLineWidthRangeMin,
|
||||
|
||||
@@ -1031,13 +1031,6 @@ namespace MobileGL {
|
||||
String GLESShadingLanguageVersionString;
|
||||
Bool SupportsPersistentMapping = false;
|
||||
Bool SupportsNorm16Texture = false;
|
||||
// GL_EXT_render_snorm is present, so the signed-normalized formats are colour-renderable
|
||||
// (and usable as multisample texture storage) rather than texture-only.
|
||||
Bool SupportsRenderSnorm = false;
|
||||
// GL_EXT_sRGB_write_control is present, so GL_FRAMEBUFFER_SRGB can be turned off.
|
||||
// GLES has no such switch in core: writes into an sRGB attachment are ALWAYS encoded,
|
||||
// while desktop GL leaves GL_FRAMEBUFFER_SRGB disabled by default and writes raw.
|
||||
Bool SupportsSrgbWriteControl = false;
|
||||
// GL_EXT_texture_filter_anisotropic is present, so sampler/texture
|
||||
// anisotropy may be forwarded without raising GL_INVALID_ENUM in GLES.
|
||||
Bool SupportsTextureFilterAnisotropy = false;
|
||||
@@ -1062,9 +1055,6 @@ namespace MobileGL {
|
||||
// SPIRV-Cross's `#extension ... : require` would fail to compile and MobileGL falls back
|
||||
// to stripping the NoPerspective decoration (smooth interpolation) via StripNoPerspectivePass.
|
||||
Bool SupportsNoperspectiveInterpolation = false;
|
||||
// GLES 3.2 core or GL_OES_shader_multisample_interpolation exposes
|
||||
// interpolateAtOffset and the three fragment-offset limit queries.
|
||||
Bool SupportsShaderMultisampleInterpolation = false;
|
||||
// GL_RENDERER contains "ANGLE".
|
||||
Bool IsAngleRenderer = false;
|
||||
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
||||
@@ -1130,9 +1120,6 @@ namespace MobileGL {
|
||||
Float ViewportBoundsRangeMin = 0.0f;
|
||||
Float ViewportBoundsRangeMax = 0.0f;
|
||||
Int ViewportSubpixelBits = 0;
|
||||
Float MinFragmentInterpolationOffset = -0.5f;
|
||||
Float MaxFragmentInterpolationOffset = 0.4375f;
|
||||
Int FragmentInterpolationOffsetBits = 4;
|
||||
};
|
||||
} // namespace MG_External
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "Loader.h"
|
||||
|
||||
#include <Config.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace MobileGL::MG_Util::BackendLoader {
|
||||
namespace {
|
||||
@@ -41,25 +40,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
return MaxSampleCountFromFlags(commonFlags);
|
||||
}
|
||||
|
||||
void FillFragmentInterpolationLimits(MG_External::VulkanCapabilities& caps,
|
||||
const VkPhysicalDeviceLimits& limits) {
|
||||
caps.MinFragmentInterpolationOffset =
|
||||
std::isfinite(limits.minInterpolationOffset) && limits.minInterpolationOffset <= -0.5f
|
||||
? limits.minInterpolationOffset
|
||||
: -0.5f;
|
||||
|
||||
caps.MaxFragmentInterpolationOffset = 0.4375f;
|
||||
caps.FragmentInterpolationOffsetBits = 4;
|
||||
const Int bits = static_cast<Int>(limits.subPixelInterpolationOffsetBits);
|
||||
if (bits >= 4 && std::isfinite(limits.maxInterpolationOffset)) {
|
||||
const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -bits);
|
||||
if (limits.maxInterpolationOffset >= requiredMaxOffset) {
|
||||
caps.MaxFragmentInterpolationOffset = limits.maxInterpolationOffset;
|
||||
caps.FragmentInterpolationOffsetBits = bits;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VulkanDynamicFunctions LoadVulkanDynamicFunctions(VkInstance instance) {
|
||||
VulkanDynamicFunctions loaded{};
|
||||
if (instance == VK_NULL_HANDLE) {
|
||||
@@ -191,7 +171,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.ViewportBoundsRangeMin = p.limits.viewportBoundsRange[0];
|
||||
caps.ViewportBoundsRangeMax = p.limits.viewportBoundsRange[1];
|
||||
caps.ViewportSubpixelBits = static_cast<Int>(p.limits.viewportSubPixelBits);
|
||||
FillFragmentInterpolationLimits(caps, p.limits);
|
||||
|
||||
VkPhysicalDeviceFeatures supportedFeatures{};
|
||||
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
|
||||
@@ -282,7 +261,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.ViewportBoundsRangeMin = properties.limits.viewportBoundsRange[0];
|
||||
caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1];
|
||||
caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits);
|
||||
FillFragmentInterpolationLimits(caps, properties.limits);
|
||||
caps.SupportsWideLines = false;
|
||||
// This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional
|
||||
// stage writes disabled rather than inferring them from descriptor limits alone.
|
||||
|
||||
@@ -68,9 +68,6 @@ namespace MobileGL {
|
||||
Float ViewportBoundsRangeMin = 0.0f;
|
||||
Float ViewportBoundsRangeMax = 0.0f;
|
||||
Int ViewportSubpixelBits = 0;
|
||||
Float MinFragmentInterpolationOffset = -0.5f;
|
||||
Float MaxFragmentInterpolationOffset = 0.4375f;
|
||||
Int FragmentInterpolationOffsetBits = 4;
|
||||
Bool SupportsWideLines = false;
|
||||
// Storage-image descriptors are limited per stage by
|
||||
// maxPerStageDescriptorStorageImages, but writes/atomics outside compute additionally
|
||||
|
||||
@@ -28,7 +28,6 @@ namespace MobileGL {
|
||||
|
||||
bool IsStencilFormatInternalFormat(TextureInternalFormat internalformat) {
|
||||
switch (internalformat) {
|
||||
case TextureInternalFormat::StencilIndex8:
|
||||
case TextureInternalFormat::Depth24Stencil8:
|
||||
case TextureInternalFormat::Depth32FStencil8:
|
||||
case TextureInternalFormat::DepthStencil:
|
||||
|
||||
@@ -251,8 +251,6 @@ namespace MobileGL {
|
||||
return TextureInternalFormat::Depth24Stencil8;
|
||||
case GL_DEPTH32F_STENCIL8:
|
||||
return TextureInternalFormat::Depth32FStencil8;
|
||||
case GL_STENCIL_INDEX8:
|
||||
return TextureInternalFormat::StencilIndex8;
|
||||
case GL_DEPTH_COMPONENT:
|
||||
return TextureInternalFormat::DepthComponent;
|
||||
case GL_DEPTH_STENCIL:
|
||||
|
||||
@@ -233,8 +233,6 @@ namespace MobileGL {
|
||||
return GL_DEPTH24_STENCIL8;
|
||||
case TextureInternalFormat::Depth32FStencil8:
|
||||
return GL_DEPTH32F_STENCIL8;
|
||||
case TextureInternalFormat::StencilIndex8:
|
||||
return GL_STENCIL_INDEX8;
|
||||
case TextureInternalFormat::DepthComponent32:
|
||||
return GL_DEPTH_COMPONENT32;
|
||||
case TextureInternalFormat::DepthStencil:
|
||||
|
||||
@@ -234,8 +234,6 @@ namespace MobileGL {
|
||||
return "Depth24Stencil8";
|
||||
case TextureInternalFormat::Depth32FStencil8:
|
||||
return "Depth32FStencil8";
|
||||
case TextureInternalFormat::StencilIndex8:
|
||||
return "StencilIndex8";
|
||||
case TextureInternalFormat::Red:
|
||||
return "Red";
|
||||
case TextureInternalFormat::RG:
|
||||
|
||||
@@ -25,19 +25,6 @@ namespace MobileGL {
|
||||
case GL_TRIANGLE_FAN:
|
||||
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN;
|
||||
case GL_LINE_LOOP:
|
||||
// DrawArrays/DrawElements rewrite line loops into closed indexed
|
||||
// strips; entry points without that rewrite (instanced/indirect)
|
||||
// degrade to an open strip, which only misses the closing segment.
|
||||
MGLOG_W("GL_LINE_LOOP without index rewrite; drawing as LINE_STRIP");
|
||||
return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
|
||||
case GL_LINES_ADJACENCY:
|
||||
return VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY;
|
||||
case GL_LINE_STRIP_ADJACENCY:
|
||||
return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY;
|
||||
case GL_TRIANGLES_ADJACENCY:
|
||||
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY;
|
||||
case GL_TRIANGLE_STRIP_ADJACENCY:
|
||||
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY;
|
||||
default:
|
||||
MGLOG_W("Unrecognized primitive topology");
|
||||
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
|
||||
@@ -133,11 +133,9 @@ namespace MobileGL {
|
||||
case TextureInternalFormat::RGBA8Snorm:
|
||||
return VK_FORMAT_R8G8B8A8_SNORM;
|
||||
case TextureInternalFormat::RGB10A2:
|
||||
// GL_UNSIGNED_INT_2_10_10_10_REV puts R in bits 0-9, which is Vulkan's
|
||||
// A2B10G10R10 layout - A2R10G10B10 silently swaps R and B on upload.
|
||||
return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
|
||||
return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
return VK_FORMAT_A2B10G10R10_UINT_PACK32;
|
||||
return VK_FORMAT_A2R10G10B10_UINT_PACK32;
|
||||
case TextureInternalFormat::RGBA16:
|
||||
return VK_FORMAT_R16G16B16A16_UNORM;
|
||||
case TextureInternalFormat::RGBA16Snorm:
|
||||
@@ -226,8 +224,6 @@ namespace MobileGL {
|
||||
return VK_FORMAT_D24_UNORM_S8_UINT;
|
||||
case TextureInternalFormat::Depth32FStencil8:
|
||||
return VK_FORMAT_D32_SFLOAT_S8_UINT;
|
||||
case TextureInternalFormat::StencilIndex8:
|
||||
return VK_FORMAT_S8_UINT;
|
||||
case TextureInternalFormat::DepthComponent32:
|
||||
return VK_FORMAT_D32_SFLOAT;
|
||||
case TextureInternalFormat::DepthStencil:
|
||||
|
||||
@@ -9,10 +9,14 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
// Severity-ordered: a build compiled at level X keeps X and everything MORE
|
||||
// severe. INFO builds must keep WARN/ERROR/FATAL — the old ordering
|
||||
// (WARN=1/ERROR=2 below INFO=3) compiled every warning and error out of
|
||||
// release builds and hid real backend failures.
|
||||
#define MOBILEGL_LOG_LEVEL_DEBUG 0
|
||||
#define MOBILEGL_LOG_LEVEL_WARN 1
|
||||
#define MOBILEGL_LOG_LEVEL_ERROR 2
|
||||
#define MOBILEGL_LOG_LEVEL_INFO 3
|
||||
#define MOBILEGL_LOG_LEVEL_INFO 1
|
||||
#define MOBILEGL_LOG_LEVEL_WARN 2
|
||||
#define MOBILEGL_LOG_LEVEL_ERROR 3
|
||||
#define MOBILEGL_LOG_LEVEL_FATAL 4
|
||||
|
||||
#define MOBILEGL_LOG_INTERNAL(levelTag, androidLogLevel, fmt, ...) \
|
||||
|
||||
@@ -18,7 +18,6 @@ namespace MobileGL {
|
||||
case TextureInternalFormat::Red: // UNorm8 shadow layout
|
||||
case TextureInternalFormat::R8Snorm:
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::StencilIndex8:
|
||||
case TextureInternalFormat::R8UI:
|
||||
return 1;
|
||||
|
||||
@@ -44,11 +43,8 @@ namespace MobileGL {
|
||||
case TextureInternalFormat::SRGB8:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
return 3;
|
||||
// Canonical depth shadow is a full 32-bit unorm word (see PixelStoreProcessor),
|
||||
// converted at upload to the image's own 24/32-bit layout.
|
||||
case TextureInternalFormat::DepthComponent24:
|
||||
return 4;
|
||||
return 3;
|
||||
|
||||
case TextureInternalFormat::RGBA2:
|
||||
case TextureInternalFormat::RGBA4:
|
||||
@@ -95,9 +91,6 @@ namespace MobileGL {
|
||||
case TextureInternalFormat::RG32F:
|
||||
case TextureInternalFormat::RG32I:
|
||||
case TextureInternalFormat::RG32UI:
|
||||
// Shadow bytes hold the GL_FLOAT_32_UNSIGNED_INT_24_8_REV wire format
|
||||
// (float depth + a word whose low 8 bits are stencil), 8 bytes/texel.
|
||||
case TextureInternalFormat::Depth32FStencil8:
|
||||
return 8;
|
||||
|
||||
case TextureInternalFormat::RGB32F:
|
||||
@@ -108,6 +101,7 @@ namespace MobileGL {
|
||||
case TextureInternalFormat::RGBA32F:
|
||||
case TextureInternalFormat::RGBA32I:
|
||||
case TextureInternalFormat::RGBA32UI:
|
||||
case TextureInternalFormat::Depth32FStencil8:
|
||||
return 16;
|
||||
|
||||
case TextureInternalFormat::R11FG11FB10F:
|
||||
@@ -259,10 +253,8 @@ namespace MobileGL {
|
||||
case TexturePixelDataType::UnsignedInt101111Rev:
|
||||
case TexturePixelDataType::UnsignedInt5999Rev:
|
||||
case TexturePixelDataType::UnsignedInt248:
|
||||
return 4;
|
||||
case TexturePixelDataType::Float32UnsignedInt248Rev:
|
||||
// A 32-bit float depth word followed by a 32-bit word holding stencil.
|
||||
return 8;
|
||||
return 4;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@@ -509,16 +501,9 @@ namespace MobileGL {
|
||||
s.Depth = 32;
|
||||
s.Stencil = 8;
|
||||
break;
|
||||
case TextureInternalFormat::StencilIndex8:
|
||||
s.Stencil = 8;
|
||||
break;
|
||||
case TextureInternalFormat::Unknown:
|
||||
// Queried for attachments that have no storage yet (e.g. framebuffer
|
||||
// parameter queries on the initial state); every size stays 0.
|
||||
break;
|
||||
default:
|
||||
MGLOG_W("Unimplemented internal format in GetComponentSizesForInternalFormat: %d",
|
||||
static_cast<Int>(internal));
|
||||
MOBILEGL_ASSERT(false, "Unimplemented internal format in GetComponentSizesForInternalFormat: %d",
|
||||
static_cast<Int>(internal));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||
#include "SpirvPasses/FoldConstOffsetFor1DFetchPass.h"
|
||||
#include "SpirvPasses/LowerClipDistanceForEsslPass.h"
|
||||
#include "SpirvPasses/DefeatConstStructArrayLutPass.h"
|
||||
#include "spirv-tools/libspirv.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
@@ -220,14 +223,11 @@ namespace MobileGL {
|
||||
auto result = ParseShaderSource(lang, shaderType, source, attrib.flags);
|
||||
if (result) return result;
|
||||
|
||||
// Legacy desktop sources are normalized to "#version 330 core" (with a marker on the
|
||||
// directive), which parses under stricter rules than the 460 they used to be forced
|
||||
// to: a shader declaring 110-150 while using e.g. layout(binding=...) without the
|
||||
// matching #extension line compiles on real drivers but is rejected here. Retry once
|
||||
// at 460 before reporting failure; a genuinely broken shader fails both attempts and
|
||||
// keeps its original diagnostics. Application-declared 330+ sources carry no marker
|
||||
// and keep their declared version's strict rules (the GL CTS negative-compile cases
|
||||
// depend on that).
|
||||
// Legacy desktop sources are normalized to "#version 330 core", which parses under
|
||||
// stricter rules than the 460 they used to be forced to: a shader declaring 330 while
|
||||
// using e.g. layout(binding=...) without the matching #extension line compiles on real
|
||||
// drivers but is rejected here. Retry once at 460 before reporting failure; a genuinely
|
||||
// broken shader fails both attempts and keeps its original diagnostics.
|
||||
String retrySource = source;
|
||||
if (!MG_Util::ShaderTranspiler::RetargetLegacyVersionDirectiveTo460(retrySource)) {
|
||||
return result;
|
||||
@@ -326,6 +326,55 @@ namespace MobileGL {
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::ClampAccessChainIndicesForEssl(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(CreateGraphicsRobustAccessPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::FoldConstOffsetFor1DFetchForEssl(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(FoldConstOffsetFor1DFetchPass::CreateFoldConstOffsetFor1DFetchPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LowerClipDistanceForEssl(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(LowerClipDistanceForEsslPass::CreateLowerClipDistanceForEsslPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DefeatConstStructArrayLutForEssl(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(
|
||||
DefeatConstStructArrayLutPass::CreateDefeatConstStructArrayLutPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
@@ -363,86 +412,6 @@ namespace MobileGL {
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LowerRectImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
constexpr SizeT kSpirvHeaderWordCount = 5;
|
||||
// OpTypeImage: [0] opcode/wordcount, [1] result id, [2] sampled type, [3] Dim, ...
|
||||
constexpr SizeT kTypeImageDimWordIndex = 3;
|
||||
constexpr SizeT kTypeImageMinWordCount = 9;
|
||||
outputBinary.clear();
|
||||
if (inputBinary.size() < kSpirvHeaderWordCount || inputBinary[0] != spv::MagicNumber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector<SizeT> rectDimWordOffsets;
|
||||
Vector<SizeT> rectCapabilityWordOffsets;
|
||||
Bool hasNormalizedCoordinateLookup = false;
|
||||
for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) {
|
||||
const Uint32 instructionWord = inputBinary[offset];
|
||||
const SizeT wordCount = instructionWord >> 16u;
|
||||
const auto opcode = static_cast<spv::Op>(instructionWord & 0xffffu);
|
||||
if (wordCount == 0 || offset + wordCount > inputBinary.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opcode == spv::Op::OpTypeImage && wordCount >= kTypeImageMinWordCount) {
|
||||
if (static_cast<spv::Dim>(inputBinary[offset + kTypeImageDimWordIndex]) == spv::Dim::Rect) {
|
||||
rectDimWordOffsets.push_back(offset + kTypeImageDimWordIndex);
|
||||
}
|
||||
} else if (opcode == spv::Op::OpCapability && wordCount >= 2) {
|
||||
const auto capability = static_cast<spv::Capability>(inputBinary[offset + 1]);
|
||||
if (capability == spv::Capability::SampledRect ||
|
||||
capability == spv::Capability::ImageRect) {
|
||||
rectCapabilityWordOffsets.push_back(offset + 1);
|
||||
}
|
||||
} else {
|
||||
switch (opcode) {
|
||||
// Everything that takes normalized coordinates. Tracing each one back to
|
||||
// its image type would let a module mix a normalized 2D lookup with a
|
||||
// rectangle fetch, but the extra reach is not worth the risk of getting
|
||||
// the trace wrong: decline the whole module instead.
|
||||
case spv::Op::OpImageSampleImplicitLod:
|
||||
case spv::Op::OpImageSampleExplicitLod:
|
||||
case spv::Op::OpImageSampleDrefImplicitLod:
|
||||
case spv::Op::OpImageSampleDrefExplicitLod:
|
||||
case spv::Op::OpImageSampleProjImplicitLod:
|
||||
case spv::Op::OpImageSampleProjExplicitLod:
|
||||
case spv::Op::OpImageSampleProjDrefImplicitLod:
|
||||
case spv::Op::OpImageSampleProjDrefExplicitLod:
|
||||
case spv::Op::OpImageGather:
|
||||
case spv::Op::OpImageDrefGather:
|
||||
case spv::Op::OpImageSparseSampleImplicitLod:
|
||||
case spv::Op::OpImageSparseSampleExplicitLod:
|
||||
case spv::Op::OpImageSparseSampleDrefImplicitLod:
|
||||
case spv::Op::OpImageSparseSampleDrefExplicitLod:
|
||||
case spv::Op::OpImageSparseGather:
|
||||
case spv::Op::OpImageSparseDrefGather:
|
||||
hasNormalizedCoordinateLookup = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
offset += wordCount;
|
||||
}
|
||||
|
||||
if (rectDimWordOffsets.empty() || hasNormalizedCoordinateLookup) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outputBinary.assign(inputBinary.begin(), inputBinary.end());
|
||||
for (const SizeT dimWordOffset : rectDimWordOffsets) {
|
||||
outputBinary[dimWordOffset] = static_cast<Uint32>(spv::Dim::Dim2D);
|
||||
}
|
||||
// The rectangle capabilities describe types that no longer exist. Shader is always
|
||||
// declared by a graphics module, so restating it keeps the word count intact
|
||||
// without leaving a capability SPIRV-Cross would key off.
|
||||
for (const SizeT capabilityWordOffset : rectCapabilityWordOffsets) {
|
||||
outputBinary[capabilityWordOffset] = static_cast<Uint32>(spv::Capability::Shader);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
|
||||
@@ -27,6 +27,36 @@ namespace MobileGL {
|
||||
// Only for backends without native draw-parameter support (DirectGLES).
|
||||
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Clamps every access-chain index to its declared bounds (spirv-tools
|
||||
// GraphicsRobustAccessPass). GL 3.3 only promises undefined *values* for
|
||||
// out-of-bounds indexing, but Adreno's ESSL compiler constant-folds a provably
|
||||
// out-of-bounds local-array index into poison that corrupts the whole shader's
|
||||
// output; clamping restores the "some value from the array" contract. Only for
|
||||
// the DirectGLES transpile path.
|
||||
static bool ClampAccessChainIndicesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Folds the ConstOffset image operand of Dim1D OpImageFetch into the integer
|
||||
// coordinate (texelFetchOffset(t,P,l,o) == texelFetch(t,P+o,l)). SPIRV-Cross
|
||||
// emulates 1D samplers as 2D for ES: it widens the coordinate to ivec2 but keeps
|
||||
// the scalar offset, and ESSL has no texelFetchOffset(sampler2D, ivec2, int,
|
||||
// scalar) overload, so Adreno rejects the shader. Only for the DirectGLES
|
||||
// transpile path.
|
||||
static bool FoldConstOffsetFor1DFetchForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Shadows gl_ClipDistance in Private mg_ClipDistance/mg_ClipDistanceIn arrays so
|
||||
// the decompiled ESSL only writes the builtin with literal constant indices
|
||||
// (flush before EmitVertex/return) and only reads gl_in clip distances with
|
||||
// dynamic loop indices (copy loop): the other shapes miscompile or crash
|
||||
// Adreno's ESSL compiler. Vertex/geometry stages; DirectGLES transpile path on
|
||||
// Qualcomm only (quirk-gated). See LowerClipDistanceForEsslPass.
|
||||
static bool LowerClipDistanceForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Splits a Function-storage array-of-structs variable's single constant-composite
|
||||
// store into per-element stores so SPIRV-Cross does not hoist it into a global
|
||||
// const struct[] LUT, which Adreno cannot dynamically index. DirectGLES
|
||||
// transpile path on Qualcomm only (quirk-gated). See DefeatConstStructArrayLutPass.
|
||||
static bool DefeatConstStructArrayLutForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Drops RelaxedPrecision member decorations from uniform-block structs so
|
||||
// SPIRV-Cross prints the same (highp) member precision in every stage; ES
|
||||
// drivers reject cross-stage uniform blocks whose member precisions differ.
|
||||
@@ -43,16 +73,6 @@ namespace MobileGL {
|
||||
// devices lacking GL_NV_shader_noperspective_interpolation. See EmulateNoPerspectivePass.
|
||||
static bool EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Rewrites rectangle images (Dim::Rect) to plain 2D so SPIRV-Cross can emit ESSL
|
||||
// for them at all - it refuses outright ("Rectangle textures are not supported on
|
||||
// OpenGL ES"), which left the whole program unlinkable. Only valid while every use
|
||||
// of the image takes integer texel coordinates (texelFetch / textureSize), where a
|
||||
// rectangle target and a 2D target are indistinguishable; a normalized-coordinate
|
||||
// lookup would also need its coordinates divided by the texture size, so the pass
|
||||
// declines those modules instead of emitting something subtly wrong. Returns false
|
||||
// when it changed nothing or cannot safely convert. DirectGLES only.
|
||||
static bool LowerRectImagesForEssl(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,
|
||||
|
||||
@@ -688,10 +688,6 @@ namespace {
|
||||
return info;
|
||||
}
|
||||
|
||||
// Stamped onto the normalized directive when a legacy (or absent) desktop
|
||||
// version was rewritten to 330; consumed by RetargetLegacyVersionDirectiveTo460.
|
||||
constexpr const char* kNormalizedLegacyMarker = "/*mobilegl-normalized-legacy*/";
|
||||
|
||||
MobileGL::String GetNormalizedVersionDirective(const ShaderLanguageInfo& info) {
|
||||
if (info.profile == MobileGL::ShaderProfile::ES) {
|
||||
// Preserve the pre-existing behavior for standard lowercase "es" directives. MobileGL's Vulkan
|
||||
@@ -706,23 +702,9 @@ namespace {
|
||||
return "#version 460 compatibility\n";
|
||||
}
|
||||
|
||||
// An explicitly declared modern core version keeps its number: the GL CTS
|
||||
// negative-compile cases (reserved names, layout-qualifier forms, missing
|
||||
// overloads) rely on the declared version's rules, and raising it would
|
||||
// silently legalize them. gpu_shader5 opt-ins keep the 460 escalation -
|
||||
// Vulkan glslang's ARB_gpu_shader5 support is not complete enough alone.
|
||||
if (info.hasValidVersionDirective && info.version >= 330 && !info.enablesGpuShader5) {
|
||||
return "#version " + std::to_string(info.version) + " core\n";
|
||||
}
|
||||
|
||||
const bool useLegacyDesktopVersion =
|
||||
info.version < 400 && !info.enablesGpuShader5;
|
||||
// The trailing marker records that this 330 came from a legacy declaration
|
||||
// (or none at all), so the compile-failure retry may re-raise it to 460.
|
||||
// An application's own "#version 330" never carries it and keeps strict
|
||||
// 3.30 semantics.
|
||||
return useLegacyDesktopVersion ? MobileGL::String("#version 330 core ") + kNormalizedLegacyMarker + "\n"
|
||||
: "#version 460 core\n";
|
||||
return useLegacyDesktopVersion ? "#version 330 core\n" : "#version 460 core\n";
|
||||
}
|
||||
|
||||
void NormalizeVersionDirective(MobileGL::String& source, const ShaderLanguageInfo& info) {
|
||||
@@ -1324,123 +1306,12 @@ namespace MobileGL {
|
||||
// Only the set NormalizeVersionDirective downgraded: desktop core below 400. ES and
|
||||
// compatibility shaders keep whatever they declared.
|
||||
if (info.profile != ShaderProfile::Core || info.version >= 400) return false;
|
||||
// Only rescue MobileGL's own legacy normalization (marked on the directive line).
|
||||
// An application-declared "#version 330" keeps strict 3.30 semantics: raising it
|
||||
// would re-legalize the CTS negative-compile cases (reserved names, arrays of
|
||||
// arrays, missing overloads).
|
||||
SizeT lineEnd = source.find('\n', info.versionDirectiveStart);
|
||||
if (lineEnd == MobileGL::String::npos) {
|
||||
lineEnd = source.size();
|
||||
}
|
||||
const SizeT markerPos = source.find(kNormalizedLegacyMarker, info.versionDirectiveStart);
|
||||
if (markerPos == MobileGL::String::npos || markerPos > lineEnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
source.replace(info.versionDirectiveStart, info.versionDirectiveEnd - info.versionDirectiveStart,
|
||||
"#version 460 core\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<String> FindReservedIdentifierViolation(const String& source) {
|
||||
// Reserved anywhere; glslang accepts them as plain identifiers.
|
||||
static constexpr const char* kAlwaysReserved[] = {
|
||||
"image1DShadow",
|
||||
"image2DShadow",
|
||||
"image1DArrayShadow",
|
||||
"image2DArrayShadow",
|
||||
};
|
||||
// Keywords legal only inside a layout(...) qualifier list.
|
||||
static constexpr const char* kLayoutOnlyKeywords[] = {
|
||||
"packed",
|
||||
"row_major",
|
||||
};
|
||||
|
||||
const auto isIdentChar = [](char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
|
||||
};
|
||||
|
||||
const SizeT length = source.size();
|
||||
SizeT i = 0;
|
||||
Int layoutParenDepth = 0; // >0 while inside layout(...)
|
||||
Bool pendingLayoutParen = false; // saw "layout", awaiting its '('
|
||||
while (i < length) {
|
||||
const char c = source[i];
|
||||
// Comments.
|
||||
if (c == '/' && i + 1 < length && source[i + 1] == '/') {
|
||||
while (i < length && source[i] != '\n') ++i;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && i + 1 < length && source[i + 1] == '*') {
|
||||
i += 2;
|
||||
while (i + 1 < length && !(source[i] == '*' && source[i + 1] == '/')) ++i;
|
||||
i = (i + 1 < length) ? i + 2 : length;
|
||||
continue;
|
||||
}
|
||||
// Preprocessor lines stay out of scope (macro names may shadow anything).
|
||||
if (c == '#' && (i == 0 || source[i - 1] == '\n' ||
|
||||
source.find_last_not_of(" \t", i - 1) == MobileGL::String::npos ||
|
||||
source[source.find_last_not_of(" \t", i - 1)] == '\n')) {
|
||||
while (i < length && source[i] != '\n') {
|
||||
if (source[i] == '\\' && i + 1 < length && source[i + 1] == '\n') ++i;
|
||||
++i;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == '(') {
|
||||
if (pendingLayoutParen) {
|
||||
layoutParenDepth = 1;
|
||||
pendingLayoutParen = false;
|
||||
} else if (layoutParenDepth > 0) {
|
||||
++layoutParenDepth;
|
||||
}
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
if (c == ')') {
|
||||
if (layoutParenDepth > 0) --layoutParenDepth;
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
if (isIdentChar(c) && !(c >= '0' && c <= '9')) {
|
||||
const SizeT start = i;
|
||||
while (i < length && isIdentChar(source[i])) ++i;
|
||||
const StringView word(source.data() + start, i - start);
|
||||
if (word == "layout") {
|
||||
pendingLayoutParen = true;
|
||||
continue;
|
||||
}
|
||||
pendingLayoutParen = false;
|
||||
for (const char* reserved : kAlwaysReserved) {
|
||||
if (word == reserved) {
|
||||
return String("ERROR: reserved identifier '") + reserved + "' may not be used.";
|
||||
}
|
||||
}
|
||||
if (layoutParenDepth == 0) {
|
||||
for (const char* keyword : kLayoutOnlyKeywords) {
|
||||
if (word == keyword) {
|
||||
return String("ERROR: '") + keyword +
|
||||
"' is a keyword and may not be used as an identifier.";
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (isIdentChar(c)) { // digit-led token: skip the whole number/identifier tail
|
||||
while (i < length && isIdentChar(source[i])) ++i;
|
||||
pendingLayoutParen = false;
|
||||
continue;
|
||||
}
|
||||
pendingLayoutParen = false;
|
||||
++i;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -40,11 +40,6 @@ namespace MobileGL {
|
||||
// uses 420-era syntax without the matching #extension line, which real drivers tend to
|
||||
// accept - can be retried instead of failing to compile.
|
||||
Bool RetargetLegacyVersionDirectiveTo460(String& source);
|
||||
|
||||
// GLSL reserves a few names glslang happily accepts as identifiers ("packed",
|
||||
// "row_major" outside a layout(...) list, the image*Shadow family). Returns the
|
||||
// compile-error text for the first violation, or nullopt for a clean source.
|
||||
std::optional<String> FindReservedIdentifierViolation(const String& source);
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.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 "DefeatConstStructArrayLutPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/constants.h"
|
||||
#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/opt/type_manager.h"
|
||||
#include "source/opt/types.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::BasicBlock;
|
||||
using spvtools::opt::Function;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
uint32_t PointerTypeTo(IRContext* ctx, uint32_t pointeeId, spv::StorageClass sc) {
|
||||
analysis::Type* pointee = ctx->get_type_mgr()->GetType(pointeeId);
|
||||
analysis::Pointer ptr(pointee, sc);
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&ptr);
|
||||
}
|
||||
|
||||
uint32_t SignedIntConstant(IRContext* ctx, uint32_t value) {
|
||||
analysis::Integer i(32, true);
|
||||
analysis::Type* reg = ctx->get_type_mgr()->GetRegisteredType(&i);
|
||||
const analysis::Constant* c = ctx->get_constant_mgr()->GetConstant(reg, {value});
|
||||
return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id();
|
||||
}
|
||||
|
||||
// True when |var| (a Function-storage OpVariable) points to an array of structs.
|
||||
// Reports the struct type id on success.
|
||||
bool IsArrayOfStructsVariable(IRContext* ctx, Instruction* var, uint32_t& structTypeId) {
|
||||
auto* defUse = ctx->get_def_use_mgr();
|
||||
Instruction* ptrType = defUse->GetDef(var->type_id());
|
||||
if (ptrType == nullptr || ptrType->opcode() != spv::Op::OpTypePointer) return false;
|
||||
Instruction* pointee = defUse->GetDef(ptrType->GetSingleWordInOperand(1));
|
||||
if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeArray) return false;
|
||||
Instruction* element = defUse->GetDef(pointee->GetSingleWordInOperand(0));
|
||||
if (element == nullptr || element->opcode() != spv::Op::OpTypeStruct) return false;
|
||||
structTypeId = element->result_id();
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status DefeatConstStructArrayLutPass::Process() {
|
||||
auto* ctx = context();
|
||||
auto* defUse = ctx->get_def_use_mgr();
|
||||
bool modified = false;
|
||||
|
||||
for (Function& function : *get_module()) {
|
||||
if (function.begin() == function.end()) continue;
|
||||
BasicBlock* entryBlock = &*function.begin();
|
||||
|
||||
// Candidate variables: Function-storage arrays of structs declared in this
|
||||
// function's entry block (where OpVariables must live).
|
||||
struct Candidate {
|
||||
Instruction* var;
|
||||
uint32_t structTypeId;
|
||||
};
|
||||
std::vector<Candidate> candidates;
|
||||
for (Instruction& inst : *entryBlock) {
|
||||
if (inst.opcode() != spv::Op::OpVariable) break;
|
||||
// Variables with initializers keep SPIRV-Cross's initializer path; the
|
||||
// glslang pattern under attack is initializer-free with one OpStore.
|
||||
if (inst.NumInOperands() > 1) continue;
|
||||
uint32_t structTypeId = 0;
|
||||
if (IsArrayOfStructsVariable(ctx, &inst, structTypeId)) {
|
||||
candidates.push_back({&inst, structTypeId});
|
||||
}
|
||||
}
|
||||
|
||||
for (const Candidate& candidate : candidates) {
|
||||
Instruction* var = candidate.var;
|
||||
|
||||
// The variable qualifies only when its single write is one direct
|
||||
// OpStore of an OpConstantComposite; any other write shape already
|
||||
// defeats SPIRV-Cross's LUT promotion, so it is left untouched.
|
||||
Instruction* singleStore = nullptr;
|
||||
bool disqualified = false;
|
||||
defUse->ForEachUser(var, [&](Instruction* user) {
|
||||
if (user->opcode() == spv::Op::OpStore &&
|
||||
user->GetSingleWordInOperand(0) == var->result_id()) {
|
||||
if (singleStore != nullptr) {
|
||||
disqualified = true;
|
||||
} else {
|
||||
singleStore = user;
|
||||
}
|
||||
} else if (user->opcode() == spv::Op::OpCopyMemory) {
|
||||
disqualified = true;
|
||||
} else if (user->opcode() == spv::Op::OpAccessChain ||
|
||||
user->opcode() == spv::Op::OpInBoundsAccessChain) {
|
||||
defUse->ForEachUser(user, [&](Instruction* chainUser) {
|
||||
if (chainUser->opcode() == spv::Op::OpStore ||
|
||||
chainUser->opcode() == spv::Op::OpCopyMemory) {
|
||||
disqualified = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (disqualified || singleStore == nullptr) continue;
|
||||
|
||||
Instruction* composite = defUse->GetDef(singleStore->GetSingleWordInOperand(1));
|
||||
if (composite == nullptr ||
|
||||
composite->opcode() != spv::Op::OpConstantComposite) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The store must sit in the entry block: that is the only placement
|
||||
// SPIRV-Cross treats as a LUT initializer.
|
||||
bool storeInEntryBlock = false;
|
||||
for (Instruction& inst : *entryBlock) {
|
||||
if (&inst == singleStore) {
|
||||
storeInEntryBlock = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!storeInEntryBlock) continue;
|
||||
|
||||
// Split the composite store into one constant-index store per element.
|
||||
const uint32_t ptrFnStruct =
|
||||
PointerTypeTo(ctx, candidate.structTypeId, spv::StorageClass::Function);
|
||||
for (uint32_t element = 0; element < composite->NumInOperands(); ++element) {
|
||||
const uint32_t elementConstId = composite->GetSingleWordInOperand(element);
|
||||
const uint32_t chainId = ctx->TakeNextId();
|
||||
Instruction* chain =
|
||||
singleStore->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpAccessChain, ptrFnStruct, chainId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {var->result_id()}},
|
||||
{SPV_OPERAND_TYPE_ID, {SignedIntConstant(ctx, element)}}}));
|
||||
ctx->AnalyzeDefUse(chain);
|
||||
Instruction* store =
|
||||
singleStore->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpStore, 0, 0,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {chainId}},
|
||||
{SPV_OPERAND_TYPE_ID, {elementConstId}}}));
|
||||
ctx->AnalyzeDefUse(store);
|
||||
}
|
||||
ctx->KillInst(singleStore);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!modified) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
ctx->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken
|
||||
DefeatConstStructArrayLutPass::CreateDefeatConstStructArrayLutPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<DefeatConstStructArrayLutPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,36 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.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 {
|
||||
// SPIRV-Cross hoists a Function-storage array variable whose only write is a single
|
||||
// constant-composite store into a global `const struct[]` LUT (variable_is_lut).
|
||||
// Adreno's ESSL compiler cannot dynamically index such a global const struct array
|
||||
// ("Cannot offset into the structure" - device-verified on Adreno 750). Splitting
|
||||
// the one composite store into per-element constant-index stores makes
|
||||
// variable_is_lut fail, so SPIRV-Cross keeps the array as an ordinary local that
|
||||
// Adreno indexes fine. Scalar/vector const arrays are unaffected on Adreno and are
|
||||
// left alone - only arrays OF STRUCTS are rewritten. Only meant for the DirectGLES
|
||||
// transpile path on Qualcomm devices.
|
||||
class DefeatConstStructArrayLutPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "defeat-const-struct-array-lut"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateDefeatConstStructArrayLutPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,131 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.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 "FoldConstOffsetFor1DFetchPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_builder.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::InstructionBuilder;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
// Number of ImageOperands ids that precede the ConstOffset id: one per
|
||||
// lower-order bit set in the mask, except Grad which carries two ids.
|
||||
uint32_t CountIdsBeforeConstOffset(uint32_t mask) {
|
||||
uint32_t count = 0;
|
||||
if (mask & static_cast<uint32_t>(spv::ImageOperandsMask::Bias)) count += 1;
|
||||
if (mask & static_cast<uint32_t>(spv::ImageOperandsMask::Lod)) count += 1;
|
||||
if (mask & static_cast<uint32_t>(spv::ImageOperandsMask::Grad)) count += 2;
|
||||
return count;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status FoldConstOffsetFor1DFetchPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
Bool modified = false;
|
||||
|
||||
constexpr uint32_t kConstOffsetBit =
|
||||
static_cast<uint32_t>(spv::ImageOperandsMask::ConstOffset);
|
||||
|
||||
for (auto& function : *get_module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& inst : block) {
|
||||
if (inst.opcode() != spv::Op::OpImageFetch) continue;
|
||||
// In-operands: image, coordinate, [ImageOperands mask, ids...].
|
||||
if (inst.NumInOperands() < 3) continue;
|
||||
const uint32_t operandsMask = inst.GetSingleWordInOperand(2);
|
||||
if ((operandsMask & kConstOffsetBit) == 0) continue;
|
||||
|
||||
Instruction* imageInst = defUseMgr->GetDef(inst.GetSingleWordInOperand(0));
|
||||
if (imageInst == nullptr) continue;
|
||||
Instruction* imageType = defUseMgr->GetDef(imageInst->type_id());
|
||||
if (imageType == nullptr || imageType->opcode() != spv::Op::OpTypeImage ||
|
||||
static_cast<spv::Dim>(imageType->GetSingleWordInOperand(1)) != spv::Dim::Dim1D) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t offsetOperandIndex = 3 + CountIdsBeforeConstOffset(operandsMask);
|
||||
const uint32_t offsetId = inst.GetSingleWordInOperand(offsetOperandIndex);
|
||||
|
||||
const uint32_t coordId = inst.GetSingleWordInOperand(1);
|
||||
Instruction* coordType = defUseMgr->GetDef(defUseMgr->GetDef(coordId)->type_id());
|
||||
|
||||
InstructionBuilder builder(
|
||||
irContext, &inst,
|
||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
|
||||
uint32_t newCoordId = 0;
|
||||
if (coordType->opcode() == spv::Op::OpTypeVector) {
|
||||
// Arrayed 1D fetch: component 0 is the texel coordinate,
|
||||
// component 1 the layer - only component 0 takes the offset.
|
||||
const uint32_t componentTypeId = coordType->GetSingleWordInOperand(0);
|
||||
Instruction* extracted = builder.AddCompositeExtract(componentTypeId, coordId, {0});
|
||||
Instruction* sum =
|
||||
builder.AddIAdd(componentTypeId, extracted->result_id(), offsetId);
|
||||
Instruction* inserted = builder.AddInstruction(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpCompositeInsert, coordType->result_id(),
|
||||
irContext->TakeNextId(),
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {sum->result_id()}},
|
||||
{SPV_OPERAND_TYPE_ID, {coordId}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {0}}}));
|
||||
newCoordId = inserted->result_id();
|
||||
} else {
|
||||
Instruction* sum = builder.AddIAdd(coordType->result_id(), coordId, offsetId);
|
||||
newCoordId = sum->result_id();
|
||||
}
|
||||
|
||||
const uint32_t newMask = operandsMask & ~kConstOffsetBit;
|
||||
// 3 fixed operands + the offset id: anything beyond that is another
|
||||
// image-operand id that must keep the mask word alive.
|
||||
const Bool otherOperandIdsRemain = inst.NumInOperands() > 4;
|
||||
|
||||
irContext->ForgetUses(&inst);
|
||||
std::vector<Operand> newOperands;
|
||||
newOperands.push_back(inst.GetInOperand(0));
|
||||
newOperands.push_back({SPV_OPERAND_TYPE_ID, {newCoordId}});
|
||||
if (newMask != 0 || otherOperandIdsRemain) {
|
||||
Operand maskOperand = inst.GetInOperand(2);
|
||||
maskOperand.words[0] = newMask;
|
||||
newOperands.push_back(maskOperand);
|
||||
for (uint32_t i = 3; i < inst.NumInOperands(); ++i) {
|
||||
if (i == offsetOperandIndex) continue;
|
||||
newOperands.push_back(inst.GetInOperand(i));
|
||||
}
|
||||
}
|
||||
inst.SetInOperands(std::move(newOperands));
|
||||
irContext->AnalyzeUses(&inst);
|
||||
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modified ? Status::SuccessWithChange : Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken FoldConstOffsetFor1DFetchPass::CreateFoldConstOffsetFor1DFetchPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<FoldConstOffsetFor1DFetchPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,36 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.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 {
|
||||
// SPIRV-Cross emulates 1D textures as 2D for ES targets: it widens the texelFetch
|
||||
// coordinate to ivec2 but keeps the ConstOffset image operand scalar, and ESSL has
|
||||
// no texelFetchOffset(sampler2D, ivec2, int, scalar-offset) overload, so drivers
|
||||
// (Adreno) reject the transpiled shader. This pass folds the constant offset into
|
||||
// the integer coordinate before the fetch - texelFetchOffset(t, P, l, o) ==
|
||||
// texelFetch(t, P + o, l) per the GLSL spec - and drops the ConstOffset operand,
|
||||
// so SPIRV-Cross emits a plain texelFetch. For arrayed 1D fetches only coordinate
|
||||
// component 0 is offset (component 1 is the layer). Only meant for the DirectGLES
|
||||
// transpile path.
|
||||
class FoldConstOffsetFor1DFetchPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "fold-const-offset-for-1d-fetch"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateFoldConstOffsetFor1DFetchPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,613 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.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 "LowerClipDistanceForEsslPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/basic_block.h"
|
||||
#include "source/opt/constants.h"
|
||||
#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/opt/type_manager.h"
|
||||
#include "source/opt/types.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::BasicBlock;
|
||||
using spvtools::opt::Function;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
spv::ExecutionModel EntryExecutionModel(IRContext* ctx) {
|
||||
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||
return static_cast<spv::ExecutionModel>(ep.GetSingleWordInOperand(0));
|
||||
}
|
||||
return spv::ExecutionModel::Max;
|
||||
}
|
||||
|
||||
uint32_t EntryFunctionId(IRContext* ctx) {
|
||||
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||
// OpEntryPoint <model> <function> "name" <interface...>
|
||||
return ep.GetSingleWordInOperand(1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t VariablePointeeType(IRContext* ctx, Instruction* var) {
|
||||
Instruction* ptrType = ctx->get_def_use_mgr()->GetDef(var->type_id());
|
||||
// OpTypePointer <storage-class> <pointee>
|
||||
return ptrType->GetSingleWordInOperand(1);
|
||||
}
|
||||
|
||||
uint32_t PointerTypeTo(IRContext* ctx, uint32_t pointeeId, spv::StorageClass sc) {
|
||||
analysis::Type* pointee = ctx->get_type_mgr()->GetType(pointeeId);
|
||||
analysis::Pointer ptr(pointee, sc);
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&ptr);
|
||||
}
|
||||
|
||||
uint32_t IntConstant(IRContext* ctx, bool isSigned, uint32_t value) {
|
||||
analysis::Integer i(32, isSigned);
|
||||
analysis::Type* reg = ctx->get_type_mgr()->GetRegisteredType(&i);
|
||||
const analysis::Constant* c = ctx->get_constant_mgr()->GetConstant(reg, {value});
|
||||
return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id();
|
||||
}
|
||||
|
||||
uint32_t UintType(IRContext* ctx) {
|
||||
analysis::Integer i(32, false);
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&i);
|
||||
}
|
||||
|
||||
uint32_t BoolType(IRContext* ctx) {
|
||||
analysis::Bool b;
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&b);
|
||||
}
|
||||
|
||||
// Constant length of OpTypeArray |arrayTypeId| (0 when not a sized constant).
|
||||
uint32_t ArrayLength(IRContext* ctx, uint32_t arrayTypeId) {
|
||||
Instruction* arrayType = ctx->get_def_use_mgr()->GetDef(arrayTypeId);
|
||||
if (arrayType == nullptr || arrayType->opcode() != spv::Op::OpTypeArray) {
|
||||
return 0;
|
||||
}
|
||||
Instruction* length = ctx->get_def_use_mgr()->GetDef(arrayType->GetSingleWordInOperand(1));
|
||||
if (length == nullptr || length->opcode() != spv::Op::OpConstant) {
|
||||
return 0;
|
||||
}
|
||||
return length->GetSingleWordInOperand(0);
|
||||
}
|
||||
|
||||
bool IsConstantWithValue(IRContext* ctx, uint32_t id, uint32_t value) {
|
||||
Instruction* def = ctx->get_def_use_mgr()->GetDef(id);
|
||||
return def != nullptr && def->opcode() == spv::Op::OpConstant &&
|
||||
def->GetSingleWordInOperand(0) == value;
|
||||
}
|
||||
|
||||
bool IsAccessChain(const Instruction* inst) {
|
||||
return inst->opcode() == spv::Op::OpAccessChain ||
|
||||
inst->opcode() == spv::Op::OpInBoundsAccessChain;
|
||||
}
|
||||
|
||||
Instruction* AddPrivateVariable(IRContext* ctx, uint32_t pointeeTypeId, const char* name) {
|
||||
const uint32_t ptrType = PointerTypeTo(ctx, pointeeTypeId, spv::StorageClass::Private);
|
||||
const uint32_t varId = ctx->TakeNextId();
|
||||
ctx->AddGlobalValue(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpVariable, ptrType, varId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||
{static_cast<uint32_t>(spv::StorageClass::Private)}}}));
|
||||
ctx->AddDebug2Inst(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpName, 0, 0,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {varId}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)}}));
|
||||
return ctx->get_def_use_mgr()->GetDef(varId);
|
||||
}
|
||||
|
||||
// Retargets |chain| onto |newBaseId|, dropping the first |dropIndexCount| index
|
||||
// operands and switching the result pointer's storage class to Private.
|
||||
void RetargetChainToPrivate(IRContext* ctx, Instruction* chain, uint32_t newBaseId,
|
||||
uint32_t dropIndexCount) {
|
||||
Instruction* chainPtrType = ctx->get_def_use_mgr()->GetDef(chain->type_id());
|
||||
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
|
||||
const uint32_t newPtrType = PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
|
||||
|
||||
ctx->ForgetUses(chain);
|
||||
std::vector<Operand> newOperands;
|
||||
newOperands.push_back({SPV_OPERAND_TYPE_ID, {newBaseId}});
|
||||
for (uint32_t i = 1 + dropIndexCount; i < chain->NumInOperands(); ++i) {
|
||||
newOperands.push_back(chain->GetInOperand(i));
|
||||
}
|
||||
chain->SetResultType(newPtrType);
|
||||
chain->SetInOperands(std::move(newOperands));
|
||||
ctx->AnalyzeUses(chain);
|
||||
}
|
||||
|
||||
// ---- Output side --------------------------------------------------------------
|
||||
|
||||
struct OutputTarget {
|
||||
Instruction* var = nullptr; // Output gl_PerVertex block or standalone builtin
|
||||
bool isBlockMember = false;
|
||||
uint32_t memberIndex = 0; // valid when isBlockMember
|
||||
uint32_t arrayTypeId = 0; // float[N]
|
||||
uint32_t elemTypeId = 0; // float
|
||||
uint32_t arrayLen = 0; // N
|
||||
};
|
||||
|
||||
// Inserts "gl_ClipDistance[k] = mg_ClipDistance[k]" for every literal k before
|
||||
// |before|. Constant-index writes are the only write shape Adreno links correctly.
|
||||
void InsertFlushBefore(IRContext* ctx, Instruction* before, const OutputTarget& target,
|
||||
uint32_t mgVarId) {
|
||||
const uint32_t ptrPrivElem =
|
||||
PointerTypeTo(ctx, target.elemTypeId, spv::StorageClass::Private);
|
||||
const uint32_t ptrOutElem =
|
||||
PointerTypeTo(ctx, target.elemTypeId, spv::StorageClass::Output);
|
||||
for (uint32_t k = 0; k < target.arrayLen; ++k) {
|
||||
const uint32_t kConst = IntConstant(ctx, true, k);
|
||||
const uint32_t srcChainId = ctx->TakeNextId();
|
||||
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpAccessChain, ptrPrivElem, srcChainId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {mgVarId}},
|
||||
{SPV_OPERAND_TYPE_ID, {kConst}}}));
|
||||
const uint32_t valId = ctx->TakeNextId();
|
||||
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpLoad, target.elemTypeId, valId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {srcChainId}}}));
|
||||
const uint32_t dstChainId = ctx->TakeNextId();
|
||||
std::vector<Operand> dstOperands;
|
||||
dstOperands.push_back({SPV_OPERAND_TYPE_ID, {target.var->result_id()}});
|
||||
if (target.isBlockMember) {
|
||||
dstOperands.push_back(
|
||||
{SPV_OPERAND_TYPE_ID, {IntConstant(ctx, true, target.memberIndex)}});
|
||||
}
|
||||
dstOperands.push_back({SPV_OPERAND_TYPE_ID, {kConst}});
|
||||
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpAccessChain, ptrOutElem, dstChainId, dstOperands));
|
||||
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpStore, 0, 0,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {dstChainId}},
|
||||
{SPV_OPERAND_TYPE_ID, {valId}}}));
|
||||
}
|
||||
}
|
||||
|
||||
bool LowerOutputClipDistance(IRContext* ctx, bool isGeometry) {
|
||||
auto* defUse = ctx->get_def_use_mgr();
|
||||
|
||||
// Collect (struct type, member) pairs decorated BuiltIn ClipDistance and
|
||||
// standalone variables decorated BuiltIn ClipDistance.
|
||||
std::vector<std::pair<uint32_t, uint32_t>> memberTargets; // (structId, member)
|
||||
std::vector<uint32_t> plainTargets; // variable ids
|
||||
for (Instruction& ann : ctx->annotations()) {
|
||||
if (ann.opcode() == spv::Op::OpMemberDecorate && ann.NumInOperands() >= 4 &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) ==
|
||||
spv::Decoration::BuiltIn &&
|
||||
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(3)) ==
|
||||
spv::BuiltIn::ClipDistance) {
|
||||
memberTargets.emplace_back(ann.GetSingleWordInOperand(0),
|
||||
ann.GetSingleWordInOperand(1));
|
||||
} else if (ann.opcode() == spv::Op::OpDecorate && ann.NumInOperands() >= 3 &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) ==
|
||||
spv::Decoration::BuiltIn &&
|
||||
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(2)) ==
|
||||
spv::BuiltIn::ClipDistance) {
|
||||
plainTargets.push_back(ann.GetSingleWordInOperand(0));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<OutputTarget> targets;
|
||||
for (Instruction& inst : ctx->module()->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable ||
|
||||
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Output) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t pointee = VariablePointeeType(ctx, &inst);
|
||||
for (const auto& [structId, member] : memberTargets) {
|
||||
if (pointee != structId) continue;
|
||||
Instruction* structType = defUse->GetDef(structId);
|
||||
if (structType == nullptr || member >= structType->NumInOperands()) continue;
|
||||
OutputTarget target;
|
||||
target.var = &inst;
|
||||
target.isBlockMember = true;
|
||||
target.memberIndex = member;
|
||||
target.arrayTypeId = structType->GetSingleWordInOperand(member);
|
||||
target.arrayLen = ArrayLength(ctx, target.arrayTypeId);
|
||||
targets.push_back(target);
|
||||
}
|
||||
for (const uint32_t varId : plainTargets) {
|
||||
if (inst.result_id() != varId) continue;
|
||||
OutputTarget target;
|
||||
target.var = &inst;
|
||||
target.isBlockMember = false;
|
||||
target.arrayTypeId = pointee;
|
||||
target.arrayLen = ArrayLength(ctx, target.arrayTypeId);
|
||||
targets.push_back(target);
|
||||
}
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
for (OutputTarget& target : targets) {
|
||||
if (target.arrayLen == 0) continue;
|
||||
Instruction* arrayType = defUse->GetDef(target.arrayTypeId);
|
||||
target.elemTypeId = arrayType->GetSingleWordInOperand(0);
|
||||
|
||||
// Collect the accesses to redirect. For the block form only chains whose
|
||||
// leading index selects the ClipDistance member count; for the standalone
|
||||
// form every chain plus whole-variable loads/stores.
|
||||
std::vector<Instruction*> chains;
|
||||
std::vector<Instruction*> directAccesses;
|
||||
bool unsupportedUse = false;
|
||||
defUse->ForEachUser(target.var, [&](Instruction* user) {
|
||||
if (IsAccessChain(user) &&
|
||||
user->GetSingleWordInOperand(0) == target.var->result_id()) {
|
||||
if (target.isBlockMember) {
|
||||
if (user->NumInOperands() >= 2 &&
|
||||
IsConstantWithValue(ctx, user->GetSingleWordInOperand(1),
|
||||
target.memberIndex)) {
|
||||
chains.push_back(user);
|
||||
}
|
||||
} else {
|
||||
chains.push_back(user);
|
||||
}
|
||||
} else if (!target.isBlockMember) {
|
||||
if (user->opcode() == spv::Op::OpLoad ||
|
||||
(user->opcode() == spv::Op::OpStore &&
|
||||
user->GetSingleWordInOperand(0) == target.var->result_id())) {
|
||||
directAccesses.push_back(user);
|
||||
} else if (user->opcode() == spv::Op::OpCopyMemory) {
|
||||
unsupportedUse = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (unsupportedUse || (chains.empty() && directAccesses.empty())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Instruction* mgVar = AddPrivateVariable(ctx, target.arrayTypeId, "mg_ClipDistance");
|
||||
const uint32_t mgVarId = mgVar->result_id();
|
||||
|
||||
for (Instruction* chain : chains) {
|
||||
const uint32_t dropCount = target.isBlockMember ? 1u : 0u;
|
||||
if (chain->NumInOperands() == 1 + dropCount) {
|
||||
// Pointer to the whole float[N]: reuse the private variable itself.
|
||||
ctx->ReplaceAllUsesWith(chain->result_id(), mgVarId);
|
||||
ctx->KillInst(chain);
|
||||
} else {
|
||||
RetargetChainToPrivate(ctx, chain, mgVarId, dropCount);
|
||||
}
|
||||
}
|
||||
for (Instruction* access : directAccesses) {
|
||||
ctx->ForgetUses(access);
|
||||
access->SetInOperand(0, {mgVarId});
|
||||
ctx->AnalyzeUses(access);
|
||||
}
|
||||
|
||||
// Flush the shadow into the real builtin: geometry right before every
|
||||
// EmitVertex, vertex before every return of the entry point. The flush is
|
||||
// also what keeps the builtin statically used for cross-stage IO matching.
|
||||
std::vector<Instruction*> flushSites;
|
||||
if (isGeometry) {
|
||||
for (Function& function : *ctx->module()) {
|
||||
function.ForEachInst([&](Instruction* inst) {
|
||||
if (inst->opcode() == spv::Op::OpEmitVertex) {
|
||||
flushSites.push_back(inst);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const uint32_t entryFuncId = EntryFunctionId(ctx);
|
||||
for (Function& function : *ctx->module()) {
|
||||
if (function.result_id() != entryFuncId) continue;
|
||||
function.ForEachInst([&](Instruction* inst) {
|
||||
if (inst->opcode() == spv::Op::OpReturn ||
|
||||
inst->opcode() == spv::Op::OpReturnValue) {
|
||||
flushSites.push_back(inst);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
for (Instruction* site : flushSites) {
|
||||
InsertFlushBefore(ctx, site, target, mgVarId);
|
||||
}
|
||||
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ---- Input side (geometry gl_in) ----------------------------------------------
|
||||
|
||||
bool LowerInputClipDistance(IRContext* ctx) {
|
||||
auto* defUse = ctx->get_def_use_mgr();
|
||||
auto* typeMgr = ctx->get_type_mgr();
|
||||
|
||||
// Locate the gl_in block member decorated ClipDistance.
|
||||
Instruction* glInVar = nullptr;
|
||||
uint32_t memberIndex = 0;
|
||||
uint32_t arrayTypeId = 0; // float[N]
|
||||
for (Instruction& ann : ctx->annotations()) {
|
||||
if (ann.opcode() != spv::Op::OpMemberDecorate || ann.NumInOperands() < 4 ||
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) !=
|
||||
spv::Decoration::BuiltIn ||
|
||||
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(3)) !=
|
||||
spv::BuiltIn::ClipDistance) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t structId = ann.GetSingleWordInOperand(0);
|
||||
const uint32_t member = ann.GetSingleWordInOperand(1);
|
||||
for (Instruction& inst : ctx->module()->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable ||
|
||||
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Input) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t pointee = VariablePointeeType(ctx, &inst);
|
||||
Instruction* pointeeType = defUse->GetDef(pointee);
|
||||
if (pointeeType == nullptr || pointeeType->opcode() != spv::Op::OpTypeArray ||
|
||||
pointeeType->GetSingleWordInOperand(0) != structId) {
|
||||
continue;
|
||||
}
|
||||
Instruction* structType = defUse->GetDef(structId);
|
||||
if (structType == nullptr || member >= structType->NumInOperands()) continue;
|
||||
glInVar = &inst;
|
||||
memberIndex = member;
|
||||
arrayTypeId = structType->GetSingleWordInOperand(member);
|
||||
break;
|
||||
}
|
||||
if (glInVar != nullptr) break;
|
||||
}
|
||||
if (glInVar == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t clipCount = ArrayLength(ctx, arrayTypeId);
|
||||
const uint32_t vertexCount = ArrayLength(ctx, VariablePointeeType(ctx, glInVar));
|
||||
if (clipCount == 0 || vertexCount == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Every gl_in chain that selects the ClipDistance member:
|
||||
// (vertex, member) yields a whole float[N], (vertex, member, k) an element.
|
||||
std::vector<Instruction*> chains;
|
||||
defUse->ForEachUser(glInVar, [&](Instruction* user) {
|
||||
if (IsAccessChain(user) && user->GetSingleWordInOperand(0) == glInVar->result_id() &&
|
||||
user->NumInOperands() >= 3 &&
|
||||
IsConstantWithValue(ctx, user->GetSingleWordInOperand(2), memberIndex)) {
|
||||
chains.push_back(user);
|
||||
}
|
||||
});
|
||||
if (chains.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Instruction* arrayTypeInst = defUse->GetDef(arrayTypeId);
|
||||
const uint32_t elemTypeId = arrayTypeInst->GetSingleWordInOperand(0);
|
||||
|
||||
// Private mg_ClipDistanceIn = float[vertexCount][clipCount].
|
||||
const uint32_t vertexCountConst = IntConstant(ctx, false, vertexCount);
|
||||
analysis::Type* innerType = typeMgr->GetType(arrayTypeId);
|
||||
analysis::Array outerArray(
|
||||
innerType, analysis::Array::LengthInfo{
|
||||
vertexCountConst,
|
||||
{analysis::Array::LengthInfo::kConstant, vertexCount}});
|
||||
const uint32_t outerArrayTypeId = typeMgr->GetTypeInstruction(&outerArray);
|
||||
Instruction* mgInVar = AddPrivateVariable(ctx, outerArrayTypeId, "mg_ClipDistanceIn");
|
||||
const uint32_t mgInVarId = mgInVar->result_id();
|
||||
|
||||
// Copy loop at the top of the entry point:
|
||||
// for (uint t = 0; t < vertexCount * clipCount; ++t)
|
||||
// mg_ClipDistanceIn[t / clipCount][t % clipCount] =
|
||||
// gl_in[t / clipCount].gl_ClipDistance[t % clipCount];
|
||||
// Both gl_in indices are loop-derived (dynamic): constant-index element reads
|
||||
// miscompile and whole-array reads crash the Adreno compiler.
|
||||
const uint32_t entryFuncId = EntryFunctionId(ctx);
|
||||
Function* entryFn = nullptr;
|
||||
for (Function& function : *ctx->module()) {
|
||||
if (function.result_id() == entryFuncId) {
|
||||
entryFn = &function;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (entryFn == nullptr || entryFn->begin() == entryFn->end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t uintTypeId = UintType(ctx);
|
||||
const uint32_t boolTypeId = BoolType(ctx);
|
||||
const uint32_t ptrFnUint = PointerTypeTo(ctx, uintTypeId, spv::StorageClass::Function);
|
||||
const uint32_t ptrInElem = PointerTypeTo(ctx, elemTypeId, spv::StorageClass::Input);
|
||||
const uint32_t ptrPrivElem = PointerTypeTo(ctx, elemTypeId, spv::StorageClass::Private);
|
||||
const uint32_t uint0 = IntConstant(ctx, false, 0);
|
||||
const uint32_t uint1 = IntConstant(ctx, false, 1);
|
||||
const uint32_t uintN = IntConstant(ctx, false, clipCount);
|
||||
const uint32_t uintTotal = IntConstant(ctx, false, vertexCount * clipCount);
|
||||
const uint32_t memberConst = IntConstant(ctx, true, memberIndex);
|
||||
|
||||
BasicBlock* entryBlock = &*entryFn->begin();
|
||||
auto splitPoint = entryBlock->begin();
|
||||
while (splitPoint != entryBlock->end() &&
|
||||
splitPoint->opcode() == spv::Op::OpVariable) {
|
||||
++splitPoint;
|
||||
}
|
||||
|
||||
// Loop counter lives with the other function-local variables.
|
||||
const uint32_t counterVarId = ctx->TakeNextId();
|
||||
splitPoint->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpVariable, ptrFnUint, counterVarId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||
{static_cast<uint32_t>(spv::StorageClass::Function)}}}));
|
||||
|
||||
const uint32_t restLabelId = ctx->TakeNextId();
|
||||
BasicBlock* restBlock = entryBlock->SplitBasicBlock(ctx, restLabelId, splitPoint);
|
||||
|
||||
const uint32_t headerLabelId = ctx->TakeNextId();
|
||||
const uint32_t checkLabelId = ctx->TakeNextId();
|
||||
const uint32_t bodyLabelId = ctx->TakeNextId();
|
||||
const uint32_t continueLabelId = ctx->TakeNextId();
|
||||
|
||||
auto makeBlock = [&](uint32_t labelId) {
|
||||
return spvtools::MakeUnique<BasicBlock>(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpLabel, 0, labelId, std::initializer_list<Operand>{}));
|
||||
};
|
||||
auto addInst = [&](BasicBlock* block, spv::Op opcode, uint32_t typeId,
|
||||
uint32_t resultId, std::vector<Operand> operands) {
|
||||
block->AddInstruction(spvtools::MakeUnique<Instruction>(
|
||||
ctx, opcode, typeId, resultId, std::move(operands)));
|
||||
};
|
||||
|
||||
// entry: t = 0; branch header
|
||||
addInst(entryBlock, spv::Op::OpStore, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {counterVarId}}, {SPV_OPERAND_TYPE_ID, {uint0}}});
|
||||
addInst(entryBlock, spv::Op::OpBranch, 0, 0, {{SPV_OPERAND_TYPE_ID, {headerLabelId}}});
|
||||
|
||||
// header: structured loop header
|
||||
auto headerBlock = makeBlock(headerLabelId);
|
||||
addInst(headerBlock.get(), spv::Op::OpLoopMerge, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {restLabelId}},
|
||||
{SPV_OPERAND_TYPE_ID, {continueLabelId}},
|
||||
{SPV_OPERAND_TYPE_LOOP_CONTROL,
|
||||
{static_cast<uint32_t>(spv::LoopControlMask::MaskNone)}}});
|
||||
addInst(headerBlock.get(), spv::Op::OpBranch, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {checkLabelId}}});
|
||||
|
||||
// check: t < vertexCount * clipCount ?
|
||||
auto checkBlock = makeBlock(checkLabelId);
|
||||
const uint32_t tCheckId = ctx->TakeNextId();
|
||||
addInst(checkBlock.get(), spv::Op::OpLoad, uintTypeId, tCheckId,
|
||||
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
|
||||
const uint32_t condId = ctx->TakeNextId();
|
||||
addInst(checkBlock.get(), spv::Op::OpULessThan, boolTypeId, condId,
|
||||
{{SPV_OPERAND_TYPE_ID, {tCheckId}}, {SPV_OPERAND_TYPE_ID, {uintTotal}}});
|
||||
addInst(checkBlock.get(), spv::Op::OpBranchConditional, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {condId}},
|
||||
{SPV_OPERAND_TYPE_ID, {bodyLabelId}},
|
||||
{SPV_OPERAND_TYPE_ID, {restLabelId}}});
|
||||
|
||||
// body: mg_ClipDistanceIn[t / N][t % N] = gl_in[t / N].gl_ClipDistance[t % N]
|
||||
auto bodyBlock = makeBlock(bodyLabelId);
|
||||
const uint32_t tBodyId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpLoad, uintTypeId, tBodyId,
|
||||
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
|
||||
const uint32_t vertexIdxId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpUDiv, uintTypeId, vertexIdxId,
|
||||
{{SPV_OPERAND_TYPE_ID, {tBodyId}}, {SPV_OPERAND_TYPE_ID, {uintN}}});
|
||||
const uint32_t clipIdxId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpUMod, uintTypeId, clipIdxId,
|
||||
{{SPV_OPERAND_TYPE_ID, {tBodyId}}, {SPV_OPERAND_TYPE_ID, {uintN}}});
|
||||
const uint32_t srcChainId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpAccessChain, ptrInElem, srcChainId,
|
||||
{{SPV_OPERAND_TYPE_ID, {glInVar->result_id()}},
|
||||
{SPV_OPERAND_TYPE_ID, {vertexIdxId}},
|
||||
{SPV_OPERAND_TYPE_ID, {memberConst}},
|
||||
{SPV_OPERAND_TYPE_ID, {clipIdxId}}});
|
||||
const uint32_t valId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpLoad, elemTypeId, valId,
|
||||
{{SPV_OPERAND_TYPE_ID, {srcChainId}}});
|
||||
const uint32_t dstChainId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpAccessChain, ptrPrivElem, dstChainId,
|
||||
{{SPV_OPERAND_TYPE_ID, {mgInVarId}},
|
||||
{SPV_OPERAND_TYPE_ID, {vertexIdxId}},
|
||||
{SPV_OPERAND_TYPE_ID, {clipIdxId}}});
|
||||
addInst(bodyBlock.get(), spv::Op::OpStore, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {dstChainId}}, {SPV_OPERAND_TYPE_ID, {valId}}});
|
||||
addInst(bodyBlock.get(), spv::Op::OpBranch, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {continueLabelId}}});
|
||||
|
||||
// continue: ++t
|
||||
auto continueBlock = makeBlock(continueLabelId);
|
||||
const uint32_t tContinueId = ctx->TakeNextId();
|
||||
addInst(continueBlock.get(), spv::Op::OpLoad, uintTypeId, tContinueId,
|
||||
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
|
||||
const uint32_t tIncId = ctx->TakeNextId();
|
||||
addInst(continueBlock.get(), spv::Op::OpIAdd, uintTypeId, tIncId,
|
||||
{{SPV_OPERAND_TYPE_ID, {tContinueId}}, {SPV_OPERAND_TYPE_ID, {uint1}}});
|
||||
addInst(continueBlock.get(), spv::Op::OpStore, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {counterVarId}}, {SPV_OPERAND_TYPE_ID, {tIncId}}});
|
||||
addInst(continueBlock.get(), spv::Op::OpBranch, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {headerLabelId}}});
|
||||
|
||||
BasicBlock* headerPtr = entryFn->InsertBasicBlockBefore(std::move(headerBlock), restBlock);
|
||||
BasicBlock* checkPtr = entryFn->InsertBasicBlockAfter(std::move(checkBlock), headerPtr);
|
||||
BasicBlock* bodyPtr = entryFn->InsertBasicBlockAfter(std::move(bodyBlock), checkPtr);
|
||||
entryFn->InsertBasicBlockAfter(std::move(continueBlock), bodyPtr);
|
||||
|
||||
// Redirect the pre-existing accesses to the shadow copy.
|
||||
for (Instruction* chain : chains) {
|
||||
if (chain->NumInOperands() == 3) {
|
||||
// (vertex, member): whole float[N] of one vertex.
|
||||
Instruction* chainPtrType = defUse->GetDef(chain->type_id());
|
||||
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
|
||||
const uint32_t newPtrType =
|
||||
PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
|
||||
ctx->ForgetUses(chain);
|
||||
std::vector<Operand> newOperands;
|
||||
newOperands.push_back({SPV_OPERAND_TYPE_ID, {mgInVarId}});
|
||||
newOperands.push_back(chain->GetInOperand(1));
|
||||
chain->SetResultType(newPtrType);
|
||||
chain->SetInOperands(std::move(newOperands));
|
||||
ctx->AnalyzeUses(chain);
|
||||
} else {
|
||||
// (vertex, member, k, ...): drop the member index.
|
||||
Instruction* chainPtrType = defUse->GetDef(chain->type_id());
|
||||
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
|
||||
const uint32_t newPtrType =
|
||||
PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
|
||||
ctx->ForgetUses(chain);
|
||||
std::vector<Operand> newOperands;
|
||||
newOperands.push_back({SPV_OPERAND_TYPE_ID, {mgInVarId}});
|
||||
newOperands.push_back(chain->GetInOperand(1));
|
||||
for (uint32_t i = 3; i < chain->NumInOperands(); ++i) {
|
||||
newOperands.push_back(chain->GetInOperand(i));
|
||||
}
|
||||
chain->SetResultType(newPtrType);
|
||||
chain->SetInOperands(std::move(newOperands));
|
||||
ctx->AnalyzeUses(chain);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status LowerClipDistanceForEsslPass::Process() {
|
||||
auto* ctx = context();
|
||||
const spv::ExecutionModel model = EntryExecutionModel(ctx);
|
||||
const bool isVertex = model == spv::ExecutionModel::Vertex;
|
||||
const bool isGeometry = model == spv::ExecutionModel::Geometry;
|
||||
if (!isVertex && !isGeometry) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
bool changed = LowerOutputClipDistance(ctx, isGeometry);
|
||||
if (isGeometry) {
|
||||
changed |= LowerInputClipDistance(ctx);
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
ctx->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken
|
||||
LowerClipDistanceForEsslPass::CreateLowerClipDistanceForEsslPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<LowerClipDistanceForEsslPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,44 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.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 {
|
||||
// Adreno's ESSL compiler mishandles gl_ClipDistance (device-verified on Adreno 750):
|
||||
// - writes through non-constant indices silently fail to link,
|
||||
// - reads of gl_in[i].gl_ClipDistance[k] with a CONSTANT k >= 1 fail to compile
|
||||
// ("array indexing out of boundary") while dynamic-index reads work,
|
||||
// - compiling a whole-array read of gl_in[i].gl_ClipDistance segfaults the
|
||||
// compiler backend (libllvm-qgl.so).
|
||||
// This pass shadows the builtin so the decompiled ESSL only ever touches it in the
|
||||
// shapes Adreno accepts. Output side (vertex + geometry): all accesses to the
|
||||
// Output ClipDistance (gl_PerVertex member or standalone variable) are redirected
|
||||
// to a Private mg_ClipDistance array, and a flush writing the real builtin with
|
||||
// literal constant indices is inserted before every OpEmitVertex (geometry) or
|
||||
// every return of the entry point (vertex). Input side (geometry): accesses to
|
||||
// gl_in[...].gl_ClipDistance are redirected to a Private mg_ClipDistanceIn
|
||||
// array-of-arrays filled once at the top of the entry point by a structured loop
|
||||
// whose gl_in reads use dynamic (loop-variable) indices. The builtin members stay
|
||||
// statically referenced by the flush/copy so cross-stage IO matching is intact.
|
||||
// Only meant for the DirectGLES transpile path on Qualcomm devices.
|
||||
class LowerClipDistanceForEsslPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "lower-clip-distance-for-essl"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateLowerClipDistanceForEsslPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -95,7 +95,6 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
Int32,
|
||||
Half,
|
||||
Float32,
|
||||
UNorm32, // 32-bit fixed-point depth shadow
|
||||
};
|
||||
|
||||
struct InternalShadowLayout {
|
||||
@@ -124,21 +123,6 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
|
||||
Bool GetInternalShadowLayout(TextureInternalFormat internal, InternalShadowLayout& out) {
|
||||
switch (internal) {
|
||||
// Depth shadows follow TextureFormatProcessor::NormalizePixelFormat: 16-bit
|
||||
// unorm for DEPTH_COMPONENT16, 32-bit unorm for the 24/32-bit fixed-point
|
||||
// depths, float for DEPTH_COMPONENT32F.
|
||||
case TextureInternalFormat::DepthComponent16:
|
||||
out = {1, ShadowComponent::UNorm16, false};
|
||||
return true;
|
||||
case TextureInternalFormat::DepthComponent24:
|
||||
case TextureInternalFormat::DepthComponent32:
|
||||
case TextureInternalFormat::DepthComponent:
|
||||
out = {1, ShadowComponent::UNorm32, false};
|
||||
return true;
|
||||
case TextureInternalFormat::DepthComponent32F:
|
||||
out = {1, ShadowComponent::Float32, false};
|
||||
return true;
|
||||
|
||||
case TextureInternalFormat::R8:
|
||||
case TextureInternalFormat::Red: out = {1, ShadowComponent::UNorm8, false}; return true;
|
||||
case TextureInternalFormat::RG8:
|
||||
@@ -315,10 +299,8 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
case TextureInputFormat::RGBAInteger: out = {{0, 1, 2, 3}, 4, true}; return true;
|
||||
case TextureInputFormat::BGRA: out = {{2, 1, 0, 3}, 4, false}; return true;
|
||||
case TextureInputFormat::BGRAInteger: out = {{2, 1, 0, 3}, 4, true}; return true;
|
||||
// A depth value converts like a single normalized/float channel.
|
||||
case TextureInputFormat::DepthComponent: out = {{0, -1, -1, -1}, 1, false}; return true;
|
||||
default:
|
||||
return false; // stencil / packed depth-stencil / unknown
|
||||
return false; // depth / stencil / unknown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +346,8 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
out = isInteger ? ShadowComponent::Int16 : ShadowComponent::SNorm16;
|
||||
return true;
|
||||
case TexturePixelDataType::UnsignedInt:
|
||||
out = isInteger ? ShadowComponent::UInt32 : ShadowComponent::UNorm32;
|
||||
if (!isInteger) return false; // no 32-bit normalized shadow layout
|
||||
out = ShadowComponent::UInt32;
|
||||
return true;
|
||||
case TexturePixelDataType::Int:
|
||||
if (!isInteger) return false;
|
||||
@@ -634,12 +617,6 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
case ShadowComponent::Float32:
|
||||
Memcpy(dst, &v, sizeof(v));
|
||||
break;
|
||||
case ShadowComponent::UNorm32: {
|
||||
const auto out = static_cast<Uint32>(
|
||||
std::llround(static_cast<double>(std::clamp(v, 0.0f, 1.0f)) * 4294967295.0));
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break; // integer components never reach the float encoder
|
||||
}
|
||||
@@ -794,64 +771,6 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
const Int effectiveHeight = (params.ImageHeight > 0) ? params.ImageHeight : height;
|
||||
const SizeT inputRowStride = CalculateRowStride(effectiveWidth, pixelSize, params.Alignment);
|
||||
|
||||
// GL_DEPTH_COMPONENT client data may populate a packed depth-stencil internal
|
||||
// format (the stencil half becomes zero); the generic channel converter cannot
|
||||
// express the packed shadow words, so convert here.
|
||||
const Bool packedDepthStencilInternal = targetInternalFormat == TextureInternalFormat::Depth24Stencil8 ||
|
||||
targetInternalFormat == TextureInternalFormat::DepthStencil ||
|
||||
targetInternalFormat == TextureInternalFormat::Depth32FStencil8;
|
||||
if (!isBitmap && packedDepthStencilInternal && textureInputFormat == TextureInputFormat::DepthComponent &&
|
||||
(inputDataType == TexturePixelDataType::Float || inputDataType == TexturePixelDataType::UnsignedInt ||
|
||||
inputDataType == TexturePixelDataType::UnsignedShort)) {
|
||||
const Bool floatShadow = targetInternalFormat == TextureInternalFormat::Depth32FStencil8;
|
||||
const SizeT outPixelSize = floatShadow ? 8 : 4;
|
||||
outSize = static_cast<SizeT>(width) * height * std::max(depth, 1) * outPixelSize;
|
||||
Uint8* outputPixels = static_cast<Uint8*>(malloc(outSize));
|
||||
if (!outputPixels) {
|
||||
outSize = 0;
|
||||
return nullptr;
|
||||
}
|
||||
const Uint8* srcBase = static_cast<const Uint8*>(inputPixels) +
|
||||
static_cast<SizeT>(params.SkipImages) * static_cast<SizeT>(effectiveHeight) * inputRowStride +
|
||||
static_cast<SizeT>(params.SkipRows) * inputRowStride +
|
||||
static_cast<SizeT>(params.SkipPixels) * pixelSize;
|
||||
Uint8* dst = outputPixels;
|
||||
for (Int z = 0; z < std::max(depth, 1); ++z) {
|
||||
for (Int y = 0; y < height; ++y) {
|
||||
const Uint8* srcRow = srcBase +
|
||||
static_cast<SizeT>(z) * static_cast<SizeT>(effectiveHeight) * inputRowStride +
|
||||
static_cast<SizeT>(y) * inputRowStride;
|
||||
for (Int x = 0; x < width; ++x) {
|
||||
Float depthValue = 0.0f;
|
||||
if (inputDataType == TexturePixelDataType::Float) {
|
||||
Memcpy(&depthValue, srcRow + static_cast<SizeT>(x) * 4, sizeof(depthValue));
|
||||
} else if (inputDataType == TexturePixelDataType::UnsignedInt) {
|
||||
Uint32 raw = 0;
|
||||
Memcpy(&raw, srcRow + static_cast<SizeT>(x) * 4, sizeof(raw));
|
||||
depthValue = static_cast<Float>(static_cast<double>(raw) / 4294967295.0);
|
||||
} else {
|
||||
Uint16 raw = 0;
|
||||
Memcpy(&raw, srcRow + static_cast<SizeT>(x) * 2, sizeof(raw));
|
||||
depthValue = static_cast<Float>(raw) / 65535.0f;
|
||||
}
|
||||
if (floatShadow) {
|
||||
const Uint32 stencilWord = 0;
|
||||
Memcpy(dst, &depthValue, sizeof(depthValue));
|
||||
Memcpy(dst + 4, &stencilWord, sizeof(stencilWord));
|
||||
dst += 8;
|
||||
} else {
|
||||
const Uint32 depth24 = static_cast<Uint32>(
|
||||
std::llround(static_cast<double>(std::clamp(depthValue, 0.0f, 1.0f)) * 16777215.0));
|
||||
const Uint32 word = depth24 << 8;
|
||||
Memcpy(dst, &word, sizeof(word));
|
||||
dst += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return outputPixels;
|
||||
}
|
||||
|
||||
UnpackConversionSpec conversion{};
|
||||
const Bool needConversion =
|
||||
!isBitmap && GetUnpackConversionSpec(targetInternalFormat, textureInputFormat, inputDataType, conversion);
|
||||
@@ -1053,11 +972,6 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
case ShadowComponent::UNorm32: {
|
||||
Uint32 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return static_cast<Float>(static_cast<double>(v) / 4294967295.0);
|
||||
}
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user