mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53c39d2421 | ||
|
|
9d1b280375 | ||
|
|
8acd885594 | ||
|
|
10ff5e2b18 | ||
|
|
a6e52476f3 | ||
|
|
0deff52a1b | ||
|
|
50fefca959 | ||
|
|
42ad62b54c | ||
|
|
f41403e227 | ||
|
|
5fbb17f6b9 | ||
|
|
92d8f7269b | ||
|
|
822e405c77 | ||
|
|
b8233f9c4e | ||
|
|
91475a7b6f | ||
|
|
cee17025a0 | ||
|
|
9642ae4d20 | ||
|
|
595d140036 | ||
|
|
b62d1f2078 | ||
|
|
5e82ff968a | ||
|
|
6a80a82dd3 | ||
|
|
f9182a5ca3 | ||
|
|
f37b511fca | ||
|
|
38027d21f8 | ||
|
|
dd2a62228f | ||
|
|
373aa44dd7 | ||
|
|
96646df12e | ||
|
|
f20b20e643 | ||
|
|
2587814970 | ||
|
|
43398e33e8 | ||
|
|
b7557d6615 | ||
|
|
bce34d7fac | ||
|
|
f91857266f | ||
|
|
49cb1be0fd | ||
|
|
a51c68bb2c | ||
|
|
1c723a6cfc | ||
|
|
44805bfa07 | ||
|
|
257fcbfd0b | ||
|
|
2b46a3db96 | ||
|
|
0b36621069 | ||
|
|
9ee2e0a1db | ||
|
|
6b6623ae72 | ||
|
|
a6e029734b | ||
|
|
e7d6bfddac | ||
|
|
f2c879528f | ||
|
|
eaba4ac1dc | ||
|
|
ed6578954e | ||
|
|
e005c8b6cb | ||
|
|
535b5e3095 | ||
|
|
1b05a84928 | ||
|
|
7ccb762936 | ||
|
|
442e7eec1c | ||
|
|
2787d15706 | ||
|
|
74ce58a6c7 | ||
|
|
bb122ebd4f | ||
|
|
9b0ed5b3af | ||
|
|
0e31c1481b |
@@ -182,6 +182,7 @@ set(ENABLE_SPVREMAPPER OFF CACHE BOOL "Enable SPVRemapper" FORCE)
|
||||
set(ENABLE_OPT ON CACHE BOOL "Enable SPIRV-Tools opt usage in glslang" FORCE)
|
||||
set(BUILD_EXTERNAL ON CACHE BOOL "Build external deps in External/" FORCE)
|
||||
set(ENABLE_GLSLANG_INSTALL OFF CACHE BOOL "Install glslang targets" FORCE)
|
||||
set(SPIRV_SKIP_EXECUTABLES ON CACHE BOOL "Skip building SPIRV-Tools executables" FORCE)
|
||||
|
||||
set(SPIRV_CROSS_C_API ON CACHE BOOL "Enable C API" FORCE)
|
||||
set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE)
|
||||
@@ -284,6 +285,8 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||
|
||||
@@ -117,6 +117,13 @@ namespace MobileGL::MG_Config {
|
||||
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
|
||||
// (negative control / driver-bug escape hatch).
|
||||
Bool DisableUboRing = false;
|
||||
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
|
||||
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
|
||||
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
|
||||
// Adreno does not), which means the emulation is dead code on exactly the stack the
|
||||
// headless suite runs on. This forces it live so the scenarios and the CTS can
|
||||
// exercise the path, and gives the device an A/B lever over the same choice.
|
||||
Bool EsprytForceDepthStencilReadbackEmulation = false;
|
||||
// MOBILEGL_RELAXED_SEMANTICS: relax strict core-profile rules (e.g. VAO-0 draws,
|
||||
// texture-name reuse after delete) even on contexts that explicitly requested a core
|
||||
// profile. Without it, relaxed semantics still apply to every context that did not
|
||||
|
||||
@@ -176,6 +176,8 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||
features.EsprytForceDepthStencilReadbackEmulation =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||
|
||||
+13
-3
@@ -52,11 +52,15 @@
|
||||
// that includes Defines.h without Log.h both tokens would silently evaluate to 0 in the
|
||||
// preprocessor conditional - enabling the assert in exactly the INFO-level builds it is
|
||||
// documented to be compiled out of. Log.h redefines them identically, which is legal.
|
||||
//
|
||||
// Severity order, ascending: DEBUG < INFO < WARN < ERROR < FATAL. MOBILEGL_LOG_ACTIVE_LEVEL
|
||||
// names the lowest severity compiled in, so the production default INFO keeps I/W/E/F and
|
||||
// drops only D. Any edit here must be mirrored in Log.h.
|
||||
#ifndef MOBILEGL_LOG_LEVEL_DEBUG
|
||||
#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
|
||||
#endif
|
||||
|
||||
@@ -91,6 +95,12 @@
|
||||
#endif
|
||||
|
||||
// =============================== Utils ================================ //
|
||||
// Asserts are live in exactly the builds where MGLOG_D is live, i.e. DEBUG builds only;
|
||||
// an INFO build (the production default) compiles them out. DEBUG is the lowest severity
|
||||
// in the ordering above, so "ACTIVE <= DEBUG" is true only for ACTIVE == DEBUG - the same
|
||||
// gate MGLOG_D uses in Log.h. That equivalence is what makes this gate survive the
|
||||
// 2026-08-13 renumbering unchanged; the contract is and stays
|
||||
// "INFO builds: asserts OFF; DEBUG builds: asserts ON".
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
#define MOBILEGL_ASSERT(condition, ...) \
|
||||
do { \
|
||||
|
||||
+7
-5
@@ -14,6 +14,7 @@
|
||||
#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/Query/GL_Query.h>
|
||||
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
@@ -45,12 +46,13 @@ namespace MobileGL {
|
||||
// both of which this function is about to destroy. This is the one
|
||||
// cancellation path in the whole design that waits.
|
||||
MG_Util::Async::ShaderCompilePool::Get().StopAndDrain();
|
||||
// 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).
|
||||
// GL syncs and queries die with their contexts, and every context is gone
|
||||
// by the time full teardown runs: drain both live registries 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 / DeleteBackendQuery).
|
||||
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
||||
MG_Impl::GLImpl::DestroyAllQueryObjects();
|
||||
MG_Backend::pActiveBackendObject.reset();
|
||||
MG_State::pGLContext.reset();
|
||||
MG_State::pEGLContext.reset();
|
||||
|
||||
@@ -712,9 +712,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
{
|
||||
.TargetGLVersion = {4, 0, 0}, // GL target version
|
||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
|
||||
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false),
|
||||
// Baseline advertisement (no runtime capabilities yet); reconciled once
|
||||
// the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||
.IsCompatibilityProfile = false // Is Compatibility Profile
|
||||
},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
||||
@@ -734,9 +734,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// thread can only observe the extension string after the
|
||||
// advertisement for its context has settled; rebuilding the whole
|
||||
// list keeps the re-run after a context recreation idempotent.
|
||||
void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions =
|
||||
BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported);
|
||||
void UpdateAdvertisedCapabilityExtensions(const MG_External::GLESCapabilities& capabilities) {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
|
||||
capabilities.SupportsDrawIndirect,
|
||||
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -779,11 +781,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return false;
|
||||
}
|
||||
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
|
||||
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and
|
||||
// GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on
|
||||
// UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension
|
||||
// list is first built).
|
||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy);
|
||||
// Now that g_GLESCapabilities knows the host extensions, entry points, and ES version,
|
||||
// reconcile every runtime-gated advertisement (see the comment on
|
||||
// UpdateAdvertisedCapabilityExtensions for why this cannot happen when the list is first
|
||||
// built).
|
||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities);
|
||||
UpdateDynamicBackendParameters();
|
||||
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
|
||||
PrintFormatCapabilities(GetFormatCapabilities());
|
||||
@@ -924,7 +926,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return MutableRendererInfo();
|
||||
}
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
@@ -940,10 +944,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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,
|
||||
// Core since GL 3.1 and implemented for every version advertised here. The string
|
||||
// matters because applications gate the ENTRY POINTS on it rather than on the
|
||||
// version: a caller that finds the extension missing never resolves
|
||||
// glGetUniformBlockIndex / glUniformBlockBinding, and one that then uses uniform
|
||||
// blocks anyway calls through a null pointer.
|
||||
E_GL_ARB_uniform_buffer_object,
|
||||
// Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3,
|
||||
// so on a 4.0 context the string is the only way to reach it. The host ES driver
|
||||
// has had the same texture parameter since ES 3.1, which every device MobileGL
|
||||
// runs on provides.
|
||||
E_GL_ARB_stencil_texturing,
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
E_GL_ARB_get_program_binary};
|
||||
// Minecraft 26.3 checks this prerequisite before it even considers
|
||||
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
|
||||
// folds the version and pointer checks into SupportsDrawIndirect.
|
||||
if (drawIndirectSupported) {
|
||||
extensions.push_back(E_GL_ARB_draw_indirect);
|
||||
}
|
||||
// ARB_base_instance also defines the last word of an indirect command. Direct calls are
|
||||
// emulated on every Espryt device, but without host GL_EXT_base_instance a native indirect
|
||||
// draw cannot shift divisor attributes by a GPU-authored non-zero value, so do not promise
|
||||
// that incomplete case.
|
||||
if (drawIndirectSupported && nonZeroIndirectBaseInstanceSupported) {
|
||||
extensions.push_back(E_GL_ARB_base_instance);
|
||||
}
|
||||
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the host ES
|
||||
// driver's: the compiler threads are MobileGL's, and glCompileShader/glLinkProgram
|
||||
// are serviced entirely inside the frontend. Whether the device driver advertises
|
||||
|
||||
@@ -67,9 +67,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const RendererInfo& GetRendererIdentity();
|
||||
|
||||
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
||||
// for a device whose timer queries / anisotropic filtering are (or are not) usable.
|
||||
// for a device whose timer queries / anisotropic filtering / native indirect draws /
|
||||
// non-zero indirect baseInstance semantics are (or are not) usable.
|
||||
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported);
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported);
|
||||
|
||||
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
||||
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -286,6 +286,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// context loss.
|
||||
Bool persistentMapped = false;
|
||||
void* persistentPtr = nullptr;
|
||||
// The GL store behind `id` was created with glBufferStorageEXT and is
|
||||
// therefore IMMUTABLE - glBufferData cannot respecify it and it must never be
|
||||
// recycled through the size-keyed buffer pool. Tracked separately from
|
||||
// persistentMapped because the two come apart: a glMapBufferRange that fails
|
||||
// after its glBufferStorageEXT succeeded leaves immutable storage behind with
|
||||
// no map, and a respecification then has to retire the id rather than hand it
|
||||
// to glBufferData, which the driver would silently refuse.
|
||||
Bool immutableStorage = false;
|
||||
};
|
||||
|
||||
// Registered as the frontend's BufferBackendOps at backend init and on
|
||||
@@ -398,6 +406,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void SyncClientSideAttributesForDrawArrays(
|
||||
const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject, GLint first, GLsizei count);
|
||||
Uint GetBackendVertexArrayId() const { return m_backendVAOId; }
|
||||
Uint GetContextGeneration() const { return m_contextGeneration; }
|
||||
void Bind() const;
|
||||
|
||||
// Draw-path memo of SyncNeccessaryBuffers' attribute walk for this VAO: the
|
||||
@@ -454,6 +463,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ResolvedDrawBuffers m_resolvedDrawBuffers;
|
||||
PendingAttribValueMask m_pendingAttribValueMask;
|
||||
Uint m_backendVAOId = 0;
|
||||
// ES context generation the VAO id and client-attribute buffer ids were
|
||||
// created under; ids from a dead context must never be deleted against a
|
||||
// successor context (both contexts restart GL names at 1).
|
||||
Uint m_contextGeneration = 0;
|
||||
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
||||
Bool m_isInitialized = false;
|
||||
Uint16 m_syncedIndexBufferVersion = 0;
|
||||
@@ -691,8 +704,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
Vec4<TextureSwizzleParam> m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green,
|
||||
TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha};
|
||||
// GL_DEPTH_STENCIL_TEXTURE_MODE. GL_DEPTH_COMPONENT is the GL and ES default, so a
|
||||
// texture that never asks for the stencil aspect never emits the call. The
|
||||
// depth/stencil readback and replicate-blit emulations also write this parameter
|
||||
// raw, but only ever on their own scratch textures (never on an application
|
||||
// texture), so they cannot desynchronise this cache.
|
||||
GLenum m_cacheDepthStencilTextureMode = GL_DEPTH_COMPONENT;
|
||||
Uint16 m_syncedSamplerVersion = 0;
|
||||
Uint16 m_syncedTextureParamsVersion = 0;
|
||||
// Set when the driver texture underneath was regenerated and has therefore lost every
|
||||
// parameter already pushed onto it: the params-version early-out has to be overridden
|
||||
// once, or an unchanged version would skip the re-push forever.
|
||||
Bool m_forceTextureParamsResync = false;
|
||||
// Same latch for the built-in sampler parameters.
|
||||
Bool m_forceSamplerResync = false;
|
||||
};
|
||||
|
||||
void ActivateTextureUnit(Uint unit);
|
||||
@@ -1069,6 +1094,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
|
||||
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
||||
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
||||
Uint GetContextGeneration() const { return m_contextGeneration; }
|
||||
// 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.
|
||||
@@ -1104,11 +1130,37 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// stale as one built before a relink - while the sampler half, which really is
|
||||
// re-issued per draw, needs nothing of the sort.
|
||||
Uint32 GetSyncedImageUnitVersion() const { return m_syncedImageUnitVersion; }
|
||||
// Whether the (unit, bound format) pairs this program's FORMAT-LESS image uniforms
|
||||
// resolve to are still the ones its ESSL was generated against.
|
||||
//
|
||||
// A fourth condition of the same family as the three above, and the only one that
|
||||
// reads live state rather than a program-side counter, because that is where the
|
||||
// dependency actually is. GLSL ES requires a format layout qualifier on every image
|
||||
// where desktop GLSL lets a writeonly declaration omit one, and the only correct
|
||||
// qualifier is whatever glBindImageTexture named - so a declaration with no format
|
||||
// is compiled against the BINDING, and a rebind to a different format makes the
|
||||
// built program wrong. Keyed on the units the program's own images address (cached
|
||||
// at sync, since a unit can only move by glUniform1i, which bumps the image-unit
|
||||
// version above and forces a re-sync anyway), so the cost on a program with no
|
||||
// format-less image - which is all but a handful - is one empty-vector test.
|
||||
//
|
||||
// Deliberately NOT reached from glBindImageTexture: that entry point must never
|
||||
// trigger a build (same constraint as glShaderStorageBlockBinding). It moves the
|
||||
// state and this comparison notices at the next Prepare, which is also what makes
|
||||
// an image first bound AFTER link work.
|
||||
Bool ImageUnitFormatsStillMatch() const;
|
||||
// The value ImageUnitFormatsStillMatch() compares against, recomputed from live
|
||||
// image-unit state. 0 when the program has no format-less image uniform.
|
||||
Uint64 ComputeImageUnitFormatSignature() const;
|
||||
|
||||
private:
|
||||
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||
|
||||
Uint m_backendProgramId = 0;
|
||||
// ES context generation the backend program and its global UBO were created
|
||||
// under. A stale twin must be recreated, never deleted against a successor
|
||||
// context (both contexts restart GL names at 1).
|
||||
Uint m_contextGeneration = 0;
|
||||
// GL name of the frontend program this was last synced from; diagnostics only, so
|
||||
// an unusable backend program can be traced back to the glCreateProgram id the app
|
||||
// knows it by.
|
||||
@@ -1137,6 +1189,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
|
||||
Uint32 m_syncedLinkVersion = ~0u;
|
||||
Uint32 m_syncedImageUnitVersion = ~0u;
|
||||
// Image units addressed by the program's FORMAT-LESS image uniforms, and the digest
|
||||
// of the (unit, format) pairs the generated ESSL baked. Empty/0 for every program
|
||||
// that declares a format on all of its images, which is the overwhelming majority -
|
||||
// and what keeps the per-draw comparison free for them.
|
||||
Vector<Int> m_formatlessImageUnits;
|
||||
Uint64 m_imageUnitFormatSignature = 0;
|
||||
SamplerPassMemo m_samplerPassMemo;
|
||||
};
|
||||
|
||||
@@ -1150,6 +1208,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the
|
||||
// ES context is recreated.
|
||||
extern Uint g_lastUsedBackendProgramId;
|
||||
// Deletes `bufferId` only while it still belongs to the live ES context. Stale
|
||||
// generations are abandoned without a GL call: the old context already reclaimed
|
||||
// the buffer, and its numeric id may now name a live buffer in a successor context.
|
||||
void DeleteBackendProgramGlobalUbo(Uint& bufferId, Uint contextGeneration);
|
||||
extern StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl>
|
||||
g_backendProgramObjects;
|
||||
|
||||
@@ -1180,6 +1242,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// already has costs nothing. 0 when nothing was ever rebound.
|
||||
Uint64 ComputeShaderStorageBlockBindingSignature(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||
|
||||
// Everything the image-format bake needs from one walk of a program's uniform
|
||||
// reflection. GLSL ES requires a format layout qualifier on every image uniform;
|
||||
// desktop GLSL lets a writeonly (or readonly) declaration omit one, and the only
|
||||
// format that is CORRECT to substitute is whatever glBindImageTexture named for the
|
||||
// unit that uniform addresses - so the transpile bakes it in and the build is keyed
|
||||
// on it.
|
||||
struct ImageFormatBakeInputs {
|
||||
// Uniform name (SPIR-V spelling, i.e. an array named once, unsubscripted) to the GL
|
||||
// internal format to bake. Holds only uniforms that DECLARED no format; a declared
|
||||
// one is authoritative and is never overridden.
|
||||
UnorderedMap<String, Uint> glFormatByUniformName;
|
||||
// The same uniforms whose format SPIRV-Cross REFUSES to print for ESSL (it throws on
|
||||
// its desktop-only set, which loses the stage), paired with the ESSL spelling to
|
||||
// write into the emitted declaration instead. Disjoint from the map above by
|
||||
// construction: a format is baked into the module or completed in the text, never
|
||||
// both. r8ui - the stencil half of the packed_depth_stencil case - lands here.
|
||||
UnorderedMap<String, String> esslFormatQualifierByUniformName;
|
||||
// Units those uniforms address, kept so the draw path can re-read their formats
|
||||
// without walking the reflection again.
|
||||
Vector<Int> units;
|
||||
// Digest of the (unit, format) pairs above. 0 when the program has no format-less
|
||||
// image uniform, which is all but a handful.
|
||||
Uint64 signature = 0;
|
||||
// Array uniforms whose elements resolved to units holding DIFFERENT formats: one
|
||||
// declaration carries one qualifier, so there is nothing correct to bake and they
|
||||
// are dropped from the map above. Kept for diagnostics.
|
||||
Vector<String> conflictedNames;
|
||||
// Some format in play - declared or baked - is outside the GLSL ES core image
|
||||
// format set, so the emitted ESSL needs the GL_NV_image_formats directive.
|
||||
Bool needsExtendedImageFormats = false;
|
||||
};
|
||||
ImageFormatBakeInputs CollectImageFormatBakeInputs(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace SamplerImpl {
|
||||
|
||||
@@ -252,7 +252,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
g_resolvedTier =
|
||||
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
|
||||
&g_tierResolution);
|
||||
MGLOG_I("DirectGLES multi-draw: %s", g_tierResolution.c_str());
|
||||
MGLOG_D("DirectGLES multi-draw: %s", g_tierResolution.c_str());
|
||||
}
|
||||
|
||||
// Which tiers have already announced themselves, one bit per GLESMultiDrawMode.
|
||||
@@ -267,7 +267,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
const Uint32 bit = 1u << static_cast<Uint32>(tier);
|
||||
if (g_announcedTiers & bit) return;
|
||||
g_announcedTiers |= bit;
|
||||
MGLOG_I("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
|
||||
MGLOG_D("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
|
||||
}
|
||||
|
||||
// The tier this particular batch can actually take. A tier is demoted here when
|
||||
@@ -490,7 +490,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
|
||||
subDrawCount, indexSize);
|
||||
if (!source) {
|
||||
MGLOG_E("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
|
||||
"buffer; skipping the batch",
|
||||
i);
|
||||
return false;
|
||||
@@ -596,7 +596,7 @@ void main() {
|
||||
|
||||
const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER);
|
||||
if (shader == 0) {
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
|
||||
return false;
|
||||
}
|
||||
const char* source = kFlattenComputeSource;
|
||||
@@ -607,14 +607,14 @@ void main() {
|
||||
if (status != GL_TRUE) {
|
||||
char log[1024] = {};
|
||||
g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
|
||||
g_GLESFuncs.glDeleteShader(shader);
|
||||
return false;
|
||||
}
|
||||
|
||||
const GLuint program = g_GLESFuncs.glCreateProgram();
|
||||
if (program == 0) {
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateProgram failed");
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateProgram failed");
|
||||
g_GLESFuncs.glDeleteShader(shader);
|
||||
return false;
|
||||
}
|
||||
@@ -625,7 +625,7 @@ void main() {
|
||||
if (status != GL_TRUE) {
|
||||
char log[1024] = {};
|
||||
g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
|
||||
g_GLESFuncs.glDeleteProgram(program);
|
||||
return false;
|
||||
}
|
||||
@@ -635,7 +635,7 @@ void main() {
|
||||
g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount");
|
||||
g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices");
|
||||
g_computeProgramFailed = false;
|
||||
MGLOG_I("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
|
||||
MGLOG_D("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -920,7 +920,7 @@ void main() {
|
||||
feedBaseVertex);
|
||||
}
|
||||
if (!drawn) {
|
||||
MGLOG_E("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
|
||||
"the batch was dropped",
|
||||
drawcount, mode, type);
|
||||
}
|
||||
|
||||
@@ -417,10 +417,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
result = std::regex_replace(result, pattern, "$1flat $2");
|
||||
};
|
||||
|
||||
// Every stage that has an integer interface at all, on BOTH sides. Interpolation is
|
||||
// only ever consumed at a fragment input, so the qualifier is semantically inert on
|
||||
// a tessellation or geometry interface - but an ES linker still compares the two
|
||||
// sides of every interface and rejects a program whose producer says `flat` and
|
||||
// whose consumer does not. Covering only the stages that "need" it left exactly two
|
||||
// holes, and a program that used tessellation fell into both:
|
||||
// vertex `flat out uint` -> tess-control `in uint` (producer flat, consumer not)
|
||||
// tess-eval `out uint` -> geometry `flat in uint` (consumer flat, producer not)
|
||||
// Adreno answers "output ... interpolation mismatch with other stage" and the whole
|
||||
// program fails to link, which is a draw that silently paints nothing.
|
||||
//
|
||||
// Adding rather than stripping, because a fragment input's `flat` is load-bearing
|
||||
// (ESSL forbids an interpolated integer) and would have to be put back for the last
|
||||
// stage before the fragment shader anyway - so "everything integer is flat" is the
|
||||
// one rule that is consistent no matter which stages a program happens to have.
|
||||
switch (shaderType) {
|
||||
case GL_VERTEX_SHADER:
|
||||
addFlatQualifier("out");
|
||||
break;
|
||||
case GL_TESS_CONTROL_SHADER:
|
||||
case GL_TESS_EVALUATION_SHADER:
|
||||
case GL_GEOMETRY_SHADER:
|
||||
addFlatQualifier("in");
|
||||
addFlatQualifier("out");
|
||||
@@ -518,6 +535,92 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
String RequestExtendedImageFormats(String glslCode, Bool needed) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// GLSL ES core has thirteen image formats; GL has forty. SPIRV-Cross prints whatever
|
||||
// format the OpTypeImage carries and asks for no extension for it, so an r8ui or
|
||||
// rg16f image - declared as such, or baked from the bound one - reaches the driver as
|
||||
// a format its core language does not know. GL_NV_image_formats is the only thing
|
||||
// that adds them, and it has to be requested by name.
|
||||
//
|
||||
// The caller decides `needed`: it knows which formats are in play (from the uniform
|
||||
// reflection and the image-unit bindings) and whether the driver advertises the
|
||||
// extension at all - `#extension` on an unadvertised name is itself a hard error, so
|
||||
// this must never be emitted speculatively.
|
||||
static constexpr const char* kDirective = "#extension GL_NV_image_formats : require\n";
|
||||
static constexpr const char* kExtName = "GL_NV_image_formats";
|
||||
if (!needed || glslCode.find(kExtName) != String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
// After the #version line, which must stay first. Everything else about the header is
|
||||
// order-insensitive, and ForceSupporterOutput's scan for the LAST #extension
|
||||
// directive still finds whichever one that is.
|
||||
const SizeT versionPos = glslCode.find("#version");
|
||||
if (versionPos == String::npos) {
|
||||
return kDirective + glslCode;
|
||||
}
|
||||
const SizeT lineEnd = glslCode.find('\n', versionPos);
|
||||
if (lineEnd == String::npos) {
|
||||
return glslCode + "\n" + kDirective;
|
||||
}
|
||||
glslCode.insert(lineEnd + 1, kDirective);
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
String BakeImageFormatQualifiers(String glslCode,
|
||||
const UnorderedMap<String, String>& esslFormatByUniformName) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (esslFormatByUniformName.empty() || glslCode.find("image") == String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
// Same declaration shape RebindImageUniformsToFrontendUnits matches, and for the same
|
||||
// reason: one line, one image uniform, the name in group 3.
|
||||
static const std::regex imageDeclRegex(
|
||||
R"((layout\s*\(([^)]*)\)\s*)?uniform\s+(?:(?:readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*[iu]?image[A-Za-z0-9]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*(\[[^\]]*\])?\s*;)");
|
||||
// Every image format spelling GLSL has, so a declaration that already carries one is
|
||||
// recognised whatever it says - the caller's map is consulted only for declarations
|
||||
// with NO format, never to override a written one.
|
||||
static const std::regex existingFormatRegex(
|
||||
R"(\b(rgba32f|rgba16f|rg32f|rg16f|r11f_g11f_b10f|r32f|r16f|rgba16|rgb10_a2|rg16|rg8|r16|r8|rgba16_snorm|rgba8_snorm|rg16_snorm|rg8_snorm|r16_snorm|r8_snorm|rgba32i|rgba16i|rgba8i|rg32i|rg16i|rg8i|r32i|r16i|r8i|rgba32ui|rgba16ui|rgba8ui|rgb10_a2ui|rg32ui|rg16ui|rg8ui|r32ui|r16ui|r8ui)\b)");
|
||||
|
||||
String result;
|
||||
result.reserve(glslCode.size());
|
||||
SizeT lineStart = 0;
|
||||
while (lineStart <= glslCode.size()) {
|
||||
const SizeT lineEnd = glslCode.find('\n', lineStart);
|
||||
const Bool lastLine = lineEnd == String::npos;
|
||||
String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
|
||||
|
||||
std::smatch match;
|
||||
if (std::regex_search(line, match, imageDeclRegex)) {
|
||||
const String name = match[3].str();
|
||||
const auto formatIt = esslFormatByUniformName.find(name);
|
||||
const String layoutContents = match[2].matched ? match[2].str() : String();
|
||||
if (formatIt != esslFormatByUniformName.end() && !formatIt->second.empty() &&
|
||||
!std::regex_search(layoutContents, existingFormatRegex)) {
|
||||
if (match[1].matched) {
|
||||
const SizeT layoutOpen = line.find('(', match.position(1));
|
||||
line.insert(layoutOpen + 1, formatIt->second + ", ");
|
||||
} else {
|
||||
line.insert(match.position(0), "layout(" + formatIt->second + ") ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result += line;
|
||||
if (lastLine) {
|
||||
break;
|
||||
}
|
||||
result += '\n';
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
String RemoveLayoutBinding(const String& glslCode) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
@@ -1073,7 +1176,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
for (GLenum err = g_GLESFuncs.glGetError(); err != GL_NO_ERROR; err = g_GLESFuncs.glGetError()) {
|
||||
MGLOG_E("-> GLES Error: %s", MG_Util::ConvertGLEnumToString(err).c_str());
|
||||
MGLOG_D("-> GLES Error: %s", MG_Util::ConvertGLEnumToString(err).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1493,88 +1596,71 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return (rowBytes + align - 1) / align * align;
|
||||
}
|
||||
|
||||
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
|
||||
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
|
||||
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
|
||||
// 4 components x GetReadbackComponentSize(wideType) bytes each.
|
||||
// Walks the client-side destination the PACK parameters describe and hands each row to
|
||||
// `fillRow(slice, row, dstRow)`, which writes width * dstPixelBytes bytes of finished client
|
||||
// texels. Shared by the converting and the raw-word stores so both address the destination -
|
||||
// and feed the bound pixel-pack buffer - identically.
|
||||
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
|
||||
// of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4).
|
||||
// Per the GL addressing rules, slice k row j lands at
|
||||
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
|
||||
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
|
||||
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||
void* pixels, Bool applyPackImageParams) {
|
||||
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
|
||||
if (dstPixelBytes == 0) {
|
||||
return false;
|
||||
}
|
||||
PackedReadbackLayout packedLayout{};
|
||||
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
|
||||
const SizeT dstComponentSize = GetReadbackComponentSize(type);
|
||||
template <typename FillRow>
|
||||
static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) {
|
||||
const auto& pixelPackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
|
||||
const auto& pixelPackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
|
||||
// rows are written so skip regions of the destination stay untouched.
|
||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
||||
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
||||
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
||||
const SizeT imageRows =
|
||||
applyPackImageParams && packParams.ImageHeight > 0
|
||||
? static_cast<SizeT>(packParams.ImageHeight)
|
||||
: static_cast<SizeT>(sliceHeight);
|
||||
const SizeT dstImageStride = imageRows * dstRowStride;
|
||||
const SizeT skipImages =
|
||||
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
|
||||
const SizeT dstSkipOffset = skipImages * dstImageStride +
|
||||
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
|
||||
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
|
||||
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
|
||||
|
||||
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
|
||||
// rows are written so skip regions of the destination stay untouched.
|
||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
||||
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
||||
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
||||
const SizeT imageRows =
|
||||
applyPackImageParams && packParams.ImageHeight > 0
|
||||
? static_cast<SizeT>(packParams.ImageHeight)
|
||||
: static_cast<SizeT>(sliceHeight);
|
||||
const SizeT dstImageStride = imageRows * dstRowStride;
|
||||
const SizeT skipImages =
|
||||
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
|
||||
const SizeT dstSkipOffset = skipImages * dstImageStride +
|
||||
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
|
||||
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
|
||||
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
|
||||
|
||||
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
|
||||
if (pixelPackBufferObject) {
|
||||
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
|
||||
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
|
||||
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
|
||||
if (requiredSize > pixelPackBufferObject->GetSize()) {
|
||||
MGLOG_E("Readback conversion: pixel pack buffer is too small");
|
||||
return true;
|
||||
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
|
||||
if (pixelPackBufferObject) {
|
||||
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
|
||||
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
|
||||
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
|
||||
if (requiredSize > pixelPackBufferObject->GetSize()) {
|
||||
MGLOG_E_ONCE("Readback conversion: pixel pack buffer is too small");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SizeT srcComponentSize = GetReadbackComponentSize(wideType);
|
||||
const SizeT srcPixelBytes = 4 * srcComponentSize;
|
||||
Vector<Uint8> convertedRow(dstRowBytes);
|
||||
Vector<Uint8> convertedRow(dstRowBytes);
|
||||
|
||||
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
|
||||
for (GLsizei row = 0; row < sliceHeight; ++row) {
|
||||
const SizeT flatRow = static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
|
||||
static_cast<SizeT>(row);
|
||||
const Uint8* srcRow = wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
|
||||
ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast<SizeT>(width), wideType,
|
||||
mapping, type);
|
||||
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
|
||||
for (GLsizei row = 0; row < sliceHeight; ++row) {
|
||||
fillRow(slice, row, convertedRow.data());
|
||||
|
||||
if (packParams.SwapBytes) {
|
||||
const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize;
|
||||
if (groupSize > 1) {
|
||||
for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) {
|
||||
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize);
|
||||
if (packParams.SwapBytes && swapGroupSize > 1) {
|
||||
for (SizeT offset = 0; offset + swapGroupSize <= dstRowBytes; offset += swapGroupSize) {
|
||||
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + swapGroupSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
|
||||
static_cast<SizeT>(row) * dstRowStride;
|
||||
if (pixelPackBufferObject) {
|
||||
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
|
||||
pboBaseOffset + dstOffset);
|
||||
} else {
|
||||
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
|
||||
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
|
||||
static_cast<SizeT>(row) * dstRowStride;
|
||||
if (pixelPackBufferObject) {
|
||||
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
|
||||
pboBaseOffset + dstOffset);
|
||||
} else {
|
||||
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pixelPackBufferObject) {
|
||||
// WritebackFromBackend bumps change serials with no backend op; re-open
|
||||
// the buffer draw-clean memos (once for the whole row loop).
|
||||
@@ -1582,5 +1668,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
|
||||
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
|
||||
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
|
||||
// 4 components x GetReadbackComponentSize(wideType) bytes each.
|
||||
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||
void* pixels, Bool applyPackImageParams) {
|
||||
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
|
||||
if (dstPixelBytes == 0) {
|
||||
return false;
|
||||
}
|
||||
PackedReadbackLayout packedLayout{};
|
||||
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
|
||||
const SizeT swapGroupSize = isPackedType ? packedLayout.byteSize : GetReadbackComponentSize(type);
|
||||
const SizeT srcPixelBytes = 4 * GetReadbackComponentSize(wideType);
|
||||
|
||||
return StoreClientRows(dstPixelBytes, swapGroupSize, width, sliceHeight, sliceCount, pixels,
|
||||
applyPackImageParams,
|
||||
[&](GLsizei slice, GLsizei row, Uint8* dstRow) {
|
||||
const SizeT flatRow = static_cast<SizeT>(slice) *
|
||||
static_cast<SizeT>(sliceHeight) +
|
||||
static_cast<SizeT>(row);
|
||||
const Uint8* srcRow =
|
||||
wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
|
||||
ConvertWideReadbackRow(srcRow, dstRow, static_cast<SizeT>(width), wideType,
|
||||
mapping, type);
|
||||
});
|
||||
}
|
||||
|
||||
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
|
||||
GLenum type, void* pixels, Bool applyPackImageParams) {
|
||||
PackedReadbackLayout packedLayout{};
|
||||
if (!GetPackedReadbackLayout(type, packedLayout) || packedLayout.byteSize != 4) {
|
||||
return false;
|
||||
}
|
||||
const SizeT srcRowBytes = static_cast<SizeT>(width) * 4;
|
||||
|
||||
return StoreClientRows(4, packedLayout.byteSize, width, sliceHeight, sliceCount, pixels,
|
||||
applyPackImageParams,
|
||||
[&](GLsizei slice, GLsizei row, Uint8* dstRow) {
|
||||
const SizeT flatRow = static_cast<SizeT>(slice) *
|
||||
static_cast<SizeT>(sliceHeight) +
|
||||
static_cast<SizeT>(row);
|
||||
Memcpy(dstRow, srcWords + flatRow * srcRowBytes, srcRowBytes);
|
||||
});
|
||||
}
|
||||
} // namespace ReadbackImpl
|
||||
} // namespace MobileGL::MG_Backend::DirectGLES
|
||||
|
||||
@@ -115,6 +115,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||
void* pixels, Bool applyPackImageParams);
|
||||
|
||||
// Stores packed 32-bit source words verbatim, with the same destination addressing, PACK
|
||||
// parameters and pixel-pack-buffer handling as StoreWideRowsToClient. For the sources whose
|
||||
// storage word already IS the client word (MG_Util::IsRawPackedPixelTransfer): routing those
|
||||
// through the wide float intermediate re-encodes them, and the RGB9_E5 encoder canonicalizes
|
||||
// the shared exponent, so glGetTexImage would answer with different bits than were stored.
|
||||
// `srcWords` holds sliceHeight * sliceCount tightly stacked rows of `width` 32-bit words.
|
||||
// False when `type` is not a 4-byte packed type.
|
||||
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
|
||||
GLenum type, void* pixels, Bool applyPackImageParams);
|
||||
} // namespace ReadbackImpl
|
||||
|
||||
namespace PrgramImpl {
|
||||
@@ -137,6 +147,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// ES 3.2 needs no directive at all and an EXT driver already has the right one.
|
||||
String RetargetTextureBufferExtension(String glslCode,
|
||||
MG_External::GLESCapabilities::TextureBufferTier tier);
|
||||
// Adds `#extension GL_NV_image_formats : require` when the shader carries an image
|
||||
// format qualifier GLSL ES has no core spelling for. SPIRV-Cross prints the format and
|
||||
// asks for nothing, so the request has to be made here. `needed` is the caller's answer,
|
||||
// because only it knows which formats are in play AND whether the driver advertises the
|
||||
// extension - requesting an unadvertised extension is itself a compile error, so this is
|
||||
// never emitted speculatively. A no-op when not needed or already present.
|
||||
String RequestExtendedImageFormats(String glslCode, Bool needed);
|
||||
// Writes a format layout qualifier into the image declarations named in
|
||||
// `esslFormatByUniformName` that still have none. The completion half of the image-format
|
||||
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
|
||||
// format in, but SPIRV-Cross throws rather than printing the formats it calls
|
||||
// desktop-only when it targets ESSL - r8ui among them, which is what the stencil half of
|
||||
// KHR-GL4x.packed_depth_stencil.stencil_texturing binds - and a throw loses the whole
|
||||
// stage. So those formats stay out of the module and are spelled here instead, on the
|
||||
// emitted text, where nothing can refuse them.
|
||||
//
|
||||
// Declarations that already carry a format are left exactly as they are, whoever wrote
|
||||
// it. Must run before RemoveLayoutBinding, which is where an image's layout qualifier
|
||||
// stops being safe to edit by hand.
|
||||
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
|
||||
String RemoveLayoutBinding(const String& glslCode);
|
||||
// Prefix of the writeonly half a read+write image uniform is split into (see
|
||||
// SplitReadWriteImageUniforms); the suffix is the image's own name.
|
||||
|
||||
@@ -497,30 +497,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.ExtraVendor = Nullopt,
|
||||
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
|
||||
.TargetGLSLVersion = {4, 6, 0},
|
||||
// Baseline advertisement (no shader subgroup, no timer queries); a
|
||||
// live backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false),
|
||||
// Baseline advertisement (no runtime-gated capabilities); a live
|
||||
// backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||
.IsCompatibilityProfile = false},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
||||
return rendererInfo;
|
||||
}
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported) {
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_multi_draw_indirect,
|
||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_draw_indirect,
|
||||
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_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,
|
||||
// Core since GL 3.1 and implemented for every version advertised here. The string
|
||||
// matters because applications gate the ENTRY POINTS on it rather than on the
|
||||
// version: a caller that finds the extension missing never resolves
|
||||
// glGetUniformBlockIndex / glUniformBlockBinding, and one that then uses uniform
|
||||
// blocks anyway calls through a null pointer.
|
||||
E_GL_ARB_uniform_buffer_object,
|
||||
// Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3,
|
||||
// so on a 4.0 context the string is the only way to reach it.
|
||||
E_GL_ARB_stencil_texturing,
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
E_GL_ARB_get_program_binary};
|
||||
// Vulkan's drawIndirectFirstInstance feature is optional. Direct base-instance calls work
|
||||
// without it, but ARB_base_instance also promises non-zero firstInstance in GPU indirect
|
||||
// commands; the renderer supplies true only when that word is legal and gl_InstanceID can
|
||||
// be rebased to OpenGL's zero-based semantics.
|
||||
if (nonZeroIndirectBaseInstanceSupported) {
|
||||
extensions.push_back(E_GL_ARB_base_instance);
|
||||
}
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||
}
|
||||
@@ -681,7 +699,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// the whole list keeps re-runs idempotent.
|
||||
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported());
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
||||
|
||||
@@ -62,8 +62,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// POST screen shows.
|
||||
|
||||
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
|
||||
// versions, ExtraVendor) with the baseline extension advertisement (no shader
|
||||
// subgroup, no timer queries). A live backend copies this in its constructor and
|
||||
// versions, ExtraVendor) with the baseline extension advertisement (no runtime-gated
|
||||
// capabilities). A live backend copies this in its constructor and
|
||||
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
|
||||
// exist; callers that need the advertised list for a known capability set must
|
||||
// use BuildAdvertisedExtensions instead.
|
||||
@@ -74,7 +74,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
|
||||
// the detected device support (passing an already-gated value is harmless).
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported);
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported);
|
||||
|
||||
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
||||
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
||||
|
||||
@@ -269,14 +269,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
drawBuffer->SyncPersistentMappedRange();
|
||||
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||
if (drawBuffer->MappedData() == nullptr || commandOffset + requiredBytes > drawBuffer->GetSize()) {
|
||||
MGLOG_E("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
|
||||
MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
|
||||
return nullptr;
|
||||
}
|
||||
return drawBuffer->MappedData() + commandOffset;
|
||||
}
|
||||
|
||||
if (!indirect) {
|
||||
MGLOG_E("%s skipped: indirect pointer is null", label);
|
||||
MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -398,7 +398,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
stride = sizeof(DrawArraysIndirectCommand);
|
||||
}
|
||||
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
||||
MGLOG_E("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
|
||||
stride, sizeof(DrawArraysIndirectCommand));
|
||||
return;
|
||||
}
|
||||
@@ -446,20 +446,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
stride = sizeof(DrawArraysIndirectCommand);
|
||||
}
|
||||
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
|
||||
stride, sizeof(DrawArraysIndirectCommand));
|
||||
return;
|
||||
}
|
||||
|
||||
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
if (!parameterBuffer || drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
||||
return;
|
||||
}
|
||||
|
||||
parameterBuffer->SyncPersistentMappedRange();
|
||||
if (parameterBuffer->MappedData() == nullptr) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read parameter buffer");
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read parameter buffer");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -513,7 +513,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0) {
|
||||
MGLOG_E("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
|
||||
MGLOG_E_ONCE("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1009,7 +1009,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// shift - the hardware divide was the hottest instruction of this loop.
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0) {
|
||||
MGLOG_E("MultiDrawElements skipped: unsupported index type 0x%x", type);
|
||||
MGLOG_E_ONCE("MultiDrawElements skipped: unsupported index type 0x%x", type);
|
||||
return;
|
||||
}
|
||||
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
|
||||
|
||||
@@ -205,7 +205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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");
|
||||
MGLOG_E_ONCE("TransitionToPresent: command buffer already closed; skipping the present barrier");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -194,53 +194,54 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
PipelineFactory::HashType PipelineFactory::ComputeHash(const PipelineCreatePayload& payload) const {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.programHash, sizeof(payload.programHash)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), m_config.CacheVersion));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.programHash, sizeof(payload.programHash)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.renderPass, sizeof(payload.renderPass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.subpass, sizeof(payload.subpass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.topology, sizeof(payload.topology)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
||||
XXH64_update(m_hashState.Get(), &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.viewportCount, sizeof(payload.viewportCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.cullMode, sizeof(payload.cullMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.frontFace, sizeof(payload.frontFace)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
|
||||
XXH64_update(m_hashState.Get(), &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOpEnable, sizeof(payload.logicOpEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.stencilTestEnable, sizeof(payload.stencilTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthCompareOp, sizeof(payload.depthCompareOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOp, sizeof(payload.logicOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp)));
|
||||
XXH64_update(m_hashState.Get(), &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.logicOpEnable, sizeof(payload.logicOpEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.stencilTestEnable, sizeof(payload.stencilTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.depthCompareOp, sizeof(payload.depthCompareOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.logicOp, sizeof(payload.logicOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp)));
|
||||
XXH64_update(m_hashState.Get(), &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilFailOp, sizeof(payload.backStencilFailOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilPassOp, sizeof(payload.backStencilPassOp)));
|
||||
XXH64_update(m_hashState.Get(), &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.backStencilFailOp, sizeof(payload.backStencilFailOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.backStencilPassOp, sizeof(payload.backStencilPassOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
|
||||
XXH64_update(m_hashState.Get(), &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
|
||||
XXH64_update(m_hashState.Get(), &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
|
||||
XXH64_update(m_hashState.Get(), &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
|
||||
if (payload.colorAttachmentCount > 0) {
|
||||
XXHASH_VERIFY(XXH64_update(
|
||||
m_hashState,
|
||||
m_hashState.Get(),
|
||||
payload.colorBlendAttachments.data(),
|
||||
sizeof(payload.colorBlendAttachments[0]) * payload.colorAttachmentCount));
|
||||
}
|
||||
return XXH64_digest(m_hashState);
|
||||
return XXH64_digest(m_hashState.Get());
|
||||
}
|
||||
|
||||
VkPipeline PipelineFactory::GetOrCreatePipeline(const PipelineCreatePayload& payload) {
|
||||
@@ -259,7 +260,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// is the correct price for a broken pipeline and is bounded by the draw itself being
|
||||
// skipped.
|
||||
if (pipeline == VK_NULL_HANDLE) {
|
||||
MGLOG_I("PipelineFactory::GetOrCreatePipeline: creation failed for hash=0x%llx "
|
||||
// Unlatched, like the CreatePipeline report it accompanies: a pipeline MobileGL
|
||||
// assembled and the driver refused is a broken invariant, not an expected failure,
|
||||
// so it stays loud for as long as it is reachable. Raised from MGLOG_I once the
|
||||
// Log.h ordering fix made MGLOG_E live in INFO builds.
|
||||
MGLOG_E("PipelineFactory::GetOrCreatePipeline: creation failed for hash=0x%llx "
|
||||
"programHash=0x%llx; not caching the failure",
|
||||
static_cast<unsigned long long>(hash),
|
||||
static_cast<unsigned long long>(payload.programHash));
|
||||
@@ -402,8 +407,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
tessellation.patchControlPoints = payload.patchControlPoints;
|
||||
|
||||
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||
vpci.viewportCount = 1;
|
||||
vpci.scissorCount = 1;
|
||||
// Both counts move together: GL has one scissor rectangle per viewport, and Vulkan
|
||||
// requires viewportCount == scissorCount whenever both are dynamic
|
||||
// (VUID-VkPipelineViewportStateCreateInfo-scissorCount-04136). The caller has already
|
||||
// clamped this to the device's multiViewport capability.
|
||||
vpci.viewportCount = std::max<Uint32>(payload.viewportCount, 1u);
|
||||
vpci.scissorCount = vpci.viewportCount;
|
||||
|
||||
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||
raster.polygonMode = payload.polygonMode;
|
||||
@@ -471,9 +480,56 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
blend.attachmentCount = payload.colorAttachmentCount;
|
||||
blend.pAttachments = colorAttachments.empty() ? nullptr : colorAttachments.data();
|
||||
|
||||
// A GL program may have a tessellation EVALUATION stage and no CONTROL stage: GL 4.6 core
|
||||
// 11.2.2 gives it a fixed-function pass-through instead. Vulkan has no such stage, and
|
||||
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 requires both tessellation stages or
|
||||
// neither - so the renderer synthesizes the pass-through GL describes and hands it in
|
||||
// here (see ProgramFactory::GetOrCreatePassthroughTessControlStage).
|
||||
//
|
||||
// The refusal below is what keeps the half-tessellated shape away from the driver when
|
||||
// there is no synthesized stage to add - because Mali does not reject it, it dereferences
|
||||
// null INSIDE vkCreateGraphicsPipelines and takes the process down (SIGSEGV, fault addr
|
||||
// 0x34, on Mali-G715/r54p2 and Mali-G925/r49p1 alike; Adreno and lavapipe merely render
|
||||
// wrong). Returning VK_NULL_HANDLE routes this through the same path a driver rejection
|
||||
// takes: the draw is skipped, nothing is memoised, and the process survives.
|
||||
const Vector<VkPipelineShaderStageCreateInfo>* effectiveStages = payload.stages;
|
||||
Vector<VkPipelineShaderStageCreateInfo> stagesWithPassthrough;
|
||||
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) {
|
||||
stagesWithPassthrough = *payload.stages;
|
||||
stagesWithPassthrough.push_back(payload.passthroughTessControlStage);
|
||||
effectiveStages = &stagesWithPassthrough;
|
||||
}
|
||||
{
|
||||
VkShaderStageFlags stagesPresent = 0;
|
||||
for (const auto& stageInfo : *effectiveStages) {
|
||||
stagesPresent |= stageInfo.stage;
|
||||
}
|
||||
const Bool hasTessControl = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT) != 0;
|
||||
const Bool hasTessEval = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT) != 0;
|
||||
if (hasTessControl != hasTessEval) {
|
||||
// Latched, and the latch is the point: a failed creation is deliberately never
|
||||
// memoised (see GetOrCreatePipeline), so a program in this state re-enters here
|
||||
// once per draw, every frame - and a refusal diagnostic that repeats per draw is
|
||||
// noise, not a diagnostic. One line names the program; the draws it explains are
|
||||
// all the same draw.
|
||||
static Bool s_warnedHalfTessellatedPipeline = false;
|
||||
if (!s_warnedHalfTessellatedPipeline) {
|
||||
s_warnedHalfTessellatedPipeline = true;
|
||||
MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with %s tessellation stage and "
|
||||
"no %s stage (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). programHash=0x%llx "
|
||||
"patchControlPoints=%u. Its draws are skipped; logged once.",
|
||||
hasTessEval ? "an evaluation" : "a control",
|
||||
hasTessEval ? "control" : "evaluation",
|
||||
static_cast<unsigned long long>(payload.programHash),
|
||||
payload.patchControlPoints);
|
||||
}
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
|
||||
VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
||||
gpi.stageCount = static_cast<Uint32>(payload.stages->size());
|
||||
gpi.pStages = payload.stages->data();
|
||||
gpi.stageCount = static_cast<Uint32>(effectiveStages->size());
|
||||
gpi.pStages = effectiveStages->data();
|
||||
gpi.pVertexInputState = payload.vertexInputState;
|
||||
gpi.pInputAssemblyState = &ia;
|
||||
gpi.pTessellationState =
|
||||
@@ -490,6 +546,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
const VkResult result = vkCreateGraphicsPipelines(m_device, m_pipelineCache, 1, &gpi, nullptr, &pipeline);
|
||||
// Loud, at MGLOG_F, and deliberately NOT latched. vkCreateGraphicsPipelines refusing a
|
||||
// pipeline MobileGL assembled is a should-never-happen state, and the driver's own
|
||||
// answer is VK_ERROR_UNKNOWN - no information at all - so this dump is the entire
|
||||
// diagnosis. It is not an expected failure mode, so the one-shot rule that quiets W/E
|
||||
// does not apply: while this is reachable it should keep saying so on every draw.
|
||||
// GetOrCreatePipeline deliberately does not cache the failure, which is what makes that
|
||||
// repetition happen; if the repetition ever needs to stop, fix the pipeline, not the log.
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_F("PipelineFactory::CreatePipeline failed: result=%s (%d) programHash=0x%llx vertexInputHash=0x%llx stageCount=%u topology=%s(%d) colorAttachmentCount=%u samples=%s(%d) subpass=%u",
|
||||
VkResultToString(result),
|
||||
@@ -522,8 +585,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
payload.vertexInputState->vertexAttributeDescriptionCount);
|
||||
// The driver's own answer is VK_ERROR_UNKNOWN, i.e. no information at all, so the only
|
||||
// way to work out WHICH shader it choked on (the open sampler-array-in-struct
|
||||
// investigation) is to name the modules. MGLOG_I, not _D/_E: this must survive in the
|
||||
// INFO-level builds that CTS actually runs against.
|
||||
// investigation) is to name the modules. MGLOG_I, not _D: this is part of a
|
||||
// should-never-happen report and must survive in the INFO-level builds that CTS
|
||||
// actually runs against, alongside the MGLOG_F lines above.
|
||||
if (payload.stageSpirvDigests) {
|
||||
for (SizeT i = 0; i < payload.stageSpirvDigests->size(); ++i) {
|
||||
const auto& digest = (*payload.stageSpirvDigests)[i];
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "../VkIncludes.h"
|
||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Types.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Enough of a fingerprint to identify the exact module the driver rejected without keeping the
|
||||
@@ -42,6 +43,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool primitiveRestartEnable = false;
|
||||
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
||||
Uint32 patchControlPoints = 3;
|
||||
// How many of ARB_viewport_array's viewports this pipeline rasterizes into. 1 for
|
||||
// every program that never assigns gl_ViewportIndex, which is all of them outside the
|
||||
// conformance suite - the wide shape costs a longer vkCmdSetViewport/Scissor per state
|
||||
// change and can cost hardware fast paths, so it is opt-in per program. Baked into the
|
||||
// pipeline (viewportCount is not dynamic without VK_EXT_extended_dynamic_state) and
|
||||
// therefore hashed; the DYNAMIC viewport/scissor arrays the draw pushes must have
|
||||
// exactly this many elements (VUID-vkCmdDraw-viewportCount-03417/-03418).
|
||||
Uint32 viewportCount = 1;
|
||||
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
|
||||
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
||||
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
||||
@@ -71,6 +80,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool fragmentReplacesDepth = false;
|
||||
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
||||
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
||||
// The tessellation control stage this renderer synthesized for a program that has
|
||||
// an evaluation stage and none of its own (GL 4.6 core 11.2.2 gives such a program a
|
||||
// fixed-function pass-through; Vulkan has no such thing and
|
||||
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 forbids the half-tessellated
|
||||
// pipeline outright). Appended to `stages` at creation. A null module means the
|
||||
// renderer could not build one, and CreatePipeline refuses the pipeline - the same
|
||||
// refusal it applies when `stages` itself is half-tessellated.
|
||||
//
|
||||
// NOT hashed: it is a pure function of the program and of patchControlPoints, both
|
||||
// of which ComputeHash already mixes in.
|
||||
VkPipelineShaderStageCreateInfo passthroughTessControlStage{};
|
||||
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
||||
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
|
||||
const Vector<ShaderStageSpirvDigest>* stageSpirvDigests = nullptr;
|
||||
@@ -146,7 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
UnorderedMap<HashType, PipelineCacheEntry> m_cache;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameCounter = 0;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline MobileGL::XXH64State m_hashState;
|
||||
static inline Bool s_suppressBlendedDepthWrite = false;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -376,12 +376,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
spv_diagnostic diagnostic = nullptr;
|
||||
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
|
||||
if (result != SPV_SUCCESS) {
|
||||
// MGLOG_I, not E: at the INFO compile level of the CI/test lanes that arm
|
||||
// the validation switch, MGLOG_E is compiled out (Log.h orders
|
||||
// DEBUG < WARN < ERROR < INFO) and the VUID would never reach a log. The
|
||||
// latch is what a test harness asserts on.
|
||||
// MGLOG_E, unlatched: reaching here already requires the validation switch to
|
||||
// be armed, which bounds the volume, and each VUID names a different defect.
|
||||
// (Parked at MGLOG_I until the Log.h level ordering was fixed, when E was
|
||||
// compiled out of every INFO build.) The latch is what a test harness asserts on.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
|
||||
MGLOG_I(
|
||||
MGLOG_E(
|
||||
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s",
|
||||
static_cast<Int>(shaderStage),
|
||||
programExternalIndex,
|
||||
@@ -1266,7 +1266,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (SizeT i = 1; i < group.offsets.size(); ++i) {
|
||||
if (group.elementBytes == 0 ||
|
||||
group.offsets[i] != group.offsets[i - 1] + group.elementBytes) {
|
||||
MGLOG_I("XfbCaptureDecoratePass: block member %u of type %%%u is captured with a "
|
||||
MGLOG_D("XfbCaptureDecoratePass: block member %u of type %%%u is captured with a "
|
||||
"non-contiguous element set; the capture layout will differ from GL's",
|
||||
key.second, key.first);
|
||||
break;
|
||||
@@ -1721,6 +1721,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return ProgramFactory::DescriptorBindingKind::CombinedImageSampler;
|
||||
case SPV_REFLECT_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER:
|
||||
return ProgramFactory::DescriptorBindingKind::UniformTexelBuffer;
|
||||
case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER:
|
||||
return ProgramFactory::DescriptorBindingKind::StorageTexelBuffer;
|
||||
case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_BUFFER:
|
||||
return ProgramFactory::DescriptorBindingKind::StorageBuffer;
|
||||
case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_IMAGE:
|
||||
@@ -1750,6 +1752,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
if (kind == ProgramFactory::DescriptorBindingKind::CombinedImageSampler ||
|
||||
kind == ProgramFactory::DescriptorBindingKind::UniformTexelBuffer ||
|
||||
kind == ProgramFactory::DescriptorBindingKind::StorageTexelBuffer ||
|
||||
kind == ProgramFactory::DescriptorBindingKind::StorageImage) {
|
||||
const auto arraySuffix = name.find("[0]");
|
||||
if (arraySuffix != String::npos) {
|
||||
@@ -1839,8 +1842,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// UniformManager::BindProgramUniformBuffers: UBO instance arrays
|
||||
// (uniform Block {...} b[N];), storage-block instance arrays, image uniform
|
||||
// arrays, and combined-image-sampler arrays (uniform sampler2D s[N];).
|
||||
// Anything else - a uniform TEXEL buffer array is the one remaining kind -
|
||||
// must fail program creation cleanly rather than continue with corrupt state.
|
||||
// Anything else - the two TEXEL buffer kinds are what remain, samplerBuffer[N]
|
||||
// and imageBuffer[N] - must fail program creation cleanly rather than continue
|
||||
// with corrupt state. Their per-draw path writes pTexelBufferView as the
|
||||
// address of a vector element sized for one descriptor per binding, so an
|
||||
// array would not merely be unresolved, it would dangle.
|
||||
//
|
||||
// Getting listed here is not cosmetic: a kind that is rejected leaves
|
||||
// GetOrCreateProgram's MOBILEGL_ASSERT(remapOk) as the only complaint, and
|
||||
@@ -1849,16 +1855,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// unification and the set->0 normalisation this function exists to do. A
|
||||
// program with an image array plus any second descriptor got aliased
|
||||
// bindings out of that, and a DEBUG build trapped on the same program.
|
||||
// Which is also why the message below is MGLOG_I: MGLOG_E is compiled out
|
||||
// of an INFO build, so a refusal that only said MGLOG_E said nothing at all
|
||||
// in the builds that ship.
|
||||
// The refusal below is MGLOG_E and per-program-compile, so it reports every
|
||||
// program it declines. It spent time at MGLOG_I because the old level
|
||||
// ordering compiled E out of the builds that ship.
|
||||
const Bool arraySupportedForKind =
|
||||
kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic ||
|
||||
kind == ProgramFactory::DescriptorBindingKind::StorageBuffer ||
|
||||
kind == ProgramFactory::DescriptorBindingKind::StorageImage ||
|
||||
kind == ProgramFactory::DescriptorBindingKind::CombinedImageSampler;
|
||||
if (binding->count != 1 && !arraySupportedForKind) {
|
||||
MGLOG_I("ProgramFactory: descriptor arrays are unsupported for this descriptor "
|
||||
MGLOG_E("ProgramFactory: descriptor arrays are unsupported for this descriptor "
|
||||
"kind (name='%s' count=%u type=%d)",
|
||||
binding->name ? binding->name : "<null>", binding->count,
|
||||
static_cast<Int>(binding->descriptor_type));
|
||||
@@ -1991,6 +1997,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInBaseVertex);
|
||||
}
|
||||
|
||||
// gl_ViewportIndex on the last pre-rasterization stage. glslang emits it natively for Vulkan
|
||||
// (BuiltIn ViewportIndex plus OpCapability MultiViewport), and nothing in the SpirvPasses
|
||||
// chain touches it, so a plain reflection of the declared output builtins is the whole test.
|
||||
Bool ProgramFactory::ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule) {
|
||||
return ReflectedDeclaresOutputBuiltin(reflectModule, SpvBuiltInViewportIndex);
|
||||
}
|
||||
|
||||
Bool ProgramFactory::ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule,
|
||||
SpvBuiltIn builtin) {
|
||||
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
|
||||
for (Uint32 variableIndex = 0; variableIndex < entryPoint.output_variable_count; ++variableIndex) {
|
||||
const SpvReflectInterfaceVariable* variable = entryPoint.output_variables[variableIndex];
|
||||
if (variable != nullptr &&
|
||||
(variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 &&
|
||||
variable->built_in == builtin) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ProgramFactory::ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule,
|
||||
SpvBuiltIn builtin) {
|
||||
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||
@@ -2127,26 +2156,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program,
|
||||
CompileOptionFlags flags) const {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), m_config.CacheVersion));
|
||||
// We expect shader stages in program object are sorted
|
||||
const auto& spirvs = program.GetGeneratedSpirv();
|
||||
for (const auto& spv : spirvs) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, spv.data(), spv.size() * sizeof(Uint)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), spv.data(), spv.size() * sizeof(Uint)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &flags, sizeof(CompileOptionFlags)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &flags, sizeof(CompileOptionFlags)));
|
||||
// Only FragCoordYFlip variants bake the height in, so mixing it unconditionally would
|
||||
// re-key every program in the cache on a resize for no reason.
|
||||
if (flags & CompileOptionBit::FragCoordYFlip) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &m_defaultFramebufferHeight,
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &m_defaultFramebufferHeight,
|
||||
sizeof(m_defaultFramebufferHeight)));
|
||||
}
|
||||
|
||||
// Include UBO block bindings in hash so different binding configurations produce different entries
|
||||
const Uint32 blockCount = static_cast<Uint32>(program.GetActiveUniformBlocksCount());
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &blockCount, sizeof(blockCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &blockCount, sizeof(blockCount)));
|
||||
for (Uint32 i = 0; i < blockCount; ++i) {
|
||||
const Uint32 binding = program.GetUniformBlockBinding(i);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding, sizeof(binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &binding, sizeof(binding)));
|
||||
}
|
||||
|
||||
// The transform feedback capture layout is baked into the modules by
|
||||
@@ -2157,18 +2186,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// hashed for a capturing compile, so nothing else changes key.
|
||||
if (flags & CompileOptionBit::XfbCapture) {
|
||||
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, varying.name.data(), varying.name.size()));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &varying.bufferIndex, sizeof(varying.bufferIndex)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &varying.offsetBytes, sizeof(varying.offsetBytes)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), varying.name.data(), varying.name.size()));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &varying.bufferIndex, sizeof(varying.bufferIndex)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &varying.offsetBytes, sizeof(varying.offsetBytes)));
|
||||
}
|
||||
const SizeT bufferCount = program.GetTransformFeedbackBufferCount();
|
||||
for (SizeT i = 0; i < bufferCount; ++i) {
|
||||
const Uint32 stride = program.GetTransformFeedbackStride(static_cast<Uint32>(i));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &stride, sizeof(stride)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &stride, sizeof(stride)));
|
||||
}
|
||||
}
|
||||
|
||||
HashType hash = XXH64_digest(m_hashState);
|
||||
HashType hash = XXH64_digest(m_hashState.Get());
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -2333,6 +2362,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
// Which pre-rasterization stage assigns gl_ViewportIndex is not fixed: GL 4.1 allows only the
|
||||
// geometry stage, ARB_shader_viewport_layer_array/GL 4.6 also the vertex and tessellation
|
||||
// evaluation stages. Rather than guess which one is last, every non-fragment, non-compute
|
||||
// module is asked - one writer anywhere means this program's draws need a multi-viewport
|
||||
// pipeline, and a false positive costs only a wider viewportCount.
|
||||
void ProgramFactory::ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const {
|
||||
entry.writesViewportIndexBuiltin = false;
|
||||
|
||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||
if (!shaders[moduleIndex]) continue;
|
||||
const ShaderStage stage = shaders[moduleIndex]->GetShaderStage();
|
||||
if (stage == ShaderStage::Fragment || stage == ShaderStage::Compute) continue;
|
||||
|
||||
const auto& module = spirv[moduleIndex];
|
||||
if (module.empty()) continue;
|
||||
|
||||
SpvReflectShaderModule reflectModule{};
|
||||
const SpvReflectResult createResult =
|
||||
spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule);
|
||||
if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
|
||||
// Fail toward the wide pipeline. Missing a real gl_ViewportIndex writer would
|
||||
// silently collapse every viewport onto 0 (the exact bug this reflection exists
|
||||
// to fix); over-declaring costs one extra viewport slot on a program that never
|
||||
// uses it.
|
||||
MGLOG_E_ONCE("ProgramFactory::ReflectViewportIndexUsage: reflection failed (result=%d); assuming the "
|
||||
"program writes gl_ViewportIndex",
|
||||
static_cast<Int>(createResult));
|
||||
entry.writesViewportIndexBuiltin = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ReflectedWritesViewportIndexBuiltin(reflectModule)) {
|
||||
entry.writesViewportIndexBuiltin = true;
|
||||
}
|
||||
spvReflectDestroyShaderModule(&reflectModule);
|
||||
}
|
||||
}
|
||||
|
||||
void ProgramFactory::ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const {
|
||||
@@ -2462,7 +2531,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// inert; a device whose binding cap is smaller than a shader's array is not a
|
||||
// configuration MobileGL can serve at all. Needs a >maxBindings-element array to
|
||||
// reach (256 on desktop, ~16 on mobile).
|
||||
MGLOG_I("ProgramFactory::ReflectLayout: %s array '%s' at binding %u has %u elements, past the %u "
|
||||
MGLOG_D("ProgramFactory::ReflectLayout: %s array '%s' at binding %u has %u elements, past the %u "
|
||||
"this device can describe - declining the program",
|
||||
kindLabel, uniformName.c_str(), binding, count, maxBindings);
|
||||
outDeclined = true;
|
||||
@@ -2470,7 +2539,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
if (baseLocation < 0 ||
|
||||
!program.UniformLocationsAliasSameUniform(baseLocation, baseLocation + static_cast<Int>(count - 1u))) {
|
||||
MGLOG_I("ProgramFactory::ReflectLayout: %s array '%s' at binding %u spans %u descriptors but the "
|
||||
MGLOG_D("ProgramFactory::ReflectLayout: %s array '%s' at binding %u spans %u descriptors but the "
|
||||
"reflection reserved fewer uniform locations for it (base=%d) - a multi-dimensional array "
|
||||
"is the usual cause, and MobileGL declines it rather than resolve elements onto a "
|
||||
"neighbouring uniform",
|
||||
@@ -2661,6 +2730,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const auto descriptorKind = ReflectDescriptorTypeToBindingKind(sampler->descriptor_type);
|
||||
if (descriptorKind != DescriptorBindingKind::CombinedImageSampler &&
|
||||
descriptorKind != DescriptorBindingKind::UniformTexelBuffer &&
|
||||
descriptorKind != DescriptorBindingKind::StorageTexelBuffer &&
|
||||
descriptorKind != DescriptorBindingKind::StorageImage &&
|
||||
descriptorKind != DescriptorBindingKind::StorageBuffer) {
|
||||
continue;
|
||||
@@ -2706,7 +2776,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// a Uint16 on the way, where 65536 would silently become 0.
|
||||
const Uint32 storageArrayCount = std::max<Uint32>(1u, sampler->count);
|
||||
if (storageArrayCount > m_maxBindings) {
|
||||
MGLOG_I("ProgramFactory::ReflectLayout: storage block array '%s' at binding %u has %u "
|
||||
MGLOG_D("ProgramFactory::ReflectLayout: storage block array '%s' at binding %u has %u "
|
||||
"elements, past the %u this device can describe - declining the program",
|
||||
uniformName.c_str(), binding, storageArrayCount, m_maxBindings);
|
||||
entry.declinedDescriptors = true;
|
||||
@@ -2729,7 +2799,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// so at a level that survives a release build, because dropping the binding
|
||||
// leaves the shader reading a descriptor the layout never declared.
|
||||
if (sampler->count > 1) {
|
||||
MGLOG_I("ProgramFactory::ReflectLayout: declining '%s' at binding %u - a %u-element "
|
||||
MGLOG_E("ProgramFactory::ReflectLayout: declining '%s' at binding %u - a %u-element "
|
||||
"descriptor array with no frontend uniform location (a multi-dimensional array "
|
||||
"of samplers or images is the known cause)",
|
||||
uniformName.c_str(), binding, sampler->count);
|
||||
@@ -2790,6 +2860,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
if (descriptorKind == DescriptorBindingKind::StorageTexelBuffer) {
|
||||
// Only the declared format is recorded, and only so the per-draw resolve can
|
||||
// prefer it over the one glBindImageTexture named. Everything the StorageImage
|
||||
// branch above does about ARRAYS is deliberately absent: an imageBuffer array
|
||||
// is refused outright by the array gate in RemapDescriptorBindingsForVulkan,
|
||||
// exactly as a samplerBuffer array is, so bindingDescriptorCounts stays at the
|
||||
// default 1 and the descriptor write below may take the address of a vector
|
||||
// element without reserving room for extra elements.
|
||||
const VkFormat reflectedFormat =
|
||||
ConvertSpirvImageFormatToVkFormat(sampler->image.image_format);
|
||||
VkFormat& existingFormat = entry.storageImageFormatByBinding[binding];
|
||||
MOBILEGL_ASSERT(existingFormat == VK_FORMAT_UNDEFINED ||
|
||||
reflectedFormat == VK_FORMAT_UNDEFINED ||
|
||||
existingFormat == reflectedFormat,
|
||||
"ProgramFactory::ReflectLayout: storage texel buffer binding %u ('%s') "
|
||||
"has conflicting reflected formats (%d vs %d)",
|
||||
binding, uniformName.c_str(), static_cast<Int>(existingFormat),
|
||||
static_cast<Int>(reflectedFormat));
|
||||
if (existingFormat == VK_FORMAT_UNDEFINED) {
|
||||
existingFormat = reflectedFormat;
|
||||
}
|
||||
}
|
||||
|
||||
const TextureTarget target = UniformTypeToTextureTarget(uniformType);
|
||||
MOBILEGL_ASSERT(target != TextureTarget::Unknown,
|
||||
"ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'",
|
||||
@@ -2867,6 +2960,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
entry.dynamicBindings.push_back(binding);
|
||||
} else if (kind == DescriptorBindingKind::UniformTexelBuffer) {
|
||||
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
|
||||
} else if (kind == DescriptorBindingKind::StorageTexelBuffer) {
|
||||
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
|
||||
} else if (kind == DescriptorBindingKind::StorageBuffer) {
|
||||
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||
} else if (kind == DescriptorBindingKind::StorageImage) {
|
||||
@@ -3157,7 +3252,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ValidateRasterizationStageInterface(shaders, moduleSpirvs, entry, program.GetExternalIndex());
|
||||
#endif
|
||||
ReflectVertexInputs(shaders, moduleSpirvs, entry);
|
||||
ReflectViewportIndexUsage(shaders, moduleSpirvs, entry);
|
||||
ReflectFragmentOutputs(shaders, moduleSpirvs, entry);
|
||||
ReflectPassthroughTessControlNeed(shaders, moduleSpirvs, entry);
|
||||
ReflectLayout(program, moduleSpirvs, entry);
|
||||
// A failed remap means the modules kept glslang's per-stage auto-mapped binding numbers -
|
||||
// no cross-stage unification, no set->0 normalisation - so the bindings this layout
|
||||
@@ -3168,7 +3265,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// "the layout and the shader disagree", so route it through that. Set AFTER ReflectLayout,
|
||||
// which clears the flag.
|
||||
if (!remapOk) {
|
||||
MGLOG_I("ProgramFactory::GetOrCreateProgram: declining program %u - its descriptor bindings could not "
|
||||
MGLOG_E("ProgramFactory::GetOrCreateProgram: declining program %u - its descriptor bindings could not "
|
||||
"be remapped, so the layout does not describe what the shader reads",
|
||||
program.GetExternalIndex());
|
||||
entry.declinedDescriptors = true;
|
||||
@@ -3215,4 +3312,235 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProgramFactory::~ProgramFactory() {
|
||||
for (auto& entry : m_passthroughTessControlStages) {
|
||||
if (entry.second.module != VK_NULL_HANDLE) {
|
||||
vkDestroyShaderModule(m_device, entry.second.module, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String ProgramFactory::BuildPassthroughTessControlSource(Uint32 patchVertices) {
|
||||
// The stage GL 4.6 core 11.2.2 describes when a program has an evaluation shader and no
|
||||
// control shader: "the input patch is passed through unmodified", the output patch has
|
||||
// as many vertices as the input one (PATCH_VERTICES), and the levels come from the
|
||||
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
|
||||
//
|
||||
// Those two levels default to 1.0 and are baked here as literals because
|
||||
// glPatchParameterfv - their only setter - is not implemented in this frontend (it is a
|
||||
// stub in MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means
|
||||
// making the levels a parameter of this source AND of the cache key in
|
||||
// GetOrCreatePassthroughTessControlStage; the two must move together, so they are named
|
||||
// together here.
|
||||
//
|
||||
// gl_out carries gl_Position and nothing else on purpose. The evaluation stage that
|
||||
// reads it was linked against the VERTEX stage directly, so its input gl_PerVertex holds
|
||||
// exactly the built-ins that stage used, and its user-defined inputs (if any) come
|
||||
// straight off the vertex stage's outputs - which a control stage sitting in between
|
||||
// would leave unwritten. ReflectPassthroughTessControlNeed refuses those programs rather
|
||||
// than let this write a partial interface.
|
||||
//
|
||||
// All four outer levels and both inner levels are written unconditionally: writing a
|
||||
// level the evaluation stage's domain does not use is legal and ignored, and it saves
|
||||
// this from having to know the domain.
|
||||
String source = "#version 450 core\n";
|
||||
source += "layout(vertices = " + std::to_string(patchVertices) + ") out;\n";
|
||||
// gl_in and gl_out are redeclared to the exact gl_PerVertex the FRONTEND's linked programs
|
||||
// carry - gl_Position, gl_PointSize, gl_ClipDistance[1], in that order - because Vulkan
|
||||
// matches built-in interface blocks by their whole shape, and the two obvious spellings
|
||||
// are both wrong:
|
||||
// * narrowing the block to gl_Position alone makes the evaluation stage read a patch of
|
||||
// zeroes (degenerate triangles, nothing rasterized), and
|
||||
// * taking glslang's DEFAULT block for a standalone control stage yields FOUR members -
|
||||
// it appends gl_CullDistance - where a linked vertex+evaluation program has three.
|
||||
// PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch: it links a
|
||||
// vertex+evaluation program through this same compiler and fails if the two shapes ever
|
||||
// stop agreeing, rather than letting the mismatch show up as a black frame.
|
||||
//
|
||||
// Only gl_Position is written. gl_PointSize is declared but left alone deliberately:
|
||||
// writing it from a tessellation stage requires the shaderTessellationAndGeometryPointSize
|
||||
// feature, which this renderer does not enable, so a program whose evaluation stage reads
|
||||
// gl_in[].gl_PointSize gets an undefined point size instead of the vertex stage's - a gap
|
||||
// this trades for not making every tessellated pipeline depend on an optional feature.
|
||||
source += "in gl_PerVertex {\n"
|
||||
" vec4 gl_Position;\n"
|
||||
" float gl_PointSize;\n"
|
||||
" float gl_ClipDistance[1];\n"
|
||||
"} gl_in[gl_MaxPatchVertices];\n";
|
||||
source += "out gl_PerVertex {\n"
|
||||
" vec4 gl_Position;\n"
|
||||
" float gl_PointSize;\n"
|
||||
" float gl_ClipDistance[1];\n"
|
||||
"} gl_out[];\n";
|
||||
source += "void main() {\n";
|
||||
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
|
||||
source += " gl_TessLevelOuter[0] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[1] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[2] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[3] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[0] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[1] = 1.0;\n";
|
||||
source += "}\n";
|
||||
return source;
|
||||
}
|
||||
|
||||
VkPipelineShaderStageCreateInfo ProgramFactory::GetOrCreatePassthroughTessControlStage(Uint32 patchVertices) {
|
||||
// A cached VK_NULL_HANDLE is a remembered failure, not a miss: returning it keeps a
|
||||
// generator that cannot compile from re-running glslang on every draw.
|
||||
const auto cached = m_passthroughTessControlStages.find(patchVertices);
|
||||
if (cached != m_passthroughTessControlStages.end()) {
|
||||
return cached->second;
|
||||
}
|
||||
|
||||
VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
|
||||
stage.stage = VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT;
|
||||
stage.module = VK_NULL_HANDLE;
|
||||
stage.pName = "main";
|
||||
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
const String source = BuildPassthroughTessControlSource(patchVertices);
|
||||
// Same compile configuration as every other stage of every other program: this runs on
|
||||
// the GL thread (the draw path), so the live compile env is the right one, and flags=0
|
||||
// is the Vulkan-targeting form (CompileForOpenGL is what the GLES backend adds).
|
||||
const SharedPtr<const CompileEnv>& env = GetCurrentCompileEnv();
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_TESS_CONTROL_SHADER,
|
||||
.sourceStr = source,
|
||||
.flags = 0,
|
||||
.env = env.get()};
|
||||
auto compiled = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
if (!compiled) {
|
||||
MGLOG_E("ProgramFactory: could not compile the pass-through tessellation control stage for "
|
||||
"patchVertices=%u; a program with an evaluation stage and no control stage cannot draw. %s",
|
||||
patchVertices, compiled.error().log.c_str());
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
ProgramAttrib programAttrib{};
|
||||
programAttrib.shaders.push_back(compiled.value());
|
||||
auto linked = ShaderCompiler::LinkProgram(programAttrib);
|
||||
if (!linked) {
|
||||
MGLOG_E("ProgramFactory: could not link the pass-through tessellation control stage for "
|
||||
"patchVertices=%u. %s", patchVertices, linked.error().log.c_str());
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_TESS_CONTROL_SHADER}, .program = *linked.value()};
|
||||
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
if (!binary || binary.value().empty() || binary.value().front().empty()) {
|
||||
MGLOG_E("ProgramFactory: could not generate SPIR-V for the pass-through tessellation control stage "
|
||||
"for patchVertices=%u", patchVertices);
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
const Vector<Uint>& spirv = binary.value().front();
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
|
||||
#else
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) {
|
||||
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
smci.codeSize = spirv.size() * sizeof(Uint);
|
||||
smci.pCode = spirv.data();
|
||||
VkShaderModule module = VK_NULL_HANDLE;
|
||||
const VkResult result = vkCreateShaderModule(m_device, &smci, nullptr, &module);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E("ProgramFactory: vkCreateShaderModule failed (%d) for the pass-through tessellation control "
|
||||
"stage for patchVertices=%u", static_cast<Int>(result), patchVertices);
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
stage.module = module;
|
||||
MGLOG_D("ProgramFactory: built the pass-through tessellation control stage for patchVertices=%u "
|
||||
"(GL 4.6 11.2.2; Vulkan has no fixed-function equivalent)", patchVertices);
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
void ProgramFactory::ReflectPassthroughTessControlNeed(
|
||||
const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const {
|
||||
entry.needsPassthroughTessControl = false;
|
||||
entry.passthroughTessControlEmulatable = false;
|
||||
|
||||
Bool hasTessEval = false;
|
||||
Bool hasTessControl = false;
|
||||
SizeT tessEvalModuleIndex = 0;
|
||||
for (SizeT i = 0; i < shaders.size(); ++i) {
|
||||
if (!shaders[i]) continue;
|
||||
const auto stage = shaders[i]->GetShaderStage();
|
||||
if (stage == ShaderStage::TessControl) hasTessControl = true;
|
||||
if (stage == ShaderStage::TessEval) {
|
||||
hasTessEval = true;
|
||||
tessEvalModuleIndex = i;
|
||||
}
|
||||
}
|
||||
if (!hasTessEval || hasTessControl) return;
|
||||
|
||||
entry.needsPassthroughTessControl = true;
|
||||
|
||||
if (tessEvalModuleIndex >= spirv.size() || spirv[tessEvalModuleIndex].empty()) return;
|
||||
const auto& module = spirv[tessEvalModuleIndex];
|
||||
|
||||
SpvReflectShaderModule reflectModule{};
|
||||
const SpvReflectResult createResult =
|
||||
spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule);
|
||||
if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
|
||||
MGLOG_E("ProgramFactory::ReflectPassthroughTessControlNeed: reflection failed (result=%d); the "
|
||||
"evaluation stage's inputs are unknown, so the pass-through is not offered",
|
||||
static_cast<Int>(createResult));
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t inputCount = 0;
|
||||
SpvReflectResult reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, nullptr);
|
||||
Vector<SpvReflectInterfaceVariable*> inputs(inputCount);
|
||||
if (reflectResult == SPV_REFLECT_RESULT_SUCCESS && inputCount > 0) {
|
||||
reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, inputs.data());
|
||||
}
|
||||
if (reflectResult != SPV_REFLECT_RESULT_SUCCESS) {
|
||||
spvReflectDestroyShaderModule(&reflectModule);
|
||||
return;
|
||||
}
|
||||
|
||||
// The question is only ever "does this stage read anything a control stage would have to
|
||||
// forward", and the answer is: does it have a LOCATION. A located input is a user-defined
|
||||
// varying (or a per-patch input), which the vertex stage writes today and would stop
|
||||
// reaching once a control stage sits in between - the pass-through carries gl_Position and
|
||||
// nothing else, so such a program is declined instead of being handed undefined values.
|
||||
// Everything without a location is a built-in: gl_in, gl_TessCoord, gl_PatchVerticesIn,
|
||||
// gl_PrimitiveID, gl_TessLevel*, all either forwarded or generated for the evaluation
|
||||
// stage by the tessellator itself.
|
||||
//
|
||||
// This deliberately does NOT judge on SpvReflectInterfaceVariable::built_in. gl_in is an
|
||||
// array of interface blocks, and for those SPIRV-Reflect reports built_in == -1 on the
|
||||
// block AND leaves every member's built_in at 0 - which is SpvBuiltInPosition, so a
|
||||
// member walk reads "Position, Position, Position" for a {Position, PointSize,
|
||||
// ClipDistance} block and would accept anything on the strength of parse garbage. The
|
||||
// location, by contrast, is decorated on the OpVariable and is what SPIRV-Reflect reads
|
||||
// straight through.
|
||||
constexpr Uint32 kNoLocation = 0xFFFFFFFFu;
|
||||
Bool emulatable = true;
|
||||
for (auto* input : inputs) {
|
||||
if (input == nullptr) continue;
|
||||
if (input->location == kNoLocation) continue;
|
||||
MGLOG_E("ProgramFactory: a tessellation evaluation stage with no control stage reads the "
|
||||
"user-defined input '%s' at location=%u; a synthesized control stage cannot forward it, so "
|
||||
"this program's draws are declined rather than fed an undefined varying",
|
||||
input->name != nullptr ? input->name : "<null>", input->location);
|
||||
emulatable = false;
|
||||
break;
|
||||
}
|
||||
|
||||
spvReflectDestroyShaderModule(&reflectModule);
|
||||
entry.passthroughTessControlEmulatable = emulatable;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Types.h>
|
||||
#include <spirv_reflect.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -33,7 +34,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
CombinedImageSampler,
|
||||
UniformTexelBuffer,
|
||||
StorageBuffer,
|
||||
StorageImage
|
||||
StorageImage,
|
||||
// GLSL `imageBuffer` - a buffer texture reached through an IMAGE unit rather than a
|
||||
// texture unit. Vulkan spells it VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, which is a
|
||||
// VkBufferView like UniformTexelBuffer and not a VkImageView like StorageImage: it is
|
||||
// the one image uniform whose descriptor is a buffer. Appended, never inserted -
|
||||
// DescriptorKeyHash mixes the enumerator's value.
|
||||
StorageTexelBuffer
|
||||
};
|
||||
|
||||
enum class CompileOptionBit : Uint {
|
||||
@@ -103,6 +110,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<Int> samplerUniformLocationByBinding;
|
||||
Vector<TextureTarget> samplerTextureTargetByBinding;
|
||||
Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
|
||||
// Shared by StorageImage and StorageTexelBuffer bindings: a binding is one kind or
|
||||
// the other, never both, and both need exactly the same thing - the format the
|
||||
// shader declared, so the per-draw resolve can tell a typed declaration from a
|
||||
// formatless one. Kept as one pair rather than two so the move operations below
|
||||
// cannot drift out of sync with a field that only one kind populates.
|
||||
Vector<VkFormat> storageImageFormatByBinding;
|
||||
Vector<Bool> storageImageUsesBindingFormatByBinding;
|
||||
Vector<String> storageBlockNameByBinding;
|
||||
@@ -140,6 +152,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// PROGRAM rather than of the variant: the zeroed variant leaves the variable
|
||||
// declared, so both variants answer the same and the draw path can ask either.
|
||||
Bool readsBaseVertexBuiltin = false;
|
||||
// Some pre-rasterization stage assigns gl_ViewportIndex. Its pipeline declares
|
||||
// viewportCount = the renderer's rasterizable viewport count instead of 1, and its
|
||||
// draws push the whole viewport/scissor array; every other program keeps the
|
||||
// single-viewport fast path untouched. Part of the program's identity (folded into
|
||||
// the pipeline hash through programHash), so no memo can serve the wrong shape.
|
||||
Bool writesViewportIndexBuiltin = false;
|
||||
// This program has a tessellation EVALUATION stage and no tessellation CONTROL
|
||||
// stage. GL allows that (4.6 core 11.2.2: with no control shader the input patch
|
||||
// is passed through unmodified, the output patch size is PATCH_VERTICES, and the
|
||||
// levels come from the PATCH_DEFAULT_*_LEVEL state); Vulkan does not - either both
|
||||
// tessellation stages are present or neither
|
||||
// (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). So the draw path has to supply
|
||||
// the pass-through stage GL describes; see GetOrCreatePassthroughTessControlStage.
|
||||
Bool needsPassthroughTessControl = false;
|
||||
// ...and the pass-through this renderer can synthesize carries gl_Position and
|
||||
// nothing else, so it is only correct when the evaluation stage's inputs are
|
||||
// built-ins. A user-defined varying would arrive at the evaluation stage
|
||||
// UNWRITTEN once a control stage sits between it and the vertex stage, which is
|
||||
// silently wrong pixels rather than a crash - so those programs are declined
|
||||
// instead (PipelineFactory::CreatePipeline refuses the pipeline and the draw is
|
||||
// skipped). See ReflectPassthroughTessControlNeed.
|
||||
Bool passthroughTessControlEmulatable = false;
|
||||
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
||||
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
||||
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
||||
@@ -191,6 +225,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
@@ -205,6 +242,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.readsBaseVertexBuiltin = false;
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.lastUsedFrame = 0;
|
||||
}
|
||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||
@@ -245,6 +285,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
@@ -259,6 +302,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.readsBaseVertexBuiltin = false;
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.lastUsedFrame = 0;
|
||||
return *this;
|
||||
}
|
||||
@@ -310,7 +356,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
|
||||
VkProgramObject::s_device = device;
|
||||
}
|
||||
~ProgramFactory() = default;
|
||||
// Destroys the pass-through tessellation control modules. Runs while the device is
|
||||
// still alive for the same reason ~VkProgramObject's does: this factory outlives
|
||||
// nothing that owns the device.
|
||||
~ProgramFactory();
|
||||
ProgramFactory(const ProgramFactory&) = delete;
|
||||
|
||||
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
|
||||
@@ -362,6 +411,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Shared by the two above: does any entry point list an input variable decorated with
|
||||
// this builtin?
|
||||
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
||||
// True when an entry point writes the ViewportIndex builtin (gl_ViewportIndex), i.e. when
|
||||
// the program can route primitives to a viewport other than 0 and its pipeline therefore
|
||||
// has to declare more than one. Asks about OUTPUT variables because that is the direction
|
||||
// a pre-rasterization stage declares it in.
|
||||
static Bool ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||
static Bool ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
||||
|
||||
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes for a
|
||||
// program that has an evaluation stage and no control stage, for an input patch of
|
||||
// `patchVertices` control points. Returned BY VALUE (a stage description is a POD, and
|
||||
// the cache below is a rehashing map, so a pointer into it would not survive the next
|
||||
// distinct patch size). `.module == VK_NULL_HANDLE` means the stage could not be built:
|
||||
// the caller then has no control stage to inject, and CreatePipeline refuses the
|
||||
// pipeline rather than handing the driver a half-tessellated one.
|
||||
//
|
||||
// Keyed on the patch size because GL takes the output patch size from PATCH_VERTICES,
|
||||
// which is draw state, not link state - the CTS case that motivated this links at the
|
||||
// default 3 and draws at 4. The pipeline cache already re-keys on patchControlPoints,
|
||||
// so the module a pipeline was built with is part of that pipeline's identity.
|
||||
// Compiling is bounded by the number of distinct patch sizes a program draws with
|
||||
// (MAX_PATCH_VERTICES = 32 in the worst case, one or two in practice) and only ever
|
||||
// happens for the rare program that has no control stage at all.
|
||||
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices);
|
||||
|
||||
// Source of the module above. Exposed for tests: the generated GLSL is the whole
|
||||
// contract with the evaluation stage, so it is worth pinning independently of a device.
|
||||
static String BuildPassthroughTessControlSource(Uint32 patchVertices);
|
||||
|
||||
private:
|
||||
struct ProgramLookupCache {
|
||||
@@ -375,11 +451,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
// Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked
|
||||
// modules. Const and reflection-only: it decides nothing about the pipeline, it only
|
||||
// records what the evaluation stage's input interface is made of.
|
||||
void ReflectPassthroughTessControlNeed(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
Uint32 m_maxBindings = 0;
|
||||
@@ -400,6 +485,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
|
||||
Uint64 m_cacheStructureEpoch = 1;
|
||||
IEvictionObserver* m_evictionObserver = nullptr;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
// Pass-through tessellation control stages by input patch size. Never evicted: at most
|
||||
// MAX_PATCH_VERTICES entries exist for the lifetime of the device, and every pipeline
|
||||
// ever built from one keeps referencing its module. A failed build is cached as
|
||||
// VK_NULL_HANDLE so a broken generator costs one compile, not one per draw.
|
||||
UnorderedMap<Uint32, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
|
||||
static inline MobileGL::XXH64State m_hashState;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -157,7 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
MGLOG_I("Got %d surface formats:", swapchainCapabilities.surfaceFormats.size());
|
||||
for (const auto& sf : swapchainCapabilities.surfaceFormats) {
|
||||
MGLOG_I(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
|
||||
MGLOG_D(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
|
||||
}
|
||||
|
||||
const auto pickedSurfaceFormat = ChooseSwapchainSurfaceFormat(swapchainCapabilities.surfaceFormats);
|
||||
@@ -166,7 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
MGLOG_I("Got %d present modes:", swapchainCapabilities.presentModes.size());
|
||||
for (const auto& pm : swapchainCapabilities.presentModes) {
|
||||
MGLOG_I(" %s", string_VkPresentModeKHR(pm));
|
||||
MGLOG_D(" %s", string_VkPresentModeKHR(pm));
|
||||
}
|
||||
|
||||
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
|
||||
|
||||
@@ -157,7 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkDescriptorPool initialPool = VK_NULL_HANDLE;
|
||||
if (!CreateDescriptorPool(m_setsPerFrame, initialPool)) {
|
||||
MGLOG_E("UniformDescriptorBinder::Initialize failed: cannot create frame descriptor pool %u",
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::Initialize failed: cannot create frame descriptor pool %u",
|
||||
frameIndex);
|
||||
Shutdown();
|
||||
return false;
|
||||
@@ -305,7 +305,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// texture/sampler resolution, completeness probe, sync, layout handling, sampler
|
||||
// and view lookups - would recompute the identical descriptor.
|
||||
if (trustUnchangedHint && descriptorMemoUsable && binding < m_samplerResolveMemo.size() &&
|
||||
m_samplerResolveMemo[binding].infoValid) {
|
||||
m_samplerResolveMemo[binding].infoValid &&
|
||||
m_samplerResolveMemo[binding].infoProgramLifetimeId == program.GetLifetimeId()) {
|
||||
outImageInfo = m_samplerResolveMemo[binding].info;
|
||||
return true;
|
||||
}
|
||||
@@ -345,13 +346,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
fallbackHolder = GetFallbackTexture(preferredTarget);
|
||||
texture = fallbackHolder.get();
|
||||
if (texture == nullptr) {
|
||||
MGLOG_E("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
|
||||
MGLOG_E_ONCE("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
|
||||
"location=%d unit=%d target=%d",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
|
||||
static_cast<Int>(preferredTarget));
|
||||
return false;
|
||||
}
|
||||
MGLOG_W(
|
||||
MGLOG_W_ONCE(
|
||||
"ResolveSamplerDescriptor: using fallback texture for unbound sampler binding=%u ('%s') location=%d unit=%d target=%d",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
|
||||
static_cast<Int>(preferredTarget));
|
||||
@@ -360,7 +361,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const MG_State::GLState::SamplerObject* samplerToUse =
|
||||
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
|
||||
if (samplerToUse == nullptr) {
|
||||
MGLOG_E(
|
||||
MGLOG_E_ONCE(
|
||||
"ResolveSamplerDescriptor: sampler binding %u ('%s') has no sampler object (textureId=%d location=%d unit=%d)",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(), location,
|
||||
unit);
|
||||
@@ -368,7 +369,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
VkTextureManager::TextureResource* resource = m_textureManager->SyncTextureAndGetDescriptor(*texture);
|
||||
if (resource == nullptr) {
|
||||
MGLOG_E(
|
||||
MGLOG_E_ONCE(
|
||||
"ResolveSamplerDescriptor: sampler binding %u ('%s') failed to create/sync texture resource (textureId=%d target=%d location=%d unit=%d)",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
||||
static_cast<Int>(texture->GetTarget()), location, unit);
|
||||
@@ -380,7 +381,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Int attachmentLevel = 0;
|
||||
if (drawFbo &&
|
||||
FindFramebufferAttachmentForTexture(*drawFbo, *texture, attachmentType, attachmentLevel)) {
|
||||
MGLOG_W("ResolveSamplerDescriptor: framebuffer feedback loop detected: textureId=%d is bound "
|
||||
MGLOG_W_ONCE("ResolveSamplerDescriptor: framebuffer feedback loop detected: textureId=%d is bound "
|
||||
"for sampling at binding=%u, but is also attached to drawFbo=%u as %s (level=%d, "
|
||||
"trackedLayout=%d)",
|
||||
texture->GetExternalIndex(), binding, drawFbo->GetExternalIndex(),
|
||||
@@ -390,7 +391,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Bool readyForSampling = m_textureManager->TransitionTextureForSampling(commandBuffer, *texture);
|
||||
if (!readyForSampling) {
|
||||
MGLOG_E("ResolveSamplerDescriptor: failed to transition textureId=%d for sampler binding=%u",
|
||||
MGLOG_E_ONCE("ResolveSamplerDescriptor: failed to transition textureId=%d for sampler binding=%u",
|
||||
texture->GetExternalIndex(), binding);
|
||||
return false;
|
||||
}
|
||||
@@ -432,7 +433,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
|
||||
MGLOG_E_ONCE("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
|
||||
"textureId=%d imageFormat=%d numericDomain=%d",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
||||
static_cast<Int>(resource->format), static_cast<Int>(numericDomain));
|
||||
@@ -445,7 +446,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
? resource->sampledView
|
||||
: m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
|
||||
if (sampledImageView == VK_NULL_HANDLE) {
|
||||
MGLOG_E("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
|
||||
MGLOG_E_ONCE("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
|
||||
"textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
||||
static_cast<Int>(resource->format), static_cast<Int>(sampledViewFormat),
|
||||
@@ -504,6 +505,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (binding < m_samplerResolveMemo.size()) {
|
||||
if (descriptorMemoUsable) {
|
||||
m_samplerResolveMemo[binding].info = outImageInfo;
|
||||
m_samplerResolveMemo[binding].infoProgramLifetimeId = program.GetLifetimeId();
|
||||
m_samplerResolveMemo[binding].infoValid = true;
|
||||
} else {
|
||||
// An arrayed binding publishes nothing here, and clears what a previous program
|
||||
@@ -671,14 +673,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
SharedPtr<MG_State::GLState::ITextureObject> texture;
|
||||
if (!ResolveSamplerTexture(program, programObj, binding, texture) || texture == nullptr) {
|
||||
MGLOG_E("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') is unbound", binding,
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') is unbound", binding,
|
||||
programObj.samplerNameByBinding[binding].c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (texture->GetStorageType() != TextureStorageType::Buffer ||
|
||||
texture->GetTarget() != TextureTarget::TextureBuffer) {
|
||||
MGLOG_E(
|
||||
MGLOG_E_ONCE(
|
||||
"ResolveTexelBufferDescriptor: binding %u ('%s') expected texture buffer, got textureId=%u target=%d storage=%d",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
||||
static_cast<Int>(texture->GetTarget()), static_cast<Int>(texture->GetStorageType()));
|
||||
@@ -688,14 +690,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
||||
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
||||
if (bufferObject == nullptr) {
|
||||
MGLOG_E("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') has no GL buffer bound",
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') has no GL buffer bound",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) || !slice.IsValid()) {
|
||||
MGLOG_E("ResolveTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
|
||||
bufferObject->GetExternalIndex(), texture->GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
@@ -703,7 +705,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const auto internalFormat = textureBuffer->GetFormat();
|
||||
const VkFormat vkFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||
if (vkFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E("ResolveTexelBufferDescriptor: unsupported texture buffer internal format %d",
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: unsupported texture buffer internal format %d",
|
||||
static_cast<Int>(internalFormat));
|
||||
return false;
|
||||
}
|
||||
@@ -719,7 +721,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
viewRange = (viewRange / texelSize) * texelSize;
|
||||
}
|
||||
if (viewRange == 0) {
|
||||
MGLOG_E("ResolveTexelBufferDescriptor: texture buffer %u has empty view range", texture->GetExternalIndex());
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer %u has empty view range", texture->GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -733,7 +735,151 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkBufferView bufferView = VK_NULL_HANDLE;
|
||||
const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &bufferView);
|
||||
if (result != VK_SUCCESS || bufferView == VK_NULL_HANDLE) {
|
||||
MGLOG_E("ResolveTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
|
||||
result, static_cast<Int>(vkFormat), static_cast<SizeT>(viewRange));
|
||||
return false;
|
||||
}
|
||||
|
||||
m_frames[frameIndex].texelBufferViews.push_back(bufferView);
|
||||
outBufferView = bufferView;
|
||||
return true;
|
||||
}
|
||||
|
||||
// GLSL `imageBuffer`. The one image uniform whose Vulkan descriptor is a VkBufferView rather
|
||||
// than a VkImageView, so it is half ResolveStorageImageDescriptor (the resource comes from an
|
||||
// IMAGE unit, i.e. from glBindImageTexture, not from a texture unit) and half
|
||||
// ResolveTexelBufferDescriptor (the descriptor is a buffer view over the GL buffer the
|
||||
// texture is attached to).
|
||||
//
|
||||
// Before this existed the descriptor kind reflected as SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_-
|
||||
// TEXEL_BUFFER and fell into ReflectDescriptorTypeToBindingKind's `default:`, whose only
|
||||
// complaint is an assert that compiles out above DEBUG - so a release build declared no
|
||||
// binding at all for a uniform the shader still read, and lavapipe segfaulted inside pipeline
|
||||
// creation on the JIT worker thread. KHR-GL44.multi_bind.dispatch_bind_image_textures is the
|
||||
// case that carries it.
|
||||
Bool UniformManager::ResolveStorageTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Uint32 binding, Uint32 frameIndex,
|
||||
VkBufferView& outBufferView) {
|
||||
outBufferView = VK_NULL_HANDLE;
|
||||
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageTexelBufferDescriptor: buffer manager is null");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveStorageTexelBufferDescriptor: GL context is null");
|
||||
MOBILEGL_ASSERT(frameIndex < m_frames.size(),
|
||||
"ResolveStorageTexelBufferDescriptor: frame index out of range");
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
||||
"ResolveStorageTexelBufferDescriptor: binding %u out of range", binding);
|
||||
|
||||
const Int location = programObj.samplerUniformLocationByBinding[binding];
|
||||
if (location < 0) {
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: binding %u ('%s') has no uniform location", binding,
|
||||
programObj.samplerNameByBinding[binding].c_str());
|
||||
return false;
|
||||
}
|
||||
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
||||
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d out of range for binding %u", imageUnit,
|
||||
binding);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||
const auto& texture = imageBinding.Texture;
|
||||
if (texture == nullptr) {
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d is unbound for binding %u", imageUnit,
|
||||
binding);
|
||||
return false;
|
||||
}
|
||||
if (texture->GetStorageType() != TextureStorageType::Buffer ||
|
||||
texture->GetTarget() != TextureTarget::TextureBuffer) {
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: binding %u ('%s') expected a texture buffer on image "
|
||||
"unit %d, got textureId=%u target=%d storage=%d",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), imageUnit,
|
||||
texture->GetExternalIndex(), static_cast<Int>(texture->GetTarget()),
|
||||
static_cast<Int>(texture->GetStorageType()));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
||||
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
||||
if (bufferObject == nullptr) {
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no GL buffer bound",
|
||||
imageUnit);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unlike the sampled texel buffer, the shader MAY write this one, and those writes land
|
||||
// in GPU memory behind the frontend's CPU shadow - which is what MapBuffer and
|
||||
// GetBufferSubData read. Same two calls, and for the same reason, as the storage-block
|
||||
// path above - but only the residency is unconditional. Marking a GL_READ_ONLY binding
|
||||
// GPU-written would make the next map or readback wait for a dispatch that could not have
|
||||
// changed a byte of it.
|
||||
bufferObject->EnsureGpuResidentStorage();
|
||||
if (imageBinding.Access != GL_READ_ONLY) {
|
||||
bufferObject->MarkGpuWritten();
|
||||
}
|
||||
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) ||
|
||||
!slice.IsValid()) {
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
|
||||
bufferObject->GetExternalIndex(), texture->GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
|
||||
// The format the SHADER declared wins over the one glBindImageTexture named, on the same
|
||||
// policy as a storage image: a typed `layout(r32ui) uniform uimageBuffer` must be read as
|
||||
// r32ui whatever the texture's own attachment format says. Falling back, in order:
|
||||
// reflected format, then the bind format, then the texture's attached format.
|
||||
MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(),
|
||||
"ResolveStorageTexelBufferDescriptor: binding %u has no reflected format slot", binding);
|
||||
const auto internalFormat = textureBuffer->GetFormat();
|
||||
const VkFormat resourceFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
|
||||
VkFormat vkFormat = reflectedFormat;
|
||||
if (vkFormat == VK_FORMAT_UNDEFINED && imageBinding.Format != 0) {
|
||||
vkFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(
|
||||
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
|
||||
}
|
||||
if (vkFormat == VK_FORMAT_UNDEFINED) {
|
||||
vkFormat = resourceFormat;
|
||||
}
|
||||
if (vkFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: unsupported image buffer format (internal=%d bind=0x%x)",
|
||||
static_cast<Int>(internalFormat), imageBinding.Format);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sized from the TEXTURE's attached format even though the view may carry a different
|
||||
// one. That is not a shortcut: GL requires the shader's format qualifier, the format
|
||||
// passed to glBindImageTexture and the texture's own internal format to belong to the
|
||||
// same format CLASS (GL 4.6 core, table 8.27), and every member of a class has the same
|
||||
// texel size. So the three can disagree on interpretation and never on bytes - which is
|
||||
// what the range below has to be a whole multiple of.
|
||||
const VkDeviceSize texelSize =
|
||||
static_cast<VkDeviceSize>(MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat));
|
||||
const VkDeviceSize rangeOffset = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeOffset());
|
||||
const VkDeviceSize rangeSize = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeSizeInBytes());
|
||||
VkDeviceSize viewRange = std::min(rangeSize, slice.size > rangeOffset ? slice.size - rangeOffset : 0);
|
||||
if (texelSize > 0) {
|
||||
viewRange = (viewRange / texelSize) * texelSize;
|
||||
}
|
||||
if (viewRange == 0) {
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer %u has empty view range",
|
||||
texture->GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
|
||||
VkBufferViewCreateInfo viewInfo{};
|
||||
viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO;
|
||||
viewInfo.buffer = slice.buffer;
|
||||
viewInfo.format = vkFormat;
|
||||
viewInfo.offset = slice.offset + rangeOffset;
|
||||
viewInfo.range = viewRange;
|
||||
|
||||
VkBufferView bufferView = VK_NULL_HANDLE;
|
||||
const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &bufferView);
|
||||
if (result != VK_SUCCESS || bufferView == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
|
||||
result, static_cast<Int>(vkFormat), static_cast<SizeT>(viewRange));
|
||||
return false;
|
||||
}
|
||||
@@ -770,7 +916,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, frontendBinding);
|
||||
const auto& bufferObject = bindingPoint.GetBoundObject();
|
||||
if (bufferObject == nullptr) {
|
||||
MGLOG_E("ResolveStorageBufferDescriptor: no SSBO bound at frontend binding %u for block '%s'",
|
||||
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: no SSBO bound at frontend binding %u for block '%s'",
|
||||
frontendBinding, programObj.storageBlockNameByBinding[binding].c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -785,7 +931,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) {
|
||||
MGLOG_E("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'",
|
||||
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'",
|
||||
bufferObject->GetExternalIndex(), programObj.storageBlockNameByBinding[binding].c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -799,7 +945,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
rangeEnd = bufferSize;
|
||||
}
|
||||
if (rangeEnd <= rangeStart) {
|
||||
MGLOG_E("ResolveStorageBufferDescriptor: empty SSBO range for block '%s'",
|
||||
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: empty SSBO range for block '%s'",
|
||||
programObj.storageBlockNameByBinding[binding].c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -823,7 +969,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
|
||||
if (baseLocation < 0) {
|
||||
MGLOG_E("ResolveStorageImageDescriptor: storage image binding %u has no uniform location", binding);
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: storage image binding %u has no uniform location", binding);
|
||||
return false;
|
||||
}
|
||||
// Per ELEMENT, and this is where an image array differs from a storage-block array: GL
|
||||
@@ -835,26 +981,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// uniform.
|
||||
const Int location = baseLocation + static_cast<Int>(element);
|
||||
if (!program.UniformLocationsAliasSameUniform(baseLocation, location)) {
|
||||
MGLOG_E("ResolveStorageImageDescriptor: binding %u element %u is past the end of its image array",
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: binding %u element %u is past the end of its image array",
|
||||
binding, element);
|
||||
return false;
|
||||
}
|
||||
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
||||
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||
MGLOG_E("ResolveStorageImageDescriptor: image unit %d out of range for binding %u",
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d out of range for binding %u",
|
||||
imageUnit, binding);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||
if (imageBinding.Texture == nullptr) {
|
||||
MGLOG_E("ResolveStorageImageDescriptor: image unit %d is unbound for binding %u", imageUnit, binding);
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d is unbound for binding %u", imageUnit, binding);
|
||||
return false;
|
||||
}
|
||||
|
||||
const Bool ready = m_textureManager->TransitionTextureForStorageImage(commandBuffer, *imageBinding.Texture);
|
||||
if (!ready) {
|
||||
MGLOG_E("ResolveStorageImageDescriptor: failed to transition textureId=%d for image unit %d",
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to transition textureId=%d for image unit %d",
|
||||
imageBinding.Texture->GetExternalIndex(), imageUnit);
|
||||
return false;
|
||||
}
|
||||
@@ -874,7 +1020,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkFormat viewFormat = ResolveStorageImageViewFormat(
|
||||
reflectedFormat, imageBinding.Format, resource->format, useBindingFormat);
|
||||
if (viewFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
|
||||
"for binding=%u imageUnit=%d textureId=%d bindingPolicy=%s",
|
||||
imageBinding.Format, binding, imageUnit, imageBinding.Texture->GetExternalIndex(),
|
||||
useBindingFormat ? "true" : "false");
|
||||
@@ -883,7 +1029,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkImageView view = m_textureManager->GetOrCreateStorageImageView(
|
||||
*imageBinding.Texture, mipLevel, viewFormat, imageBinding.Layered != GL_FALSE, imageBinding.Layer);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
MGLOG_E("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
|
||||
"bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s",
|
||||
imageBinding.Texture->GetExternalIndex(), mipLevel, imageBinding.Format,
|
||||
static_cast<Int>(resource->format), static_cast<Int>(reflectedFormat),
|
||||
@@ -904,7 +1050,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Report that there is no fallback and let the caller decline the draw - aborting the
|
||||
// process over an unbound sampler is never the right answer.
|
||||
if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) {
|
||||
MGLOG_E("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
|
||||
MGLOG_E_ONCE("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
|
||||
static_cast<Int>(target));
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1080,13 +1226,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
if (binding >= programObj.samplerUniformLocationByBinding.size()) {
|
||||
MGLOG_E("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding);
|
||||
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding);
|
||||
return false;
|
||||
}
|
||||
|
||||
const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
|
||||
if (baseLocation < 0) {
|
||||
MGLOG_E("CollectStorageImageTextures: binding %u has no image uniform location", binding);
|
||||
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u has no image uniform location", binding);
|
||||
return false;
|
||||
}
|
||||
// Per ELEMENT, for the same reason the sampled walk above is: an image ARRAY is one
|
||||
@@ -1098,20 +1244,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
||||
const Int location = ResolveDescriptorElementLocation(program, baseLocation, element);
|
||||
if (location < 0) {
|
||||
MGLOG_E("CollectStorageImageTextures: binding %u element %u is past the end of its image array",
|
||||
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u element %u is past the end of its image array",
|
||||
binding, element);
|
||||
return false;
|
||||
}
|
||||
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
||||
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||
MGLOG_E("CollectStorageImageTextures: image unit %d is invalid for binding %u element %u",
|
||||
MGLOG_E_ONCE("CollectStorageImageTextures: image unit %d is invalid for binding %u element %u",
|
||||
imageUnit, binding, element);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
|
||||
if (texture == nullptr) {
|
||||
MGLOG_E("CollectStorageImageTextures: image unit %d is unbound for binding %u element %u",
|
||||
MGLOG_E_ONCE("CollectStorageImageTextures: image unit %d is unbound for binding %u element %u",
|
||||
imageUnit, binding, element);
|
||||
return false;
|
||||
}
|
||||
@@ -1262,12 +1408,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Uint64 descriptorCount64 =
|
||||
static_cast<Uint64>(maxSets) * static_cast<Uint64>(std::min(m_maxBindings, kEstimatedBindingsPerSet));
|
||||
if (descriptorCount64 > static_cast<Uint64>(std::numeric_limits<Uint32>::max())) {
|
||||
MGLOG_E("UniformDescriptorBinder::CreateDescriptorPool failed: descriptorCount overflow");
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::CreateDescriptorPool failed: descriptorCount overflow");
|
||||
return false;
|
||||
}
|
||||
|
||||
const Uint32 descriptorCount = static_cast<Uint32>(descriptorCount64);
|
||||
VkDescriptorPoolSize poolSizes[5]{};
|
||||
VkDescriptorPoolSize poolSizes[6]{};
|
||||
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
|
||||
poolSizes[0].descriptorCount = descriptorCount;
|
||||
poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
@@ -1278,6 +1424,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
poolSizes[3].descriptorCount = descriptorCount;
|
||||
poolSizes[4].type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
|
||||
poolSizes[4].descriptorCount = descriptorCount;
|
||||
poolSizes[5].type = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
|
||||
poolSizes[5].descriptorCount = descriptorCount;
|
||||
|
||||
VkDescriptorPoolCreateInfo poolInfo{};
|
||||
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
@@ -1292,7 +1440,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkResult result = vkCreateDescriptorPool(m_device, &poolInfo, nullptr, &outPool);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E("UniformDescriptorBinder::CreateDescriptorPool failed: vkCreateDescriptorPool returned %d",
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::CreateDescriptorPool failed: vkCreateDescriptorPool returned %d",
|
||||
result);
|
||||
return false;
|
||||
}
|
||||
@@ -1311,7 +1459,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkDescriptorPool grownPool = VK_NULL_HANDLE;
|
||||
if (!CreateDescriptorPool(grownMaxSets, grownPool)) {
|
||||
MGLOG_E("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
|
||||
currentMaxSets, grownMaxSets);
|
||||
return false;
|
||||
}
|
||||
@@ -1370,7 +1518,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
|
||||
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
|
||||
if (!GrowFrameDescriptorPool(frame, frameIndex)) {
|
||||
MGLOG_E("UniformDescriptorBinder::AcquireDescriptorSet failed: descriptor pool growth failed");
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::AcquireDescriptorSet failed: descriptor pool growth failed");
|
||||
return allocResult;
|
||||
}
|
||||
allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
|
||||
@@ -1501,7 +1649,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
auto& frame = m_frames[frameIndex];
|
||||
if (frame.descriptorPools.empty()) {
|
||||
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: frame descriptor pools are invalid");
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: frame descriptor pools are invalid");
|
||||
return false;
|
||||
}
|
||||
if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size()) {
|
||||
@@ -1578,6 +1726,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// is reachable wherever m_maxBindings is small (it clamps to ~16 on Adreno and Mali),
|
||||
// which is exactly where a 7-element CTS sampler array does not fit the slack.
|
||||
imageInfos.reserve(m_maxBindings + arrayDescriptorExtra);
|
||||
// Exact, and safe only because it is: BOTH texel kinds (samplerBuffer and imageBuffer)
|
||||
// refuse descriptor arrays at program creation, so each contributes at most one view and
|
||||
// the total cannot exceed the binding count. The branches below take the address of
|
||||
// back(), so making a texel kind array-capable without also giving this the surplus
|
||||
// imageInfos gets would dangle every pTexelBufferView already recorded in `writes`.
|
||||
texelBufferViews.reserve(m_maxBindings);
|
||||
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
|
||||
|
||||
@@ -1633,7 +1786,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkBufferView bufferView = VK_NULL_HANDLE;
|
||||
if (!ResolveTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
|
||||
bufferView == VK_NULL_HANDLE) {
|
||||
MGLOG_E(
|
||||
MGLOG_E_ONCE(
|
||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: texture buffer binding %u has no valid descriptor",
|
||||
binding);
|
||||
return false;
|
||||
@@ -1644,6 +1797,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
|
||||
write.pTexelBufferView = &texelBufferViews.back();
|
||||
writes.push_back(write);
|
||||
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageTexelBuffer) {
|
||||
// Shares texelBufferViews with the sampled kind above, and may do so safely for
|
||||
// the same reason: neither kind can be an array, so each contributes exactly one
|
||||
// element and the reserve of m_maxBindings cannot be outrun - which is what keeps
|
||||
// the &back() below from dangling when a later binding pushes.
|
||||
VkBufferView bufferView = VK_NULL_HANDLE;
|
||||
if (!ResolveStorageTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
|
||||
bufferView == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: image buffer binding %u "
|
||||
"has no valid descriptor",
|
||||
binding);
|
||||
return false;
|
||||
}
|
||||
|
||||
texelBufferViews.push_back(bufferView);
|
||||
fastRebindKindsEligible = false;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
|
||||
write.pTexelBufferView = &texelBufferViews.back();
|
||||
writes.push_back(write);
|
||||
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageBuffer) {
|
||||
// One write per binding, but `descriptorCount` buffer infos: a GLSL block
|
||||
// instance array occupies a single binding whose elements each come from their
|
||||
@@ -1653,7 +1825,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
||||
VkDescriptorBufferInfo bufferInfo{};
|
||||
if (!ResolveStorageBufferDescriptor(program, programObj, binding, element, bufferInfo)) {
|
||||
MGLOG_E(
|
||||
MGLOG_E_ONCE(
|
||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage buffer binding %u "
|
||||
"element %u has no valid descriptor",
|
||||
binding, element);
|
||||
@@ -1680,7 +1852,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDescriptorImageInfo imageInfo{};
|
||||
if (!ResolveStorageImageDescriptor(commandBuffer, program, programObj, binding, element,
|
||||
imageInfo)) {
|
||||
MGLOG_E(
|
||||
MGLOG_E_ONCE(
|
||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage image binding %u "
|
||||
"element %u has no valid descriptor",
|
||||
binding, element);
|
||||
@@ -1722,14 +1894,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
imageInfo, samplerDescriptorsUnchangedHint);
|
||||
}
|
||||
if (!hasImage) {
|
||||
MGLOG_E(
|
||||
MGLOG_E_ONCE(
|
||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
|
||||
"has no valid texture descriptor",
|
||||
binding, element);
|
||||
return false;
|
||||
}
|
||||
if (imageInfo.sampler == VK_NULL_HANDLE || imageInfo.imageView == VK_NULL_HANDLE) {
|
||||
MGLOG_E(
|
||||
MGLOG_E_ONCE(
|
||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
|
||||
"has null sampler or imageView",
|
||||
binding, element);
|
||||
@@ -1803,7 +1975,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} else {
|
||||
VkResult allocResult = AcquireDescriptorSet(frameIndex, programObj, descriptorSet);
|
||||
if (allocResult != VK_SUCCESS || descriptorSet == VK_NULL_HANDLE) {
|
||||
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: descriptor set acquire returned %d",
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: descriptor set acquire returned %d",
|
||||
allocResult);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -175,6 +175,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 frameIndex, VkBufferView& outBufferView);
|
||||
// GLSL `imageBuffer`: the same VkBufferView descriptor as the sampled texel buffer above,
|
||||
// but resolved from an IMAGE unit (glBindImageTexture) rather than a texture unit, and
|
||||
// made GPU-resident-writable because the shader may store to it. No `element` parameter:
|
||||
// an imageBuffer ARRAY is refused at program creation, so a binding is always one
|
||||
// descriptor (see the array gate in RemapDescriptorBindingsForVulkan).
|
||||
Bool ResolveStorageTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 frameIndex, VkBufferView& outBufferView);
|
||||
// `element` indexes a block INSTANCE array's descriptors; it is 0 for every ordinary
|
||||
// block. Each element resolves through its own GL storage block, and so its own GL
|
||||
// binding point, buffer and glBindBufferRange window.
|
||||
@@ -333,8 +341,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// lifetime id, so a freed-and-reallocated sampler or texture at the same heap address
|
||||
// always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a
|
||||
// stale guess can only miss and fall through to the hash, never resolve wrong. Still
|
||||
// reset each frame alongside the descriptor-set cache. Indexed by binding.
|
||||
// reset each frame alongside the descriptor-set cache. Indexed by binding, but the
|
||||
// whole-descriptor entry is additionally keyed by program lifetime: Vulkan binding
|
||||
// numbers are layout-local and unrelated programs routinely reuse binding 0/1.
|
||||
struct SamplerResolveMemo {
|
||||
Uint64 infoProgramLifetimeId = 0;
|
||||
Uint64 samplerLifetimeId = 0;
|
||||
Uint64 textureLifetimeId = 0;
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
|
||||
@@ -13,25 +13,25 @@
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VertexInputStateFactory::HashType VertexInputStateFactory::ComputeHash(
|
||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), m_config.CacheVersion));
|
||||
|
||||
for (Int i = 0; i < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++i) {
|
||||
const auto& attr = vao.GetAttribute(i);
|
||||
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Enabled, sizeof(attr.Enabled)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Enabled, sizeof(attr.Enabled)));
|
||||
if (!attr.Enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Size, sizeof(attr.Size)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Type, sizeof(attr.Type)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Normalized, sizeof(attr.Normalized)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsLong, sizeof(attr.IsLong)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Size, sizeof(attr.Size)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Type, sizeof(attr.Type)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Normalized, sizeof(attr.Normalized)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Stride, sizeof(attr.Stride)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Offset, sizeof(attr.Offset)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.IsInteger, sizeof(attr.IsInteger)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.IsLong, sizeof(attr.IsLong)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.IsBgra, sizeof(attr.IsBgra)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Divisor, sizeof(attr.Divisor)));
|
||||
|
||||
// The bound buffer's IDENTITY is a component of the key, and it has to be the
|
||||
// buffer's never-reused lifetime id - NOT its heap address, which this used to
|
||||
@@ -45,10 +45,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// test's positions) instead of its own.
|
||||
// Zero for client memory (no buffer), which is a distinct identity of its own.
|
||||
const Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &bufferKey, sizeof(bufferKey)));
|
||||
}
|
||||
|
||||
return XXH64_digest(m_hashState);
|
||||
return XXH64_digest(m_hashState.Get());
|
||||
}
|
||||
|
||||
VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash(
|
||||
@@ -110,7 +110,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkFormat sourceVkFormat =
|
||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
|
||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
||||
MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
||||
"enabled but cannot be mapped to a VkFormat",
|
||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||
unsupportedAttribMask |= (1u << location);
|
||||
@@ -125,7 +125,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
|
||||
vkFormat = fallbackFormat;
|
||||
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
|
||||
MGLOG_W("Vertex attribute location=%u format=%d lacks "
|
||||
MGLOG_W_ONCE("Vertex attribute location=%u format=%d lacks "
|
||||
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
|
||||
"(type=%s size=%d normalized=%s integer=%s)",
|
||||
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
|
||||
@@ -135,7 +135,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (conversion == VertexStreamConversion::None) {
|
||||
MGLOG_E("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
|
||||
MGLOG_E_ONCE("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
|
||||
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
|
||||
location, static_cast<Int>(sourceVkFormat),
|
||||
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||
@@ -146,7 +146,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
|
||||
if (attribByteSize == 0) {
|
||||
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
|
||||
MGLOG_E_ONCE("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
|
||||
"enabled but cannot be sized",
|
||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
|
||||
unsupportedAttribMask |= (1u << location);
|
||||
@@ -175,7 +175,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
|
||||
// attribute into a tightly packed transient stream without changing its format.
|
||||
conversion = VertexStreamConversion::Repack;
|
||||
MGLOG_W("Vertex attribute location=%u uses Vulkan-incompatible alignment "
|
||||
MGLOG_W_ONCE("Vertex attribute location=%u uses Vulkan-incompatible alignment "
|
||||
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
|
||||
location, attr.Offset, sourceStride, requiredAlignment);
|
||||
}
|
||||
@@ -225,24 +225,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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));
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), 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)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &binding.binding, sizeof(binding.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &binding.stride, sizeof(binding.stride)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &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.Get(), &attribute.location, sizeof(attribute.location)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attribute.binding, sizeof(attribute.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attribute.format, sizeof(attribute.format)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attribute.offset, sizeof(attribute.offset)));
|
||||
}
|
||||
for (const auto& divisor : entry.bindingDivisors) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.binding, sizeof(divisor.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.divisor, sizeof(divisor.divisor)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &divisor.binding, sizeof(divisor.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &divisor.divisor, sizeof(divisor.divisor)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
||||
entry.layoutHash = XXH64_digest(m_hashState);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
||||
entry.layoutHash = XXH64_digest(m_hashState.Get());
|
||||
entry.attributeLocationMask = 0;
|
||||
for (const auto& attribute : entry.attributes) {
|
||||
if (attribute.location < 32u) {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "VertexInputStateBuilder.h"
|
||||
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Types.h>
|
||||
#include "../VkIncludes.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -126,6 +127,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// matches, so an evicted entry can never be dereferenced through a
|
||||
// stale memo.
|
||||
Uint64 m_evictionEpoch = 1;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline MobileGL::XXH64State m_hashState;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -23,7 +23,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
|
||||
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;
|
||||
// "Every usage" has to mean every usage: a buffer texture reached through an IMAGE
|
||||
// unit takes a VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER descriptor, and the write is
|
||||
// invalid unless the buffer was created with this bit. Nothing asked for it until
|
||||
// imageBuffer support existed, so the omission was invisible.
|
||||
VK_BUFFER_USAGE_STORAGE_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 =
|
||||
@@ -298,7 +302,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.requiredFlags = requiredFlags,
|
||||
});
|
||||
if (!created || resource.buffer.Map() == nullptr) {
|
||||
MGLOG_E("VkBufferManager::CreateResidentStorage failed (size=%llu)",
|
||||
MGLOG_E_ONCE("VkBufferManager::CreateResidentStorage failed (size=%llu)",
|
||||
static_cast<unsigned long long>(size));
|
||||
resource.buffer.Destroy();
|
||||
resource.storageSize = 0;
|
||||
@@ -320,7 +324,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) {
|
||||
MGLOG_E("VkBufferManager::SwapStorageAndUploadAll: upload failed");
|
||||
MGLOG_E_ONCE("VkBufferManager::SwapStorageAndUploadAll: upload failed");
|
||||
resource.pendingFullUpload = true;
|
||||
return false;
|
||||
}
|
||||
@@ -379,6 +383,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
BumpSliceEpoch(*resource);
|
||||
// Any cached streaming slice refers to the previous contents.
|
||||
resource->transientFrameSerial = 0;
|
||||
// Redefining the store hands any adopted mapping back to the CPU shadow
|
||||
// (BufferObject::RedefineStorage), so a buffer that reaches here persistent-mapped
|
||||
// is an ordinary resident one again: it needs the busy-tracking and conditional
|
||||
// orphan below, and the next AcquirePersistentMap has to mint storage for the new
|
||||
// store rather than hand back a mapping of the old one.
|
||||
resource->persistentMapped = false;
|
||||
if (!resource->buffer.IsValid()) {
|
||||
return; // streaming-only resource: shadow + serial are enough
|
||||
}
|
||||
@@ -399,7 +409,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
|
||||
MGLOG_E("VkBufferManager::OnRespecify: in-place upload failed");
|
||||
MGLOG_E_ONCE("VkBufferManager::OnRespecify: in-place upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
}
|
||||
}
|
||||
@@ -424,7 +434,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!IsResourceBusy(*resource)) {
|
||||
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
|
||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
||||
MGLOG_E("VkBufferManager::OnSubData: host upload failed");
|
||||
MGLOG_E_ONCE("VkBufferManager::OnSubData: host upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
}
|
||||
return;
|
||||
@@ -461,7 +471,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) {
|
||||
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
|
||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
||||
MGLOG_E("VkBufferManager::OnFlushMappedRange: host upload failed");
|
||||
MGLOG_E_ONCE("VkBufferManager::OnFlushMappedRange: host upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
}
|
||||
return;
|
||||
@@ -553,7 +563,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
||||
if (size == 0) {
|
||||
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
|
||||
MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -575,7 +585,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
if (!resource->buffer.Upload(bufferObject->MappedData(), size, 0)) {
|
||||
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
|
||||
MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
|
||||
resource->buffer.Destroy();
|
||||
resource->storageSize = 0;
|
||||
resource->usageFlags = 0;
|
||||
@@ -610,7 +620,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
||||
if (size == 0) {
|
||||
MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
|
||||
MGLOG_E_ONCE("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -708,7 +718,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case BufferKind::Uniform:
|
||||
return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
|
||||
case BufferKind::TextureBuffer:
|
||||
return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT;
|
||||
// Both texel roles, for the same reason vertex/index carry both bits: one GL buffer
|
||||
// texture can be read as a samplerBuffer and written as an imageBuffer, and which of
|
||||
// the two it is only becomes known when a shader that uses it is bound - long after
|
||||
// the resident buffer was created. A VkBufferView for a storage-texel descriptor is
|
||||
// invalid unless the buffer was created with the storage bit, so a buffer that
|
||||
// acquired only the uniform bit could never be given one.
|
||||
return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT;
|
||||
case BufferKind::ShaderStorage:
|
||||
return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
|
||||
case BufferKind::Indirect:
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkResult result =
|
||||
vmaCreateBuffer(m_allocator, &bufferInfo, &allocationInfo, &m_buffer, &m_allocation, nullptr);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
|
||||
MGLOG_E_ONCE("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
|
||||
m_allocator = nullptr;
|
||||
m_buffer = VK_NULL_HANDLE;
|
||||
m_allocation = nullptr;
|
||||
@@ -108,7 +108,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkResult mapResult = vmaMapMemory(m_allocator, m_allocation, &m_mappedData);
|
||||
if (mapResult != VK_SUCCESS || m_mappedData == nullptr) {
|
||||
MGLOG_E("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
|
||||
MGLOG_E_ONCE("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
|
||||
m_mappedData = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
@@ -138,14 +138,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool wasMapped = IsMapped();
|
||||
void* mapped = wasMapped ? m_mappedData : Map();
|
||||
if (mapped == nullptr) {
|
||||
MGLOG_E("VkBufferObject::Upload failed: unable to map buffer");
|
||||
MGLOG_E_ONCE("VkBufferObject::Upload failed: unable to map buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
|
||||
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
|
||||
if (flushResult != VK_SUCCESS) {
|
||||
MGLOG_E("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
|
||||
MGLOG_E_ONCE("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
|
||||
if (!wasMapped) {
|
||||
Unmap();
|
||||
}
|
||||
@@ -170,7 +170,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
|
||||
MGLOG_E_ONCE("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (!attachment.IsComplete()) {
|
||||
MGLOG_W("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED",
|
||||
MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED",
|
||||
drawBufferIndex,
|
||||
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
||||
fbo.GetExternalIndex());
|
||||
@@ -132,7 +132,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
auto* texture = attachment.GetTexture().get();
|
||||
if (texture == nullptr) {
|
||||
MGLOG_W("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED",
|
||||
MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED",
|
||||
drawBufferIndex,
|
||||
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
||||
fbo.GetExternalIndex());
|
||||
@@ -311,7 +311,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
if (!TryResolveSampleCountFlagBits(renderbuffer->GetSamples(), sampleCount)) {
|
||||
MGLOG_E("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u",
|
||||
MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u",
|
||||
renderbuffer->GetSamples(),
|
||||
renderbuffer->GetExternalIndex());
|
||||
return nullptr;
|
||||
@@ -457,7 +457,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, imageInfo.flags,
|
||||
&imageFormatProperties);
|
||||
if (imageFormatResult != VK_SUCCESS || (imageFormatProperties.sampleCounts & sampleCount) == 0) {
|
||||
MGLOG_E("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u",
|
||||
MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u",
|
||||
static_cast<Int>(format),
|
||||
static_cast<Int>(sampleCount),
|
||||
renderbuffer->GetExternalIndex());
|
||||
@@ -594,27 +594,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkRenderPassManager::HashType VkRenderPassManager::ComputeHash(
|
||||
const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear,
|
||||
Bool includeDefaultFboDepthStencil) {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), m_config.CacheVersion));
|
||||
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
||||
if (isDefaultFbo) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &swapchainImageIndex, sizeof(swapchainImageIndex)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &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)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled)));
|
||||
auto& drawBuffers = fbo.GetDrawBuffers();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
|
||||
auto readBuffer = fbo.GetReadBuffer();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &readBuffer, sizeof(FramebufferAttachmentType)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &readBuffer, sizeof(FramebufferAttachmentType)));
|
||||
Int validDrawBufCount = 0;
|
||||
for (Int i = 0; i < drawBuffers.size(); ++i) {
|
||||
auto drawbuf = drawBuffers[i];
|
||||
if (drawbuf != FramebufferAttachmentType::None)
|
||||
validDrawBufCount = std::max(validDrawBufCount, i + 1);
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &validDrawBufCount, sizeof(validDrawBufCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &validDrawBufCount, sizeof(validDrawBufCount)));
|
||||
|
||||
auto combineFramebufferAttachmentObjHash = [&](FramebufferAttachmentType attachment) {
|
||||
auto& att = fbo.GetAttachment(attachment);
|
||||
@@ -623,49 +623,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (att.IsEmpty()) type = 0;
|
||||
else if (att.IsTexture()) type = 1;
|
||||
else if (att.IsRenderbuffer()) type = 2;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &type, sizeof(type)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &type, sizeof(type)));
|
||||
void* contentPtr = nullptr;
|
||||
if (att.IsTexture())
|
||||
contentPtr = att.GetTexture().get();
|
||||
else if (att.IsRenderbuffer())
|
||||
contentPtr = att.GetRenderbuffer().get();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &contentPtr, sizeof(contentPtr)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &contentPtr, sizeof(contentPtr)));
|
||||
if (att.IsTexture()) {
|
||||
const Uint64 textureLifetimeId = att.GetTexture()->GetLifetimeId();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLifetimeId, sizeof(textureLifetimeId)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &textureLifetimeId, sizeof(textureLifetimeId)));
|
||||
const Int textureLevel = att.GetTextureLevel();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLevel, sizeof(textureLevel)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &textureLevel, sizeof(textureLevel)));
|
||||
const TextureUploadTarget textureUploadTarget = att.GetTextureUploadTarget();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureUploadTarget, sizeof(textureUploadTarget)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &textureUploadTarget, sizeof(textureUploadTarget)));
|
||||
const Int textureLayer = att.GetTextureLayer();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayer, sizeof(textureLayer)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &textureLayer, sizeof(textureLayer)));
|
||||
const Bool textureLayered = att.IsLayered();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayered, sizeof(textureLayered)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &textureLayered, sizeof(textureLayered)));
|
||||
|
||||
Uint64 imageIdentity = 0;
|
||||
auto* texture = att.GetTexture().get();
|
||||
auto* resource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
|
||||
if (resource != nullptr) {
|
||||
imageIdentity = reinterpret_cast<Uint64>(resource->image);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &resource->sampleCount, sizeof(resource->sampleCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &resource->sampleCount, sizeof(resource->sampleCount)));
|
||||
} else {
|
||||
const VkSampleCountFlagBits fallbackSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &fallbackSampleCount, sizeof(fallbackSampleCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &fallbackSampleCount, sizeof(fallbackSampleCount)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &imageIdentity, sizeof(imageIdentity)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &imageIdentity, sizeof(imageIdentity)));
|
||||
}
|
||||
|
||||
if (includePendingClear && att.IsTexture()) {
|
||||
auto* texture = att.GetTexture().get();
|
||||
const auto pendingClearKey = VkClearManager::MakePendingClearKey(att);
|
||||
auto hasClear = m_clearManager.HasPendingClear(pendingClearKey);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &hasClear, sizeof(hasClear)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &hasClear, sizeof(hasClear)));
|
||||
if (hasClear) {
|
||||
ClearAttachmentPayload clearPayload{};
|
||||
Bool hasPayload = m_clearManager.GetPendingClear(pendingClearKey, clearPayload);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &hasPayload, sizeof(hasPayload)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &hasPayload, sizeof(hasPayload)));
|
||||
if (hasPayload) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &clearPayload.mask, sizeof(clearPayload.mask)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &clearPayload.mask, sizeof(clearPayload.mask)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -695,7 +695,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
currentLayout = textureResource->layout;
|
||||
}
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, ¤tLayout, sizeof(currentLayout)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), ¤tLayout, sizeof(currentLayout)));
|
||||
}
|
||||
if (att.IsRenderbuffer() && att.GetRenderbuffer()) {
|
||||
const auto& renderbuffer = att.GetRenderbuffer();
|
||||
@@ -703,10 +703,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Int width = renderbuffer->GetWidth();
|
||||
const Int height = renderbuffer->GetHeight();
|
||||
const Int samples = renderbuffer->GetSamples();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &internalFormat, sizeof(internalFormat)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &width, sizeof(width)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &height, sizeof(height)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &samples, sizeof(samples)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &internalFormat, sizeof(internalFormat)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &width, sizeof(width)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &height, sizeof(height)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &samples, sizeof(samples)));
|
||||
|
||||
Uint64 imageIdentity = 0;
|
||||
VkImageLayout currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
@@ -714,25 +714,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (resource != nullptr) {
|
||||
imageIdentity = reinterpret_cast<Uint64>(resource->image);
|
||||
currentLayout = resource->layout;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &resource->sampleCount, sizeof(resource->sampleCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &resource->sampleCount, sizeof(resource->sampleCount)));
|
||||
} else {
|
||||
const VkSampleCountFlagBits fallbackSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &fallbackSampleCount, sizeof(fallbackSampleCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &fallbackSampleCount, sizeof(fallbackSampleCount)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &imageIdentity, sizeof(imageIdentity)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &imageIdentity, sizeof(imageIdentity)));
|
||||
|
||||
if (includePendingClear) {
|
||||
const Bool hasClear = HasPendingRenderbufferClear(att);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &hasClear, sizeof(hasClear)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &hasClear, sizeof(hasClear)));
|
||||
if (hasClear) {
|
||||
ClearAttachmentPayload clearPayload{};
|
||||
const Bool hasPayload = GetPendingRenderbufferClear(renderbuffer.get(), clearPayload);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &hasPayload, sizeof(hasPayload)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &hasPayload, sizeof(hasPayload)));
|
||||
if (hasPayload) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &clearPayload.mask, sizeof(clearPayload.mask)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &clearPayload.mask, sizeof(clearPayload.mask)));
|
||||
}
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, ¤tLayout, sizeof(currentLayout)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), ¤tLayout, sizeof(currentLayout)));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -745,13 +745,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &depthStencilIncluded, sizeof(depthStencilIncluded)));
|
||||
if (depthStencilIncluded) {
|
||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
|
||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
|
||||
}
|
||||
|
||||
return XXH64_digest(m_hashState);
|
||||
return XXH64_digest(m_hashState.Get());
|
||||
}
|
||||
|
||||
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
@@ -929,7 +929,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const auto& renderbuffer = rbAtt.GetRenderbuffer();
|
||||
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
|
||||
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
||||
MGLOG_E("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
|
||||
"renderbuffer %u; using VK_ATTACHMENT_UNUSED",
|
||||
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
|
||||
continue;
|
||||
@@ -1105,7 +1105,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
|
||||
|
||||
if (!hasClear && trackedColorLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
MGLOG_W("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; "
|
||||
MGLOG_W_ONCE("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; "
|
||||
"using LOAD_OP_DONT_CARE",
|
||||
texture->GetExternalIndex());
|
||||
desc.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
@@ -1161,7 +1161,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
|
||||
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
|
||||
if (hasDistinctDepthAndStencilAttachments) {
|
||||
MGLOG_E("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u",
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u",
|
||||
fbo.GetExternalIndex());
|
||||
}
|
||||
if (selectedDepthStencilAttachment != nullptr) {
|
||||
@@ -1223,7 +1223,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
depthAttachmentDescription.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
depthAttachmentDescription.initialLayout = loadInfo.initialLayout;
|
||||
if (trackedDepthLayout == VK_IMAGE_LAYOUT_UNDEFINED && (!clearDepth || !clearStencil)) {
|
||||
MGLOG_W("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout "
|
||||
MGLOG_W_ONCE("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout "
|
||||
"and partial/no clear; using DONT_CARE for uncleared aspects",
|
||||
depthAttachmentId);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Types.h>
|
||||
#include <unordered_map>
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
@@ -391,7 +392,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
|
||||
void CollectDeferredRenderbufferReleases(Bool destroyAll);
|
||||
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline MobileGL::XXH64State m_hashState;
|
||||
static inline ActiveRenderPassInfo s_activeRenderPass{};
|
||||
static inline Bool s_hasActiveRenderPass = false;
|
||||
static inline VkClearManager* s_clearManager = nullptr;
|
||||
|
||||
@@ -134,41 +134,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Bool singleLevelView) const {
|
||||
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), m_config->CacheVersion));
|
||||
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &singleLevelView, sizeof(singleLevelView)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &forceNearestFiltering, sizeof(forceNearestFiltering)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &singleLevelView, sizeof(singleLevelView)));
|
||||
|
||||
const auto minFilter = sampler.GetMinFilter();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &minFilter, sizeof(minFilter)));
|
||||
const auto magFilter = sampler.GetMagFilter();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &magFilter, sizeof(magFilter)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &magFilter, sizeof(magFilter)));
|
||||
const auto mipmapMode = sampler.GetMipmapMode();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &mipmapMode, sizeof(mipmapMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &mipmapMode, sizeof(mipmapMode)));
|
||||
const auto wrapS = sampler.GetWrapS();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapS, sizeof(wrapS)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &wrapS, sizeof(wrapS)));
|
||||
const auto wrapT = sampler.GetWrapT();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &wrapT, sizeof(wrapT)));
|
||||
const auto wrapR = sampler.GetWrapR();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &wrapR, sizeof(wrapR)));
|
||||
const auto maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
||||
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &minLod, sizeof(minLod)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &maxLod, sizeof(maxLod)));
|
||||
const auto lodBias = sampler.GetLodBias();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &lodBias, sizeof(lodBias)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &lodBias, sizeof(lodBias)));
|
||||
// The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan
|
||||
// will not apply (NEAREST filtering, or requests past the device limit) must still share one
|
||||
// VkSampler, while two samplers that really do differ must not collide onto the first one's.
|
||||
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &maxAnisotropy, sizeof(maxAnisotropy)));
|
||||
const auto compareMode = sampler.GetCompareMode();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &compareMode, sizeof(compareMode)));
|
||||
const auto compareFunc = sampler.GetSamplerCompareFunc();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &compareFunc, sizeof(compareFunc)));
|
||||
const auto borderColor = ResolveVkBorderColor(sampler, texture);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor)));
|
||||
return XXH64_digest(m_hashState);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &borderColor, sizeof(borderColor)));
|
||||
return XXH64_digest(m_hashState.Get());
|
||||
}
|
||||
|
||||
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "../VkIncludes.h"
|
||||
#include "../VulkanRendererConfig.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Types.h>
|
||||
#include <MG_State/GLState/SamplerState/SamplerObject.h>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
@@ -85,6 +86,6 @@ private:
|
||||
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();
|
||||
static inline MobileGL::XXH64State m_hashState;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -300,8 +300,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, newResource.image, newResource.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels,
|
||||
newResource.arrayLayers);
|
||||
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare destination image");
|
||||
|
||||
VkImageLayout srcTrackedLayout = oldResource.layout;
|
||||
@@ -311,8 +310,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, oldResource.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels,
|
||||
oldResource.arrayLayers);
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels);
|
||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare source image");
|
||||
|
||||
Vector<VkImageCopy> copyRegions;
|
||||
@@ -344,8 +342,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, newResource.image, newResource.layout, oldResource.layout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels,
|
||||
newResource.arrayLayers);
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to restore destination layout");
|
||||
|
||||
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(texture preserve)");
|
||||
@@ -950,7 +947,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
||||
const VkImageAspectFlags sampledAspect = ResolveSampledImageViewAspectMask(resource->aspect);
|
||||
const VkImageAspectFlags sampledAspect =
|
||||
ResolveSampledImageViewAspectMask(resource->aspect, texture.GetDepthStencilTextureMode());
|
||||
perMipSampledView = CreateImageView(resource->image, resource->format, sampledAspect, resource->viewType,
|
||||
mipLevel, 1, 0, resource->arrayLayers, &sampledComponents);
|
||||
if (perMipSampledView == VK_NULL_HANDLE) {
|
||||
@@ -974,13 +972,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return resource->sampledView;
|
||||
}
|
||||
if (!AreSampledImageViewFormatsCompatible(resource->format, format)) {
|
||||
MGLOG_E("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
|
||||
MGLOG_E_ONCE("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||
static_cast<Int>(resource->format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||
MGLOG_E("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
|
||||
MGLOG_E_ONCE("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
@@ -1000,7 +998,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||
if ((formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) {
|
||||
MGLOG_E("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
|
||||
MGLOG_E_ONCE("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
|
||||
"for textureId=%d (available=0x%x)",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||
static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
||||
@@ -1014,7 +1012,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource->sampledBaseMipLevel, resource->sampledLevelCount, 0, resource->arrayLayers,
|
||||
&sampledComponents, VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
MGLOG_E("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
|
||||
MGLOG_E_ONCE("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
@@ -1042,14 +1040,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
format = resource->format;
|
||||
}
|
||||
if (!AreStorageImageViewFormatsCompatible(resource->format, format)) {
|
||||
MGLOG_E("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
|
||||
MGLOG_E_ONCE("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||
static_cast<Int>(resource->format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
if (format != resource->format &&
|
||||
(resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||
MGLOG_E("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
|
||||
MGLOG_E_ONCE("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
@@ -1069,7 +1067,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
break;
|
||||
case VK_IMAGE_VIEW_TYPE_3D:
|
||||
MGLOG_E("%s: non-layered 3D storage views are unsupported for textureId=%d",
|
||||
MGLOG_E_ONCE("%s: non-layered 3D storage views are unsupported for textureId=%d",
|
||||
__func__, texture.GetExternalIndex());
|
||||
return VK_NULL_HANDLE;
|
||||
default:
|
||||
@@ -1078,7 +1076,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
if (viewType != resource->viewType) {
|
||||
if (layer < 0 || static_cast<Uint32>(layer) >= resource->arrayLayers) {
|
||||
MGLOG_E("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
|
||||
MGLOG_E_ONCE("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
|
||||
__func__, layer, texture.GetExternalIndex(), resource->arrayLayers);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
@@ -1113,7 +1111,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||
if ((formatProperties.optimalTilingFeatures & requiredFormatFeatures) != requiredFormatFeatures) {
|
||||
MGLOG_E("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
|
||||
MGLOG_E_ONCE("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
|
||||
"(available=0x%x)",
|
||||
__func__, static_cast<Int>(format), static_cast<Uint32>(requiredFormatFeatures),
|
||||
texture.GetExternalIndex(), static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
||||
@@ -1124,7 +1122,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
mipLevel, 1, baseArrayLayer, layerCount, nullptr,
|
||||
VK_IMAGE_USAGE_STORAGE_BIT);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
MGLOG_E("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
|
||||
MGLOG_E_ONCE("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
|
||||
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
@@ -1190,7 +1188,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool lowerTransitioned = TransitionImageLayout(
|
||||
commandBuffer, resource.image, lowerMipLayout, newLayout,
|
||||
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
||||
resource.aspect, 0, writtenMipLevel, resource.arrayLayers);
|
||||
resource.aspect, 0, writtenMipLevel);
|
||||
MOBILEGL_ASSERT(lowerTransitioned,
|
||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition lower mip levels for textureId=%d",
|
||||
texture->GetExternalIndex());
|
||||
@@ -1202,8 +1200,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool upperTransitioned = TransitionImageLayout(
|
||||
commandBuffer, resource.image, upperMipLayout, newLayout,
|
||||
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
||||
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel,
|
||||
resource.arrayLayers);
|
||||
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel);
|
||||
MOBILEGL_ASSERT(upperTransitioned,
|
||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition upper mip levels for textureId=%d",
|
||||
texture->GetExternalIndex());
|
||||
@@ -1222,7 +1219,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
if (resource->layout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
MGLOG_W("TransitionTextureForSampling: textureId=%d is still in VK_IMAGE_LAYOUT_UNDEFINED before sampling",
|
||||
MGLOG_W_ONCE("TransitionTextureForSampling: textureId=%d is still in VK_IMAGE_LAYOUT_UNDEFINED before sampling",
|
||||
texture.GetExternalIndex());
|
||||
}
|
||||
|
||||
@@ -1256,8 +1253,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
||||
s_sampledReadStages, srcAccessMask,
|
||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
||||
resource->arrayLayers);
|
||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
StampResourceRecordingUse(*resource);
|
||||
@@ -1287,7 +1283,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_IMAGE_LAYOUT_GENERAL, srcStageMask,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, srcAccessMask,
|
||||
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
||||
resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
|
||||
resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
|
||||
texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
@@ -1354,8 +1350,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageLayout& trackedLayout, VkImageLayout newLayout,
|
||||
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
|
||||
VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask,
|
||||
VkImageAspectFlags aspectMask, Uint32 baseMipLevel, Uint32 levelCount,
|
||||
Uint32 layerCount) {
|
||||
VkImageAspectFlags aspectMask, Uint32 baseMipLevel,
|
||||
Uint32 levelCount) {
|
||||
MOBILEGL_ASSERT(image != VK_NULL_HANDLE, "TransitionImageLayout: m_image == VK_NULL_HANDLE");
|
||||
MOBILEGL_ASSERT(!((dstAccessMask & VK_ACCESS_TRANSFER_READ_BIT) != 0 &&
|
||||
(dstStageMask & VK_PIPELINE_STAGE_TRANSFER_BIT) == 0),
|
||||
@@ -1380,7 +1376,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
barrier.subresourceRange.baseMipLevel = baseMipLevel;
|
||||
barrier.subresourceRange.levelCount = levelCount;
|
||||
barrier.subresourceRange.baseArrayLayer = 0;
|
||||
barrier.subresourceRange.layerCount = layerCount;
|
||||
// Every layer, always - see the declaration for why layout tracking leaves no other
|
||||
// correct answer. VK_REMAINING_ARRAY_LAYERS rather than the image's own `arrayLayers`
|
||||
// because those are not the same number for a 3D image: MobileGL creates 3D images
|
||||
// 2D_ARRAY_COMPATIBLE and their arrayLayers is 1, which today Vulkan reads as "all depth
|
||||
// slices" but will read as "depth slice 0" once VK_KHR_maintenance9 is enabled. The
|
||||
// validation layer warns about that literal 1 by name.
|
||||
barrier.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS;
|
||||
vkCmdPipelineBarrier(commandBuffer, srcStageMask, dstStageMask, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
|
||||
trackedLayout = newLayout;
|
||||
@@ -1573,7 +1575,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// targets this manager has no Vulkan image shape for yet (cube map arrays above all).
|
||||
// Declining the sync leaves the texture unbacked - wrong, but recoverable - where an
|
||||
// assertion would take the whole process down instead.
|
||||
MGLOG_W("SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
|
||||
MGLOG_W_ONCE("SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
|
||||
"mipLevels=%u vkViewType=%d",
|
||||
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
||||
MG_Util::ConvertTextureTargetToString(texture.GetTarget()).c_str(), texture.GetExternalIndex(),
|
||||
@@ -1802,7 +1804,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Losing reinterpreted views only degrades the formatless-image feature for
|
||||
// this texture; failing creation would lose the texture entirely, so retry
|
||||
// as a plain immutable-format image.
|
||||
MGLOG_W("%s: mutable image format=%d is unsupported for textureId=%d; creating "
|
||||
MGLOG_W_ONCE("%s: mutable image format=%d is unsupported for textureId=%d; creating "
|
||||
"without VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT (format reinterpretation "
|
||||
"will be unavailable for it)",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
||||
@@ -1820,7 +1822,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
|
||||
// format; failing creation would lose the texture entirely. Remembered so later syncs
|
||||
// neither reprobe nor flag-mismatch against this image and recreate it.
|
||||
MGLOG_W("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
|
||||
MGLOG_W_ONCE("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
|
||||
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
|
||||
"unavailable for it)",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
||||
@@ -1852,7 +1854,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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 "
|
||||
// E_ONCE, not F: the comment above says it - this is a soft failure the caller
|
||||
// recovers from, and it re-fires on every sync of every texture the driver refuses.
|
||||
MGLOG_E_ONCE("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,
|
||||
@@ -1990,7 +1994,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Bound the idle pool: a one-off giant upload (initial atlas define)
|
||||
// must not pin its staging memory forever.
|
||||
constexpr VkDeviceSize kMaxFreeUploadStagingBytes = 32u * 1024u * 1024u;
|
||||
if (m_allocator == nullptr || m_freeUploadStagingBytes + block.capacity > kMaxFreeUploadStagingBytes) {
|
||||
if (m_allocator == nullptr) {
|
||||
// The normal shutdown path destroys the free list through
|
||||
// DestroyUploadPools while the allocator is still valid, so this is a
|
||||
// defensive backstop only. Never pass a null allocator to VMA.
|
||||
MGLOG_W_ONCE("VkTextureManager::RecycleUploadStagingBlock called with a null allocator");
|
||||
return;
|
||||
}
|
||||
if (m_freeUploadStagingBytes + block.capacity > kMaxFreeUploadStagingBytes) {
|
||||
vmaDestroyBuffer(m_allocator, block.buffer, block.allocation);
|
||||
return;
|
||||
}
|
||||
@@ -2238,7 +2249,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (resource.fullView == VK_NULL_HANDLE) {
|
||||
return false;
|
||||
}
|
||||
const VkImageAspectFlags sampledAspect = ResolveSampledImageViewAspectMask(resource.aspect);
|
||||
const VkImageAspectFlags sampledAspect =
|
||||
ResolveSampledImageViewAspectMask(resource.aspect, texture.GetDepthStencilTextureMode());
|
||||
resource.sampledView = CreateImageView(resource.image, resource.format, sampledAspect, resource.viewType,
|
||||
baseMipLevel, levelCount, 0, resource.arrayLayers, &sampledComponents);
|
||||
if (resource.sampledView == VK_NULL_HANDLE) {
|
||||
@@ -2424,7 +2436,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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",
|
||||
MGLOG_E_ONCE("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);
|
||||
@@ -2601,7 +2613,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
uploadSrcAccessMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
||||
aspectMask, 0, outResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionImageLayout to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL failed");
|
||||
|
||||
// Array textures keep their GL "depth" in VkImage array layers, so the
|
||||
@@ -2705,7 +2717,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
s_sampledReadStages,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
VK_ACCESS_SHADER_READ_BIT,
|
||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
||||
aspectMask, 0, outResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionImageLayout to sampled read-only layout failed");
|
||||
outResource.layout = finalLayout;
|
||||
|
||||
@@ -2860,10 +2872,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
VkImageAspectFlags VkTextureManager::ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect) {
|
||||
VkImageAspectFlags VkTextureManager::ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect,
|
||||
GLenum depthStencilTextureMode) {
|
||||
if ((imageAspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
|
||||
return VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
}
|
||||
// A sampled view of a combined depth/stencil image may name exactly one aspect
|
||||
// (VUID-VkDescriptorImageInfo-imageView-01976), and GL_DEPTH_STENCIL_TEXTURE_MODE is
|
||||
// what picks it - the whole content of GL_ARB_stencil_texturing. Depth stays the
|
||||
// default, so nothing that never sets the mode changes shape. The texture's params
|
||||
// version moves with the mode, which is what makes the cached views be rebuilt.
|
||||
if (depthStencilTextureMode == GL_STENCIL_INDEX && (imageAspect & VK_IMAGE_ASPECT_STENCIL_BIT) != 0) {
|
||||
return VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
}
|
||||
if ((imageAspect & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) {
|
||||
return VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
}
|
||||
|
||||
@@ -379,17 +379,33 @@ public:
|
||||
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
|
||||
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
|
||||
|
||||
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect);
|
||||
// `depthStencilTextureMode` is the texture's GL_DEPTH_STENCIL_TEXTURE_MODE; it only decides
|
||||
// anything for an image that carries both aspects. Defaulted so the call sites that have no
|
||||
// texture in hand keep the depth-aspect answer they have always given.
|
||||
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect,
|
||||
GLenum depthStencilTextureMode = GL_DEPTH_COMPONENT);
|
||||
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
|
||||
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||
|
||||
// Moves `image` to `newLayout` and writes the new layout back through `trackedLayout`.
|
||||
//
|
||||
// The barrier covers EVERY array layer of the image, and there is deliberately no layer
|
||||
// parameter to say otherwise: layout here is tracked per IMAGE (one `TextureResource::layout`,
|
||||
// or one caller-owned variable), so a barrier narrower than the image would leave the layers it
|
||||
// skipped in the old layout while the tracker claims they moved. Every transfer against a
|
||||
// framebuffer attachment above layer 0 - glReadPixels, glBlitFramebuffer, glCopyTexSubImage,
|
||||
// glCopyImageSubData - then ran its copy on a layer no barrier had transitioned.
|
||||
//
|
||||
// The mip range IS a parameter, because mip levels really are transitioned piecewise (see
|
||||
// UpdateTrackedImageLayoutAfterAttachmentWrite and the mipmap generation loops): those callers
|
||||
// move the complement of the level they wrote so the whole image converges on one layout again.
|
||||
// Nothing does, or can, do that per layer.
|
||||
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
|
||||
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
|
||||
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
|
||||
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask,
|
||||
Uint32 baseMipLevel = 0, Uint32 levelCount = 1,
|
||||
Uint32 layerCount = 1);
|
||||
Uint32 baseMipLevel = 0, Uint32 levelCount = 1);
|
||||
|
||||
SizeT CollectGarbage();
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(initInfo.device != VK_NULL_HANDLE, "VkTimerQueryManager::Initialize requires valid VkDevice");
|
||||
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkTimerQueryManager::Initialize requires non-zero frame count");
|
||||
if (initInfo.timestampValidBits == 0 || initInfo.timestampPeriodNs <= 0.0f || initInfo.slotsPerPool == 0) {
|
||||
MGLOG_W("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
|
||||
MGLOG_W_ONCE("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
|
||||
initInfo.timestampValidBits, initInfo.timestampPeriodNs, initInfo.slotsPerPool);
|
||||
return false;
|
||||
}
|
||||
@@ -35,7 +35,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (auto& poolState : m_pools) {
|
||||
const VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &poolState.pool);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
|
||||
MGLOG_E_ONCE("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
@@ -90,7 +90,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& poolState = m_pools[frameIndex];
|
||||
if (poolState.cursor >= m_slotsPerPool) {
|
||||
if (!poolState.exhaustionWarned) {
|
||||
MGLOG_W("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
|
||||
MGLOG_W_ONCE("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
|
||||
"this frame fall back to the frontend path",
|
||||
frameIndex, m_slotsPerPool);
|
||||
poolState.exhaustionWarned = true;
|
||||
@@ -120,7 +120,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_device, m_pools[record.poolIndex].pool, record.slot, 1, sizeof(resultWithAvailability),
|
||||
resultWithAvailability, sizeof(Uint64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
|
||||
if (result != VK_SUCCESS && result != VK_NOT_READY) {
|
||||
MGLOG_E("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
|
||||
MGLOG_E_ONCE("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
|
||||
return false;
|
||||
}
|
||||
if (resultWithAvailability[1] == 0) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -229,6 +229,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
|
||||
VkImageLayout dstRestoreLayout, Bool stencilAspect);
|
||||
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
||||
// Map a GL bottom-left-origin rectangle into the display-oriented swapchain image.
|
||||
// Quarter-turn surface transforms swap the copy extent's axes.
|
||||
static Bool MapDefaultFramebufferReadbackRect(GLint x, GLint y, GLsizei width, GLsizei height,
|
||||
VkExtent2D imageExtent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
VkOffset2D* imageOffset, VkExtent2D* imageCopyExtent);
|
||||
// Reorder a tightly packed block copied with MapDefaultFramebufferReadbackRect back into
|
||||
// GL row order. The input block has swapped dimensions for 90/270 degree transforms.
|
||||
static Bool RemapDefaultFramebufferReadback(const Uint8* rawPixels, Uint32 logicalWidth,
|
||||
Uint32 logicalHeight,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
SizeT texelSize, Uint8* outPixels);
|
||||
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
||||
GLsizei width, GLsizei height, GLenum destinationFormat,
|
||||
GLenum destinationType, SizeT destinationRowStride,
|
||||
@@ -298,6 +310,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
|
||||
// honored rather than accepted-and-ignored.
|
||||
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
|
||||
// ARB_base_instance extends indirect command records with a non-zero firstInstance and
|
||||
// requires gl_InstanceID to remain zero-based. Vulkan needs both features to honor that
|
||||
// complete contract: one legalizes the command word, the other enables the shader rebase.
|
||||
Bool IsNonZeroIndirectBaseInstanceSupported() const {
|
||||
return m_drawIndirectFirstInstanceFeatureEnabled && m_shaderDrawParametersFeatureEnabled;
|
||||
}
|
||||
// Ensures the frame command buffer is recording (same lazy pattern as
|
||||
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
|
||||
// frame's pool. Null when unsupported or the pool is exhausted.
|
||||
@@ -547,6 +565,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
||||
Bool m_dualSrcBlendFeatureEnabled = false;
|
||||
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
|
||||
// multiViewport gates rasterizing into more than one of ARB_viewport_array's 16 viewports
|
||||
// (gl_ViewportIndex). m_maxRasterizableViewports is min(MAX_VIEWPORTS, device limit), or 1
|
||||
// when the feature is off, and is the viewportCount a gl_ViewportIndex-writing pipeline
|
||||
// declares - it is NOT what GL_MAX_VIEWPORTS reports, which is the frontend state width.
|
||||
Bool m_multiViewportFeatureEnabled = false;
|
||||
Uint32 m_maxRasterizableViewports = 1;
|
||||
// Union of shader stages sampled-read barriers may name; built at device creation
|
||||
// because geometry/tessellation stage bits are invalid in a barrier when their
|
||||
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
|
||||
@@ -830,6 +854,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// re-resolve just the pipeline against the active pass; a change that
|
||||
// flips it must fall back to the full path's pass selection.
|
||||
Bool drawUsesDepthStencil = false;
|
||||
// The snapshotting draw's pipeline viewportCount. A pure function of the PROGRAM
|
||||
// (writesViewportIndexBuiltin) and of a device feature fixed at renderer init, both
|
||||
// of which the programLifetimeId/programVersion guards above already pin - carried
|
||||
// here so the fast path does not re-fetch the program object to re-derive it.
|
||||
Uint32 viewportCount = 1;
|
||||
IntVec2 renderPassExtent = {0, 0};
|
||||
// colorAttachmentCount of the snapshotting draw's render pass: the
|
||||
// pipeline-state hash input, so the fast path can refresh that hash and
|
||||
@@ -1120,7 +1149,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
|
||||
// bias, line width, stencil), gated behind one render-state-parameters-version
|
||||
// compare per command buffer - see the gate fields in DynamicStateShadow.
|
||||
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo);
|
||||
// viewportCount is the bound pipeline's declared viewport count: 1 for every program that
|
||||
// does not write gl_ViewportIndex (the memoized fast path), otherwise the renderer's
|
||||
// rasterizable viewport count, which takes the unmemoized array path.
|
||||
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo,
|
||||
Uint32 viewportCount = 1);
|
||||
void ApplyMultiViewportDynamicState(VkCommandBuffer commandBuffer, Uint32 viewportCount, const IntVec2& extent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo);
|
||||
VkRect2D ComputeGLScissorRect(Uint32 index, const IntVec2& extent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo) const;
|
||||
// How many viewports a draw with this program rasterizes into: 1 unless the program
|
||||
// assigns gl_ViewportIndex AND the device enabled multiViewport. Both the pipeline's
|
||||
// baked viewportCount and the dynamic arrays come from this one answer, so they cannot
|
||||
// disagree.
|
||||
Uint32 ResolveDrawViewportCount(Bool programWritesViewportIndex) const {
|
||||
return programWritesViewportIndex && m_multiViewportFeatureEnabled ? m_maxRasterizableViewports : 1u;
|
||||
}
|
||||
|
||||
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
|
||||
@@ -74,6 +74,18 @@ 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).
|
||||
//
|
||||
// MGLOG_F and deliberately NOT latched. VK_VERIFY is the invariant-check macro: a Vulkan call
|
||||
// MobileGL believes it has already made legal came back non-success, which is a
|
||||
// should-never-happen state, not an expected failure mode a user hits. Those fast-fail loudly
|
||||
// and keep saying so - the log-quietness rules that latch W/E cover expected failures (driver
|
||||
// capability gaps, app misuse), not broken internal invariants. MOBILEGL_ASSERT below traps in
|
||||
// a DEBUG build; MGLOG_F is what makes the same condition visible in an INFO test run, where
|
||||
// the assert is compiled out by contract.
|
||||
//
|
||||
// A soft, recoverable failure must therefore NOT be routed through VK_VERIFY. Check the
|
||||
// VkResult directly and report it with MGLOG_E_ONCE - see VkTextureManager::SyncTextureResource,
|
||||
// where a driver legitimately refuses an image the format pre-check accepted.
|
||||
#define VK_VERIFY(expr, ...) \
|
||||
do { \
|
||||
VkResult _vk_verify_result = (expr); \
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
EGLStateContext* GetState() {
|
||||
if (!MG_State::pEGLContext) {
|
||||
MGLOG_E("pEGLContext is null. MG_State may not be initialized.");
|
||||
MGLOG_E_ONCE("pEGLContext is null. MG_State may not be initialized.");
|
||||
}
|
||||
return MG_State::pEGLContext.get();
|
||||
}
|
||||
@@ -146,7 +146,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
state->DestroySurface(dpy, surface);
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
@@ -172,11 +172,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (!backendObject->SwapEGLBuffers(dpy, draw)) {
|
||||
MGLOG_E("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
|
||||
MGLOG_E_ONCE("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
|
||||
state->SetError(EGL_BAD_SURFACE);
|
||||
return EGL_FALSE;
|
||||
}
|
||||
@@ -211,7 +211,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (!backendObject->InitializeEGLDisplay(dpy, major, minor)) {
|
||||
@@ -265,7 +265,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
if (releaseCurrentRequest) {
|
||||
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
|
||||
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
|
||||
MGLOG_E("eglMakeCurrent release failed in backend thread=%s", threadId.c_str());
|
||||
MGLOG_E_ONCE("eglMakeCurrent release failed in backend thread=%s", threadId.c_str());
|
||||
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
||||
state->SetError(EGL_BAD_ACCESS);
|
||||
return EGL_FALSE;
|
||||
@@ -277,12 +277,12 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
|
||||
MGLOG_E("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
|
||||
MGLOG_E_ONCE("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
|
||||
dpy, draw, read, ctx);
|
||||
state->SetError(EGL_BAD_ACCESS);
|
||||
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
||||
@@ -703,7 +703,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
state->DestroySurface(dpy, surface);
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
@@ -726,7 +726,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
}
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
return EGL_FALSE;
|
||||
}
|
||||
width = std::max<EGLint>(width, 1);
|
||||
@@ -764,7 +764,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
MGLOG_D("eglGetProcAddress(%s)", name);
|
||||
void* proc = MG_Impl::GetProcAddress(name);
|
||||
if (!proc) {
|
||||
MGLOG_W("Failed to get function: %s", name);
|
||||
MGLOG_D("Failed to get function: %s", name);
|
||||
return nullptr;
|
||||
}
|
||||
return (__eglMustCastToProperFunctionPointerType)proc;
|
||||
|
||||
@@ -149,7 +149,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// quietly writing a differently-sized pattern.
|
||||
const SizeT sourceSize = MG_Util::GetInputBytesPerPixel(inputFormat, pixelType);
|
||||
if (sourceSize != elementSize) {
|
||||
MGLOG_W("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
|
||||
MGLOG_W_ONCE("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
|
||||
"converting between them is not implemented",
|
||||
GetBufferOpName(op), sourceSize, internalformat, elementSize);
|
||||
}
|
||||
|
||||
@@ -1259,7 +1259,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "instancecount must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (!MG_State::pGLContext->ValidateTransformFeedbackName(id)) {
|
||||
// "id is not the name of a transform feedback object" has to mean the same thing here
|
||||
// as it does to glIsTransformFeedback, and the two predicates are not interchangeable:
|
||||
// a name glGenTransformFeedbacks handed out is only reserved until it is first bound,
|
||||
// and only the bind turns it into an object (GL 4.6 core 13.2.1). ValidateTransformFeedbackName
|
||||
// answers the reservation question - the right one for glBindTransformFeedback, which is
|
||||
// what turns a reserved name into an object - so using it here let a generated-but-unbound
|
||||
// name through to the completed-span check below and raised INVALID_OPERATION where the
|
||||
// spec asks for INVALID_VALUE. Name 0 is the default object and always drawable.
|
||||
if (id != 0 && !MG_State::pGLContext->IsTransformFeedbackObject(id)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
|
||||
@@ -25,12 +25,12 @@
|
||||
#define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
|
||||
|
||||
#define DECLARE_GL_FUNCTION_STUB_END(type, name, ...) \
|
||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \
|
||||
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
|
||||
return (type)1; \
|
||||
}
|
||||
|
||||
#define DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(type, name, ...) \
|
||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \
|
||||
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
|
||||
}
|
||||
|
||||
#define DECLARE_GL_FUNCTION_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
|
||||
@@ -969,14 +969,14 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL3dv, GLuint index, const GLdoub
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL4dv, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL4dv, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLPointer, index, size, type, stride, pointer)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexAttribLdv, GLuint index, GLenum pname, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexAttribLdv, index, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedfv, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedfv, index, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetFloati_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetDoublei_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance)
|
||||
@@ -1061,7 +1061,7 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage1D, GLuint texture, GLint level, G
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, type, pixels)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
|
||||
@@ -1849,7 +1849,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage3DEXT, GLuint texture,
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage2DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage2DEXT, texture, target, level, internalformat, width, height, border, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage1DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage1DEXT, texture, target, level, internalformat, width, border, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3DEXT, texture, target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2DEXT, texture, target, level, xoffset, yoffset, width, height, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1DEXT, texture, target, level, xoffset, width, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImageEXT, GLuint texture, GLenum target, GLint lod, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImageEXT, texture, target, lod, img)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage3DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage3DEXT, texunit, target, level, internalformat, width, height, depth, border, imageSize, bits)
|
||||
@@ -2585,7 +2585,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedbackNV, GLenum target, GLui
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacksNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacksNV, n, ids)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacksNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacksNV, n, ids)
|
||||
MOBILEGL_GL_API GLboolean glIsTransformFeedbackNV(GLuint id) {
|
||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
||||
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
|
||||
return GL_FALSE;
|
||||
}
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, )
|
||||
@@ -3181,5 +3181,5 @@ MOBILEGL_GL_API void glVertexAttribDivisorARB(GLuint index, GLuint divisor) {
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glWindowRectanglesEXT(GLenum mode, GLsizei count, const GLint* box) {
|
||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
||||
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
|
||||
}
|
||||
|
||||
@@ -547,7 +547,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
|
||||
auto blitNamedFramebuffer = MG_Backend::gBackendFunctionsTable.GL.BlitNamedFramebuffer;
|
||||
if (!blitNamedFramebuffer) {
|
||||
MGLOG_E("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
|
||||
MGLOG_E_ONCE("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
|
||||
return;
|
||||
}
|
||||
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
||||
@@ -558,7 +558,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
||||
auto clearNamedFramebufferfv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfv;
|
||||
if (!clearNamedFramebufferfv) {
|
||||
MGLOG_E("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
||||
@@ -568,7 +568,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||
auto clearNamedFramebufferfi = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfi;
|
||||
if (!clearNamedFramebufferfi) {
|
||||
MGLOG_E("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
|
||||
@@ -578,7 +578,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum buffer, GLint drawbuffer, const GLint* value) {
|
||||
auto clearNamedFramebufferiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferiv;
|
||||
if (!clearNamedFramebufferiv) {
|
||||
MGLOG_E("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
||||
@@ -588,7 +588,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
||||
auto clearNamedFramebufferuiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferuiv;
|
||||
if (!clearNamedFramebufferuiv) {
|
||||
MGLOG_E("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
@@ -27,6 +28,11 @@
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Declared rather than #included from GL_RenderState.h on purpose: that header also declares
|
||||
// a free function named BlendEquation, which would hide the ::MobileGL::BlendEquation enum
|
||||
// this file's blend-state queries name unqualified.
|
||||
GLboolean IsEnabledi(GLenum target, GLuint index);
|
||||
|
||||
namespace {
|
||||
enum class IndexedBufferQueryKind {
|
||||
Binding,
|
||||
@@ -339,26 +345,70 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
||||
}
|
||||
|
||||
// The ARB_viewport_array indexed rectangles. MobileGL keeps exactly one viewport, one
|
||||
// scissor box and one depth range, so every in-range index answers with that single
|
||||
// value - but it has to come from the frontend state the non-indexed getters read.
|
||||
// The generic path at the bottom of GetIntegeri_v is a raw backend passthrough that
|
||||
// has no case for these, so routing them through it returned zeros.
|
||||
// The ARB_viewport_array indexed rectangles. Each of these is genuinely per-viewport
|
||||
// frontend state (RenderStateParameters::Viewports / ScissorBoxes / DepthRanges), so the
|
||||
// indexed getters must read the indexed storage - the generic path at the bottom of
|
||||
// GetIntegeri_v is a raw backend passthrough that has no case for them and returned
|
||||
// zeros, and routing them to the NON-indexed getter (what this used to do) answered every
|
||||
// index with viewport 0's value, which is what
|
||||
// KHR-GL43.viewport_array.{viewport,scissor,depth_range}_api caught.
|
||||
Bool IsIndexedViewportQuery(GLenum target) {
|
||||
return target == GL_VIEWPORT || target == GL_SCISSOR_BOX || target == GL_DEPTH_RANGE;
|
||||
}
|
||||
|
||||
// ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it.
|
||||
// Component count of an indexed viewport-array query, so every width of getter writes the
|
||||
// caller's whole buffer instead of just element 0 (GL 4.6 core 22.1).
|
||||
GLsizei IndexedViewportQueryComponents(GLenum target) {
|
||||
return target == GL_DEPTH_RANGE ? 2 : 4;
|
||||
}
|
||||
|
||||
// ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it. The bound is
|
||||
// the frontend's own state width, which is also exactly what GL_MAX_VIEWPORTS reports -
|
||||
// taking it from the backend caps instead would let a device limit of 1 (a Vulkan device
|
||||
// without the multiViewport feature) make index 1 illegal even though the state exists.
|
||||
Bool ValidateViewportQueryIndex(GLuint index, const char* caller) {
|
||||
GLint maxViewports = 0;
|
||||
GetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
if (index < static_cast<GLuint>(std::max(maxViewports, 1))) return true;
|
||||
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Viewport index is out of range."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// The indexed viewport/scissor/depth-range state as floats, which is the widest lossless
|
||||
// shape MobileGL stores (the viewport really is float state; the scissor box is integral
|
||||
// and well inside float's exact range, and every depth range is in [0, 1]). Every indexed
|
||||
// getter width funnels through this so they can never disagree with each other.
|
||||
void ReadIndexedViewportStateFloat(GLenum target, GLuint index, GLfloat* out) {
|
||||
switch (target) {
|
||||
case GL_VIEWPORT: {
|
||||
const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(index);
|
||||
out[0] = viewport.x();
|
||||
out[1] = viewport.y();
|
||||
out[2] = viewport.z();
|
||||
out[3] = viewport.w();
|
||||
return;
|
||||
}
|
||||
case GL_SCISSOR_BOX: {
|
||||
const IntVec4& box = MG_State::pGLContext->GetScissorBoxIndexed(index);
|
||||
out[0] = static_cast<GLfloat>(box.x());
|
||||
out[1] = static_cast<GLfloat>(box.y());
|
||||
out[2] = static_cast<GLfloat>(box.z());
|
||||
out[3] = static_cast<GLfloat>(box.w());
|
||||
return;
|
||||
}
|
||||
case GL_DEPTH_RANGE: {
|
||||
const FloatVec2& range = MG_State::pGLContext->GetDepthRangeIndexed(index);
|
||||
out[0] = range.x();
|
||||
out[1] = range.y();
|
||||
return;
|
||||
}
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "ReadIndexedViewportStateFloat: unexpected target 0x%x",
|
||||
static_cast<Uint32>(target));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void CopyIntsToBooleans(const GLint* src, SizeT count, GLboolean* dst) {
|
||||
for (SizeT i = 0; i < count; ++i) {
|
||||
dst[i] = src[i] ? GL_TRUE : GL_FALSE;
|
||||
@@ -383,7 +433,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
MGLOG_D("glGetString, name: %s", MG_Util::ConvertGLEnumToString(name).c_str());
|
||||
if (!activeBackendObject) {
|
||||
MGLOG_E("activeBackendObject is not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||
return (GLubyte*)"Unknown";
|
||||
}
|
||||
|
||||
@@ -442,7 +492,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) {
|
||||
MGLOG_E("activeBackendObject is not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||
return (GLubyte*)"Unknown";
|
||||
}
|
||||
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
||||
@@ -629,6 +679,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = dynamicParameters.ViewportBoundsRangeMax;
|
||||
return;
|
||||
}
|
||||
// Viewport 0's rectangle, verbatim. Falling through to the integer width below would
|
||||
// round the fractional rectangle a glViewportIndexedf(0, ...) is allowed to set, and
|
||||
// glGetFloatv(GL_VIEWPORT) is a lossless query of float state.
|
||||
case GL_VIEWPORT: {
|
||||
const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(0);
|
||||
params[0] = viewport.x();
|
||||
params[1] = viewport.y();
|
||||
params[2] = viewport.z();
|
||||
params[3] = viewport.w();
|
||||
return;
|
||||
}
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||
@@ -792,15 +853,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// GL 4.6 core 22.1: an indexed query answers EVERY indexed state, and GL_SCISSOR_TEST is
|
||||
// indexed by viewport just like GL_BLEND is by draw buffer. Without this the integer
|
||||
// width fell through to the backend passthrough and answered GL_INVALID_ENUM, which is
|
||||
// the sticky error KHR-GL43.viewport_array.queries trips over at its next error check.
|
||||
if (MG_Util::ConvertGLEnumToCapabilityInput(target) != CapabilityInput::Unknown) {
|
||||
*data = IsEnabledi(target, index);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (target) {
|
||||
// ARB_viewport_array queries the indexed rectangles through glGetIntegeri_v as well
|
||||
// (gl4cMultiBindTests and the viewport_array group both do). The frontend keeps one
|
||||
// viewport and one scissor box, so every in-range index reports that one.
|
||||
// (gl4cMultiBindTests and the viewport_array group both do).
|
||||
case GL_VIEWPORT:
|
||||
case GL_SCISSOR_BOX:
|
||||
case GL_DEPTH_RANGE: {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetIntegerv(target, data);
|
||||
GLfloat values[4] = {};
|
||||
ReadIndexedViewportStateFloat(target, index, values);
|
||||
const GLsizei components = IndexedViewportQueryComponents(target);
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
// Round, not truncate: glGetIntegerv on floating-point state rounds to nearest
|
||||
// (GL 4.6 core 22.2), so a 255.875-wide viewport reads back as 256 and not 255.
|
||||
data[i] = static_cast<GLint>(std::lround(values[i]));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// The vertex buffer binding points of the vertex array object that is bound. Indexed by
|
||||
// binding point, not by attribute (GL 4.6 core 10.3.1).
|
||||
case GL_VERTEX_BINDING_BUFFER:
|
||||
@@ -927,7 +1005,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (IsIndexedViewportQuery(target)) {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetFloatv(target, data);
|
||||
// Verbatim, NOT via the integer width: the viewport is float state and
|
||||
// KHR-GL43.viewport_array.viewport_api compares the read-back with ==, so a
|
||||
// glViewportIndexedf(i, 0.125f, ...) has to come back as 0.125f exactly.
|
||||
ReadIndexedViewportStateFloat(target, index, data);
|
||||
return;
|
||||
}
|
||||
GLint ints[4] = {};
|
||||
@@ -944,7 +1025,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (IsIndexedViewportQuery(target)) {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetDoublev(target, data);
|
||||
GLfloat values[4] = {};
|
||||
ReadIndexedViewportStateFloat(target, index, values);
|
||||
const GLsizei components = IndexedViewportQueryComponents(target);
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
data[i] = static_cast<GLdouble>(values[i]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
GLint ints[4] = {};
|
||||
@@ -1020,7 +1106,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// frontend-only value simply is not in the driver's table.
|
||||
GLint values[4] = {};
|
||||
GetIntegeri_v(target, index, values);
|
||||
*data = static_cast<GLint64>(values[0]);
|
||||
// The viewport-array rectangles are the only multi-component indexed state here; every
|
||||
// other pname is scalar, so widening element 0 alone would silently truncate them.
|
||||
const GLsizei components = IsIndexedViewportQuery(target) ? IndexedViewportQueryComponents(target) : 1;
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
data[i] = static_cast<GLint64>(values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void GetInteger64v(GLenum pname, GLint64* params) {
|
||||
@@ -1954,7 +2045,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) {
|
||||
MGLOG_E("activeBackendObject is not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||
return;
|
||||
}
|
||||
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
||||
@@ -2192,7 +2283,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = dynamicParameters.MaxViewportHeight;
|
||||
break;
|
||||
case GL_MAX_VIEWPORTS:
|
||||
*params = dynamicParameters.MaxViewports;
|
||||
// The frontend's own state width, not the backend's device limit. GL 4.3 core
|
||||
// requires MAX_VIEWPORTS >= 16 and every indexed viewport entry point validates
|
||||
// against RenderStateParameters::MAX_VIEWPORTS, so reporting anything else would
|
||||
// either advertise viewports the state cannot hold or reject indices it can. A
|
||||
// Vulkan device without the multiViewport feature reports maxViewports == 1, which
|
||||
// limits what can be RASTERIZED to more than one rectangle (see the multiViewport
|
||||
// gate in VulkanRenderer), not what the GL state can hold; caps.MaxViewports keeps
|
||||
// carrying that device number for exactly that decision.
|
||||
*params = static_cast<GLint>(RenderStateParameters::MAX_VIEWPORTS);
|
||||
break;
|
||||
case GL_MINOR_VERSION:
|
||||
*params = rendererInfo.RendererGLInfo.TargetGLVersion.Minor;
|
||||
@@ -2248,7 +2347,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxTextureMaxAnisotropy));
|
||||
break;
|
||||
default:
|
||||
MGLOG_E("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
|
||||
MGLOG_D("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetIntegerv",
|
||||
std::format("Invalid enum: 0x{:X}", pname)));
|
||||
|
||||
@@ -908,7 +908,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + span > programObject->GetUBOSize()) {
|
||||
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
program, location);
|
||||
return;
|
||||
}
|
||||
@@ -962,7 +962,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + span > programObject->GetUBOSize()) {
|
||||
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
program, location);
|
||||
return;
|
||||
}
|
||||
@@ -1062,7 +1062,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!initialized) {
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) {
|
||||
MGLOG_E("activeBackendObject is not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||
return;
|
||||
}
|
||||
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
||||
@@ -1152,7 +1152,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
SizeT writeSize = ItemCount * sizeof(T);
|
||||
if (size < writeSize) {
|
||||
// Metadata bug: degrade to a clamped copy instead of killing the process.
|
||||
MGLOG_E("%s: uniform size mismatch at program %u location %u: expected at least %zu bytes, got %zu "
|
||||
MGLOG_E_ONCE("%s: uniform size mismatch at program %u location %u: expected at least %zu bytes, got %zu "
|
||||
"bytes; clamping",
|
||||
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
|
||||
writeSize = size;
|
||||
@@ -1173,7 +1173,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
offset + byteOffsetInsideUniform + writeSize > uboSize) {
|
||||
// Should not happen: linking gives every settable uniform backing
|
||||
// storage. Log and drop the write instead of faulting.
|
||||
MGLOG_E("%s: uniform at program %u location %u has no backing storage (ubo=%p offset=%u size=%zu "
|
||||
MGLOG_E_ONCE("%s: uniform at program %u location %u has no backing storage (ubo=%p offset=%u size=%zu "
|
||||
"uboSize=%zu); dropping write",
|
||||
__func__, programObject.GetExternalIndex(), location, static_cast<void*>(pUBO), offset,
|
||||
writeSize, uboSize);
|
||||
@@ -1807,7 +1807,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
MGLOG_E("%s: unknown pname = %p %s", __func__, pname, MG_Util::ConvertGLEnumToString(pname).c_str());
|
||||
MGLOG_D("%s: unknown pname = %p %s", __func__, pname, MG_Util::ConvertGLEnumToString(pname).c_str());
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
|
||||
@@ -344,6 +344,41 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
void DestroyAllQueryObjects() {
|
||||
// Detach the registry under the lock and release it outside. Entries the app
|
||||
// already deleted were erased by DeleteQueries, so nothing here double-frees;
|
||||
// a DeleteQueries racing this sweep finds an empty registry and ignores the
|
||||
// names. The active-query slots and the name allocator are reset under the
|
||||
// same lock: query names are context-owned state, so a fresh context must
|
||||
// start clean instead of inheriting the dead context's allocator cursor or
|
||||
// a stale "a query is already active on this target" latch.
|
||||
UnorderedMap<GLuint, QueryObject*> orphans;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||
orphans.swap(g_liveQueryObjects);
|
||||
g_nextQueryId = 1;
|
||||
g_activeTimeElapsedQueryId = 0;
|
||||
g_activePrimitivesWrittenQueryId = 0;
|
||||
g_activePrimitivesGeneratedQueryId = 0;
|
||||
g_activeSamplesPassedQueryId = 0;
|
||||
}
|
||||
if (orphans.empty()) {
|
||||
return;
|
||||
}
|
||||
// Both backends' DeleteBackendQuery 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 deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery;
|
||||
for (const auto& [_, queryObject] : orphans) {
|
||||
if (deleteBackendQuery && queryObject->backendHandle) {
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
delete queryObject;
|
||||
}
|
||||
MGLOG_D("DestroyAllQueryObjects: reclaimed %zu query object(s) the app left undeleted", orphans.size());
|
||||
}
|
||||
|
||||
GLboolean IsQuery(GLuint id) {
|
||||
if (id == 0) {
|
||||
return GL_FALSE;
|
||||
|
||||
@@ -13,6 +13,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void GenQueries(GLsizei n, GLuint* ids);
|
||||
void CreateQueries(GLenum target, GLsizei n, GLuint* ids);
|
||||
void DeleteQueries(GLsizei n, const GLuint* ids);
|
||||
// Destroys every still-registered query object exactly as DeleteQueries would.
|
||||
// Query objects are context-owned, and MobileGL::Destroy() tears every context
|
||||
// down, so the process-global registry has to be drained there: without this the
|
||||
// QueryObject and any backend timer-query wrapper leaked across every
|
||||
// eglTerminate/eglInitialize cycle, and the active-query/name-allocator state
|
||||
// from the dead context survived into the next one. Must run while the backend
|
||||
// function table is still populated, and before a re-initialized library could
|
||||
// pair the handles with the wrong backend's DeleteBackendQuery.
|
||||
void DestroyAllQueryObjects();
|
||||
GLboolean IsQuery(GLuint id);
|
||||
void BeginQuery(GLenum target, GLuint id);
|
||||
void EndQuery(GLenum target);
|
||||
|
||||
@@ -20,28 +20,118 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return std::clamp(static_cast<Float>(value), 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
static Bool ValidateIndexedBlendCapability(GLenum target, GLuint index, const char* functionName) {
|
||||
if (target != GL_BLEND) {
|
||||
// GL 4.6 core 17.3.2 and 22.1 give exactly two indexed capabilities: GL_BLEND, indexed by
|
||||
// draw buffer, and GL_SCISSOR_TEST, indexed by viewport. They have DIFFERENT bounds
|
||||
// (MAX_DRAW_BUFFERS vs MAX_VIEWPORTS), so the limit is picked per target rather than shared.
|
||||
static Bool ValidateIndexedCapability(GLenum target, GLuint index, const char* functionName) {
|
||||
GLuint limit = 0;
|
||||
const char* indexName = nullptr;
|
||||
switch (target) {
|
||||
case GL_BLEND:
|
||||
limit = MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS;
|
||||
indexName = "Buffer";
|
||||
break;
|
||||
case GL_SCISSOR_TEST:
|
||||
limit = RenderStateParameters::MAX_VIEWPORTS;
|
||||
indexName = "Viewport";
|
||||
break;
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Only GL_BLEND is supported for indexed capability state."));
|
||||
"Only GL_BLEND and GL_SCISSOR_TEST are supported for indexed "
|
||||
"capability state."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
|
||||
if (index >= limit) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"Buffer index " + std::to_string(index) + " is out of range. Max supported is " +
|
||||
std::to_string(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS - 1) + "."));
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
String(indexName) + " index " + std::to_string(index) +
|
||||
" is out of range. Max supported is " + std::to_string(limit - 1) +
|
||||
"."));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ------------------ ARB_viewport_array parameter validation ------------------
|
||||
// All three families share the same two shapes, so they share the two checkers. GL 4.6 core
|
||||
// 13.6.1/17.3.2: an out-of-range index is GL_INVALID_VALUE, and so is a negative width or
|
||||
// height. `first + count == MAX_VIEWPORTS` is LEGAL - only strictly greater is an error,
|
||||
// which KHR-GL43.viewport_array.api_errors checks explicitly in both directions.
|
||||
static Bool ValidateViewportIndex(GLuint index, const char* functionName) {
|
||||
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Viewport index " + std::to_string(index) +
|
||||
" is out of range. Max supported is " +
|
||||
std::to_string(RenderStateParameters::MAX_VIEWPORTS - 1) + "."));
|
||||
return false;
|
||||
}
|
||||
|
||||
static Bool ValidateViewportRange(GLuint first, GLsizei count, const char* functionName) {
|
||||
if (count < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "count must not be negative."));
|
||||
return false;
|
||||
}
|
||||
// Widened before adding: first is a GLuint and count a GLsizei, so `first + count` in
|
||||
// 32 bits can wrap past MAX_VIEWPORTS and let an out-of-range range through.
|
||||
const Uint64 last = static_cast<Uint64>(first) + static_cast<Uint64>(count);
|
||||
if (last > RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"first (" + std::to_string(first) + ") + count (" +
|
||||
std::to_string(count) + ") exceeds GL_MAX_VIEWPORTS (" +
|
||||
std::to_string(RenderStateParameters::MAX_VIEWPORTS) + ")."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static Bool ValidateNonNegativeExtent(T width, T height, const char* functionName) {
|
||||
if (width >= T(0) && height >= T(0)) return true;
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "Width and height must be non-negative."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// The array forms are all-or-nothing: one bad element rejects the whole call with a SINGLE
|
||||
// GL_INVALID_VALUE and leaves every rectangle untouched. api_errors relies on both halves -
|
||||
// it passes a full 16-element array with exactly one negative extent and then asserts the
|
||||
// error queue holds exactly one entry.
|
||||
template <typename T>
|
||||
static Bool ValidateArrayExtents(GLsizei count, const T* v, const char* functionName) {
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
if (v[i * 4 + 2] >= T(0) && v[i * 4 + 3] >= T(0)) continue;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Width and height must be non-negative (element " + std::to_string(i) +
|
||||
")."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool ValidateNonNullArray(const void* v, const char* functionName) {
|
||||
if (v != nullptr) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "value pointer cannot be null."));
|
||||
return false;
|
||||
}
|
||||
|
||||
static Bool TryConvertBlendEquation(GLenum mode, const char* functionName,
|
||||
::MobileGL::BlendEquation& outEquation) {
|
||||
outEquation = MG_Util::ConvertGLEnumToBlendEquation(mode);
|
||||
@@ -93,16 +183,70 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Viewport_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||
if (width < 0 || height < 0) {
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Viewport_State",
|
||||
"Width abd height must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateNonNegativeExtent(width, height, "Viewport_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetViewport(IntVec4(x, y, width, height));
|
||||
}
|
||||
|
||||
// ------------------ ARB_viewport_array setters ------------------
|
||||
void ViewportArrayv_State(GLuint first, GLsizei count, const GLfloat* v) {
|
||||
if (!ValidateViewportRange(first, count, "ViewportArrayv_State")) return;
|
||||
if (count == 0) return;
|
||||
if (!ValidateNonNullArray(v, "ViewportArrayv_State")) return;
|
||||
if (!ValidateArrayExtents(count, v, "ViewportArrayv_State")) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->SetViewportIndexed(first + static_cast<GLuint>(i),
|
||||
FloatVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3]));
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportIndexedf_State(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
|
||||
if (!ValidateViewportIndex(index, "ViewportIndexedf_State")) return;
|
||||
if (!ValidateNonNegativeExtent(w, h, "ViewportIndexedf_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetViewportIndexed(index, FloatVec4(x, y, w, h));
|
||||
}
|
||||
|
||||
void ScissorArrayv_State(GLuint first, GLsizei count, const GLint* v) {
|
||||
if (!ValidateViewportRange(first, count, "ScissorArrayv_State")) return;
|
||||
if (count == 0) return;
|
||||
if (!ValidateNonNullArray(v, "ScissorArrayv_State")) return;
|
||||
if (!ValidateArrayExtents(count, v, "ScissorArrayv_State")) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->SetScissorBoxIndexed(first + static_cast<GLuint>(i),
|
||||
IntVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3]));
|
||||
}
|
||||
}
|
||||
|
||||
void ScissorIndexed_State(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) {
|
||||
if (!ValidateViewportIndex(index, "ScissorIndexed_State")) return;
|
||||
if (!ValidateNonNegativeExtent(width, height, "ScissorIndexed_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetScissorBoxIndexed(index, IntVec4(left, bottom, width, height));
|
||||
}
|
||||
|
||||
void DepthRangeArrayv_State(GLuint first, GLsizei count, const GLdouble* v) {
|
||||
if (!ValidateViewportRange(first, count, "DepthRangeArrayv_State")) return;
|
||||
if (count == 0) return;
|
||||
if (!ValidateNonNullArray(v, "DepthRangeArrayv_State")) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->SetDepthRangeIndexed(
|
||||
first + static_cast<GLuint>(i),
|
||||
FloatVec2(ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 0])),
|
||||
ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 1]))));
|
||||
}
|
||||
}
|
||||
|
||||
void DepthRangeIndexed_State(GLuint index, GLdouble n, GLdouble f) {
|
||||
if (!ValidateViewportIndex(index, "DepthRangeIndexed_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetDepthRangeIndexed(
|
||||
index, FloatVec2(ClampUnitFloat(static_cast<GLfloat>(n)), ClampUnitFloat(static_cast<GLfloat>(f))));
|
||||
}
|
||||
|
||||
void StencilOpSeparate_State(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
||||
Bool applyFront = false;
|
||||
Bool applyBack = false;
|
||||
@@ -175,12 +319,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Scissor_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||
if (width < 0 || height < 0) {
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Scissor_State",
|
||||
"Width abd height must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateNonNegativeExtent(width, height, "Scissor_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetScissorBox(IntVec4(x, y, width, height));
|
||||
}
|
||||
@@ -336,7 +475,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
GLboolean IsEnabledi_State(GLenum target, GLuint index) {
|
||||
if (!ValidateIndexedBlendCapability(target, index, "IsEnabledi_State")) {
|
||||
if (!ValidateIndexedCapability(target, index, "IsEnabledi_State")) {
|
||||
return GL_FALSE;
|
||||
}
|
||||
|
||||
@@ -392,7 +531,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
GLint values[4] = {};
|
||||
GetIntegeri_v(target, index, values);
|
||||
*data = values[0] != 0 ? GL_TRUE : GL_FALSE;
|
||||
// The ARB_viewport_array rectangles are the only multi-component indexed state that
|
||||
// reaches here; writing element 0 alone would leave the caller's other three untouched.
|
||||
const GLsizei components = target == GL_VIEWPORT || target == GL_SCISSOR_BOX
|
||||
? 4
|
||||
: (target == GL_DEPTH_RANGE ? 2 : 1);
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
data[i] = values[i] != 0 ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
GLboolean IsEnabled_State(GLenum cap) {
|
||||
@@ -725,7 +871,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Disablei_State(GLenum target, GLuint index) {
|
||||
if (!ValidateIndexedBlendCapability(target, index, "Disablei_State")) {
|
||||
if (!ValidateIndexedCapability(target, index, "Disablei_State")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -743,7 +889,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Enablei_State(GLenum target, GLuint index) {
|
||||
if (!ValidateIndexedBlendCapability(target, index, "Enablei_State")) {
|
||||
if (!ValidateIndexedCapability(target, index, "Enablei_State")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -797,6 +943,44 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Viewport_State(x, y, width, height);
|
||||
}
|
||||
|
||||
void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v) {
|
||||
ViewportArrayv_State(first, count, v);
|
||||
}
|
||||
|
||||
void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
|
||||
ViewportIndexedf_State(index, x, y, w, h);
|
||||
}
|
||||
|
||||
void ViewportIndexedfv(GLuint index, const GLfloat* v) {
|
||||
// The index is validated before the pointer is touched: glViewportIndexedfv(MAX, nullptr)
|
||||
// must be one GL_INVALID_VALUE, not a null dereference.
|
||||
if (!ValidateViewportIndex(index, "ViewportIndexedfv")) return;
|
||||
if (!ValidateNonNullArray(v, "ViewportIndexedfv")) return;
|
||||
ViewportIndexedf_State(index, v[0], v[1], v[2], v[3]);
|
||||
}
|
||||
|
||||
void ScissorArrayv(GLuint first, GLsizei count, const GLint* v) {
|
||||
ScissorArrayv_State(first, count, v);
|
||||
}
|
||||
|
||||
void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) {
|
||||
ScissorIndexed_State(index, left, bottom, width, height);
|
||||
}
|
||||
|
||||
void ScissorIndexedv(GLuint index, const GLint* v) {
|
||||
if (!ValidateViewportIndex(index, "ScissorIndexedv")) return;
|
||||
if (!ValidateNonNullArray(v, "ScissorIndexedv")) return;
|
||||
ScissorIndexed_State(index, v[0], v[1], v[2], v[3]);
|
||||
}
|
||||
|
||||
void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v) {
|
||||
DepthRangeArrayv_State(first, count, v);
|
||||
}
|
||||
|
||||
void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f) {
|
||||
DepthRangeIndexed_State(index, n, f);
|
||||
}
|
||||
|
||||
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
||||
StencilOpSeparate_State(face, sfail, dpfail, dppass);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void Enablei(GLenum target, GLuint index);
|
||||
void BlendFunc(GLenum sfactor, GLenum dfactor);
|
||||
void Viewport(GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
// ARB_viewport_array (core since GL 4.1). Every one of these addresses the same 16-element
|
||||
// indexed state the classic glViewport/glScissor/glDepthRange trio broadcasts to.
|
||||
void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v);
|
||||
void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h);
|
||||
void ViewportIndexedfv(GLuint index, const GLfloat* v);
|
||||
void ScissorArrayv(GLuint first, GLsizei count, const GLint* v);
|
||||
void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height);
|
||||
void ScissorIndexedv(GLuint index, const GLint* v);
|
||||
void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v);
|
||||
void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f);
|
||||
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);
|
||||
void StencilOp(GLenum fail, GLenum zfail, GLenum zpass);
|
||||
void StencilMaskSeparate(GLenum face, GLuint mask);
|
||||
|
||||
@@ -14,10 +14,29 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Frontend sync object: wraps an optional backend fence handle. A null
|
||||
// backend handle (backend has no fence support, or could not create a
|
||||
// fence at call time) keeps the legacy always-signaled behavior.
|
||||
//
|
||||
// SharedPtr-owned, not raw: DeleteSync can remove the registry entry while
|
||||
// another thread is inside ClientWaitSync/GetSynciv. Those callers hold a
|
||||
// SharedPtr copy, so the object stays alive until the last reader leaves.
|
||||
// `mutex` then serializes backend-handle reads against the one-time
|
||||
// backend-handle release performed by DeleteSync / DestroyAllSyncObjects.
|
||||
struct SyncObject {
|
||||
std::mutex mutex;
|
||||
MG_Backend::BackendSyncHandle backendHandle = nullptr;
|
||||
GLenum condition = GL_SYNC_GPU_COMMANDS_COMPLETE;
|
||||
GLbitfield flags = 0;
|
||||
|
||||
void ReleaseBackendHandle() {
|
||||
const std::lock_guard<std::mutex> lock(mutex);
|
||||
if (backendHandle == nullptr) {
|
||||
return;
|
||||
}
|
||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
||||
if (backendDeleteSync) {
|
||||
backendDeleteSync(backendHandle);
|
||||
}
|
||||
backendHandle = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// Sync calls may arrive from any thread (launchers migrate the context
|
||||
@@ -25,9 +44,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Entries left at process shutdown are simply dropped; their backend
|
||||
// handles die with the backend.
|
||||
std::mutex g_syncObjectsMutex;
|
||||
UnorderedMap<GLsync, SyncObject*> g_liveSyncObjects;
|
||||
UnorderedMap<GLsync, SharedPtr<SyncObject>> g_liveSyncObjects;
|
||||
|
||||
SyncObject* FindSyncObject(GLsync sync) {
|
||||
SharedPtr<SyncObject> FindSyncObject(GLsync sync) {
|
||||
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||
const auto it = g_liveSyncObjects.find(sync);
|
||||
return it != g_liveSyncObjects.end() ? it->second : nullptr;
|
||||
@@ -35,13 +54,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
} // namespace
|
||||
|
||||
GLsync FenceSync(GLenum condition, GLbitfield flags) {
|
||||
auto* syncObject = new SyncObject;
|
||||
auto syncObject = MakeShared<SyncObject>();
|
||||
syncObject->condition = condition;
|
||||
syncObject->flags = flags;
|
||||
if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) {
|
||||
syncObject->backendHandle = backendFenceSync();
|
||||
}
|
||||
const GLsync handle = reinterpret_cast<GLsync>(syncObject);
|
||||
const GLsync handle = reinterpret_cast<GLsync>(syncObject.get());
|
||||
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||
g_liveSyncObjects[handle] = syncObject;
|
||||
return handle;
|
||||
@@ -52,24 +71,31 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
|
||||
const auto* syncObject = FindSyncObject(sync);
|
||||
const SharedPtr<SyncObject> syncObject = FindSyncObject(sync);
|
||||
if (!syncObject) {
|
||||
return GL_WAIT_FAILED;
|
||||
}
|
||||
const auto backendClientWaitSync = MG_Backend::gBackendFunctionsTable.GL.ClientWaitSync;
|
||||
if (!backendClientWaitSync || !syncObject->backendHandle) {
|
||||
// Hold the per-object lock across the backend call: a concurrent
|
||||
// DeleteSync may already have removed this object from the registry, but
|
||||
// it cannot free the backend handle (or the wrapper) until this reader
|
||||
// finishes. ClientWaitSync can block for `timeout`; that blocks only this
|
||||
// sync object, never the registry or unrelated syncs.
|
||||
const std::lock_guard<std::mutex> lock(syncObject->mutex);
|
||||
if (!backendClientWaitSync || syncObject->backendHandle == nullptr) {
|
||||
return GL_ALREADY_SIGNALED; // legacy always-signaled fallback
|
||||
}
|
||||
return backendClientWaitSync(syncObject->backendHandle, flags, timeout);
|
||||
}
|
||||
|
||||
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
|
||||
const auto* syncObject = FindSyncObject(sync);
|
||||
const SharedPtr<SyncObject> syncObject = FindSyncObject(sync);
|
||||
if (!syncObject) {
|
||||
return;
|
||||
}
|
||||
const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync;
|
||||
if (backendWaitSync && syncObject->backendHandle) {
|
||||
const std::lock_guard<std::mutex> lock(syncObject->mutex);
|
||||
if (backendWaitSync && syncObject->backendHandle != nullptr) {
|
||||
backendWaitSync(syncObject->backendHandle, flags, timeout);
|
||||
}
|
||||
}
|
||||
@@ -78,7 +104,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (sync == nullptr) {
|
||||
return; // glDeleteSync(0) is silently ignored
|
||||
}
|
||||
SyncObject* syncObject = nullptr;
|
||||
SharedPtr<SyncObject> syncObject;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||
const auto it = g_liveSyncObjects.find(sync);
|
||||
@@ -88,15 +114,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
syncObject = it->second;
|
||||
g_liveSyncObjects.erase(it);
|
||||
}
|
||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
||||
if (backendDeleteSync && syncObject->backendHandle) {
|
||||
backendDeleteSync(syncObject->backendHandle);
|
||||
}
|
||||
delete syncObject;
|
||||
// Release the backend handle under the object lock. The local SharedPtr
|
||||
// (and any reader's SharedPtr) keeps the wrapper itself alive until every
|
||||
// in-flight backend call has returned.
|
||||
syncObject->ReleaseBackendHandle();
|
||||
}
|
||||
|
||||
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values) {
|
||||
const auto* syncObject = FindSyncObject(sync);
|
||||
const SharedPtr<SyncObject> syncObject = FindSyncObject(sync);
|
||||
if (!syncObject) {
|
||||
if (length) {
|
||||
*length = 0;
|
||||
@@ -111,7 +136,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
case GL_SYNC_STATUS: {
|
||||
const auto backendGetSyncStatus = MG_Backend::gBackendFunctionsTable.GL.GetSyncStatus;
|
||||
const Bool signaled = !backendGetSyncStatus || !syncObject->backendHandle ||
|
||||
const std::lock_guard<std::mutex> lock(syncObject->mutex);
|
||||
const Bool signaled = !backendGetSyncStatus || syncObject->backendHandle == nullptr ||
|
||||
backendGetSyncStatus(syncObject->backendHandle);
|
||||
value = signaled ? GL_SIGNALED : GL_UNSIGNALED;
|
||||
break;
|
||||
@@ -137,11 +163,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
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;
|
||||
// a DeleteSync racing this sweep finds an empty registry and returns.
|
||||
// Readers racing this sweep keep their SharedPtr copy alive, and each
|
||||
// object's own lock makes the backend-handle release wait for them.
|
||||
UnorderedMap<GLsync, SharedPtr<SyncObject>> orphans;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||
orphans.swap(g_liveSyncObjects);
|
||||
@@ -153,12 +178,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// 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);
|
||||
if (syncObject) {
|
||||
syncObject->ReleaseBackendHandle();
|
||||
}
|
||||
delete syncObject;
|
||||
}
|
||||
MGLOG_D("DestroyAllSyncObjects: reclaimed %zu sync object(s) the app left undeleted", orphans.size());
|
||||
}
|
||||
|
||||
@@ -621,7 +621,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// the process down, which is never an acceptable answer to a query - see the same reasoning
|
||||
// above for the compressed-format path.
|
||||
void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) {
|
||||
MGLOG_I("%s: glGetTexLevelParameter(pname=%s) is not implemented for texture-buffer "
|
||||
MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for texture-buffer "
|
||||
"storage; recording GL_INVALID_OPERATION instead of terminating",
|
||||
caller, MG_Util::ConvertGLEnumToString(pname).c_str());
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -870,7 +870,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
|
||||
if (readBytesPerTexel != bytesPerTexel) {
|
||||
MGLOG_I("%s: cannot copy into a %zu-byte texel from a %zu-byte readback layout", caller,
|
||||
MGLOG_W_ONCE("%s: cannot copy into a %zu-byte texel from a %zu-byte readback layout", caller,
|
||||
bytesPerTexel, readBytesPerTexel);
|
||||
return false;
|
||||
}
|
||||
@@ -1376,6 +1376,47 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The same rules for the COMPRESSED entry points, whose payload size is the imageSize the
|
||||
// caller passed rather than something derived from a (format, type) pair - and which have no
|
||||
// datum size, so the alignment rule above does not apply to them. Shared by
|
||||
// glCompressedTexImage2D and glCompressedTexSubImage2D so the two cannot drift; the point
|
||||
// that is easy to get wrong and that KHR-GL44.buffer_storage.map_persistent_texture exists to
|
||||
// check is the first one: a PERSISTENT mapping stays a legal transfer source.
|
||||
Bool ValidateCompressedUnpackBufferSource(const void* data, SizeT imageSize, const char* caller) {
|
||||
const auto& unpackBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
|
||||
if (!unpackBuffer) return true;
|
||||
|
||||
if (unpackBuffer->IsMapped() && !(unpackBuffer->GetMappingAccess() & BufferMappingAccessBit::Persistent)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Pixel unpack buffer is currently mapped."));
|
||||
return false;
|
||||
}
|
||||
|
||||
const SizeT offset = reinterpret_cast<SizeT>(data);
|
||||
const SizeT bufferSize = unpackBuffer->GetSize();
|
||||
if (offset > bufferSize || imageSize > bufferSize - offset) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Unpacking would read past the end of the pixel unpack buffer."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Where a compressed upload reads its blocks from: `data` is an offset into the bound unpack
|
||||
// buffer when there is one, and a client pointer otherwise. Only meaningful once
|
||||
// ValidateCompressedUnpackBufferSource has passed. Null means there is nothing to read, which
|
||||
// GL leaves undefined and which callers must not dereference.
|
||||
const void* CompressedUnpackSource(const void* data) {
|
||||
const auto& unpackBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
|
||||
if (!unpackBuffer) return data;
|
||||
return reinterpret_cast<const char*>(unpackBuffer->MappedData()) + reinterpret_cast<SizeT>(data);
|
||||
}
|
||||
|
||||
void TexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
||||
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) {
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
@@ -1449,7 +1490,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
|
||||
yoffset + height > static_cast<GLsizei>(texelSize.y()) ||
|
||||
zoffset + depth > static_cast<GLsizei>(texelSize.z())) {
|
||||
MGLOG_E("TexSubImage3D_State: Specified region exceeds texture level dimensions");
|
||||
MGLOG_E_ONCE("TexSubImage3D_State: Specified region exceeds texture level dimensions");
|
||||
free(processedPixels);
|
||||
return;
|
||||
}
|
||||
@@ -1558,7 +1599,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
{width, height, 1}, false, inputSize);
|
||||
|
||||
if (!processedPixels || inputSize == 0) {
|
||||
MGLOG_E("TexSubImage2D_State: Failed to process pixel data for TexSubImage2D, width: %d, height: %d", width,
|
||||
MGLOG_E_ONCE("TexSubImage2D_State: Failed to process pixel data for TexSubImage2D, width: %d, height: %d", width,
|
||||
height);
|
||||
if (processedPixels) free(processedPixels);
|
||||
return;
|
||||
@@ -1572,7 +1613,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
|
||||
yoffset + height > static_cast<GLsizei>(texelSize.y())) {
|
||||
MGLOG_E("TexSubImage2D_State: Specified region exceeds texture dimensions");
|
||||
MGLOG_E_ONCE("TexSubImage2D_State: Specified region exceeds texture dimensions");
|
||||
free(processedPixels);
|
||||
return;
|
||||
}
|
||||
@@ -2123,7 +2164,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (processedPixels && imageSize > 0) {
|
||||
if (imageSize != internalBytes) {
|
||||
MGLOG_W("%s: Processed pixel data size (%zu) does not match expected size (%zu). "
|
||||
MGLOG_W_ONCE("%s: Processed pixel data size (%zu) does not match expected size (%zu). "
|
||||
"This may indicate an alignment or processing issue.",
|
||||
__func__, imageSize, internalBytes);
|
||||
}
|
||||
@@ -2238,6 +2279,23 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
|
||||
{{width, height, 1}, internalBytes});
|
||||
// GL 4.6 core 8.5: a SPECIFIC compressed internalformat (unlike a generic
|
||||
// GL_COMPRESSED_* one, where the implementation is free to choose) commits the
|
||||
// level to that format - GL_TEXTURE_COMPRESSED must then answer true for it and
|
||||
// GL_TEXTURE_INTERNAL_FORMAT must report it, which is how an application asks for
|
||||
// the size to hand glCompressedTexSubImage2D afterwards. Only the tag and the size
|
||||
// are recorded: there is no BC/ETC codec here, so the texel shadow keeps the
|
||||
// uncompressed storage this format resolved to (which is also what lets the level
|
||||
// sample as the application's texels), and the compressed image the tag describes
|
||||
// is zero-filled - the one reproducible answer glGetCompressedTexImage can give for
|
||||
// an image nothing ever compressed. AllocateStorage above clears the tag, so this
|
||||
// has to follow it.
|
||||
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(static_cast<GLenum>(internalformat));
|
||||
if (compressedInfo.blockWidth != 0) {
|
||||
textureMipmapObject->SetMipmapCompressedImage(
|
||||
textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr,
|
||||
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1}));
|
||||
}
|
||||
}
|
||||
|
||||
if (!originalPixels) {
|
||||
@@ -2252,7 +2310,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (processedPixels && imageSize > 0) {
|
||||
if (imageSize != internalBytes) {
|
||||
MGLOG_W("TexImage2D_State: Processed pixel data size (%zu) does not match expected size (%zu). "
|
||||
MGLOG_W_ONCE("TexImage2D_State: Processed pixel data size (%zu) does not match expected size (%zu). "
|
||||
"This may indicate an alignment or processing issue.",
|
||||
imageSize, internalBytes);
|
||||
}
|
||||
@@ -2965,9 +3023,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_INTERNAL_FORMAT:
|
||||
if (params) {
|
||||
// A level stored compressed must report the token it was given, not the
|
||||
// uncompressed format backing it (GL 4.6 core 8.11). Only glCompressedTexImage* sets
|
||||
// that tag, so every level created by glTexImage*D - including one given a compressed
|
||||
// internalformat - still answers with its resolved storage format.
|
||||
// uncompressed format backing it (GL 4.6 core 8.11). glCompressedTexImage2D sets
|
||||
// that tag, and so does a glTexImage2D given a SPECIFIC compressed internalformat;
|
||||
// every other level answers with its resolved storage format.
|
||||
const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level);
|
||||
*params = (compressedFormat != GL_NONE)
|
||||
? (GLint)compressedFormat
|
||||
@@ -3103,9 +3161,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_INTERNAL_FORMAT:
|
||||
if (params) {
|
||||
// A level stored compressed must report the token it was given, not the
|
||||
// uncompressed format backing it (GL 4.6 core 8.11). Only glCompressedTexImage* sets
|
||||
// that tag, so every level created by glTexImage*D - including one given a compressed
|
||||
// internalformat - still answers with its resolved storage format.
|
||||
// uncompressed format backing it (GL 4.6 core 8.11). glCompressedTexImage2D sets
|
||||
// that tag, and so does a glTexImage2D given a SPECIFIC compressed internalformat;
|
||||
// every other level answers with its resolved storage format.
|
||||
const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level);
|
||||
*params = (GLfloat)((compressedFormat != GL_NONE)
|
||||
? compressedFormat
|
||||
@@ -3324,12 +3382,84 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The eleven targets GL 4.6 core 18.3.2 accepts. GL_TEXTURE_BUFFER, the six cube FACE
|
||||
// enums and every PROXY enum all convert to a TextureTarget this frontend recognises,
|
||||
// so ValidateTextureTarget lets them through; here they are INVALID_ENUM.
|
||||
Bool ValidateCopyImageTarget(GLenum target, const char* endpointName) {
|
||||
switch (target) {
|
||||
case GL_RENDERBUFFER:
|
||||
case GL_TEXTURE_1D:
|
||||
case GL_TEXTURE_1D_ARRAY:
|
||||
case GL_TEXTURE_2D:
|
||||
case GL_TEXTURE_2D_ARRAY:
|
||||
case GL_TEXTURE_2D_MULTISAMPLE:
|
||||
case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
|
||||
case GL_TEXTURE_3D:
|
||||
case GL_TEXTURE_CUBE_MAP:
|
||||
case GL_TEXTURE_CUBE_MAP_ARRAY:
|
||||
case GL_TEXTURE_RECTANGLE:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
||||
std::format("{} is not a target glCopyImageSubData accepts as the {}.",
|
||||
MG_Util::ConvertGLEnumToString(target), endpointName)));
|
||||
return false;
|
||||
}
|
||||
|
||||
IntVec3 GetCopyImageLevelSize(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget uploadTarget, GLint level) {
|
||||
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||
if (!mipmapTexture) return textureObject->GetBaseSize();
|
||||
return mipmapTexture->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
|
||||
}
|
||||
|
||||
// glCopyImageSubData names an object that must already exist, and GL 4.6 core 18.3.2
|
||||
// spells the failure INVALID_VALUE - "if either name does not correspond to a valid
|
||||
// object". The shared ValidateTextureObject says INVALID_OPERATION, which is right for
|
||||
// the ~30 entry points that reach it through a BOUND object (where the name was never
|
||||
// in question and the fault is the binding), so this is a local rule rather than a
|
||||
// change to the helper.
|
||||
Bool ValidateCopyImageObjectExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
const char* endpointName) {
|
||||
if (textureObject) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
||||
std::format("The {} name does not correspond to an existing image object.", endpointName)));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Same split for the target/object disagreement: GL 4.6 core 18.3.2 makes a target that
|
||||
// does not match the object INVALID_ENUM, where the shared uniformity helper records
|
||||
// INVALID_OPERATION for the upload paths that share it.
|
||||
Bool ValidateCopyImageTargetMatchesObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureTarget target, const char* endpointName) {
|
||||
if (!textureObject || textureObject->GetTarget() == target) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
||||
std::format("The {} target {} does not match the target the object was created with ({}).",
|
||||
endpointName, MG_Util::ConvertTextureTargetToString(target),
|
||||
MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ValidateCopyImageSubData_State(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
GLenum dstTarget, GLint dstLevel,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
if (!TextureImpl::ValidateTextureObject(srcTexture) || !TextureImpl::ValidateTextureObject(dstTexture)) {
|
||||
if (!ValidateCopyImageObjectExists(srcTexture, "source") ||
|
||||
!ValidateCopyImageObjectExists(dstTexture, "destination")) {
|
||||
return false;
|
||||
}
|
||||
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
|
||||
@@ -3338,14 +3468,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
!TextureImpl::ValidateTextureTarget(dstTextureTarget)) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureTargetUniformity(srcTexture, srcTextureTarget) ||
|
||||
!TextureImpl::ValidateTextureTargetUniformity(dstTexture, dstTextureTarget)) {
|
||||
// GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but
|
||||
// 18.3.2 does not accept them here - only the eleven whole-image targets do.
|
||||
if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) {
|
||||
return false;
|
||||
}
|
||||
if (!ValidateCopyImageTargetMatchesObject(srcTexture, srcTextureTarget, "source") ||
|
||||
!ValidateCopyImageTargetMatchesObject(dstTexture, dstTextureTarget, "destination")) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
|
||||
!TextureImpl::ValidateTextureLevelNumber(dstLevel)) {
|
||||
return false;
|
||||
}
|
||||
// ValidateTextureLevelNumber only bounds the index by GL_MAX_TEXTURE_SIZE; it cannot
|
||||
// see that this particular texture stops at level 0. Both backends turn <level> into an
|
||||
// image subresource with no further checking (DirectVulkan builds a VkImageCopy from it,
|
||||
// DirectGLES forwards it to the ES copy), so a level the texture never had reached the
|
||||
// driver as an out-of-range mip index - on Adreno that is a SIGSEGV inside
|
||||
// vkCmdCopyImage, which is what KHR-GL43.copy_image.non_existent_mipmap used to do to
|
||||
// the whole glcts process. The answer the spec asks for is GL_INVALID_VALUE.
|
||||
if (!TextureImpl::ValidateTextureLevelExists(srcTexture, srcLevel, __func__) ||
|
||||
!TextureImpl::ValidateTextureLevelExists(dstTexture, dstLevel, __func__)) {
|
||||
return false;
|
||||
}
|
||||
if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
@@ -3356,7 +3502,44 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (srcWidth == 0 || srcHeight == 0 || srcDepth == 0) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateBaseInternalFormatMatch(srcTexture->GetFormat(), dstTexture->GetFormat())) {
|
||||
// A multisample image can only be copied to one with the same sample count, and a
|
||||
// single-sample image reports zero - so this one comparison is also what rejects
|
||||
// copying between a multisample target and a non-multisample one.
|
||||
if (srcTexture->GetSamples() != dstTexture->GetSamples()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("The two images have different sample counts ({} vs. {}).",
|
||||
srcTexture->GetSamples(), dstTexture->GetSamples())));
|
||||
return false;
|
||||
}
|
||||
// 18.3.2: both images must be complete. An incomplete one has no defined texels to copy
|
||||
// and no defined storage to copy into.
|
||||
if (!srcTexture->IsComplete() || !dstTexture->IsComplete()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("A copied image is incomplete (source complete: {}, destination complete: {}).",
|
||||
srcTexture->IsComplete(), dstTexture->IsComplete())));
|
||||
return false;
|
||||
}
|
||||
const auto srcUploadTarget = GetPrimaryUploadTarget(srcTexture);
|
||||
const auto dstUploadTarget = GetPrimaryUploadTarget(dstTexture);
|
||||
const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
||||
srcTexture->GetFormat(), GetCompressedLevelFormat(srcTexture, srcUploadTarget, srcLevel));
|
||||
const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
||||
dstTexture->GetFormat(), GetCompressedLevelFormat(dstTexture, dstUploadTarget, dstLevel));
|
||||
if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) {
|
||||
return false;
|
||||
}
|
||||
const IntVec3 srcLevelSize = GetCopyImageLevelSize(srcTexture, srcUploadTarget, srcLevel);
|
||||
const IntVec3 dstLevelSize = GetCopyImageLevelSize(dstTexture, dstUploadTarget, dstLevel);
|
||||
if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight,
|
||||
srcLevelSize.x(), srcLevelSize.y(), "source") ||
|
||||
!TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight,
|
||||
dstLevelSize.x(), dstLevelSize.y(), "destination")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -3468,10 +3651,170 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
}
|
||||
|
||||
// Replaces a block-aligned rectangle of the compressed image glCompressedTexImage2D (or a
|
||||
// compressed glTexImage2D) shadowed for this level. Same deviation as the image call it
|
||||
// patches: the uncompressed texel shadow beside it is NOT touched, because there is no
|
||||
// BC/ETC codec here to decode the incoming blocks with - so what changes is the image
|
||||
// glGetCompressedTexImage hands back, not what the level samples as. Marking the texels
|
||||
// dirty would therefore only re-upload bytes that did not change.
|
||||
void CompressedTexSubImage2D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
|
||||
GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
|
||||
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
// ======================= Converting ================================
|
||||
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
// Zero block width doubles as "format is not a specific compressed format", the
|
||||
// INVALID_ENUM case - one lookup answers both questions.
|
||||
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(format);
|
||||
|
||||
// ===================== Error Checking ==============================
|
||||
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
|
||||
// A proxy holds no image to modify; only the glTexImage*/glCompressedTexImage* pair
|
||||
// accepts one.
|
||||
if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"A proxy target has no texture image to modify."));
|
||||
return;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
|
||||
if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;
|
||||
if (width < 0 || height < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "width and height must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (compressedInfo.blockWidth == 0) {
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||
if (textureMipmapObject == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture storage is not mipmap-backed."));
|
||||
return;
|
||||
}
|
||||
// GL 4.6 core 8.7: INVALID_OPERATION unless the image being modified is stored in
|
||||
// exactly this compressed format. That is also what makes the block arithmetic below
|
||||
// sound - the level's grid is measured with THIS format's block size.
|
||||
const GLenum levelFormat =
|
||||
textureMipmapObject->GetMipmapCompressedFormat(textureUploadTarget, static_cast<Uint>(level));
|
||||
if (levelFormat != format) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"format does not match the internal format of the texture image."));
|
||||
return;
|
||||
}
|
||||
|
||||
const IntVec3 levelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, static_cast<Uint>(level));
|
||||
// Written as a subtraction rather than `xoffset + width > levelSize.x()`: both operands
|
||||
// are application-supplied GLints, so the sum is free to overflow, and a signed overflow
|
||||
// is undefined behaviour that a compiler may resolve by assuming the check passes.
|
||||
// levelSize is our own and non-negative, and the offsets are known non-negative by the
|
||||
// time the subtraction runs, so this form cannot wrap.
|
||||
if (xoffset < 0 || yoffset < 0 || width > levelSize.x() - xoffset || height > levelSize.y() - yoffset) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"The replaced region does not lie within the texture image."));
|
||||
return;
|
||||
}
|
||||
// GL 4.6 core 8.7 for block-based formats: the region must start on a block boundary
|
||||
// and must either be a whole number of blocks wide/high or run to the image's edge.
|
||||
const Int blockWidth = static_cast<Int>(compressedInfo.blockWidth);
|
||||
const Int blockHeight = static_cast<Int>(compressedInfo.blockHeight);
|
||||
const Bool alignedX = (xoffset % blockWidth == 0) &&
|
||||
(width % blockWidth == 0 || xoffset + width == levelSize.x());
|
||||
const Bool alignedY = (yoffset % blockHeight == 0) &&
|
||||
(height % blockHeight == 0 || yoffset + height == levelSize.y());
|
||||
if (!alignedX || !alignedY) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"The replaced region is not aligned to the format's compressed blocks."));
|
||||
return;
|
||||
}
|
||||
// Exactly the size the format and dimensions imply, which is also what keeps the copy
|
||||
// below in bounds.
|
||||
const SizeT expectedImageSize =
|
||||
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1});
|
||||
if (imageSize < 0 || static_cast<SizeT>(imageSize) != expectedImageSize) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"imageSize does not match the compressed image size."));
|
||||
return;
|
||||
}
|
||||
|
||||
// ======================= Processing ================================
|
||||
if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return;
|
||||
const void* compressedBytes = CompressedUnpackSource(data);
|
||||
if (expectedImageSize == 0) return; // a zero-sized region is a legal no-op
|
||||
if (compressedBytes == nullptr) {
|
||||
// No unpack buffer and a null client pointer: there is nothing to read. GL leaves
|
||||
// this undefined rather than erroring, and dereferencing it is the one answer that
|
||||
// is never acceptable.
|
||||
MGLOG_D("%s: null data with no pixel unpack buffer bound, nothing to replace", __func__);
|
||||
return;
|
||||
}
|
||||
|
||||
// Once per process: the call is about to succeed, and what it does is narrower than what
|
||||
// an application has every right to expect from it. Before this existed the call answered
|
||||
// GL_INVALID_ENUM, which was wrong but at least visible; a silent success that leaves the
|
||||
// sampled texels untouched is the kind of thing that costs a day to find from the other
|
||||
// end. MGLOG_W is the right level and now survives at INFO; it sat at MGLOG_I only
|
||||
// while the Log.h ordering compiled warnings out of the builds that ship.
|
||||
static std::atomic<Bool> announcedNoCodec{false};
|
||||
if (!announcedNoCodec.exchange(true)) {
|
||||
MGLOG_W("%s: the compressed blocks are stored verbatim and returned by "
|
||||
"glGetCompressedTexImage, but there is no BC/ETC decoder here, so they do not "
|
||||
"reach the texels this level SAMPLES as. Upload through glTexSubImage2D for "
|
||||
"that.",
|
||||
__func__);
|
||||
}
|
||||
|
||||
// The level's compressed image is stored as one blob, so the rectangle is patched into
|
||||
// a copy of it and the whole thing handed back. Compressed sub-image uploads are not a
|
||||
// hot path, and this keeps the storage layer's compressed API to the two calls it has.
|
||||
const SizeT blobSize =
|
||||
textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast<Uint>(level));
|
||||
const void* existing =
|
||||
textureMipmapObject->MapMipmapCompressedImage(textureUploadTarget, static_cast<Uint>(level));
|
||||
if (blobSize == 0 || existing == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"The texture level holds no compressed image to modify."));
|
||||
return;
|
||||
}
|
||||
Vector<Uint8> blob(blobSize);
|
||||
Memcpy(blob.data(), existing, blobSize);
|
||||
|
||||
const SizeT blockByteSize = compressedInfo.blockByteSize;
|
||||
const SizeT levelBlocksX = (static_cast<SizeT>(levelSize.x()) + compressedInfo.blockWidth - 1) /
|
||||
compressedInfo.blockWidth;
|
||||
const SizeT levelRowBytes = levelBlocksX * blockByteSize;
|
||||
const SizeT regionBlocksX = (static_cast<SizeT>(width) + compressedInfo.blockWidth - 1) /
|
||||
compressedInfo.blockWidth;
|
||||
const SizeT regionBlocksY = (static_cast<SizeT>(height) + compressedInfo.blockHeight - 1) /
|
||||
compressedInfo.blockHeight;
|
||||
const SizeT firstBlockX = static_cast<SizeT>(xoffset) / compressedInfo.blockWidth;
|
||||
const SizeT firstBlockY = static_cast<SizeT>(yoffset) / compressedInfo.blockHeight;
|
||||
const SizeT regionRowBytes = regionBlocksX * blockByteSize;
|
||||
const auto* source = static_cast<const Uint8*>(compressedBytes);
|
||||
for (SizeT row = 0; row < regionBlocksY; ++row) {
|
||||
const SizeT destOffset = (firstBlockY + row) * levelRowBytes + firstBlockX * blockByteSize;
|
||||
if (destOffset + regionRowBytes > blobSize) break; // a level whose blob predates its size
|
||||
Memcpy(blob.data() + destOffset, source + row * regionRowBytes, regionRowBytes);
|
||||
}
|
||||
textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, static_cast<Uint>(level), format,
|
||||
blob.data(), blobSize);
|
||||
}
|
||||
|
||||
void CompressedTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format,
|
||||
@@ -3564,28 +3907,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// SetMipmapCompressedImage re-arms it.
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, 1}, internalBytes});
|
||||
|
||||
const void* compressedBytes = data;
|
||||
const auto& pixelUnpackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
|
||||
if (pixelUnpackBufferObject) {
|
||||
if (pixelUnpackBufferObject->IsMapped()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Pixel unpack buffer is currently mapped."));
|
||||
return;
|
||||
}
|
||||
const SizeT offset = reinterpret_cast<SizeT>(data);
|
||||
const SizeT bufferSize = pixelUnpackBufferObject->GetSize();
|
||||
if (offset > bufferSize || expectedImageSize > bufferSize - offset) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Unpacking would read past the end of the pixel unpack buffer."));
|
||||
return;
|
||||
}
|
||||
compressedBytes = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) + offset;
|
||||
}
|
||||
if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return;
|
||||
const void* compressedBytes = CompressedUnpackSource(data);
|
||||
textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, level, internalformat, compressedBytes,
|
||||
expectedImageSize);
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
|
||||
@@ -4095,6 +4418,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// core 8.19). Allocating only the primary one left the object cube-incomplete, so every
|
||||
// framebuffer it was attached to reported GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. Every other
|
||||
// 2D target has exactly one upload target, so this loop is a no-op change for them.
|
||||
// A specific compressed internalformat commits every level it allocates to that
|
||||
// format, the same way glTexImage2D does - and here it matters twice over, because
|
||||
// immutable storage plus glCompressedTexSubImage2D IS the modern way to upload a
|
||||
// compressed texture: without the tag that sub-image call finds an uncompressed
|
||||
// level and refuses it. Zero width means a generic (implementation's choice)
|
||||
// format, which MobileGL answers with uncompressed storage, so it is not tagged.
|
||||
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat);
|
||||
for (const auto uploadTarget : textureObject->GetUploadTargets()) {
|
||||
for (GLsizei level = 0; level < levels; ++level) {
|
||||
const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
|
||||
@@ -4103,6 +4433,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
static_cast<SizeT>(levelWidth) * static_cast<SizeT>(levelHeight) * bytesPerPixel;
|
||||
textureMipmapObject->AllocateStorage(uploadTarget, level, {{levelWidth, levelHeight, 1}, byteSize});
|
||||
textureMipmapObject->MarkStorageDirty(uploadTarget, level, false);
|
||||
if (compressedInfo.blockWidth != 0) {
|
||||
// After AllocateStorage, which clears the tag.
|
||||
textureMipmapObject->SetMipmapCompressedImage(
|
||||
uploadTarget, static_cast<Uint>(level), internalformat, nullptr,
|
||||
MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
|
||||
{levelWidth, levelHeight, 1}));
|
||||
}
|
||||
}
|
||||
// See TextureStorage1D.
|
||||
textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
|
||||
@@ -4472,6 +4809,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
free(processedPixels);
|
||||
}
|
||||
|
||||
void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
|
||||
GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
|
||||
CompressedTexSubImage2D_State(target, level, xoffset, yoffset, width, height, format, imageSize, data);
|
||||
});
|
||||
}
|
||||
|
||||
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
||||
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
@@ -5369,10 +5714,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void CopyImageSubData(GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
auto srcTexture = GetTextureObjectByName(srcName, __func__);
|
||||
auto dstTexture = GetTextureObjectByName(dstName, __func__);
|
||||
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, dstTexture, dstTarget, dstLevel,
|
||||
srcWidth, srcHeight, srcDepth)) {
|
||||
// A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is
|
||||
// INVALID_OPERATION - so resolve through the plain lookup, which answers a null
|
||||
// SharedPtr, and let the validator record the error this entry point owes.
|
||||
const SharedPtr<MG_State::GLState::ITextureObject> srcTexture =
|
||||
MG_State::pGLContext->GetTextureObject(srcName);
|
||||
const SharedPtr<MG_State::GLState::ITextureObject> dstTexture =
|
||||
MG_State::pGLContext->GetTextureObject(dstName);
|
||||
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, srcX, srcY, dstTexture, dstTarget,
|
||||
dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) {
|
||||
return;
|
||||
}
|
||||
CopyImageSubData_Backend(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel,
|
||||
|
||||
@@ -37,6 +37,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum format, GLenum type, const void* pixels);
|
||||
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
||||
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels);
|
||||
void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
|
||||
GLsizei height, GLenum format, GLsizei imageSize, const void* data);
|
||||
void TextureParameterf(GLuint texture, GLenum pname, GLfloat param);
|
||||
void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params);
|
||||
void TextureParameteri(GLuint texture, GLenum pname, GLint param);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
Bool ValidateTextureTarget(TextureTarget target) {
|
||||
@@ -353,6 +354,63 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int level,
|
||||
const char* caller) {
|
||||
// A null object is somebody else's error to report - ValidateTextureObject runs
|
||||
// first at every call site and has already recorded it.
|
||||
if (!textureObject) return false;
|
||||
|
||||
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||
if (mipmapTexture == nullptr) {
|
||||
// The only non-mipmap storage class is a buffer texture, and GL_TEXTURE_BUFFER is
|
||||
// not a target glCopyImageSubData accepts at all (it is in the CTS's invalid-target
|
||||
// set). Declining here is not the error code the spec asks for - that would be
|
||||
// INVALID_ENUM from a target check this validator is not - but it does keep a
|
||||
// texture with no image levels whatsoever from reaching a backend that would
|
||||
// dereference a backend texture it never created.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Texture has no mipmap levels to address."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// What this number is, exactly, because two other things are almost it and neither is
|
||||
// safe to assume: it is the number of level SLOTS the shadow has allocated - holes
|
||||
// included, since MipmapStorage::AllocateLevel grows to level+1 and never fills the gap.
|
||||
// For a cube map MipmapUploadTargetArray reports face +X's chain rather than the union.
|
||||
//
|
||||
// The guarantee that matters is one-sided: this count is always >= the level count the
|
||||
// backends derive (VkTextureManager::GetUploadMipLevelCount stops at the first level
|
||||
// with a non-positive extent, so it can only be shorter). That is the safe direction -
|
||||
// no copy to a level the texture genuinely has is ever rejected here. It is NOT an
|
||||
// exact match, so the backends keep their own range guard for the band in between: a
|
||||
// chain with a hole (level 0 and 2 defined, 1 not) is accepted by this predicate and
|
||||
// declined by the backend, which is a silent no-op rather than a copy. That band is a
|
||||
// backend storage limitation, not a validation one - rejecting it here with
|
||||
// INVALID_VALUE would be refusing a copy the spec permits.
|
||||
const Uint levelCount = mipmapTexture->GetMipmapLevelCount();
|
||||
|
||||
if (levelCount == 0) {
|
||||
// No image has ever been defined on this texture, so the fault is the texture,
|
||||
// not the number: GL 4.6 core 18.3.2 asks for INVALID_OPERATION when an object a
|
||||
// copy names is an incomplete texture.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Texture has no image defined at any level."));
|
||||
return false;
|
||||
}
|
||||
if (level < 0 || static_cast<Uint>(level) >= levelCount) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Texture level does not exist in this texture."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
|
||||
if (!textureObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -458,26 +516,86 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2) {
|
||||
const auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1);
|
||||
const auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2);
|
||||
if (unsizedFormat1 != unsizedFormat2) {
|
||||
// The 3-argument GenericErrorInfo constructor used to be spelled as a single
|
||||
// std::format() call whose format string was the component name, so every
|
||||
// diagnostic collapsed to the literal "MG_Impl/GLImpl". Format the message, then
|
||||
// hand over component/function/message separately.
|
||||
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat) {
|
||||
CopyImageTexelBlock block{};
|
||||
if (compressedFormat != GL_NONE) {
|
||||
const auto info = MG_Util::GetCompressedFormatInfo(compressedFormat);
|
||||
if (info.blockByteSize != 0) {
|
||||
block.byteSize = info.blockByteSize;
|
||||
block.blockWidth = info.blockWidth;
|
||||
block.blockHeight = info.blockHeight;
|
||||
block.compressed = true;
|
||||
return block;
|
||||
}
|
||||
}
|
||||
// The size MobileGL actually stores a texel of this format in, which for every format GL
|
||||
// gives a required size is that required size. The handful of legacy formats GL leaves
|
||||
// implementation-defined (R3_G3_B2, RGB4/5/10/12, RGBA2/12) have no view class in table
|
||||
// 8.22 to be compared against anyway, and this is the size that decides whether a raw
|
||||
// copy between them would in fact preserve the bytes.
|
||||
block.byteSize = MG_Util::GetSizedInternalFormatSizeInBytes(format);
|
||||
return block;
|
||||
}
|
||||
|
||||
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
|
||||
const CopyImageTexelBlock& dstBlock) {
|
||||
if (srcBlock.byteSize == 0 || dstBlock.byteSize == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||
"A copied image has no storage whose texel size is known."));
|
||||
return false;
|
||||
}
|
||||
if (srcBlock.byteSize != dstBlock.byteSize) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateBaseInternalFormatMatch",
|
||||
std::format("The base internal format of the two formats do not match ({} vs. {})",
|
||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1),
|
||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2))));
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||
std::format("The two images' texel blocks are different sizes ({} vs. {} bytes), so the "
|
||||
"formats are not copy-compatible.",
|
||||
srcBlock.byteSize, dstBlock.byteSize)));
|
||||
return false;
|
||||
}
|
||||
// Two compressed images additionally have to agree on the SHAPE of the block, not only
|
||||
// its size: an 8-byte 4x4 block and a hypothetical 8-byte 8x8 one hold different texel
|
||||
// counts, and GL 4.6 core 18.3.2 requires both dimensions to match.
|
||||
if (srcBlock.compressed && dstBlock.compressed &&
|
||||
(srcBlock.blockWidth != dstBlock.blockWidth || srcBlock.blockHeight != dstBlock.blockHeight)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||
std::format("The two compressed images have different block dimensions ({}x{} vs. {}x{}).",
|
||||
srcBlock.blockWidth, srcBlock.blockHeight, dstBlock.blockWidth,
|
||||
dstBlock.blockHeight)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
|
||||
Int imageWidth, Int imageHeight, const char* endpointName) {
|
||||
if (!block.compressed) return true;
|
||||
const Int blockWidth = static_cast<Int>(block.blockWidth);
|
||||
const Int blockHeight = static_cast<Int>(block.blockHeight);
|
||||
if (blockWidth <= 1 && blockHeight <= 1) return true;
|
||||
// The origin is unconditional; the extent gets the "or it reaches the edge of the image"
|
||||
// exemption GL 4.6 core 18.3.2 grants, which is what lets a 16x16 BPTC image be copied
|
||||
// whole even when the last block is partial.
|
||||
const Bool originAligned = (x % blockWidth == 0) && (y % blockHeight == 0);
|
||||
const Bool widthOk = (width % blockWidth == 0) || (x + width == imageWidth);
|
||||
const Bool heightOk = (height % blockHeight == 0) || (y + height == imageHeight);
|
||||
if (originAligned && widthOk && heightOk) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageBlockAlignment",
|
||||
std::format("The {} region [{}, {}] + [{} x {}] is not aligned to the {}x{} compressed block "
|
||||
"grid of a {} x {} image.",
|
||||
endpointName, x, y, width, height, blockWidth, blockHeight, imageWidth, imageHeight)));
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat) {
|
||||
const auto unsizedDest = MG_Util::ConvertInternalFormatToUnsized(destFormat);
|
||||
const auto unsizedSrc = MG_Util::ConvertInternalFormatToUnsized(srcFormat);
|
||||
|
||||
@@ -30,6 +30,16 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
TextureInternalFormat internalFormat,
|
||||
TexturePixelDataType type);
|
||||
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
|
||||
// "Is <level> a level this texture actually has?", which ValidateTextureLevelNumber above
|
||||
// does NOT answer - that one only bounds the index by GL_MAX_TEXTURE_SIZE and knows nothing
|
||||
// about the object. Entry points that resolve a level straight into a backend image
|
||||
// subresource need this one: a level the texture never had is GL_INVALID_VALUE (GL 4.6 core
|
||||
// 18.3.2), and passing it through instead reaches the driver as an out-of-range subresource.
|
||||
// Note the error split is per-entry-point, so this is not universally reusable:
|
||||
// glClearTexImage owes INVALID_OPERATION for the same out-of-range level and spells its own
|
||||
// copy of this predicate in GL_Texture.cpp (GetClearTextureObject).
|
||||
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int level,
|
||||
const char* caller);
|
||||
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
|
||||
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
|
||||
// points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION
|
||||
@@ -40,8 +50,32 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
TextureTarget target);
|
||||
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
|
||||
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
|
||||
// Exact base-format equality - what glCopyImageSubData's format compatibility needs.
|
||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2);
|
||||
// The texel block of one glCopyImageSubData endpoint, resolved to the two things the
|
||||
// compatibility rule actually asks about. `compressed` is not redundant with a block bigger
|
||||
// than 1x1: it is what distinguishes "compressed, and so the region is measured in texels of
|
||||
// a blocked image" from "uncompressed, and so it is measured in texels".
|
||||
struct CopyImageTexelBlock {
|
||||
SizeT byteSize = 0;
|
||||
Uint blockWidth = 1;
|
||||
Uint blockHeight = 1;
|
||||
Bool compressed = false;
|
||||
};
|
||||
// `compressedFormat` is the GLenum a glCompressedTexImage* upload recorded for the level, or
|
||||
// GL_NONE. It has to be asked for separately because MobileGL stores every compressed format
|
||||
// in uncompressed storage (ConvertGLEnumToTextureInternalFormat), so the TextureInternalFormat
|
||||
// alone can no longer tell a BPTC image from the RGBA8 backing it.
|
||||
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat);
|
||||
// GL 4.6 core 18.3.2: the two images must be COMPATIBLE, and compatible means their texel
|
||||
// blocks are the same SIZE - not that they share a base internal format. RGBA32UI into
|
||||
// RGBA32F is legal (both 128-bit) while RGBA8 into RGBA32F is not, and a compressed image
|
||||
// pairs with an uncompressed one whose texel is as big as the compressed block.
|
||||
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
|
||||
const CopyImageTexelBlock& dstBlock);
|
||||
// GL 4.6 core 18.3.2: for a compressed image the region's origin must sit on a block
|
||||
// boundary and its size must be a whole number of blocks - unless the edge it runs to is
|
||||
// the edge of the image.
|
||||
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
|
||||
Int imageWidth, Int imageHeight, const char* endpointName);
|
||||
// GL 4.6 SS 8.6 subset rule for glCopyTexImage*: the read buffer must supply every component
|
||||
// the requested internalformat asks for, but may supply more.
|
||||
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat);
|
||||
|
||||
@@ -527,7 +527,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (!MG_Backend::pActiveBackendObject ||
|
||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||
MGLOG_I("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
|
||||
MGLOG_W_ONCE("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
|
||||
"backend has no double-precision vertex attribute support - see the "
|
||||
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs",
|
||||
attribindex);
|
||||
|
||||
@@ -166,32 +166,32 @@ MOBILEGL_GLX_API int glXSwapIntervalSGI(int interval) {
|
||||
|
||||
// Legacy entry points some loaders probe for; harmless no-op stubs.
|
||||
MOBILEGL_GLX_API void glXCopyContext(Display*, void*, void*, unsigned long) {
|
||||
MGLOG_W("glx: glXCopyContext is not supported");
|
||||
MGLOG_W_ONCE("glx: glXCopyContext is not supported");
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API unsigned long glXCreateGLXPixmap(Display*, void*, unsigned long) {
|
||||
MGLOG_W("glx: glXCreateGLXPixmap is not supported");
|
||||
MGLOG_W_ONCE("glx: glXCreateGLXPixmap is not supported");
|
||||
return 0;
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXDestroyGLXPixmap(Display*, unsigned long) {}
|
||||
|
||||
MOBILEGL_GLX_API unsigned long glXCreatePixmap(Display*, void*, unsigned long, const int*) {
|
||||
MGLOG_W("glx: glXCreatePixmap is not supported");
|
||||
MGLOG_W_ONCE("glx: glXCreatePixmap is not supported");
|
||||
return 0;
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXDestroyPixmap(Display*, unsigned long) {}
|
||||
|
||||
MOBILEGL_GLX_API unsigned long glXCreatePbuffer(Display*, void*, const int*) {
|
||||
MGLOG_W("glx: glXCreatePbuffer is not supported");
|
||||
MGLOG_W_ONCE("glx: glXCreatePbuffer is not supported");
|
||||
return 0;
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXDestroyPbuffer(Display*, unsigned long) {}
|
||||
|
||||
MOBILEGL_GLX_API void glXUseXFont(unsigned long, int, int, int) {
|
||||
MGLOG_W("glx: glXUseXFont is not supported");
|
||||
MGLOG_W_ONCE("glx: glXUseXFont is not supported");
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXSelectEvent(Display*, unsigned long, unsigned long) {}
|
||||
|
||||
@@ -149,7 +149,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
fns->Sync = reinterpret_cast<decltype(fns->Sync)>(dlsym(fns->Library, "XSync"));
|
||||
}
|
||||
if (!fns->Valid()) {
|
||||
MGLOG_E("glx: failed to load libX11 entry points");
|
||||
MGLOG_E_ONCE("glx: failed to load libX11 entry points");
|
||||
}
|
||||
return fns;
|
||||
}();
|
||||
@@ -314,7 +314,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
Uint32 width = 0;
|
||||
Uint32 height = 0;
|
||||
if (!QueryDrawableSize(dpy, drawable, width, height)) {
|
||||
MGLOG_E("glx: XGetGeometry failed for drawable 0x%lx", drawable);
|
||||
MGLOG_E_ONCE("glx: XGetGeometry failed for drawable 0x%lx", drawable);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -326,7 +326,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
EGLSurface surface = EGLImpl::CreatePlatformWindowSurface(
|
||||
context.Display, context.Config, reinterpret_cast<void*>(drawable), attribs);
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
MGLOG_E("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable,
|
||||
MGLOG_E_ONCE("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable,
|
||||
width, height);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -347,7 +347,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
EGLDisplay display = EnsureDisplay();
|
||||
if (display == EGL_NO_DISPLAY) {
|
||||
MGLOG_E("glx: no EGL display");
|
||||
MGLOG_E_ONCE("glx: no EGL display");
|
||||
return nullptr;
|
||||
}
|
||||
EGLImpl::BindAPI(EGL_OPENGL_API);
|
||||
@@ -376,13 +376,13 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
EGLint configCount = 0;
|
||||
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) ||
|
||||
configCount <= 0) {
|
||||
MGLOG_E("glx: eglChooseConfig failed");
|
||||
MGLOG_E_ONCE("glx: eglChooseConfig failed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
|
||||
if (eglContext == EGL_NO_CONTEXT) {
|
||||
MGLOG_E("glx: eglCreateContext failed");
|
||||
MGLOG_E_ONCE("glx: eglCreateContext failed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -931,7 +931,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
|
||||
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface,
|
||||
object->Context)) {
|
||||
MGLOG_E("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context);
|
||||
MGLOG_E_ONCE("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context);
|
||||
return 0;
|
||||
}
|
||||
t_current = {dpy, drawable, drawable, context};
|
||||
@@ -943,7 +943,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
if (context && draw != read) {
|
||||
// MobileGL's backends reject split draw/read surfaces; bind the draw
|
||||
// drawable for both, which is what every real caller here needs.
|
||||
MGLOG_W("glx: glXMakeContextCurrent draw 0x%lx != read 0x%lx, using draw for both", draw,
|
||||
MGLOG_W_ONCE("glx: glXMakeContextCurrent draw 0x%lx != read 0x%lx, using draw for both", draw,
|
||||
read);
|
||||
}
|
||||
const int result = MakeCurrent(dpy, draw, context);
|
||||
@@ -958,7 +958,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
auto& surfaces = DrawableSurfaces();
|
||||
auto it = surfaces.find(drawable);
|
||||
if (it == surfaces.end()) {
|
||||
MGLOG_W("glx: glXSwapBuffers with no surface for drawable 0x%lx", drawable);
|
||||
MGLOG_W_ONCE("glx: glXSwapBuffers with no surface for drawable 0x%lx", drawable);
|
||||
return;
|
||||
}
|
||||
SyncSurfaceSize(dpy, drawable, it->second);
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace MG_Impl::GLXImpl {
|
||||
#endif
|
||||
void* proc = MobileGL::MG_Impl::GetProcAddress(name);
|
||||
if (!proc) {
|
||||
MGLOG_W("Failed to get function: %s", (const char*)name);
|
||||
MGLOG_D("Failed to get function: %s", (const char*)name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -1403,7 +1403,7 @@ namespace MobileGL::MG_Impl {
|
||||
GETPROC(glFramebufferTextureMultiviewOVR, name);
|
||||
// GETPROC(glNamedFramebufferTextureMultiviewOVR, name);
|
||||
|
||||
MGLOG_W("GetProcAddress(%s) = nullptr!", name);
|
||||
MGLOG_D("GetProcAddress(%s) = nullptr!", name);
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl
|
||||
|
||||
@@ -269,7 +269,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
}
|
||||
id metalLayerClass = reinterpret_cast<id>(objc_getClass("CAMetalLayer"));
|
||||
if (!metalLayerClass) {
|
||||
MGLOG_E("NSOpenGLImpl: CAMetalLayer class not found");
|
||||
MGLOG_E_ONCE("NSOpenGLImpl: CAMetalLayer class not found");
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
static_cast<GLint>(geometry.DrawableSize.width),
|
||||
static_cast<GLint>(geometry.DrawableSize.height));
|
||||
if (error != kCGLNoError) {
|
||||
MGLOG_E("NSOpenGLImpl: failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
||||
MGLOG_E_ONCE("NSOpenGLImpl: failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
}
|
||||
const auto error = CGLImpl::SetCurrentContext(context);
|
||||
if (error != kCGLNoError) {
|
||||
MGLOG_E("NSOpenGLImpl: makeCurrentContext failed: %s", CGLImpl::ErrorString(error));
|
||||
MGLOG_E_ONCE("NSOpenGLImpl: makeCurrentContext failed: %s", CGLImpl::ErrorString(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
}
|
||||
const auto error = CGLImpl::FlushDrawable(context);
|
||||
if (error != kCGLNoError) {
|
||||
MGLOG_E("NSOpenGLImpl: flushBuffer failed: %s", CGLImpl::ErrorString(error));
|
||||
MGLOG_E_ONCE("NSOpenGLImpl: flushBuffer failed: %s", CGLImpl::ErrorString(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,7 +377,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
static_cast<GLint>(geometry.DrawableSize.width),
|
||||
static_cast<GLint>(geometry.DrawableSize.height));
|
||||
if (error != kCGLNoError) {
|
||||
MGLOG_E("NSOpenGLImpl: update failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
||||
MGLOG_E_ONCE("NSOpenGLImpl: update failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
||||
return;
|
||||
}
|
||||
CGLImpl::UpdateContext(context);
|
||||
@@ -421,7 +421,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
SEL selector = sel_registerName(selectorName);
|
||||
Method method = class_getInstanceMethod(cls, selector);
|
||||
if (!method) {
|
||||
MGLOG_W("NSOpenGLImpl: missing instance method %s", selectorName);
|
||||
MGLOG_W_ONCE("NSOpenGLImpl: missing instance method %s", selectorName);
|
||||
return;
|
||||
}
|
||||
if (original) {
|
||||
@@ -434,7 +434,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
SEL selector = sel_registerName(selectorName);
|
||||
Method method = class_getClassMethod(cls, selector);
|
||||
if (!method) {
|
||||
MGLOG_W("NSOpenGLImpl: missing class method %s", selectorName);
|
||||
MGLOG_W_ONCE("NSOpenGLImpl: missing class method %s", selectorName);
|
||||
return;
|
||||
}
|
||||
method_setImplementation(method, replacement);
|
||||
@@ -444,7 +444,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
|
||||
Class contextClass = objc_getClass("NSOpenGLContext");
|
||||
if (!pixelFormatClass || !contextClass) {
|
||||
MGLOG_W("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
|
||||
MGLOG_W_ONCE("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ extern "C" HGLRC WINAPI wglCreateLayerContext(HDC hdc, int iLayerPlane) {
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglCopyContext(HGLRC, HGLRC, UINT) {
|
||||
MGLOG_W("wglCopyContext is not supported");
|
||||
MGLOG_W_ONCE("wglCopyContext is not supported");
|
||||
SetLastError(ERROR_NOT_SUPPORTED);
|
||||
return FALSE;
|
||||
}
|
||||
@@ -132,24 +132,24 @@ extern "C" DWORD WINAPI wglSwapMultipleBuffers(UINT n, CONST WGLSWAP* ps) {
|
||||
// ---- Font rendering (legacy immediate-mode feature; not supported) ----
|
||||
|
||||
extern "C" BOOL WINAPI wglUseFontBitmapsA(HDC, DWORD, DWORD, DWORD) {
|
||||
MGLOG_W("wglUseFontBitmapsA is not supported");
|
||||
MGLOG_W_ONCE("wglUseFontBitmapsA is not supported");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglUseFontBitmapsW(HDC, DWORD, DWORD, DWORD) {
|
||||
MGLOG_W("wglUseFontBitmapsW is not supported");
|
||||
MGLOG_W_ONCE("wglUseFontBitmapsW is not supported");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglUseFontOutlinesA(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
||||
LPGLYPHMETRICSFLOAT) {
|
||||
MGLOG_W("wglUseFontOutlinesA is not supported");
|
||||
MGLOG_W_ONCE("wglUseFontOutlinesA is not supported");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglUseFontOutlinesW(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
||||
LPGLYPHMETRICSFLOAT) {
|
||||
MGLOG_W("wglUseFontOutlinesW is not supported");
|
||||
MGLOG_W_ONCE("wglUseFontOutlinesW is not supported");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
Uint32 width = 0;
|
||||
Uint32 height = 0;
|
||||
if (!QueryClientSize(hwnd, width, height)) {
|
||||
MGLOG_E("wgl: GetClientRect failed for HWND %p", hwnd);
|
||||
MGLOG_E_ONCE("wgl: GetClientRect failed for HWND %p", hwnd);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
EGLSurface surface =
|
||||
EGLImpl::CreatePlatformWindowSurface(context.Display, context.Config, hwnd, attribs);
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
MGLOG_E("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
|
||||
MGLOG_E_ONCE("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
EGLDisplay display = EnsureDisplay();
|
||||
if (display == EGL_NO_DISPLAY) {
|
||||
MGLOG_E("wgl: no EGL display");
|
||||
MGLOG_E_ONCE("wgl: no EGL display");
|
||||
return nullptr;
|
||||
}
|
||||
EGLImpl::BindAPI(EGL_OPENGL_API);
|
||||
@@ -275,13 +275,13 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
EGLConfig config = nullptr;
|
||||
EGLint configCount = 0;
|
||||
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) || configCount <= 0) {
|
||||
MGLOG_E("wgl: eglChooseConfig failed");
|
||||
MGLOG_E_ONCE("wgl: eglChooseConfig failed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
|
||||
if (eglContext == EGL_NO_CONTEXT) {
|
||||
MGLOG_E("wgl: eglCreateContext failed");
|
||||
MGLOG_E_ONCE("wgl: eglCreateContext failed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -612,7 +612,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
auto& surfaces = WindowSurfaces();
|
||||
auto it = surfaces.find(hwnd);
|
||||
if (it == surfaces.end()) {
|
||||
MGLOG_W("wglSwapBuffers: no surface for HWND %p", hwnd);
|
||||
MGLOG_W_ONCE("wglSwapBuffers: no surface for HWND %p", hwnd);
|
||||
return FALSE;
|
||||
}
|
||||
SyncSurfaceSize(hwnd, it->second);
|
||||
@@ -685,7 +685,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
}
|
||||
|
||||
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface, object->Context)) {
|
||||
MGLOG_E("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
|
||||
MGLOG_E_ONCE("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
|
||||
return FALSE;
|
||||
}
|
||||
t_current = {hdc, hglrc};
|
||||
|
||||
@@ -60,17 +60,28 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/FragCoordOriginScenario.cpp
|
||||
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||
Scenarios/DepthStencilReadbackScenario.cpp
|
||||
Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
||||
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
|
||||
Scenarios/ClipDistanceScenario.cpp
|
||||
Scenarios/ViewportArrayScenario.cpp
|
||||
Scenarios/SsboArrayLengthScenario.cpp
|
||||
Scenarios/DoublePrecisionScenario.cpp
|
||||
Scenarios/UniformInitializerScenario.cpp
|
||||
Scenarios/SwizzleAccessRoutineScenario.cpp
|
||||
Scenarios/ProgramPipelineScenario.cpp
|
||||
Scenarios/ImageLoadStoreSsoScenario.cpp
|
||||
Scenarios/ImageTargetKindScenario.cpp
|
||||
Scenarios/ImageFormatQualifierScenario.cpp
|
||||
Scenarios/SsboDeclarationFormScenario.cpp
|
||||
Scenarios/Glsl420DeclarationScenario.cpp
|
||||
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
||||
Scenarios/BufferTextureScenario.cpp
|
||||
Scenarios/VertexAttribBindingScenario.cpp
|
||||
Scenarios/XfbCaptureBufferReuseScenario.cpp
|
||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||
Scenarios/CopyImageLayeredScenario.cpp
|
||||
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
@@ -238,6 +249,8 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" ${MGL_ITEST_VULKAN_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_VULKAN_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1" ${MGL_ITEST_COMMON_ENV})
|
||||
|
||||
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
|
||||
set(MGL_ITEST_TIMEOUT 120)
|
||||
@@ -285,3 +298,21 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# A fourth registration, of the depth/stencil readback scenarios, with the ES
|
||||
# shader-sampling emulation forced on. Not paranoia - without it these scenarios are
|
||||
# UNFALSIFIABLE on the machines this suite runs on: OpenGL ES has no depth or stencil
|
||||
# readback in core, but Mesa accepts the reads anyway, so on llvmpipe every one of them
|
||||
# goes green through a native path that the Adreno device does not have. Deleting the
|
||||
# entire emulation left all of them passing. With the flag the native spellings are off
|
||||
# the table and only the path the device actually takes remains. DirectGLES only - the
|
||||
# emulation is DirectGLES's.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.ForcedDepthStencilEmulation."
|
||||
TEST_FILTER "DepthStencilReadback*Scenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
@@ -199,5 +199,61 @@ namespace MGITest {
|
||||
"derived component limits are computed in";
|
||||
}
|
||||
|
||||
// ARB_viewport_array's own limits. They are advertised from three different places -
|
||||
// GL_MAX_VIEWPORTS from the frontend's indexed state width, the bounds range and the
|
||||
// subpixel bits from the backend caps table - and each backend fills that table from a
|
||||
// different source, so all three are checked on both lanes.
|
||||
//
|
||||
// GL_VIEWPORT_BOUNDS_RANGE is the one that shipped wrong: GLES has no such query, the
|
||||
// DirectGLES loader's glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE) therefore raised
|
||||
// GL_INVALID_ENUM and left the probe's zero-initialized array in place, and MobileGL
|
||||
// advertised [0, 0] - a range that admits no viewport origin at all, and the check that
|
||||
// kept KHR-GL43.viewport_array.queries red on Espryt after the indexed-state work.
|
||||
TEST_F(AdvertisedLimitsScenario, ViewportArrayLimitsMeetTheirGL43Floors) {
|
||||
GLint maxViewports = -1;
|
||||
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_GE(maxViewports, 16) << "GL 4.3 core table 23.53 sets the MAX_VIEWPORTS minimum at 16";
|
||||
EXPECT_LE(maxViewports, 256) << "one viewport rectangle of indexed state is allocated per advertised "
|
||||
"viewport, and the CTS sizes its arrays off this number";
|
||||
|
||||
GLfloat boundsRange[2] = {1.0f, -1.0f};
|
||||
glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE, boundsRange);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_LE(boundsRange[0], -32768.0f)
|
||||
<< "GL 4.6 core table 23.60 sets the VIEWPORT_BOUNDS_RANGE minimum at [-32768, 32767]; got ["
|
||||
<< boundsRange[0] << ", " << boundsRange[1] << "]";
|
||||
EXPECT_GE(boundsRange[1], 32767.0f)
|
||||
<< "GL 4.6 core table 23.60 sets the VIEWPORT_BOUNDS_RANGE minimum at [-32768, 32767]; got ["
|
||||
<< boundsRange[0] << ", " << boundsRange[1] << "]";
|
||||
|
||||
// KNOWN INFIDELITY, pinned here rather than hidden. MobileGL reports the driver's own
|
||||
// VIEWPORT_SUBPIXEL_BITS (4 on llvmpipe, i.e. 1/16-pixel viewport precision), but the
|
||||
// float viewport rectangle glViewportIndexedf stores is snapped to integers on its
|
||||
// way to both backends (ComputeGLViewport, DirectGLES SyncRenderState). The STATE
|
||||
// round trip is exact - which is all KHR-GL43.viewport_array.viewport_api checks, and
|
||||
// all this cluster set out to fix - so the gap is in rasterization only: a fractional
|
||||
// viewport origin rasterizes as if it had been rounded. Nothing in the suite or in
|
||||
// Minecraft sets one. Only the spec floor is asserted; tightening this to EQ(0) would
|
||||
// mean advertising no subpixel precision at all, which is a separate decision about a
|
||||
// limit MobileGL currently passes through from the driver.
|
||||
GLint subpixelBits = -1;
|
||||
glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &subpixelBits);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_GE(subpixelBits, 0) << "GL 4.6 core table 23.60: VIEWPORT_SUBPIXEL_BITS has a minimum of 0, and "
|
||||
"a negative value is what a sign-flipped uint32 looks like";
|
||||
|
||||
GLint viewportDims[2] = {-1, -1};
|
||||
glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewportDims);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
GLint maxRenderbufferSize = -1;
|
||||
glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maxRenderbufferSize);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
// GL 4.6 core 13.6.1: MAX_VIEWPORT_DIMS must be at least as large as the largest
|
||||
// renderable surface, or a full-size framebuffer could not be fully viewported.
|
||||
EXPECT_GE(viewportDims[0], maxRenderbufferSize);
|
||||
EXPECT_GE(viewportDims[1], maxRenderbufferSize);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
// branch on. The driver POST's "Buffer textures" row is where that verdict is stated.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -73,7 +74,60 @@ out vec4 o_color;
|
||||
void main() { o_color = vec4(float(vFace) / 255.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
class BufferTextureScenario : public ScenarioTest {};
|
||||
// A buffer texture bound as a WRITABLE image: the shader reads one texel and writes
|
||||
// another, so a single dispatch proves the read direction (which already worked) and
|
||||
// the write direction (which is what this exists for) apart from each other.
|
||||
constexpr const char* kImageBufferCS = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 0, rgba8) uniform imageBuffer uImage;
|
||||
void main() {
|
||||
vec4 read = imageLoad(uImage, 1);
|
||||
imageStore(uImage, 0, vec4(0.0, 1.0, 0.0, 1.0));
|
||||
imageStore(uImage, 2, read);
|
||||
}
|
||||
)";
|
||||
|
||||
class BufferTextureScenario : public ScenarioTest {
|
||||
protected:
|
||||
bool ComputeImagesAreUsable() const {
|
||||
GLint maxImageUnits = 0;
|
||||
GLint maxComputeImageUniforms = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return maxImageUnits >= 1 && maxComputeImageUniforms >= 1;
|
||||
}
|
||||
|
||||
unsigned int MakeComputeProgram(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute shader did not compile: " << log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute program did not link: " << log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
};
|
||||
|
||||
// Draws the full-viewport quad and returns the red byte every fragment was painted with,
|
||||
// or -1 if the quad did not come out uniform (which would mean the flat varying, not the
|
||||
@@ -171,4 +225,78 @@ void main() { o_color = vec4(float(vFace) / 255.0, 0.0, 0.0, 1.0); }
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
// A shader may WRITE a buffer texture too, through an image unit, and the bytes it writes
|
||||
// land in the backend's buffer - not in the frontend's CPU shadow, which is what MapBuffer
|
||||
// and GetBufferSubData hand back. A storage-block write is flagged for exactly this reason
|
||||
// and the shadow is refreshed on the next read; a buffer reached through an image unit is
|
||||
// the same write through a different binding, and Espryt used to flag only the first, so
|
||||
// an imageStore into a buffer texture was invisible to every CPU read that followed it -
|
||||
// silently, with the correct value sitting in the driver's buffer the whole time.
|
||||
//
|
||||
// The read direction is asserted in the same dispatch (texel 2 is a copy of texel 1) so a
|
||||
// failure here cannot be blamed on the image binding not working at all.
|
||||
TEST_F(BufferTextureScenario, AnImageStoreIntoABufferTextureIsVisibleToTheCpu) {
|
||||
if (!Ready()) return;
|
||||
if (!ComputeImagesAreUsable()) GTEST_SKIP() << "no compute image units on this host";
|
||||
|
||||
constexpr GLuint kRed = 0x000000ffu; // RGBA8 little-endian: r = 255
|
||||
constexpr GLuint kGreen = 0xff00ff00u; // what the shader stores: (0, 1, 0, 1)
|
||||
constexpr int kTexels = 16;
|
||||
|
||||
FirstGLError();
|
||||
|
||||
const unsigned int program = MakeComputeProgram(kImageBufferCS);
|
||||
ASSERT_NE(program, 0u);
|
||||
|
||||
const std::vector<GLuint> texels(kTexels, kRed);
|
||||
GLuint buffer = 0;
|
||||
glGenBuffers(1, &buffer);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(texels.size() * sizeof(GLuint)), texels.data(),
|
||||
GL_DYNAMIC_COPY);
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, buffer);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glTexBuffer(GL_RGBA8) was refused";
|
||||
|
||||
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glBindImageTexture on a buffer texture was refused";
|
||||
|
||||
glUseProgram(program);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
|
||||
// Both CPU read paths, because they are two entry points onto the same refresh and a
|
||||
// fix that reaches only one of them is not a fix. Everything below is EXPECT rather than
|
||||
// ASSERT so that a failure still reaches the cleanup at the end: the harness shares one
|
||||
// context across every scenario in the process, and a leaked buffer or image binding
|
||||
// here would surface as a failure somewhere else entirely.
|
||||
std::vector<GLuint> readBack(kTexels, 0u);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||
glGetBufferSubData(GL_TEXTURE_BUFFER, 0, static_cast<GLsizeiptr>(readBack.size() * sizeof(GLuint)),
|
||||
readBack.data());
|
||||
EXPECT_EQ(readBack[0], kGreen) << "glGetBufferSubData did not see the imageStore";
|
||||
EXPECT_EQ(readBack[2], kRed) << "the imageLoad side of the same dispatch read the wrong texel";
|
||||
|
||||
const void* mapped = glMapBuffer(GL_TEXTURE_BUFFER, GL_READ_ONLY);
|
||||
EXPECT_NE(mapped, nullptr) << "glMapBuffer(GL_READ_ONLY) on the texture's buffer failed";
|
||||
if (mapped != nullptr) {
|
||||
GLuint mappedTexel0 = 0;
|
||||
std::memcpy(&mappedTexel0, mapped, sizeof(mappedTexel0));
|
||||
EXPECT_EQ(mappedTexel0, kGreen) << "glMapBuffer did not see the imageStore";
|
||||
glUnmapBuffer(GL_TEXTURE_BUFFER);
|
||||
}
|
||||
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteTextures(1, &texture);
|
||||
glDeleteBuffers(1, &buffer);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClipDistanceScenario.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
|
||||
//
|
||||
// Scenario - gl_ClipDistance ACTUALLY CLIPS, AND ONLY WHERE IT IS ENABLED.
|
||||
//
|
||||
// CapabilityInput::ClipDistance0..7 existed end to end - the GL enum converted to it, the
|
||||
// string converter named it, glEnable(GL_CLIP_DISTANCE0 + i) raised no error - and then
|
||||
// RenderState::SetCapability had no case for it and dropped it into `default: break`. Nothing
|
||||
// was stored, no version was bumped, and neither backend ever heard about it. The shader half
|
||||
// worked all along (SPIRV-Cross emits gl_ClipDistance with a
|
||||
// `#extension GL_EXT_clip_cull_distance : require` that Adreno accepts), so the distances were
|
||||
// computed and then ignored: no clipping ever happened on DirectGLES, which is the whole of
|
||||
// KHR-GLxx.clip_distance.functional. glIsEnabled lied about it too - it returned GL_FALSE
|
||||
// immediately after a successful glEnable.
|
||||
//
|
||||
// The assertions are behavioural, not query-shaped, because a query-only test passes against a
|
||||
// backend that stores the bit and never forwards it. Each case draws one full-viewport triangle
|
||||
// whose clip distance is positive on one side of the viewport and negative on the other, then
|
||||
// checks BOTH sides: the kept side proves the draw happened at all, and the clipped side is the
|
||||
// actual claim. The disabled case is the negative control - the identical shader with the
|
||||
// identical distances and the enable turned off must leave both sides painted, which is what
|
||||
// says the pixels below are being removed by clipping and not by something else.
|
||||
//
|
||||
// HONEST LIMIT OF THIS FILE IN CI. Of the four cases, only EnableIsObservableThroughIsEnabled is
|
||||
// falsifiable on the software rasterizers every automated lane runs on. llvmpipe and lavapipe
|
||||
// clip by EVERY declared gl_ClipDistance regardless of the enables, so
|
||||
// AnEnabledClipDistanceRemovesTheNegativeHalf goes green there against the broken tree as well,
|
||||
// and the two cases that need real per-distance semantics skip (see
|
||||
// DriverHonoursPerDistanceEnables). What actually pins the behaviour is Adreno, through
|
||||
// KHR-GLxx.clip_distance.functional - whose "without dynamic redeclaration" variants declare all
|
||||
// gl_MaxClipDistances slots and enable only the first N, i.e. exactly the subset semantics these
|
||||
// skipped cases assert. Read a green CI run here as "the state survives the frontend", not as
|
||||
// "clipping is correct"; the second claim is a device claim.
|
||||
//
|
||||
// Every case disables all eight distances on entry rather than assuming they start off:
|
||||
// XfbAfterClipDistanceScenario deliberately leaves one enabled for the rest of the process, and
|
||||
// forwarding the enables is what turned that leftover from inert bookkeeping into live driver
|
||||
// state.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
#ifndef GL_CLIP_DISTANCE0
|
||||
#define GL_CLIP_DISTANCE0 0x3000
|
||||
#endif
|
||||
#ifndef GL_CLIP_DISTANCE1
|
||||
#define GL_CLIP_DISTANCE1 0x3001
|
||||
#endif
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// One clip distance per half of the viewport: distance 0 is positive on the right half
|
||||
// (x > 0 in clip space) and distance 1 is positive on the top half. A vertex shader
|
||||
// producing a full-screen triangle from gl_VertexID, so no buffers are needed.
|
||||
const char* const kVertexSource = R"(#version 400 core
|
||||
out float gl_ClipDistance[2];
|
||||
void main() {
|
||||
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
vec2 p = positions[gl_VertexID];
|
||||
gl_Position = vec4(p, 0.0, 1.0);
|
||||
gl_ClipDistance[0] = p.x;
|
||||
gl_ClipDistance[1] = p.y;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kFragmentSource = R"(#version 400 core
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
class ClipDistanceScenario : public ScenarioTest {
|
||||
protected:
|
||||
GLuint BuildProgram() {
|
||||
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderSource(vs, 1, &kVertexSource, nullptr);
|
||||
glCompileShader(vs);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(vs, GL_COMPILE_STATUS, &compiled);
|
||||
if (!compiled) {
|
||||
m_buildLog = ShaderLog(vs);
|
||||
glDeleteShader(vs);
|
||||
return 0;
|
||||
}
|
||||
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(fs, 1, &kFragmentSource, nullptr);
|
||||
glCompileShader(fs);
|
||||
glGetShaderiv(fs, GL_COMPILE_STATUS, &compiled);
|
||||
if (!compiled) {
|
||||
m_buildLog = ShaderLog(fs);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
if (!linked) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
m_buildLog = log.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
const std::string& BuildLog() const { return m_buildLog; }
|
||||
|
||||
// Paints the whole viewport red, then draws the clipped triangle in green.
|
||||
void DrawClippedTriangle(GLuint program, GLuint vao) const {
|
||||
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(program);
|
||||
glBindVertexArray(vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
}
|
||||
|
||||
static bool IsGreen(const unsigned char* px) {
|
||||
return px[0] < 64 && px[1] > 192;
|
||||
}
|
||||
|
||||
static bool IsRed(const unsigned char* px) {
|
||||
return px[0] > 192 && px[1] < 64;
|
||||
}
|
||||
|
||||
void PixelAt(int x, int y, unsigned char* out) const {
|
||||
glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out);
|
||||
}
|
||||
|
||||
// Never assume the eight start disabled - see the header note about
|
||||
// XfbAfterClipDistanceScenario leaving one on for the rest of the process.
|
||||
static void DisableEveryClipDistance() {
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
glDisable(static_cast<GLenum>(GL_CLIP_DISTANCE0 + i));
|
||||
}
|
||||
}
|
||||
|
||||
// True when the driver under this backend actually implements PER-DISTANCE enable
|
||||
// state, i.e. when a written-but-disabled gl_ClipDistance leaves its fragments
|
||||
// alone. Not every stack does, and the difference is not MobileGL's to hide:
|
||||
//
|
||||
// - Adreno's ES driver honours GL_CLIP_DISTANCE0_EXT..7_EXT, which is what makes
|
||||
// KHR-GLxx.clip_distance.functional pass on the device once the enables are
|
||||
// forwarded at all.
|
||||
// - Vulkan has no such state: every clip distance a shader declares is active,
|
||||
// always. DirectVulkan therefore clips by a disabled distance.
|
||||
// - Mesa's llvmpipe ES driver behaves like Vulkan here.
|
||||
//
|
||||
// Emulating GL's semantics on those two would mean forcing the disabled slots to a
|
||||
// non-negative value inside the shader, which makes the enable mask part of the
|
||||
// pipeline key - a feature, not a fix, and deliberately not attempted here. The
|
||||
// cases that need the real semantics gate on this probe and say so when they skip,
|
||||
// rather than being deleted or silently weakened.
|
||||
bool DriverHonoursPerDistanceEnables(GLuint program, GLuint vao) const {
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
glDisable(static_cast<GLenum>(GL_CLIP_DISTANCE0 + i));
|
||||
}
|
||||
DrawClippedTriangle(program, vao);
|
||||
unsigned char negativeSide[4] = {0, 0, 0, 0};
|
||||
glReadPixels(Gl().Width() / 4, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, negativeSide);
|
||||
return IsGreen(negativeSide);
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string ShaderLog(GLuint shader) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
return log.data();
|
||||
}
|
||||
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The state itself: glEnable must be observable through glIsEnabled. This is the cheap half
|
||||
// of the bug - SetCapability's missing case made the query answer GL_FALSE for a capability
|
||||
// that had just been enabled without error.
|
||||
TEST_F(ClipDistanceScenario, EnableIsObservableThroughIsEnabled) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
DisableEveryClipDistance();
|
||||
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE0), GL_FALSE)
|
||||
<< "glDisable(GL_CLIP_DISTANCE0) is not observable through glIsEnabled";
|
||||
glEnable(GL_CLIP_DISTANCE0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE0), GL_TRUE)
|
||||
<< "glEnable(GL_CLIP_DISTANCE0) raised no error but glIsEnabled still reports it disabled";
|
||||
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE1), GL_FALSE)
|
||||
<< "enabling distance 0 must not enable distance 1 - the eight are independent";
|
||||
|
||||
glEnable(GL_CLIP_DISTANCE1);
|
||||
glDisable(GL_CLIP_DISTANCE0);
|
||||
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE0), GL_FALSE);
|
||||
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE1), GL_TRUE);
|
||||
|
||||
glDisable(GL_CLIP_DISTANCE1);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// The claim: an enabled clip distance removes the fragments where it is negative.
|
||||
TEST_F(ClipDistanceScenario, AnEnabledClipDistanceRemovesTheNegativeHalf) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
const GLuint program = BuildProgram();
|
||||
ASSERT_NE(program, 0u) << "the gl_ClipDistance program did not build: " << BuildLog();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
|
||||
// Distance 1 is positive by a single pixel at the sampled row, so a stray enable on it
|
||||
// would put the "kept" probe right on the clip boundary.
|
||||
DisableEveryClipDistance();
|
||||
glEnable(GL_CLIP_DISTANCE0);
|
||||
DrawClippedTriangle(program, vao);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
unsigned char right[4] = {0, 0, 0, 0};
|
||||
unsigned char left[4] = {0, 0, 0, 0};
|
||||
PixelAt(width - 1 - width / 4, height / 2, right);
|
||||
PixelAt(width / 4, height / 2, left);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
EXPECT_TRUE(IsGreen(right)) << "the kept half is not painted (" << int(right[0]) << "," << int(right[1])
|
||||
<< "," << int(right[2]) << ") - the draw itself did not happen, so the clipped "
|
||||
"half below proves nothing";
|
||||
EXPECT_TRUE(IsRed(left)) << "gl_ClipDistance[0] is negative on the left half and GL_CLIP_DISTANCE0 is "
|
||||
"enabled, so those fragments must be clipped away; found ("
|
||||
<< int(left[0]) << "," << int(left[1]) << "," << int(left[2]) << ")";
|
||||
|
||||
glDisable(GL_CLIP_DISTANCE0);
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// The negative control: the same shader writing the same distances, with the enable off,
|
||||
// must paint both halves. Without this a backend that clipped everything - or one whose
|
||||
// draw simply failed - would pass the case above.
|
||||
TEST_F(ClipDistanceScenario, ADisabledClipDistanceRemovesNothing) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
const GLuint program = BuildProgram();
|
||||
ASSERT_NE(program, 0u) << "the gl_ClipDistance program did not build: " << BuildLog();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
DisableEveryClipDistance();
|
||||
|
||||
DrawClippedTriangle(program, vao);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
unsigned char right[4] = {0, 0, 0, 0};
|
||||
unsigned char left[4] = {0, 0, 0, 0};
|
||||
PixelAt(width - 1 - width / 4, height / 2, right);
|
||||
PixelAt(width / 4, height / 2, left);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
EXPECT_TRUE(IsGreen(right)) << "with every clip distance disabled the whole triangle must survive";
|
||||
const bool driverHonoursEnables = IsGreen(left);
|
||||
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
gl.EndFrame();
|
||||
if (!driverHonoursEnables) {
|
||||
GTEST_SKIP() << "renderer " << gl.RendererString()
|
||||
<< " clips by a DISABLED gl_ClipDistance - it does not implement per-distance enable state "
|
||||
"(see DriverHonoursPerDistanceEnables). Emulating GL's semantics there needs shader-side "
|
||||
"masking keyed on the enable mask, which is a separate feature";
|
||||
}
|
||||
}
|
||||
|
||||
// The eight enables are independent: enabling only distance 1 must clip by distance 1 and
|
||||
// leave distance 0 alone. A backend that forwarded "any clip distance enabled" as a single
|
||||
// bit, or that always enables every declared distance (which is what Vulkan does natively),
|
||||
// passes both cases above and fails this one.
|
||||
TEST_F(ClipDistanceScenario, TheEnablesAreIndependentPerDistance) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
const GLuint program = BuildProgram();
|
||||
ASSERT_NE(program, 0u) << "the gl_ClipDistance program did not build: " << BuildLog();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
if (!DriverHonoursPerDistanceEnables(program, vao)) {
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
DisableEveryClipDistance();
|
||||
gl.EndFrame();
|
||||
GTEST_SKIP() << "renderer " << gl.RendererString()
|
||||
<< " clips by every declared gl_ClipDistance regardless of the enables, so per-distance "
|
||||
"independence is not observable here";
|
||||
}
|
||||
|
||||
DisableEveryClipDistance();
|
||||
glEnable(GL_CLIP_DISTANCE1);
|
||||
|
||||
DrawClippedTriangle(program, vao);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// Distance 1 is negative on the bottom half, distance 0 on the left half. With only
|
||||
// distance 1 enabled, the bottom-left must survive (distance 0 is off) and the bottom
|
||||
// must not.
|
||||
unsigned char topLeft[4] = {0, 0, 0, 0};
|
||||
unsigned char bottomRight[4] = {0, 0, 0, 0};
|
||||
PixelAt(width / 4, height - 1 - height / 4, topLeft);
|
||||
PixelAt(width - 1 - width / 4, height / 4, bottomRight);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
EXPECT_TRUE(IsGreen(topLeft)) << "gl_ClipDistance[0] is negative here but GL_CLIP_DISTANCE0 is disabled, so "
|
||||
"this fragment must survive";
|
||||
EXPECT_TRUE(IsRed(bottomRight)) << "gl_ClipDistance[1] is negative here and GL_CLIP_DISTANCE1 is enabled, so "
|
||||
"this fragment must be clipped";
|
||||
|
||||
glDisable(GL_CLIP_DISTANCE1);
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,331 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageLayeredScenario.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
|
||||
//
|
||||
// Scenario - glCopyImageSubData MOVES EVERY SLICE IT WAS ASKED FOR, NOT JUST SLICE 0.
|
||||
//
|
||||
// KHR-GL43.copy_image.functional_* copies a whole 12-layer region in one call whenever both
|
||||
// endpoints are layered, i.e. for the four target pairs 2d_array->2d_array, 2d_array->3d,
|
||||
// 3d->2d_array and 3d->3d. DirectVulkan built its VkImageCopy with baseArrayLayer 0, layerCount 1
|
||||
// and srcOffset.z 0 no matter what the call asked for, so slice 0 landed correctly and slices 1..N
|
||||
// were never written - 64 conformance cases (16 compatible format pairs x those 4 pairs) failing
|
||||
// with "first mismatch at [x, y, 1]", the first texel of the first slice the copy skipped.
|
||||
//
|
||||
// The reason one hardcode covered both shapes wrongly is that GL states a layered copy ONE way -
|
||||
// srcZ/dstZ and srcDepth - while Vulkan states it two ways and picks by image type:
|
||||
//
|
||||
// GL_TEXTURE_3D -> VK_IMAGE_TYPE_3D: slices are z, so srcOffset.z/dstOffset.z select them
|
||||
// and extent.depth counts them; the layer range must stay (0, 1).
|
||||
// GL_TEXTURE_2D_ARRAY -> VK_IMAGE_TYPE_2D: slices are array layers, so baseArrayLayer selects
|
||||
// them and layerCount counts them; offset.z stays 0.
|
||||
//
|
||||
// A mixed pair is legal (maintenance1, core in Vulkan 1.1) but only when the counts correspond:
|
||||
// the 3D side's extent.depth has to equal the array side's layerCount. So the four pairs below are
|
||||
// four DIFFERENT VkImageCopy shapes, not one shape with different arguments, which is why one
|
||||
// scenario per pair is the coverage that matters here.
|
||||
//
|
||||
// Every case also asserts the slices OUTSIDE the copied range still hold their fill. A backend
|
||||
// that "fixed" the miss by copying the whole image regardless of srcZ/srcDepth would pass a
|
||||
// slices-landed check and fail this one.
|
||||
//
|
||||
// The verification path is an FBO attachment per slice plus glReadPixels, not glGetTexImage: it is
|
||||
// the readback both backends share, and glFramebufferTextureLayer names an array layer and a 3D
|
||||
// slice through the same call, so the two texture kinds are read back identically.
|
||||
//
|
||||
// DirectGLES is the control - it forwards to the driver's own glCopyImageSubData - so a failure on
|
||||
// both backends means the scenario is wrong, and a failure on DirectVulkan alone means Magma is.
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kWidth = 4;
|
||||
constexpr int kHeight = 4;
|
||||
// Six is enough for a copy that starts and ends away from both edges of both endpoints
|
||||
// while still leaving untouched slices on either side to assert against.
|
||||
constexpr int kSlices = 6;
|
||||
|
||||
struct Rgba8 {
|
||||
GLubyte r = 0, g = 0, b = 0, a = 0;
|
||||
|
||||
bool operator==(const Rgba8& other) const {
|
||||
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||
}
|
||||
};
|
||||
|
||||
std::string Describe(const Rgba8& color) {
|
||||
return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " + std::to_string(color.b) +
|
||||
", " + std::to_string(color.a) + ")";
|
||||
}
|
||||
|
||||
// Per-slice constants, uniform within a slice. A uniform fill is deliberate: the defect is
|
||||
// in which SLICE the copy addresses, and a value that also varied within the slice would
|
||||
// make the assertions depend on the framebuffer row order as well.
|
||||
Rgba8 SourceColor(int slice) {
|
||||
return {static_cast<GLubyte>(10 + slice * 20), static_cast<GLubyte>(40 + slice * 10),
|
||||
static_cast<GLubyte>(200 - slice * 15), 255};
|
||||
}
|
||||
|
||||
Rgba8 DestinationFill(int slice) {
|
||||
return {static_cast<GLubyte>(3 + slice), static_cast<GLubyte>(250 - slice * 7),
|
||||
static_cast<GLubyte>(120 + slice * 5), 255};
|
||||
}
|
||||
|
||||
class CopyImageLayeredScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (!CopyImageSubDataUsable()) {
|
||||
GTEST_SKIP() << "glCopyImageSubData is unavailable on backend " << Gl().BackendName();
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
for (const GLuint texture : m_textures) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
m_textures.clear();
|
||||
if (m_fbo != 0) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
m_fbo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// A trivial 1x1x1 array-to-array copy: it exercises the entry point without depending
|
||||
// on any of the behaviour under test, so a driver (or a backend function table) that
|
||||
// simply does not have the call skips instead of failing every case below.
|
||||
bool CopyImageSubDataUsable() {
|
||||
GLuint probe[2] = {0, 0};
|
||||
glGenTextures(2, probe);
|
||||
for (const GLuint texture : probe) {
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 1, 1, 1);
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
glCopyImageSubData(probe[0], GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, probe[1], GL_TEXTURE_2D_ARRAY, 0, 0, 0,
|
||||
0, 1, 1, 1);
|
||||
const bool usable = glGetError() == GL_NO_ERROR;
|
||||
glDeleteTextures(2, probe);
|
||||
return usable;
|
||||
}
|
||||
|
||||
// `target` is GL_TEXTURE_2D_ARRAY or GL_TEXTURE_3D; both take glTexStorage3D and
|
||||
// glTexSubImage3D with the slice on the same axis, which is the whole reason GL can
|
||||
// copy between them. `levels` > 1 puts a real mip chain behind the level the copy
|
||||
// names, so the level's own extent - a 3D level's depth included - has to be resolved
|
||||
// rather than assumed to be the image's.
|
||||
GLuint MakeTexture(GLenum target, int levels, Rgba8 (*colorForSlice)(int)) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(target, texture);
|
||||
glTexStorage3D(target, levels, GL_RGBA8, kWidth << (levels - 1), kHeight << (levels - 1),
|
||||
target == GL_TEXTURE_3D ? (kSlices << (levels - 1)) : kSlices);
|
||||
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
|
||||
// Fill every level, so nothing below can pass by reading a level that was never
|
||||
// written and happened to hold the expected bytes.
|
||||
for (int level = 0; level < levels; ++level) {
|
||||
const int levelWidth = kWidth << (levels - 1 - level);
|
||||
const int levelHeight = kHeight << (levels - 1 - level);
|
||||
const int levelSlices =
|
||||
target == GL_TEXTURE_3D ? (kSlices << (levels - 1 - level)) : kSlices;
|
||||
for (int slice = 0; slice < levelSlices; ++slice) {
|
||||
const Rgba8 color = colorForSlice(slice % kSlices);
|
||||
std::vector<Rgba8> texels(static_cast<size_t>(levelWidth) * levelHeight, color);
|
||||
glTexSubImage3D(target, level, 0, 0, slice, levelWidth, levelHeight, 1, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, texels.data());
|
||||
}
|
||||
}
|
||||
glBindTexture(target, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// One slice of one level, through an FBO attachment. glFramebufferTextureLayer takes an
|
||||
// array layer and a 3D slice through the same argument, so both targets read back the
|
||||
// same way.
|
||||
Rgba8 ReadSlice(GLuint texture, int level, int slice, int width, int height) {
|
||||
if (m_fbo == 0) {
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, level, slice);
|
||||
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "slice " << slice << " of level " << level << " is not attachable";
|
||||
std::vector<Rgba8> pixels(static_cast<size_t>(width) * height, Rgba8{});
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
// The fill is uniform within a slice, so any disagreement between texels is itself
|
||||
// a failure - reported here rather than silently reduced to pixels[0].
|
||||
for (size_t i = 1; i < pixels.size(); ++i) {
|
||||
EXPECT_TRUE(pixels[i] == pixels[0])
|
||||
<< "slice " << slice << " of level " << level << " is not uniform: texel 0 is "
|
||||
<< Describe(pixels[0]) << ", texel " << i << " is " << Describe(pixels[i]);
|
||||
}
|
||||
return pixels[0];
|
||||
}
|
||||
|
||||
// The assertion every case ends with: slices inside [dstZ, dstZ + depth) hold the
|
||||
// source slice they were fed, and every slice outside it still holds its own fill.
|
||||
void ExpectCopied(GLuint destination, int level, int width, int height, int sliceCount, int srcZ,
|
||||
int dstZ, int depth, const char* what) {
|
||||
for (int slice = 0; slice < sliceCount; ++slice) {
|
||||
const bool inRange = slice >= dstZ && slice < dstZ + depth;
|
||||
const Rgba8 expected =
|
||||
inRange ? SourceColor(srcZ + (slice - dstZ)) : DestinationFill(slice);
|
||||
const Rgba8 actual = ReadSlice(destination, level, slice, width, height);
|
||||
EXPECT_TRUE(actual == expected)
|
||||
<< what << ": destination slice " << slice << (inRange ? " (copied)" : " (untouched)")
|
||||
<< " is " << Describe(actual) << ", expected " << Describe(expected);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_textures;
|
||||
GLuint m_fbo = 0;
|
||||
};
|
||||
|
||||
// 2d_array -> 2d_array. Both endpoints put the slices on the layer axis, so BOTH layer
|
||||
// counts carry the depth and extent.depth must stay 1.
|
||||
TEST_F(CopyImageLayeredScenario, ArrayToArrayCopiesEverySlice) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0,
|
||||
kWidth, kHeight, kSlices);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, 0, 0, kSlices, "array->array, all slices");
|
||||
}
|
||||
|
||||
// The same pair with the layer ranges offset differently on the two sides: the shape that
|
||||
// separates "copies more than slice 0" from "copies the RIGHT slices". A backend that read
|
||||
// the source range but wrote from layer 0 (or vice versa) passes the case above.
|
||||
TEST_F(CopyImageLayeredScenario, ArrayToArrayHonoursDifferentLayerOffsets) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 3;
|
||||
constexpr int kDstZ = 1;
|
||||
constexpr int kDepth = 2;
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0,
|
||||
kDstZ, kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
|
||||
"array->array, offset layer ranges");
|
||||
}
|
||||
|
||||
// 3d -> 3d. Neither endpoint has array layers at all: the depth travels on extent.depth and
|
||||
// the offsets on srcOffset.z/dstOffset.z, with both layer counts pinned to 1.
|
||||
TEST_F(CopyImageLayeredScenario, VolumeToVolumeHonoursNonZeroZ) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 1;
|
||||
constexpr int kDstZ = 3;
|
||||
constexpr int kDepth = 3;
|
||||
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
|
||||
kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->3d, non-zero z");
|
||||
}
|
||||
|
||||
// The same pair one mip level down. A 3D level's DEPTH halves with its width and height, so
|
||||
// this is the only case where the slice count the copy may name is not the image's own -
|
||||
// the bound a layered endpoint is checked against has to come from the level.
|
||||
TEST_F(CopyImageLayeredScenario, VolumeToVolumeAtNonZeroMipLevel) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_3D, 2, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 2, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kLevel = 1;
|
||||
constexpr int kSrcZ = 2;
|
||||
constexpr int kDstZ = 0;
|
||||
constexpr int kDepth = 4;
|
||||
glCopyImageSubData(source, GL_TEXTURE_3D, kLevel, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, kLevel, 0, 0,
|
||||
kDstZ, kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, kLevel, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
|
||||
"3d->3d at mip level 1");
|
||||
}
|
||||
|
||||
// 2d_array -> 3d. The mixed shape: the source counts its slices as layers, the destination
|
||||
// as depth, and Vulkan requires extent.depth to equal the source's layerCount.
|
||||
TEST_F(CopyImageLayeredScenario, ArrayToVolumeCopiesEverySlice) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 2;
|
||||
constexpr int kDstZ = 1;
|
||||
constexpr int kDepth = 4;
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
|
||||
kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "2d_array->3d");
|
||||
}
|
||||
|
||||
// 3d -> 2d_array, the mirror image: the depth now has to reach the DESTINATION's layerCount
|
||||
// while the source states it as extent.depth from a z offset.
|
||||
TEST_F(CopyImageLayeredScenario, VolumeToArrayCopiesEverySlice) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 1;
|
||||
constexpr int kDstZ = 2;
|
||||
constexpr int kDepth = 4;
|
||||
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kDstZ,
|
||||
kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->2d_array");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,209 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageLevelRangeScenario.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
|
||||
//
|
||||
// KHR-GL43.copy_image.non_existent_mipmap, and what it cost.
|
||||
//
|
||||
// The CTS case is a pure negative test: two 16x16 textures that have level 0 and
|
||||
// nothing else, and a glCopyImageSubData naming level 1. The answer is
|
||||
// GL_INVALID_VALUE (GL 4.6 core 18.3.2 / ARB_copy_image: "srcLevel/dstLevel is not
|
||||
// a valid level"). MobileGL's frontend only checked the level against
|
||||
// GL_MAX_TEXTURE_SIZE, so level 1 sailed through into the backends, DirectVulkan
|
||||
// resolved it into a VkImageCopy subresource on a VkImage that was created with
|
||||
// exactly one mip level, and the Adreno driver dereferenced the level it was
|
||||
// promised - SIGSEGV inside vkCmdCopyImage, taking the whole glcts process down
|
||||
// mid-run. A negative case must never do that.
|
||||
//
|
||||
// So the level-1-on-a-one-level-texture rejection is the regression proper, and the
|
||||
// rest of this file is what keeps the fix honest. A validator that answered
|
||||
// GL_INVALID_VALUE to every level would satisfy the regression tests alone, so the
|
||||
// scenarios below pin the BOUNDARY rather than the symptom:
|
||||
//
|
||||
// * a texture that really does have two levels must accept a copy at level 1,
|
||||
// * the same texture must still reject level 2,
|
||||
// * and a plain level-0 copy must move pixels, which is checked by reading the
|
||||
// destination back rather than by trusting glGetError.
|
||||
//
|
||||
// Both backends are covered because the fix is in the shared frontend: DirectGLES
|
||||
// forwards to the ES glCopyImageSubData (whose own error lands in the ES context,
|
||||
// not in MobileGL's, so it never reached the application either) and DirectVulkan
|
||||
// records the copy itself.
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr GLsizei kSize = 16;
|
||||
|
||||
struct Rgba8 {
|
||||
GLubyte r, g, b, a;
|
||||
bool operator==(const Rgba8& other) const {
|
||||
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<Rgba8> SolidImage(GLsizei width, GLsizei height, Rgba8 color) {
|
||||
return std::vector<Rgba8>(static_cast<std::size_t>(width) * static_cast<std::size_t>(height), color);
|
||||
}
|
||||
|
||||
class CopyImageLevelRangeScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
DeleteTextures();
|
||||
if (m_fbo != 0) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
m_fbo = 0;
|
||||
}
|
||||
DrainErrors();
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
void DeleteTextures() {
|
||||
if (m_src != 0) glDeleteTextures(1, &m_src);
|
||||
if (m_dst != 0) glDeleteTextures(1, &m_dst);
|
||||
m_src = 0;
|
||||
m_dst = 0;
|
||||
}
|
||||
|
||||
// One 16x16 RGBA8 texture with `levelCount` levels defined through
|
||||
// glTexImage2D - the same way the CTS case builds its textures, and
|
||||
// deliberately NOT glTexStorage2D: an immutable allocation would define the
|
||||
// whole chain up front and could not express "level 1 does not exist".
|
||||
GLuint MakeTexture(int levelCount, Rgba8 baseColor) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
for (int level = 0; level < levelCount; ++level) {
|
||||
const GLsizei extent = kSize >> level;
|
||||
const std::vector<Rgba8> pixels = SolidImage(extent, extent, baseColor);
|
||||
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, extent, extent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
pixels.data());
|
||||
}
|
||||
// What Utils::makeTextureComplete does in the CTS case: the texture is
|
||||
// complete for the levels it actually has, not for a chain it does not.
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, levelCount - 1);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
void MakePair(int levelCount) {
|
||||
DeleteTextures();
|
||||
m_src = MakeTexture(levelCount, Rgba8{11, 22, 33, 255});
|
||||
m_dst = MakeTexture(levelCount, Rgba8{200, 100, 50, 255});
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "texture setup with " << levelCount << " level(s)";
|
||||
}
|
||||
|
||||
// The call under test, at whatever levels the caller wants, over a 1x1
|
||||
// region so the region check can never be what rejects it.
|
||||
GLenum CopyAt(GLint srcLevel, GLint dstLevel, GLsizei extent = 1) {
|
||||
DrainErrors();
|
||||
glCopyImageSubData(m_src, GL_TEXTURE_2D, srcLevel, 0, 0, 0, m_dst, GL_TEXTURE_2D, dstLevel, 0, 0, 0,
|
||||
extent, extent, 1);
|
||||
const GLenum error = glGetError();
|
||||
// A second pending error would mean the entry point queued more than one,
|
||||
// and the extra would be handed out at an unrelated call site later.
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "the copy recorded more than one error";
|
||||
return error;
|
||||
}
|
||||
|
||||
Rgba8 ReadBackDestinationLevel0() {
|
||||
if (m_fbo == 0) glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_dst, 0);
|
||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
ADD_FAILURE() << "readback framebuffer incomplete: " << status;
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return Rgba8{0, 0, 0, 0};
|
||||
}
|
||||
Rgba8 texel{0, 0, 0, 0};
|
||||
glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &texel);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return texel;
|
||||
}
|
||||
|
||||
GLuint m_src = 0;
|
||||
GLuint m_dst = 0;
|
||||
GLuint m_fbo = 0;
|
||||
};
|
||||
|
||||
// The regression. Level 1 of a texture that has only level 0 is not a level, and
|
||||
// saying so is the whole job: before the fix this reached DirectVulkan, which
|
||||
// handed mipLevel=1 to vkCmdCopyImage on a one-level VkImage and died inside the
|
||||
// Adreno driver.
|
||||
TEST_F(CopyImageLevelRangeScenario, LevelOneOfASingleLevelTextureIsRejected) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakePair(1);
|
||||
|
||||
EXPECT_EQ(CopyAt(1, 0), static_cast<GLenum>(GL_INVALID_VALUE)) << "source level 1";
|
||||
EXPECT_EQ(CopyAt(0, 1), static_cast<GLenum>(GL_INVALID_VALUE)) << "destination level 1";
|
||||
EXPECT_EQ(CopyAt(1, 1), static_cast<GLenum>(GL_INVALID_VALUE)) << "both levels 1";
|
||||
}
|
||||
|
||||
// The negative control that makes the test above falsifiable: the same level
|
||||
// index, on textures that genuinely have it, must be accepted. A validator that
|
||||
// rejected every non-zero level would pass the regression test and fail here.
|
||||
TEST_F(CopyImageLevelRangeScenario, LevelOneOfATwoLevelTextureIsAccepted) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakePair(2);
|
||||
|
||||
EXPECT_EQ(CopyAt(1, 1), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
// And the boundary from the other side: two levels means 0 and 1, not 2.
|
||||
TEST_F(CopyImageLevelRangeScenario, LevelTwoOfATwoLevelTextureIsRejected) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakePair(2);
|
||||
|
||||
EXPECT_EQ(CopyAt(2, 0), static_cast<GLenum>(GL_INVALID_VALUE)) << "source level 2";
|
||||
EXPECT_EQ(CopyAt(0, 2), static_cast<GLenum>(GL_INVALID_VALUE)) << "destination level 2";
|
||||
}
|
||||
|
||||
// Errors alone cannot tell an accepted copy from a silently dropped one, so the
|
||||
// ordinary case is checked by reading the destination back: the copy has to move
|
||||
// the source's texel, not merely decline to complain.
|
||||
TEST_F(CopyImageLevelRangeScenario, AValidLevelZeroCopyStillMovesPixels) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakePair(1);
|
||||
|
||||
ASSERT_EQ(ReadBackDestinationLevel0(), (Rgba8{200, 100, 50, 255})) << "destination before the copy";
|
||||
EXPECT_EQ(CopyAt(0, 0, kSize), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_EQ(ReadBackDestinationLevel0(), (Rgba8{11, 22, 33, 255})) << "destination after the copy";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackAttachmentShapeScenario.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
|
||||
//
|
||||
// Scenario - DEPTH/STENCIL READBACK WHEN THE ATTACHMENT IS NOT A PLAIN GL_TEXTURE_2D,
|
||||
// AND THE DEFAULT FRAMEBUFFER'S ADVERTISED DEPTH/STENCIL FORMAT.
|
||||
//
|
||||
// Three shipped defects, all of them invisible to a test that only ever attaches a 2D texture
|
||||
// or only ever asks the default framebuffer for a colour value.
|
||||
//
|
||||
// (1) The ES depth/stencil readback emulation identifies the source format by binding the
|
||||
// attachment's texture NAME to GL_TEXTURE_2D and asking that target for its internal
|
||||
// format. A name whose target is GL_TEXTURE_2D_ARRAY (attached by
|
||||
// glFramebufferTextureLayer) makes the bind answer GL_INVALID_OPERATION and change
|
||||
// nothing - so the query then truthfully describes whatever texture was already on
|
||||
// GL_TEXTURE_2D, which on that path is the emulation's own staging scratch. A wrong
|
||||
// answer that looks like a right one: the staging blit is issued between mismatched
|
||||
// depth formats, ES rejects it, and the read reports nothing at all.
|
||||
//
|
||||
// (2) Adreno answers GL_NONE for GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE on an attachment made
|
||||
// by glFramebufferTexture (a cube map, attached layered) while still reporting its depth
|
||||
// and stencil bits correctly. The emulation took OBJECT_TYPE as the sole witness for "is
|
||||
// there an aspect here at all" and declined the whole read.
|
||||
//
|
||||
// (3) DirectGLES never told the frontend what its default framebuffer's depth/stencil format
|
||||
// actually is, so the placeholder from MG_Impl/Init.cpp - GL_DEPTH32F_STENCIL8 - was what
|
||||
// every attachment query answered, whatever the surface really had. That is not cosmetic:
|
||||
// GL blits depth/stencil only between IDENTICAL formats, so an application that reads
|
||||
// GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, allocates the buffer it was just told about and
|
||||
// blits gets GL_INVALID_OPERATION - and a rejected glBlitFramebuffer transfers NOTHING,
|
||||
// colour bits included. DirectVulkan has published its real format since the swapchain
|
||||
// work; this is the half that was missing.
|
||||
//
|
||||
// Every case poisons its destination with a value the correct answer cannot be, so "the
|
||||
// backend wrote nothing" fails loudly instead of passing on stale memory. The plain
|
||||
// GL_TEXTURE_2D case at the end is the built-in control: it shares every line of the readback
|
||||
// path with the array and cube cases, so its passing is what says a failure above is about the
|
||||
// attachment's SHAPE and not about depth readback in general.
|
||||
//
|
||||
// The scenario name starts with DepthStencilReadback on purpose - that is the filter the
|
||||
// forced-emulation ctest registration uses (MG_IntegrationTest/CMakeLists.txt), and without
|
||||
// that registration these cases are unfalsifiable on llvmpipe, which accepts the native ES
|
||||
// depth reads that the Adreno device does not have.
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr float kDepthPoison = 0.2f;
|
||||
constexpr int kStencilPoison = 50;
|
||||
constexpr float kDepthValue = 0.75f;
|
||||
constexpr int kStencilValue = 7;
|
||||
constexpr int kSize = 16;
|
||||
|
||||
class DepthStencilReadbackAttachmentShapeScenario : public ScenarioTest {
|
||||
protected:
|
||||
float ReadDepthAt(int x, int y) const {
|
||||
float depth = kDepthPoison;
|
||||
glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
|
||||
return depth;
|
||||
}
|
||||
|
||||
int ReadStencilAt(int x, int y) const {
|
||||
int stencil = kStencilPoison;
|
||||
glReadPixels(x, y, 1, 1, GL_STENCIL_INDEX, GL_INT, &stencil);
|
||||
return stencil;
|
||||
}
|
||||
|
||||
// Clears the currently bound framebuffer's depth and stencil to the shared
|
||||
// reference values, with both write masks explicitly open (glClear honours them,
|
||||
// and a leftover mask from another scenario in this shared context would look
|
||||
// exactly like the bug under test).
|
||||
void ClearDepthStencil() const {
|
||||
glDepthMask(GL_TRUE);
|
||||
glStencilMask(0xFFu);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearDepth(kDepthValue);
|
||||
glClearStencil(kStencilValue);
|
||||
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
}
|
||||
};
|
||||
|
||||
// Fails the calling test if the framebuffer bound at both targets is not complete;
|
||||
// an incomplete framebuffer would make every read below return the poison for a
|
||||
// reason that has nothing to do with what is being tested.
|
||||
::testing::AssertionResult FramebufferIsComplete() {
|
||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status == GL_FRAMEBUFFER_COMPLETE) return ::testing::AssertionSuccess();
|
||||
return ::testing::AssertionFailure() << "framebuffer status 0x" << std::hex << status;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// (1) A depth slice of a 2D ARRAY texture, attached with glFramebufferTextureLayer.
|
||||
// Pre-fix this read back the poison: the format probe answered with the staging scratch's
|
||||
// GL_DEPTH24_STENCIL8 instead of the array's GL_DEPTH_COMPONENT24, and the mismatched
|
||||
// staging blit was rejected.
|
||||
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DepthOfAnArrayLayerAttachmentReadsBack) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
GLuint fbo = 0;
|
||||
GLuint depthArray = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glGenTextures(1, &depthArray);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, depthArray);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH_COMPONENT24, kSize, kSize, 4);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
// Layer 2, not layer 0: a backend that silently reads the wrong slice would still
|
||||
// agree with a single-layer texture.
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthArray, 0, 2);
|
||||
glDrawBuffer(GL_NONE);
|
||||
glReadBuffer(GL_NONE);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
ClearDepthStencil();
|
||||
|
||||
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||
<< "glReadPixels(GL_DEPTH_COMPONENT) of a GL_TEXTURE_2D_ARRAY layer attachment returned " << depth
|
||||
<< (std::fabs(depth - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &depthArray);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// (2) A depth cube map, attached whole with glFramebufferTexture - a LAYERED attachment.
|
||||
// Pre-fix the emulation declined outright, because the driver reports GL_NONE for that
|
||||
// attachment's OBJECT_TYPE.
|
||||
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DepthOfALayeredCubeAttachmentReadsBack) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
GLuint fbo = 0;
|
||||
GLuint depthCube = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glGenTextures(1, &depthCube);
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCube);
|
||||
glTexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_DEPTH_COMPONENT24, kSize, kSize);
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCube, 0);
|
||||
glDrawBuffer(GL_NONE);
|
||||
glReadBuffer(GL_NONE);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
ClearDepthStencil();
|
||||
|
||||
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||
<< "glReadPixels(GL_DEPTH_COMPONENT) of a layered GL_TEXTURE_CUBE_MAP attachment returned " << depth
|
||||
<< (std::fabs(depth - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &depthCube);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// Both aspects of a packed array attachment. The stencil half goes through a different
|
||||
// sampling mode than the depth half, and only the depth half was covered above.
|
||||
TEST_F(DepthStencilReadbackAttachmentShapeScenario, PackedArrayLayerAttachmentReadsBackBothAspects) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
GLuint fbo = 0;
|
||||
GLuint packedArray = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glGenTextures(1, &packedArray);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, packedArray);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH24_STENCIL8, kSize, kSize, 3);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, packedArray, 0, 1);
|
||||
glDrawBuffer(GL_NONE);
|
||||
glReadBuffer(GL_NONE);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
ClearDepthStencil();
|
||||
|
||||
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
|
||||
const int stencil = ReadStencilAt(kSize / 2, kSize / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||
<< "depth of a packed GL_TEXTURE_2D_ARRAY layer attachment returned " << depth;
|
||||
EXPECT_EQ(stencil, kStencilValue)
|
||||
<< "stencil of a packed GL_TEXTURE_2D_ARRAY layer attachment returned " << stencil
|
||||
<< (stencil == kStencilPoison ? " - the destination was never written at all" : "");
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &packedArray);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// The control: the plain GL_TEXTURE_2D shape, which always worked. If this one ever fails
|
||||
// alongside the three above, the fault is in depth readback generally rather than in how
|
||||
// the attachment's format and presence are discovered.
|
||||
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DepthOfAPlainTexture2DAttachmentReadsBack) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
GLuint fbo = 0;
|
||||
GLuint depthTex = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glGenTextures(1, &depthTex);
|
||||
glBindTexture(GL_TEXTURE_2D, depthTex);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH_COMPONENT24, kSize, kSize);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0);
|
||||
glDrawBuffer(GL_NONE);
|
||||
glReadBuffer(GL_NONE);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
ClearDepthStencil();
|
||||
|
||||
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||
<< "the control case failed: even a plain GL_TEXTURE_2D depth attachment read back " << depth;
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &depthTex);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// (3) The default framebuffer must describe its depth/stencil truthfully enough that a
|
||||
// buffer allocated from that description is blit-compatible with it. This is the exact
|
||||
// sequence KHR-GLxx.framebuffer_blit performs, and the exact reason 22 of its cases died
|
||||
// on DirectGLES: the frontend answered 32-bit float depth for a 24-bit fixed-point
|
||||
// surface, so the renderbuffer the caller allocated could never be blitted to.
|
||||
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DefaultFramebufferDepthStencilFormatIsBlitCompatible) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
GLint depthBits = 0;
|
||||
GLint stencilBits = 0;
|
||||
GLint componentType = GL_UNSIGNED_NORMALIZED;
|
||||
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &depthBits);
|
||||
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_STENCIL,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &stencilBits);
|
||||
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE, &componentType);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
if (depthBits <= 0 || stencilBits <= 0) {
|
||||
GTEST_SKIP() << "this surface has no packed depth/stencil (depth=" << depthBits
|
||||
<< " stencil=" << stencilBits << "); the blit-compatibility contract needs both";
|
||||
}
|
||||
|
||||
// The one sized format the reported description names. Getting here with the wrong
|
||||
// answer is the bug: the two candidates are not interchangeable for a blit.
|
||||
const GLenum reported = (componentType == GL_FLOAT || depthBits > 24) ? GL_DEPTH32F_STENCIL8
|
||||
: GL_DEPTH24_STENCIL8;
|
||||
|
||||
GLuint fbo = 0;
|
||||
GLuint colorRbo = 0;
|
||||
GLuint depthRbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glGenRenderbuffers(1, &colorRbo);
|
||||
glGenRenderbuffers(1, &depthRbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, colorRbo);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, depthRbo);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, reported, width, height);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorRbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthRbo);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
// Put a known depth in the default framebuffer, then blit colour+depth+stencil out of
|
||||
// it into the buffer that its own description asked for.
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
ClearDepthStencil();
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height,
|
||||
GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, GL_NEAREST);
|
||||
EXPECT_EQ(FirstGLError(), 0u)
|
||||
<< "blitting depth/stencil out of the default framebuffer into a buffer allocated from the format "
|
||||
"the default framebuffer itself reported was rejected - the report and the storage disagree";
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
||||
unsigned char color[4] = {0, 0, 0, 0};
|
||||
glReadPixels(width / 2, height / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, color);
|
||||
const float depth = ReadDepthAt(width / 2, height / 2);
|
||||
const int stencil = ReadStencilAt(width / 2, height / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
// The colour bit is the precondition, not the claim: it says this stack can blit out of
|
||||
// its default framebuffer at all, which has nothing to do with depth/stencil formats.
|
||||
// DirectVulkan on a surfaceless pbuffer cannot - the whole call, colour included, is a
|
||||
// no-op there, while the same blit works on a real surface (KHR-GLxx.framebuffer_blit
|
||||
// exercises exactly it and Magma passes 33/33 on device). Skipping keeps the
|
||||
// depth/stencil claim below falsifiable instead of drowning it in an unrelated
|
||||
// harness limitation.
|
||||
if (int(color[1]) <= 192) {
|
||||
// GTEST_SKIP() expands to a return, so the teardown below it would never run and this
|
||||
// scenario would hand the next one a foreign framebuffer plus three leaked objects -
|
||||
// and this is the path DirectVulkan takes on every headless run, not a rare one.
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteRenderbuffers(1, &colorRbo);
|
||||
glDeleteRenderbuffers(1, &depthRbo);
|
||||
gl.EndFrame();
|
||||
GTEST_SKIP() << "backend " << gl.BackendName() << " on this surface transferred no colour either (green="
|
||||
<< int(color[1])
|
||||
<< "): it cannot blit out of the default framebuffer here, so the depth/stencil half proves "
|
||||
"nothing. The GL-error assertion above still ran, and it is the format contract";
|
||||
}
|
||||
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||
<< "depth blitted out of the default framebuffer read back " << depth
|
||||
<< (std::fabs(depth - kDepthPoison) < 1e-6f ? " - the blit transferred nothing" : "");
|
||||
EXPECT_EQ(stencil, kStencilValue) << "stencil blitted out of the default framebuffer read back " << stencil;
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteRenderbuffers(1, &colorRbo);
|
||||
glDeleteRenderbuffers(1, &depthRbo);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,784 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackMatrixScenario.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
|
||||
//
|
||||
// Scenario - THE DEPTH/STENCIL READBACK MATRIX: every verb, every source kind.
|
||||
//
|
||||
// DepthStencilReadbackScenario pins the default framebuffer. This file pins the rest of
|
||||
// the surface a depth/stencil read has to cover, because the three verbs and the four
|
||||
// source kinds do NOT share a code path by accident - they share one on purpose, and a
|
||||
// change that quietly serves only one of them is exactly what these assertions catch:
|
||||
//
|
||||
// verbs glReadPixels(GL_DEPTH_COMPONENT | GL_STENCIL_INDEX | GL_DEPTH_STENCIL),
|
||||
// glGetTexImage(GL_DEPTH_STENCIL), glCopyTexImage2D followed by a read
|
||||
// source kinds depth(-stencil) TEXTURE, RENDERBUFFER (not samplable at all),
|
||||
// MULTISAMPLE renderbuffer (needs a resolve first), default framebuffer
|
||||
// formats DEPTH24_STENCIL8, DEPTH32F_STENCIL8, DEPTH_COMPONENT16/24/32F,
|
||||
// STENCIL_INDEX8
|
||||
// client types GL_FLOAT / GL_UNSIGNED_INT / GL_UNSIGNED_SHORT depth, GL_INT /
|
||||
// GL_UNSIGNED_BYTE stencil, both packed GL_DEPTH_STENCIL layouts
|
||||
//
|
||||
// On DirectGLES none of this exists natively - ES has no depth or stencil readback in
|
||||
// core - so every assertion here is really an assertion about the shader-sampling
|
||||
// emulation. The catch is that some ES drivers accept the reads anyway (Mesa does,
|
||||
// Adreno does not), which would make the emulation dead code on the very stack the
|
||||
// headless suite runs on. That is what the second ctest registration is for: the same
|
||||
// scenarios run again with MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1, which takes the
|
||||
// native spellings off the table and leaves only the path the device actually uses.
|
||||
//
|
||||
// Every destination is poisoned with a value the correct answer cannot be, so "the
|
||||
// backend wrote nothing" fails loudly instead of passing on a coincidence - a test that
|
||||
// only checked "no GL error" would pass against a readback that never touched the buffer,
|
||||
// which is precisely how this whole cluster hid for so long.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr float kDepthPoison = 0.2f;
|
||||
constexpr int kStencilPoison = 50;
|
||||
constexpr int kWidth = 64;
|
||||
constexpr int kHeight = 48;
|
||||
|
||||
// A depth-stencil pair no clear in these tests produces, packed both ways.
|
||||
constexpr unsigned int kPacked24_8Poison = 0xAAAAAA33u;
|
||||
|
||||
struct D32fS8 {
|
||||
float depth;
|
||||
unsigned int stencil;
|
||||
};
|
||||
|
||||
// Everything a source needs to be read: the framebuffer to bind, plus the objects
|
||||
// to delete afterwards.
|
||||
struct DepthSource {
|
||||
GLuint fbo = 0;
|
||||
GLuint colorTexture = 0;
|
||||
GLuint depthTexture = 0;
|
||||
GLuint depthRenderbuffer = 0;
|
||||
GLuint colorRenderbuffer = 0;
|
||||
};
|
||||
|
||||
void DestroySource(DepthSource& source) {
|
||||
if (source.fbo != 0) glDeleteFramebuffers(1, &source.fbo);
|
||||
if (source.colorTexture != 0) glDeleteTextures(1, &source.colorTexture);
|
||||
if (source.depthTexture != 0) glDeleteTextures(1, &source.depthTexture);
|
||||
if (source.depthRenderbuffer != 0) glDeleteRenderbuffers(1, &source.depthRenderbuffer);
|
||||
if (source.colorRenderbuffer != 0) glDeleteRenderbuffers(1, &source.colorRenderbuffer);
|
||||
source = DepthSource{};
|
||||
}
|
||||
|
||||
GLenum AttachmentPointFor(GLenum internalFormat) {
|
||||
switch (internalFormat) {
|
||||
case GL_DEPTH24_STENCIL8:
|
||||
case GL_DEPTH32F_STENCIL8: return GL_DEPTH_STENCIL_ATTACHMENT;
|
||||
case GL_STENCIL_INDEX8: return GL_STENCIL_ATTACHMENT;
|
||||
default: return GL_DEPTH_ATTACHMENT;
|
||||
}
|
||||
}
|
||||
|
||||
bool FormatHasDepth(GLenum internalFormat) { return internalFormat != GL_STENCIL_INDEX8; }
|
||||
bool FormatHasStencil(GLenum internalFormat) {
|
||||
return internalFormat == GL_DEPTH24_STENCIL8 || internalFormat == GL_DEPTH32F_STENCIL8 ||
|
||||
internalFormat == GL_STENCIL_INDEX8;
|
||||
}
|
||||
|
||||
// A framebuffer whose depth/stencil lives in a TEXTURE. The colour attachment is
|
||||
// there so a stencil-only or depth-only framebuffer still has something to size it.
|
||||
DepthSource MakeTextureSource(GLenum internalFormat) {
|
||||
DepthSource source;
|
||||
glGenFramebuffers(1, &source.fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, source.fbo);
|
||||
glGenTextures(1, &source.colorTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, source.colorTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kWidth, kHeight);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, source.colorTexture, 0);
|
||||
glGenTextures(1, &source.depthTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, source.depthTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kWidth, kHeight);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, AttachmentPointFor(internalFormat), GL_TEXTURE_2D,
|
||||
source.depthTexture, 0);
|
||||
return source;
|
||||
}
|
||||
|
||||
// The same, with the depth/stencil in a RENDERBUFFER - which cannot be sampled at
|
||||
// all, so the readback has no choice but to copy it somewhere samplable first.
|
||||
// `samples` > 0 makes it multisample, which additionally needs a resolve.
|
||||
DepthSource MakeRenderbufferSource(GLenum internalFormat, int samples) {
|
||||
DepthSource source;
|
||||
glGenFramebuffers(1, &source.fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, source.fbo);
|
||||
glGenRenderbuffers(1, &source.colorRenderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, source.colorRenderbuffer);
|
||||
if (samples > 0) {
|
||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, kWidth, kHeight);
|
||||
} else {
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kWidth, kHeight);
|
||||
}
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, source.colorRenderbuffer);
|
||||
glGenRenderbuffers(1, &source.depthRenderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, source.depthRenderbuffer);
|
||||
if (samples > 0) {
|
||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, internalFormat, kWidth, kHeight);
|
||||
} else {
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, kWidth, kHeight);
|
||||
}
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, AttachmentPointFor(internalFormat), GL_RENDERBUFFER,
|
||||
source.depthRenderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
return source;
|
||||
}
|
||||
|
||||
// Clears the bound framebuffer's depth and stencil to known values, with the masks
|
||||
// and the scissor explicitly out of the way (a leaked scissor from an earlier
|
||||
// scenario would clip the clear and every assertion after it).
|
||||
void ClearDepthStencil(GLenum internalFormat, float depth, int stencil) {
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
GLbitfield mask = 0;
|
||||
if (FormatHasDepth(internalFormat)) {
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(depth);
|
||||
mask |= GL_DEPTH_BUFFER_BIT;
|
||||
}
|
||||
if (FormatHasStencil(internalFormat)) {
|
||||
glStencilMask(0xFFu);
|
||||
glClearStencil(stencil);
|
||||
mask |= GL_STENCIL_BUFFER_BIT;
|
||||
}
|
||||
glClear(mask);
|
||||
}
|
||||
|
||||
class DepthStencilReadbackMatrixScenario : public ScenarioTest {
|
||||
protected:
|
||||
// Not every ES driver can render to every depth format (DEPTH_COMPONENT32F and
|
||||
// the multisample counts in particular), and an incomplete framebuffer would
|
||||
// turn a legitimate "this machine cannot host the source" into a spurious
|
||||
// failure about the readback.
|
||||
static bool SourceIsUsable() {
|
||||
return glCheckFramebufferStatus(GL_FRAMEBUFFER) == GLenum(GL_FRAMEBUFFER_COMPLETE);
|
||||
}
|
||||
|
||||
static std::vector<float> ReadDepthFloat(int x, int y, int width, int height) {
|
||||
std::vector<float> depth(static_cast<size_t>(width) * height, kDepthPoison);
|
||||
glReadPixels(x, y, width, height, GL_DEPTH_COMPONENT, GL_FLOAT, depth.data());
|
||||
return depth;
|
||||
}
|
||||
|
||||
static std::vector<int> ReadStencilInt(int x, int y, int width, int height) {
|
||||
std::vector<int> stencil(static_cast<size_t>(width) * height, kStencilPoison);
|
||||
glReadPixels(x, y, width, height, GL_STENCIL_INDEX, GL_INT, stencil.data());
|
||||
return stencil;
|
||||
}
|
||||
|
||||
// "every value in the region is `expected`" rather than "the middle pixel is":
|
||||
// a staging blit that lands the wrong rectangle, or a conversion pass with a
|
||||
// half-texel offset, still gets the centre right.
|
||||
static void ExpectAllDepth(const std::vector<float>& values, float expected, const char* what) {
|
||||
size_t bad = 0;
|
||||
float worst = expected;
|
||||
for (float value : values) {
|
||||
if (std::fabs(value - expected) > 1.0f / 4096.0f) {
|
||||
if (bad == 0) worst = value;
|
||||
++bad;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(bad, 0u) << what << ": " << bad << " of " << values.size()
|
||||
<< " depth values differ from " << expected << "; first bad value " << worst
|
||||
<< (std::fabs(worst - kDepthPoison) < 1e-6f
|
||||
? " - which is the poison value, so nothing was written at all"
|
||||
: "");
|
||||
}
|
||||
|
||||
static void ExpectAllStencil(const std::vector<int>& values, int expected, const char* what) {
|
||||
size_t bad = 0;
|
||||
int worst = expected;
|
||||
for (int value : values) {
|
||||
if (value != expected) {
|
||||
if (bad == 0) worst = value;
|
||||
++bad;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(bad, 0u) << what << ": " << bad << " of " << values.size()
|
||||
<< " stencil values differ from " << expected << "; first bad value " << worst
|
||||
<< (worst == kStencilPoison
|
||||
? " - which is the poison value, so nothing was written at all"
|
||||
: "");
|
||||
}
|
||||
};
|
||||
|
||||
// ---- glReadPixels across the source kinds -----------------------------------
|
||||
|
||||
struct SourceCase {
|
||||
const char* name;
|
||||
GLenum internalFormat;
|
||||
int samples;
|
||||
bool renderbuffer;
|
||||
};
|
||||
|
||||
const SourceCase kSourceCases[] = {
|
||||
{"texture depth24_stencil8", GL_DEPTH24_STENCIL8, 0, false},
|
||||
{"texture depth32f_stencil8", GL_DEPTH32F_STENCIL8, 0, false},
|
||||
{"texture depth_component16", GL_DEPTH_COMPONENT16, 0, false},
|
||||
{"texture depth_component24", GL_DEPTH_COMPONENT24, 0, false},
|
||||
{"texture depth_component32f", GL_DEPTH_COMPONENT32F, 0, false},
|
||||
{"renderbuffer depth24_stencil8", GL_DEPTH24_STENCIL8, 0, true},
|
||||
{"renderbuffer depth_component24", GL_DEPTH_COMPONENT24, 0, true},
|
||||
{"renderbuffer stencil_index8", GL_STENCIL_INDEX8, 0, true},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, EverySourceKindReadsItsClearBack) {
|
||||
if (!Ready()) return;
|
||||
int exercised = 0;
|
||||
for (const SourceCase& testCase : kSourceCases) {
|
||||
SCOPED_TRACE(testCase.name);
|
||||
DepthSource source = testCase.renderbuffer
|
||||
? MakeRenderbufferSource(testCase.internalFormat, testCase.samples)
|
||||
: MakeTextureSource(testCase.internalFormat);
|
||||
if (!SourceIsUsable()) {
|
||||
DestroySource(source);
|
||||
continue;
|
||||
}
|
||||
FirstGLError(); // the storage calls above may have probed an unsupported combination
|
||||
ClearDepthStencil(testCase.internalFormat, 0.625f, 9);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "clearing the source";
|
||||
|
||||
if (FormatHasDepth(testCase.internalFormat)) {
|
||||
const std::vector<float> depth = ReadDepthFloat(0, 0, kWidth, kHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_DEPTH_COMPONENT, GL_FLOAT)";
|
||||
ExpectAllDepth(depth, 0.625f, testCase.name);
|
||||
}
|
||||
if (FormatHasStencil(testCase.internalFormat)) {
|
||||
const std::vector<int> stencil = ReadStencilInt(0, 0, kWidth, kHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_STENCIL_INDEX, GL_INT)";
|
||||
ExpectAllStencil(stencil, 9, testCase.name);
|
||||
}
|
||||
++exercised;
|
||||
DestroySource(source);
|
||||
}
|
||||
// A machine that hosted none of the sources would report a vacuous pass.
|
||||
EXPECT_GE(exercised, 4) << "too few depth/stencil source kinds were usable to call this a matrix";
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// Depth and stencil in two SEPARATE objects, with two different formats, on the same
|
||||
// framebuffer. Legal GL, and the shape KHR-GL3x.framebuffer_blit builds when its depth
|
||||
// config and its stencil config are configured independently - so a readback that
|
||||
// describes "the" depth/stencil source as one thing serves whichever aspect it happened
|
||||
// to find first and silently abandons the other. Each aspect has to be staged from its
|
||||
// own attachment, in its own format.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, SeparateDepthAndStencilAttachmentsAreBothReadable) {
|
||||
if (!Ready()) return;
|
||||
DepthSource source;
|
||||
glGenFramebuffers(1, &source.fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, source.fbo);
|
||||
glGenRenderbuffers(1, &source.colorRenderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, source.colorRenderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kWidth, kHeight);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, source.colorRenderbuffer);
|
||||
// Depth in a DEPTH_COMPONENT24 renderbuffer...
|
||||
glGenRenderbuffers(1, &source.depthRenderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, source.depthRenderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, kWidth, kHeight);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, source.depthRenderbuffer);
|
||||
// ...and stencil in a STENCIL_INDEX8 one of its own.
|
||||
GLuint stencilRenderbuffer = 0;
|
||||
glGenRenderbuffers(1, &stencilRenderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, stencilRenderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, kWidth, kHeight);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, stencilRenderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
if (!SourceIsUsable()) {
|
||||
// Separate depth and stencil images are legal GL but many stacks answer
|
||||
// GL_FRAMEBUFFER_UNSUPPORTED for them; say which, so a skip here is a fact about
|
||||
// the driver rather than an unexplained hole in the matrix.
|
||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
glDeleteRenderbuffers(1, &stencilRenderbuffer);
|
||||
DestroySource(source);
|
||||
GTEST_SKIP() << "this driver cannot host separate DEPTH_COMPONENT24 and STENCIL_INDEX8 attachments: "
|
||||
<< "glCheckFramebufferStatus = 0x" << std::hex << status;
|
||||
}
|
||||
FirstGLError();
|
||||
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDepthMask(GL_TRUE);
|
||||
glStencilMask(0xFFu);
|
||||
glClearDepth(0.3125);
|
||||
glClearStencil(17);
|
||||
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const std::vector<float> depth = ReadDepthFloat(0, 0, kWidth, kHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "reading depth from a separately-attached DEPTH_COMPONENT24";
|
||||
ExpectAllDepth(depth, 0.3125f, "separate depth attachment");
|
||||
|
||||
const std::vector<int> stencil = ReadStencilInt(0, 0, kWidth, kHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "reading stencil from a separately-attached STENCIL_INDEX8";
|
||||
ExpectAllStencil(stencil, 17, "separate stencil attachment");
|
||||
|
||||
glDeleteRenderbuffers(1, &stencilRenderbuffer);
|
||||
DestroySource(source);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// A multisample source is never read directly - glReadPixels on a multisampled
|
||||
// framebuffer is INVALID_OPERATION in GL as much as in ES, and the state layer says so.
|
||||
// The way multisample depth reaches a reader is a resolve blit into a single-sampled
|
||||
// framebuffer, which is then read; that pair is
|
||||
// KHR-GL3x.framebuffer_blit.multisampled_to_singlesampled_blit_depth_config_test, and
|
||||
// the assertion here is that the resolved depth arrives intact rather than as the
|
||||
// destination's own clear value.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, AResolvedMultisampleDepthReadsBackFromTheDestination) {
|
||||
if (!Ready()) return;
|
||||
DepthSource multisampled = MakeRenderbufferSource(GL_DEPTH24_STENCIL8, 4);
|
||||
if (!SourceIsUsable()) {
|
||||
DestroySource(multisampled);
|
||||
GTEST_SKIP() << "this driver cannot host a 4x multisample DEPTH24_STENCIL8 renderbuffer";
|
||||
}
|
||||
FirstGLError();
|
||||
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.875f, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// The destination starts at a depth the resolve must overwrite everywhere.
|
||||
DepthSource resolved = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||
ASSERT_TRUE(SourceIsUsable());
|
||||
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.125f, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampled.fbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolved.fbo);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glBlitFramebuffer(0, 0, kWidth, kHeight, 0, 0, kWidth, kHeight, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "resolving a multisample depth buffer into a single-sampled one";
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, resolved.fbo);
|
||||
const std::vector<float> depth = ReadDepthFloat(0, 0, kWidth, kHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectAllDepth(depth, 0.875f, "resolved multisample depth");
|
||||
|
||||
DestroySource(resolved);
|
||||
DestroySource(multisampled);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// A read whose rectangle is NOT the whole attachment. The staging copy has to carry
|
||||
// the requested rect (not the origin) and hand back its rows bottom-up, which a
|
||||
// full-extent uniform read is a fixed point of and therefore cannot see.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, ASubRectangleReadsTheRightBandInTheRightOrder) {
|
||||
if (!Ready()) return;
|
||||
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||
ASSERT_TRUE(SourceIsUsable());
|
||||
|
||||
// Bottom half 0.25, top half 0.75, and the stencil banded the other way round so a
|
||||
// mix-up between the two aspects cannot pass either.
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDepthMask(GL_TRUE);
|
||||
glStencilMask(0xFFu);
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(0, 0, kWidth, kHeight / 2);
|
||||
glClearDepth(0.25);
|
||||
glClearStencil(11);
|
||||
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
glScissor(0, kHeight / 2, kWidth, kHeight - kHeight / 2);
|
||||
glClearDepth(0.75);
|
||||
glClearStencil(22);
|
||||
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// A rect wholly inside the bottom band, offset from the origin in both axes.
|
||||
const int rectWidth = 8;
|
||||
const int rectHeight = 4;
|
||||
const std::vector<float> bottom = ReadDepthFloat(16, 4, rectWidth, rectHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectAllDepth(bottom, 0.25f, "sub-rect inside the bottom depth band");
|
||||
const std::vector<int> bottomStencil = ReadStencilInt(16, 4, rectWidth, rectHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectAllStencil(bottomStencil, 11, "sub-rect inside the bottom stencil band");
|
||||
|
||||
// And one wholly inside the top band. Reading the mirrored row would answer 0.25.
|
||||
const std::vector<float> top = ReadDepthFloat(16, kHeight - 4 - rectHeight, rectWidth, rectHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectAllDepth(top, 0.75f, "sub-rect inside the top depth band");
|
||||
|
||||
// A rect that STRADDLES the boundary pins the row order itself: its first rows must
|
||||
// be the bottom band and its last rows the top one.
|
||||
const int straddleHeight = 8;
|
||||
const std::vector<float> straddle =
|
||||
ReadDepthFloat(16, kHeight / 2 - straddleHeight / 2, rectWidth, straddleHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_EQ(straddle.size(), static_cast<size_t>(rectWidth) * straddleHeight);
|
||||
EXPECT_NEAR(straddle[0], 0.25f, 1.0f / 4096.0f)
|
||||
<< "the first row of the returned rect must be its BOTTOM row (GL order), which is in the 0.25 band";
|
||||
EXPECT_NEAR(straddle[straddle.size() - 1], 0.75f, 1.0f / 4096.0f)
|
||||
<< "the last row of the returned rect must be its TOP row, which is in the 0.75 band";
|
||||
|
||||
DestroySource(source);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The packed layouts the packed_depth_stencil family reads its gradients with.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, PackedDepthStencilReadPixelsCarriesBothAspects) {
|
||||
if (!Ready()) return;
|
||||
struct PackedCase {
|
||||
const char* name;
|
||||
GLenum internalFormat;
|
||||
GLenum type;
|
||||
};
|
||||
const PackedCase cases[] = {
|
||||
{"depth24_stencil8 / GL_UNSIGNED_INT_24_8", GL_DEPTH24_STENCIL8, GL_UNSIGNED_INT_24_8},
|
||||
{"depth32f_stencil8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV", GL_DEPTH32F_STENCIL8,
|
||||
GL_FLOAT_32_UNSIGNED_INT_24_8_REV},
|
||||
};
|
||||
int exercised = 0;
|
||||
for (const PackedCase& testCase : cases) {
|
||||
SCOPED_TRACE(testCase.name);
|
||||
DepthSource source = MakeTextureSource(testCase.internalFormat);
|
||||
if (!SourceIsUsable()) {
|
||||
DestroySource(source);
|
||||
continue;
|
||||
}
|
||||
FirstGLError();
|
||||
ClearDepthStencil(testCase.internalFormat, 0.5f, 3);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const size_t pixels = static_cast<size_t>(kWidth) * kHeight;
|
||||
if (testCase.type == GL_UNSIGNED_INT_24_8) {
|
||||
std::vector<unsigned int> packed(pixels, kPacked24_8Poison);
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_STENCIL, testCase.type, packed.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
size_t bad = 0;
|
||||
for (unsigned int value : packed) {
|
||||
const float depth = static_cast<float>(value >> 8) / 16777215.0f;
|
||||
const int stencil = static_cast<int>(value & 0xFFu);
|
||||
if (std::fabs(depth - 0.5f) > 0.01f || stencil != 3) ++bad;
|
||||
}
|
||||
EXPECT_EQ(bad, 0u) << testCase.name << ": " << bad << " of " << pixels
|
||||
<< " packed words carry the wrong depth or stencil (first word 0x" << std::hex
|
||||
<< packed[0] << std::dec << ")";
|
||||
} else {
|
||||
std::vector<D32fS8> packed(pixels, D32fS8{kDepthPoison, static_cast<unsigned int>(kStencilPoison)});
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_STENCIL, testCase.type, packed.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
size_t bad = 0;
|
||||
for (const D32fS8& value : packed) {
|
||||
if (std::fabs(value.depth - 0.5f) > 0.01f || (value.stencil & 0xFFu) != 3u) ++bad;
|
||||
}
|
||||
EXPECT_EQ(bad, 0u) << testCase.name << ": " << bad << " of " << pixels
|
||||
<< " packed pairs carry the wrong depth or stencil (first pair depth "
|
||||
<< packed[0].depth << " stencil " << (packed[0].stencil & 0xFFu) << ")";
|
||||
}
|
||||
++exercised;
|
||||
DestroySource(source);
|
||||
}
|
||||
EXPECT_GE(exercised, 1) << "neither packed depth/stencil format was renderable";
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// glGetTexImage reads a TEXTURE, not the bound framebuffer - a different entry point
|
||||
// that has to reach the same machinery. This is verify_get_tex_image's shape.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, GetTexImageReadsAPackedDepthStencilTexture) {
|
||||
if (!Ready()) return;
|
||||
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||
ASSERT_TRUE(SourceIsUsable());
|
||||
FirstGLError();
|
||||
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.375f, 5);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// Read it back through the texture, with the framebuffer that owns it unbound so a
|
||||
// path that secretly read the framebuffer instead would answer from somewhere else.
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, source.depthTexture);
|
||||
const size_t pixels = static_cast<size_t>(kWidth) * kHeight;
|
||||
std::vector<unsigned int> packed(pixels, kPacked24_8Poison);
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, packed.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
size_t bad = 0;
|
||||
for (unsigned int value : packed) {
|
||||
const float depth = static_cast<float>(value >> 8) / 16777215.0f;
|
||||
if (std::fabs(depth - 0.375f) > 0.01f || (value & 0xFFu) != 5u) ++bad;
|
||||
}
|
||||
EXPECT_EQ(bad, 0u) << bad << " of " << pixels
|
||||
<< " words from glGetTexImage(GL_DEPTH_STENCIL) are wrong (first word 0x" << std::hex
|
||||
<< packed[0] << std::dec << ")";
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
DestroySource(source);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// glCopyTexImage2D out of a depth attachment, then read the copy - verify_copy_tex_image.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, CopyTexImageFromADepthAttachmentSurvivesAReadBack) {
|
||||
if (!Ready()) return;
|
||||
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||
ASSERT_TRUE(SourceIsUsable());
|
||||
FirstGLError();
|
||||
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.75f, 6);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
GLuint copy = 0;
|
||||
glGenTextures(1, ©);
|
||||
glBindTexture(GL_TEXTURE_2D, copy);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, kWidth, kHeight, 0, GL_DEPTH_STENCIL,
|
||||
GL_UNSIGNED_INT_24_8, nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 0, 0, kWidth, kHeight, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glCopyTexImage2D from a depth/stencil attachment";
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
const size_t pixels = static_cast<size_t>(kWidth) * kHeight;
|
||||
std::vector<unsigned int> packed(pixels, kPacked24_8Poison);
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, packed.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
size_t bad = 0;
|
||||
for (unsigned int value : packed) {
|
||||
const float depth = static_cast<float>(value >> 8) / 16777215.0f;
|
||||
if (std::fabs(depth - 0.75f) > 0.01f) ++bad;
|
||||
}
|
||||
EXPECT_EQ(bad, 0u) << bad << " of " << pixels << " copied depth values are wrong (first word 0x" << std::hex
|
||||
<< packed[0] << std::dec << ")";
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glDeleteTextures(1, ©);
|
||||
DestroySource(source);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The integer client widths, which are a separate conversion each.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, DepthAndStencilConvertIntoEveryClientWidth) {
|
||||
if (!Ready()) return;
|
||||
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||
ASSERT_TRUE(SourceIsUsable());
|
||||
FirstGLError();
|
||||
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.5f, 200);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const size_t pixels = static_cast<size_t>(kWidth) * kHeight;
|
||||
|
||||
std::vector<unsigned int> depthUint(pixels, 0xDEADBEEFu);
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, depthUint.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_DEPTH_COMPONENT, GL_UNSIGNED_INT)";
|
||||
// 0.5 of the full 32-bit range, with room for the source's 24-bit quantisation.
|
||||
EXPECT_NEAR(static_cast<double>(depthUint[0]) / 4294967295.0, 0.5, 0.01)
|
||||
<< "GL_UNSIGNED_INT depth came back as " << depthUint[0];
|
||||
|
||||
std::vector<unsigned short> depthUshort(pixels, 0xBEEFu);
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, depthUshort.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT)";
|
||||
EXPECT_NEAR(static_cast<double>(depthUshort[0]) / 65535.0, 0.5, 0.01)
|
||||
<< "GL_UNSIGNED_SHORT depth came back as " << depthUshort[0];
|
||||
|
||||
// A stencil index is written unconverted into whichever width was asked for, so 200
|
||||
// must survive intact in all of them - it is also large enough that a signed byte
|
||||
// would wrap, which is the point of choosing it.
|
||||
std::vector<unsigned char> stencilByte(pixels, static_cast<unsigned char>(kStencilPoison));
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, stencilByte.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_STENCIL_INDEX, GL_UNSIGNED_BYTE)";
|
||||
EXPECT_EQ(static_cast<int>(stencilByte[0]), 200);
|
||||
|
||||
std::vector<int> stencilInt(pixels, kStencilPoison);
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_STENCIL_INDEX, GL_INT, stencilInt.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_STENCIL_INDEX, GL_INT)";
|
||||
EXPECT_EQ(stencilInt[0], 200);
|
||||
|
||||
DestroySource(source);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The PACK pixel-store parameters apply to a depth read exactly as they do to a colour
|
||||
// one, and the gap regions they create must be left alone.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, DepthReadbackHonoursThePackPixelStoreParameters) {
|
||||
if (!Ready()) return;
|
||||
DepthSource source = MakeTextureSource(GL_DEPTH_COMPONENT24);
|
||||
ASSERT_TRUE(SourceIsUsable());
|
||||
FirstGLError();
|
||||
ClearDepthStencil(GL_DEPTH_COMPONENT24, 0.5f, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const int rectWidth = 4;
|
||||
const int rectHeight = 3;
|
||||
const int rowLength = 8;
|
||||
const int skipPixels = 2;
|
||||
const int skipRows = 1;
|
||||
constexpr float kGap = -7.0f;
|
||||
std::vector<float> destination(static_cast<size_t>(rowLength) * (skipRows + rectHeight) + 16, kGap);
|
||||
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, rowLength);
|
||||
glPixelStorei(GL_PACK_SKIP_PIXELS, skipPixels);
|
||||
glPixelStorei(GL_PACK_SKIP_ROWS, skipRows);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
glReadPixels(0, 0, rectWidth, rectHeight, GL_DEPTH_COMPONENT, GL_FLOAT, destination.data());
|
||||
const unsigned int readError = FirstGLError();
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
|
||||
glPixelStorei(GL_PACK_SKIP_ROWS, 0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
EXPECT_EQ(readError, 0u);
|
||||
|
||||
size_t written = 0;
|
||||
size_t gapsTouched = 0;
|
||||
for (size_t index = 0; index < destination.size(); ++index) {
|
||||
const long row = static_cast<long>(index) / rowLength - skipRows;
|
||||
const long column = static_cast<long>(index) % rowLength - skipPixels;
|
||||
const bool inRect = row >= 0 && row < rectHeight && column >= 0 && column < rectWidth;
|
||||
if (inRect) {
|
||||
if (std::fabs(destination[index] - 0.5f) <= 1.0f / 4096.0f) ++written;
|
||||
} else if (destination[index] != kGap) {
|
||||
++gapsTouched;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(written, static_cast<size_t>(rectWidth) * rectHeight)
|
||||
<< "only " << written << " of " << (rectWidth * rectHeight)
|
||||
<< " destination pixels landed where GL_PACK_ROW_LENGTH/SKIP_* put them";
|
||||
EXPECT_EQ(gapsTouched, 0u) << gapsTouched << " bytes outside the packed rectangle were overwritten";
|
||||
|
||||
DestroySource(source);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The readback borrows the application's context for a full-screen pass. Everything it
|
||||
// touches has to come back, or the next draw inherits it - which is how an emulation
|
||||
// that "works" takes the rest of the renderer down with it.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, ReadbackLeavesNoGLStateBehind) {
|
||||
if (!Ready()) return;
|
||||
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||
ASSERT_TRUE(SourceIsUsable());
|
||||
FirstGLError();
|
||||
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.5f, 4);
|
||||
|
||||
// A deliberately awkward state: nothing here is what an emulation pass would want,
|
||||
// so anything it forgets to put back shows up below.
|
||||
GLuint scratchTexture = 0;
|
||||
glGenTextures(1, &scratchTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, scratchTexture);
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
glBindTexture(GL_TEXTURE_2D, scratchTexture);
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(3, 5, 7, 11);
|
||||
glEnable(GL_CULL_FACE);
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_GEQUAL);
|
||||
glDepthMask(GL_FALSE);
|
||||
glEnable(GL_STENCIL_TEST);
|
||||
glStencilFunc(GL_NOTEQUAL, 0x5, 0x0Fu);
|
||||
glStencilOp(GL_INCR, GL_DECR, GL_INVERT);
|
||||
glStencilMask(0x3Cu);
|
||||
glColorMask(GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE);
|
||||
glViewport(2, 3, 5, 7);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const std::vector<float> depth = ReadDepthFloat(0, 0, kWidth, kHeight);
|
||||
const std::vector<int> stencil = ReadStencilInt(0, 0, kWidth, kHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectAllDepth(depth, 0.5f, "state-preservation case depth");
|
||||
ExpectAllStencil(stencil, 4, "state-preservation case stencil");
|
||||
|
||||
GLint viewport[4] = {0, 0, 0, 0};
|
||||
GLint scissorBox[4] = {0, 0, 0, 0};
|
||||
GLboolean colorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE};
|
||||
GLint depthFunc = 0;
|
||||
GLboolean depthMask = GL_TRUE;
|
||||
GLint stencilFunc = 0, stencilRef = 0, stencilValueMask = 0, stencilWriteMask = 0;
|
||||
GLint stencilFail = 0, stencilPassDepthFail = 0, stencilPassDepthPass = 0;
|
||||
GLint activeTexture = 0, boundTexture = 0;
|
||||
glGetIntegerv(GL_VIEWPORT, viewport);
|
||||
glGetIntegerv(GL_SCISSOR_BOX, scissorBox);
|
||||
glGetBooleanv(GL_COLOR_WRITEMASK, colorMask);
|
||||
glGetIntegerv(GL_DEPTH_FUNC, &depthFunc);
|
||||
glGetBooleanv(GL_DEPTH_WRITEMASK, &depthMask);
|
||||
glGetIntegerv(GL_STENCIL_FUNC, &stencilFunc);
|
||||
glGetIntegerv(GL_STENCIL_REF, &stencilRef);
|
||||
glGetIntegerv(GL_STENCIL_VALUE_MASK, &stencilValueMask);
|
||||
glGetIntegerv(GL_STENCIL_WRITEMASK, &stencilWriteMask);
|
||||
glGetIntegerv(GL_STENCIL_FAIL, &stencilFail);
|
||||
glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &stencilPassDepthFail);
|
||||
glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &stencilPassDepthPass);
|
||||
glGetIntegerv(GL_ACTIVE_TEXTURE, &activeTexture);
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTexture);
|
||||
|
||||
EXPECT_EQ(viewport[0], 2);
|
||||
EXPECT_EQ(viewport[1], 3);
|
||||
EXPECT_EQ(viewport[2], 5);
|
||||
EXPECT_EQ(viewport[3], 7);
|
||||
EXPECT_EQ(scissorBox[0], 3);
|
||||
EXPECT_EQ(scissorBox[1], 5);
|
||||
EXPECT_EQ(scissorBox[2], 7);
|
||||
EXPECT_EQ(scissorBox[3], 11);
|
||||
EXPECT_EQ(glIsEnabled(GL_SCISSOR_TEST), GLboolean(GL_TRUE));
|
||||
EXPECT_EQ(glIsEnabled(GL_CULL_FACE), GLboolean(GL_TRUE));
|
||||
EXPECT_EQ(glIsEnabled(GL_BLEND), GLboolean(GL_TRUE));
|
||||
EXPECT_EQ(glIsEnabled(GL_DEPTH_TEST), GLboolean(GL_TRUE));
|
||||
EXPECT_EQ(glIsEnabled(GL_STENCIL_TEST), GLboolean(GL_TRUE));
|
||||
EXPECT_EQ(colorMask[0], GLboolean(GL_FALSE));
|
||||
EXPECT_EQ(colorMask[1], GLboolean(GL_TRUE));
|
||||
EXPECT_EQ(colorMask[2], GLboolean(GL_FALSE));
|
||||
EXPECT_EQ(colorMask[3], GLboolean(GL_TRUE));
|
||||
EXPECT_EQ(depthFunc, GLint(GL_GEQUAL));
|
||||
EXPECT_EQ(depthMask, GLboolean(GL_FALSE));
|
||||
EXPECT_EQ(stencilFunc, GLint(GL_NOTEQUAL));
|
||||
EXPECT_EQ(stencilRef, 0x5);
|
||||
EXPECT_EQ(stencilValueMask, 0x0F);
|
||||
EXPECT_EQ(stencilWriteMask, 0x3C);
|
||||
EXPECT_EQ(stencilFail, GLint(GL_INCR));
|
||||
EXPECT_EQ(stencilPassDepthFail, GLint(GL_DECR));
|
||||
EXPECT_EQ(stencilPassDepthPass, GLint(GL_INVERT));
|
||||
EXPECT_EQ(activeTexture, GLint(GL_TEXTURE3));
|
||||
EXPECT_EQ(boundTexture, GLint(scratchTexture))
|
||||
<< "the readback left a scratch texture on the application's texture unit";
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// Put the awkward state back so the next scenario in this process starts clean.
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glDepthFunc(GL_LESS);
|
||||
glDepthMask(GL_TRUE);
|
||||
glStencilFunc(GL_ALWAYS, 0, 0xFFFFFFFFu);
|
||||
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
|
||||
glStencilMask(0xFFFFFFFFu);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glDeleteTextures(1, &scratchTexture);
|
||||
DestroySource(source);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -58,12 +58,12 @@ namespace MGITest {
|
||||
|
||||
class DepthStencilReadbackScenario : public ScenarioTest {
|
||||
protected:
|
||||
// DirectGLES reads depth and stencil back through the ES driver, which has no
|
||||
// guaranteed path for either (GL_NV_read_depth / GL_NV_read_stencil are optional and
|
||||
// absent on both the Adreno device and Mesa's ES). That gap is tracked separately as
|
||||
// the packed_depth_stencil cluster and needs a shader-sampling emulation, not this
|
||||
// change; asserting it here would only pin a known-missing feature.
|
||||
bool BackendReadsDepthStencil() const { return Gl().BackendName() == "DirectVulkan"; }
|
||||
// Both backends now answer these reads. DirectGLES has no native ES path for
|
||||
// either aspect (GL_NV_read_depth / GL_NV_read_stencil are optional and absent on
|
||||
// both the Adreno device and Mesa's ES), so it stages the attachment into a
|
||||
// scratch depth texture and samples it into a colour target; the assertions below
|
||||
// are the same either way, which is the point.
|
||||
bool BackendReadsDepthStencil() const { return true; }
|
||||
|
||||
float ReadDepthAt(int x, int y) const {
|
||||
float depth = kDepthPoison;
|
||||
|
||||
@@ -99,6 +99,8 @@ void main() {
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
if (m_shapeOutput != 0) glDeleteBuffers(1, &m_shapeOutput);
|
||||
if (m_shapeProgram != 0) glDeleteProgram(m_shapeProgram);
|
||||
if (m_output != 0) glDeleteBuffers(1, &m_output);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
}
|
||||
@@ -146,9 +148,147 @@ void main() {
|
||||
|
||||
unsigned int m_program = 0;
|
||||
unsigned int m_output = 0;
|
||||
unsigned int m_shapeProgram = 0;
|
||||
unsigned int m_shapeOutput = 0;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
// Every double-typed uniform shape GLSL has, all thirteen of them, in one program - the
|
||||
// shape of KHR-GL43.compute_shader.fp64-case2. The scalar and the square matrices are
|
||||
// covered by the cases above; what only a set like this reaches is the NON-SQUARE
|
||||
// matrices, whose column stride and total size both change when the demotion turns a
|
||||
// 64-bit column into a 32-bit one, and whose members therefore move every uniform
|
||||
// declared after them.
|
||||
//
|
||||
// The shader reports every component separately rather than one pass/fail flag, because
|
||||
// "the readback is wrong" is not a diagnosis: a wrong column stride, a wrong member
|
||||
// offset and a wrong narrowing all fail the same single comparison, and only the
|
||||
// component map says which.
|
||||
// No #version here on purpose: it is handed over as a separate source string, the way
|
||||
// the CTS case hands it over.
|
||||
constexpr const char* kAllDoubleShapesSource = R"(
|
||||
layout(local_size_x = 1) in;
|
||||
uniform double g_0;
|
||||
uniform dvec2 g_1;
|
||||
uniform dvec3 g_2;
|
||||
uniform dvec4 g_3;
|
||||
uniform dmat2 g_4;
|
||||
uniform dmat2x3 g_5;
|
||||
uniform dmat2x4 g_6;
|
||||
uniform dmat3x2 g_7;
|
||||
uniform dmat3 g_8;
|
||||
uniform dmat3x4 g_9;
|
||||
uniform dmat4x2 g_10;
|
||||
uniform dmat4x3 g_11;
|
||||
uniform dmat4 g_12;
|
||||
layout(std430, binding = 0) buffer Output {
|
||||
float g_out[];
|
||||
};
|
||||
void main() {
|
||||
g_out[0] = float(g_0);
|
||||
for (int i = 0; i < 2; ++i) g_out[1 + i] = float(g_1[i]);
|
||||
for (int i = 0; i < 3; ++i) g_out[3 + i] = float(g_2[i]);
|
||||
for (int i = 0; i < 4; ++i) g_out[6 + i] = float(g_3[i]);
|
||||
for (int c = 0; c < 2; ++c) for (int r = 0; r < 2; ++r) g_out[10 + c * 2 + r] = float(g_4[c][r]);
|
||||
for (int c = 0; c < 2; ++c) for (int r = 0; r < 3; ++r) g_out[14 + c * 3 + r] = float(g_5[c][r]);
|
||||
for (int c = 0; c < 2; ++c) for (int r = 0; r < 4; ++r) g_out[20 + c * 4 + r] = float(g_6[c][r]);
|
||||
for (int c = 0; c < 3; ++c) for (int r = 0; r < 2; ++r) g_out[28 + c * 2 + r] = float(g_7[c][r]);
|
||||
for (int c = 0; c < 3; ++c) for (int r = 0; r < 3; ++r) g_out[34 + c * 3 + r] = float(g_8[c][r]);
|
||||
for (int c = 0; c < 3; ++c) for (int r = 0; r < 4; ++r) g_out[43 + c * 4 + r] = float(g_9[c][r]);
|
||||
for (int c = 0; c < 4; ++c) for (int r = 0; r < 2; ++r) g_out[55 + c * 2 + r] = float(g_10[c][r]);
|
||||
for (int c = 0; c < 4; ++c) for (int r = 0; r < 3; ++r) g_out[63 + c * 3 + r] = float(g_11[c][r]);
|
||||
for (int c = 0; c < 4; ++c) for (int r = 0; r < 4; ++r) g_out[75 + c * 4 + r] = float(g_12[c][r]);
|
||||
}
|
||||
)";
|
||||
|
||||
// The values the CTS case sets, spelled the way it spells them - column-major, and small
|
||||
// enough that every one is exact in a float. Nothing here is a precision question; a
|
||||
// component that comes back wrong came back from the wrong bytes.
|
||||
constexpr double kG0 = 1.0;
|
||||
constexpr double kG1[2] = {2.0, 3.0};
|
||||
constexpr double kG2[3] = {4.0, 5.0, 6.0};
|
||||
constexpr double kG3[4] = {7.0, 8.0, 9.0, 10.0};
|
||||
constexpr double kG4[4] = {11.0, 12.0, 13.0, 14.0};
|
||||
constexpr double kG5[6] = {15.0, 16.0, 17.0, 18.0, 19.0, 20.0};
|
||||
constexpr double kG6[8] = {21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0};
|
||||
constexpr double kG7[6] = {29.0, 30.0, 31.0, 32.0, 33.0, 34.0};
|
||||
constexpr double kG8[9] = {35.0, 36.0, 37.0, 38.0, 39.0, 40.0, 41.0, 42.0, 43.0};
|
||||
constexpr double kG9[12] = {44.0, 45.0, 46.0, 47.0, 48.0, 49.0, 50.0, 51.0, 52.0, 53.0, 54.0, 55.0};
|
||||
constexpr double kG10[8] = {56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, 63.0};
|
||||
constexpr double kG11[12] = {63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0, 71.0, 27.0, 73.0, 74.0};
|
||||
constexpr double kG12[16] = {75.0, 76.0, 77.0, 78.0, 79.0, 80.0, 81.0, 82.0,
|
||||
83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, 90.0};
|
||||
|
||||
struct DoubleShape {
|
||||
const char* name;
|
||||
int base;
|
||||
int columns; // 1 for the scalar and the vectors
|
||||
int rows; // component count for the scalar and the vectors
|
||||
const double* values;
|
||||
};
|
||||
|
||||
constexpr DoubleShape kDoubleShapes[] = {
|
||||
{"g_0 double", 0, 1, 1, &kG0}, {"g_1 dvec2", 1, 1, 2, kG1},
|
||||
{"g_2 dvec3", 3, 1, 3, kG2}, {"g_3 dvec4", 6, 1, 4, kG3},
|
||||
{"g_4 dmat2", 10, 2, 2, kG4}, {"g_5 dmat2x3", 14, 2, 3, kG5},
|
||||
{"g_6 dmat2x4", 20, 2, 4, kG6}, {"g_7 dmat3x2", 28, 3, 2, kG7},
|
||||
{"g_8 dmat3", 34, 3, 3, kG8}, {"g_9 dmat3x4", 43, 3, 4, kG9},
|
||||
{"g_10 dmat4x2", 55, 4, 2, kG10}, {"g_11 dmat4x3", 63, 4, 3, kG11},
|
||||
{"g_12 dmat4", 75, 4, 4, kG12},
|
||||
};
|
||||
|
||||
constexpr int kAllShapeSlots = 91;
|
||||
|
||||
// The conformance case's own shader, kept verbatim down to the literal suffixes and the
|
||||
// unnamed, unqualified storage block - except that each comparison sets its OWN bit
|
||||
// instead of collapsing all thirteen into one flag. That single flag is the whole reason
|
||||
// the case was unexplained for a wave: it says "something is wrong" and nothing else.
|
||||
//
|
||||
// Verbatim matters here. Reading the components out one at a time (the case above)
|
||||
// passes; whatever fails does so through the shape the conformance case actually
|
||||
// writes - whole-matrix comparison against a constructor, a storage block with no
|
||||
// layout qualifier and no instance name, values reached with constant indices.
|
||||
constexpr const char* kCtsShapedSource = R"(
|
||||
layout(local_size_x = 1) in;
|
||||
buffer Result {
|
||||
int g_result;
|
||||
};
|
||||
uniform double g_0;
|
||||
uniform dvec2 g_1;
|
||||
uniform dvec3 g_2;
|
||||
uniform dvec4 g_3;
|
||||
uniform dmat2 g_4;
|
||||
uniform dmat2x3 g_5;
|
||||
uniform dmat2x4 g_6;
|
||||
uniform dmat3x2 g_7;
|
||||
uniform dmat3 g_8;
|
||||
uniform dmat3x4 g_9;
|
||||
uniform dmat4x2 g_10;
|
||||
uniform dmat4x3 g_11;
|
||||
uniform dmat4 g_12;
|
||||
|
||||
void main() {
|
||||
g_result = 0;
|
||||
|
||||
if (g_0 != 1.0LF) g_result |= 1;
|
||||
if (g_1 != dvec2(2.0LF, 3.0LF)) g_result |= 2;
|
||||
if (g_2 != dvec3(4.0LF, 5.0LF, 6.0LF)) g_result |= 4;
|
||||
if (g_3 != dvec4(7.0LF, 8.0LF, 9.0LF, 10.0LF)) g_result |= 8;
|
||||
|
||||
if (g_4 != dmat2(11.0LF, 12.0LF, 13.0LF, 14.0LF)) g_result |= 16;
|
||||
if (g_5 != dmat2x3(15.0LF, 16.0LF, 17.0LF, 18.0LF, 19.0LF, 20.0LF)) g_result |= 32;
|
||||
if (g_6 != dmat2x4(21.0LF, 22.0LF, 23.0LF, 24.0LF, 25.0LF, 26.0LF, 27.0LF, 28.0LF)) g_result |= 64;
|
||||
|
||||
if (g_7 != dmat3x2(29.0LF, 30.0LF, 31.0LF, 32.0LF, 33.0LF, 34.0LF)) g_result |= 128;
|
||||
if (g_8 != dmat3(35.0LF, 36.0LF, 37.0LF, 38.0LF, 39.0LF, 40.0LF, 41.0LF, 42.0LF, 43.0LF)) g_result |= 256;
|
||||
if (g_9 != dmat3x4(44.0LF, 45.0LF, 46.0LF, 47.0LF, 48.0LF, 49.0LF, 50.0LF, 51.0LF, 52.0LF, 53.0LF, 54.0LF, 55.0LF)) g_result |= 512;
|
||||
|
||||
if (g_10 != dmat4x2(56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, 63.0)) g_result |= 1024;
|
||||
if (g_11 != dmat4x3(63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0, 71.0, 27.0, 73, 74.0)) g_result |= 2048;
|
||||
if (g_12 != dmat4(75.0, 76.0, 77.0, 78.0, 79.0, 80.0, 81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, 90.0)) g_result |= 4096;
|
||||
}
|
||||
)";
|
||||
|
||||
TEST_F(DoublePrecisionScenario, ADoubleUniformReachesTheShaderAtFloatPrecision) {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(m_program);
|
||||
@@ -353,6 +493,190 @@ void main() {
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, EveryDoubleUniformShapeArrivesWhereTheShaderReadsIt) {
|
||||
if (!Ready()) return;
|
||||
// Built the way the CTS case builds it, because every step of that build has been a
|
||||
// bug here at least once: the source arrives as TWO strings (the version directive
|
||||
// and the body), the shader is attached before it has a source and deleted while
|
||||
// still attached, and the program is linked twice.
|
||||
m_shapeProgram = glCreateProgram();
|
||||
ASSERT_NE(m_shapeProgram, 0u);
|
||||
{
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glAttachShader(m_shapeProgram, shader);
|
||||
glDeleteShader(shader);
|
||||
const char* const sources[2] = {"#version 430 core\n", kAllDoubleShapesSource};
|
||||
glShaderSource(shader, 2, sources, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
FAIL() << "compute shader did not compile: " << log;
|
||||
}
|
||||
}
|
||||
glLinkProgram(m_shapeProgram);
|
||||
{
|
||||
GLint linkedOnce = 0;
|
||||
glGetProgramiv(m_shapeProgram, GL_LINK_STATUS, &linkedOnce);
|
||||
if (linkedOnce == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetProgramInfoLog(m_shapeProgram, sizeof(log) - 1, nullptr, log);
|
||||
FAIL() << "compute program did not link: " << log;
|
||||
}
|
||||
}
|
||||
|
||||
glGenBuffers(1, &m_shapeOutput);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_shapeOutput);
|
||||
const std::vector<float> zeroes(kAllShapeSlots, 0.0f);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, kAllShapeSlots * sizeof(float), zeroes.data(), GL_DYNAMIC_DRAW);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_shapeOutput);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
|
||||
const auto location = [&](const char* name) { return glGetUniformLocation(m_shapeProgram, name); };
|
||||
|
||||
// Pass one sets through glProgramUniform*, pass two through glUniform* after a
|
||||
// re-link - the two entry-point families the CTS case exercises, and two different
|
||||
// routes into the same uniform storage.
|
||||
const auto setWithProgramUniform = [&]() {
|
||||
glProgramUniform1d(m_shapeProgram, location("g_0"), kG0);
|
||||
glProgramUniform2d(m_shapeProgram, location("g_1"), kG1[0], kG1[1]);
|
||||
glProgramUniform3d(m_shapeProgram, location("g_2"), kG2[0], kG2[1], kG2[2]);
|
||||
glProgramUniform4d(m_shapeProgram, location("g_3"), kG3[0], kG3[1], kG3[2], kG3[3]);
|
||||
glProgramUniformMatrix2dv(m_shapeProgram, location("g_4"), 1, GL_FALSE, kG4);
|
||||
glProgramUniformMatrix2x3dv(m_shapeProgram, location("g_5"), 1, GL_FALSE, kG5);
|
||||
glProgramUniformMatrix2x4dv(m_shapeProgram, location("g_6"), 1, GL_FALSE, kG6);
|
||||
glProgramUniformMatrix3x2dv(m_shapeProgram, location("g_7"), 1, GL_FALSE, kG7);
|
||||
glProgramUniformMatrix3dv(m_shapeProgram, location("g_8"), 1, GL_FALSE, kG8);
|
||||
glProgramUniformMatrix3x4dv(m_shapeProgram, location("g_9"), 1, GL_FALSE, kG9);
|
||||
glProgramUniformMatrix4x2dv(m_shapeProgram, location("g_10"), 1, GL_FALSE, kG10);
|
||||
glProgramUniformMatrix4x3dv(m_shapeProgram, location("g_11"), 1, GL_FALSE, kG11);
|
||||
glProgramUniformMatrix4dv(m_shapeProgram, location("g_12"), 1, GL_FALSE, kG12);
|
||||
};
|
||||
// Deliberately does NOT re-issue glUseProgram: the CTS case leaves the program
|
||||
// current across the re-link and writes into it from there, so this is the path
|
||||
// where a re-link has to keep the current program's uniform storage addressable.
|
||||
const auto setWithUniform = [&]() {
|
||||
glUniform1d(location("g_0"), kG0);
|
||||
glUniform2d(location("g_1"), kG1[0], kG1[1]);
|
||||
glUniform3d(location("g_2"), kG2[0], kG2[1], kG2[2]);
|
||||
glUniform4d(location("g_3"), kG3[0], kG3[1], kG3[2], kG3[3]);
|
||||
glUniformMatrix2dv(location("g_4"), 1, GL_FALSE, kG4);
|
||||
glUniformMatrix2x3dv(location("g_5"), 1, GL_FALSE, kG5);
|
||||
glUniformMatrix2x4dv(location("g_6"), 1, GL_FALSE, kG6);
|
||||
glUniformMatrix3x2dv(location("g_7"), 1, GL_FALSE, kG7);
|
||||
glUniformMatrix3dv(location("g_8"), 1, GL_FALSE, kG8);
|
||||
glUniformMatrix3x4dv(location("g_9"), 1, GL_FALSE, kG9);
|
||||
glUniformMatrix4x2dv(location("g_10"), 1, GL_FALSE, kG10);
|
||||
glUniformMatrix4x3dv(location("g_11"), 1, GL_FALSE, kG11);
|
||||
glUniformMatrix4dv(location("g_12"), 1, GL_FALSE, kG12);
|
||||
};
|
||||
|
||||
const auto dispatchAndRead = [&]() {
|
||||
glUseProgram(m_shapeProgram);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
std::vector<float> values(kAllShapeSlots, -1.0f);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_shapeOutput);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, kAllShapeSlots * sizeof(float), values.data());
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
// The program stays current on purpose - see setWithUniform.
|
||||
return values;
|
||||
};
|
||||
|
||||
const auto expectEverything = [](const std::vector<float>& values, const char* pass) {
|
||||
for (const DoubleShape& shape : kDoubleShapes) {
|
||||
for (int c = 0; c < shape.columns; ++c) {
|
||||
for (int r = 0; r < shape.rows; ++r) {
|
||||
const int component = c * shape.rows + r;
|
||||
EXPECT_FLOAT_EQ(values[shape.base + component],
|
||||
static_cast<float>(shape.values[component]))
|
||||
<< pass << ": " << shape.name << " column " << c << " row " << r;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setWithProgramUniform();
|
||||
expectEverything(dispatchAndRead(), "glProgramUniform*");
|
||||
|
||||
// A re-link zeroes every uniform, so pass two proves its own writes rather than
|
||||
// reading pass one's bytes back.
|
||||
glLinkProgram(m_shapeProgram);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(m_shapeProgram, GL_LINK_STATUS, &linked);
|
||||
ASSERT_EQ(linked, GL_TRUE);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_shapeOutput);
|
||||
glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, kAllShapeSlots * sizeof(float), zeroes.data());
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
|
||||
setWithUniform();
|
||||
expectEverything(dispatchAndRead(), "glUniform* after re-link");
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, TheConformanceUniformShaderAgreesWithEveryValueItWasGiven) {
|
||||
if (!Ready()) return;
|
||||
m_shapeProgram = glCreateProgram();
|
||||
ASSERT_NE(m_shapeProgram, 0u);
|
||||
{
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glAttachShader(m_shapeProgram, shader);
|
||||
glDeleteShader(shader);
|
||||
const char* const sources[2] = {"#version 430 core\n", kCtsShapedSource};
|
||||
glShaderSource(shader, 2, sources, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
FAIL() << "compute shader did not compile: " << log;
|
||||
}
|
||||
}
|
||||
glLinkProgram(m_shapeProgram);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(m_shapeProgram, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetProgramInfoLog(m_shapeProgram, sizeof(log) - 1, nullptr, log);
|
||||
FAIL() << "compute program did not link: " << log;
|
||||
}
|
||||
|
||||
glGenBuffers(1, &m_shapeOutput);
|
||||
const GLint seed = 123;
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_shapeOutput);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(seed), &seed, GL_STATIC_DRAW);
|
||||
|
||||
const auto location = [&](const char* name) { return glGetUniformLocation(m_shapeProgram, name); };
|
||||
glProgramUniform1d(m_shapeProgram, location("g_0"), kG0);
|
||||
glProgramUniform2d(m_shapeProgram, location("g_1"), kG1[0], kG1[1]);
|
||||
glProgramUniform3d(m_shapeProgram, location("g_2"), kG2[0], kG2[1], kG2[2]);
|
||||
glProgramUniform4d(m_shapeProgram, location("g_3"), kG3[0], kG3[1], kG3[2], kG3[3]);
|
||||
glProgramUniformMatrix2dv(m_shapeProgram, location("g_4"), 1, GL_FALSE, kG4);
|
||||
glProgramUniformMatrix2x3dv(m_shapeProgram, location("g_5"), 1, GL_FALSE, kG5);
|
||||
glProgramUniformMatrix2x4dv(m_shapeProgram, location("g_6"), 1, GL_FALSE, kG6);
|
||||
glProgramUniformMatrix3x2dv(m_shapeProgram, location("g_7"), 1, GL_FALSE, kG7);
|
||||
glProgramUniformMatrix3dv(m_shapeProgram, location("g_8"), 1, GL_FALSE, kG8);
|
||||
glProgramUniformMatrix3x4dv(m_shapeProgram, location("g_9"), 1, GL_FALSE, kG9);
|
||||
glProgramUniformMatrix4x2dv(m_shapeProgram, location("g_10"), 1, GL_FALSE, kG10);
|
||||
glProgramUniformMatrix4x3dv(m_shapeProgram, location("g_11"), 1, GL_FALSE, kG11);
|
||||
glProgramUniformMatrix4dv(m_shapeProgram, location("g_12"), 1, GL_FALSE, kG12);
|
||||
|
||||
glUseProgram(m_shapeProgram);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
|
||||
GLint disagreements = -1;
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(disagreements), &disagreements);
|
||||
for (int bit = 0; bit < 13; ++bit) {
|
||||
EXPECT_EQ(disagreements & (1 << bit), 0)
|
||||
<< kDoubleShapes[bit].name << " did not compare equal to the value it was given";
|
||||
}
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, TheFp64ExtensionIsNotAdvertised) {
|
||||
if (!Ready()) return;
|
||||
// The shader above compiled, linked and ran without the extension string, which is
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageFormatQualifierScenario.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
|
||||
//
|
||||
// Scenario - AN IMAGE UNIFORM THAT DECLARES NO FORMAT.
|
||||
//
|
||||
// Desktop GLSL 4.2 lets a writeonly image declaration omit its format layout qualifier:
|
||||
//
|
||||
// writeonly uniform uimage2D uni_image; // legal desktop GLSL
|
||||
//
|
||||
// GLSL ES has no such relaxation; every image uniform must carry one, and Adreno says so as "all
|
||||
// images have to define layout format", which fails the whole program. That is what took the
|
||||
// compute half of KHR-GL4x.packed_depth_stencil.stencil_texturing.
|
||||
//
|
||||
// The only qualifier that is CORRECT to substitute is whatever glBindImageTexture named for the
|
||||
// unit that uniform addresses - GL requires the qualifier, the bind format and the texture's
|
||||
// internal format to belong to one format class - so the format is not knowable when the shader
|
||||
// is compiled, only when it is drawn with. Espryt therefore BAKES it into the program it
|
||||
// generates and keys that program on the (unit, format) pairs it baked
|
||||
// (BackendProgramObjectImpl::ImageUnitFormatsStillMatch, MG_Backend/DirectGLES).
|
||||
//
|
||||
// Three separate things follow from "the program is built against live binding state", and each
|
||||
// one is a case below:
|
||||
//
|
||||
// 1. the format reaches the shader at all, so the store lands where the texture is (Writes);
|
||||
// 2. binding a DIFFERENT format to the same unit rebuilds the program, rather than reusing one
|
||||
// compiled against the old format (RebindToADifferentFormatRebuilds);
|
||||
// 3. an image bound for the FIRST time after the link works, i.e. the program built against
|
||||
// "nothing bound yet" is not the one the dispatch runs (FirstBindAfterLinkRebuilds).
|
||||
//
|
||||
// Magma needs none of this - Vulkan takes an Unknown-format storage image given
|
||||
// shaderStorageImageWriteWithoutFormat, and the view format is resolved from the same bind state
|
||||
// at descriptor time - so every case here runs on both backends and must agree, which is what
|
||||
// makes the ES-only machinery falsifiable rather than merely exercised.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kExtent = 4;
|
||||
// The image unit is deliberately NOT 0 and the uniform declares no binding, so the unit
|
||||
// has to travel through glUniform1i and be baked into the ESSL alongside the format -
|
||||
// the two bakes share a rebuild key and a bug in either shows up as the wrong texel.
|
||||
constexpr GLint kImageUnit = 1;
|
||||
|
||||
// KHR-GL4x.packed_depth_stencil.stencil_texturing's own image declaration, verbatim.
|
||||
const char* kStoreSource = R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
writeonly uniform uimage2D uni_image;
|
||||
|
||||
void main()
|
||||
{
|
||||
imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(gl_GlobalInvocationID.x + 100u, 0u, 0u, 0u));
|
||||
}
|
||||
)";
|
||||
|
||||
class ImageFormatQualifierScenario : public ScenarioTest {
|
||||
protected:
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (GLuint p : m_programs) glDeleteProgram(p);
|
||||
for (GLuint t : m_textures) glDeleteTextures(1, &t);
|
||||
m_programs.clear();
|
||||
m_textures.clear();
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
for (GLint unit = 0; unit < maxImageUnits; ++unit) {
|
||||
glBindImageTexture(static_cast<GLuint>(unit), 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
|
||||
}
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
bool ImagesAreUsable() const {
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
GLint maxComputeImageUniforms = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return maxImageUnits > kImageUnit && maxComputeImageUniforms >= 1;
|
||||
}
|
||||
|
||||
GLuint MakeComputeProgram(const std::string& source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
const char* text = source.c_str();
|
||||
glShaderSource(shader, 1, &text, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute shader did not compile: " << log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
m_programs.push_back(program);
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute program did not link: " << log;
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint MakeTexture(GLenum internalFormat) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kExtent, kExtent);
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << "allocating storage errored with " << GLErrorName(error);
|
||||
return 0;
|
||||
}
|
||||
// Seeded to a value no dispatch writes, so "the store never happened" and "the
|
||||
// store wrote the right thing" cannot be confused.
|
||||
const std::vector<GLuint> zeros(static_cast<std::size_t>(kExtent) * kExtent * 4u, 0u);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent,
|
||||
internalFormat == GL_RGBA32UI ? GL_RGBA_INTEGER : GL_RED_INTEGER, GL_UNSIGNED_INT,
|
||||
zeros.data());
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
// Texel (x, 0) of the texture's red channel, read back through the GL frontend rather
|
||||
// than through a second image uniform: a defect in the format bake would be shared by
|
||||
// a reader declared the same way and could cancel itself out.
|
||||
GLuint ReadRedTexel(GLuint texture, GLenum internalFormat, int x) {
|
||||
const bool rgba = internalFormat == GL_RGBA32UI;
|
||||
std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * (rgba ? 4u : 1u),
|
||||
0xFFFFFFFFu);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, rgba ? GL_RGBA_INTEGER : GL_RED_INTEGER, GL_UNSIGNED_INT,
|
||||
texels.data());
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << "reading the image back errored with " << GLErrorName(error);
|
||||
return 0xFFFFFFFFu;
|
||||
}
|
||||
return texels[static_cast<std::size_t>(x) * (rgba ? 4u : 1u)];
|
||||
}
|
||||
|
||||
void DispatchStore(GLuint program, GLuint texture, GLenum internalFormat) {
|
||||
glBindImageTexture(static_cast<GLuint>(kImageUnit), texture, 0, GL_FALSE, 0, GL_WRITE_ONLY,
|
||||
internalFormat);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "glBindImageTexture errored";
|
||||
glUseProgram(program);
|
||||
const GLint location = glGetUniformLocation(program, "uni_image");
|
||||
ASSERT_GE(location, 0) << "the image uniform was not reflected";
|
||||
glUniform1i(location, kImageUnit);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "assigning the image unit errored";
|
||||
glDispatchCompute(kExtent, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_programs;
|
||||
std::vector<GLuint> m_textures;
|
||||
};
|
||||
|
||||
// The defect itself. Without the bake the ES driver refuses the program outright and the
|
||||
// texture keeps its seed - which is also exactly what a silently no-op dispatch looks
|
||||
// like, and why the seed is a value no store writes.
|
||||
TEST_F(ImageFormatQualifierScenario, AFormatlessWriteonlyImageWrites) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
const GLuint program = MakeComputeProgram(kStoreSource);
|
||||
const GLuint texture = MakeTexture(GL_R32UI);
|
||||
if (program == 0 || texture == 0) return;
|
||||
|
||||
DispatchStore(program, texture, GL_R32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "texel " << x << " of a format-less writeonly image did not take the store";
|
||||
}
|
||||
}
|
||||
|
||||
// The rebuild key. The SAME program is dispatched twice with a different format bound to
|
||||
// its unit; a build keyed only on the link (or only on the image UNIT) would reuse the
|
||||
// r32ui program for the rgba32ui texture, and the second half would come back seeded.
|
||||
//
|
||||
// What the SOFTWARE lanes cannot falsify: with the key disabled this case still passes on
|
||||
// Mesa, because the reused r32ui declaration writes the red channel of an RGBA32UI image
|
||||
// anyway - a format-class mismatch GL leaves undefined and that driver happens to absorb.
|
||||
// FirstBindAfterLinkRebuilds below is the case that fails there, because the reused
|
||||
// program was built with no format at all and never compiled. Both are kept: this one is
|
||||
// the shape a strict driver is entitled to reject, and it is the shape the device runs.
|
||||
TEST_F(ImageFormatQualifierScenario, RebindToADifferentFormatRebuilds) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
const GLuint program = MakeComputeProgram(kStoreSource);
|
||||
const GLuint first = MakeTexture(GL_R32UI);
|
||||
const GLuint second = MakeTexture(GL_RGBA32UI);
|
||||
if (program == 0 || first == 0 || second == 0) return;
|
||||
|
||||
DispatchStore(program, first, GL_R32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
ASSERT_EQ(ReadRedTexel(first, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "the first format must work before the rebind can be blamed for anything";
|
||||
}
|
||||
|
||||
DispatchStore(program, second, GL_RGBA32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
EXPECT_EQ(ReadRedTexel(second, GL_RGBA32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "texel " << x << ": the program was not rebuilt for the newly bound format";
|
||||
}
|
||||
|
||||
// ...and back, so the rebuild is not a one-way door: returning to a format the
|
||||
// program was once built against must build for it again, not resurrect a cache row.
|
||||
const GLuint third = MakeTexture(GL_R32UI);
|
||||
if (third == 0) return;
|
||||
DispatchStore(program, third, GL_R32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
EXPECT_EQ(ReadRedTexel(third, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "texel " << x << ": going back to the first format did not rebuild";
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing is bound to the unit when the program links, so whatever the first build sees
|
||||
// is not the format the dispatch needs. glBindImageTexture must not itself trigger a
|
||||
// build - it is an entry point, and building there is the constraint
|
||||
// glShaderStorageBlockBinding is held to as well - so the rebuild has to happen at the
|
||||
// next dispatch preparation instead. This case fails either way round: no rebuild, or a
|
||||
// build attempted from the entry point before the state settles.
|
||||
TEST_F(ImageFormatQualifierScenario, FirstBindAfterLinkRebuilds) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
const GLuint program = MakeComputeProgram(kStoreSource);
|
||||
if (program == 0) return;
|
||||
|
||||
// Use it once with NOTHING bound to the unit, which is what makes the backend build
|
||||
// against an empty binding. The dispatch writes nowhere and must not error.
|
||||
glUseProgram(program);
|
||||
const GLint location = glGetUniformLocation(program, "uni_image");
|
||||
ASSERT_GE(location, 0);
|
||||
glUniform1i(location, kImageUnit);
|
||||
glDispatchCompute(kExtent, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "dispatching with an unbound image unit must not error";
|
||||
glUseProgram(0);
|
||||
|
||||
const GLuint texture = MakeTexture(GL_R32UI);
|
||||
if (texture == 0) return;
|
||||
DispatchStore(program, texture, GL_R32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "texel " << x << ": the first bind after the link did not reach the shader";
|
||||
}
|
||||
}
|
||||
|
||||
// A DECLARED format is authoritative and the bake must never touch it - including when
|
||||
// the texture behind the unit has a different (but class-compatible) internal format,
|
||||
// which GL explicitly allows. If the bake ever overrode a declaration, this is the case
|
||||
// that would go wrong while every other one stayed green.
|
||||
TEST_F(ImageFormatQualifierScenario, ADeclaredFormatStillWins) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
const GLuint program = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (r32ui) writeonly uniform uimage2D uni_image;
|
||||
|
||||
void main()
|
||||
{
|
||||
imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(gl_GlobalInvocationID.x + 100u, 0u, 0u, 0u));
|
||||
}
|
||||
)");
|
||||
const GLuint texture = MakeTexture(GL_R32UI);
|
||||
if (program == 0 || texture == 0) return;
|
||||
|
||||
DispatchStore(program, texture, GL_R32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "texel " << x << ": a declared format stopped working";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,550 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageTargetKindScenario.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
|
||||
//
|
||||
// Scenario - ONE IMAGE TARGET KIND AT A TIME, THROUGH A COMPUTE DISPATCH.
|
||||
//
|
||||
// KHR-GL44.multi_bind.dispatch_bind_image_textures decomposed. That conformance case declares
|
||||
// ELEVEN image uniforms of eleven different target kinds in one compute shader, binds a texture
|
||||
// of the matching kind to each unit, sums one texel from every one of them and compares the sum
|
||||
// against N*(N-1)/2. It is a single pass/fail bit over eleven independent mechanisms: if any one
|
||||
// of them is wrong - or merely fails to compile - the case fails and says nothing about which.
|
||||
// That is what it did here, on both backends, for two waves.
|
||||
//
|
||||
// So the eleven are pulled apart into one case each. Each case declares ONE image uniform, binds
|
||||
// ONE texture and checks the value that comes back, so a failure names the target kind and the
|
||||
// direction. What the conformance case does with eleven at once, AllKindsInOneProgram at the
|
||||
// bottom still does - a defect that only appears when several kinds share a program is invisible
|
||||
// to the single-kind cases by construction.
|
||||
//
|
||||
// The shape is deliberately the conformance case's own, not a cleaner equivalent:
|
||||
//
|
||||
// * r32ui / GL_R32UI throughout, 6x6x6 storage, one level, texel (0,0,0) read;
|
||||
// * `layout (location = N, r32ui) readonly uniform` - an explicit uniform LOCATION, not a
|
||||
// binding, with the image unit then assigned by glUniform1i. That combination is the one ES
|
||||
// cannot express directly, because ES forbids glUniform1i on an image uniform and the unit
|
||||
// has to be baked into the generated ESSL (RebindImageUniformsToFrontendUnits);
|
||||
// * `layout (std140, ...) buffer` for the result block - legal, but unusual enough that a
|
||||
// frontend could plausibly mishandle it. Mirroring it means a green scenario cannot be green
|
||||
// for a reason the conformance case excludes;
|
||||
// * glBindImageTexture with layered = GL_TRUE, which is what glBindImageTextures is specified
|
||||
// to pass, and which is where a target kind whose layeredness a backend does not recognise
|
||||
// goes wrong.
|
||||
//
|
||||
// MULTISAMPLE is the one kind that is not merely an emulation problem, and the conformance case
|
||||
// already knows it: it reads GL_MAX_IMAGE_SAMPLES and, when that is zero, substitutes a plain 2D
|
||||
// texture and a plain uimage2D for both multisample entries. MobileGL reports zero, so the
|
||||
// conformance case never asks it for a multisample image at all. The two cases below are kept
|
||||
// and skip on that same query, so the coverage is already written the day a backend advertises
|
||||
// them - and so the skip is a standing record of WHY the conformance case passes without them.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// The conformance case's own dimensions: one level, 6 on every axis (which is also
|
||||
// exactly one cube's worth for a cube array), and a single texel read at the origin.
|
||||
constexpr int kExtent = 6;
|
||||
constexpr GLuint kFilledValue = 7u;
|
||||
constexpr GLuint kStoredValue = 13u;
|
||||
|
||||
// Everything that differs between the eleven kinds, in one row.
|
||||
struct TargetKind {
|
||||
const char* name; // this scenario's name for it, which failure messages carry
|
||||
GLenum target; // the GL texture target
|
||||
const char* imageType; // the GLSL image uniform type
|
||||
const char* coord; // the coordinate expression imageLoad/imageStore takes
|
||||
bool multisample; // needs GL_MAX_IMAGE_SAMPLES > 0
|
||||
bool buffer; // storage comes from a buffer object, not TexStorage
|
||||
};
|
||||
|
||||
constexpr TargetKind kKind1D{"1D", GL_TEXTURE_1D, "uimage1D", "0", false, false};
|
||||
constexpr TargetKind kKind1DArray{"1DArray", GL_TEXTURE_1D_ARRAY, "uimage1DArray", "ivec2(0, 0)", false,
|
||||
false};
|
||||
constexpr TargetKind kKind2D{"2D", GL_TEXTURE_2D, "uimage2D", "ivec2(0, 0)", false, false};
|
||||
constexpr TargetKind kKind2DArray{"2DArray", GL_TEXTURE_2D_ARRAY, "uimage2DArray", "ivec3(0, 0, 0)", false,
|
||||
false};
|
||||
constexpr TargetKind kKind3D{"3D", GL_TEXTURE_3D, "uimage3D", "ivec3(0, 0, 0)", false, false};
|
||||
constexpr TargetKind kKindBuffer{"Buffer", GL_TEXTURE_BUFFER, "uimageBuffer", "0", false, true};
|
||||
constexpr TargetKind kKindCube{"Cube", GL_TEXTURE_CUBE_MAP, "uimageCube", "ivec3(0, 0, 0)", false, false};
|
||||
constexpr TargetKind kKindCubeArray{"CubeArray", GL_TEXTURE_CUBE_MAP_ARRAY, "uimageCubeArray",
|
||||
"ivec3(0, 0, 0)", false, false};
|
||||
constexpr TargetKind kKindRect{"Rect", GL_TEXTURE_RECTANGLE, "uimage2DRect", "ivec2(0, 0)", false, false};
|
||||
constexpr TargetKind kKind2DMS{"2DMS", GL_TEXTURE_2D_MULTISAMPLE, "uimage2DMS", "ivec2(0, 0)", true, false};
|
||||
constexpr TargetKind kKind2DMSArray{"2DMSArray", GL_TEXTURE_2D_MULTISAMPLE_ARRAY, "uimage2DMSArray",
|
||||
"ivec3(0, 0, 0)", true, false};
|
||||
|
||||
// A multisample image load/store takes the sample index as an extra argument; no other
|
||||
// kind does. Keeping that in one place stops the two spellings drifting apart.
|
||||
std::string LoadExpression(const TargetKind& kind, const std::string& name) {
|
||||
return "imageLoad(" + name + ", " + kind.coord + (kind.multisample ? ", 0)" : ")");
|
||||
}
|
||||
|
||||
std::string StoreStatement(const TargetKind& kind, const std::string& name, const char* value) {
|
||||
return "imageStore(" + name + ", " + kind.coord + (kind.multisample ? ", 0, uvec4(" : ", uvec4(") +
|
||||
value + ", 0, 0, 0));";
|
||||
}
|
||||
|
||||
const char* kComputePrologue = "#version 440 core\n"
|
||||
"\n"
|
||||
"layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;\n"
|
||||
"\n";
|
||||
|
||||
const char* kResultBlock = "layout (std140, binding = 0) buffer SSB {\n"
|
||||
" uint sum;\n"
|
||||
"} ssb;\n"
|
||||
"\n";
|
||||
|
||||
// The conformance case's shader, narrowed to a single image.
|
||||
std::string SingleLoadSource(const TargetKind& kind) {
|
||||
return std::string(kComputePrologue) + "layout (location = 0, r32ui) readonly uniform " + kind.imageType +
|
||||
" i0;\n" + kResultBlock + "void main()\n{\n uvec4 v = " + LoadExpression(kind, "i0") +
|
||||
";\n ssb.sum = v.r;\n}\n";
|
||||
}
|
||||
|
||||
// The other direction. Written as its own program rather than a read-write one so that a
|
||||
// backend which gets the store right and the load wrong (or the reverse) is not able to
|
||||
// cancel its own defect out.
|
||||
std::string SingleStoreSource(const TargetKind& kind) {
|
||||
return std::string(kComputePrologue) + "layout (location = 0, r32ui) writeonly uniform " +
|
||||
kind.imageType + " i0;\n\nvoid main()\n{\n " + StoreStatement(kind, "i0", "13u") + "\n}\n";
|
||||
}
|
||||
|
||||
class ImageTargetKindScenario : public ScenarioTest {
|
||||
protected:
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (GLuint p : m_programs) glDeleteProgram(p);
|
||||
for (GLuint t : m_textures) glDeleteTextures(1, &t);
|
||||
for (GLuint b : m_buffers) glDeleteBuffers(1, &b);
|
||||
m_programs.clear();
|
||||
m_textures.clear();
|
||||
m_buffers.clear();
|
||||
// Leave no image unit bound. These scenarios share one context, and a stale image
|
||||
// binding is exactly the kind of state that makes the NEXT scenario's failure
|
||||
// impossible to reproduce on its own.
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
for (GLint unit = 0; unit < maxImageUnits; ++unit) {
|
||||
glBindImageTexture(static_cast<GLuint>(unit), 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
|
||||
}
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
bool ImagesAreUsable() const {
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
GLint maxComputeImageUniforms = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return maxImageUnits >= 1 && maxComputeImageUniforms >= 1;
|
||||
}
|
||||
|
||||
// The conformance case's own multisample gate, asked the same way it asks it.
|
||||
bool MultisampleImagesAreUsable() const {
|
||||
GLint maxImageSamples = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_SAMPLES, &maxImageSamples);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return maxImageSamples > 0;
|
||||
}
|
||||
|
||||
GLuint MakeComputeProgram(const std::string& source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
const char* text = source.c_str();
|
||||
glShaderSource(shader, 1, &text, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute shader did not compile: " << log << "\nsource:\n" << source;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
m_programs.push_back(program);
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute program did not link: " << log << "\nsource:\n" << source;
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// Storage plus a full fill with `value`, in the spelling each target kind needs.
|
||||
// Returns 0 - having already reported - when the target could not be created.
|
||||
GLuint MakeTexture(const TargetKind& kind, bool fill, GLuint value = kFilledValue) {
|
||||
const std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * kExtent, value);
|
||||
|
||||
if (kind.buffer) {
|
||||
GLuint buffer = 0;
|
||||
glGenBuffers(1, &buffer);
|
||||
m_buffers.push_back(buffer);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(texels.size() * sizeof(GLuint)),
|
||||
fill ? texels.data() : nullptr, GL_DYNAMIC_COPY);
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||
glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, buffer);
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << kind.name << ": creating the texture buffer errored with "
|
||||
<< GLErrorName(error);
|
||||
return 0;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(kind.target, texture);
|
||||
|
||||
switch (kind.target) {
|
||||
case GL_TEXTURE_1D:
|
||||
glTexStorage1D(kind.target, 1, GL_R32UI, kExtent);
|
||||
break;
|
||||
case GL_TEXTURE_2D:
|
||||
case GL_TEXTURE_RECTANGLE:
|
||||
case GL_TEXTURE_1D_ARRAY:
|
||||
case GL_TEXTURE_CUBE_MAP:
|
||||
glTexStorage2D(kind.target, 1, GL_R32UI, kExtent, kExtent);
|
||||
break;
|
||||
case GL_TEXTURE_2D_ARRAY:
|
||||
case GL_TEXTURE_3D:
|
||||
case GL_TEXTURE_CUBE_MAP_ARRAY:
|
||||
glTexStorage3D(kind.target, 1, GL_R32UI, kExtent, kExtent, kExtent);
|
||||
break;
|
||||
case GL_TEXTURE_2D_MULTISAMPLE:
|
||||
glTexStorage2DMultisample(kind.target, 1, GL_R32UI, kExtent, kExtent, GL_FALSE);
|
||||
break;
|
||||
case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
|
||||
glTexStorage3DMultisample(kind.target, 1, GL_R32UI, kExtent, kExtent, kExtent, GL_FALSE);
|
||||
break;
|
||||
default:
|
||||
ADD_FAILURE() << kind.name << ": no storage spelling for target 0x" << std::hex << kind.target;
|
||||
return 0;
|
||||
}
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << kind.name << ": allocating storage errored with " << GLErrorName(error);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A multisample texture has no TexSubImage - the conformance case fills it with a
|
||||
// compute pass, which is what the store cases below do.
|
||||
if (!fill || kind.multisample) return texture;
|
||||
|
||||
switch (kind.target) {
|
||||
case GL_TEXTURE_1D:
|
||||
glTexSubImage1D(kind.target, 0, 0, kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
|
||||
break;
|
||||
case GL_TEXTURE_2D:
|
||||
case GL_TEXTURE_RECTANGLE:
|
||||
case GL_TEXTURE_1D_ARRAY:
|
||||
glTexSubImage2D(kind.target, 0, 0, 0, kExtent, kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT,
|
||||
texels.data());
|
||||
break;
|
||||
case GL_TEXTURE_CUBE_MAP:
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
glTexSubImage2D(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, 0, 0, kExtent,
|
||||
kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_2D_ARRAY:
|
||||
case GL_TEXTURE_3D:
|
||||
case GL_TEXTURE_CUBE_MAP_ARRAY:
|
||||
glTexSubImage3D(kind.target, 0, 0, 0, 0, kExtent, kExtent, kExtent, GL_RED_INTEGER,
|
||||
GL_UNSIGNED_INT, texels.data());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << kind.name << ": uploading texels errored with " << GLErrorName(error);
|
||||
return 0;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A 4-byte `buffer` block bound to base 0, which is where every case puts its answer.
|
||||
GLuint MakeResultBuffer() {
|
||||
GLuint ssbo = 0;
|
||||
glGenBuffers(1, &ssbo);
|
||||
m_buffers.push_back(ssbo);
|
||||
const GLuint zero = 0u;
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(GLuint), &zero, GL_DYNAMIC_COPY);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo);
|
||||
return ssbo;
|
||||
}
|
||||
|
||||
GLuint ReadResult(GLuint ssbo) {
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
|
||||
GLuint value = 0xFFFFFFFFu;
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(GLuint), &value);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Fill a texture of `kind`, read texel (0,0,0) of it through an image uniform in a
|
||||
// compute dispatch, and require the value back.
|
||||
void RunLoadCase(const TargetKind& kind) {
|
||||
const GLuint program = MakeComputeProgram(SingleLoadSource(kind));
|
||||
if (program == 0) return;
|
||||
const GLuint texture = MakeTexture(kind, true);
|
||||
if (texture == 0) return;
|
||||
const GLuint ssbo = MakeResultBuffer();
|
||||
|
||||
glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_ONLY, GL_R32UI);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": glBindImageTexture errored";
|
||||
|
||||
glUseProgram(program);
|
||||
// The unit, by LOCATION - the conformance case's own redundant-but-legal
|
||||
// assignment, and the one ES cannot take at the API level.
|
||||
glUniform1i(0, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": assigning the image unit errored";
|
||||
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the dispatch leaked a GL error";
|
||||
|
||||
EXPECT_EQ(ReadResult(ssbo), kFilledValue)
|
||||
<< kind.name << ": the compute dispatch did not read the value the texture was filled with";
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
// The other direction: store through an image uniform, then read the same texel back
|
||||
// through a SECOND program, so a defect cannot cancel itself out.
|
||||
void RunStoreCase(const TargetKind& kind) {
|
||||
const GLuint storeProgram = MakeComputeProgram(SingleStoreSource(kind));
|
||||
const GLuint loadProgram = MakeComputeProgram(SingleLoadSource(kind));
|
||||
if (storeProgram == 0 || loadProgram == 0) return;
|
||||
const GLuint texture = MakeTexture(kind, false);
|
||||
if (texture == 0) return;
|
||||
const GLuint ssbo = MakeResultBuffer();
|
||||
|
||||
glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_WRITE, GL_R32UI);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": glBindImageTexture errored";
|
||||
|
||||
glUseProgram(storeProgram);
|
||||
glUniform1i(0, 0);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the storing dispatch leaked a GL error";
|
||||
|
||||
glUseProgram(loadProgram);
|
||||
glUniform1i(0, 0);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the loading dispatch leaked a GL error";
|
||||
|
||||
EXPECT_EQ(ReadResult(ssbo), kStoredValue)
|
||||
<< kind.name << ": the value stored through the image did not come back";
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_programs;
|
||||
std::vector<GLuint> m_textures;
|
||||
std::vector<GLuint> m_buffers;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- the load direction, one target kind per case -----------------------
|
||||
//
|
||||
// Exactly what the conformance case does with each of its eleven uniforms, but alone, so a
|
||||
// failure names the kind.
|
||||
|
||||
#define MGL_DEFINE_LOAD_CASE(CaseName, Kind) \
|
||||
TEST_F(ImageTargetKindScenario, Loads##CaseName) { \
|
||||
if (!Ready()) return; \
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms"; \
|
||||
if ((Kind).multisample && !MultisampleImagesAreUsable()) { \
|
||||
GTEST_SKIP() << "GL_MAX_IMAGE_SAMPLES is 0, so the conformance case substitutes a plain 2D image " \
|
||||
"here and never asks for a multisample one"; \
|
||||
} \
|
||||
RunLoadCase(Kind); \
|
||||
}
|
||||
|
||||
#define MGL_DEFINE_STORE_CASE(CaseName, Kind) \
|
||||
TEST_F(ImageTargetKindScenario, Stores##CaseName) { \
|
||||
if (!Ready()) return; \
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms"; \
|
||||
if ((Kind).multisample && !MultisampleImagesAreUsable()) { \
|
||||
GTEST_SKIP() << "GL_MAX_IMAGE_SAMPLES is 0, so the conformance case substitutes a plain 2D image " \
|
||||
"here and never asks for a multisample one"; \
|
||||
} \
|
||||
RunStoreCase(Kind); \
|
||||
}
|
||||
|
||||
MGL_DEFINE_LOAD_CASE(Texture1D, kKind1D)
|
||||
MGL_DEFINE_LOAD_CASE(Texture1DArray, kKind1DArray)
|
||||
MGL_DEFINE_LOAD_CASE(Texture2D, kKind2D)
|
||||
MGL_DEFINE_LOAD_CASE(Texture2DArray, kKind2DArray)
|
||||
MGL_DEFINE_LOAD_CASE(Texture3D, kKind3D)
|
||||
MGL_DEFINE_LOAD_CASE(TextureBuffer, kKindBuffer)
|
||||
MGL_DEFINE_LOAD_CASE(TextureCube, kKindCube)
|
||||
MGL_DEFINE_LOAD_CASE(TextureCubeArray, kKindCubeArray)
|
||||
MGL_DEFINE_LOAD_CASE(TextureRectangle, kKindRect)
|
||||
MGL_DEFINE_LOAD_CASE(Texture2DMultisample, kKind2DMS)
|
||||
MGL_DEFINE_LOAD_CASE(Texture2DMultisampleArray, kKind2DMSArray)
|
||||
|
||||
MGL_DEFINE_STORE_CASE(Texture1D, kKind1D)
|
||||
MGL_DEFINE_STORE_CASE(Texture1DArray, kKind1DArray)
|
||||
MGL_DEFINE_STORE_CASE(Texture2D, kKind2D)
|
||||
MGL_DEFINE_STORE_CASE(Texture2DArray, kKind2DArray)
|
||||
MGL_DEFINE_STORE_CASE(Texture3D, kKind3D)
|
||||
MGL_DEFINE_STORE_CASE(TextureBuffer, kKindBuffer)
|
||||
MGL_DEFINE_STORE_CASE(TextureCube, kKindCube)
|
||||
MGL_DEFINE_STORE_CASE(TextureCubeArray, kKindCubeArray)
|
||||
MGL_DEFINE_STORE_CASE(TextureRectangle, kKindRect)
|
||||
MGL_DEFINE_STORE_CASE(Texture2DMultisample, kKind2DMS)
|
||||
MGL_DEFINE_STORE_CASE(Texture2DMultisampleArray, kKind2DMSArray)
|
||||
|
||||
#undef MGL_DEFINE_LOAD_CASE
|
||||
#undef MGL_DEFINE_STORE_CASE
|
||||
|
||||
// ---- and all of them at once -------------------------------------------
|
||||
//
|
||||
// The conformance case's actual shape. The single-kind cases above cannot see a defect that
|
||||
// needs several kinds in one program - a binding remap that only collides when two image
|
||||
// types share a descriptor set, a per-kind rewrite that is not idempotent across declarations
|
||||
// - and that class of defect is precisely what "each kind passes alone but the case still
|
||||
// fails" would mean.
|
||||
//
|
||||
// Each unit is filled with its own DISTINCT value rather than a shared one, so a shortfall
|
||||
// names WHICH kind is missing rather than merely how many are: with one shared value, "three
|
||||
// kinds read zero" and "one kind read zero" differ only by a multiple, and any two kinds are
|
||||
// interchangeable in the total. A sum still cannot see two kinds SWAPPING - addition is
|
||||
// commutative, and the conformance case has exactly the same blind spot - but the single-kind
|
||||
// cases above pin each kind to its own texture already, so a swap cannot hide there.
|
||||
TEST_F(ImageTargetKindScenario, AllKindsInOneProgram) {
|
||||
if (!Ready()) return;
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";
|
||||
|
||||
// The two kinds this whole scenario file exists for come FIRST, and that ordering is
|
||||
// load-bearing rather than cosmetic. The list has to be truncated to the device's image
|
||||
// unit count, and the guaranteed minimum is small - ES 3.1 promises only four compute
|
||||
// image uniforms - so a list in the conformance case's own order would put imageBuffer
|
||||
// at index five and drop it on exactly the devices most likely to get it wrong. A test
|
||||
// that quietly stops covering its own subject is worse than one that fails.
|
||||
const bool multisample = MultisampleImagesAreUsable();
|
||||
std::vector<TargetKind> kinds{kKind1DArray, kKindBuffer, kKind2D, kKind1D, kKind2DArray,
|
||||
kKind3D, kKindCube, kKindRect, kKindCubeArray};
|
||||
if (multisample) {
|
||||
kinds.push_back(kKind2DMS);
|
||||
kinds.push_back(kKind2DMSArray);
|
||||
}
|
||||
|
||||
GLint maxComputeImageUniforms = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
const std::size_t count =
|
||||
std::min<std::size_t>(kinds.size(), static_cast<std::size_t>(std::max(0, std::min(maxComputeImageUniforms,
|
||||
maxImageUnits))));
|
||||
if (count == 0) GTEST_SKIP() << "no image units";
|
||||
// Named, not silently dropped: `expected` is computed over whatever survives, so a
|
||||
// truncated run is self-consistently green and would otherwise never say what it stopped
|
||||
// covering.
|
||||
if (count < kinds.size()) {
|
||||
std::string dropped;
|
||||
for (std::size_t i = count; i < kinds.size(); ++i) {
|
||||
if (!dropped.empty()) dropped += ", ";
|
||||
dropped += kinds[i].name;
|
||||
}
|
||||
RecordProperty("dropped_image_target_kinds", dropped);
|
||||
GTEST_LOG_(INFO) << "only " << count << " image units, so these kinds are not covered by the "
|
||||
<< "combined case: " << dropped;
|
||||
}
|
||||
kinds.resize(count);
|
||||
|
||||
std::string declarations;
|
||||
std::string sum;
|
||||
for (std::size_t i = 0; i < kinds.size(); ++i) {
|
||||
const std::string name = "i" + std::to_string(i);
|
||||
declarations += "layout (location = " + std::to_string(i) + ", r32ui) readonly uniform " +
|
||||
kinds[i].imageType + " " + name + ";\n";
|
||||
if (!sum.empty()) sum += " + ";
|
||||
sum += LoadExpression(kinds[i], name);
|
||||
}
|
||||
const std::string source = std::string(kComputePrologue) + declarations + kResultBlock +
|
||||
"void main()\n{\n uvec4 v = " + sum + ";\n ssb.sum = v.r;\n}\n";
|
||||
|
||||
const GLuint program = MakeComputeProgram(source);
|
||||
if (program == 0) return;
|
||||
|
||||
// Powers of two, so the shortfall's bit pattern names exactly which kinds read zero -
|
||||
// no other subset of the values can sum to the same total. Eleven kinds at most, so the
|
||||
// largest is 1 << 10 and the sum cannot approach a uint's range.
|
||||
GLuint expected = 0;
|
||||
for (std::size_t i = 0; i < kinds.size(); ++i) {
|
||||
const GLuint value = 1u << i;
|
||||
const GLuint texture = MakeTexture(kinds[i], true, value);
|
||||
if (texture == 0) return;
|
||||
expected += value;
|
||||
glBindImageTexture(static_cast<GLuint>(i), texture, 0, GL_TRUE, 0, GL_READ_ONLY, GL_R32UI);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << kinds[i].name << ": glBindImageTexture errored";
|
||||
}
|
||||
const GLuint ssbo = MakeResultBuffer();
|
||||
|
||||
glUseProgram(program);
|
||||
for (std::size_t i = 0; i < kinds.size(); ++i) {
|
||||
glUniform1i(static_cast<GLint>(i), static_cast<GLint>(i));
|
||||
}
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "assigning the image units errored";
|
||||
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
|
||||
|
||||
const GLuint actual = ReadResult(ssbo);
|
||||
std::string missing;
|
||||
for (std::size_t i = 0; i < kinds.size(); ++i) {
|
||||
if ((actual & (1u << i)) == 0u) {
|
||||
if (!missing.empty()) missing += ", ";
|
||||
missing += kinds[i].name;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(actual, expected)
|
||||
<< "the sum over " << kinds.size()
|
||||
<< " image target kinds is wrong; each kind contributes its own bit, and these read "
|
||||
"zero: "
|
||||
<< (missing.empty() ? "(none - so some kind read a value it was never given)" : missing);
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,378 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LayeredAttachmentBarrierScenario.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
|
||||
//
|
||||
// Scenario - A TRANSFER OFF A NON-ZERO ATTACHMENT LAYER READS THE LAYER THE BARRIER MOVED.
|
||||
//
|
||||
// Every transfer DirectVulkan performs against a framebuffer attachment is three commands: a
|
||||
// barrier that puts the image in TRANSFER_SRC/DST, the copy or blit itself, and a barrier that
|
||||
// puts it back. The copy names the attachment's layer - glFramebufferTextureLayer(.., layer) ends
|
||||
// up in `srcSubresource.baseArrayLayer` - but TransitionImageLayout used to emit `layerCount = 1`
|
||||
// from `baseArrayLayer 0`, so for every attachment on a layer above zero the barrier moved layer 0
|
||||
// and the copy read layer N. The layer the transfer touched was never transitioned: it sat in
|
||||
// COLOR_ATTACHMENT_OPTIMAL (or DEPTH_STENCIL_ATTACHMENT_OPTIMAL) while being read as TRANSFER_SRC.
|
||||
//
|
||||
// That is undefined behaviour, not a guaranteed wrong pixel: a layout is a compression/tiling
|
||||
// promise, so a driver that stores both layouts identically returns the right bytes anyway. The
|
||||
// software lanes (lavapipe) are exactly such a driver, which is why this scenario is paired with a
|
||||
// validation-layer run - the layer names the mismatch outright
|
||||
// (VUID-vkCmdCopyImageToBuffer-srcImageLayout-00189, "srcImageLayout ... doesn't match the actual
|
||||
// current layout") where the pixels here cannot. On a tiler that really does re-tile per layout,
|
||||
// these are the reads that come back as garbage.
|
||||
//
|
||||
// The four cases below are the four transfer paths that take an attachment layer from GL:
|
||||
//
|
||||
// glReadPixels (colour) -> VulkanRenderer::ReadPixels
|
||||
// glBlitFramebuffer (colour) -> VulkanRenderer::BlitNamedFramebuffer
|
||||
// glReadPixels (GL_DEPTH_COMPONENT) -> VulkanRenderer::ReadDepthStencilImageToClient
|
||||
// glBlitFramebuffer (GL_DEPTH_BUFFER_BIT) -> VulkanRenderer::BlitNamedFramebuffer, depth leg
|
||||
//
|
||||
// Each one renders or clears INTO the non-zero layer first, so the image is genuinely sitting in
|
||||
// its attachment layout when the transfer starts - a scenario that only uploaded texels would
|
||||
// leave it in a transfer layout already and the mismatched barrier would be a no-op.
|
||||
//
|
||||
// Every case also asserts the layers it did not name still hold their own fill, so a backend that
|
||||
// "fixed" the miss by transferring the whole image passes neither half.
|
||||
//
|
||||
// DirectGLES is the control: it hands the same calls to the driver, so a failure on both backends
|
||||
// means the scenario is wrong and a failure on DirectVulkan alone means Magma is.
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kWidth = 8;
|
||||
constexpr int kHeight = 8;
|
||||
// Four layers with the subject at index 2: layers on both sides of it stay untouched, so
|
||||
// "moved the whole image" and "moved layer 0" are both distinguishable from correct.
|
||||
constexpr int kLayers = 4;
|
||||
constexpr int kSubjectLayer = 2;
|
||||
|
||||
// A value no correct read can produce, so "the backend wrote nothing" fails loudly.
|
||||
constexpr float kDepthPoison = 0.2f;
|
||||
|
||||
std::string Describe(const Rgba8& color) {
|
||||
return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " + std::to_string(color.b) +
|
||||
", " + std::to_string(color.a) + ")";
|
||||
}
|
||||
|
||||
// Per-layer fill, uniform within a layer: the defect is about WHICH layer is addressed, and
|
||||
// a value that also varied inside the layer would make the assertions depend on row order.
|
||||
Rgba8 LayerFill(int layer) {
|
||||
return {static_cast<GLubyte>(17 + layer * 30), static_cast<GLubyte>(200 - layer * 25),
|
||||
static_cast<GLubyte>(60 + layer * 40), 255};
|
||||
}
|
||||
|
||||
// What the draw paints - matches kFS below, and is deliberately none of the LayerFill
|
||||
// values so "the draw never landed" cannot read as a pass.
|
||||
constexpr Rgba8 kPaintedColor{26, 51, 204, 255};
|
||||
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(0.1, 0.2, 0.8, 1.0); }
|
||||
)";
|
||||
|
||||
void DrawFullViewportQuad(unsigned int program) {
|
||||
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
GLuint vao = 0, vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
}
|
||||
|
||||
class LayeredAttachmentBarrierScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
for (const GLuint fbo : m_fbos) {
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
}
|
||||
m_fbos.clear();
|
||||
for (const GLuint texture : m_textures) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
m_textures.clear();
|
||||
if (m_program != 0) {
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(m_program);
|
||||
m_program = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// An RGBA8 2D array with a different uniform colour per layer.
|
||||
GLuint MakeColorArray() {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kWidth, kHeight, kLayers);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
const std::vector<Rgba8> texels(static_cast<std::size_t>(kWidth) * kHeight, LayerFill(layer));
|
||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, kWidth, kHeight, 1, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, texels.data());
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A depth 2D array. No initial upload: depth arrays are filled by clearing through an
|
||||
// attachment, which is also the state the transfer paths have to cope with.
|
||||
GLuint MakeDepthArray() {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH_COMPONENT24, kWidth, kHeight, kLayers);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// One FBO naming `layer` of the given arrays. Depth is optional (0 = colour only).
|
||||
GLuint MakeLayerFbo(GLuint colorArray, GLuint depthArray, int layer) {
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
m_fbos.push_back(fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, colorArray, 0, layer);
|
||||
if (depthArray != 0) {
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthArray, 0, layer);
|
||||
}
|
||||
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "layer " << layer << " is not attachable";
|
||||
return fbo;
|
||||
}
|
||||
|
||||
// glReadPixels of one whole layer, through an FBO that names it.
|
||||
Rgba8 ReadLayer(GLuint colorArray, int layer) {
|
||||
const GLuint fbo = MakeLayerFbo(colorArray, 0, layer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
std::vector<Rgba8> pixels(static_cast<std::size_t>(kWidth) * kHeight, Rgba8{});
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
// The fill is uniform within a layer, so any disagreement between texels is itself
|
||||
// a failure - reported here rather than silently reduced to pixels[0].
|
||||
for (std::size_t i = 1; i < pixels.size(); ++i) {
|
||||
EXPECT_TRUE(pixels[i] == pixels[0])
|
||||
<< "layer " << layer << " is not uniform: texel 0 is " << Describe(pixels[0]) << ", texel "
|
||||
<< i << " is " << Describe(pixels[i]);
|
||||
}
|
||||
return pixels[0];
|
||||
}
|
||||
|
||||
// Every layer but `changed` still holds its own fill.
|
||||
void ExpectOtherLayersUntouched(GLuint colorArray, int changed, const char* what) {
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
if (layer == changed) continue;
|
||||
const Rgba8 actual = ReadLayer(colorArray, layer);
|
||||
EXPECT_TRUE(actual == LayerFill(layer))
|
||||
<< what << ": layer " << layer << " should still hold its fill but is " << Describe(actual)
|
||||
<< ", expected " << Describe(LayerFill(layer));
|
||||
}
|
||||
}
|
||||
|
||||
float ReadDepthAt(int x, int y) const {
|
||||
float depth = kDepthPoison;
|
||||
glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
|
||||
return depth;
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_textures;
|
||||
std::vector<GLuint> m_fbos;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
// glReadPixels straight off a layer that was just rendered to. The image is in
|
||||
// COLOR_ATTACHMENT_OPTIMAL when the readback barrier runs, so the barrier and the copy
|
||||
// disagreeing about the layer is a live layout mismatch, not a bookkeeping detail.
|
||||
TEST_F(LayeredAttachmentBarrierScenario, ReadPixelsOffRenderedNonZeroLayer) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint colorArray = MakeColorArray();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||
|
||||
const GLuint fbo = MakeLayerFbo(colorArray, 0, kSubjectLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
DrawFullViewportQuad(m_program);
|
||||
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
std::vector<Rgba8> pixels(static_cast<std::size_t>(kWidth) * kHeight, Rgba8{});
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
for (std::size_t i = 0; i < pixels.size(); ++i) {
|
||||
ASSERT_NEAR(pixels[i].r, kPaintedColor.r, 2)
|
||||
<< "texel " << i << " of the rendered layer is " << Describe(pixels[i]);
|
||||
ASSERT_NEAR(pixels[i].g, kPaintedColor.g, 2) << "texel " << i;
|
||||
ASSERT_NEAR(pixels[i].b, kPaintedColor.b, 2) << "texel " << i;
|
||||
}
|
||||
|
||||
ExpectOtherLayersUntouched(colorArray, kSubjectLayer, "readback off a rendered layer");
|
||||
}
|
||||
|
||||
// glBlitFramebuffer between two non-zero layers of two different arrays. Both endpoints are
|
||||
// above layer 0, so the source and destination barriers are each wrong on their own side.
|
||||
TEST_F(LayeredAttachmentBarrierScenario, BlitBetweenNonZeroColorLayers) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint sourceArray = MakeColorArray();
|
||||
const GLuint destinationArray = MakeColorArray();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||
|
||||
constexpr int kSourceLayer = 3;
|
||||
constexpr int kDestinationLayer = 1;
|
||||
|
||||
const GLuint sourceFbo = MakeLayerFbo(sourceArray, 0, kSourceLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, sourceFbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
DrawFullViewportQuad(m_program);
|
||||
|
||||
const GLuint destinationFbo = MakeLayerFbo(destinationArray, 0, kDestinationLayer);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFbo);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destinationFbo);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glBlitFramebuffer(0, 0, kWidth, kHeight, 0, 0, kWidth, kHeight, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const Rgba8 blitted = ReadLayer(destinationArray, kDestinationLayer);
|
||||
EXPECT_NEAR(blitted.r, kPaintedColor.r, 2) << "blit destination layer is " << Describe(blitted);
|
||||
EXPECT_NEAR(blitted.g, kPaintedColor.g, 2);
|
||||
EXPECT_NEAR(blitted.b, kPaintedColor.b, 2);
|
||||
|
||||
ExpectOtherLayersUntouched(destinationArray, kDestinationLayer, "colour blit destination");
|
||||
// The source layer was rendered, not blitted into, so it is checked separately.
|
||||
const Rgba8 source = ReadLayer(sourceArray, kSourceLayer);
|
||||
EXPECT_NEAR(source.r, kPaintedColor.r, 2) << "blit source layer is " << Describe(source);
|
||||
ExpectOtherLayersUntouched(sourceArray, kSourceLayer, "colour blit source");
|
||||
}
|
||||
|
||||
// The depth aspect of the same readback path: the depth image sits in
|
||||
// DEPTH_STENCIL_ATTACHMENT_OPTIMAL after the clear, and the copy names the attached layer.
|
||||
TEST_F(LayeredAttachmentBarrierScenario, ReadDepthOffClearedNonZeroLayer) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint colorArray = MakeColorArray();
|
||||
const GLuint depthArray = MakeDepthArray();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||
|
||||
const GLuint fbo = MakeLayerFbo(colorArray, depthArray, kSubjectLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.375);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
const float centre = ReadDepthAt(kWidth / 2, kHeight / 2);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(centre, 0.375f, 1.0f / 4096.0f)
|
||||
<< "glReadPixels(GL_DEPTH_COMPONENT) off layer " << kSubjectLayer << " returned " << centre
|
||||
<< (std::fabs(centre - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
|
||||
}
|
||||
|
||||
// The depth leg of the blit path, both endpoints above layer 0. Verified by reading the
|
||||
// destination's depth back, which is the same readback the case above pins - so a failure
|
||||
// here with that one passing is the blit, not the readback.
|
||||
TEST_F(LayeredAttachmentBarrierScenario, BlitDepthBetweenNonZeroLayers) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint sourceColor = MakeColorArray();
|
||||
const GLuint sourceDepth = MakeDepthArray();
|
||||
const GLuint destinationColor = MakeColorArray();
|
||||
const GLuint destinationDepth = MakeDepthArray();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||
|
||||
constexpr int kSourceLayer = 3;
|
||||
constexpr int kDestinationLayer = 1;
|
||||
|
||||
const GLuint sourceFbo = MakeLayerFbo(sourceColor, sourceDepth, kSourceLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, sourceFbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.625);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// A destination pre-cleared to something the blit must overwrite, so "the blit did
|
||||
// nothing" and "the blit landed" are different answers.
|
||||
const GLuint destinationFbo = MakeLayerFbo(destinationColor, destinationDepth, kDestinationLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destinationFbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.125);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destinationFbo);
|
||||
glBlitFramebuffer(0, 0, kWidth, kHeight, 0, 0, kWidth, kHeight, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destinationFbo);
|
||||
const float blitted = ReadDepthAt(kWidth / 2, kHeight / 2);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(blitted, 0.625f, 1.0f / 4096.0f)
|
||||
<< "depth blitted onto layer " << kDestinationLayer << " reads back as " << blitted
|
||||
<< (std::fabs(blitted - 0.125f) < 1e-3f ? " - the destination kept its own clear" : "");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,264 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/VertexArrayEnableDisableScenario.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
|
||||
//
|
||||
// KHR-GL45.direct_state_access.vertex_arrays_enable_disable_attributes, rebuilt.
|
||||
//
|
||||
// The case is small and does one unusual thing twice: it turns half of
|
||||
// GL_MAX_VERTEX_ATTRIBS attribute arrays on and the other half off with
|
||||
// glEnableVertexArrayAttrib / glDisableVertexArrayAttrib on a vertex array object
|
||||
// that is NOT bound (it binds the default one first, on purpose), draws one point
|
||||
// through a program that reads exactly the enabled half, and checks the sum those
|
||||
// arrays produced. Then it swaps which half is enabled, draws again through a
|
||||
// SECOND program, and checks the other sum.
|
||||
//
|
||||
// Both draws capture into ONE four-byte transform feedback buffer, allocated once
|
||||
// with immutable storage and read back with glMapBuffer - so anything that only
|
||||
// works on the first capture span through a buffer fails the second check while
|
||||
// leaving the first one green.
|
||||
//
|
||||
// It is reassembled here rather than shortened because every one of those details
|
||||
// is a candidate: the unbound-VAO enables, the two-program swap, the integer
|
||||
// attributes fetched with glVertexAttribIPointer at a stride wider than one
|
||||
// element, the second capture span, and the fact that the sums differ ONLY in
|
||||
// which arrays contributed (a fetch that ignored the enable state, or one that
|
||||
// read the wrong element, lands on a different number, not on garbage).
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
GLuint CompileShader(GLenum type, const std::string& source, std::string* log) {
|
||||
const GLuint shader = glCreateShader(type);
|
||||
const char* text = source.c_str();
|
||||
glShaderSource(shader, 1, &text, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
// Declares and sums the even (parity 0) or odd (parity 1) attributes only, with the
|
||||
// locations assigned by glBindAttribLocation rather than a layout qualifier - which is
|
||||
// what the CTS case does, and which makes the attribute set the program reads a link
|
||||
// property rather than a source one.
|
||||
GLuint BuildSumProgram(int parity, int attributeCount, std::string* log) {
|
||||
std::string declarations;
|
||||
std::string copies = " sum = 0;\n";
|
||||
for (int i = parity; i < attributeCount; i += 2) {
|
||||
declarations += "in int a_" + std::to_string(i) + ";\n";
|
||||
copies += " sum += a_" + std::to_string(i) + ";\n";
|
||||
}
|
||||
// `flat` where the CTS case has none: an integral shader output cannot be
|
||||
// interpolated, so a driver is within its rights to reject the unqualified form
|
||||
// even with no matching fragment input. The capture reads the same value either
|
||||
// way, and the qualifier keeps this scenario portable off llvmpipe.
|
||||
const std::string vertexSource = "#version 450\n\n" + declarations +
|
||||
"flat out int sum;\n\nvoid main()\n{\n" + copies + "}\n";
|
||||
const std::string fragmentSource = R"(#version 450
|
||||
|
||||
out vec4 color;
|
||||
|
||||
void main()
|
||||
{
|
||||
color = vec4(1.0);
|
||||
}
|
||||
)";
|
||||
const GLuint vertexShader = CompileShader(GL_VERTEX_SHADER, vertexSource, log);
|
||||
if (vertexShader == 0) return 0;
|
||||
const GLuint fragmentShader = CompileShader(GL_FRAGMENT_SHADER, fragmentSource, log);
|
||||
if (fragmentShader == 0) {
|
||||
glDeleteShader(vertexShader);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vertexShader);
|
||||
glAttachShader(program, fragmentShader);
|
||||
const char* varying = "sum";
|
||||
glTransformFeedbackVaryings(program, 1, &varying, GL_INTERLEAVED_ATTRIBS);
|
||||
for (int i = parity; i < attributeCount; i += 2) {
|
||||
const std::string name = "a_" + std::to_string(i);
|
||||
glBindAttribLocation(program, static_cast<GLuint>(i), name.c_str());
|
||||
}
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(vertexShader);
|
||||
glDeleteShader(fragmentShader);
|
||||
|
||||
GLint status = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
class VertexArrayEnableDisableScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &m_attributeCount);
|
||||
ASSERT_GE(m_attributeCount, 16);
|
||||
|
||||
std::string log;
|
||||
m_even = BuildSumProgram(0, m_attributeCount, &log);
|
||||
ASSERT_NE(m_even, 0u) << "even program failed to build: " << log;
|
||||
m_odd = BuildSumProgram(1, m_attributeCount, &log);
|
||||
ASSERT_NE(m_odd, 0u) << "odd program failed to build: " << log;
|
||||
|
||||
// One element per attribute, read as one vertex whose stride spans them all.
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
std::vector<GLint> reference(static_cast<std::size_t>(m_attributeCount));
|
||||
for (int i = 0; i < m_attributeCount; ++i) reference[static_cast<std::size_t>(i)] = i;
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(reference.size() * sizeof(GLint)),
|
||||
reference.data(), GL_STATIC_DRAW);
|
||||
for (int i = 0; i < m_attributeCount; ++i) {
|
||||
glVertexAttribIPointer(static_cast<GLuint>(i), 1, GL_INT,
|
||||
static_cast<GLsizei>(sizeof(GLint) * m_attributeCount),
|
||||
reinterpret_cast<const void*>(static_cast<std::size_t>(i) * sizeof(GLint)));
|
||||
}
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
|
||||
// Immutable storage, allocated once, read back with glMapBuffer - the capture
|
||||
// buffer is never respecified between the two spans.
|
||||
glGenBuffers(1, &m_xfb);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, m_xfb);
|
||||
glBufferStorage(GL_TRANSFORM_FEEDBACK_BUFFER, sizeof(GLint), nullptr, GL_MAP_READ_BIT);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_xfb);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "capture buffer setup";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
if (m_xfb != 0) glDeleteBuffers(1, &m_xfb);
|
||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_even != 0) glDeleteProgram(m_even);
|
||||
if (m_odd != 0) glDeleteProgram(m_odd);
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
// Enables one parity's arrays and disables the other's, THROUGH THE OBJECT NAME
|
||||
// while a different vertex array object is bound.
|
||||
void TurnOnAttributes(int enabledParity) {
|
||||
glBindVertexArray(0);
|
||||
for (int i = 0; i < m_attributeCount; ++i) {
|
||||
if (i % 2 == enabledParity % 2) {
|
||||
glEnableVertexArrayAttrib(m_vao, static_cast<GLuint>(i));
|
||||
} else {
|
||||
glDisableVertexArrayAttrib(m_vao, static_cast<GLuint>(i));
|
||||
}
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "attribute " << i << ", parity " << enabledParity;
|
||||
}
|
||||
glBindVertexArray(m_vao);
|
||||
}
|
||||
|
||||
int ExpectedSum(int parity) const {
|
||||
int sum = 0;
|
||||
for (int i = parity; i < m_attributeCount; i += 2) sum += i;
|
||||
return sum;
|
||||
}
|
||||
|
||||
// One capture span, read back the way the CTS case does.
|
||||
int DrawAndRead(int parity) {
|
||||
glUseProgram(parity == 0 ? m_even : m_odd);
|
||||
glBindVertexArray(m_vao);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
|
||||
const void* mapped = glMapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, GL_READ_ONLY);
|
||||
if (mapped == nullptr) {
|
||||
ADD_FAILURE() << "glMapBuffer returned null for parity " << parity;
|
||||
return -1;
|
||||
}
|
||||
GLint result = -1;
|
||||
std::memcpy(&result, mapped, sizeof(result));
|
||||
glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);
|
||||
return result;
|
||||
}
|
||||
|
||||
GLint m_attributeCount = 16;
|
||||
GLuint m_even = 0;
|
||||
GLuint m_odd = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
GLuint m_xfb = 0;
|
||||
};
|
||||
|
||||
// The case verbatim: even half on, draw, check; odd half on, draw, check.
|
||||
TEST_F(VertexArrayEnableDisableScenario, EitherHalfOfTheAttributesInTurn) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
TurnOnAttributes(0);
|
||||
EXPECT_EQ(DrawAndRead(0), ExpectedSum(0)) << "even attributes";
|
||||
|
||||
TurnOnAttributes(1);
|
||||
EXPECT_EQ(DrawAndRead(1), ExpectedSum(1)) << "odd attributes";
|
||||
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The first span on its own, so a failure of the case above can be read as "the second
|
||||
// span" rather than "the enables".
|
||||
TEST_F(VertexArrayEnableDisableScenario, TheEvenHalfAlone) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
TurnOnAttributes(0);
|
||||
EXPECT_EQ(DrawAndRead(0), ExpectedSum(0));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// And the odd half as the FIRST span, which separates "the odd program/arrays are
|
||||
// wrong" from "the second span is wrong".
|
||||
TEST_F(VertexArrayEnableDisableScenario, TheOddHalfAlone) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
TurnOnAttributes(1);
|
||||
EXPECT_EQ(DrawAndRead(1), ExpectedSum(1));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -139,16 +139,18 @@ void main() {
|
||||
// As CapturePoints, but through the baseInstance entry point, and on a capture buffer
|
||||
// of its own.
|
||||
//
|
||||
// Kept separate from CapturePoints rather than defaulting a parameter, for two
|
||||
// reasons. Every existing caller stays on the draw command that carries no
|
||||
// baseInstance at all, so the negative control is a DIFFERENT command rather than
|
||||
// the same one passed a zero. And baseInstance is the first thing here that needs
|
||||
// several captures in ONE test, which the shared helper cannot currently do: a
|
||||
// second capture into the same buffer object comes back empty on DirectVulkan
|
||||
// (respecifying a buffer that is bound to a transform-feedback binding point does
|
||||
// not reach that binding - reproduced with two plain CapturePoints calls, so it is
|
||||
// neither about baseInstance nor about this helper). A fresh buffer per capture
|
||||
// sidesteps it; without that, this scenario would be pinning that bug instead.
|
||||
// Kept separate from CapturePoints rather than defaulting a parameter, so that every
|
||||
// existing caller stays on the draw command that carries no baseInstance at all: the
|
||||
// negative control is then a DIFFERENT command rather than the same one passed a zero.
|
||||
//
|
||||
// The buffer per capture is a leftover. baseInstance was the first thing here that
|
||||
// needed several captures in ONE test, and at the time a second capture into the same
|
||||
// buffer object came back empty on DirectVulkan - respecifying a buffer whose bytes the
|
||||
// backend had handed the frontend a pointer into replaced the storage under that
|
||||
// pointer, so the capture wrote one store and the readback read another. That is fixed
|
||||
// and pinned by XfbCaptureBufferReuseScenario, which owns the shape now; a buffer per
|
||||
// capture is simply the cheapest thing that still isolates these three draws from each
|
||||
// other.
|
||||
std::vector<float> CaptureOwnBufferBaseInstance(GLuint program, int vertexCount, int instanceCount,
|
||||
GLuint baseInstance, bool useBaseInstanceCommand) {
|
||||
const std::size_t floats = static_cast<std::size_t>(vertexCount) * instanceCount * 16;
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ViewportArrayScenario.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
|
||||
//
|
||||
// Scenario - gl_ViewportIndex ACTUALLY ROUTES, AND THE PER-INDEX STATE IT SELECTS IS REAL.
|
||||
//
|
||||
// The state half of ARB_viewport_array is asserted in MG_Test/State/RenderStateTest.cpp, which
|
||||
// is a pure set/get exercise and would pass just as green against a backend that stores all 16
|
||||
// rectangles and rasterizes only the first. This file is the other half: every case here routes
|
||||
// primitives to a viewport OTHER than 0 and then looks at where the pixels landed.
|
||||
//
|
||||
// Three claims, one per case:
|
||||
// 1. gl_ViewportIndex selects the viewport RECTANGLE - a 4x4 grid of 32x32 viewports, one
|
||||
// geometry-shader invocation per cell, and every cell must hold its own index.
|
||||
// 2. gl_ViewportIndex selects the DEPTH RANGE - 16 one-pixel-wide viewports whose ranges are
|
||||
// (i/16, 1 - i/16), a quad at each end of clip space, and gl_FragCoord.z read back.
|
||||
// This is the claim that fails loudest against a single-viewport backend, because the
|
||||
// geometry is still in the right place while every depth comes back as viewport 0's.
|
||||
// 3. The per-index SCISSOR TEST ENABLE is honoured. Vulkan has no per-viewport scissor-test
|
||||
// toggle, so a disabled index has to be given the whole framebuffer as its rectangle; the
|
||||
// case draws the same primitive into the same index twice, once with the test off and once
|
||||
// with it on, and requires the two results to differ in the documented direction.
|
||||
//
|
||||
// Case 1 runs a second time against the DEFAULT framebuffer. MobileGL Y-flips (and pre-transform
|
||||
// rotates) the default framebuffer's rectangles and does not touch an FBO's, so a port that
|
||||
// applies the flip to viewport 0 and forgets the other fifteen renders a correct-looking FBO and
|
||||
// an upside-down window - the classic multi-viewport bug, and invisible to every FBO-only case.
|
||||
//
|
||||
// HONEST LIMIT OF THIS FILE. DirectGLES SKIPS every case: GLES has one viewport, one scissor
|
||||
// rectangle and no gl_ViewportIndex, so routing to index > 0 is an emulation feature that has
|
||||
// not been built (the Espryt half of KHR-GL43.viewport_array's rendering group is deliberately
|
||||
// still red). The skip is explicit rather than silent so a future emulation lands here as a
|
||||
// failing test and not as a test that was quietly never running. DirectVulkan additionally
|
||||
// skips when the device lacks the multiViewport feature - Vulkan then forbids a pipeline from
|
||||
// declaring more than one viewport at all, which is a device limit and not a MobileGL bug;
|
||||
// lavapipe (every CI lane) and both Mali/Adreno devices support it, so the cases do run where
|
||||
// it matters.
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kViewportCount = 16;
|
||||
constexpr int kGridSide = 4; // 4x4 grid of viewports
|
||||
constexpr int kCellSize = 32; // ... each 32x32
|
||||
constexpr int kSurfaceSide = kGridSide * kCellSize;
|
||||
constexpr GLint kUnwritten = -1;
|
||||
|
||||
// A geometry shader is the only stage GL 4.1 lets write gl_ViewportIndex, and
|
||||
// `invocations` runs it once per viewport off a single input point - the same shape
|
||||
// KHR-GL43.viewport_array.draw_to_single_layer_with_multiple_viewports uses.
|
||||
const char* const kVertexSource = R"(#version 410 core
|
||||
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
const char* const kGridGeometrySource = R"(#version 410 core
|
||||
layout(points, invocations = 16) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
flat out int gsIndex;
|
||||
void main() {
|
||||
gsIndex = gl_InvocationID;
|
||||
gl_ViewportIndex = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// One invocation, viewport chosen by a uniform: lets a case draw the SAME primitive into
|
||||
// the SAME index twice under two different scissor-enable states.
|
||||
const char* const kSingleGeometrySource = R"(#version 410 core
|
||||
layout(points, invocations = 1) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
uniform int uViewport;
|
||||
flat out int gsIndex;
|
||||
void main() {
|
||||
gsIndex = uViewport;
|
||||
gl_ViewportIndex = uViewport;
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kIntFragmentSource = R"(#version 410 core
|
||||
flat in int gsIndex;
|
||||
layout(location = 0) out int fragColor;
|
||||
void main() { fragColor = gsIndex; }
|
||||
)";
|
||||
|
||||
// Two quads, one at each end of clip space, so the fragment stage can report the depth
|
||||
// the viewport's range mapped them to. gl_FragCoord.z IS the post-range window depth, so
|
||||
// it reads back the per-viewport minDepth/maxDepth directly.
|
||||
const char* const kDepthGeometrySource = R"(#version 410 core
|
||||
layout(points, invocations = 16) in;
|
||||
layout(triangle_strip, max_vertices = 8) out;
|
||||
void main() {
|
||||
gl_ViewportIndex = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, -1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, -1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 0.0, -1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 0.0, -1.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
gl_Position = vec4(-1.0, 0.0, 1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 0.0, 1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 1.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kDepthFragmentSource = R"(#version 410 core
|
||||
layout(location = 0) out float fragColor;
|
||||
void main() { fragColor = gl_FragCoord.z; }
|
||||
)";
|
||||
|
||||
class ViewportArrayScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
if (Gl().BackendName() == "DirectGLES") {
|
||||
GTEST_SKIP() << "gl_ViewportIndex routing is not emulated on DirectGLES: GLES has one viewport "
|
||||
"and one scissor rectangle, so every index rasterizes as index 0. The indexed "
|
||||
"STATE is still asserted (MG_Test RenderStateTest); this is the deferred "
|
||||
"rendering half of KHR-GL43.viewport_array.";
|
||||
}
|
||||
|
||||
GLint maxViewports = 0;
|
||||
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
ASSERT_GE(maxViewports, kViewportCount) << "GL 4.3 core requires GL_MAX_VIEWPORTS >= 16";
|
||||
|
||||
m_program = BuildProgram(kGridGeometrySource, kIntFragmentSource);
|
||||
ASSERT_NE(m_program, 0u) << "grid program failed to build: " << m_buildLog;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
ResetViewportArrayState();
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "setup left a GL error behind";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ResetViewportArrayState();
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
// Every case starts from the same slate: this fixture shares its context with every
|
||||
// other scenario in the process, and a leftover per-index scissor enable is exactly
|
||||
// the kind of state that would make a later case pass or fail for the wrong reason.
|
||||
static void ResetViewportArrayState() {
|
||||
for (int i = 0; i < kViewportCount; ++i) {
|
||||
glDisablei(GL_SCISSOR_TEST, static_cast<GLuint>(i));
|
||||
}
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glViewport(0, 0, kSurfaceSide, kSurfaceSide);
|
||||
glScissor(0, 0, kSurfaceSide, kSurfaceSide);
|
||||
glDepthRange(0.0, 1.0);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
// The 4x4 grid: viewport y*4+x covers the cell whose lower-left corner is
|
||||
// (x*cellW, y*cellH), in GL's bottom-left-origin window coordinates. Parameterized on
|
||||
// the cell size because the default framebuffer this scenario also renders into is
|
||||
// deliberately non-square (HeadlessGL is 128x96, so a transposing bug cannot hide).
|
||||
static void SetupGridViewports(int cellW, int cellH) {
|
||||
std::vector<GLfloat> data(static_cast<size_t>(kViewportCount) * 4);
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
const size_t base = static_cast<size_t>(y * kGridSide + x) * 4;
|
||||
data[base + 0] = static_cast<GLfloat>(x * cellW);
|
||||
data[base + 1] = static_cast<GLfloat>(y * cellH);
|
||||
data[base + 2] = static_cast<GLfloat>(cellW);
|
||||
data[base + 3] = static_cast<GLfloat>(cellH);
|
||||
}
|
||||
}
|
||||
glViewportArrayv(0, kViewportCount, data.data());
|
||||
}
|
||||
|
||||
GLuint BuildProgram(const char* geometrySource, const char* fragmentSource) {
|
||||
const GLuint vs = CompileStage(GL_VERTEX_SHADER, kVertexSource);
|
||||
if (vs == 0) return 0;
|
||||
const GLuint gs = CompileStage(GL_GEOMETRY_SHADER, geometrySource);
|
||||
if (gs == 0) {
|
||||
glDeleteShader(vs);
|
||||
return 0;
|
||||
}
|
||||
const GLuint fs = CompileStage(GL_FRAGMENT_SHADER, fragmentSource);
|
||||
if (fs == 0) {
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(gs);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, gs);
|
||||
glAttachShader(program, fs);
|
||||
glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(gs);
|
||||
glDeleteShader(fs);
|
||||
if (!linked) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
m_buildLog = log.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint CompileStage(GLenum stage, const char* source) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled) return shader;
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
m_buildLog = log.data();
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// An R32I colour target, pre-filled with kUnwritten so "nothing was drawn here" is
|
||||
// distinguishable from "index 0 was drawn here".
|
||||
struct IntTarget {
|
||||
GLuint fbo = 0;
|
||||
GLuint texture = 0;
|
||||
};
|
||||
|
||||
// The "nothing drawn here" value is UPLOADED, not cleared: the CTS fills its R32I
|
||||
// targets the same way (fillTexture), and an upload cannot be confused with a clear
|
||||
// that a backend defers, reorders or drops - which is exactly the ambiguity a case
|
||||
// asserting "this cell must be untouched" cannot afford.
|
||||
static void FillIntTarget(const IntTarget& target, int width, int height) {
|
||||
const std::vector<GLint> unwritten(static_cast<size_t>(width) * height, kUnwritten);
|
||||
glBindTexture(GL_TEXTURE_2D, target.texture);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RED_INTEGER, GL_INT, unwritten.data());
|
||||
}
|
||||
|
||||
static IntTarget MakeIntTarget(int width, int height) {
|
||||
IntTarget target;
|
||||
glGenTextures(1, &target.texture);
|
||||
glBindTexture(GL_TEXTURE_2D, target.texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32I, width, height, 0, GL_RED_INTEGER, GL_INT, nullptr);
|
||||
glGenFramebuffers(1, &target.fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
|
||||
FillIntTarget(target, width, height);
|
||||
return target;
|
||||
}
|
||||
|
||||
static void DestroyIntTarget(IntTarget& target) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (target.fbo != 0) glDeleteFramebuffers(1, &target.fbo);
|
||||
if (target.texture != 0) glDeleteTextures(1, &target.texture);
|
||||
}
|
||||
|
||||
static std::vector<GLint> ReadInts(int width, int height) {
|
||||
std::vector<GLint> pixels(static_cast<size_t>(width) * height, 0);
|
||||
glReadPixels(0, 0, width, height, GL_RED_INTEGER, GL_INT, pixels.data());
|
||||
return pixels;
|
||||
}
|
||||
|
||||
// The centre of grid cell (x, y), in the bottom-left-origin coordinates glReadPixels
|
||||
// returns. Sampling the centre rather than a corner keeps the assertion about WHICH
|
||||
// viewport was selected rather than about edge rounding.
|
||||
static GLint CellCentre(const std::vector<GLint>& pixels, int stride, int x, int y) {
|
||||
const int px = x * kCellSize + kCellSize / 2;
|
||||
const int py = y * kCellSize + kCellSize / 2;
|
||||
return pixels[static_cast<size_t>(py) * stride + px];
|
||||
}
|
||||
|
||||
std::string m_buildLog;
|
||||
GLuint m_program = 0;
|
||||
GLuint m_vao = 0;
|
||||
};
|
||||
|
||||
// --- 1. the viewport rectangle -------------------------------------------------------
|
||||
|
||||
TEST_F(ViewportArrayScenario, EachViewportIndexRasterizesIntoItsOwnRectangle) {
|
||||
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||
SetupGridViewports(kCellSize, kCellSize);
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
const GLint expected = y * kGridSide + x;
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), expected)
|
||||
<< "cell (" << x << ", " << y << ") should hold viewport index " << expected
|
||||
<< "; a single-viewport backend paints the whole image with 15 (the last invocation)";
|
||||
}
|
||||
}
|
||||
DestroyIntTarget(target);
|
||||
}
|
||||
|
||||
// The same claim against the DEFAULT framebuffer, where MobileGL applies its Y-flip and
|
||||
// pre-transform rotation. Index 0 alone getting the mapping is the classic bug.
|
||||
TEST_F(ViewportArrayScenario, TheDefaultFramebufferAppliesTheSameFlipToEveryViewport) {
|
||||
const int surfaceW = Gl().Width();
|
||||
const int surfaceH = Gl().Height();
|
||||
ASSERT_GE(surfaceW, kGridSide);
|
||||
ASSERT_GE(surfaceH, kGridSide);
|
||||
const int cellW = surfaceW / kGridSide;
|
||||
const int cellH = surfaceH / kGridSide;
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
// Paint a value no viewport index can produce, so an unwritten cell is obvious.
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
// The default framebuffer is 8-bit RGBA, so the index travels as a colour: cell i is
|
||||
// painted with red = i * 16, which is exact in 8 bits for i in [0, 16).
|
||||
const char* const kColorFragmentSource = R"(#version 410 core
|
||||
flat in int gsIndex;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
const GLuint colorProgram = BuildProgram(kGridGeometrySource, kColorFragmentSource);
|
||||
ASSERT_NE(colorProgram, 0u) << "colour program failed to build: " << m_buildLog;
|
||||
|
||||
SetupGridViewports(cellW, cellH);
|
||||
glUseProgram(colorProgram);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
std::vector<unsigned char> pixels(static_cast<size_t>(surfaceW) * surfaceH * 4, 0);
|
||||
glReadPixels(0, 0, surfaceW, surfaceH, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
const int px = x * cellW + cellW / 2;
|
||||
const int py = y * cellH + cellH / 2;
|
||||
const int red = pixels[(static_cast<size_t>(py) * surfaceW + px) * 4];
|
||||
const int expected = (y * kGridSide + x) * 16;
|
||||
// One LSB of slack for an 8-bit round trip; the values are 16 apart, so this
|
||||
// cannot confuse two neighbouring indices.
|
||||
EXPECT_LE(std::abs(red - expected), 1)
|
||||
<< "default-framebuffer cell (" << x << ", " << y << ") holds red=" << red << ", expected "
|
||||
<< expected << ". A vertically mirrored grid means the Y-flip was applied to viewport 0 "
|
||||
<< "only";
|
||||
}
|
||||
}
|
||||
glDeleteProgram(colorProgram);
|
||||
}
|
||||
|
||||
// --- 2. the depth range --------------------------------------------------------------
|
||||
|
||||
TEST_F(ViewportArrayScenario, EachViewportIndexUsesItsOwnDepthRange) {
|
||||
// 16 columns one pixel wide and two rows tall: row 0 gets the near-plane quad, row 1
|
||||
// the far-plane one, so both ends of viewport i's range land in the same column.
|
||||
constexpr int kWidth = kViewportCount;
|
||||
constexpr int kHeight = 2;
|
||||
|
||||
GLuint texture = 0;
|
||||
GLuint fbo = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, kWidth, kHeight, 0, GL_RED, GL_FLOAT, nullptr);
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
const GLfloat clearValue[4] = {-1.0f, 0.0f, 0.0f, 0.0f};
|
||||
glClearBufferfv(GL_COLOR, 0, clearValue);
|
||||
|
||||
std::vector<GLfloat> viewports(static_cast<size_t>(kViewportCount) * 4);
|
||||
std::vector<GLdouble> ranges(static_cast<size_t>(kViewportCount) * 2);
|
||||
for (int i = 0; i < kViewportCount; ++i) {
|
||||
viewports[static_cast<size_t>(i) * 4 + 0] = static_cast<GLfloat>(i);
|
||||
viewports[static_cast<size_t>(i) * 4 + 1] = 0.0f;
|
||||
viewports[static_cast<size_t>(i) * 4 + 2] = 1.0f;
|
||||
viewports[static_cast<size_t>(i) * 4 + 3] = 2.0f;
|
||||
ranges[static_cast<size_t>(i) * 2 + 0] = static_cast<GLdouble>(i) / 16.0;
|
||||
ranges[static_cast<size_t>(i) * 2 + 1] = 1.0 - static_cast<GLdouble>(i) / 16.0;
|
||||
}
|
||||
glViewportArrayv(0, kViewportCount, viewports.data());
|
||||
glDepthRangeArrayv(0, kViewportCount, ranges.data());
|
||||
|
||||
const GLuint depthProgram = BuildProgram(kDepthGeometrySource, kDepthFragmentSource);
|
||||
ASSERT_NE(depthProgram, 0u) << "depth program failed to build: " << m_buildLog;
|
||||
glUseProgram(depthProgram);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
std::vector<GLfloat> pixels(static_cast<size_t>(kWidth) * kHeight, 0.0f);
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RED, GL_FLOAT, pixels.data());
|
||||
for (int i = 0; i < kViewportCount; ++i) {
|
||||
const float near = static_cast<float>(i) / 16.0f;
|
||||
const float far = 1.0f - static_cast<float>(i) / 16.0f;
|
||||
// The tolerance covers depth-buffer-free rasterization of gl_FragCoord.z on a
|
||||
// software rasterizer; the per-index values are 1/16 apart, so it cannot let a
|
||||
// neighbouring viewport's range through, and viewport 0's range (0, 1) differs
|
||||
// from every other index by at least 1/16.
|
||||
EXPECT_NEAR(pixels[i], near, 1.0e-3f)
|
||||
<< "viewport " << i << " near-plane depth; got viewport 0's range if this is 0";
|
||||
EXPECT_NEAR(pixels[static_cast<size_t>(kWidth) + i], far, 1.0e-3f)
|
||||
<< "viewport " << i << " far-plane depth; got viewport 0's range if this is 1";
|
||||
}
|
||||
|
||||
glDeleteProgram(depthProgram);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
|
||||
// --- 3. the per-index scissor-test enable --------------------------------------------
|
||||
|
||||
TEST_F(ViewportArrayScenario, AnIndexedScissorEnableClipsOnlyThatIndex) {
|
||||
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||
|
||||
// One full-size viewport per index so the scissor rectangle is the ONLY thing that
|
||||
// can shrink the quad - the same separation KHR-GL43.viewport_array.scissor uses.
|
||||
glViewport(0, 0, kSurfaceSide, kSurfaceSide);
|
||||
std::vector<GLint> boxes(static_cast<size_t>(kViewportCount) * 4);
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
const size_t base = static_cast<size_t>(y * kGridSide + x) * 4;
|
||||
boxes[base + 0] = x * kCellSize;
|
||||
boxes[base + 1] = y * kCellSize;
|
||||
boxes[base + 2] = kCellSize;
|
||||
boxes[base + 3] = kCellSize;
|
||||
}
|
||||
}
|
||||
glScissorArrayv(0, kViewportCount, boxes.data());
|
||||
|
||||
const GLuint singleProgram = BuildProgram(kSingleGeometrySource, kIntFragmentSource);
|
||||
ASSERT_NE(singleProgram, 0u) << "single-viewport program failed to build: " << m_buildLog;
|
||||
glUseProgram(singleProgram);
|
||||
glBindVertexArray(m_vao);
|
||||
const GLint uViewport = glGetUniformLocation(singleProgram, "uViewport");
|
||||
ASSERT_NE(uViewport, -1);
|
||||
|
||||
constexpr GLint kProbeIndex = 6; // grid cell (2, 1)
|
||||
constexpr int kProbeX = kProbeIndex % kGridSide;
|
||||
constexpr int kProbeY = kProbeIndex / kGridSide;
|
||||
|
||||
// (a) scissor test ENABLED for this index: the quad is clipped to its 32x32 box.
|
||||
glUniform1i(uViewport, kProbeIndex);
|
||||
glEnablei(GL_SCISSOR_TEST, kProbeIndex);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
{
|
||||
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, kProbeX, kProbeY), kProbeIndex)
|
||||
<< "the scissored index must still paint inside its own box";
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
if (x == kProbeX && y == kProbeY) continue;
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kUnwritten)
|
||||
<< "cell (" << x << ", " << y << ") is outside scissor rectangle " << kProbeIndex
|
||||
<< " and must be untouched";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (b) scissor test DISABLED for the same index, everything else identical: with no
|
||||
// per-viewport toggle in Vulkan this is the case that needs the disabled index to be
|
||||
// given the full framebuffer rectangle, and it is exactly where "leave the last
|
||||
// rectangle bound" would show up as a still-clipped quad.
|
||||
FillIntTarget(target, kSurfaceSide, kSurfaceSide);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||
glDisablei(GL_SCISSOR_TEST, kProbeIndex);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
{
|
||||
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kProbeIndex)
|
||||
<< "with the scissor test off for index " << kProbeIndex
|
||||
<< ", its full-viewport quad must cover cell (" << x << ", " << y << ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glDeleteProgram(singleProgram);
|
||||
DestroyIntTarget(target);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,317 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/XfbCaptureBufferReuseScenario.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
|
||||
//
|
||||
// ONE capture buffer, SEVERAL capture spans - the shape most KHR-GL4x cases that
|
||||
// use transform feedback as a readback channel are built on. They allocate the
|
||||
// capture buffer once in a setup step and then run span after span through it,
|
||||
// so a defect that only shows from the second span onwards fails the whole case
|
||||
// while the first span (and every single-span scenario in this suite) stays
|
||||
// green. The first thing checked here is therefore not the capture itself but
|
||||
// that the bytes the capture wrote are the bytes the readback reads.
|
||||
//
|
||||
// Two ways of reusing the buffer, because they exercise different machinery:
|
||||
//
|
||||
// * respecified between spans (glBufferData while the buffer is still bound to
|
||||
// the transform-feedback binding point), which is what a test helper that
|
||||
// poisons its capture buffer before every span does;
|
||||
// * allocated ONCE with immutable storage and never touched again, which is
|
||||
// what KHR-GL45.direct_state_access.vertex_arrays_enable_disable_attributes
|
||||
// does - glBufferStorage(4 bytes) in its setup, then two draws.
|
||||
//
|
||||
// The negative control (a fresh buffer object per span) is a separate case
|
||||
// rather than a parameter: it is the configuration that already worked, so it
|
||||
// has to keep working for the others to mean anything.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr float kPoison = -1234.0f;
|
||||
// One vec4 per point, one point per draw.
|
||||
constexpr std::size_t kCaptureFloats = 4;
|
||||
constexpr std::size_t kCaptureBytes = kCaptureFloats * sizeof(float);
|
||||
|
||||
GLuint CompileShader(GLenum type, const std::string& source, std::string* log) {
|
||||
const GLuint shader = glCreateShader(type);
|
||||
const char* text = source.c_str();
|
||||
glShaderSource(shader, 1, &text, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
// Vertex-only capture program: whatever the draw fetched at location 0 comes
|
||||
// straight back out through the capture. Runs under GL_RASTERIZER_DISCARD, so
|
||||
// there is no fragment stage.
|
||||
GLuint BuildCaptureProgram(std::string* log) {
|
||||
const std::string vertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec4 vs_in_value;
|
||||
out vec4 vs_out_value;
|
||||
void main() {
|
||||
vs_out_value = vs_in_value;
|
||||
}
|
||||
)";
|
||||
const GLuint vertexShader = CompileShader(GL_VERTEX_SHADER, vertexSource, log);
|
||||
if (vertexShader == 0) return 0;
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vertexShader);
|
||||
const char* varying = "vs_out_value";
|
||||
glTransformFeedbackVaryings(program, 1, &varying, GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(vertexShader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
class XfbCaptureBufferReuseScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
std::string log;
|
||||
m_program = BuildCaptureProgram(&log);
|
||||
ASSERT_NE(m_program, 0u) << "capture program failed to build: " << log;
|
||||
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, kCaptureBytes, nullptr, GL_DYNAMIC_DRAW);
|
||||
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glBindVertexArray(0);
|
||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
glUseProgram(0);
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
// The vertex the next span will fetch and capture.
|
||||
void SetVertex(float value) {
|
||||
const float data[kCaptureFloats] = {value, value + 1.0f, value + 2.0f, value + 3.0f};
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, 0, kCaptureBytes, data);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
// One capture span over the buffer currently bound to capture point 0.
|
||||
void RunSpan() {
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(m_vao);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
static ::testing::AssertionResult CapturedIs(const float* data, float value) {
|
||||
for (std::size_t i = 0; i < kCaptureFloats; ++i) {
|
||||
const float expected = value + static_cast<float>(i);
|
||||
const float got = data[i];
|
||||
// isfinite first: every ordered comparison against a NaN is false, so a
|
||||
// pair of one-sided range tests REPORTS SUCCESS for uninitialised
|
||||
// storage that happens to read as NaN - which is exactly the failure
|
||||
// these scenarios exist to catch.
|
||||
if (!std::isfinite(got) || std::fabs(got - expected) > 0.01f) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "component " << i << " is " << got << ", expected " << expected
|
||||
<< (got == kPoison ? " (the capture never reached these bytes)" : "");
|
||||
}
|
||||
}
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
|
||||
GLuint m_program = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
};
|
||||
|
||||
// The negative control: one buffer object per span. This is the configuration
|
||||
// every multi-span scenario in this suite works around the others with, so it
|
||||
// has to hold or nothing below is interpretable.
|
||||
TEST_F(XfbCaptureBufferReuseScenario, EverySpanIntoABufferObjectOfItsOwn) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
for (int span = 0; span < 3; ++span) {
|
||||
const float value = 10.0f * static_cast<float>(span + 1);
|
||||
const std::vector<float> poison(kCaptureFloats, kPoison);
|
||||
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kCaptureBytes, poison.data(), GL_DYNAMIC_DRAW);
|
||||
|
||||
SetVertex(value);
|
||||
RunSpan();
|
||||
|
||||
float readback[kCaptureFloats] = {kPoison, kPoison, kPoison, kPoison};
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, kCaptureBytes, readback);
|
||||
EXPECT_TRUE(CapturedIs(readback, value)) << "span " << span;
|
||||
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
}
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The same three spans through ONE buffer object, respecified before each of
|
||||
// them WHILE it is bound to capture point 0 - a helper poisoning its capture
|
||||
// buffer, which is what makes "captured nothing" legible in the first place.
|
||||
//
|
||||
// A respecification is free to replace the storage underneath (that is what
|
||||
// orphaning is), and on a buffer whose bytes the backend has already handed
|
||||
// the frontend a pointer into, the replacement has to reach that pointer too.
|
||||
// It did not: the capture wrote the new storage and the readback kept reading
|
||||
// the old one, so every span after the first came back poison.
|
||||
TEST_F(XfbCaptureBufferReuseScenario, EverySpanIntoOneRespecifiedBufferObject) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
|
||||
for (int span = 0; span < 3; ++span) {
|
||||
const float value = 10.0f * static_cast<float>(span + 1);
|
||||
const std::vector<float> poison(kCaptureFloats, kPoison);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kCaptureBytes, poison.data(), GL_DYNAMIC_DRAW);
|
||||
|
||||
SetVertex(value);
|
||||
RunSpan();
|
||||
|
||||
float readback[kCaptureFloats] = {kPoison, kPoison, kPoison, kPoison};
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, kCaptureBytes, readback);
|
||||
EXPECT_TRUE(CapturedIs(readback, value)) << "span " << span;
|
||||
}
|
||||
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// A respecification that CHANGES the size, which is the case a re-pointing
|
||||
// that only handled same-size storage would still get wrong - and, before the
|
||||
// fix, the case that wrote the new (larger) contents through a mapping sized
|
||||
// for the old ones.
|
||||
TEST_F(XfbCaptureBufferReuseScenario, ARespecificationMayChangeTheCaptureBufferSize) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
|
||||
// Sized for one point, then for four, then back down to one.
|
||||
const std::size_t pointCapacity[] = {1, 4, 1};
|
||||
for (int span = 0; span < 3; ++span) {
|
||||
const float value = 10.0f * static_cast<float>(span + 1);
|
||||
const std::size_t floats = kCaptureFloats * pointCapacity[span];
|
||||
const std::vector<float> poison(floats, kPoison);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(floats * sizeof(float)),
|
||||
poison.data(), GL_DYNAMIC_DRAW);
|
||||
|
||||
SetVertex(value);
|
||||
RunSpan();
|
||||
|
||||
std::vector<float> readback(floats, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(floats * sizeof(float)), readback.data());
|
||||
EXPECT_TRUE(CapturedIs(readback.data(), value)) << "span " << span;
|
||||
// The bytes past the one point the draw produced must still be the
|
||||
// poison the respecification put there, not whatever the previous
|
||||
// (differently sized) storage held.
|
||||
for (std::size_t i = kCaptureFloats; i < floats; ++i) {
|
||||
EXPECT_FLOAT_EQ(readback[i], kPoison) << "span " << span << " float " << i;
|
||||
}
|
||||
}
|
||||
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The KHR-GL45.direct_state_access.vertex_arrays_enable_disable_attributes
|
||||
// shape: the capture buffer gets IMMUTABLE storage once, in a setup step, and
|
||||
// is never respecified - two spans simply run through it, each read back with
|
||||
// glMapBuffer. Nothing here may depend on a respecification to reset the
|
||||
// capture: glBeginTransformFeedback does that on its own.
|
||||
TEST_F(XfbCaptureBufferReuseScenario, EverySpanIntoOneImmutableStorageBuffer) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffer);
|
||||
// Poisoned at creation - the storage is immutable, so this is the only chance to
|
||||
// put a recognisable value there, and without it a span that captured nothing
|
||||
// would be indistinguishable from one that captured the right thing whenever the
|
||||
// untouched bytes happened to read back as the expected number.
|
||||
const std::vector<float> poison(kCaptureFloats, kPoison);
|
||||
glBufferStorage(GL_TRANSFORM_FEEDBACK_BUFFER, kCaptureBytes, poison.data(), GL_MAP_READ_BIT);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glBufferStorage on the capture buffer";
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
|
||||
for (int span = 0; span < 3; ++span) {
|
||||
const float value = 10.0f * static_cast<float>(span + 1);
|
||||
SetVertex(value);
|
||||
RunSpan();
|
||||
|
||||
const void* mapped = glMapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, GL_READ_ONLY);
|
||||
ASSERT_NE(mapped, nullptr) << "span " << span << ": glMapBuffer returned null";
|
||||
float readback[kCaptureFloats] = {kPoison, kPoison, kPoison, kPoison};
|
||||
std::memcpy(readback, mapped, kCaptureBytes);
|
||||
glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);
|
||||
EXPECT_TRUE(CapturedIs(readback, value)) << "span " << span;
|
||||
}
|
||||
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -75,10 +75,42 @@ namespace MobileGL::MG_State::GLState {
|
||||
NotifySubData(offset, size);
|
||||
}
|
||||
|
||||
void BufferObject::Respecify(SizeT size, const void* data) {
|
||||
ReleaseMemory();
|
||||
// A (re)definition of the store is about to write `size` bytes through Bytes().
|
||||
// Sizing the shadow is all that takes for a shadow-backed buffer. A buffer whose
|
||||
// bytes were adopted into backend GPU memory has to give the adoption back first,
|
||||
// because the mapping it holds describes exactly the OLD store: writing the new
|
||||
// contents through it runs past its end the moment the store grows, and a backend
|
||||
// that replaces the storage for the new store - which is what an orphaning
|
||||
// respecification asks for - would leave that mapping, and therefore every later
|
||||
// read of this buffer, addressing storage nothing writes to any more. That was the
|
||||
// transform feedback capture that wrote one buffer while the readback read another.
|
||||
//
|
||||
// Given back rather than renewed here, deliberately. Renewing in place would mean
|
||||
// memcpying the new contents into storage that submitted-but-unretired draws may
|
||||
// still be reading, which is precisely what the orphaning idiom exists to avoid;
|
||||
// avoiding THAT would mean either stalling on a fence in the middle of a frame or
|
||||
// teaching the persistent-map op to orphan, and the op must never orphan for the
|
||||
// other kind of caller (an application-held GL_MAP_PERSISTENT_BIT mapping, whose
|
||||
// pointer has to stay valid for the buffer's whole life). Handing the store back to
|
||||
// the CPU shadow needs none of that: the backend's ordinary respecification path
|
||||
// then does the busy-tracking and the conditional orphan it has always done, and the
|
||||
// next binding that wants GPU residency takes a fresh mapping of the new store.
|
||||
void BufferObject::RedefineStorage(SizeT size) {
|
||||
if (m_resource.IsGpuResident()) {
|
||||
m_resource.ReleasePersistentMap();
|
||||
// Whatever a shader or a capture wrote is in the store being replaced, so
|
||||
// there is nothing left to reconcile - and leaving the flag set would make
|
||||
// the next read of this buffer wait for GPU work on behalf of bytes the
|
||||
// application has just thrown away.
|
||||
m_gpuWritePending = false;
|
||||
}
|
||||
m_size = size;
|
||||
m_resource.ResizeShadow(size);
|
||||
}
|
||||
|
||||
void BufferObject::Respecify(SizeT size, const void* data) {
|
||||
ReleaseMemory();
|
||||
RedefineStorage(size);
|
||||
if (data && size > 0) {
|
||||
Memcpy(m_resource.Bytes(), data, size);
|
||||
}
|
||||
@@ -96,8 +128,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
void BufferObject::AllocateImmutableStorage(SizeT size, const void* data, GLbitfield storageFlags) {
|
||||
ReleaseMemory();
|
||||
m_size = size;
|
||||
m_resource.ResizeShadow(size);
|
||||
RedefineStorage(size);
|
||||
if (data) {
|
||||
Memcpy(m_resource.Bytes(), data, size);
|
||||
} else if (size > 0) {
|
||||
|
||||
@@ -205,6 +205,9 @@ namespace MobileGL {
|
||||
void SetBackendResource(SharedPtr<BackendBufferResource> resource);
|
||||
|
||||
private:
|
||||
// Sizes the store for a (re)definition, renewing an adopted GPU-resident
|
||||
// mapping across it. See the definition for why the renewal is not optional.
|
||||
void RedefineStorage(SizeT size);
|
||||
void NotifyRespecify();
|
||||
void NotifySubData(SizeT offset, SizeT size);
|
||||
void NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess);
|
||||
|
||||
@@ -70,6 +70,15 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_shadow->shrink_to_fit();
|
||||
}
|
||||
|
||||
// Give the adoption back: the bytes resolve against the shadow again (which
|
||||
// the caller must (re)size, it was released on adoption). Used when the store
|
||||
// itself is redefined - the mapping describes exactly the store that is going
|
||||
// away, so it may neither be written through nor kept. It is NOT a general
|
||||
// "unmap": a persistent map the application holds outlives every unmap by
|
||||
// definition, and the calls that could redefine such a buffer's store are
|
||||
// errors the frontend refuses before reaching here.
|
||||
void ReleasePersistentMap() { m_gpuMapped = nullptr; }
|
||||
|
||||
// Backend GPU resource, owned here in both modes.
|
||||
const SharedPtr<BackendBufferResource>& Backend() const { return m_backend; }
|
||||
void SetBackend(SharedPtr<BackendBufferResource> backend) { m_backend = std::move(backend); }
|
||||
|
||||
@@ -196,7 +196,7 @@ namespace MobileGL::MG_State {
|
||||
// (which expands to nothing outside debug builds).
|
||||
void GLContext::SetCurrentVertexAttributeFloat(Uint index, const Array<Float, 4>& value) {
|
||||
if (index >= m_currentVertexAttributes.size()) {
|
||||
MGLOG_E("SetCurrentVertexAttributeFloat: index %u is out of range", index);
|
||||
MGLOG_E_ONCE("SetCurrentVertexAttributeFloat: index %u is out of range", index);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ namespace MobileGL::MG_State {
|
||||
|
||||
void GLContext::SetCurrentVertexAttributeInt(Uint index, const Array<Int32, 4>& value) {
|
||||
if (index >= m_currentVertexAttributes.size()) {
|
||||
MGLOG_E("SetCurrentVertexAttributeInt: index %u is out of range", index);
|
||||
MGLOG_E_ONCE("SetCurrentVertexAttributeInt: index %u is out of range", index);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ namespace MobileGL::MG_State {
|
||||
|
||||
void GLContext::SetCurrentVertexAttributeUint(Uint index, const Array<Uint32, 4>& value) {
|
||||
if (index >= m_currentVertexAttributes.size()) {
|
||||
MGLOG_E("SetCurrentVertexAttributeUint: index %u is out of range", index);
|
||||
MGLOG_E_ONCE("SetCurrentVertexAttributeUint: index %u is out of range", index);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ namespace MobileGL::MG_State {
|
||||
const CurrentVertexAttributeValue& GLContext::GetCurrentVertexAttribute(Uint index) const {
|
||||
static const CurrentVertexAttributeValue defaultValue{};
|
||||
if (index >= m_currentVertexAttributes.size()) {
|
||||
MGLOG_E("GetCurrentVertexAttribute: index %u is out of range", index);
|
||||
MGLOG_E_ONCE("GetCurrentVertexAttribute: index %u is out of range", index);
|
||||
return defaultValue;
|
||||
}
|
||||
return m_currentVertexAttributes[index];
|
||||
@@ -712,10 +712,18 @@ namespace MobileGL::MG_State {
|
||||
m_renderState.SetViewport(viewport);
|
||||
}
|
||||
|
||||
const IntVec4& GLContext::GetViewport() const {
|
||||
IntVec4 GLContext::GetViewport() const {
|
||||
return m_renderState.GetViewport();
|
||||
}
|
||||
|
||||
void GLContext::SetViewportIndexed(Uint index, FloatVec4 viewport) {
|
||||
m_renderState.SetViewportIndexed(index, viewport);
|
||||
}
|
||||
|
||||
const FloatVec4& GLContext::GetViewportIndexed(Uint index) const {
|
||||
return m_renderState.GetViewportIndexed(index);
|
||||
}
|
||||
|
||||
void GLContext::SetLineWidth(Float width) {
|
||||
m_renderState.SetLineWidth(width);
|
||||
}
|
||||
@@ -953,6 +961,14 @@ namespace MobileGL::MG_State {
|
||||
return m_renderState.GetDepthRange();
|
||||
}
|
||||
|
||||
void GLContext::SetDepthRangeIndexed(Uint index, FloatVec2 range) {
|
||||
m_renderState.SetDepthRangeIndexed(index, range);
|
||||
}
|
||||
|
||||
const FloatVec2& GLContext::GetDepthRangeIndexed(Uint index) const {
|
||||
return m_renderState.GetDepthRangeIndexed(index);
|
||||
}
|
||||
|
||||
void GLContext::SetSampleCoverage(Float value, Bool invert) {
|
||||
m_renderState.SetSampleCoverage(value, invert);
|
||||
}
|
||||
@@ -1017,6 +1033,14 @@ namespace MobileGL::MG_State {
|
||||
return m_renderState.GetScissorBox();
|
||||
}
|
||||
|
||||
void GLContext::SetScissorBoxIndexed(Uint index, IntVec4 box) {
|
||||
m_renderState.SetScissorBoxIndexed(index, box);
|
||||
}
|
||||
|
||||
const IntVec4& GLContext::GetScissorBoxIndexed(Uint index) const {
|
||||
return m_renderState.GetScissorBoxIndexed(index);
|
||||
}
|
||||
|
||||
// Framebuffer
|
||||
void GLContext::GenFramebufferNames(Uint number, Vector<Uint>& framebuffers) {
|
||||
m_framebufferState.GenerateNames(number, framebuffers);
|
||||
|
||||
@@ -198,8 +198,10 @@ namespace MobileGL {
|
||||
// Only the pipeline-relevant subset - see RenderState::m_pipelineStateVersion.
|
||||
Uint GetPipelineStateVersion() const;
|
||||
const RenderStateParameters& GetRenderStateParameters() const;
|
||||
void SetViewport(IntVec4 viewport); // x, y, width, height
|
||||
const IntVec4& GetViewport() const; // x, y, width, height
|
||||
void SetViewport(IntVec4 viewport); // x, y, width, height; writes ALL viewports
|
||||
IntVec4 GetViewport() const; // x, y, width, height; viewport 0, rounded
|
||||
void SetViewportIndexed(Uint index, FloatVec4 viewport);
|
||||
const FloatVec4& GetViewportIndexed(Uint index) const;
|
||||
void SetLineWidth(Float width);
|
||||
Float GetLineWidth() const;
|
||||
void SetPointSize(Float size);
|
||||
@@ -260,8 +262,10 @@ namespace MobileGL {
|
||||
Uint32 GetClearStencil() const;
|
||||
void SetBlendColor(FloatVec4 color);
|
||||
const FloatVec4& GetBlendColor() const;
|
||||
void SetDepthRange(FloatVec2 range);
|
||||
void SetDepthRange(FloatVec2 range); // writes ALL viewports' depth ranges
|
||||
const FloatVec2& GetDepthRange() const;
|
||||
void SetDepthRangeIndexed(Uint index, FloatVec2 range);
|
||||
const FloatVec2& GetDepthRangeIndexed(Uint index) const;
|
||||
void SetSampleCoverage(Float value, Bool invert);
|
||||
Float GetSampleCoverageValue() const;
|
||||
Bool GetSampleCoverageInvert() const;
|
||||
@@ -276,8 +280,10 @@ namespace MobileGL {
|
||||
FrontFaceMode GetFrontFaceMode() const;
|
||||
void SetProvokingVertexMode(ProvokingVertexMode mode);
|
||||
ProvokingVertexMode GetProvokingVertexMode() const;
|
||||
void SetScissorBox(IntVec4 box); // x, y, width, height
|
||||
const IntVec4& GetScissorBox() const; // x, y, width, height
|
||||
void SetScissorBox(IntVec4 box); // x, y, width, height; writes ALL rectangles
|
||||
const IntVec4& GetScissorBox() const; // x, y, width, height; rectangle 0
|
||||
void SetScissorBoxIndexed(Uint index, IntVec4 box);
|
||||
const IntVec4& GetScissorBoxIndexed(Uint index) const;
|
||||
|
||||
// Transform feedback. The fields below are the state of the transform
|
||||
// feedback object currently bound to GL_TRANSFORM_FEEDBACK; see the object
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
void ErrorState::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
|
||||
if (code == ErrorCode::NoError) {
|
||||
MGLOG_E("Recording Non-OpenGL error:\n%s", info->toString().c_str());
|
||||
MGLOG_D("Recording Non-OpenGL error:\n%s", info->toString().c_str());
|
||||
m_nonGLErrors.push_back(MakeUnique<Error>(code, Move(info)));
|
||||
} else {
|
||||
MGLOG_E("Recording OpenGL error (%s):\n%s",
|
||||
MGLOG_D("Recording OpenGL error (%s):\n%s",
|
||||
MG_Util::ConvertGLEnumToString(MG_Util::ConvertErrorCodeToGLEnum(code)).c_str(),
|
||||
info->toString().c_str());
|
||||
// GL error semantics are sticky flags, not a queue (GL 3.3 core §2.5): with multiple
|
||||
|
||||
@@ -255,7 +255,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
static_cast<SizeT>(offset) + write.byteOffsetInUniform + write.byteSize > uboSize) {
|
||||
// Same verdict the live write path reaches for a uniform without backing
|
||||
// storage: log and drop, rather than fault.
|
||||
MGLOG_E("ProgramObject %u: buffered uniform write at location %u has no backing storage "
|
||||
MGLOG_E_ONCE("ProgramObject %u: buffered uniform write at location %u has no backing storage "
|
||||
"(offset=%u size=%u uboSize=%zu); dropping write",
|
||||
m_externalIndex, write.location, offset, write.byteSize, uboSize);
|
||||
continue;
|
||||
@@ -436,7 +436,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
defaultFS->Compile(); // TODO: use a global default FS object.
|
||||
auto status = defaultFS->GetCompileStatus();
|
||||
if (!status) {
|
||||
MGLOG_E("ProgramObject %u: Failed to compile default fragment shader. InfoLog:\n%s", m_externalIndex,
|
||||
MGLOG_E_ONCE("ProgramObject %u: Failed to compile default fragment shader. InfoLog:\n%s", m_externalIndex,
|
||||
defaultFS->GetInfoLog().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ namespace MobileGL {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Every viewport's scissor-test bit set, i.e. what glEnable(GL_SCISSOR_TEST) writes.
|
||||
constexpr Uint32 kAllViewportsMask =
|
||||
RenderStateParameters::MAX_VIEWPORTS >= 32
|
||||
? ~0u
|
||||
: (1u << RenderStateParameters::MAX_VIEWPORTS) - 1u;
|
||||
} // namespace
|
||||
|
||||
RenderState::RenderState() {
|
||||
@@ -32,6 +38,15 @@ namespace MobileGL {
|
||||
for (auto& mask : m_parameters.ColorMasks) {
|
||||
mask = BoolVec4(true, true, true, true);
|
||||
}
|
||||
// Every viewport's depth range starts at (0, 1) - GL 4.6 core table 23.4. The
|
||||
// viewport and scissor rectangles legitimately start all-zero here: their spec
|
||||
// initial value is the size of the window the context is first made current to,
|
||||
// which the frontend does not know yet, so an all-zero rectangle means "never
|
||||
// written" and the backends resolve it against the live surface (see
|
||||
// DirectGLES' SyncRenderState and VulkanRenderer's ApplyGLViewportState).
|
||||
for (auto& range : m_parameters.DepthRanges) {
|
||||
range = FloatVec2(0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
Uint RenderState::GetVersion() const {
|
||||
@@ -47,15 +62,47 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
// -------------------- Rasterization --------------------
|
||||
// ARB_viewport_array, "Additions to Chapter 2": Viewport(x, y, w, h) is equivalent to
|
||||
// ViewportIndexedf(i, x, y, w, h) for every i in [0, MAX_VIEWPORTS) - it is not a
|
||||
// synonym for "viewport 0".
|
||||
void RenderState::SetViewport(IntVec4 viewport) {
|
||||
if (m_parameters.Viewport == viewport) return;
|
||||
const FloatVec4 asFloat(static_cast<Float>(viewport.x()), static_cast<Float>(viewport.y()),
|
||||
static_cast<Float>(viewport.z()), static_cast<Float>(viewport.w()));
|
||||
Bool stateChanged = false;
|
||||
for (auto& stored : m_parameters.Viewports) {
|
||||
if (stored == asFloat) continue;
|
||||
stored = asFloat;
|
||||
stateChanged = true;
|
||||
}
|
||||
if (stateChanged) ++m_version;
|
||||
}
|
||||
|
||||
m_parameters.Viewport = viewport;
|
||||
IntVec4 RenderState::GetViewport() const {
|
||||
const FloatVec4& viewport = m_parameters.Viewports[0];
|
||||
// Round rather than truncate: glGetIntegerv on floating-point state rounds to
|
||||
// nearest (GL 4.6 core 22.2), and truncating a 63.5-wide viewport to 63 would
|
||||
// also hand the backends a rectangle one pixel short of what was asked for.
|
||||
return IntVec4(static_cast<Int>(std::lround(viewport.x())), static_cast<Int>(std::lround(viewport.y())),
|
||||
static_cast<Int>(std::lround(viewport.z())), static_cast<Int>(std::lround(viewport.w())));
|
||||
}
|
||||
|
||||
void RenderState::SetViewportIndexed(Uint index, FloatVec4 viewport) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Viewport index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
if (m_parameters.Viewports[index] == viewport) return;
|
||||
|
||||
m_parameters.Viewports[index] = viewport;
|
||||
++m_version;
|
||||
}
|
||||
|
||||
const IntVec4& RenderState::GetViewport() const {
|
||||
return m_parameters.Viewport;
|
||||
const FloatVec4& RenderState::GetViewportIndexed(Uint index) const {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Viewport index out of range: %u", index);
|
||||
return m_parameters.Viewports[0];
|
||||
}
|
||||
return m_parameters.Viewports[index];
|
||||
}
|
||||
|
||||
void RenderState::SetLineWidth(Float width) {
|
||||
@@ -187,6 +234,14 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
// -------------------- Capabilities --------------------
|
||||
namespace {
|
||||
// CapabilityInput lists ClipDistance0..7 contiguously (RenderState.h); the caller
|
||||
// has already rejected anything outside that run, so the subtraction is in range.
|
||||
Uint32 ClipDistanceBit(CapabilityInput cap) {
|
||||
return 1u << (static_cast<Uint>(cap) - static_cast<Uint>(CapabilityInput::ClipDistance0));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void RenderState::SetCapability(CapabilityInput cap, Bool enabled) {
|
||||
#define SET_CAPABILITY(capability, flag) \
|
||||
case CapabilityInput::capability: \
|
||||
@@ -215,7 +270,6 @@ namespace MobileGL {
|
||||
SET_CAPABILITY(SampleAlphaToOne, enabled);
|
||||
SET_CAPABILITY(SampleCoverage, enabled);
|
||||
SET_CAPABILITY(SampleMask, enabled);
|
||||
SET_CAPABILITY(ScissorTest, enabled);
|
||||
SET_CAPABILITY(StencilTest, enabled);
|
||||
SET_CAPABILITY(ProgramPointSize, enabled);
|
||||
case CapabilityInput::Blend: {
|
||||
@@ -228,6 +282,38 @@ namespace MobileGL {
|
||||
if (stateChanged) BumpVersions();
|
||||
break;
|
||||
}
|
||||
// GL 4.6 core 17.3.2: the non-indexed Enable/Disable(SCISSOR_TEST) enables or
|
||||
// disables the test for ALL viewports, exactly like glViewport writes all
|
||||
// viewports. Anything narrower fails KHR-GL43.viewport_array.scissor_test_state_api,
|
||||
// whose "enable all" phase reads every index back through glIsEnabledi.
|
||||
case CapabilityInput::ScissorTest: {
|
||||
const Uint32 updated = enabled ? kAllViewportsMask : 0u;
|
||||
if (m_parameters.ScissorTestEnabledMask == updated) break;
|
||||
m_parameters.ScissorTestEnabledMask = updated;
|
||||
BumpVersions();
|
||||
break;
|
||||
}
|
||||
case CapabilityInput::ClipDistance0:
|
||||
case CapabilityInput::ClipDistance1:
|
||||
case CapabilityInput::ClipDistance2:
|
||||
case CapabilityInput::ClipDistance3:
|
||||
case CapabilityInput::ClipDistance4:
|
||||
case CapabilityInput::ClipDistance5:
|
||||
case CapabilityInput::ClipDistance6:
|
||||
case CapabilityInput::ClipDistance7: {
|
||||
const Uint32 bit = ClipDistanceBit(cap);
|
||||
const Uint32 updated =
|
||||
enabled ? (m_parameters.ClipDistanceEnabledMask | bit)
|
||||
: (m_parameters.ClipDistanceEnabledMask & ~bit);
|
||||
if (updated == m_parameters.ClipDistanceEnabledMask) break;
|
||||
m_parameters.ClipDistanceEnabledMask = updated;
|
||||
// Deliberately NOT BumpVersions(): no backend bakes a clip-distance enable
|
||||
// into a pipeline object (DirectGLES issues glEnable, DirectVulkan takes the
|
||||
// set from the shader's declared array), so bumping the pipeline version here
|
||||
// would evict cached pipelines for state they do not contain.
|
||||
++m_version;
|
||||
break;
|
||||
}
|
||||
default: // not supported currently
|
||||
break;
|
||||
}
|
||||
@@ -258,24 +344,52 @@ namespace MobileGL {
|
||||
RETURN_CAPABILITY(SampleAlphaToOne);
|
||||
RETURN_CAPABILITY(SampleCoverage);
|
||||
RETURN_CAPABILITY(SampleMask);
|
||||
RETURN_CAPABILITY(ScissorTest);
|
||||
RETURN_CAPABILITY(StencilTest);
|
||||
RETURN_CAPABILITY(ProgramPointSize);
|
||||
case CapabilityInput::Blend:
|
||||
return m_parameters.BlendStates[0].Enabled;
|
||||
// The non-indexed query of an indexed capability answers for index 0
|
||||
// (GL 4.6 core 22.1), which is also the only bit either backend consumes today.
|
||||
case CapabilityInput::ScissorTest:
|
||||
return (m_parameters.ScissorTestEnabledMask & 1u) != 0;
|
||||
case CapabilityInput::ClipDistance0:
|
||||
case CapabilityInput::ClipDistance1:
|
||||
case CapabilityInput::ClipDistance2:
|
||||
case CapabilityInput::ClipDistance3:
|
||||
case CapabilityInput::ClipDistance4:
|
||||
case CapabilityInput::ClipDistance5:
|
||||
case CapabilityInput::ClipDistance6:
|
||||
case CapabilityInput::ClipDistance7:
|
||||
return (m_parameters.ClipDistanceEnabledMask & ClipDistanceBit(cap)) != 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderState::SetCapabilityIndexed(CapabilityInput cap, Uint index, Bool enabled) {
|
||||
// Only for BlendState currently. The GL entry points (glEnablei/glDisablei) already
|
||||
// reject every non-GL_BLEND target with GL_INVALID_ENUM before reaching here, so this
|
||||
// is a backstop - but it must stay a backstop: THROW_UNIMPL_EXCEPTION unwinds a C++
|
||||
// exception through the C GL ABI and terminates the process.
|
||||
// GL_BLEND (indexed by draw buffer) and GL_SCISSOR_TEST (indexed by viewport) are
|
||||
// the only indexed capabilities in GL 4.6 core. The GL entry points
|
||||
// (glEnablei/glDisablei) already reject every other target with GL_INVALID_ENUM
|
||||
// and every out-of-range index with GL_INVALID_VALUE before reaching here, so the
|
||||
// guards below are backstops - but they must stay backstops:
|
||||
// THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and
|
||||
// terminates the process.
|
||||
if (cap == CapabilityInput::ScissorTest) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Scissor test capability index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
const Uint32 bit = 1u << index;
|
||||
const Uint32 updated = enabled ? (m_parameters.ScissorTestEnabledMask | bit)
|
||||
: (m_parameters.ScissorTestEnabledMask & ~bit);
|
||||
if (updated == m_parameters.ScissorTestEnabledMask) return;
|
||||
m_parameters.ScissorTestEnabledMask = updated;
|
||||
BumpVersions();
|
||||
return;
|
||||
}
|
||||
if (cap != CapabilityInput::Blend) {
|
||||
MGLOG_I("RenderState::SetCapabilityIndexed: indexed capability state exists only for "
|
||||
"GL_BLEND (cap=%d, index=%u); ignoring",
|
||||
"GL_BLEND and GL_SCISSOR_TEST (cap=%d, index=%u); ignoring",
|
||||
static_cast<int>(cap), index);
|
||||
return;
|
||||
}
|
||||
@@ -290,9 +404,17 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
Bool RenderState::IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
|
||||
// Only for BlendState currently - same backstop reasoning as SetCapabilityIndexed:
|
||||
// glIsEnabledi has already answered GL_INVALID_ENUM/GL_FALSE for anything else, and a
|
||||
// query must never be able to terminate the process.
|
||||
// GL_BLEND and GL_SCISSOR_TEST only - same backstop reasoning as
|
||||
// SetCapabilityIndexed: glIsEnabledi has already answered
|
||||
// GL_INVALID_ENUM/GL_INVALID_VALUE for anything else, and a query must never be
|
||||
// able to terminate the process.
|
||||
if (cap == CapabilityInput::ScissorTest) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Scissor test capability index out of range: %u", index);
|
||||
return false;
|
||||
}
|
||||
return (m_parameters.ScissorTestEnabledMask & (1u << index)) != 0;
|
||||
}
|
||||
if (cap != CapabilityInput::Blend) {
|
||||
MGLOG_I("RenderState::IsCapabilityEnabledIndexed: indexed capability state exists only "
|
||||
"for GL_BLEND (cap=%d, index=%u); reporting disabled",
|
||||
@@ -553,15 +675,39 @@ namespace MobileGL {
|
||||
return m_parameters.BlendColor;
|
||||
}
|
||||
|
||||
// Like Viewport: ARB_viewport_array makes DepthRange(n, f) the same as
|
||||
// DepthRangeIndexed(i, n, f) for every i.
|
||||
void RenderState::SetDepthRange(FloatVec2 range) {
|
||||
if (m_parameters.DepthRange == range) return;
|
||||
|
||||
m_parameters.DepthRange = range;
|
||||
++m_version;
|
||||
Bool stateChanged = false;
|
||||
for (auto& stored : m_parameters.DepthRanges) {
|
||||
if (stored == range) continue;
|
||||
stored = range;
|
||||
stateChanged = true;
|
||||
}
|
||||
if (stateChanged) ++m_version;
|
||||
}
|
||||
|
||||
const FloatVec2& RenderState::GetDepthRange() const {
|
||||
return m_parameters.DepthRange;
|
||||
return m_parameters.DepthRanges[0];
|
||||
}
|
||||
|
||||
void RenderState::SetDepthRangeIndexed(Uint index, FloatVec2 range) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Depth range index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
if (m_parameters.DepthRanges[index] == range) return;
|
||||
|
||||
m_parameters.DepthRanges[index] = range;
|
||||
++m_version;
|
||||
}
|
||||
|
||||
const FloatVec2& RenderState::GetDepthRangeIndexed(Uint index) const {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Depth range index out of range: %u", index);
|
||||
return m_parameters.DepthRanges[0];
|
||||
}
|
||||
return m_parameters.DepthRanges[index];
|
||||
}
|
||||
|
||||
void RenderState::SetSampleCoverage(Float value, Bool invert) {
|
||||
@@ -688,15 +834,39 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
// --------------------- Scissor ---------------------
|
||||
// Like Viewport: ARB_viewport_array makes Scissor(x, y, w, h) the same as
|
||||
// ScissorIndexed(i, x, y, w, h) for every i.
|
||||
void RenderState::SetScissorBox(IntVec4 box) {
|
||||
if (m_parameters.ScissorBox == box) return;
|
||||
|
||||
m_parameters.ScissorBox = box;
|
||||
++m_version;
|
||||
Bool stateChanged = false;
|
||||
for (auto& stored : m_parameters.ScissorBoxes) {
|
||||
if (stored == box) continue;
|
||||
stored = box;
|
||||
stateChanged = true;
|
||||
}
|
||||
if (stateChanged) ++m_version;
|
||||
}
|
||||
|
||||
const IntVec4& RenderState::GetScissorBox() const {
|
||||
return m_parameters.ScissorBox;
|
||||
return m_parameters.ScissorBoxes[0];
|
||||
}
|
||||
|
||||
void RenderState::SetScissorBoxIndexed(Uint index, IntVec4 box) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Scissor box index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
if (m_parameters.ScissorBoxes[index] == box) return;
|
||||
|
||||
m_parameters.ScissorBoxes[index] = box;
|
||||
++m_version;
|
||||
}
|
||||
|
||||
const IntVec4& RenderState::GetScissorBoxIndexed(Uint index) const {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Scissor box index out of range: %u", index);
|
||||
return m_parameters.ScissorBoxes[0];
|
||||
}
|
||||
return m_parameters.ScissorBoxes[index];
|
||||
}
|
||||
} // namespace GLState
|
||||
} // namespace MG_State
|
||||
|
||||
@@ -220,8 +220,22 @@ namespace MobileGL {
|
||||
};
|
||||
|
||||
struct RenderStateParameters {
|
||||
// ARB_viewport_array / GL 4.6 core 13.6.1: the viewport, the scissor rectangle, the depth
|
||||
// range and the scissor-test enable are all arrays indexed by gl_ViewportIndex, and the
|
||||
// spec floor for MAX_VIEWPORTS is 16. MobileGL advertises exactly 16 on both backends, so
|
||||
// this is also what GL_MAX_VIEWPORTS reports (see the backend loaders' caps.MaxViewports).
|
||||
static constexpr Uint MAX_VIEWPORTS = 16;
|
||||
|
||||
// Rasterization
|
||||
IntVec4 Viewport = IntVec4(0, 0, 0, 0); // x, y, width, height
|
||||
// The viewport rectangle is FLOAT state as of GL 4.1 - ViewportIndexedf writes fractional
|
||||
// values and GetFloati_v(GL_VIEWPORT) must hand them back bit-exact
|
||||
// (KHR-GL43.viewport_array.viewport_api compares with ==, no tolerance). glViewport's
|
||||
// integers are simply one way to write it. Index 0 is what a program that never assigns
|
||||
// gl_ViewportIndex rasterizes against, and what the classic glViewport /
|
||||
// glGetIntegerv(GL_VIEWPORT) pair addresses. Both backends rasterize the rectangle
|
||||
// rounded back to integers; the STATE stays exact, which is the half the conformance
|
||||
// suite checks (see the KNOWN INFIDELITY note in AdvertisedLimitsScenario.cpp).
|
||||
Array<FloatVec4, MAX_VIEWPORTS> Viewports{}; // x, y, width, height
|
||||
Float LineWidth = 1.0f;
|
||||
Float PointSize = 1.0f;
|
||||
// GL_PATCH_VERTICES: how many vertices one tessellation patch consumes.
|
||||
@@ -247,7 +261,13 @@ namespace MobileGL {
|
||||
Float ClearDepth = 1.0f;
|
||||
Uint32 ClearStencil = 0;
|
||||
FloatVec4 BlendColor = FloatVec4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
FloatVec2 DepthRange = FloatVec2(0.0f, 1.0f);
|
||||
// Per-viewport depth range (glDepthRangeIndexed / glDepthRangeArrayv). Every entry is
|
||||
// initialized to (0, 1) in RenderState's constructor - a default member initializer would
|
||||
// not survive the Array<> aggregate. Kept float rather than double: DepthRangeArrayv takes
|
||||
// GLdouble, but the value reaches the hardware as VkViewport::minDepth/maxDepth (float) on
|
||||
// Magma and glDepthRangef on Espryt, so a double store would only widen the readback and
|
||||
// then lose it again at the same place.
|
||||
Array<FloatVec2, MAX_VIEWPORTS> DepthRanges{};
|
||||
Float SampleCoverageValue = 1.0f;
|
||||
Bool SampleCoverageInvert = false;
|
||||
Uint32 SampleMaskValue = 0xffffffffu;
|
||||
@@ -299,10 +319,21 @@ namespace MobileGL {
|
||||
Bool SampleAlphaToOneEnabled = false;
|
||||
Bool SampleCoverageEnabled = false;
|
||||
Bool SampleMaskEnabled = false;
|
||||
Bool ScissorTestEnabled = false;
|
||||
Bool StencilTestEnabled = false;
|
||||
Bool ProgramPointSizeEnabled = false;
|
||||
IntVec4 ScissorBox = IntVec4(0, 0, 0, 0); // x, y, width, height
|
||||
// glEnable(GL_SCISSOR_TEST) enables the test for EVERY viewport, glEnablei for one
|
||||
// (GL 4.6 core 17.3.2), so this is 16 bits and not a bool. Bit 0 is what the classic
|
||||
// glIsEnabled(GL_SCISSOR_TEST) reports and what both backends currently consume. Unlike
|
||||
// ClipDistanceEnabledMask below it DOES bump the pipeline version, because DirectGLES
|
||||
// turns it into a real glEnable/glDisable.
|
||||
Uint32 ScissorTestEnabledMask = 0;
|
||||
Array<IntVec4, MAX_VIEWPORTS> ScissorBoxes{}; // x, y, width, height
|
||||
// glEnable(GL_CLIP_DISTANCE0 + i) for i in [0, 8), one bit each. A bitmask rather than
|
||||
// eight bools because every consumer wants the set, not an individual flag, and because
|
||||
// the SYNC_CAPABILITY/SET_CAPABILITY macros key off a "<Name>Enabled" field name that
|
||||
// eight numbered capabilities cannot share. Lives in the tail span (after LogicOp), so
|
||||
// DirectGLES' span memcmp picks a change up like any other capability.
|
||||
Uint32 ClipDistanceEnabledMask = 0;
|
||||
};
|
||||
|
||||
namespace MG_State {
|
||||
@@ -317,8 +348,14 @@ namespace MobileGL {
|
||||
const RenderStateParameters& GetAllParameters() const;
|
||||
|
||||
// Rasterization
|
||||
// ARB_viewport_array defines glViewport as ViewportIndexedf on EVERY index, so the
|
||||
// classic setter broadcasts; GetViewport answers for index 0 (rounded to the
|
||||
// integers glGetIntegerv(GL_VIEWPORT) and both backends want) and is BY VALUE for
|
||||
// that reason. The indexed pair is the verbatim float state.
|
||||
void SetViewport(IntVec4 viewport); // x, y, width, height
|
||||
const IntVec4& GetViewport() const; // x, y, width, height
|
||||
IntVec4 GetViewport() const; // x, y, width, height, viewport 0, rounded
|
||||
void SetViewportIndexed(Uint index, FloatVec4 viewport);
|
||||
const FloatVec4& GetViewportIndexed(Uint index) const;
|
||||
void SetLineWidth(Float width);
|
||||
Float GetLineWidth() const;
|
||||
void SetPointSize(Float size);
|
||||
@@ -394,8 +431,12 @@ namespace MobileGL {
|
||||
Uint32 GetClearStencil() const;
|
||||
void SetBlendColor(FloatVec4 color);
|
||||
const FloatVec4& GetBlendColor() const;
|
||||
// glDepthRange(f) writes every viewport's range (ARB_viewport_array); the indexed
|
||||
// pair is glDepthRangeIndexed / glDepthRangeArrayv. GetDepthRange answers index 0.
|
||||
void SetDepthRange(FloatVec2 range);
|
||||
const FloatVec2& GetDepthRange() const;
|
||||
void SetDepthRangeIndexed(Uint index, FloatVec2 range);
|
||||
const FloatVec2& GetDepthRangeIndexed(Uint index) const;
|
||||
void SetSampleCoverage(Float value, Bool invert);
|
||||
Float GetSampleCoverageValue() const;
|
||||
Bool GetSampleCoverageInvert() const;
|
||||
@@ -415,9 +456,12 @@ namespace MobileGL {
|
||||
void SetProvokingVertexMode(ProvokingVertexMode mode);
|
||||
ProvokingVertexMode GetProvokingVertexMode() const;
|
||||
|
||||
// Scissor
|
||||
// Scissor. glScissor writes every rectangle (ARB_viewport_array); GetScissorBox
|
||||
// answers for index 0.
|
||||
void SetScissorBox(IntVec4 box); // x, y, width, height
|
||||
const IntVec4& GetScissorBox() const; // x, y, width, height
|
||||
void SetScissorBoxIndexed(Uint index, IntVec4 box);
|
||||
const IntVec4& GetScissorBoxIndexed(Uint index) const;
|
||||
|
||||
private:
|
||||
// Bump both: any state change invalidates the draw snapshot, and this one also
|
||||
|
||||
@@ -117,7 +117,15 @@ namespace MobileGL::MG_State::GLState {
|
||||
void SetFixedSampleLocations(Bool fixedSampleLocations) override;
|
||||
Uint64 GetLifetimeId() const override;
|
||||
GLenum GetDepthStencilTextureMode() const override { return m_depthStencilTextureMode; }
|
||||
void SetDepthStencilTextureMode(GLenum mode) override { m_depthStencilTextureMode = mode; }
|
||||
// Bumps the params version like every other backend-visible texture parameter: the mode
|
||||
// decides which ASPECT of a packed depth/stencil image a sampler reads, which DirectGLES
|
||||
// forwards as a texture parameter and DirectVulkan bakes into the sampled image view. A
|
||||
// silent write here would leave both backends showing the aspect they last built.
|
||||
void SetDepthStencilTextureMode(GLenum mode) override {
|
||||
if (m_depthStencilTextureMode == mode) return;
|
||||
m_depthStencilTextureMode = mode;
|
||||
++m_textureParamsVersion;
|
||||
}
|
||||
|
||||
protected:
|
||||
static Uint64 AllocateLifetimeId();
|
||||
|
||||
@@ -16,8 +16,11 @@
|
||||
#include <MG_Backend/DirectGLES/Utils.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BakeImageFormatQualifiers;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ForceFlatIntegerVaryings;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::SplitReadWriteImageUniforms;
|
||||
|
||||
namespace {
|
||||
@@ -367,3 +370,183 @@ void main() {}
|
||||
<< "an unrelated extension must survive untouched:\n" << out;
|
||||
EXPECT_EQ(CountOf(out, "GL_OES_texture_buffer"), 1u);
|
||||
}
|
||||
|
||||
// Interpolation is only ever consumed at a fragment input, but an ES linker still compares the
|
||||
// two sides of EVERY stage interface and rejects a program whose producer says `flat` and whose
|
||||
// consumer does not. SPIRV-Cross prints `flat` on a vertex output and a geometry input of
|
||||
// integer type and on nothing else, so a program with tessellation in the middle came out
|
||||
// mismatched at both ends of the tessellator - "output vs_tcs_result interpolation mismatch
|
||||
// with other stage" on Adreno, and a program that fails to link is a draw that paints nothing.
|
||||
TEST(ForceFlatIntegerVaryingsTest, TessellationStagesGetTheQualifierOnBothSides) {
|
||||
const String tessControl = R"(#version 320 es
|
||||
layout(vertices = 1) out;
|
||||
layout(location = 0) in uint vs_tcs_result[];
|
||||
layout(location = 0) out uint tcs_tes_result[1];
|
||||
void main() { tcs_tes_result[gl_InvocationID] = vs_tcs_result[gl_InvocationID]; }
|
||||
)";
|
||||
const String control = ForceFlatIntegerVaryings(tessControl, GL_TESS_CONTROL_SHADER);
|
||||
EXPECT_TRUE(Contains(control, "layout(location = 0) flat in uint vs_tcs_result[];")) << control;
|
||||
EXPECT_TRUE(Contains(control, "layout(location = 0) flat out uint tcs_tes_result[1];")) << control;
|
||||
|
||||
const String tessEval = R"(#version 320 es
|
||||
layout(isolines, point_mode) in;
|
||||
layout(location = 0) in uint tcs_tes_result[];
|
||||
layout(location = 0) out uint tes_gs_result;
|
||||
void main() { tes_gs_result = tcs_tes_result[0]; }
|
||||
)";
|
||||
const String eval = ForceFlatIntegerVaryings(tessEval, GL_TESS_EVALUATION_SHADER);
|
||||
EXPECT_TRUE(Contains(eval, "layout(location = 0) flat in uint tcs_tes_result[];")) << eval;
|
||||
EXPECT_TRUE(Contains(eval, "layout(location = 0) flat out uint tes_gs_result;")) << eval;
|
||||
}
|
||||
|
||||
// The two ends the tessellation stages have to meet: what a vertex shader and a geometry shader
|
||||
// already emitted before this pass learned about tessellation at all. Pinned here so the two
|
||||
// sides cannot drift apart again.
|
||||
TEST(ForceFlatIntegerVaryingsTest, TheStagesAroundTessellationAreUnchanged) {
|
||||
const String vertex = R"(#version 320 es
|
||||
layout(location = 0) out uint vs_tcs_result;
|
||||
void main() { vs_tcs_result = 1u; }
|
||||
)";
|
||||
EXPECT_TRUE(Contains(ForceFlatIntegerVaryings(vertex, GL_VERTEX_SHADER),
|
||||
"layout(location = 0) flat out uint vs_tcs_result;"));
|
||||
|
||||
const String geometry = R"(#version 320 es
|
||||
layout(points) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
layout(location = 0) in uint tes_gs_result[1];
|
||||
layout(location = 0) out uint gs_fs_result;
|
||||
void main() { gs_fs_result = tes_gs_result[0]; EmitVertex(); }
|
||||
)";
|
||||
const String gs = ForceFlatIntegerVaryings(geometry, GL_GEOMETRY_SHADER);
|
||||
EXPECT_TRUE(Contains(gs, "layout(location = 0) flat in uint tes_gs_result[1];")) << gs;
|
||||
EXPECT_TRUE(Contains(gs, "layout(location = 0) flat out uint gs_fs_result;")) << gs;
|
||||
}
|
||||
|
||||
// Non-integer interfaces keep whatever interpolation they were given: adding `flat` to a float
|
||||
// varying would turn a smoothly interpolated value into a per-provoking-vertex constant, which
|
||||
// is a rendering change, not a linker one.
|
||||
TEST(ForceFlatIntegerVaryingsTest, FloatVaryingsAreNotTouched) {
|
||||
const String tessEval = R"(#version 320 es
|
||||
layout(isolines, point_mode) in;
|
||||
layout(location = 1) in vec2 tcs_tes_coord[];
|
||||
layout(location = 1) out vec2 tes_gs_coord;
|
||||
void main() { tes_gs_coord = tcs_tes_coord[0]; }
|
||||
)";
|
||||
const String out = ForceFlatIntegerVaryings(tessEval, GL_TESS_EVALUATION_SHADER);
|
||||
EXPECT_TRUE(Contains(out, "layout(location = 1) in vec2 tcs_tes_coord[];")) << out;
|
||||
EXPECT_TRUE(Contains(out, "layout(location = 1) out vec2 tes_gs_coord;")) << out;
|
||||
EXPECT_EQ(CountOf(out, "flat"), 0u) << out;
|
||||
}
|
||||
|
||||
// --- image format qualifier completion ---------------------------------------------------------
|
||||
//
|
||||
// GLSL ES requires a format layout qualifier on every image; desktop GLSL lets a writeonly
|
||||
// declaration omit one. The format is normally written into the SPIR-V before SPIRV-Cross runs
|
||||
// (BakeImageFormatsPass), but SPIRV-Cross THROWS rather than printing the formats it calls
|
||||
// desktop-only for ESSL - r8ui among them - so those are completed here, on the emitted text.
|
||||
|
||||
// The KHR-GL4x.packed_depth_stencil.stencil_texturing stencil half: `writeonly uniform uimage2D`
|
||||
// with GL_R8UI bound to its unit.
|
||||
TEST(BakeImageFormatQualifiersTest, AFormatlessDeclarationGetsTheBoundFormat) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(binding = 1) uniform writeonly highp uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); }
|
||||
)";
|
||||
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
|
||||
EXPECT_TRUE(Contains(out, "layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;")) << out;
|
||||
}
|
||||
|
||||
// A declaration with NO layout at all still has to end up with one, or the driver rejects it for
|
||||
// exactly the reason this pass exists.
|
||||
TEST(BakeImageFormatQualifiersTest, ADeclarationWithNoLayoutGetsOne) {
|
||||
const String source = R"(#version 320 es
|
||||
uniform writeonly highp uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
)";
|
||||
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r16i"}});
|
||||
EXPECT_TRUE(Contains(out, "layout(r16i) uniform writeonly highp uimage2D uni_image;")) << out;
|
||||
}
|
||||
|
||||
// A DECLARED format is authoritative and must survive, whatever the map says - the frontend never
|
||||
// puts a declared image in the map, and the pass must not depend on that being true.
|
||||
TEST(BakeImageFormatQualifiersTest, ADeclaredFormatIsNeverOverwritten) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(binding = 1, rgba8ui) uniform writeonly highp uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
)";
|
||||
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
|
||||
EXPECT_EQ(out, source) << out;
|
||||
}
|
||||
|
||||
// Only the named uniform. A second image in the same shader - format-less because the pass
|
||||
// declined it, or because its unit holds nothing - must be left exactly as it is.
|
||||
TEST(BakeImageFormatQualifiersTest, OnlyTheNamedUniformIsTouched) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(binding = 0) uniform writeonly highp uimage2D named;
|
||||
layout(binding = 1) uniform writeonly highp uimage2D other;
|
||||
void main() { imageStore(named, ivec2(0), uvec4(1u)); imageStore(other, ivec2(0), uvec4(2u)); }
|
||||
)";
|
||||
const String out = BakeImageFormatQualifiers(source, {{"named", "r8ui"}});
|
||||
EXPECT_TRUE(Contains(out, "layout(r8ui, binding = 0) uniform writeonly highp uimage2D named;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 1) uniform writeonly highp uimage2D other;")) << out;
|
||||
}
|
||||
|
||||
// The format the pass writes has to survive the two passes that run after it, or nothing was
|
||||
// gained: the read+write split copies declarations, and the binding strip edits layout qualifiers.
|
||||
TEST(BakeImageFormatQualifiersTest, TheWrittenFormatSurvivesTheLaterImagePasses) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(binding = 3) uniform writeonly highp uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
)";
|
||||
String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
|
||||
out = SplitReadWriteImageUniforms(out);
|
||||
out = RemoveLayoutBinding(out);
|
||||
EXPECT_TRUE(Contains(out, "r8ui")) << out;
|
||||
EXPECT_TRUE(Contains(out, "binding = 3")) << out;
|
||||
}
|
||||
|
||||
TEST(BakeImageFormatQualifiersTest, AnEmptyMapOrAnImagelessShaderIsANoOp) {
|
||||
const String withImage = R"(#version 320 es
|
||||
layout(binding = 1) uniform writeonly highp uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
)";
|
||||
EXPECT_EQ(BakeImageFormatQualifiers(withImage, {}), withImage);
|
||||
|
||||
const String withoutImage = R"(#version 320 es
|
||||
layout(location = 0) out highp vec4 mg_FragColor;
|
||||
void main() { mg_FragColor = vec4(1.0); }
|
||||
)";
|
||||
EXPECT_EQ(BakeImageFormatQualifiers(withoutImage, {{"uni_image", "r8ui"}}), withoutImage);
|
||||
}
|
||||
|
||||
// --- GL_NV_image_formats directive --------------------------------------------------------------
|
||||
|
||||
TEST(RequestExtendedImageFormatsTest, TheDirectiveGoesRightAfterTheVersionLine) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
)";
|
||||
const String out = RequestExtendedImageFormats(source, true);
|
||||
EXPECT_TRUE(Contains(out, "#version 320 es\n#extension GL_NV_image_formats : require\n")) << out;
|
||||
}
|
||||
|
||||
// Never speculatively: `#extension` naming an extension the driver does not advertise is itself a
|
||||
// compile error, so the caller's "not needed" answer has to be honoured exactly.
|
||||
TEST(RequestExtendedImageFormatsTest, NotNeededMeansNotEmitted) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
)";
|
||||
EXPECT_EQ(RequestExtendedImageFormats(source, false), source);
|
||||
}
|
||||
|
||||
TEST(RequestExtendedImageFormatsTest, AnAlreadyPresentDirectiveIsNotDuplicated) {
|
||||
const String source = R"(#version 320 es
|
||||
#extension GL_NV_image_formats : require
|
||||
layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
)";
|
||||
const String out = RequestExtendedImageFormats(source, true);
|
||||
EXPECT_EQ(out, source);
|
||||
EXPECT_EQ(CountOf(out, "GL_NV_image_formats"), 1u) << out;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,28 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <iostream>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
||||
|
||||
TEST(DirectVulkanSanity, ProgramMovePreservesViewportIndexUsage) {
|
||||
using VkProgramObject = MobileGL::MG_Backend::DirectVulkan::ProgramFactory::VkProgramObject;
|
||||
|
||||
VkProgramObject moveConstructedSource;
|
||||
moveConstructedSource.writesViewportIndexBuiltin = true;
|
||||
VkProgramObject moveConstructed(std::move(moveConstructedSource));
|
||||
EXPECT_TRUE(moveConstructed.writesViewportIndexBuiltin);
|
||||
EXPECT_FALSE(moveConstructedSource.writesViewportIndexBuiltin);
|
||||
|
||||
VkProgramObject moveAssignedSource;
|
||||
moveAssignedSource.writesViewportIndexBuiltin = true;
|
||||
VkProgramObject moveAssigned;
|
||||
moveAssigned = std::move(moveAssignedSource);
|
||||
EXPECT_TRUE(moveAssigned.writesViewportIndexBuiltin);
|
||||
EXPECT_FALSE(moveAssignedSource.writesViewportIndexBuiltin);
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, ExtensionEnumeration) {
|
||||
uint32_t extensionCount = 0;
|
||||
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
|
||||
|
||||
@@ -725,22 +725,57 @@ TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSu
|
||||
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
|
||||
};
|
||||
|
||||
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false);
|
||||
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false);
|
||||
EXPECT_FALSE(contains(without, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_FALSE(contains(without, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
|
||||
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true);
|
||||
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true, false, false);
|
||||
EXPECT_TRUE(contains(with, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_TRUE(contains(with, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
|
||||
// Same rule on the Vulkan backend, where the gate is the samplerAnisotropy device feature.
|
||||
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false);
|
||||
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false);
|
||||
EXPECT_FALSE(contains(vkWithout, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true);
|
||||
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true, false);
|
||||
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
}
|
||||
|
||||
// Minecraft 26.3 checks ARB_draw_indirect before it considers the already-advertised
|
||||
// ARB_multi_draw_indirect, then separately requires ARB_base_instance before enabling its terrain
|
||||
// indirect path. Pin both strings and, just as importantly, the non-zero firstInstance gate.
|
||||
TEST(IndirectDrawAdvertisement, MatchesEachBackendsUsableCommandSemantics) {
|
||||
const auto contains = [](const MobileGL::Vector<MobileGL::GLExtension>& extensions,
|
||||
MobileGL::GLExtension wanted) {
|
||||
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
|
||||
};
|
||||
|
||||
const auto esWithoutIndirect =
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false);
|
||||
EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto esWithoutBaseInstance =
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, false);
|
||||
EXPECT_TRUE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_FALSE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto esWithBoth =
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, true);
|
||||
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto vkWithoutBaseInstance =
|
||||
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false);
|
||||
EXPECT_TRUE(contains(vkWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_FALSE(contains(vkWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto vkWithBoth =
|
||||
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, true);
|
||||
EXPECT_TRUE(contains(vkWithBoth, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_TRUE(contains(vkWithBoth, MobileGL::E_GL_ARB_base_instance));
|
||||
}
|
||||
|
||||
TEST(TextureAnisotropyCapabilities, MaxAnisotropyIsQueriedOnlyWhenTheExtensionIsPresent) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
@@ -836,3 +871,63 @@ TEST(MultiDrawCapabilities, ExtensionWithoutResolvedPointerIsNotSupport) {
|
||||
EXPECT_FALSE(caps.SupportsMultiDrawIndirect);
|
||||
EXPECT_FALSE(caps.SupportsMultiDrawElementsBaseVertex);
|
||||
}
|
||||
|
||||
TEST(DrawIndirectCapabilities, RequiresEs31AndBothCoreEntryPoints) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
auto funcs = MakeFakeGLESFunctions();
|
||||
funcs.glDrawElementsIndirect = [](GLenum, GLenum, const void*) {};
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities supportedCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(supportedCaps, funcs));
|
||||
EXPECT_TRUE(supportedCaps.SupportsDrawIndirect);
|
||||
|
||||
// The same pointers on an ES 3.0 context are not core entry points and cannot back the
|
||||
// desktop extension contract.
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.glesMinorVersion = 0;
|
||||
MobileGL::MG_External::GLESCapabilities es30Caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es30Caps, funcs));
|
||||
EXPECT_FALSE(es30Caps.SupportsDrawIndirect);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
const auto missingElements = MakeFakeGLESFunctions();
|
||||
MobileGL::MG_External::GLESCapabilities missingEntryPointCaps;
|
||||
ASSERT_TRUE(
|
||||
MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(missingEntryPointCaps, missingElements));
|
||||
EXPECT_FALSE(missingEntryPointCaps.SupportsDrawIndirect);
|
||||
}
|
||||
|
||||
TEST(BaseInstanceCapabilities, RequiresTheExtensionAndAllThreeEntryPoints) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
auto funcs = MakeFakeGLESFunctions();
|
||||
funcs.glDrawArraysInstancedBaseInstanceEXT = [](GLenum, GLint, GLsizei, GLsizei, GLuint) {};
|
||||
funcs.glDrawElementsInstancedBaseInstanceEXT =
|
||||
[](GLenum, GLsizei, GLenum, const void*, GLsizei, GLuint) {};
|
||||
funcs.glDrawElementsInstancedBaseVertexBaseInstanceEXT =
|
||||
[](GLenum, GLsizei, GLenum, const void*, GLsizei, GLint, GLuint) {};
|
||||
|
||||
// Resolved stubs alone must never make the capability true.
|
||||
MobileGL::MG_External::GLESCapabilities pointersOnlyCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(pointersOnlyCaps, funcs));
|
||||
EXPECT_FALSE(pointersOnlyCaps.SupportsBaseInstance);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_EXT_base_instance");
|
||||
MobileGL::MG_External::GLESCapabilities supportedCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(supportedCaps, funcs));
|
||||
EXPECT_TRUE(supportedCaps.SupportsBaseInstance);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_EXT_base_instance");
|
||||
funcs.glDrawElementsInstancedBaseInstanceEXT = nullptr;
|
||||
MobileGL::MG_External::GLESCapabilities missingEntryPointCaps;
|
||||
ASSERT_TRUE(
|
||||
MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(missingEntryPointCaps, funcs));
|
||||
EXPECT_FALSE(missingEntryPointCaps.SupportsBaseInstance);
|
||||
}
|
||||
|
||||
@@ -1610,3 +1610,188 @@ TEST_F(GeneralBufferTest, General_CoherentAsFlush_PersistentMapAdoptsZeroCopyBac
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// A buffer whose bytes the backend adopted into its own GPU memory - which is what
|
||||
// EnsureGpuResidentStorage does for a transform-feedback capture target or a shader
|
||||
// storage binding, so that MapBuffer/GetBufferSubData read real GPU results - and which
|
||||
// the application then REDEFINES.
|
||||
//
|
||||
// The store the adopted mapping describes is the one being thrown away. Keeping that
|
||||
// mapping across the redefinition is what let a transform feedback capture be written to
|
||||
// one buffer and read back out of another: the backend replaced the storage (a
|
||||
// respecification is the orphaning point) while the frontend went on resolving every read
|
||||
// through a mapping of the storage it had just released. Two capture spans into one
|
||||
// re-specified buffer came back empty from the second one onwards.
|
||||
//
|
||||
// So the mapping is handed back and the buffer returns to the CPU-shadow model until
|
||||
// something asks for residency again. These pin all three parts of that: the adoption
|
||||
// really is dropped, the new contents really do land where later reads resolve, and the
|
||||
// backend really is told to respecify - it must not skip the storage, or its copy would
|
||||
// keep the old bytes.
|
||||
TEST_F(BufferTest, RedefiningAnAdoptedBufferHandsTheMappingBack) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
|
||||
GLuint buffer = 0;
|
||||
GenBuffers(1, &buffer);
|
||||
BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
const GLint before[4] = {1, 2, 3, 4};
|
||||
BufferData(GL_ARRAY_BUFFER, sizeof(before), before, GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
// The backend adopts the bytes, exactly as a capture target or an SSBO binding does.
|
||||
ASSERT_TRUE(bufferObject->EnsureGpuResidentStorage());
|
||||
ASSERT_TRUE(bufferObject->IsBackendPersistentMapped());
|
||||
ASSERT_EQ(static_cast<const void*>(bufferObject->MappedData()),
|
||||
static_cast<const void*>(mock.gpu.data()));
|
||||
mock.respecifyCalls = 0;
|
||||
|
||||
const GLint after[4] = {10, 20, 30, 40};
|
||||
BufferData(GL_ARRAY_BUFFER, sizeof(after), after, GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
|
||||
EXPECT_NE(static_cast<const void*>(bufferObject->MappedData()),
|
||||
static_cast<const void*>(mock.gpu.data()));
|
||||
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), after, sizeof(after)), 0);
|
||||
// The backend has a separate copy again, so it must have been told to refresh it.
|
||||
EXPECT_EQ(mock.respecifyCalls, 1);
|
||||
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// The same redefinition at a LARGER size, which is the case nothing could paper over: the
|
||||
// adopted mapping is exactly as big as the old store, so writing the new contents through
|
||||
// it ran past the end of the backend allocation.
|
||||
TEST_F(BufferTest, RedefiningAnAdoptedBufferAtANewSizeStaysInBounds) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
|
||||
GLuint buffer = 0;
|
||||
GenBuffers(1, &buffer);
|
||||
BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
const GLint small[2] = {1, 2};
|
||||
BufferData(GL_ARRAY_BUFFER, sizeof(small), small, GL_DYNAMIC_DRAW);
|
||||
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
ASSERT_TRUE(bufferObject->EnsureGpuResidentStorage());
|
||||
ASSERT_EQ(mock.gpu.size(), sizeof(small));
|
||||
|
||||
const GLint large[8] = {1, 2, 3, 4, 5, 6, 7, 8};
|
||||
BufferData(GL_ARRAY_BUFFER, sizeof(large), large, GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
EXPECT_EQ(bufferObject->GetSize(), sizeof(large));
|
||||
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
|
||||
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), large, sizeof(large)), 0);
|
||||
// The old, smaller GPU block was not written through: still the old size, still the
|
||||
// old bytes.
|
||||
EXPECT_EQ(mock.gpu.size(), sizeof(small));
|
||||
EXPECT_EQ(std::memcmp(mock.gpu.data(), small, sizeof(small)), 0);
|
||||
|
||||
// And residency can be taken again, now over the new store.
|
||||
ASSERT_TRUE(bufferObject->EnsureGpuResidentStorage());
|
||||
EXPECT_TRUE(bufferObject->IsBackendPersistentMapped());
|
||||
EXPECT_EQ(mock.gpu.size(), sizeof(large));
|
||||
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), large, sizeof(large)), 0);
|
||||
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// glBufferStorage is the other way into a redefinition, and an adopted buffer can reach
|
||||
// it: the adoption came from a binding rather than from an application map, so the buffer
|
||||
// is still mutable and glBufferStorage is still legal on it.
|
||||
TEST_F(BufferTest, ImmutableStorageOnAnAdoptedBufferHandsTheMappingBackToo) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
|
||||
GLuint buffer = 0;
|
||||
GenBuffers(1, &buffer);
|
||||
BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
const GLint before[4] = {1, 2, 3, 4};
|
||||
BufferData(GL_ARRAY_BUFFER, sizeof(before), before, GL_DYNAMIC_DRAW);
|
||||
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
ASSERT_TRUE(bufferObject->EnsureGpuResidentStorage());
|
||||
ASSERT_TRUE(bufferObject->IsBackendPersistentMapped());
|
||||
|
||||
const GLint after[6] = {9, 8, 7, 6, 5, 4};
|
||||
BufferStorage(GL_ARRAY_BUFFER, sizeof(after), after, GL_MAP_READ_BIT);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
EXPECT_TRUE(bufferObject->IsImmutableStorage());
|
||||
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
|
||||
EXPECT_EQ(bufferObject->GetSize(), sizeof(after));
|
||||
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), after, sizeof(after)), 0);
|
||||
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// A redefinition to nothing. The backend declines residency for an empty store, so this
|
||||
// is also the path where the mapping is given back and never retaken.
|
||||
TEST_F(BufferTest, RedefiningAnAdoptedBufferToZeroBytesLeavesItOnTheShadow) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
|
||||
GLuint buffer = 0;
|
||||
GenBuffers(1, &buffer);
|
||||
BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
const GLint before[4] = {1, 2, 3, 4};
|
||||
BufferData(GL_ARRAY_BUFFER, sizeof(before), before, GL_DYNAMIC_DRAW);
|
||||
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
ASSERT_TRUE(bufferObject->EnsureGpuResidentStorage());
|
||||
ASSERT_TRUE(bufferObject->IsBackendPersistentMapped());
|
||||
|
||||
BufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
EXPECT_EQ(bufferObject->GetSize(), 0u);
|
||||
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
|
||||
EXPECT_FALSE(bufferObject->EnsureGpuResidentStorage()); // nothing to make resident
|
||||
|
||||
// ...and it comes back to life on the next non-empty store.
|
||||
const GLint again[3] = {5, 6, 7};
|
||||
BufferData(GL_ARRAY_BUFFER, sizeof(again), again, GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
EXPECT_TRUE(bufferObject->EnsureGpuResidentStorage());
|
||||
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), again, sizeof(again)), 0);
|
||||
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// The negative control for the four above: a backend that DECLINES to hand out a mapping
|
||||
// leaves the buffer shadow-backed throughout, so a redefinition is just a redefinition -
|
||||
// no adoption to give back, and the backend still gets its Respecify.
|
||||
TEST_F(BufferTest, RedefiningANonAdoptedBufferIsUnchanged) {
|
||||
ZeroCopyMockBackend mock;
|
||||
mock.provideMap = false;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
|
||||
GLuint buffer = 0;
|
||||
GenBuffers(1, &buffer);
|
||||
BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
const GLint before[4] = {1, 2, 3, 4};
|
||||
BufferData(GL_ARRAY_BUFFER, sizeof(before), before, GL_DYNAMIC_DRAW);
|
||||
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
EXPECT_FALSE(bufferObject->EnsureGpuResidentStorage());
|
||||
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
|
||||
mock.respecifyCalls = 0;
|
||||
|
||||
const GLint after[4] = {10, 20, 30, 40};
|
||||
BufferData(GL_ARRAY_BUFFER, sizeof(after), after, GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
|
||||
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), after, sizeof(after)), 0);
|
||||
EXPECT_EQ(mock.respecifyCalls, 1);
|
||||
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ cmake_minimum_required(VERSION 3.14)
|
||||
add_executable(
|
||||
PipelineQuirkTest
|
||||
PipelineQuirkTest.cpp
|
||||
PassthroughTessControlTest.cpp
|
||||
ViewportIndexReflectionTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(PipelineQuirkTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
// MobileGL - MobileGL/MG_Test/Pipeline/PassthroughTessControlTest.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 <gtest/gtest.h>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
|
||||
namespace {
|
||||
// A test-side SPIR-V walker, deliberately independent of the production reflection: the
|
||||
// generator's contract with the evaluation stage is "declare this many output vertices and
|
||||
// write these built-ins", and that has to be readable off the module itself.
|
||||
constexpr Uint32 kSpirvHeaderWordCount = 5;
|
||||
constexpr Uint32 kOpExecutionMode = 16;
|
||||
constexpr Uint32 kOpDecorate = 71;
|
||||
constexpr Uint32 kOpMemberDecorate = 72;
|
||||
constexpr Uint32 kExecutionModeOutputVertices = 26;
|
||||
constexpr Uint32 kDecorationBuiltIn = 11;
|
||||
|
||||
// SpvBuiltIn values used below.
|
||||
constexpr Uint32 kBuiltInPosition = 0;
|
||||
constexpr Uint32 kBuiltInInvocationId = 8;
|
||||
constexpr Uint32 kBuiltInTessLevelOuter = 11;
|
||||
constexpr Uint32 kBuiltInTessLevelInner = 12;
|
||||
|
||||
template <typename Visitor>
|
||||
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
|
||||
for (SizeT i = kSpirvHeaderWordCount; i < spirv.size();) {
|
||||
const Uint32 wordCount = spirv[i] >> 16;
|
||||
const Uint32 opcode = spirv[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
||||
visit(opcode, &spirv[i], wordCount);
|
||||
i += wordCount;
|
||||
}
|
||||
}
|
||||
|
||||
// -1 when the module declares no OutputVertices mode at all, which is itself a failure the
|
||||
// tests want to see named rather than silently compared against a wrong number.
|
||||
Int DeclaredOutputVertices(const Vector<Uint32>& spirv) {
|
||||
Int declared = -1;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == kOpExecutionMode && wordCount >= 4 && words[2] == kExecutionModeOutputVertices) {
|
||||
declared = static_cast<Int>(words[3]);
|
||||
}
|
||||
});
|
||||
return declared;
|
||||
}
|
||||
|
||||
// The built-in members of every block in the module, keyed by the struct's result id, in
|
||||
// member order. A gl_PerVertex is exactly such a struct, and its member list IS the shape the
|
||||
// neighbouring stage has to agree with.
|
||||
constexpr Uint32 kOpTypeStruct = 30;
|
||||
|
||||
std::map<Uint32, Vector<Uint32>> BuiltInBlockShapes(const Vector<Uint32>& spirv) {
|
||||
std::map<Uint32, Vector<Uint32>> shapes;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == kOpMemberDecorate && wordCount >= 5 && words[3] == kDecorationBuiltIn) {
|
||||
shapes[words[1]].push_back(words[4]);
|
||||
}
|
||||
});
|
||||
return shapes;
|
||||
}
|
||||
|
||||
// Member count of a struct type, so a shape comparison can also catch a block that grew a
|
||||
// NON-built-in member (which the decoration walk above would not see).
|
||||
Uint32 StructMemberCount(const Vector<Uint32>& spirv, Uint32 structId) {
|
||||
Uint32 count = 0;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == kOpTypeStruct && wordCount >= 2 && words[1] == structId) {
|
||||
count = wordCount - 2;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
std::set<Uint32> DeclaredBuiltIns(const Vector<Uint32>& spirv) {
|
||||
std::set<Uint32> builtIns;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == kOpDecorate && wordCount >= 4 && words[2] == kDecorationBuiltIn) {
|
||||
builtIns.insert(words[3]);
|
||||
}
|
||||
if (opcode == kOpMemberDecorate && wordCount >= 5 && words[3] == kDecorationBuiltIn) {
|
||||
builtIns.insert(words[4]);
|
||||
}
|
||||
});
|
||||
return builtIns;
|
||||
}
|
||||
|
||||
Vector<Uint32> CompileGeneratedSource(Uint32 patchVertices) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
const String source = ProgramFactory::BuildPassthroughTessControlSource(patchVertices);
|
||||
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_TESS_CONTROL_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log) << "\n" << source;
|
||||
if (!shaderResult) return {};
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_TESS_CONTROL_SHADER},
|
||||
.program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class PassthroughTessControlTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { MobileGL::Initialize(); }
|
||||
};
|
||||
|
||||
// The whole reason this stage is generated per patch size rather than once: GL takes the output
|
||||
// patch size from PATCH_VERTICES, which is draw state. A program that links at the default 3 and
|
||||
// draws at 4 - which is exactly what
|
||||
// KHR-GL43.shader_storage_buffer_object.advanced-write-tessellation does - must get a stage built
|
||||
// for 4, or its evaluation stage reads gl_in[3] out of a three-element array.
|
||||
TEST_F(PassthroughTessControlTest, DeclaresTheRequestedPatchSize) {
|
||||
for (const Uint32 patchVertices : {1u, 2u, 3u, 4u, 16u, 32u}) {
|
||||
const Vector<Uint32> spirv = CompileGeneratedSource(patchVertices);
|
||||
ASSERT_FALSE(spirv.empty()) << "patchVertices=" << patchVertices;
|
||||
EXPECT_EQ(DeclaredOutputVertices(spirv), static_cast<Int>(patchVertices))
|
||||
<< "patchVertices=" << patchVertices;
|
||||
}
|
||||
}
|
||||
|
||||
// gl_Position in, gl_Position out, and both tessellation level arrays written: the four facts the
|
||||
// evaluation stage downstream of this depends on. Position appearing at all is what makes the
|
||||
// pass-through a pass-through; the levels are what GL's PATCH_DEFAULT_*_LEVEL state supplies when
|
||||
// there is no control shader, and without them the tessellator produces nothing.
|
||||
TEST_F(PassthroughTessControlTest, ForwardsPositionAndWritesBothLevelArrays) {
|
||||
const Vector<Uint32> spirv = CompileGeneratedSource(4);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
const std::set<Uint32> builtIns = DeclaredBuiltIns(spirv);
|
||||
EXPECT_TRUE(builtIns.contains(kBuiltInPosition));
|
||||
EXPECT_TRUE(builtIns.contains(kBuiltInInvocationId));
|
||||
EXPECT_TRUE(builtIns.contains(kBuiltInTessLevelOuter));
|
||||
EXPECT_TRUE(builtIns.contains(kBuiltInTessLevelInner));
|
||||
}
|
||||
|
||||
// The generated source carries nothing but gl_Position across the interface. If that ever grows a
|
||||
// user-defined varying, ReflectPassthroughTessControlNeed's "built-ins only" refusal stops being
|
||||
// the right gate and both have to move together.
|
||||
TEST_F(PassthroughTessControlTest, InterfaceIsBuiltInsOnly) {
|
||||
const String source = ProgramFactory::BuildPassthroughTessControlSource(4);
|
||||
EXPECT_EQ(source.find("layout(location"), String::npos) << source;
|
||||
EXPECT_NE(source.find("layout(vertices = 4) out;"), String::npos) << source;
|
||||
}
|
||||
|
||||
// THE load-bearing test. Vulkan matches built-in interface blocks by their whole shape, and this
|
||||
// stage is compiled ON ITS OWN - it never goes through the glslang link that gives a real program
|
||||
// its gl_PerVertex. So the shape it declares has to equal the shape a linked vertex+evaluation
|
||||
// program carries, and nothing at runtime says otherwise: a mismatch renders a black frame, no
|
||||
// error, no validation message. That is exactly how the first cut of this shipped-and-failed
|
||||
// (gl_Position only, three members short), and how the second did (glslang's default block for a
|
||||
// standalone control stage, which appends gl_CullDistance where a linked program has no such
|
||||
// member). This links the shader pair the motivating CTS case uses and compares the two shapes
|
||||
// directly.
|
||||
TEST_F(PassthroughTessControlTest, MatchesTheFrontendPerVertexBlock) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// Deliberately the shape of KHR-GL43.shader_storage_buffer_object.advanced-write-tessellation:
|
||||
// a vertex stage feeding an evaluation stage with no control stage in between.
|
||||
static const char* kVs = R"(#version 430 core
|
||||
layout(location = 0) in vec4 g_in_position;
|
||||
void main() { gl_Position = g_in_position; }
|
||||
)";
|
||||
static const char* kTes = R"(#version 430 core
|
||||
layout(quads) in;
|
||||
void main() {
|
||||
vec4 p0 = mix(gl_in[0].gl_Position, gl_in[1].gl_Position, gl_TessCoord.x);
|
||||
vec4 p1 = mix(gl_in[3].gl_Position, gl_in[2].gl_Position, gl_TessCoord.x);
|
||||
gl_Position = mix(p0, p1, gl_TessCoord.y);
|
||||
}
|
||||
)";
|
||||
static const char* kFs = R"(#version 430 core
|
||||
layout(location = 0) out vec4 g_fs_out;
|
||||
void main() { g_fs_out = vec4(0, 1, 0, 1); }
|
||||
)";
|
||||
|
||||
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_EVALUATION_SHADER, GL_FRAGMENT_SHADER};
|
||||
const Vector<const char*> sources{kVs, kTes, kFs};
|
||||
Vector<SharedPtr<glslang::TShader>> shaders;
|
||||
for (SizeT i = 0; i < types.size(); ++i) {
|
||||
ShaderAttrib attrib{.shaderType = types[i], .sourceStr = sources[i]};
|
||||
auto compiled = ShaderCompiler::CompileShader(attrib);
|
||||
ASSERT_TRUE(compiled) << compiled.error().log;
|
||||
shaders.push_back(compiled.value());
|
||||
}
|
||||
ProgramAttrib programAttrib{.shaders = shaders};
|
||||
auto linked = ShaderCompiler::LinkProgram(programAttrib);
|
||||
ASSERT_TRUE(linked) << linked.error().log;
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = types, .program = *linked.value()};
|
||||
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
ASSERT_TRUE(binary);
|
||||
ASSERT_EQ(binary->size(), types.size());
|
||||
|
||||
// The evaluation stage's gl_in is the block the pass-through has to feed. It is the only
|
||||
// built-in block that stage declares as an input, so the module holds exactly one such shape
|
||||
// besides its own gl_PerVertex output - and both are the same shape, which is the point.
|
||||
const auto tesShapes = BuiltInBlockShapes((*binary)[1]);
|
||||
ASSERT_FALSE(tesShapes.empty());
|
||||
const Vector<Uint32> frontendShape = tesShapes.begin()->second;
|
||||
const Uint32 frontendMembers = StructMemberCount((*binary)[1], tesShapes.begin()->first);
|
||||
for (const auto& [structId, shape] : tesShapes) {
|
||||
EXPECT_EQ(shape, frontendShape) << "the evaluation stage's own built-in blocks disagree";
|
||||
EXPECT_EQ(StructMemberCount((*binary)[1], structId), frontendMembers);
|
||||
}
|
||||
|
||||
const Vector<Uint32> passthrough = CompileGeneratedSource(4);
|
||||
ASSERT_FALSE(passthrough.empty());
|
||||
const auto passthroughShapes = BuiltInBlockShapes(passthrough);
|
||||
ASSERT_FALSE(passthroughShapes.empty());
|
||||
|
||||
Uint32 perVertexBlocksChecked = 0;
|
||||
for (const auto& [structId, shape] : passthroughShapes) {
|
||||
// gl_TessLevelOuter/Inner are decorated on plain variables, not on a block, so every
|
||||
// struct that reaches here is a gl_PerVertex - gl_in's and gl_out's.
|
||||
EXPECT_EQ(shape, frontendShape)
|
||||
<< "the pass-through control stage's gl_PerVertex no longer matches the one the "
|
||||
"frontend gives a linked vertex+evaluation program";
|
||||
EXPECT_EQ(StructMemberCount(passthrough, structId), frontendMembers)
|
||||
<< "the pass-through control stage's gl_PerVertex has a different member count";
|
||||
++perVertexBlocksChecked;
|
||||
}
|
||||
EXPECT_EQ(perVertexBlocksChecked, 2u) << "expected both gl_in and gl_out to be gl_PerVertex blocks";
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// MobileGL - MobileGL/MG_Test/Pipeline/ViewportIndexReflectionTest.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
|
||||
//
|
||||
// ProgramFactory::ReflectedWritesViewportIndexBuiltin is the switch that decides whether a
|
||||
// DirectVulkan pipeline declares one viewport or all sixteen. Getting it wrong is silent in both
|
||||
// directions and neither direction is caught by a state test:
|
||||
//
|
||||
// - a false NEGATIVE collapses every gl_ViewportIndex onto viewport 0, which is precisely the
|
||||
// bug the multi-viewport work exists to fix and which a set/get round trip cannot see;
|
||||
// - a false POSITIVE widens viewportCount for an ordinary Minecraft shader, costing a longer
|
||||
// vkCmdSetViewport per state change and, on a tiler, possibly a hardware fast path.
|
||||
//
|
||||
// So this compiles REAL GLSL through the same glslang path the renderer uses and reflects the
|
||||
// SPIR-V that comes out, rather than asserting against hand-assembled words: what has to hold is
|
||||
// that the detector agrees with what glslang actually emits for a shader that writes the builtin,
|
||||
// including the stage-by-stage question of WHERE it may be written (GL 4.1 allows the geometry
|
||||
// stage; ARB_shader_viewport_layer_array adds vertex and tessellation evaluation).
|
||||
//
|
||||
// The end-to-end claim - that a detected writer really does route pixels to its own viewport -
|
||||
// lives in MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
|
||||
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv_reflect.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
|
||||
namespace {
|
||||
|
||||
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||
if (!shaderResult) return {};
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
// Owns the reflection module so a failing EXPECT cannot leak it.
|
||||
class ReflectModule {
|
||||
public:
|
||||
explicit ReflectModule(const Vector<Uint32>& spirv) {
|
||||
if (spirv.empty()) return;
|
||||
m_created = spvReflectCreateShaderModule(spirv.size() * sizeof(Uint32), spirv.data(), &m_module) ==
|
||||
SPV_REFLECT_RESULT_SUCCESS;
|
||||
}
|
||||
~ReflectModule() {
|
||||
if (m_created) spvReflectDestroyShaderModule(&m_module);
|
||||
}
|
||||
ReflectModule(const ReflectModule&) = delete;
|
||||
ReflectModule& operator=(const ReflectModule&) = delete;
|
||||
|
||||
Bool Created() const { return m_created; }
|
||||
const SpvReflectShaderModule& Get() const { return m_module; }
|
||||
|
||||
private:
|
||||
SpvReflectShaderModule m_module{};
|
||||
Bool m_created = false;
|
||||
};
|
||||
|
||||
class ViewportIndexReflectionTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { MobileGL::Initialize(); }
|
||||
};
|
||||
|
||||
const char* const kGeometryWritesViewportIndex = R"(#version 410 core
|
||||
layout(points, invocations = 16) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
void main() {
|
||||
gl_ViewportIndex = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// Same stage, same shape, writing gl_Layer INSTEAD. Layered rendering and viewport routing
|
||||
// are different features and the detector must not confuse them: a Minecraft-style cubemap
|
||||
// pass writes gl_Layer and must keep the one-viewport pipeline.
|
||||
const char* const kGeometryWritesLayerOnly = R"(#version 410 core
|
||||
layout(points, invocations = 6) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
void main() {
|
||||
gl_Layer = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPlainGeometry = R"(#version 410 core
|
||||
layout(points, invocations = 1) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
void main() {
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPlainVertex = R"(#version 410 core
|
||||
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
const char* const kPlainFragment = R"(#version 410 core
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, TrueForAGeometryShaderThatAssignsViewportIndex) {
|
||||
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesViewportIndex));
|
||||
ASSERT_TRUE(module.Created());
|
||||
EXPECT_TRUE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()))
|
||||
<< "a shader that assigns gl_ViewportIndex must get a multi-viewport pipeline; missing it is what "
|
||||
"collapses every index onto viewport 0";
|
||||
}
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, FalseForAGeometryShaderThatOnlyAssignsLayer) {
|
||||
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesLayerOnly));
|
||||
ASSERT_TRUE(module.Created());
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()))
|
||||
<< "gl_Layer is layered rendering, not viewport routing; widening viewportCount for it costs the "
|
||||
"single-viewport fast path for nothing";
|
||||
}
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, FalseForAPlainGeometryShader) {
|
||||
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kPlainGeometry));
|
||||
ASSERT_TRUE(module.Created());
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()));
|
||||
}
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, FalseForTheOrdinaryVertexAndFragmentStages) {
|
||||
// The shape every real application ships: neither stage may widen the pipeline.
|
||||
const ReflectModule vertexModule(CompileToSpirv(GL_VERTEX_SHADER, kPlainVertex));
|
||||
ASSERT_TRUE(vertexModule.Created());
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(vertexModule.Get()));
|
||||
|
||||
const ReflectModule fragmentModule(CompileToSpirv(GL_FRAGMENT_SHADER, kPlainFragment));
|
||||
ASSERT_TRUE(fragmentModule.Created());
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(fragmentModule.Get()));
|
||||
}
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, FalseForAnEmptyModuleWithoutDereferencing) {
|
||||
// A default-constructed module has no entry points. The scan runs on every link, so it
|
||||
// must survive a reflection that never got built rather than walk a null array.
|
||||
SpvReflectShaderModule emptyModule{};
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(emptyModule));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -516,17 +516,17 @@ TEST_F(ParallelShaderCompileTest, MaxShaderCompilerThreadsIgnoresTheCurrentBudge
|
||||
TEST_F(ParallelShaderCompileTest, BothBackendsAdvertiseTheExtensionIffAsyncIsEnabled) {
|
||||
{
|
||||
const AsyncModeScope async(true);
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false),
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile));
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false),
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile));
|
||||
}
|
||||
{
|
||||
const AsyncModeScope async(false);
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false),
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile))
|
||||
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false),
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile))
|
||||
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
|
||||
}
|
||||
|
||||
@@ -2692,11 +2692,13 @@ out vec4 fragColor;
|
||||
float fma
|
||||
(float a, float b, float c) { return a * b + c; }
|
||||
float sinh(float x, float y) { return x * y; }
|
||||
float length_squared(vec3 value) { return dot(value, value); }
|
||||
float round(float x) { return floor(x + 0.5); }
|
||||
float min3(float a, float b, float c) { return min(min(a, b), c); }
|
||||
|
||||
void main() {
|
||||
fragColor = vec4(fma(0.1, 0.2, 0.3), sinh(0.4, 2.0), round(1.25), min3(0.1, 0.2, 0.3));
|
||||
fragColor = vec4(fma(0.1, 0.2, 0.3), sinh(0.4, 2.0), round(1.25),
|
||||
min3(0.1, 0.2, 0.3) + length_squared(vec3(0.1, 0.2, 0.3)));
|
||||
}
|
||||
)";
|
||||
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
|
||||
@@ -2707,6 +2709,7 @@ void main() {
|
||||
if (essl.find("fragColor") == String::npos) continue; // fragment module only
|
||||
EXPECT_NE(essl.find("mg_fma("), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("mg_sinh("), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("mg_length_squared("), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("mg_round("), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("mg_min3("), String::npos) << essl;
|
||||
EXPECT_EQ(essl.find("float fma("), String::npos) << essl;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
#include <MG_Util/ShaderTranspiler/glslang/UniformTraverser.h>
|
||||
@@ -51,20 +52,24 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
||||
OpEntryPoint Fragment %main "main" %outColor
|
||||
OpExecutionMode %main OriginUpperLeft
|
||||
OpName %globalSampler "sampler"
|
||||
OpName %globalNew "new"
|
||||
OpName %paramSampler "sampler"
|
||||
OpName %paramNew "new"
|
||||
OpName %main "main"
|
||||
OpDecorate %outColor Location 0
|
||||
%void = OpTypeVoid
|
||||
%float = OpTypeFloat 32
|
||||
%v4float = OpTypeVector %float 4
|
||||
%mainFn = OpTypeFunction %void
|
||||
%paramFn = OpTypeFunction %void %float
|
||||
%paramFn = OpTypeFunction %void %float %float
|
||||
%outV4Ptr = OpTypePointer Output %v4float
|
||||
%privatePtr = OpTypePointer Private %float
|
||||
%outColor = OpVariable %outV4Ptr Output
|
||||
%globalSampler = OpVariable %privatePtr Private
|
||||
%globalNew = OpVariable %privatePtr Private
|
||||
%helper = OpFunction %void None %paramFn
|
||||
%paramSampler = OpFunctionParameter %float
|
||||
%paramNew = OpFunctionParameter %float
|
||||
%helperBody = OpLabel
|
||||
OpReturn
|
||||
OpFunctionEnd
|
||||
@@ -90,6 +95,7 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
||||
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
|
||||
|
||||
EXPECT_NE(outputText.find("\"MGL_COMPAT_sampler\""), String::npos);
|
||||
EXPECT_NE(outputText.find("\"MGL_COMPAT_new\""), String::npos);
|
||||
|
||||
SizeT exactSamplerNameCount = 0;
|
||||
SizeT searchOffset = 0;
|
||||
@@ -98,6 +104,14 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
||||
searchOffset += std::strlen("\"sampler\"");
|
||||
}
|
||||
EXPECT_EQ(exactSamplerNameCount, 1u);
|
||||
|
||||
SizeT exactNewNameCount = 0;
|
||||
searchOffset = 0;
|
||||
while ((searchOffset = outputText.find("\"new\"", searchOffset)) != String::npos) {
|
||||
++exactNewNameCount;
|
||||
searchOffset += std::strlen("\"new\"");
|
||||
}
|
||||
EXPECT_EQ(exactNewNameCount, 1u);
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, UnformattedFloatStorageImagesKeepIntegerAtomicImagesTyped) {
|
||||
@@ -3481,3 +3495,523 @@ void main() { fragColor = vec4(texelFetch(Data, 3)); }
|
||||
<< essl320;
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
// OpTypeImage words: result id (+1), sampled type (+2), Dim (+3), Depth (+4), Arrayed (+5),
|
||||
// MS (+6), Sampled (+7). Dim::Dim1D == 0, and Sampled == 2 is a storage image.
|
||||
SizeT Count1DArrayStorageImageTypes(const Vector<Uint32>& spirv) {
|
||||
constexpr unsigned kOpTypeImage = 25, kDim1D = 0;
|
||||
SizeT count = 0;
|
||||
for (SizeT i = 5; i < spirv.size();) {
|
||||
const unsigned wordCount = spirv[i] >> 16;
|
||||
const unsigned opcode = spirv[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
||||
if (opcode == kOpTypeImage && wordCount >= 8 && spirv[i + 3] == kDim1D && spirv[i + 5] == 1u &&
|
||||
spirv[i + 7] == 2u) {
|
||||
++count;
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
const char* k1DArrayImageCompute = R"(#version 440 core
|
||||
layout (local_size_x = 1) in;
|
||||
layout (location = 0, r32ui) readonly uniform uimage1DArray i0;
|
||||
layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
|
||||
void main() { ssb.sum = imageLoad(i0, ivec2(2, 3)).r; }
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
// The negative control, and the whole reason the pass exists: SPIRV-Cross's ES emulation of 1D
|
||||
// images does not ask whether the type is arrayed, so it wraps an already-two-component
|
||||
// coordinate in a two-component constructor. Pinning the upstream behaviour here means that if a
|
||||
// future SPIRV-Cross bump fixes it, this test fails and says so, rather than the pass quietly
|
||||
// becoming dead weight.
|
||||
TEST_F(ProgramUtilTest, SpirvCrossEmitsAMalformedCoordinateFor1DArrayImages) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(k1DArrayImageCompute, GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_EQ(Count1DArrayStorageImageTypes(spirv), 1u)
|
||||
<< "glslang no longer emits a Dim1D/Arrayed/Sampled=2 image for uimage1DArray";
|
||||
|
||||
const String essl = DecompileToEssl(spirv);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_NE(essl.find("ivec2(ivec2("), String::npos)
|
||||
<< "SPIRV-Cross is expected to emit ivec2(ivec2(...), 0) here - three components in a "
|
||||
"two-component constructor, which every ES driver rejects. If this no longer happens, "
|
||||
"Lower1DArrayImagesForEssl may no longer be needed:\n"
|
||||
<< essl;
|
||||
}
|
||||
|
||||
// The fix: the type becomes a 2D array and the coordinate becomes three components, so
|
||||
// SPIRV-Cross's 1D path never fires and the emitted ESSL is something a driver accepts.
|
||||
TEST_F(ProgramUtilTest, Lower1DArrayImagesRewritesTheTypeAndWidensTheCoordinate) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> raw = BuildSpirvForStage(k1DArrayImageCompute, GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(raw.empty());
|
||||
|
||||
// Through the shared chain first, exactly as the DirectGLES transpile path does: the pass
|
||||
// runs on sanitized bytes, and the explicit uniform LOCATION this fixture carries (the
|
||||
// conformance case's own spelling) is illegal on UniformConstant storage until
|
||||
// StripUniformLocationsPass has removed it. Validating raw glslang output would latch that
|
||||
// pre-existing property against this pass.
|
||||
Vector<Uint32> spirv;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
|
||||
ASSERT_EQ(Count1DArrayStorageImageTypes(spirv), 1u)
|
||||
<< "the shared chain must leave the 1D-array image for this pass to handle";
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
|
||||
ASSERT_FALSE(lowered.empty());
|
||||
|
||||
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 0u)
|
||||
<< "no 1D-array storage image type may survive the pass:\n"
|
||||
<< DisassembleSpirv(lowered);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the lowered module must stay validator-clean";
|
||||
|
||||
const String essl = DecompileToEssl(lowered);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_NE(essl.find("uimage2DArray"), String::npos)
|
||||
<< "the image must be declared as the 2D array the texture is stored as:\n" << essl;
|
||||
EXPECT_EQ(essl.find("ivec2(ivec2("), String::npos)
|
||||
<< "the malformed constructor must be gone:\n" << essl;
|
||||
// The ORDER is the whole point, and it is what a widening that merely appended the 0 would
|
||||
// get wrong while still producing a three-component constructor that compiles. The fixture
|
||||
// reads (u=2, layer=3), and the ES 2D array holds height 1 with the layers in depth
|
||||
// (TextureImpl::GetBackendUploadSize), so the only correct spelling is (2, 0, 3).
|
||||
EXPECT_NE(essl.find("ivec3(2, 0, 3)"), String::npos)
|
||||
<< "the layer must land in the third component and Y must be 0; ivec3(2, 3, 0) would read "
|
||||
"row 3 of a one-row texture and layer 0 of every access:\n"
|
||||
<< essl;
|
||||
}
|
||||
|
||||
// The shape that made the first cut of this pass emit INVALID SPIR-V, and the shape the
|
||||
// conformance case actually has: a 1D-array image and a real 2D-array image of the same sampled
|
||||
// type and format in one module. Rewriting the first one's Dim in place makes the two
|
||||
// OpTypeImage declarations structurally identical, and SPIR-V forbids duplicate non-aggregate
|
||||
// types - so the module the ESSL path hands on failed validation and quietly bumped the latch.
|
||||
// A single-image fixture cannot see any of that.
|
||||
TEST_F(ProgramUtilTest, Lower1DArrayImagesDeduplicatesAgainstAnExisting2DArrayImage) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> raw = BuildSpirvForStage(R"(#version 440 core
|
||||
layout (local_size_x = 1) in;
|
||||
layout (location = 0, r32ui) readonly uniform uimage1DArray i0;
|
||||
layout (location = 1, r32ui) readonly uniform uimage2DArray i1;
|
||||
layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
|
||||
void main() { ssb.sum = imageLoad(i0, ivec2(2, 3)).r + imageLoad(i1, ivec3(1, 1, 1)).r; }
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(raw.empty());
|
||||
|
||||
Vector<Uint32> spirv;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
|
||||
ASSERT_EQ(Count1DArrayStorageImageTypes(spirv), 1u);
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
|
||||
ASSERT_FALSE(lowered.empty());
|
||||
|
||||
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 0u) << DisassembleSpirv(lowered);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the rewritten 1D-array image collided with the module's own 2D-array image and left a "
|
||||
"duplicate type declaration behind:\n"
|
||||
<< DisassembleSpirv(lowered);
|
||||
|
||||
const String essl = DecompileToEssl(lowered);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_NE(essl.find("ivec3(2, 0, 3)"), String::npos) << essl;
|
||||
}
|
||||
|
||||
// Scope, half one: a NON-arrayed 1D storage image is emitted correctly by the very same
|
||||
// SPIRV-Cross code, so the pass must not touch it - replacing working emission with our own buys
|
||||
// nothing and risks everything.
|
||||
TEST_F(ProgramUtilTest, Lower1DArrayImagesLeavesNonArrayed1DImagesToSpirvCross) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 440 core
|
||||
layout (local_size_x = 1) in;
|
||||
layout (location = 0, r32ui) readonly uniform uimage1D i0;
|
||||
layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
|
||||
void main() { ssb.sum = imageLoad(i0, 2).r; }
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
|
||||
EXPECT_EQ(lowered, spirv) << "a non-arrayed 1D storage image must pass through byte for byte";
|
||||
|
||||
const String essl = DecompileToEssl(lowered);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_NE(essl.find("uimage2D "), String::npos)
|
||||
<< "SPIRV-Cross's own 1D-as-2D emulation must still be what handles this:\n" << essl;
|
||||
}
|
||||
|
||||
// Scope, half two: a 1D-array SAMPLER reaches SPIRV-Cross's sampler path, which does check
|
||||
// `arrayed` and does move the layer into the third component. The pass is storage-image only.
|
||||
TEST_F(ProgramUtilTest, Lower1DArrayImagesLeavesSampledImagesAlone) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 440 core
|
||||
uniform sampler1DArray uTex;
|
||||
in vec2 vUv;
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = texture(uTex, vUv); }
|
||||
)",
|
||||
GL_FRAGMENT_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
|
||||
EXPECT_EQ(lowered, spirv) << "a sampled 1D-array image must pass through byte for byte";
|
||||
}
|
||||
|
||||
// The declined shape. After the rewrite the image is a 2D array, so a size query on it yields
|
||||
// three components where the shader consumes two, and there is no correct two-component answer to
|
||||
// substitute - the ES texture genuinely has a height the GL one does not. The module is handed
|
||||
// back untouched rather than half-translated.
|
||||
TEST_F(ProgramUtilTest, Lower1DArrayImagesDeclinesAModuleThatQueriesTheImageSize) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 440 core
|
||||
layout (local_size_x = 1) in;
|
||||
layout (location = 0, r32ui) readonly uniform uimage1DArray i0;
|
||||
layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
|
||||
void main() { ssb.sum = uint(imageSize(i0).x) + imageLoad(i0, ivec2(0, 0)).r; }
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const auto traits = Lower1DArrayImagesPass::InspectBinary(spirv);
|
||||
ASSERT_TRUE(traits.declaresImage && traits.queriesImageSize)
|
||||
<< "the fixture must contain the shape the pass declines";
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
|
||||
EXPECT_EQ(lowered, spirv) << "a declined module must be handed back untouched, not partly rewritten";
|
||||
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 1u)
|
||||
<< "declining means the 1D-array type is still there for the driver to reject";
|
||||
}
|
||||
|
||||
// --- image format qualifier bake (BakeImageFormatsPass) ---------------------------------------
|
||||
//
|
||||
// Desktop GLSL 4.2 lets a writeonly image declaration omit its format layout qualifier; GLSL ES
|
||||
// requires one of every image, and Adreno says so as "all images have to define layout format",
|
||||
// losing the whole program. The only correct qualifier to substitute is the format the
|
||||
// application passed to glBindImageTexture for that unit, so the transpile bakes it in.
|
||||
|
||||
namespace {
|
||||
Uint CountSpirvOpcode(const String& disassembly, const String& opcode) {
|
||||
Uint count = 0;
|
||||
SizeT offset = 0;
|
||||
const String needle = opcode + " ";
|
||||
while ((offset = disassembly.find(needle, offset)) != String::npos) {
|
||||
count += 1;
|
||||
offset += needle.size();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
constexpr Uint kGlR32ui = 0x8236;
|
||||
constexpr Uint kGlRgba32ui = 0x8D70;
|
||||
constexpr Uint kGlR8ui = 0x8232;
|
||||
constexpr Uint kGlR32f = 0x822E;
|
||||
} // namespace
|
||||
|
||||
// The KHR-GL4x.packed_depth_stencil.stencil_texturing compute shader, reduced: one format-less
|
||||
// writeonly image, and a bind of a concrete format to the unit it addresses. (The DEPTH half of
|
||||
// that case binds GL_R32F; the stencil half's GL_R8UI is one SPIRV-Cross will not print and takes
|
||||
// the text route instead - see the test below.)
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsGivesAFormatlessImageTheFormatBoundToItsUnit) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
layout (local_size_x = 1) in;
|
||||
writeonly uniform uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(15u, 0u, 0u, 0u)); }
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv))
|
||||
<< "the fixture must reproduce the defect before the fix is asked to remove it:\n"
|
||||
<< DisassembleSpirv(spirv);
|
||||
// Precondition: SPIRV-Cross prints no format for it, which is the ESSL the driver refuses.
|
||||
EXPECT_EQ(DecompileToEssl(spirv).find("r32ui"), String::npos);
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
|
||||
ASSERT_FALSE(baked.empty());
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked)) << DisassembleSpirv(baked);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the baked module must stay validator-clean:\n"
|
||||
<< DisassembleSpirv(baked);
|
||||
|
||||
const String essl = DecompileToEssl(baked);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_NE(essl.find("r32ui"), String::npos)
|
||||
<< "the bound format must reach the declaration as a layout qualifier:\n" << essl;
|
||||
EXPECT_NE(essl.find("writeonly"), String::npos)
|
||||
<< "the access qualifier the declaration already had must survive:\n" << essl;
|
||||
}
|
||||
|
||||
// SPIRV-Cross THROWS rather than printing the formats it calls desktop-only when it targets ESSL
|
||||
// (Compiler::is_desktop_only_format), and a throw loses the whole stage - so baking one of those
|
||||
// into the module would trade a missing qualifier for a missing shader. They are left format-less
|
||||
// here and completed on the emitted text instead (PrgramImpl::BakeImageFormatQualifiers). r8ui,
|
||||
// which the stencil half of the packed_depth_stencil case binds, is one of them.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsLeavesTheFormatsSpirvCrossRefusesToPrint) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR8ui))
|
||||
<< "if SPIRV-Cross ever learns to print r8ui for ES, the text completion can go";
|
||||
ASSERT_TRUE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR32ui));
|
||||
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(kGlR8ui), "r8ui");
|
||||
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(0x8051 /*GL_RGB8*/), "");
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
layout (local_size_x = 1) in;
|
||||
writeonly uniform uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); }
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR8ui}}, baked));
|
||||
EXPECT_EQ(baked, spirv) << "a format SPIRV-Cross cannot print must leave the module untouched";
|
||||
// ...and the stage still transpiles, which is the whole point of declining.
|
||||
EXPECT_FALSE(DecompileToEssl(baked).empty());
|
||||
}
|
||||
|
||||
// A DECLARED format is authoritative: GL requires the qualifier, the bind format and the
|
||||
// texture's internal format to be in the same class, but the qualifier is what the shader is
|
||||
// specified to read the memory as, and a bake that overrode it would change what the shader does.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsNeverOverridesADeclaredFormat) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
layout (local_size_x = 1) in;
|
||||
layout (binding = 0, rgba32ui) writeonly uniform uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
|
||||
|
||||
Vector<Uint32> baked;
|
||||
// Even asked to, with a format of the right component class.
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
|
||||
EXPECT_EQ(baked, spirv) << "a module with nothing format-less must pass through byte for byte";
|
||||
EXPECT_NE(DecompileToEssl(baked).find("rgba32ui"), String::npos);
|
||||
}
|
||||
|
||||
// Review finding. Every use has to be one the retype can carry end to end, and the decision has
|
||||
// to be made BEFORE anything is mutated - a half-retyped module is not something a later decline
|
||||
// could undo. An image handed to a FUNCTION is the shape that reaches SPIRV-Cross intact (nothing
|
||||
// in the ESSL chain inlines), and its OpFunctionCall is a use this pass does not follow.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsDeclinesAnImagePassedToAFunction) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
layout (local_size_x = 1) in;
|
||||
writeonly uniform uimage2D uni_image;
|
||||
void writeIt(writeonly uimage2D img) { imageStore(img, ivec2(0), uvec4(1u)); }
|
||||
void main() { writeIt(uni_image); }
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
|
||||
EXPECT_EQ(baked, spirv) << "a shape the retype cannot follow must leave the module untouched, "
|
||||
"not partly rewritten:\n"
|
||||
<< DisassembleSpirv(baked);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
|
||||
}
|
||||
|
||||
// spirv-val requires the Image Format's component class to agree with the OpTypeImage's Sampled
|
||||
// Type. Binding a uint format to a float image is an application error GL leaves undefined;
|
||||
// baking it would turn that into an INVALID module, which is strictly worse than the compile
|
||||
// error the shader already has, so the image is left format-less.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsDeclinesAFormatOfTheWrongComponentClass) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
layout (local_size_x = 1) in;
|
||||
writeonly uniform image2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), vec4(1.0)); }
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
|
||||
EXPECT_EQ(baked, spirv) << "a declined module must be handed back untouched, not partly rewritten";
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
|
||||
|
||||
// ...and the same image with a float bind format is baked, so the decline above is about the
|
||||
// class and not about the pass refusing float images.
|
||||
Vector<Uint32> bakedFloat;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32f}}, bakedFloat));
|
||||
EXPECT_NE(DecompileToEssl(bakedFloat).find("r32f"), String::npos) << DisassembleSpirv(bakedFloat);
|
||||
}
|
||||
|
||||
// Two format-less images of the same type share ONE OpTypeImage. Giving them different formats
|
||||
// therefore cannot be an in-place edit of that type - each needs its own declaration, and the
|
||||
// variable, the loads and (for arrays) the access chains all have to follow.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsSplitsATypeTwoImagesShareWhenTheirFormatsDiffer) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
layout (local_size_x = 1) in;
|
||||
writeonly uniform uimage2D imgA;
|
||||
writeonly uniform uimage2D imgB;
|
||||
void main() {
|
||||
imageStore(imgA, ivec2(0), uvec4(1u));
|
||||
imageStore(imgB, ivec2(0), uvec4(2u));
|
||||
}
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_EQ(CountSpirvOpcode(DisassembleSpirv(spirv), "OpTypeImage"), 1u)
|
||||
<< "the fixture must have the two images sharing one type:\n" << DisassembleSpirv(spirv);
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(
|
||||
spirv, {{"imgA", kGlR32ui}, {"imgB", kGlRgba32ui}}, baked));
|
||||
ASSERT_FALSE(baked.empty());
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "splitting the shared type must not leave a dangling or duplicate declaration:\n"
|
||||
<< DisassembleSpirv(baked);
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked));
|
||||
|
||||
const String essl = DecompileToEssl(baked);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_NE(essl.find("r32ui"), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("rgba32ui"), String::npos) << essl;
|
||||
}
|
||||
|
||||
// The mirror of the split: when the module ALREADY declares the type the bake wants, the two must
|
||||
// be JOINED, not duplicated. SPIR-V forbids two identical non-aggregate type declarations, and
|
||||
// that is exactly the defect an earlier image pass shipped and a reviewer caught.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsJoinsATypeTheModuleAlreadyDeclares) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
layout (local_size_x = 1) in;
|
||||
writeonly uniform uimage2D formatless;
|
||||
layout (binding = 1, r32ui) writeonly uniform uimage2D declared;
|
||||
void main() {
|
||||
imageStore(formatless, ivec2(0), uvec4(1u));
|
||||
imageStore(declared, ivec2(0), uvec4(2u));
|
||||
}
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_EQ(CountSpirvOpcode(DisassembleSpirv(spirv), "OpTypeImage"), 2u)
|
||||
<< "the fixture needs one Unknown-format and one r32ui image type:\n" << DisassembleSpirv(spirv);
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"formatless", kGlR32ui}}, baked));
|
||||
ASSERT_FALSE(baked.empty());
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the baked image collided with the module's own r32ui image and left a duplicate type:\n"
|
||||
<< DisassembleSpirv(baked);
|
||||
EXPECT_EQ(CountSpirvOpcode(DisassembleSpirv(baked), "OpTypeImage"), 1u)
|
||||
<< "the two identical image types must be the same declaration:\n" << DisassembleSpirv(baked);
|
||||
}
|
||||
|
||||
// An ARRAY of format-less images: the variable's type is a pointer to an array, every use goes
|
||||
// through an OpAccessChain, and all three levels have to be rebuilt for the load to still type-check.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsRetypesAnArrayOfFormatlessImagesThroughItsAccessChains) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
layout (local_size_x = 1) in;
|
||||
writeonly uniform uimage2D imgs[2];
|
||||
void main() {
|
||||
for (int i = 0; i < 2; ++i) imageStore(imgs[i], ivec2(0), uvec4(uint(i)));
|
||||
}
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"imgs", kGlR32ui}}, baked));
|
||||
ASSERT_FALSE(baked.empty());
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked)) << DisassembleSpirv(baked);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the array and pointer types above the image must have been rebuilt too:\n"
|
||||
<< DisassembleSpirv(baked);
|
||||
EXPECT_NE(DecompileToEssl(baked).find("r32ui"), String::npos);
|
||||
}
|
||||
|
||||
// A SAMPLED image's format operand is Unknown in every GLSL dialect and has no qualifier to bake;
|
||||
// only storage images (Sampled == 2) are in scope.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsLeavesSampledImagesAlone) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
uniform usampler2D uni_sampler;
|
||||
out uvec4 fragColor;
|
||||
in vec2 vUv;
|
||||
void main() { fragColor = texture(uni_sampler, vUv); }
|
||||
)",
|
||||
GL_FRAGMENT_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv))
|
||||
<< "a sampled image must not read as a format-less STORAGE image:\n" << DisassembleSpirv(spirv);
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_sampler", kGlR32ui}}, baked));
|
||||
EXPECT_EQ(baked, spirv) << "a sampled image must pass through byte for byte";
|
||||
}
|
||||
|
||||
// The core/extended split the emitted ESSL depends on: GLSL ES has thirteen image formats, and a
|
||||
// bind format outside them only compiles with GL_NV_image_formats - which the backend must not
|
||||
// request on a driver that does not advertise it.
|
||||
TEST_F(ProgramUtilTest, EsslCoreImageFormatSetIsTheThirteenTheSpecLists) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlR32ui));
|
||||
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlRgba32ui));
|
||||
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlR32f));
|
||||
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8058 /*GL_RGBA8*/));
|
||||
// The stencil half of KHR-GL4x.packed_depth_stencil.stencil_texturing binds this one, and it
|
||||
// is NOT core - the whole reason the directive machinery exists.
|
||||
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlR8ui));
|
||||
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x822D /*GL_R16F*/));
|
||||
// Not an image format at all.
|
||||
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8051 /*GL_RGB8*/));
|
||||
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0 /*GL_NONE*/));
|
||||
}
|
||||
|
||||
@@ -448,6 +448,39 @@ TEST_F(QueryTest, BackendResultsPropagateThroughFrontend) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(QueryTest, DestroyAllQueryObjectsReclaimsRegistryAndResetsContextState) {
|
||||
const ScopedFeaturesOverride featuresGuard;
|
||||
const ScopedBackendFunctionsOverride backendGuard;
|
||||
InstallStubBackendTimerQueries();
|
||||
MG_Config::Features.DisableTimerQuery = false;
|
||||
|
||||
GLuint id = 0;
|
||||
MG_Impl::GLImpl::GenQueries(1, &id);
|
||||
ASSERT_NE(id, 0u);
|
||||
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, id);
|
||||
|
||||
GLint currentQuery = -1;
|
||||
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_CURRENT_QUERY, ¤tQuery);
|
||||
EXPECT_EQ(currentQuery, static_cast<GLint>(id));
|
||||
|
||||
// Full teardown drains the registry through this function while the backend
|
||||
// table is still valid. The unread backend handle must be released, the query
|
||||
// must disappear, and a fresh context must restart with no active query and a
|
||||
// fresh name allocator.
|
||||
MG_Impl::GLImpl::DestroyAllQueryObjects();
|
||||
EXPECT_EQ(g_stubDeleteCount, 1);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(id), GL_FALSE);
|
||||
|
||||
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_CURRENT_QUERY, ¤tQuery);
|
||||
EXPECT_EQ(currentQuery, 0);
|
||||
|
||||
GLuint freshId = 0;
|
||||
MG_Impl::GLImpl::GenQueries(1, &freshId);
|
||||
EXPECT_EQ(freshId, 1u);
|
||||
MG_Impl::GLImpl::DeleteQueries(1, &freshId);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// Environment-agnostic property test for the env -> ConfigLoader -> Features
|
||||
// chain: whatever MOBILEGL_DISABLE_TIMERQUERY is set to in the environment of
|
||||
// this test process, MG_ConfigLoader::Init must have parsed it with the
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user