mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53c39d2421 | ||
|
|
9d1b280375 | ||
|
|
8acd885594 | ||
|
|
10ff5e2b18 | ||
|
|
a6e52476f3 | ||
|
|
0deff52a1b | ||
|
|
50fefca959 | ||
|
|
42ad62b54c | ||
|
|
f41403e227 | ||
|
|
5fbb17f6b9 | ||
|
|
92d8f7269b | ||
|
|
822e405c77 | ||
|
|
b8233f9c4e | ||
|
|
91475a7b6f |
@@ -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(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(BUILD_EXTERNAL ON CACHE BOOL "Build external deps in External/" FORCE)
|
||||||
set(ENABLE_GLSLANG_INSTALL OFF CACHE BOOL "Install glslang targets" 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_C_API ON CACHE BOOL "Enable C API" FORCE)
|
||||||
set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE)
|
set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE)
|
||||||
|
|||||||
+7
-5
@@ -14,6 +14,7 @@
|
|||||||
#include <MG_State/EGLState/Core.h>
|
#include <MG_State/EGLState/Core.h>
|
||||||
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
||||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.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_Impl/GLImpl/Sync/GL_Sync.h>
|
||||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.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
|
// both of which this function is about to destroy. This is the one
|
||||||
// cancellation path in the whole design that waits.
|
// cancellation path in the whole design that waits.
|
||||||
MG_Util::Async::ShaderCompilePool::Get().StopAndDrain();
|
MG_Util::Async::ShaderCompilePool::Get().StopAndDrain();
|
||||||
// GL syncs die with their contexts, and every context is gone by the
|
// GL syncs and queries die with their contexts, and every context is gone
|
||||||
// time full teardown runs: drain the live-sync registry while the
|
// by the time full teardown runs: drain both live registries while the
|
||||||
// backend function table can still release the backend handles (and
|
// backend function table can still release the backend handles (and before
|
||||||
// before a re-initialized library could pair them with the wrong
|
// a re-initialized library could pair them with the wrong backend's
|
||||||
// backend's DeleteSync).
|
// DeleteSync / DeleteBackendQuery).
|
||||||
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
||||||
|
MG_Impl::GLImpl::DestroyAllQueryObjects();
|
||||||
MG_Backend::pActiveBackendObject.reset();
|
MG_Backend::pActiveBackendObject.reset();
|
||||||
MG_State::pGLContext.reset();
|
MG_State::pGLContext.reset();
|
||||||
MG_State::pEGLContext.reset();
|
MG_State::pEGLContext.reset();
|
||||||
|
|||||||
@@ -712,9 +712,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
{
|
{
|
||||||
.TargetGLVersion = {4, 0, 0}, // GL target version
|
.TargetGLVersion = {4, 0, 0}, // GL target version
|
||||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||||
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
|
// Baseline advertisement (no runtime capabilities yet); reconciled once
|
||||||
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
// the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||||
.Extensions = BuildAdvertisedExtensions(false, false),
|
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||||
.IsCompatibilityProfile = false // Is Compatibility Profile
|
.IsCompatibilityProfile = false // Is Compatibility Profile
|
||||||
},
|
},
|
||||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
||||||
@@ -734,9 +734,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// thread can only observe the extension string after the
|
// thread can only observe the extension string after the
|
||||||
// advertisement for its context has settled; rebuilding the whole
|
// advertisement for its context has settled; rebuilding the whole
|
||||||
// list keeps the re-run after a context recreation idempotent.
|
// list keeps the re-run after a context recreation idempotent.
|
||||||
void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) {
|
void UpdateAdvertisedCapabilityExtensions(const MG_External::GLESCapabilities& capabilities) {
|
||||||
MutableRendererInfo().RendererGLInfo.Extensions =
|
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||||
BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported);
|
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
|
||||||
|
capabilities.SupportsDrawIndirect,
|
||||||
|
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance);
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -779,11 +781,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
|
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
|
||||||
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and
|
// Now that g_GLESCapabilities knows the host extensions, entry points, and ES version,
|
||||||
// GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on
|
// reconcile every runtime-gated advertisement (see the comment on
|
||||||
// UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension
|
// UpdateAdvertisedCapabilityExtensions for why this cannot happen when the list is first
|
||||||
// list is first built).
|
// built).
|
||||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy);
|
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities);
|
||||||
UpdateDynamicBackendParameters();
|
UpdateDynamicBackendParameters();
|
||||||
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
|
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
|
||||||
PrintFormatCapabilities(GetFormatCapabilities());
|
PrintFormatCapabilities(GetFormatCapabilities());
|
||||||
@@ -924,7 +926,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return MutableRendererInfo();
|
return MutableRendererInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
|
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||||
|
Bool drawIndirectSupported,
|
||||||
|
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||||
Vector<GLExtension> extensions = {
|
Vector<GLExtension> extensions = {
|
||||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
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_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||||
@@ -955,6 +959,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// extension explicitly permits. It is also the only thing that
|
// extension explicitly permits. It is also the only thing that
|
||||||
// exposes glProgramParameteri before GL 4.1.
|
// exposes glProgramParameteri before GL 4.1.
|
||||||
E_GL_ARB_get_program_binary};
|
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
|
// 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
|
// driver's: the compiler threads are MobileGL's, and glCompileShader/glLinkProgram
|
||||||
// are serviced entirely inside the frontend. Whether the device driver advertises
|
// are serviced entirely inside the frontend. Whether the device driver advertises
|
||||||
|
|||||||
@@ -67,9 +67,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const RendererInfo& GetRendererIdentity();
|
const RendererInfo& GetRendererIdentity();
|
||||||
|
|
||||||
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
// 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.
|
// 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
|
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
||||||
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
||||||
|
|||||||
@@ -1600,7 +1600,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
!g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanEnd, syncedBytes + kBlendSpanEnd,
|
!g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanEnd, syncedBytes + kBlendSpanEnd,
|
||||||
sizeof(RenderStateParameters) - kBlendSpanEnd) != 0;
|
sizeof(RenderStateParameters) - kBlendSpanEnd) != 0;
|
||||||
|
|
||||||
IntVec4 backendViewport = parameters.Viewport;
|
IntVec4 backendViewport = MG_State::pGLContext->GetViewport();
|
||||||
if (backendViewport.z() <= 0 || backendViewport.w() <= 0) {
|
if (backendViewport.z() <= 0 || backendViewport.w() <= 0) {
|
||||||
Int surfaceWidth = 0;
|
Int surfaceWidth = 0;
|
||||||
Int surfaceHeight = 0;
|
Int surfaceHeight = 0;
|
||||||
@@ -1614,7 +1614,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_syncedBackendViewport = backendViewport;
|
g_syncedBackendViewport = backendViewport;
|
||||||
}
|
}
|
||||||
|
|
||||||
// All 12 capability bools live after LogicOp in the struct, i.e. in the tail span.
|
// Every capability bool (and the scissor-test mask below) lives after LogicOp in the
|
||||||
|
// struct, i.e. in the tail span.
|
||||||
if (tailSpanDirty) {
|
if (tailSpanDirty) {
|
||||||
#define SYNC_CAPABILITY(cap_mg, cap_gl) \
|
#define SYNC_CAPABILITY(cap_mg, cap_gl) \
|
||||||
if (forceFullPush || parameters.cap_mg##Enabled != g_syncedRenderStateParameters.cap_mg##Enabled) { \
|
if (forceFullPush || parameters.cap_mg##Enabled != g_syncedRenderStateParameters.cap_mg##Enabled) { \
|
||||||
@@ -1633,11 +1634,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
SYNC_CAPABILITY(SampleMask, GL_SAMPLE_MASK);
|
SYNC_CAPABILITY(SampleMask, GL_SAMPLE_MASK);
|
||||||
SYNC_CAPABILITY(PolygonOffsetFill, GL_POLYGON_OFFSET_FILL);
|
SYNC_CAPABILITY(PolygonOffsetFill, GL_POLYGON_OFFSET_FILL);
|
||||||
SYNC_CAPABILITY(RasterizerDiscard, GL_RASTERIZER_DISCARD);
|
SYNC_CAPABILITY(RasterizerDiscard, GL_RASTERIZER_DISCARD);
|
||||||
SYNC_CAPABILITY(ScissorTest, GL_SCISSOR_TEST);
|
|
||||||
SYNC_CAPABILITY(StencilTest, GL_STENCIL_TEST);
|
SYNC_CAPABILITY(StencilTest, GL_STENCIL_TEST);
|
||||||
SYNC_CAPABILITY(CullFace, GL_CULL_FACE);
|
SYNC_CAPABILITY(CullFace, GL_CULL_FACE);
|
||||||
|
|
||||||
#undef SYNC_CAPABILITY
|
#undef SYNC_CAPABILITY
|
||||||
|
|
||||||
|
// GL_SCISSOR_TEST is per-viewport enable state (ARB_viewport_array), so it is a
|
||||||
|
// 16-bit mask and not a "<Name>Enabled" bool the macro above could key off. ES
|
||||||
|
// has exactly one scissor rectangle and one scissor enable, so only bit 0 - the
|
||||||
|
// index every ES draw rasterizes against - can be forwarded; a program that
|
||||||
|
// enables the test for viewport 3 alone gets viewport 0's answer here. That is
|
||||||
|
// the same limitation as the unemulated gl_ViewportIndex on this backend and is
|
||||||
|
// why the multi-viewport half of KHR-GL43.viewport_array stays red on Espryt.
|
||||||
|
{
|
||||||
|
const Bool scissorTest = (parameters.ScissorTestEnabledMask & 1u) != 0;
|
||||||
|
const Bool syncedScissorTest =
|
||||||
|
(g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0;
|
||||||
|
if (forceFullPush || scissorTest != syncedScissorTest) {
|
||||||
|
scissorTest ? g_GLESFuncs.glEnable(GL_SCISSOR_TEST) : g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tailSpanDirty && g_GLESCapabilities.SupportsClipDistance) {
|
if (tailSpanDirty && g_GLESCapabilities.SupportsClipDistance) {
|
||||||
@@ -1864,8 +1880,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (forceFullPush || parameters.DepthMask != g_syncedRenderStateParameters.DepthMask) {
|
if (forceFullPush || parameters.DepthMask != g_syncedRenderStateParameters.DepthMask) {
|
||||||
g_GLESFuncs.glDepthMask(parameters.DepthMask ? GL_TRUE : GL_FALSE);
|
g_GLESFuncs.glDepthMask(parameters.DepthMask ? GL_TRUE : GL_FALSE);
|
||||||
}
|
}
|
||||||
if (forceFullPush || parameters.DepthRange != g_syncedRenderStateParameters.DepthRange) {
|
if (forceFullPush || parameters.DepthRanges[0] != g_syncedRenderStateParameters.DepthRanges[0]) {
|
||||||
g_GLESFuncs.glDepthRangef(parameters.DepthRange.x(), parameters.DepthRange.y());
|
g_GLESFuncs.glDepthRangef(parameters.DepthRanges[0].x(), parameters.DepthRanges[0].y());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2003,7 +2019,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// everything drawn with GL_SCISSOR_TEST enabled before the app's first glScissor
|
// everything drawn with GL_SCISSOR_TEST enabled before the app's first glScissor
|
||||||
// is clipped away - Minecraft 26.2 keeps only its unscissored sky and hand and
|
// is clipped away - Minecraft 26.2 keeps only its unscissored sky and hand and
|
||||||
// loses the terrain and the whole GUI.
|
// loses the terrain and the whole GUI.
|
||||||
IntVec4 backendScissorBox = parameters.ScissorBox;
|
IntVec4 backendScissorBox = parameters.ScissorBoxes[0];
|
||||||
if (backendScissorBox.z() <= 0 || backendScissorBox.w() <= 0) {
|
if (backendScissorBox.z() <= 0 || backendScissorBox.w() <= 0) {
|
||||||
Int surfaceWidth = 0;
|
Int surfaceWidth = 0;
|
||||||
Int surfaceHeight = 0;
|
Int surfaceHeight = 0;
|
||||||
@@ -2189,6 +2205,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// accident - and it never covered the monolithic glUseProgram path at all - so the
|
// accident - and it never covered the monolithic glUseProgram path at all - so the
|
||||||
// dependency is stated here instead.
|
// dependency is stated here instead.
|
||||||
if (!twin->GetBackendProgramId() ||
|
if (!twin->GetBackendProgramId() ||
|
||||||
|
twin->GetContextGeneration() != g_backendContextGeneration ||
|
||||||
twin->GetSyncedLinkVersion() != currentProgram->GetLinkVersion() ||
|
twin->GetSyncedLinkVersion() != currentProgram->GetLinkVersion() ||
|
||||||
twin->GetSyncedImageUnitVersion() != currentProgram->GetImageUnitVersion() ||
|
twin->GetSyncedImageUnitVersion() != currentProgram->GetImageUnitVersion() ||
|
||||||
twin->GetSnormFallbackClampOutputMask() != g_snormFallbackClampOutputMask ||
|
twin->GetSnormFallbackClampOutputMask() != g_snormFallbackClampOutputMask ||
|
||||||
@@ -3038,6 +3055,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||||
const auto program = GetCurrentBackendProgram();
|
const auto program = GetCurrentBackendProgram();
|
||||||
if (!currentProgram || program == nullptr ||
|
if (!currentProgram || program == nullptr ||
|
||||||
|
program->GetContextGeneration() != g_backendContextGeneration ||
|
||||||
program->GetSyncedLinkVersion() != currentProgram->GetLinkVersion()) {
|
program->GetSyncedLinkVersion() != currentProgram->GetLinkVersion()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -3045,10 +3063,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Bool SupportsNativeIndirectDraws() {
|
static Bool SupportsNativeIndirectDraws() {
|
||||||
const auto& version = g_GLESCapabilities.GLESVersion;
|
return g_GLESCapabilities.SupportsDrawIndirect;
|
||||||
const Bool esVersionOk = version.Major > 3 || (version.Major == 3 && version.Minor >= 1);
|
|
||||||
return esVersionOk && g_GLESFuncs.glDrawElementsIndirect != nullptr &&
|
|
||||||
g_GLESFuncs.glDrawArraysIndirect != nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs an (indexed) indirect multi-draw. When a GL_DRAW_INDIRECT_BUFFER is bound the draws
|
// Runs an (indexed) indirect multi-draw. When a GL_DRAW_INDIRECT_BUFFER is bound the draws
|
||||||
@@ -4762,7 +4777,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// restores the app state on exit, tracked via the render-state shadow.
|
// restores the app state on exit, tracked via the render-state shadow.
|
||||||
class ScopedScissorDisable {
|
class ScopedScissorDisable {
|
||||||
public:
|
public:
|
||||||
ScopedScissorDisable() : m_wasEnabled(RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabled) {
|
ScopedScissorDisable()
|
||||||
|
: m_wasEnabled((RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0) {
|
||||||
if (m_wasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
if (m_wasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
||||||
}
|
}
|
||||||
~ScopedScissorDisable() {
|
~ScopedScissorDisable() {
|
||||||
@@ -5885,6 +5901,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// fallen behind is about to be rebuilt anyway, and its current driver interface is
|
// fallen behind is about to be rebuilt anyway, and its current driver interface is
|
||||||
// the PREVIOUS link's - applying to it could land the binding on an unrelated block.
|
// the PREVIOUS link's - applying to it could land the binding on an unrelated block.
|
||||||
if (!backendObj->GetBackendProgramId() ||
|
if (!backendObj->GetBackendProgramId() ||
|
||||||
|
backendObj->GetContextGeneration() != g_backendContextGeneration ||
|
||||||
backendObj->GetSyncedLinkVersion() != programObject->GetLinkVersion()) {
|
backendObj->GetSyncedLinkVersion() != programObject->GetLinkVersion()) {
|
||||||
return; // SyncToBackend's reseed will carry it
|
return; // SyncToBackend's reseed will carry it
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1468,6 +1468,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
m_clientAttributeBufferIds.fill(0);
|
m_clientAttributeBufferIds.fill(0);
|
||||||
|
m_contextGeneration = g_backendContextGeneration;
|
||||||
g_GLESFuncs.glGenVertexArrays(1, &m_backendVAOId);
|
g_GLESFuncs.glGenVertexArrays(1, &m_backendVAOId);
|
||||||
if (m_backendVAOId == 0) {
|
if (m_backendVAOId == 0) {
|
||||||
MGLOG_E_ONCE("Failed to generate vertex array object.");
|
MGLOG_E_ONCE("Failed to generate vertex array object.");
|
||||||
@@ -1481,17 +1482,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (InProcessTeardown()) {
|
if (InProcessTeardown()) {
|
||||||
return; // see InProcessTeardown(): the driver may be unloaded already
|
return; // see InProcessTeardown(): the driver may be unloaded already
|
||||||
}
|
}
|
||||||
|
const Bool contextCurrent = m_contextGeneration == g_backendContextGeneration;
|
||||||
if (m_backendVAOId != 0) {
|
if (m_backendVAOId != 0) {
|
||||||
|
// Scrub the binding shadow whether or not the id can still be
|
||||||
|
// deleted: a recycled name must never satisfy the shadow's dedup.
|
||||||
NoteVAOIdDeleted(m_backendVAOId);
|
NoteVAOIdDeleted(m_backendVAOId);
|
||||||
g_GLESFuncs.glDeleteVertexArrays(1, &m_backendVAOId);
|
if (contextCurrent && g_GLESFuncs.glDeleteVertexArrays) {
|
||||||
|
g_GLESFuncs.glDeleteVertexArrays(1, &m_backendVAOId);
|
||||||
|
}
|
||||||
m_backendVAOId = 0;
|
m_backendVAOId = 0;
|
||||||
}
|
}
|
||||||
for (auto& bufferId : m_clientAttributeBufferIds) {
|
for (auto& bufferId : m_clientAttributeBufferIds) {
|
||||||
if (bufferId != 0) {
|
if (bufferId == 0) {
|
||||||
BufferImpl::NoteBufferIdDeleted(bufferId);
|
continue;
|
||||||
g_GLESFuncs.glDeleteBuffers(1, &bufferId);
|
|
||||||
bufferId = 0;
|
|
||||||
}
|
}
|
||||||
|
// Same discipline as the VAO id itself: a buffer id from a dead
|
||||||
|
// context belongs to that context and must never be deleted as a
|
||||||
|
// recycled name in a successor context.
|
||||||
|
BufferImpl::NoteBufferIdDeleted(bufferId);
|
||||||
|
if (contextCurrent && g_GLESFuncs.glDeleteBuffers) {
|
||||||
|
g_GLESFuncs.glDeleteBuffers(1, &bufferId);
|
||||||
|
}
|
||||||
|
bufferId = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1635,6 +1647,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// PrepareForDraw's BindCurrentVAO establishes the draw binding regardless.
|
// PrepareForDraw's BindCurrentVAO establishes the draw binding regardless.
|
||||||
const Uint32 currentConfigVersion = stateVAOObject->GetConfigVersion();
|
const Uint32 currentConfigVersion = stateVAOObject->GetConfigVersion();
|
||||||
const Uint16 currentIndexBufferVersion = stateVAOObject->GetIndexBufferBindingSlot().GetVersion();
|
const Uint16 currentIndexBufferVersion = stateVAOObject->GetIndexBufferBindingSlot().GetVersion();
|
||||||
|
|
||||||
|
// The ES context was recreated since this twin last ran. Its GL names belong to
|
||||||
|
// the dead context and are gone; mint a fresh VAO and force every attribute /
|
||||||
|
// index-binding cache to re-emit. No glDelete* here: the old names are not ours
|
||||||
|
// to delete in the successor context.
|
||||||
|
if (m_contextGeneration != g_backendContextGeneration) {
|
||||||
|
InvalidateVAOBindingCache();
|
||||||
|
m_backendVAOId = 0;
|
||||||
|
m_contextGeneration = g_backendContextGeneration;
|
||||||
|
m_clientAttributeBufferIds.fill(0);
|
||||||
|
m_isInitialized = false;
|
||||||
|
m_resolvedDrawBuffers = {};
|
||||||
|
m_pendingAttribValueMask = {};
|
||||||
|
m_hasSyncedConfigVersion = false;
|
||||||
|
m_syncedConfigVersion = 0;
|
||||||
|
m_syncedIndexBufferVersion = static_cast<Uint16>(currentIndexBufferVersion + 1);
|
||||||
|
m_syncedAttributeVersions.fill({});
|
||||||
|
m_syncedFetchBaseInstance = 0;
|
||||||
|
g_GLESFuncs.glGenVertexArrays(1, &m_backendVAOId);
|
||||||
|
if (m_backendVAOId == 0) {
|
||||||
|
MGLOG_E_ONCE("Failed to recreate vertex array object for a new ES context.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const Bool attributesDirty = !m_hasSyncedConfigVersion || m_syncedConfigVersion != currentConfigVersion;
|
const Bool attributesDirty = !m_hasSyncedConfigVersion || m_syncedConfigVersion != currentConfigVersion;
|
||||||
const Bool indexBufferDirty = currentIndexBufferVersion != m_syncedIndexBufferVersion;
|
const Bool indexBufferDirty = currentIndexBufferVersion != m_syncedIndexBufferVersion;
|
||||||
|
|
||||||
@@ -1978,6 +2014,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
TextureSwizzleParam::Alpha};
|
TextureSwizzleParam::Alpha};
|
||||||
m_cacheDepthStencilTextureMode = GL_DEPTH_COMPONENT;
|
m_cacheDepthStencilTextureMode = GL_DEPTH_COMPONENT;
|
||||||
m_forceTextureParamsResync = true;
|
m_forceTextureParamsResync = true;
|
||||||
|
m_forceSamplerResync = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the backend GL unpack state to MobileGL's upload default for the scope,
|
// Sets the backend GL unpack state to MobileGL's upload default for the scope,
|
||||||
@@ -2395,6 +2432,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The ES context was recreated since this twin last ran. Recreate the
|
||||||
|
// texture id before any version-based early-out below: those versions are
|
||||||
|
// frontend versions and do not move when only the backend context changed.
|
||||||
|
if (m_contextGeneration != g_backendContextGeneration) {
|
||||||
|
RecreateBackendTexture();
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
@@ -3119,14 +3163,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_contextGeneration != g_backendContextGeneration) {
|
||||||
|
RecreateBackendTexture();
|
||||||
|
}
|
||||||
|
|
||||||
auto* samplerObject = stateTextureObject->GetSamplerObject().get();
|
auto* samplerObject = stateTextureObject->GetSamplerObject().get();
|
||||||
Uint currentSamplerVersion = samplerObject->GetVersion();
|
Uint currentSamplerVersion = samplerObject->GetVersion();
|
||||||
if (m_syncedSamplerVersion == currentSamplerVersion) {
|
if (m_syncedSamplerVersion == currentSamplerVersion && !m_forceSamplerResync) {
|
||||||
MGLOG_D("Sampler parameters have not changed for texture ID: %u, skipping sync.", m_backendTextureId);
|
MGLOG_D("Sampler parameters have not changed for texture ID: %u, skipping sync.", m_backendTextureId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_syncedSamplerVersion = currentSamplerVersion;
|
m_syncedSamplerVersion = currentSamplerVersion;
|
||||||
|
m_forceSamplerResync = false;
|
||||||
|
|
||||||
MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u",
|
MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u",
|
||||||
m_backendTextureId, stateTextureObject->GetExternalIndex());
|
m_backendTextureId, stateTextureObject->GetExternalIndex());
|
||||||
@@ -3229,6 +3278,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_contextGeneration != g_backendContextGeneration) {
|
||||||
|
RecreateBackendTexture();
|
||||||
|
}
|
||||||
|
|
||||||
Uint16 currentTextureParamsVersion = stateTextureObject->GetTextureParamsVersion();
|
Uint16 currentTextureParamsVersion = stateTextureObject->GetTextureParamsVersion();
|
||||||
if (m_syncedTextureParamsVersion == currentTextureParamsVersion && !m_forceTextureParamsResync) {
|
if (m_syncedTextureParamsVersion == currentTextureParamsVersion && !m_forceTextureParamsResync) {
|
||||||
MGLOG_D("Texture parameters have not changed for texture ID: %u, skipping sync.", m_backendTextureId);
|
MGLOG_D("Texture parameters have not changed for texture ID: %u, skipping sync.", m_backendTextureId);
|
||||||
@@ -3902,6 +3955,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
MGLOG_E_ONCE("State FBO object is null, cannot sync to backend.");
|
MGLOG_E_ONCE("State FBO object is null, cannot sync to backend.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Recreate the driver FBO when the ES context has moved on. The old id is
|
||||||
|
// gone with the old context; calling glDeleteFramebuffers on its recycled
|
||||||
|
// numeric value could delete a new live FBO, so simply abandon it.
|
||||||
|
if (m_contextGeneration != g_backendContextGeneration) {
|
||||||
|
m_backendFBOId = 0;
|
||||||
|
m_contextGeneration = g_backendContextGeneration;
|
||||||
|
g_GLESFuncs.glGenFramebuffers(1, &m_backendFBOId);
|
||||||
|
if (m_backendFBOId == 0) {
|
||||||
|
MGLOG_E_ONCE("Failed to recreate framebuffer object for a new ES context.");
|
||||||
|
}
|
||||||
|
InvalidateFramebufferBindingCache();
|
||||||
|
InvalidateSyncedState();
|
||||||
|
}
|
||||||
MGLOG_D("Syncing FBO with backend ID %u to backend for state ID %u, as %s FBO", m_backendFBOId,
|
MGLOG_D("Syncing FBO with backend ID %u to backend for state ID %u, as %s FBO", m_backendFBOId,
|
||||||
stateFBOObject->GetExternalIndex(), (asTarget == FramebufferTarget::Draw ? "DRAW" : "READ"));
|
stateFBOObject->GetExternalIndex(), (asTarget == FramebufferTarget::Draw ? "DRAW" : "READ"));
|
||||||
GLenum glFBOTarget = MG_Util::ConvertFramebufferTargetToGLEnum(asTarget);
|
GLenum glFBOTarget = MG_Util::ConvertFramebufferTargetToGLEnum(asTarget);
|
||||||
@@ -4412,10 +4478,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Uint g_lastUsedBackendProgramId = 0;
|
Uint g_lastUsedBackendProgramId = 0;
|
||||||
StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl> g_backendProgramObjects;
|
StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl> g_backendProgramObjects;
|
||||||
|
|
||||||
|
void DeleteBackendProgramGlobalUbo(Uint& bufferId, Uint contextGeneration) {
|
||||||
|
if (bufferId == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Only a buffer that belongs to the LIVE context may be deleted. A stale
|
||||||
|
// generation means the old ES context already reclaimed it; handing its
|
||||||
|
// recycled numeric id to glDeleteBuffers could delete a new live buffer.
|
||||||
|
if (contextGeneration == g_backendContextGeneration) {
|
||||||
|
BufferImpl::NoteBufferIdDeleted(bufferId);
|
||||||
|
if (g_GLESFuncs.glDeleteBuffers) {
|
||||||
|
g_GLESFuncs.glDeleteBuffers(1, &bufferId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bufferId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
BackendProgramObjectImpl::BackendProgramObjectImpl() {
|
BackendProgramObjectImpl::BackendProgramObjectImpl() {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
m_contextGeneration = g_backendContextGeneration;
|
||||||
m_backendProgramId = g_GLESFuncs.glCreateProgram();
|
m_backendProgramId = g_GLESFuncs.glCreateProgram();
|
||||||
if (m_backendProgramId == 0) {
|
if (m_backendProgramId == 0) {
|
||||||
MGLOG_E_ONCE("Failed to create program object in backend.");
|
MGLOG_E_ONCE("Failed to create program object in backend.");
|
||||||
@@ -4433,14 +4516,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (InProcessTeardown()) {
|
if (InProcessTeardown()) {
|
||||||
return; // see InProcessTeardown(): the driver may be unloaded already
|
return; // see InProcessTeardown(): the driver may be unloaded already
|
||||||
}
|
}
|
||||||
|
DeleteBackendProgramGlobalUbo(m_backendGlobalUBOId, m_contextGeneration);
|
||||||
if (m_backendProgramId != 0) {
|
if (m_backendProgramId != 0) {
|
||||||
MGLOG_D("Deleting backend program object with ID: %u", m_backendProgramId);
|
// Same generation rule as the global UBO: a program id from a dead
|
||||||
g_GLESFuncs.glDeleteProgram(m_backendProgramId);
|
// context is gone already and must not be deleted as a recycled name
|
||||||
|
// in a successor context.
|
||||||
|
if (m_contextGeneration == g_backendContextGeneration) {
|
||||||
|
MGLOG_D("Deleting backend program object with ID: %u", m_backendProgramId);
|
||||||
|
if (g_GLESFuncs.glDeleteProgram) {
|
||||||
|
g_GLESFuncs.glDeleteProgram(m_backendProgramId);
|
||||||
|
}
|
||||||
|
}
|
||||||
// The driver may recycle this GL name for a future program; a stale
|
// The driver may recycle this GL name for a future program; a stale
|
||||||
// guard entry would then wrongly skip the glUseProgram for it.
|
// guard entry would then wrongly skip the glUseProgram for it.
|
||||||
if (g_lastUsedBackendProgramId == m_backendProgramId) {
|
if (g_lastUsedBackendProgramId == m_backendProgramId) {
|
||||||
g_lastUsedBackendProgramId = 0;
|
g_lastUsedBackendProgramId = 0;
|
||||||
}
|
}
|
||||||
|
m_backendProgramId = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4677,6 +4769,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The ES context was recreated since this twin last ran. The old program id
|
||||||
|
// and global UBO id belong to the dead context; drop them without GL calls
|
||||||
|
// and mint a fresh program before reusing any cached reflection/version data.
|
||||||
|
if (m_contextGeneration != g_backendContextGeneration) {
|
||||||
|
DeleteBackendProgramGlobalUbo(m_backendGlobalUBOId, m_contextGeneration);
|
||||||
|
m_backendProgramId = 0;
|
||||||
|
m_contextGeneration = g_backendContextGeneration;
|
||||||
|
m_backendProgramId = g_GLESFuncs.glCreateProgram();
|
||||||
|
if (m_backendProgramId == 0) {
|
||||||
|
MGLOG_E_ONCE("Failed to recreate backend program object for a new ES context.");
|
||||||
|
}
|
||||||
|
m_isInitialized = false;
|
||||||
|
m_backendProgramUsable = false;
|
||||||
|
m_syncedLinkVersion = ~0u;
|
||||||
|
m_syncedImageUnitVersion = ~0u;
|
||||||
|
m_lastUploadedGlobalUboVersion = ~0u;
|
||||||
|
m_globalUboBackendBlockIndex = -1;
|
||||||
|
m_globalUboBackendBlockSize = 0;
|
||||||
|
m_uniformBlockBackendIndices.clear();
|
||||||
|
m_samplerUniformBindings.clear();
|
||||||
|
m_formatlessImageUnits.clear();
|
||||||
|
m_imageUnitFormatSignature = 0;
|
||||||
|
m_globalUboRingAllocation = {};
|
||||||
|
}
|
||||||
|
|
||||||
MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u",
|
MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u",
|
||||||
stateProgramObject->GetExternalIndex(), m_backendProgramId);
|
stateProgramObject->GetExternalIndex(), m_backendProgramId);
|
||||||
// Every link-derived cache below (incl. m_samplerUniformBindings and its
|
// Every link-derived cache below (incl. m_samplerUniformBindings and its
|
||||||
@@ -5154,7 +5271,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create global UBO
|
// Create global UBO. Delete any previous one first: relink reuses this
|
||||||
|
// backend program, and without this every relink leaked the old buffer.
|
||||||
|
DeleteBackendProgramGlobalUbo(m_backendGlobalUBOId, m_contextGeneration);
|
||||||
if (stateProgramObject->GetUBOSize() > 0) {
|
if (stateProgramObject->GetUBOSize() > 0) {
|
||||||
g_GLESFuncs.glGenBuffers(1, &m_backendGlobalUBOId);
|
g_GLESFuncs.glGenBuffers(1, &m_backendGlobalUBOId);
|
||||||
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, m_backendGlobalUBOId);
|
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, m_backendGlobalUBOId);
|
||||||
@@ -5389,6 +5508,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_contextGeneration != g_backendContextGeneration) {
|
||||||
|
// Old sampler id died with the old context; abandon it and mint a new
|
||||||
|
// one before the version-based early-out below can reuse a dead name.
|
||||||
|
m_backendSamplerId = 0;
|
||||||
|
m_contextGeneration = g_backendContextGeneration;
|
||||||
|
g_GLESFuncs.glGenSamplers(1, &m_backendSamplerId);
|
||||||
|
if (m_backendSamplerId == 0) {
|
||||||
|
MGLOG_E_ONCE("Failed to recreate sampler object for a new ES context.");
|
||||||
|
}
|
||||||
|
m_isInitialized = false;
|
||||||
|
m_cacheSamplerParameters = {};
|
||||||
|
g_boundSamplersCache.fill(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
Uint currentSamplerVersion = stateSamplerObject->GetVersion();
|
Uint currentSamplerVersion = stateSamplerObject->GetVersion();
|
||||||
if (m_isInitialized && m_syncedSamplerVersion == currentSamplerVersion) {
|
if (m_isInitialized && m_syncedSamplerVersion == currentSamplerVersion) {
|
||||||
MGLOG_D("Sampler parameters have not changed for sampler ID: %u, skipping sync.",
|
MGLOG_D("Sampler parameters have not changed for sampler ID: %u, skipping sync.",
|
||||||
@@ -5533,6 +5666,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_contextGeneration != g_backendContextGeneration) {
|
||||||
|
// The old renderbuffer id died with the old context. Abandon it and
|
||||||
|
// force a fresh allocation instead of letting the parameter early-out
|
||||||
|
// below keep using a dead name.
|
||||||
|
m_backendRBOId = 0;
|
||||||
|
m_contextGeneration = g_backendContextGeneration;
|
||||||
|
g_GLESFuncs.glGenRenderbuffers(1, &m_backendRBOId);
|
||||||
|
if (m_backendRBOId == 0) {
|
||||||
|
MGLOG_E_ONCE("Failed to recreate renderbuffer object for a new ES context.");
|
||||||
|
}
|
||||||
|
m_isInitialized = false;
|
||||||
|
m_cacheInternalFormat = TextureInternalFormat::Unknown;
|
||||||
|
m_cacheWidth = -1;
|
||||||
|
m_cacheHeight = -1;
|
||||||
|
m_cacheSamples = -1;
|
||||||
|
}
|
||||||
|
|
||||||
MGLOG_D("Syncing RBO with backend ID %u to backend for state ID %u", m_backendRBOId,
|
MGLOG_D("Syncing RBO with backend ID %u to backend for state ID %u", m_backendRBOId,
|
||||||
stateRBOObject->GetExternalIndex());
|
stateRBOObject->GetExternalIndex());
|
||||||
|
|
||||||
|
|||||||
@@ -406,6 +406,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void SyncClientSideAttributesForDrawArrays(
|
void SyncClientSideAttributesForDrawArrays(
|
||||||
const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject, GLint first, GLsizei count);
|
const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject, GLint first, GLsizei count);
|
||||||
Uint GetBackendVertexArrayId() const { return m_backendVAOId; }
|
Uint GetBackendVertexArrayId() const { return m_backendVAOId; }
|
||||||
|
Uint GetContextGeneration() const { return m_contextGeneration; }
|
||||||
void Bind() const;
|
void Bind() const;
|
||||||
|
|
||||||
// Draw-path memo of SyncNeccessaryBuffers' attribute walk for this VAO: the
|
// Draw-path memo of SyncNeccessaryBuffers' attribute walk for this VAO: the
|
||||||
@@ -462,6 +463,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
ResolvedDrawBuffers m_resolvedDrawBuffers;
|
ResolvedDrawBuffers m_resolvedDrawBuffers;
|
||||||
PendingAttribValueMask m_pendingAttribValueMask;
|
PendingAttribValueMask m_pendingAttribValueMask;
|
||||||
Uint m_backendVAOId = 0;
|
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;
|
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
Uint16 m_syncedIndexBufferVersion = 0;
|
Uint16 m_syncedIndexBufferVersion = 0;
|
||||||
@@ -711,6 +716,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// parameter already pushed onto it: the params-version early-out has to be overridden
|
// 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.
|
// once, or an unchanged version would skip the re-push forever.
|
||||||
Bool m_forceTextureParamsResync = false;
|
Bool m_forceTextureParamsResync = false;
|
||||||
|
// Same latch for the built-in sampler parameters.
|
||||||
|
Bool m_forceSamplerResync = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
void ActivateTextureUnit(Uint unit);
|
void ActivateTextureUnit(Uint unit);
|
||||||
@@ -1087,6 +1094,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
|
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
|
||||||
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
||||||
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
||||||
|
Uint GetContextGeneration() const { return m_contextGeneration; }
|
||||||
// False when the last SyncToBackend could not produce a usable program (a
|
// False when the last SyncToBackend could not produce a usable program (a
|
||||||
// shader failed to transpile or compile, or the link itself failed). Use()
|
// shader failed to transpile or compile, or the link itself failed). Use()
|
||||||
// must not leave the previously bound program current in that case.
|
// must not leave the previously bound program current in that case.
|
||||||
@@ -1149,6 +1157,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||||
|
|
||||||
Uint m_backendProgramId = 0;
|
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
|
// 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
|
// an unusable backend program can be traced back to the glCreateProgram id the app
|
||||||
// knows it by.
|
// knows it by.
|
||||||
@@ -1196,6 +1208,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the
|
// skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the
|
||||||
// ES context is recreated.
|
// ES context is recreated.
|
||||||
extern Uint g_lastUsedBackendProgramId;
|
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>
|
extern StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl>
|
||||||
g_backendProgramObjects;
|
g_backendProgramObjects;
|
||||||
|
|
||||||
|
|||||||
@@ -497,20 +497,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
.ExtraVendor = Nullopt,
|
.ExtraVendor = Nullopt,
|
||||||
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
|
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
|
||||||
.TargetGLSLVersion = {4, 6, 0},
|
.TargetGLSLVersion = {4, 6, 0},
|
||||||
// Baseline advertisement (no shader subgroup, no timer queries); a
|
// Baseline advertisement (no runtime-gated capabilities); a live
|
||||||
// live backend reconciles its copy in UpdateAdvertisedExtensions.
|
// backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||||
.Extensions = BuildAdvertisedExtensions(false, false, false),
|
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||||
.IsCompatibilityProfile = false},
|
.IsCompatibilityProfile = false},
|
||||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
||||||
return rendererInfo;
|
return rendererInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||||
Bool anisotropicFilteringSupported) {
|
Bool anisotropicFilteringSupported,
|
||||||
|
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||||
Vector<GLExtension> extensions = {
|
Vector<GLExtension> extensions = {
|
||||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
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_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_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_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_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters,
|
||||||
@@ -530,6 +532,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// extension explicitly permits. It is also the only thing that
|
// extension explicitly permits. It is also the only thing that
|
||||||
// exposes glProgramParameteri before GL 4.1.
|
// exposes glProgramParameteri before GL 4.1.
|
||||||
E_GL_ARB_get_program_binary};
|
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) {
|
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||||
}
|
}
|
||||||
@@ -690,7 +699,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// the whole list keeps re-runs idempotent.
|
// the whole list keeps re-runs idempotent.
|
||||||
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||||
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
|
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
|
||||||
|
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported());
|
||||||
}
|
}
|
||||||
|
|
||||||
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// POST screen shows.
|
// POST screen shows.
|
||||||
|
|
||||||
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
|
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
|
||||||
// versions, ExtraVendor) with the baseline extension advertisement (no shader
|
// versions, ExtraVendor) with the baseline extension advertisement (no runtime-gated
|
||||||
// subgroup, no timer queries). A live backend copies this in its constructor and
|
// capabilities). A live backend copies this in its constructor and
|
||||||
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
|
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
|
||||||
// exist; callers that need the advertised list for a known capability set must
|
// exist; callers that need the advertised list for a known capability set must
|
||||||
// use BuildAdvertisedExtensions instead.
|
// use BuildAdvertisedExtensions instead.
|
||||||
@@ -74,7 +74,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
|
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
|
||||||
// the detected device support (passing an already-gated value is harmless).
|
// the detected device support (passing an already-gated value is harmless).
|
||||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||||
Bool anisotropicFilteringSupported);
|
Bool anisotropicFilteringSupported,
|
||||||
|
Bool nonZeroIndirectBaseInstanceSupported);
|
||||||
|
|
||||||
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
||||||
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
||||||
|
|||||||
@@ -194,53 +194,54 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PipelineFactory::HashType PipelineFactory::ComputeHash(const PipelineCreatePayload& payload) const {
|
PipelineFactory::HashType PipelineFactory::ComputeHash(const PipelineCreatePayload& payload) const {
|
||||||
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, &payload.programHash, sizeof(payload.programHash)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.programHash, sizeof(payload.programHash)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.renderPass, sizeof(payload.renderPass)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.subpass, sizeof(payload.subpass)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.topology, sizeof(payload.topology)));
|
||||||
XXHASH_VERIFY(
|
XXHASH_VERIFY(
|
||||||
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
XXH64_update(m_hashState.Get(), &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.viewportCount, sizeof(payload.viewportCount)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
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(
|
XXHASH_VERIFY(
|
||||||
XXH64_update(m_hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
|
XXH64_update(m_hashState.Get(), &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
|
||||||
XXHASH_VERIFY(
|
XXHASH_VERIFY(
|
||||||
XXH64_update(m_hashState, &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable)));
|
XXH64_update(m_hashState.Get(), &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOpEnable, sizeof(payload.logicOpEnable)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.logicOpEnable, sizeof(payload.logicOpEnable)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.stencilTestEnable, sizeof(payload.stencilTestEnable)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.stencilTestEnable, sizeof(payload.stencilTestEnable)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthCompareOp, sizeof(payload.depthCompareOp)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.depthCompareOp, sizeof(payload.depthCompareOp)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOp, sizeof(payload.logicOp)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.logicOp, sizeof(payload.logicOp)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp)));
|
||||||
XXHASH_VERIFY(
|
XXHASH_VERIFY(
|
||||||
XXH64_update(m_hashState, &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp)));
|
XXH64_update(m_hashState.Get(), &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp)));
|
||||||
XXHASH_VERIFY(
|
XXHASH_VERIFY(
|
||||||
XXH64_update(m_hashState, &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp)));
|
XXH64_update(m_hashState.Get(), &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilFailOp, sizeof(payload.backStencilFailOp)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.backStencilFailOp, sizeof(payload.backStencilFailOp)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilPassOp, sizeof(payload.backStencilPassOp)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.backStencilPassOp, sizeof(payload.backStencilPassOp)));
|
||||||
XXHASH_VERIFY(
|
XXHASH_VERIFY(
|
||||||
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
|
XXH64_update(m_hashState.Get(), &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
|
||||||
XXHASH_VERIFY(
|
XXHASH_VERIFY(
|
||||||
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
|
XXH64_update(m_hashState.Get(), &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
|
||||||
XXHASH_VERIFY(
|
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) {
|
if (payload.colorAttachmentCount > 0) {
|
||||||
XXHASH_VERIFY(XXH64_update(
|
XXHASH_VERIFY(XXH64_update(
|
||||||
m_hashState,
|
m_hashState.Get(),
|
||||||
payload.colorBlendAttachments.data(),
|
payload.colorBlendAttachments.data(),
|
||||||
sizeof(payload.colorBlendAttachments[0]) * payload.colorAttachmentCount));
|
sizeof(payload.colorBlendAttachments[0]) * payload.colorAttachmentCount));
|
||||||
}
|
}
|
||||||
return XXH64_digest(m_hashState);
|
return XXH64_digest(m_hashState.Get());
|
||||||
}
|
}
|
||||||
|
|
||||||
VkPipeline PipelineFactory::GetOrCreatePipeline(const PipelineCreatePayload& payload) {
|
VkPipeline PipelineFactory::GetOrCreatePipeline(const PipelineCreatePayload& payload) {
|
||||||
@@ -406,8 +407,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
tessellation.patchControlPoints = payload.patchControlPoints;
|
tessellation.patchControlPoints = payload.patchControlPoints;
|
||||||
|
|
||||||
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||||
vpci.viewportCount = 1;
|
// Both counts move together: GL has one scissor rectangle per viewport, and Vulkan
|
||||||
vpci.scissorCount = 1;
|
// 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};
|
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||||
raster.polygonMode = payload.polygonMode;
|
raster.polygonMode = payload.polygonMode;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "../VkIncludes.h"
|
#include "../VkIncludes.h"
|
||||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
#include <MG_Util/Types.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
// Enough of a fingerprint to identify the exact module the driver rejected without keeping the
|
// 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;
|
Bool primitiveRestartEnable = false;
|
||||||
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
||||||
Uint32 patchControlPoints = 3;
|
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;
|
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
|
||||||
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
||||||
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
||||||
@@ -157,7 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
UnorderedMap<HashType, PipelineCacheEntry> m_cache;
|
UnorderedMap<HashType, PipelineCacheEntry> m_cache;
|
||||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||||
Uint64 m_frameCounter = 0;
|
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;
|
static inline Bool s_suppressBlendedDepthWrite = false;
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -1997,6 +1997,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInBaseVertex);
|
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,
|
Bool ProgramFactory::ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule,
|
||||||
SpvBuiltIn builtin) {
|
SpvBuiltIn builtin) {
|
||||||
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||||
@@ -2133,26 +2156,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program,
|
ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program,
|
||||||
CompileOptionFlags flags) const {
|
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
|
// We expect shader stages in program object are sorted
|
||||||
const auto& spirvs = program.GetGeneratedSpirv();
|
const auto& spirvs = program.GetGeneratedSpirv();
|
||||||
for (const auto& spv : spirvs) {
|
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
|
// 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.
|
// re-key every program in the cache on a resize for no reason.
|
||||||
if (flags & CompileOptionBit::FragCoordYFlip) {
|
if (flags & CompileOptionBit::FragCoordYFlip) {
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &m_defaultFramebufferHeight,
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &m_defaultFramebufferHeight,
|
||||||
sizeof(m_defaultFramebufferHeight)));
|
sizeof(m_defaultFramebufferHeight)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include UBO block bindings in hash so different binding configurations produce different entries
|
// Include UBO block bindings in hash so different binding configurations produce different entries
|
||||||
const Uint32 blockCount = static_cast<Uint32>(program.GetActiveUniformBlocksCount());
|
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) {
|
for (Uint32 i = 0; i < blockCount; ++i) {
|
||||||
const Uint32 binding = program.GetUniformBlockBinding(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
|
// The transform feedback capture layout is baked into the modules by
|
||||||
@@ -2163,18 +2186,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// hashed for a capturing compile, so nothing else changes key.
|
// hashed for a capturing compile, so nothing else changes key.
|
||||||
if (flags & CompileOptionBit::XfbCapture) {
|
if (flags & CompileOptionBit::XfbCapture) {
|
||||||
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
|
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, varying.name.data(), varying.name.size()));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), varying.name.data(), varying.name.size()));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &varying.bufferIndex, sizeof(varying.bufferIndex)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &varying.bufferIndex, sizeof(varying.bufferIndex)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &varying.offsetBytes, sizeof(varying.offsetBytes)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &varying.offsetBytes, sizeof(varying.offsetBytes)));
|
||||||
}
|
}
|
||||||
const SizeT bufferCount = program.GetTransformFeedbackBufferCount();
|
const SizeT bufferCount = program.GetTransformFeedbackBufferCount();
|
||||||
for (SizeT i = 0; i < bufferCount; ++i) {
|
for (SizeT i = 0; i < bufferCount; ++i) {
|
||||||
const Uint32 stride = program.GetTransformFeedbackStride(static_cast<Uint32>(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;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2339,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,
|
void ProgramFactory::ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const {
|
VkProgramObject& entry) const {
|
||||||
@@ -3189,6 +3252,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ValidateRasterizationStageInterface(shaders, moduleSpirvs, entry, program.GetExternalIndex());
|
ValidateRasterizationStageInterface(shaders, moduleSpirvs, entry, program.GetExternalIndex());
|
||||||
#endif
|
#endif
|
||||||
ReflectVertexInputs(shaders, moduleSpirvs, entry);
|
ReflectVertexInputs(shaders, moduleSpirvs, entry);
|
||||||
|
ReflectViewportIndexUsage(shaders, moduleSpirvs, entry);
|
||||||
ReflectFragmentOutputs(shaders, moduleSpirvs, entry);
|
ReflectFragmentOutputs(shaders, moduleSpirvs, entry);
|
||||||
ReflectPassthroughTessControlNeed(shaders, moduleSpirvs, entry);
|
ReflectPassthroughTessControlNeed(shaders, moduleSpirvs, entry);
|
||||||
ReflectLayout(program, moduleSpirvs, entry);
|
ReflectLayout(program, moduleSpirvs, entry);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||||
|
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
#include <MG_Util/Types.h>
|
||||||
#include <spirv_reflect.h>
|
#include <spirv_reflect.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
@@ -151,6 +152,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// PROGRAM rather than of the variant: the zeroed variant leaves the variable
|
// 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.
|
// declared, so both variants answer the same and the draw path can ask either.
|
||||||
Bool readsBaseVertexBuiltin = false;
|
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
|
// 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
|
// 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
|
// is passed through unmodified, the output patch size is PATCH_VERTICES, and the
|
||||||
@@ -218,6 +225,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||||
|
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||||
lastUsedFrame = other.lastUsedFrame;
|
lastUsedFrame = other.lastUsedFrame;
|
||||||
@@ -234,6 +242,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
other.fragmentInputComponentCount = 0;
|
other.fragmentInputComponentCount = 0;
|
||||||
other.fragmentReplacesDepth = false;
|
other.fragmentReplacesDepth = false;
|
||||||
other.readsBaseVertexBuiltin = false;
|
other.readsBaseVertexBuiltin = false;
|
||||||
|
other.writesViewportIndexBuiltin = false;
|
||||||
other.needsPassthroughTessControl = false;
|
other.needsPassthroughTessControl = false;
|
||||||
other.passthroughTessControlEmulatable = false;
|
other.passthroughTessControlEmulatable = false;
|
||||||
other.lastUsedFrame = 0;
|
other.lastUsedFrame = 0;
|
||||||
@@ -276,6 +285,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||||
|
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||||
lastUsedFrame = other.lastUsedFrame;
|
lastUsedFrame = other.lastUsedFrame;
|
||||||
@@ -292,6 +302,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
other.fragmentInputComponentCount = 0;
|
other.fragmentInputComponentCount = 0;
|
||||||
other.fragmentReplacesDepth = false;
|
other.fragmentReplacesDepth = false;
|
||||||
other.readsBaseVertexBuiltin = false;
|
other.readsBaseVertexBuiltin = false;
|
||||||
|
other.writesViewportIndexBuiltin = false;
|
||||||
other.needsPassthroughTessControl = false;
|
other.needsPassthroughTessControl = false;
|
||||||
other.passthroughTessControlEmulatable = false;
|
other.passthroughTessControlEmulatable = false;
|
||||||
other.lastUsedFrame = 0;
|
other.lastUsedFrame = 0;
|
||||||
@@ -400,6 +411,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Shared by the two above: does any entry point list an input variable decorated with
|
// Shared by the two above: does any entry point list an input variable decorated with
|
||||||
// this builtin?
|
// this builtin?
|
||||||
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn 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
|
// 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
|
// program that has an evaluation stage and no control stage, for an input patch of
|
||||||
@@ -434,6 +451,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const;
|
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,
|
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const;
|
VkProgramObject& entry) const;
|
||||||
@@ -470,6 +490,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// ever built from one keeps referencing its module. A failed build is cached as
|
// 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.
|
// VK_NULL_HANDLE so a broken generator costs one compile, not one per draw.
|
||||||
UnorderedMap<Uint32, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
|
UnorderedMap<Uint32, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline MobileGL::XXH64State m_hashState;
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -305,7 +305,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// texture/sampler resolution, completeness probe, sync, layout handling, sampler
|
// texture/sampler resolution, completeness probe, sync, layout handling, sampler
|
||||||
// and view lookups - would recompute the identical descriptor.
|
// and view lookups - would recompute the identical descriptor.
|
||||||
if (trustUnchangedHint && descriptorMemoUsable && binding < m_samplerResolveMemo.size() &&
|
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;
|
outImageInfo = m_samplerResolveMemo[binding].info;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -504,6 +505,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (binding < m_samplerResolveMemo.size()) {
|
if (binding < m_samplerResolveMemo.size()) {
|
||||||
if (descriptorMemoUsable) {
|
if (descriptorMemoUsable) {
|
||||||
m_samplerResolveMemo[binding].info = outImageInfo;
|
m_samplerResolveMemo[binding].info = outImageInfo;
|
||||||
|
m_samplerResolveMemo[binding].infoProgramLifetimeId = program.GetLifetimeId();
|
||||||
m_samplerResolveMemo[binding].infoValid = true;
|
m_samplerResolveMemo[binding].infoValid = true;
|
||||||
} else {
|
} else {
|
||||||
// An arrayed binding publishes nothing here, and clears what a previous program
|
// An arrayed binding publishes nothing here, and clears what a previous program
|
||||||
|
|||||||
@@ -341,8 +341,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// lifetime id, so a freed-and-reallocated sampler or texture at the same heap address
|
// 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
|
// 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
|
// 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 {
|
struct SamplerResolveMemo {
|
||||||
|
Uint64 infoProgramLifetimeId = 0;
|
||||||
Uint64 samplerLifetimeId = 0;
|
Uint64 samplerLifetimeId = 0;
|
||||||
Uint64 textureLifetimeId = 0;
|
Uint64 textureLifetimeId = 0;
|
||||||
VkSampler sampler = VK_NULL_HANDLE;
|
VkSampler sampler = VK_NULL_HANDLE;
|
||||||
|
|||||||
@@ -13,25 +13,25 @@
|
|||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
VertexInputStateFactory::HashType VertexInputStateFactory::ComputeHash(
|
VertexInputStateFactory::HashType VertexInputStateFactory::ComputeHash(
|
||||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
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) {
|
for (Int i = 0; i < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++i) {
|
||||||
const auto& attr = vao.GetAttribute(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) {
|
if (!attr.Enabled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Size, sizeof(attr.Size)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Size, sizeof(attr.Size)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Type, sizeof(attr.Type)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Type, sizeof(attr.Type)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Normalized, sizeof(attr.Normalized)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Normalized, sizeof(attr.Normalized)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Stride, sizeof(attr.Stride)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Offset, sizeof(attr.Offset)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.IsInteger, sizeof(attr.IsInteger)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsLong, sizeof(attr.IsLong)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.IsLong, sizeof(attr.IsLong)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.IsBgra, sizeof(attr.IsBgra)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
|
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
|
// 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
|
// 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.
|
// test's positions) instead of its own.
|
||||||
// Zero for client memory (no buffer), which is a distinct identity 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;
|
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(
|
VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash(
|
||||||
@@ -225,24 +225,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
entry.attributes = builder.GetAttributes();
|
entry.attributes = builder.GetAttributes();
|
||||||
// See the layoutHash declaration: hash only the resolved layout, never
|
// See the layoutHash declaration: hash only the resolved layout, never
|
||||||
// buffer identities, so identical layouts across VAOs/buffers agree.
|
// 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) {
|
for (const auto& binding : entry.bindings) {
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.binding, sizeof(binding.binding)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &binding.binding, sizeof(binding.binding)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.stride, sizeof(binding.stride)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &binding.stride, sizeof(binding.stride)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.inputRate, sizeof(binding.inputRate)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &binding.inputRate, sizeof(binding.inputRate)));
|
||||||
}
|
}
|
||||||
for (const auto& attribute : entry.attributes) {
|
for (const auto& attribute : entry.attributes) {
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.location, sizeof(attribute.location)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attribute.location, sizeof(attribute.location)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.binding, sizeof(attribute.binding)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attribute.binding, sizeof(attribute.binding)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attribute.format, sizeof(attribute.format)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attribute.offset, sizeof(attribute.offset)));
|
||||||
}
|
}
|
||||||
for (const auto& divisor : entry.bindingDivisors) {
|
for (const auto& divisor : entry.bindingDivisors) {
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.binding, sizeof(divisor.binding)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &divisor.binding, sizeof(divisor.binding)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.divisor, sizeof(divisor.divisor)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &divisor.divisor, sizeof(divisor.divisor)));
|
||||||
}
|
}
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
||||||
entry.layoutHash = XXH64_digest(m_hashState);
|
entry.layoutHash = XXH64_digest(m_hashState.Get());
|
||||||
entry.attributeLocationMask = 0;
|
entry.attributeLocationMask = 0;
|
||||||
for (const auto& attribute : entry.attributes) {
|
for (const auto& attribute : entry.attributes) {
|
||||||
if (attribute.location < 32u) {
|
if (attribute.location < 32u) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "VertexInputStateBuilder.h"
|
#include "VertexInputStateBuilder.h"
|
||||||
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
|
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
#include <MG_Util/Types.h>
|
||||||
#include "../VkIncludes.h"
|
#include "../VkIncludes.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
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
|
// matches, so an evicted entry can never be dereferenced through a
|
||||||
// stale memo.
|
// stale memo.
|
||||||
Uint64 m_evictionEpoch = 1;
|
Uint64 m_evictionEpoch = 1;
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline MobileGL::XXH64State m_hashState;
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -594,27 +594,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkRenderPassManager::HashType VkRenderPassManager::ComputeHash(
|
VkRenderPassManager::HashType VkRenderPassManager::ComputeHash(
|
||||||
const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear,
|
const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear,
|
||||||
Bool includeDefaultFboDepthStencil) {
|
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();
|
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
||||||
if (isDefaultFbo) {
|
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
|
// sRGB attachments switch between their sRGB and UNORM-twin views with this
|
||||||
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
|
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
|
||||||
const Bool framebufferSrgbEnabled =
|
const Bool framebufferSrgbEnabled =
|
||||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
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();
|
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();
|
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;
|
Int validDrawBufCount = 0;
|
||||||
for (Int i = 0; i < drawBuffers.size(); ++i) {
|
for (Int i = 0; i < drawBuffers.size(); ++i) {
|
||||||
auto drawbuf = drawBuffers[i];
|
auto drawbuf = drawBuffers[i];
|
||||||
if (drawbuf != FramebufferAttachmentType::None)
|
if (drawbuf != FramebufferAttachmentType::None)
|
||||||
validDrawBufCount = std::max(validDrawBufCount, i + 1);
|
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 combineFramebufferAttachmentObjHash = [&](FramebufferAttachmentType attachment) {
|
||||||
auto& att = fbo.GetAttachment(attachment);
|
auto& att = fbo.GetAttachment(attachment);
|
||||||
@@ -623,49 +623,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (att.IsEmpty()) type = 0;
|
if (att.IsEmpty()) type = 0;
|
||||||
else if (att.IsTexture()) type = 1;
|
else if (att.IsTexture()) type = 1;
|
||||||
else if (att.IsRenderbuffer()) type = 2;
|
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;
|
void* contentPtr = nullptr;
|
||||||
if (att.IsTexture())
|
if (att.IsTexture())
|
||||||
contentPtr = att.GetTexture().get();
|
contentPtr = att.GetTexture().get();
|
||||||
else if (att.IsRenderbuffer())
|
else if (att.IsRenderbuffer())
|
||||||
contentPtr = att.GetRenderbuffer().get();
|
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()) {
|
if (att.IsTexture()) {
|
||||||
const Uint64 textureLifetimeId = att.GetTexture()->GetLifetimeId();
|
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();
|
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();
|
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();
|
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();
|
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;
|
Uint64 imageIdentity = 0;
|
||||||
auto* texture = att.GetTexture().get();
|
auto* texture = att.GetTexture().get();
|
||||||
auto* resource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
|
auto* resource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
|
||||||
if (resource != nullptr) {
|
if (resource != nullptr) {
|
||||||
imageIdentity = reinterpret_cast<Uint64>(resource->image);
|
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 {
|
} else {
|
||||||
const VkSampleCountFlagBits fallbackSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
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()) {
|
if (includePendingClear && att.IsTexture()) {
|
||||||
auto* texture = att.GetTexture().get();
|
auto* texture = att.GetTexture().get();
|
||||||
const auto pendingClearKey = VkClearManager::MakePendingClearKey(att);
|
const auto pendingClearKey = VkClearManager::MakePendingClearKey(att);
|
||||||
auto hasClear = m_clearManager.HasPendingClear(pendingClearKey);
|
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) {
|
if (hasClear) {
|
||||||
ClearAttachmentPayload clearPayload{};
|
ClearAttachmentPayload clearPayload{};
|
||||||
Bool hasPayload = m_clearManager.GetPendingClear(pendingClearKey, 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) {
|
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;
|
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()) {
|
if (att.IsRenderbuffer() && att.GetRenderbuffer()) {
|
||||||
const auto& renderbuffer = att.GetRenderbuffer();
|
const auto& renderbuffer = att.GetRenderbuffer();
|
||||||
@@ -703,10 +703,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Int width = renderbuffer->GetWidth();
|
const Int width = renderbuffer->GetWidth();
|
||||||
const Int height = renderbuffer->GetHeight();
|
const Int height = renderbuffer->GetHeight();
|
||||||
const Int samples = renderbuffer->GetSamples();
|
const Int samples = renderbuffer->GetSamples();
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &internalFormat, sizeof(internalFormat)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &internalFormat, sizeof(internalFormat)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &width, sizeof(width)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &width, sizeof(width)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &height, sizeof(height)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &height, sizeof(height)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &samples, sizeof(samples)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &samples, sizeof(samples)));
|
||||||
|
|
||||||
Uint64 imageIdentity = 0;
|
Uint64 imageIdentity = 0;
|
||||||
VkImageLayout currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
VkImageLayout currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
@@ -714,25 +714,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (resource != nullptr) {
|
if (resource != nullptr) {
|
||||||
imageIdentity = reinterpret_cast<Uint64>(resource->image);
|
imageIdentity = reinterpret_cast<Uint64>(resource->image);
|
||||||
currentLayout = resource->layout;
|
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 {
|
} else {
|
||||||
const VkSampleCountFlagBits fallbackSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
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) {
|
if (includePendingClear) {
|
||||||
const Bool hasClear = HasPendingRenderbufferClear(att);
|
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) {
|
if (hasClear) {
|
||||||
ClearAttachmentPayload clearPayload{};
|
ClearAttachmentPayload clearPayload{};
|
||||||
const Bool hasPayload = GetPendingRenderbufferClear(renderbuffer.get(), 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) {
|
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
|
// The depth-less default-FBO flavor omits the depth/stencil attachment
|
||||||
// entirely, so it must hash differently from the depth-full flavor.
|
// entirely, so it must hash differently from the depth-full flavor.
|
||||||
const Bool depthStencilIncluded = !isDefaultFbo || includeDefaultFboDepthStencil;
|
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) {
|
if (depthStencilIncluded) {
|
||||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
|
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
|
||||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
|
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
|
||||||
}
|
}
|
||||||
|
|
||||||
return XXH64_digest(m_hashState);
|
return XXH64_digest(m_hashState.Get());
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||||
|
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
#include <MG_Util/Types.h>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vk_mem_alloc.h>
|
#include <vk_mem_alloc.h>
|
||||||
|
|
||||||
@@ -391,7 +392,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
|
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
|
||||||
void CollectDeferredRenderbufferReleases(Bool destroyAll);
|
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 ActiveRenderPassInfo s_activeRenderPass{};
|
||||||
static inline Bool s_hasActiveRenderPass = false;
|
static inline Bool s_hasActiveRenderPass = false;
|
||||||
static inline VkClearManager* s_clearManager = nullptr;
|
static inline VkClearManager* s_clearManager = nullptr;
|
||||||
|
|||||||
@@ -134,41 +134,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const MG_State::GLState::ITextureObject& texture,
|
const MG_State::GLState::ITextureObject& texture,
|
||||||
Bool forceNearestFiltering, Bool singleLevelView) const {
|
Bool forceNearestFiltering, Bool singleLevelView) const {
|
||||||
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
|
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.Get(), &forceNearestFiltering, sizeof(forceNearestFiltering)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &singleLevelView, sizeof(singleLevelView)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &singleLevelView, sizeof(singleLevelView)));
|
||||||
|
|
||||||
const auto minFilter = sampler.GetMinFilter();
|
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();
|
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();
|
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();
|
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();
|
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();
|
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 maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
||||||
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
|
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &minLod, sizeof(minLod)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &maxLod, sizeof(maxLod)));
|
||||||
const auto lodBias = sampler.GetLodBias();
|
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
|
// 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
|
// 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.
|
// VkSampler, while two samplers that really do differ must not collide onto the first one's.
|
||||||
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering);
|
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();
|
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();
|
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);
|
const auto borderColor = ResolveVkBorderColor(sampler, texture);
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor)));
|
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &borderColor, sizeof(borderColor)));
|
||||||
return XXH64_digest(m_hashState);
|
return XXH64_digest(m_hashState.Get());
|
||||||
}
|
}
|
||||||
|
|
||||||
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "../VkIncludes.h"
|
#include "../VkIncludes.h"
|
||||||
#include "../VulkanRendererConfig.h"
|
#include "../VulkanRendererConfig.h"
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
#include <MG_Util/Types.h>
|
||||||
#include <MG_State/GLState/SamplerState/SamplerObject.h>
|
#include <MG_State/GLState/SamplerState/SamplerObject.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
@@ -85,6 +86,6 @@ private:
|
|||||||
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
|
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
|
||||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||||
Uint64 m_frameBoundaryCounter = 0;
|
Uint64 m_frameBoundaryCounter = 0;
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline MobileGL::XXH64State m_hashState;
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -300,8 +300,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||||
commandBuffer, newResource.image, newResource.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
commandBuffer, newResource.image, newResource.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels,
|
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels);
|
||||||
newResource.arrayLayers);
|
|
||||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare destination image");
|
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare destination image");
|
||||||
|
|
||||||
VkImageLayout srcTrackedLayout = oldResource.layout;
|
VkImageLayout srcTrackedLayout = oldResource.layout;
|
||||||
@@ -311,8 +310,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ok = VkTextureManager::TransitionImageLayout(
|
ok = VkTextureManager::TransitionImageLayout(
|
||||||
commandBuffer, oldResource.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
commandBuffer, oldResource.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels,
|
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels);
|
||||||
oldResource.arrayLayers);
|
|
||||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare source image");
|
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare source image");
|
||||||
|
|
||||||
Vector<VkImageCopy> copyRegions;
|
Vector<VkImageCopy> copyRegions;
|
||||||
@@ -344,8 +342,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ok = VkTextureManager::TransitionImageLayout(
|
ok = VkTextureManager::TransitionImageLayout(
|
||||||
commandBuffer, newResource.image, newResource.layout, oldResource.layout,
|
commandBuffer, newResource.image, newResource.layout, oldResource.layout,
|
||||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask,
|
VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask,
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels,
|
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels);
|
||||||
newResource.arrayLayers);
|
|
||||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to restore destination layout");
|
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to restore destination layout");
|
||||||
|
|
||||||
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(texture preserve)");
|
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(texture preserve)");
|
||||||
@@ -1191,7 +1188,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Bool lowerTransitioned = TransitionImageLayout(
|
const Bool lowerTransitioned = TransitionImageLayout(
|
||||||
commandBuffer, resource.image, lowerMipLayout, newLayout,
|
commandBuffer, resource.image, lowerMipLayout, newLayout,
|
||||||
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
||||||
resource.aspect, 0, writtenMipLevel, resource.arrayLayers);
|
resource.aspect, 0, writtenMipLevel);
|
||||||
MOBILEGL_ASSERT(lowerTransitioned,
|
MOBILEGL_ASSERT(lowerTransitioned,
|
||||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition lower mip levels for textureId=%d",
|
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition lower mip levels for textureId=%d",
|
||||||
texture->GetExternalIndex());
|
texture->GetExternalIndex());
|
||||||
@@ -1203,8 +1200,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Bool upperTransitioned = TransitionImageLayout(
|
const Bool upperTransitioned = TransitionImageLayout(
|
||||||
commandBuffer, resource.image, upperMipLayout, newLayout,
|
commandBuffer, resource.image, upperMipLayout, newLayout,
|
||||||
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
||||||
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel,
|
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel);
|
||||||
resource.arrayLayers);
|
|
||||||
MOBILEGL_ASSERT(upperTransitioned,
|
MOBILEGL_ASSERT(upperTransitioned,
|
||||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition upper mip levels for textureId=%d",
|
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition upper mip levels for textureId=%d",
|
||||||
texture->GetExternalIndex());
|
texture->GetExternalIndex());
|
||||||
@@ -1257,8 +1253,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
||||||
s_sampledReadStages, srcAccessMask,
|
s_sampledReadStages, srcAccessMask,
|
||||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels);
|
||||||
resource->arrayLayers);
|
|
||||||
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
||||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||||
StampResourceRecordingUse(*resource);
|
StampResourceRecordingUse(*resource);
|
||||||
@@ -1288,7 +1283,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VK_IMAGE_LAYOUT_GENERAL, srcStageMask,
|
VK_IMAGE_LAYOUT_GENERAL, srcStageMask,
|
||||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, srcAccessMask,
|
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, srcAccessMask,
|
||||||
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
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",
|
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
|
||||||
texture.GetExternalIndex());
|
texture.GetExternalIndex());
|
||||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||||
@@ -1355,8 +1350,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkImageLayout& trackedLayout, VkImageLayout newLayout,
|
VkImageLayout& trackedLayout, VkImageLayout newLayout,
|
||||||
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
|
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
|
||||||
VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask,
|
VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask,
|
||||||
VkImageAspectFlags aspectMask, Uint32 baseMipLevel, Uint32 levelCount,
|
VkImageAspectFlags aspectMask, Uint32 baseMipLevel,
|
||||||
Uint32 layerCount) {
|
Uint32 levelCount) {
|
||||||
MOBILEGL_ASSERT(image != VK_NULL_HANDLE, "TransitionImageLayout: m_image == VK_NULL_HANDLE");
|
MOBILEGL_ASSERT(image != VK_NULL_HANDLE, "TransitionImageLayout: m_image == VK_NULL_HANDLE");
|
||||||
MOBILEGL_ASSERT(!((dstAccessMask & VK_ACCESS_TRANSFER_READ_BIT) != 0 &&
|
MOBILEGL_ASSERT(!((dstAccessMask & VK_ACCESS_TRANSFER_READ_BIT) != 0 &&
|
||||||
(dstStageMask & VK_PIPELINE_STAGE_TRANSFER_BIT) == 0),
|
(dstStageMask & VK_PIPELINE_STAGE_TRANSFER_BIT) == 0),
|
||||||
@@ -1381,7 +1376,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
barrier.subresourceRange.baseMipLevel = baseMipLevel;
|
barrier.subresourceRange.baseMipLevel = baseMipLevel;
|
||||||
barrier.subresourceRange.levelCount = levelCount;
|
barrier.subresourceRange.levelCount = levelCount;
|
||||||
barrier.subresourceRange.baseArrayLayer = 0;
|
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);
|
vkCmdPipelineBarrier(commandBuffer, srcStageMask, dstStageMask, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||||
|
|
||||||
trackedLayout = newLayout;
|
trackedLayout = newLayout;
|
||||||
@@ -1993,7 +1994,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Bound the idle pool: a one-off giant upload (initial atlas define)
|
// Bound the idle pool: a one-off giant upload (initial atlas define)
|
||||||
// must not pin its staging memory forever.
|
// must not pin its staging memory forever.
|
||||||
constexpr VkDeviceSize kMaxFreeUploadStagingBytes = 32u * 1024u * 1024u;
|
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);
|
vmaDestroyBuffer(m_allocator, block.buffer, block.allocation);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2605,7 +2613,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
uploadSrcAccessMask,
|
uploadSrcAccessMask,
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
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");
|
MOBILEGL_ASSERT(ok, "TransitionImageLayout to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL failed");
|
||||||
|
|
||||||
// Array textures keep their GL "depth" in VkImage array layers, so the
|
// Array textures keep their GL "depth" in VkImage array layers, so the
|
||||||
@@ -2709,7 +2717,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
s_sampledReadStages,
|
s_sampledReadStages,
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||||
VK_ACCESS_SHADER_READ_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");
|
MOBILEGL_ASSERT(ok, "TransitionImageLayout to sampled read-only layout failed");
|
||||||
outResource.layout = finalLayout;
|
outResource.layout = finalLayout;
|
||||||
|
|
||||||
|
|||||||
@@ -388,12 +388,24 @@ public:
|
|||||||
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||||
static Bool AreStorageImageViewFormatsCompatible(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,
|
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
|
||||||
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
|
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
|
||||||
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
|
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
|
||||||
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask,
|
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask,
|
||||||
Uint32 baseMipLevel = 0, Uint32 levelCount = 1,
|
Uint32 baseMipLevel = 0, Uint32 levelCount = 1);
|
||||||
Uint32 layerCount = 1);
|
|
||||||
|
|
||||||
SizeT CollectGarbage();
|
SizeT CollectGarbage();
|
||||||
|
|
||||||
|
|||||||
@@ -238,11 +238,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// nothing else across all of gl33.
|
// nothing else across all of gl33.
|
||||||
//
|
//
|
||||||
// The mapping below is derived from - and at full extent exactly reproduces - the pixel
|
// The mapping below is derived from - and at full extent exactly reproduces - the pixel
|
||||||
// mapping RemapDefaultFboReadbackToGLOrientation has always used:
|
// mapping VulkanRenderer::RemapDefaultFramebufferReadback uses:
|
||||||
// identity : image(x, H-1-y) -> flip Y
|
// identity : image(x, H-1-y) -> flip Y
|
||||||
// 180 : image(W-1-x, y) -> mirror X (the rotation already flips the rows)
|
// 180 : image(W-1-x, y) -> mirror X (the rotation already flips the rows)
|
||||||
// Quarter turns swap the axes; nothing in this renderer models that (the readback declines to
|
// Quarter turns swap the axes and are handled by MapDefaultFramebufferReadbackRect rather than
|
||||||
// remap them and the viewport path only rescales), so they are left exactly as they were.
|
// this same-axis helper.
|
||||||
struct DefaultFramebufferRectMapping {
|
struct DefaultFramebufferRectMapping {
|
||||||
Bool flipY = false;
|
Bool flipY = false;
|
||||||
Bool mirrorX = false;
|
Bool mirrorX = false;
|
||||||
@@ -335,19 +335,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Complete input inventory of ApplyDynamicDrawStateTail, one line per reader
|
// Complete input inventory of ApplyDynamicDrawStateTail, one line per reader
|
||||||
// (each accessor it replaces is a verified plain field read of the same
|
// (each accessor it replaces is a verified plain field read of the same
|
||||||
// RenderStateParameters field - RenderState.cpp):
|
// RenderStateParameters field - RenderState.cpp):
|
||||||
// ApplyGLViewportState : Viewport, DepthRange, + extent/isDefaultFbo/preTransform
|
// ApplyGLViewportState : Viewports[0], DepthRanges[0], + extent/isDefaultFbo/preTransform
|
||||||
// ApplyBlendConstants : BlendColor
|
// ApplyBlendConstants : BlendColor
|
||||||
// ApplyPolygonOffsetState : PolygonOffsetUnits, PolygonOffsetFactor
|
// ApplyPolygonOffsetState : PolygonOffsetUnits, PolygonOffsetFactor
|
||||||
// ApplyLineWidthState : LineWidth (see the caveat below)
|
// ApplyLineWidthState : LineWidth (see the caveat below)
|
||||||
// ApplyStencilState : StencilStates[0..1].{ValueMask, WriteMask, Ref}
|
// ApplyStencilState : StencilStates[0..1].{ValueMask, WriteMask, Ref}
|
||||||
// scissor rect : ScissorTestEnabled, ScissorBox,
|
// scissor rect : ScissorTestEnabledMask bit 0, ScissorBoxes[0],
|
||||||
// + extent/isDefaultFbo/preTransform
|
// + extent/isDefaultFbo/preTransform
|
||||||
// Caveat, unchanged from the version-only gate: ApplyLineWidthState also clamps
|
// Caveat, unchanged from the version-only gate: ApplyLineWidthState also clamps
|
||||||
// to the ACTIVE BACKEND OBJECT's aliased line-width range. Those are device
|
// to the ACTIVE BACKEND OBJECT's aliased line-width range. Those are device
|
||||||
// limits queried once at backend init and constant for the renderer's lifetime,
|
// limits queried once at backend init and constant for the renderer's lifetime,
|
||||||
// so they are not part of the key (the version gate never covered them either).
|
// so they are not part of the key (the version gate never covered them either).
|
||||||
struct DynamicTailKey {
|
struct DynamicTailKey {
|
||||||
Int viewport[4] = {0, 0, 0, 0};
|
Float viewport[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
Float depthRange[2] = {0.0f, 0.0f};
|
Float depthRange[2] = {0.0f, 0.0f};
|
||||||
Float blendColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
Float blendColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
Float polygonOffsetFactor = 0.0f;
|
Float polygonOffsetFactor = 0.0f;
|
||||||
@@ -437,12 +437,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
|
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ApplyGLViewportState(VkCommandBuffer commandBuffer,
|
// One viewport of the ARB_viewport_array state, mapped into Vulkan's frame. Split out of
|
||||||
const IntVec2& framebufferExtent,
|
// ApplyGLViewportState so the multi-viewport path derives index i through EXACTLY the same
|
||||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
// arithmetic as index 0 - the default-framebuffer Y-flip and pre-transform rotation
|
||||||
Bool isDefaultFramebuffer) {
|
// especially, which is the classic way a multi-viewport port comes out upside down for every
|
||||||
const IntVec4& viewportState = MG_State::pGLContext->GetViewport();
|
// index but the one that was tested.
|
||||||
const FloatVec2& depthRange = MG_State::pGLContext->GetDepthRange();
|
static VkViewport ComputeGLViewport(Uint32 index,
|
||||||
|
const IntVec2& framebufferExtent,
|
||||||
|
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||||
|
Bool isDefaultFramebuffer) {
|
||||||
|
// Snapped to integers. The viewport is float STATE (glViewportIndexedf may set a
|
||||||
|
// fractional origin, and GetFloati_v hands it back verbatim), but what rasterizes here is
|
||||||
|
// the rounded rectangle - a deliberate, documented infidelity rather than a spec claim:
|
||||||
|
// MobileGL passes the driver's VIEWPORT_SUBPIXEL_BITS through, so it does advertise
|
||||||
|
// subpixel viewport precision it does not deliver. Nothing in KHR-GL43.viewport_array or
|
||||||
|
// in Minecraft sets a fractional viewport (the conformance checks are all on the state
|
||||||
|
// round trip), which is why the honest-but-lossy path was kept over widening every
|
||||||
|
// default-framebuffer Y-flip/pre-transform helper to floats. See the KNOWN INFIDELITY
|
||||||
|
// note in MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp.
|
||||||
|
const FloatVec4& stored = MG_State::pGLContext->GetViewportIndexed(index);
|
||||||
|
const IntVec4 viewportState(static_cast<Int>(std::lround(stored.x())),
|
||||||
|
static_cast<Int>(std::lround(stored.y())),
|
||||||
|
static_cast<Int>(std::lround(stored.z())),
|
||||||
|
static_cast<Int>(std::lround(stored.w())));
|
||||||
|
const FloatVec2& depthRange = MG_State::pGLContext->GetDepthRangeIndexed(index);
|
||||||
const IntVec2 logicalExtent = isDefaultFramebuffer
|
const IntVec2 logicalExtent = isDefaultFramebuffer
|
||||||
? ResolveDefaultFramebufferLogicalExtent(preTransform, framebufferExtent)
|
? ResolveDefaultFramebufferLogicalExtent(preTransform, framebufferExtent)
|
||||||
: framebufferExtent;
|
: framebufferExtent;
|
||||||
@@ -477,6 +495,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
viewport.height = static_cast<float>(viewportHeight);
|
viewport.height = static_cast<float>(viewportHeight);
|
||||||
viewport.minDepth = depthRange.x();
|
viewport.minDepth = depthRange.x();
|
||||||
viewport.maxDepth = depthRange.y();
|
viewport.maxDepth = depthRange.y();
|
||||||
|
return viewport;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ApplyGLViewportState(VkCommandBuffer commandBuffer,
|
||||||
|
const IntVec2& framebufferExtent,
|
||||||
|
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||||
|
Bool isDefaultFramebuffer) {
|
||||||
|
const VkViewport viewport = ComputeGLViewport(0, framebufferExtent, preTransform, isDefaultFramebuffer);
|
||||||
auto& shadow = g_dynamicStateShadow;
|
auto& shadow = g_dynamicStateShadow;
|
||||||
if (shadow.viewportValid && shadow.viewport.x == viewport.x && shadow.viewport.y == viewport.y &&
|
if (shadow.viewportValid && shadow.viewport.x == viewport.x && shadow.viewport.y == viewport.y &&
|
||||||
shadow.viewport.width == viewport.width && shadow.viewport.height == viewport.height &&
|
shadow.viewport.width == viewport.width && shadow.viewport.height == viewport.height &&
|
||||||
@@ -2125,47 +2151,6 @@ void main() {
|
|||||||
return static_cast<Uint8>(value * 255.0f + 0.5f);
|
return static_cast<Uint8>(value * 255.0f + 0.5f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-order the copied BLOCK - not the whole image - from the default framebuffer's stored
|
|
||||||
// orientation into GL's. The caller has already aimed the copy at the right place with
|
|
||||||
// MapDefaultFramebufferRectAxis, so what arrives here is exactly the requested
|
|
||||||
// rectWidth x rectHeight rect, and all that is left is the order of rows (identity) or of
|
|
||||||
// columns (180) WITHIN it.
|
|
||||||
//
|
|
||||||
// This used to iterate the full swapchain extent and index both sides with that stride,
|
|
||||||
// which is why its caller could only use it on an exact full-extent read - and why every
|
|
||||||
// partial glReadPixels of the default framebuffer came back in Vulkan row order. Only
|
|
||||||
// identity/180 share the swapchain extent with the default framebuffer; 90/270 swap
|
|
||||||
// extents and are still declined.
|
|
||||||
static Bool RemapDefaultFboReadbackToGLOrientation(const Uint8* rawPixels,
|
|
||||||
Uint32 rectWidth,
|
|
||||||
Uint32 rectHeight,
|
|
||||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
|
||||||
SizeT texelSize,
|
|
||||||
Uint8* outPixels) {
|
|
||||||
if (IsQuarterTurnPreTransform(preTransform)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (rectWidth == 0 || rectHeight == 0 || texelSize == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const DefaultFramebufferRectMapping mapping = GetDefaultFramebufferRectMapping(preTransform);
|
|
||||||
const SizeT rowBytes = static_cast<SizeT>(rectWidth) * texelSize;
|
|
||||||
for (Uint32 outY = 0; outY < rectHeight; ++outY) {
|
|
||||||
const Uint32 srcY = mapping.flipY ? (rectHeight - 1 - outY) : outY;
|
|
||||||
const Uint8* srcRow = rawPixels + static_cast<SizeT>(srcY) * rowBytes;
|
|
||||||
Uint8* dstRow = outPixels + static_cast<SizeT>(outY) * rowBytes;
|
|
||||||
if (!mapping.mirrorX) {
|
|
||||||
Memcpy(dstRow, srcRow, rowBytes);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (Uint32 outX = 0; outX < rectWidth; ++outX) {
|
|
||||||
Memcpy(dstRow + static_cast<SizeT>(outX) * texelSize,
|
|
||||||
srcRow + static_cast<SizeT>(rectWidth - 1 - outX) * texelSize, texelSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static SizeT AlignPixelRow(SizeT rowBytes, Int alignment) {
|
static SizeT AlignPixelRow(SizeT rowBytes, Int alignment) {
|
||||||
const SizeT resolvedAlignment = static_cast<SizeT>(std::max(alignment, 1));
|
const SizeT resolvedAlignment = static_cast<SizeT>(std::max(alignment, 1));
|
||||||
return (rowBytes + resolvedAlignment - 1) & ~(resolvedAlignment - 1);
|
return (rowBytes + resolvedAlignment - 1) & ~(resolvedAlignment - 1);
|
||||||
@@ -2712,6 +2697,95 @@ void main() {
|
|||||||
return formatInfo.texel_block_size;
|
return formatInfo.texel_block_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool VulkanRenderer::MapDefaultFramebufferReadbackRect(
|
||||||
|
GLint x, GLint y, GLsizei width, GLsizei height, VkExtent2D imageExtent,
|
||||||
|
VkSurfaceTransformFlagBitsKHR preTransform, VkOffset2D* imageOffset,
|
||||||
|
VkExtent2D* imageCopyExtent) {
|
||||||
|
if (width <= 0 || height <= 0 || imageOffset == nullptr || imageCopyExtent == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Int imageWidth = static_cast<Int>(imageExtent.width);
|
||||||
|
const Int imageHeight = static_cast<Int>(imageExtent.height);
|
||||||
|
Int mappedX = x;
|
||||||
|
Int mappedY = y;
|
||||||
|
Uint32 mappedWidth = static_cast<Uint32>(width);
|
||||||
|
Uint32 mappedHeight = static_cast<Uint32>(height);
|
||||||
|
|
||||||
|
// InsertPositionFixup first flips GL Y and then applies the surface transform. In pixel
|
||||||
|
// coordinates that gives these half-open rectangle mappings into the stored image:
|
||||||
|
// identity: (x, H-y-h), 90: (y, x), 180: (W-x-w, y), 270: (H-y-h, W-x-w).
|
||||||
|
// Quarter turns also transpose the copied block's extent.
|
||||||
|
switch (preTransform) {
|
||||||
|
case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR:
|
||||||
|
mappedX = y;
|
||||||
|
mappedY = x;
|
||||||
|
mappedWidth = static_cast<Uint32>(height);
|
||||||
|
mappedHeight = static_cast<Uint32>(width);
|
||||||
|
break;
|
||||||
|
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
|
||||||
|
mappedX = imageWidth - x - width;
|
||||||
|
mappedY = y;
|
||||||
|
break;
|
||||||
|
case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR:
|
||||||
|
mappedX = imageWidth - y - height;
|
||||||
|
mappedY = imageHeight - x - width;
|
||||||
|
mappedWidth = static_cast<Uint32>(height);
|
||||||
|
mappedHeight = static_cast<Uint32>(width);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
mappedY = imageHeight - y - height;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mappedX < 0 || mappedY < 0 || mappedWidth > imageExtent.width ||
|
||||||
|
mappedHeight > imageExtent.height ||
|
||||||
|
static_cast<Uint64>(mappedX) + mappedWidth > imageExtent.width ||
|
||||||
|
static_cast<Uint64>(mappedY) + mappedHeight > imageExtent.height) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*imageOffset = {mappedX, mappedY};
|
||||||
|
*imageCopyExtent = {mappedWidth, mappedHeight};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool VulkanRenderer::RemapDefaultFramebufferReadback(
|
||||||
|
const Uint8* rawPixels, Uint32 logicalWidth, Uint32 logicalHeight,
|
||||||
|
VkSurfaceTransformFlagBitsKHR preTransform, SizeT texelSize, Uint8* outPixels) {
|
||||||
|
if (rawPixels == nullptr || outPixels == nullptr || logicalWidth == 0 || logicalHeight == 0 ||
|
||||||
|
texelSize == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Uint32 rawWidth = IsQuarterTurnPreTransform(preTransform) ? logicalHeight : logicalWidth;
|
||||||
|
for (Uint32 outY = 0; outY < logicalHeight; ++outY) {
|
||||||
|
for (Uint32 outX = 0; outX < logicalWidth; ++outX) {
|
||||||
|
Uint32 srcX = outX;
|
||||||
|
Uint32 srcY = outY;
|
||||||
|
switch (preTransform) {
|
||||||
|
case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR:
|
||||||
|
srcX = outY;
|
||||||
|
srcY = outX;
|
||||||
|
break;
|
||||||
|
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
|
||||||
|
srcX = logicalWidth - 1 - outX;
|
||||||
|
break;
|
||||||
|
case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR:
|
||||||
|
srcX = logicalHeight - 1 - outY;
|
||||||
|
srcY = logicalWidth - 1 - outX;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
srcY = logicalHeight - 1 - outY;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Memcpy(outPixels + (static_cast<SizeT>(outY) * logicalWidth + outX) * texelSize,
|
||||||
|
rawPixels + (static_cast<SizeT>(srcY) * rawWidth + srcX) * texelSize,
|
||||||
|
texelSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Bool VulkanRenderer::ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
Bool VulkanRenderer::ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
||||||
GLsizei width, GLsizei height, GLenum destinationFormat,
|
GLsizei width, GLsizei height, GLenum destinationFormat,
|
||||||
GLenum destinationType, SizeT destinationRowStride,
|
GLenum destinationType, SizeT destinationRowStride,
|
||||||
@@ -4879,6 +4953,7 @@ void main() {
|
|||||||
.topology = vkTopology,
|
.topology = vkTopology,
|
||||||
.primitiveRestartEnable = primitiveRestartEnabled,
|
.primitiveRestartEnable = primitiveRestartEnabled,
|
||||||
.patchControlPoints = static_cast<Uint32>(MG_State::pGLContext->GetPatchVertices()),
|
.patchControlPoints = static_cast<Uint32>(MG_State::pGLContext->GetPatchVertices()),
|
||||||
|
.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin),
|
||||||
.polygonMode = effectivePolygonMode,
|
.polygonMode = effectivePolygonMode,
|
||||||
.cullMode = cullFaceEnabled
|
.cullMode = cullFaceEnabled
|
||||||
? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise)
|
? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise)
|
||||||
@@ -5321,9 +5396,71 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void VulkanRenderer::ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent,
|
// The scissor rectangle Vulkan needs for ARB_viewport_array index `index`. Vulkan has no
|
||||||
Bool isDefaultFbo) {
|
// per-viewport scissor-test TOGGLE - a scissor rectangle always applies - so an index whose
|
||||||
|
// GL scissor test is disabled gets the whole framebuffer, which is exactly "the test always
|
||||||
|
// passes" (GL 4.6 core 17.3.2).
|
||||||
|
VkRect2D VulkanRenderer::ComputeGLScissorRect(Uint32 index, const IntVec2& extent,
|
||||||
|
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||||
|
Bool isDefaultFbo) const {
|
||||||
|
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
|
||||||
|
if ((parameters.ScissorTestEnabledMask & (1u << index)) == 0) {
|
||||||
|
VkRect2D full{};
|
||||||
|
full.offset = {0, 0};
|
||||||
|
full.extent = {static_cast<Uint32>(extent.x()), static_cast<Uint32>(extent.y())};
|
||||||
|
return full;
|
||||||
|
}
|
||||||
|
const IntVec4& scissorBox = parameters.ScissorBoxes[index];
|
||||||
|
return isDefaultFbo ? MakeDefaultFramebufferScissorRect(scissorBox, extent, preTransform)
|
||||||
|
: MakeClampedScissorRect(scissorBox, extent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The wide half of ApplyDynamicDrawStateTail: a pipeline built for a gl_ViewportIndex-writing
|
||||||
|
// program declares viewportCount > 1, and Vulkan then requires that many viewports AND that
|
||||||
|
// many scissors to have been set before the draw
|
||||||
|
// (VUID-vkCmdDraw-viewportCount-03417/-03418). Deliberately unmemoized: only conformance
|
||||||
|
// shaders reach it, the single-element dynamic-state shadow cannot describe an array, and
|
||||||
|
// leaving that shadow invalidated is what makes the next ordinary draw re-push its own
|
||||||
|
// single viewport instead of believing the array's element 0 is already bound.
|
||||||
|
void VulkanRenderer::ApplyMultiViewportDynamicState(VkCommandBuffer commandBuffer, Uint32 viewportCount,
|
||||||
|
const IntVec2& extent,
|
||||||
|
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||||
|
Bool isDefaultFbo) {
|
||||||
|
MOBILEGL_ASSERT(viewportCount <= RenderStateParameters::MAX_VIEWPORTS,
|
||||||
|
"ApplyMultiViewportDynamicState: viewportCount=%u exceeds the indexed state width",
|
||||||
|
viewportCount);
|
||||||
|
const Uint32 count = std::min<Uint32>(viewportCount, RenderStateParameters::MAX_VIEWPORTS);
|
||||||
|
|
||||||
|
Array<VkViewport, RenderStateParameters::MAX_VIEWPORTS> viewports{};
|
||||||
|
Array<VkRect2D, RenderStateParameters::MAX_VIEWPORTS> scissors{};
|
||||||
|
for (Uint32 i = 0; i < count; ++i) {
|
||||||
|
viewports[i] = ComputeGLViewport(i, extent, preTransform, isDefaultFbo);
|
||||||
|
scissors[i] = ComputeGLScissorRect(i, extent, preTransform, isDefaultFbo);
|
||||||
|
}
|
||||||
|
vkCmdSetViewport(commandBuffer, 0, count, viewports.data());
|
||||||
|
vkCmdSetScissor(commandBuffer, 0, count, scissors.data());
|
||||||
|
|
||||||
auto& shadow = g_dynamicStateShadow;
|
auto& shadow = g_dynamicStateShadow;
|
||||||
|
shadow.viewportValid = false;
|
||||||
|
shadow.scissorValid = false;
|
||||||
|
shadow.dynamicTailValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VulkanRenderer::ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent,
|
||||||
|
Bool isDefaultFbo, Uint32 viewportCount) {
|
||||||
|
auto& shadow = g_dynamicStateShadow;
|
||||||
|
if (viewportCount > 1) {
|
||||||
|
// The other five Apply* still run: blend constants, depth bias, line width and the
|
||||||
|
// stencil masks are not per-viewport and a multi-viewport draw needs them just as
|
||||||
|
// much. Only the viewport/scissor pair takes the array shape.
|
||||||
|
ApplyBlendConstants(frame.commandBuffer);
|
||||||
|
ApplyPolygonOffsetState(frame.commandBuffer);
|
||||||
|
ApplyLineWidthState(frame.commandBuffer);
|
||||||
|
ApplyStencilState(frame.commandBuffer);
|
||||||
|
ApplyMultiViewportDynamicState(frame.commandBuffer, viewportCount, extent,
|
||||||
|
m_swapchainObject.GetPreTransform(), isDefaultFbo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// One compare for the whole tail: see the gate's declaration in
|
// One compare for the whole tail: see the gate's declaration in
|
||||||
// DynamicStateShadow for why (version, extent, default-FBO flag) pins every
|
// DynamicStateShadow for why (version, extent, default-FBO flag) pins every
|
||||||
// input the six Apply* below read.
|
// input the six Apply* below read.
|
||||||
@@ -5342,12 +5479,14 @@ void main() {
|
|||||||
DynamicStateShadow::DynamicTailKey key;
|
DynamicStateShadow::DynamicTailKey key;
|
||||||
{
|
{
|
||||||
const RenderStateParameters& p = MG_State::pGLContext->GetRenderStateParameters();
|
const RenderStateParameters& p = MG_State::pGLContext->GetRenderStateParameters();
|
||||||
key.viewport[0] = p.Viewport.x();
|
// Viewport 0 and its depth range: ApplyGLViewportState reads exactly those two
|
||||||
key.viewport[1] = p.Viewport.y();
|
// (per-index state for indices > 0 is keyed separately, see multiViewportKey below).
|
||||||
key.viewport[2] = p.Viewport.z();
|
key.viewport[0] = p.Viewports[0].x();
|
||||||
key.viewport[3] = p.Viewport.w();
|
key.viewport[1] = p.Viewports[0].y();
|
||||||
key.depthRange[0] = p.DepthRange.x();
|
key.viewport[2] = p.Viewports[0].z();
|
||||||
key.depthRange[1] = p.DepthRange.y();
|
key.viewport[3] = p.Viewports[0].w();
|
||||||
|
key.depthRange[0] = p.DepthRanges[0].x();
|
||||||
|
key.depthRange[1] = p.DepthRanges[0].y();
|
||||||
key.blendColor[0] = p.BlendColor.x();
|
key.blendColor[0] = p.BlendColor.x();
|
||||||
key.blendColor[1] = p.BlendColor.y();
|
key.blendColor[1] = p.BlendColor.y();
|
||||||
key.blendColor[2] = p.BlendColor.z();
|
key.blendColor[2] = p.BlendColor.z();
|
||||||
@@ -5362,11 +5501,11 @@ void main() {
|
|||||||
key.stencilWriteMask[face] = p.StencilStates[face].WriteMask;
|
key.stencilWriteMask[face] = p.StencilStates[face].WriteMask;
|
||||||
key.stencilRef[face] = p.StencilStates[face].Ref;
|
key.stencilRef[face] = p.StencilStates[face].Ref;
|
||||||
}
|
}
|
||||||
key.scissorEnabled = p.ScissorTestEnabled;
|
key.scissorEnabled = (p.ScissorTestEnabledMask & 1u) != 0;
|
||||||
key.scissorBox[0] = p.ScissorBox.x();
|
key.scissorBox[0] = p.ScissorBoxes[0].x();
|
||||||
key.scissorBox[1] = p.ScissorBox.y();
|
key.scissorBox[1] = p.ScissorBoxes[0].y();
|
||||||
key.scissorBox[2] = p.ScissorBox.z();
|
key.scissorBox[2] = p.ScissorBoxes[0].z();
|
||||||
key.scissorBox[3] = p.ScissorBox.w();
|
key.scissorBox[3] = p.ScissorBoxes[0].w();
|
||||||
key.extentX = extent.x();
|
key.extentX = extent.x();
|
||||||
key.extentY = extent.y();
|
key.extentY = extent.y();
|
||||||
key.preTransform = static_cast<Uint32>(preTransform);
|
key.preTransform = static_cast<Uint32>(preTransform);
|
||||||
@@ -5759,7 +5898,7 @@ void main() {
|
|||||||
const Bool idxUploadOk = UploadAndBindIndexBuffer(frame, vao, pIndexBufferView);
|
const Bool idxUploadOk = UploadAndBindIndexBuffer(frame, vao, pIndexBufferView);
|
||||||
MOBILEGL_ASSERT(idxUploadOk, "SetupDraw fast path: failed to upload index buffer");
|
MOBILEGL_ASSERT(idxUploadOk, "SetupDraw fast path: failed to upload index buffer");
|
||||||
}
|
}
|
||||||
ApplyDynamicDrawStateTail(frame, snap.renderPassExtent, snap.drawFboIsDefault);
|
ApplyDynamicDrawStateTail(frame, snap.renderPassExtent, snap.drawFboIsDefault, snap.viewportCount);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6199,7 +6338,8 @@ void main() {
|
|||||||
MOBILEGL_ASSERT(idxUploadOk, "SetupDraw skipped: failed to upload index buffer");
|
MOBILEGL_ASSERT(idxUploadOk, "SetupDraw skipped: failed to upload index buffer");
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyDynamicDrawStateTail(frame, renderPassEntry->extent, drawFbo->IsDefaultFramebuffer());
|
ApplyDynamicDrawStateTail(frame, renderPassEntry->extent, drawFbo->IsDefaultFramebuffer(),
|
||||||
|
ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin));
|
||||||
|
|
||||||
// Snapshot the fully resolved configuration for the consecutive-draw
|
// Snapshot the fully resolved configuration for the consecutive-draw
|
||||||
// fast path (see TrySetupDrawFastPath).
|
// fast path (see TrySetupDrawFastPath).
|
||||||
@@ -6218,6 +6358,7 @@ void main() {
|
|||||||
snap.drawFbo = drawFbo.get();
|
snap.drawFbo = drawFbo.get();
|
||||||
snap.fboVersion = drawFbo->GetObjectVersion();
|
snap.fboVersion = drawFbo->GetObjectVersion();
|
||||||
snap.drawFboIsDefault = drawFboIsDefault;
|
snap.drawFboIsDefault = drawFboIsDefault;
|
||||||
|
snap.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin);
|
||||||
snap.renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
|
snap.renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
|
||||||
snap.bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
snap.bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||||
snap.baseTransformFlags = GetBaseTransformFlagsRaw(drawFboIsDefault);
|
snap.baseTransformFlags = GetBaseTransformFlagsRaw(drawFboIsDefault);
|
||||||
@@ -7113,7 +7254,7 @@ void main() {
|
|||||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||||
commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||||
resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
|
resource->aspect, 0, resource->mipLevels);
|
||||||
MOBILEGL_ASSERT(ok,
|
MOBILEGL_ASSERT(ok,
|
||||||
"MaterializePendingClearForTexture: failed to transition textureId=%d to TRANSFER_DST",
|
"MaterializePendingClearForTexture: failed to transition textureId=%d to TRANSFER_DST",
|
||||||
texture.GetExternalIndex());
|
texture.GetExternalIndex());
|
||||||
@@ -7231,8 +7372,7 @@ void main() {
|
|||||||
ok = VkTextureManager::TransitionImageLayout(
|
ok = VkTextureManager::TransitionImageLayout(
|
||||||
commandBuffer, resource->image, clearLayout, sampledLayout,
|
commandBuffer, resource->image, clearLayout, sampledLayout,
|
||||||
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT,
|
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT,
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels);
|
||||||
resource->arrayLayers);
|
|
||||||
MOBILEGL_ASSERT(ok,
|
MOBILEGL_ASSERT(ok,
|
||||||
"MaterializePendingClearForTexture: failed to transition textureId=%d to sampled layout",
|
"MaterializePendingClearForTexture: failed to transition textureId=%d to sampled layout",
|
||||||
texture.GetExternalIndex());
|
texture.GetExternalIndex());
|
||||||
@@ -7270,7 +7410,7 @@ void main() {
|
|||||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||||
commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||||
resource->aspect, 0, 1, 1);
|
resource->aspect, 0, 1);
|
||||||
MOBILEGL_ASSERT(ok,
|
MOBILEGL_ASSERT(ok,
|
||||||
"MaterializePendingClearForRenderbuffer: failed to transition renderbuffer %u to TRANSFER_DST",
|
"MaterializePendingClearForRenderbuffer: failed to transition renderbuffer %u to TRANSFER_DST",
|
||||||
renderbuffer->GetExternalIndex());
|
renderbuffer->GetExternalIndex());
|
||||||
@@ -7321,7 +7461,7 @@ void main() {
|
|||||||
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
|
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
|
||||||
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
|
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
|
||||||
VK_ACCESS_TRANSFER_READ_BIT,
|
VK_ACCESS_TRANSFER_READ_BIT,
|
||||||
resource->aspect, 0, 1, 1);
|
resource->aspect, 0, 1);
|
||||||
MOBILEGL_ASSERT(ok,
|
MOBILEGL_ASSERT(ok,
|
||||||
"MaterializePendingClearForRenderbuffer: failed to transition renderbuffer %u to steady layout",
|
"MaterializePendingClearForRenderbuffer: failed to transition renderbuffer %u to steady layout",
|
||||||
renderbuffer->GetExternalIndex());
|
renderbuffer->GetExternalIndex());
|
||||||
@@ -7928,6 +8068,9 @@ void main() {
|
|||||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
VkAccessFlags srcAccessMask = 0;
|
VkAccessFlags srcAccessMask = 0;
|
||||||
GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask);
|
GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask);
|
||||||
|
// Both blit regions below name `baseArrayLayer` from their binding, and a layered depth
|
||||||
|
// attachment puts that above 0. These barriers carry a mip range only - their layer
|
||||||
|
// range is every layer (see VkTextureManager::TransitionImageLayout).
|
||||||
if (readIsDefaultFbo) {
|
if (readIsDefaultFbo) {
|
||||||
VkImageLayout srcTrackedLayout = srcOriginalLayout;
|
VkImageLayout srcTrackedLayout = srcOriginalLayout;
|
||||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||||
@@ -8755,30 +8898,22 @@ void main() {
|
|||||||
VkAccessFlags srcAccessMask = 0;
|
VkAccessFlags srcAccessMask = 0;
|
||||||
GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask);
|
GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask);
|
||||||
VkImageLayout srcCopyLayout = srcOriginalLayout;
|
VkImageLayout srcCopyLayout = srcOriginalLayout;
|
||||||
// The barrier has to name every layer the copy touches, not just layer 0 - otherwise the
|
// The barriers below name a MIP range only. Their layer range is not a parameter:
|
||||||
// slice fix above lands the copy on layers the barrier never transitioned, which is the
|
// TransitionImageLayout always covers every layer of the image, which is a superset of the
|
||||||
// same defect one level down. TransitionImageLayout always starts its range at
|
// [baseSlice, baseSlice + depth) the slice mapping above hands the copy.
|
||||||
// baseArrayLayer 0, so VK_REMAINING_ARRAY_LAYERS is the whole range and a superset of
|
|
||||||
// [baseSlice, baseSlice + depth).
|
|
||||||
//
|
|
||||||
// Not `arrayLayers`, which is 1 for a 3D image: MobileGL creates 3D images
|
|
||||||
// 2D_ARRAY_COMPATIBLE, and a literal 1 on one of those means "every depth slice" today but
|
|
||||||
// "depth slice 0" once VK_KHR_maintenance9 is enabled - i.e. it would silently become a
|
|
||||||
// single-slice barrier again on a newer driver. The validation layer says so by name.
|
|
||||||
static constexpr Uint32 kAllLayers = VK_REMAINING_ARRAY_LAYERS;
|
|
||||||
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||||
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
||||||
frame.commandBuffer, srcResource->image, srcResource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
frame.commandBuffer, srcResource->image, srcResource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT,
|
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT,
|
||||||
srcResource->aspect, 0, srcResource->mipLevels, kAllLayers);
|
srcResource->aspect, 0, srcResource->mipLevels);
|
||||||
MOBILEGL_ASSERT(srcReady, "%s: failed to transition undefined source image", __func__);
|
MOBILEGL_ASSERT(srcReady, "%s: failed to transition undefined source image", __func__);
|
||||||
srcCopyLayout = srcResource->layout;
|
srcCopyLayout = srcResource->layout;
|
||||||
} else {
|
} else {
|
||||||
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
||||||
frame.commandBuffer, srcResource->image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
frame.commandBuffer, srcResource->image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1, kAllLayers);
|
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1);
|
||||||
MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__);
|
MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8791,14 +8926,14 @@ void main() {
|
|||||||
frame.commandBuffer, dstResource->image, dstResource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
frame.commandBuffer, dstResource->image, dstResource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||||
dstResource->aspect, 0, dstResource->mipLevels, kAllLayers);
|
dstResource->aspect, 0, dstResource->mipLevels);
|
||||||
MOBILEGL_ASSERT(dstReady, "%s: failed to transition undefined destination image", __func__);
|
MOBILEGL_ASSERT(dstReady, "%s: failed to transition undefined destination image", __func__);
|
||||||
dstCopyLayout = dstResource->layout;
|
dstCopyLayout = dstResource->layout;
|
||||||
} else {
|
} else {
|
||||||
Bool dstReady = VkTextureManager::TransitionImageLayout(
|
Bool dstReady = VkTextureManager::TransitionImageLayout(
|
||||||
frame.commandBuffer, dstResource->image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
frame.commandBuffer, dstResource->image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1, kAllLayers);
|
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1);
|
||||||
MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__);
|
MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8840,13 +8975,13 @@ void main() {
|
|||||||
frame.commandBuffer, srcResource->image, srcResource->layout, srcRestoreLayout,
|
frame.commandBuffer, srcResource->image, srcResource->layout, srcRestoreLayout,
|
||||||
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
|
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
|
||||||
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask,
|
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask,
|
||||||
srcResource->aspect, 0, srcResource->mipLevels, kAllLayers);
|
srcResource->aspect, 0, srcResource->mipLevels);
|
||||||
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore undefined source image layout", __func__);
|
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore undefined source image layout", __func__);
|
||||||
} else {
|
} else {
|
||||||
Bool srcRestored = VkTextureManager::TransitionImageLayout(
|
Bool srcRestored = VkTextureManager::TransitionImageLayout(
|
||||||
frame.commandBuffer, srcResource->image, srcCopyLayout, srcRestoreLayout,
|
frame.commandBuffer, srcResource->image, srcCopyLayout, srcRestoreLayout,
|
||||||
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
|
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
|
||||||
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1, kAllLayers);
|
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1);
|
||||||
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__);
|
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8858,13 +8993,13 @@ void main() {
|
|||||||
frame.commandBuffer, dstResource->image, dstResource->layout, dstRestoreLayout,
|
frame.commandBuffer, dstResource->image, dstResource->layout, dstRestoreLayout,
|
||||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
|
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask,
|
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask,
|
||||||
dstResource->aspect, 0, dstResource->mipLevels, kAllLayers);
|
dstResource->aspect, 0, dstResource->mipLevels);
|
||||||
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore undefined destination image layout", __func__);
|
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore undefined destination image layout", __func__);
|
||||||
} else {
|
} else {
|
||||||
Bool dstRestored = VkTextureManager::TransitionImageLayout(
|
Bool dstRestored = VkTextureManager::TransitionImageLayout(
|
||||||
frame.commandBuffer, dstResource->image, dstCopyLayout, dstRestoreLayout,
|
frame.commandBuffer, dstResource->image, dstCopyLayout, dstRestoreLayout,
|
||||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
|
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1, kAllLayers);
|
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1);
|
||||||
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
|
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9023,6 +9158,9 @@ void main() {
|
|||||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
VkAccessFlags srcAccessMask = 0;
|
VkAccessFlags srcAccessMask = 0;
|
||||||
GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask);
|
GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask);
|
||||||
|
// The copy below reads `srcBinding.baseArrayLayer`, which for a glFramebufferTextureLayer
|
||||||
|
// attachment is any layer of the array - the barrier covers all of them (see
|
||||||
|
// VkTextureManager::TransitionImageLayout), so the layer being read is one it moved.
|
||||||
if (readIsDefaultFbo) {
|
if (readIsDefaultFbo) {
|
||||||
VkImageLayout trackedLayout = srcOriginalLayout;
|
VkImageLayout trackedLayout = srcOriginalLayout;
|
||||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||||
@@ -9048,19 +9186,18 @@ void main() {
|
|||||||
// The GL rect, aimed at the default framebuffer's stored orientation. Using the GL y
|
// The GL rect, aimed at the default framebuffer's stored orientation. Using the GL y
|
||||||
// verbatim copied rows [y, y+h) counted from the TOP of the image, i.e. the wrong band for
|
// verbatim copied rows [y, y+h) counted from the TOP of the image, i.e. the wrong band for
|
||||||
// every read that was not full-height.
|
// every read that was not full-height.
|
||||||
Int32 copyOffsetX = x;
|
VkOffset2D copyOffset{x, y};
|
||||||
Int32 copyOffsetY = y;
|
VkExtent2D copyExtent{static_cast<Uint32>(width), static_cast<Uint32>(height)};
|
||||||
if (readIsDefaultFbo) {
|
if (readIsDefaultFbo) {
|
||||||
const VkExtent2D defaultFboExtent = m_swapchainObject.GetExtent();
|
const VkExtent2D defaultFboExtent = m_swapchainObject.GetExtent();
|
||||||
const DefaultFramebufferRectMapping mapping =
|
const Bool mapped = MapDefaultFramebufferReadbackRect(
|
||||||
GetDefaultFramebufferRectMapping(m_swapchainObject.GetPreTransform());
|
x, y, width, height, defaultFboExtent, m_swapchainObject.GetPreTransform(), ©Offset,
|
||||||
copyOffsetX = MapDefaultFramebufferRectAxis(x, width, static_cast<Int>(defaultFboExtent.width),
|
©Extent);
|
||||||
mapping.mirrorX);
|
MOBILEGL_ASSERT(mapped, "ReadPixels: default framebuffer read rectangle is out of bounds");
|
||||||
copyOffsetY = MapDefaultFramebufferRectAxis(y, height, static_cast<Int>(defaultFboExtent.height),
|
if (!mapped) return;
|
||||||
mapping.flipY);
|
|
||||||
}
|
}
|
||||||
copyRegion.imageOffset = {copyOffsetX, copyOffsetY, static_cast<Int32>(srcBinding.depthOffset)};
|
copyRegion.imageOffset = {copyOffset.x, copyOffset.y, static_cast<Int32>(srcBinding.depthOffset)};
|
||||||
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
copyRegion.imageExtent = {copyExtent.width, copyExtent.height, 1};
|
||||||
vkCmdCopyImageToBuffer(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
vkCmdCopyImageToBuffer(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
readback.GetHandle(), 1, ©Region);
|
readback.GetHandle(), 1, ©Region);
|
||||||
|
|
||||||
@@ -9102,16 +9239,14 @@ void main() {
|
|||||||
// already aimed with the same mapping. The gate is exactly what made every partial
|
// already aimed with the same mapping. The gate is exactly what made every partial
|
||||||
// read of the default framebuffer come back in Vulkan row order.
|
// read of the default framebuffer come back in Vulkan row order.
|
||||||
Vector<Uint8> remapped(static_cast<SizeT>(width) * static_cast<SizeT>(height) * sourceTexelSize);
|
Vector<Uint8> remapped(static_cast<SizeT>(width) * static_cast<SizeT>(height) * sourceTexelSize);
|
||||||
if (RemapDefaultFboReadbackToGLOrientation(mapped, static_cast<Uint32>(width),
|
if (RemapDefaultFramebufferReadback(mapped, static_cast<Uint32>(width),
|
||||||
static_cast<Uint32>(height), preTransform, sourceTexelSize,
|
static_cast<Uint32>(height), preTransform, sourceTexelSize,
|
||||||
remapped.data())) {
|
remapped.data())) {
|
||||||
PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, 1, format, type, pixels,
|
PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, 1, format, type, pixels,
|
||||||
/*applyPackImageParams=*/false, /*applyReadColorClamp=*/true);
|
/*applyPackImageParams=*/false, /*applyReadColorClamp=*/true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Only a quarter-turn pre-transform reaches this, and nothing in this renderer models
|
MGLOG_D("DirectVulkan::ReadPixels: default-FBO remap failed (w=%d h=%d preTransform=%d); falling back "
|
||||||
// one. MGLOG_I because the INFO builds are the ones that run conformance.
|
|
||||||
MGLOG_D("DirectVulkan::ReadPixels: default-FBO remap declined (w=%d h=%d preTransform=%d); falling back "
|
|
||||||
"to raw readback",
|
"to raw readback",
|
||||||
width, height, static_cast<Int>(preTransform));
|
width, height, static_cast<Int>(preTransform));
|
||||||
}
|
}
|
||||||
@@ -9492,16 +9627,15 @@ void main() {
|
|||||||
// The swapchain's depth/stencil image is stored display-side-up like its colour twin, so
|
// The swapchain's depth/stencil image is stored display-side-up like its colour twin, so
|
||||||
// the GL rect has to be mapped into that space before the copy and the copied rows
|
// the GL rect has to be mapped into that space before the copy and the copied rows
|
||||||
// re-oriented afterwards - the same two halves the colour ReadPixels path applies.
|
// re-oriented afterwards - the same two halves the colour ReadPixels path applies.
|
||||||
Int32 copyOffsetX = x;
|
VkOffset2D copyOffset{x, y};
|
||||||
Int32 copyOffsetY = y;
|
VkExtent2D copyExtent{static_cast<Uint32>(width), static_cast<Uint32>(height)};
|
||||||
if (defaultFramebufferOrientation) {
|
if (defaultFramebufferOrientation) {
|
||||||
const VkExtent2D defaultFboExtent = m_swapchainObject.GetExtent();
|
const VkExtent2D defaultFboExtent = m_swapchainObject.GetExtent();
|
||||||
const DefaultFramebufferRectMapping mapping =
|
const Bool mapped = MapDefaultFramebufferReadbackRect(
|
||||||
GetDefaultFramebufferRectMapping(m_swapchainObject.GetPreTransform());
|
x, y, width, height, defaultFboExtent, m_swapchainObject.GetPreTransform(), ©Offset,
|
||||||
copyOffsetX = MapDefaultFramebufferRectAxis(x, width, static_cast<Int>(defaultFboExtent.width),
|
©Extent);
|
||||||
mapping.mirrorX);
|
MOBILEGL_ASSERT(mapped, "ReadDepthStencilPixels: default framebuffer read rectangle is out of bounds");
|
||||||
copyOffsetY = MapDefaultFramebufferRectAxis(y, height, static_cast<Int>(defaultFboExtent.height),
|
if (!mapped) return;
|
||||||
mapping.flipY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VkBufferImageCopy regions[2]{};
|
VkBufferImageCopy regions[2]{};
|
||||||
@@ -9513,8 +9647,8 @@ void main() {
|
|||||||
region.imageSubresource.mipLevel = mipLevel;
|
region.imageSubresource.mipLevel = mipLevel;
|
||||||
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
||||||
region.imageSubresource.layerCount = 1;
|
region.imageSubresource.layerCount = 1;
|
||||||
region.imageOffset = {copyOffsetX, copyOffsetY, 0};
|
region.imageOffset = {copyOffset.x, copyOffset.y, 0};
|
||||||
region.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
region.imageExtent = {copyExtent.width, copyExtent.height, 1};
|
||||||
}
|
}
|
||||||
if (wantStencil) {
|
if (wantStencil) {
|
||||||
auto& region = regions[regionCount++];
|
auto& region = regions[regionCount++];
|
||||||
@@ -9523,8 +9657,8 @@ void main() {
|
|||||||
region.imageSubresource.mipLevel = mipLevel;
|
region.imageSubresource.mipLevel = mipLevel;
|
||||||
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
||||||
region.imageSubresource.layerCount = 1;
|
region.imageSubresource.layerCount = 1;
|
||||||
region.imageOffset = {copyOffsetX, copyOffsetY, 0};
|
region.imageOffset = {copyOffset.x, copyOffset.y, 0};
|
||||||
region.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
region.imageExtent = {copyExtent.width, copyExtent.height, 1};
|
||||||
}
|
}
|
||||||
vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(),
|
vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(),
|
||||||
regionCount, regions);
|
regionCount, regions);
|
||||||
@@ -9558,23 +9692,21 @@ void main() {
|
|||||||
Bool remapped = true;
|
Bool remapped = true;
|
||||||
if (wantDepth && depthCopyBytes > 0) {
|
if (wantDepth && depthCopyBytes > 0) {
|
||||||
remappedDepth.resize(pixelCount * depthCopyBytes);
|
remappedDepth.resize(pixelCount * depthCopyBytes);
|
||||||
remapped = RemapDefaultFboReadbackToGLOrientation(depthSrc, static_cast<Uint32>(width),
|
remapped = RemapDefaultFramebufferReadback(depthSrc, static_cast<Uint32>(width),
|
||||||
static_cast<Uint32>(height), preTransform,
|
static_cast<Uint32>(height), preTransform,
|
||||||
depthCopyBytes, remappedDepth.data());
|
depthCopyBytes, remappedDepth.data());
|
||||||
}
|
}
|
||||||
if (remapped && wantStencil) {
|
if (remapped && wantStencil) {
|
||||||
remappedStencil.resize(pixelCount);
|
remappedStencil.resize(pixelCount);
|
||||||
remapped = RemapDefaultFboReadbackToGLOrientation(stencilSrc, static_cast<Uint32>(width),
|
remapped = RemapDefaultFramebufferReadback(stencilSrc, static_cast<Uint32>(width),
|
||||||
static_cast<Uint32>(height), preTransform, 1,
|
static_cast<Uint32>(height), preTransform, 1,
|
||||||
remappedStencil.data());
|
remappedStencil.data());
|
||||||
}
|
}
|
||||||
if (remapped) {
|
if (remapped) {
|
||||||
if (!remappedDepth.empty()) depthSrc = remappedDepth.data();
|
if (!remappedDepth.empty()) depthSrc = remappedDepth.data();
|
||||||
if (!remappedStencil.empty()) stencilSrc = remappedStencil.data();
|
if (!remappedStencil.empty()) stencilSrc = remappedStencil.data();
|
||||||
} else {
|
} else {
|
||||||
// Only a quarter-turn pre-transform reaches this, and nothing in this renderer
|
MGLOG_D("DirectVulkan::ReadDepthStencilPixels: default-FBO remap failed (w=%d h=%d "
|
||||||
// models one. MGLOG_I because the INFO builds are the ones that run conformance.
|
|
||||||
MGLOG_D("DirectVulkan::ReadDepthStencilPixels: default-FBO remap declined (w=%d h=%d "
|
|
||||||
"preTransform=%d); falling back to raw readback",
|
"preTransform=%d); falling back to raw readback",
|
||||||
width, height, static_cast<Int>(preTransform));
|
width, height, static_cast<Int>(preTransform));
|
||||||
}
|
}
|
||||||
@@ -9825,14 +9957,13 @@ void main() {
|
|||||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
VkAccessFlags srcAccessMask = 0;
|
VkAccessFlags srcAccessMask = 0;
|
||||||
GetImageTransitionSourceState(originalLayout, srcStageMask, srcAccessMask);
|
GetImageTransitionSourceState(originalLayout, srcStageMask, srcAccessMask);
|
||||||
// The copy below reads EVERY layer of the level, so the barrier has to name every layer
|
// The copy below reads EVERY layer of the level, which is exactly the range
|
||||||
// too; a layerCount of 1 left an array texture's layers 1.. in whatever layout they were
|
// TransitionImageLayout barriers cover.
|
||||||
// last left in while the transfer read them.
|
|
||||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||||
frame.commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
frame.commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, resource->aspect,
|
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, resource->aspect,
|
||||||
static_cast<Uint32>(level), 1, VK_REMAINING_ARRAY_LAYERS);
|
static_cast<Uint32>(level), 1);
|
||||||
MOBILEGL_ASSERT(ok, "%s: failed to transition texture image", __func__);
|
MOBILEGL_ASSERT(ok, "%s: failed to transition texture image", __func__);
|
||||||
|
|
||||||
VkBufferImageCopy copyRegion{};
|
VkBufferImageCopy copyRegion{};
|
||||||
@@ -9852,7 +9983,7 @@ void main() {
|
|||||||
frame.commandBuffer, resource->image, resource->layout, originalLayout,
|
frame.commandBuffer, resource->image, resource->layout, originalLayout,
|
||||||
VK_PIPELINE_STAGE_TRANSFER_BIT, restoreStageMask,
|
VK_PIPELINE_STAGE_TRANSFER_BIT, restoreStageMask,
|
||||||
VK_ACCESS_TRANSFER_READ_BIT, restoreAccessMask, resource->aspect,
|
VK_ACCESS_TRANSFER_READ_BIT, restoreAccessMask, resource->aspect,
|
||||||
static_cast<Uint32>(level), 1, VK_REMAINING_ARRAY_LAYERS);
|
static_cast<Uint32>(level), 1);
|
||||||
MOBILEGL_ASSERT(ok, "%s: failed to restore texture image layout", __func__);
|
MOBILEGL_ASSERT(ok, "%s: failed to restore texture image layout", __func__);
|
||||||
|
|
||||||
if (!SubmitReadbackCommandsAndWait(frame)) {
|
if (!SubmitReadbackCommandsAndWait(frame)) {
|
||||||
@@ -9960,7 +10091,7 @@ void main() {
|
|||||||
Bool transitioned = VkTextureManager::TransitionImageLayout(
|
Bool transitioned = VkTextureManager::TransitionImageLayout(
|
||||||
frame.commandBuffer, resource->image, resource->layout, finalLayout,
|
frame.commandBuffer, resource->image, resource->layout, finalLayout,
|
||||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
||||||
0, VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
|
0, VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels);
|
||||||
MOBILEGL_ASSERT(transitioned, "GenerateMipmap: failed to transition uninitialized mip chain");
|
MOBILEGL_ASSERT(transitioned, "GenerateMipmap: failed to transition uninitialized mip chain");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -12218,6 +12349,28 @@ void main() {
|
|||||||
m_fillModeNonSolidFeatureEnabled = deviceFeatures.fillModeNonSolid == VK_TRUE;
|
m_fillModeNonSolidFeatureEnabled = deviceFeatures.fillModeNonSolid == VK_TRUE;
|
||||||
deviceFeatures.dualSrcBlend = supportedDeviceFeatures.dualSrcBlend;
|
deviceFeatures.dualSrcBlend = supportedDeviceFeatures.dualSrcBlend;
|
||||||
m_dualSrcBlendFeatureEnabled = deviceFeatures.dualSrcBlend == VK_TRUE;
|
m_dualSrcBlendFeatureEnabled = deviceFeatures.dualSrcBlend == VK_TRUE;
|
||||||
|
// ARB_viewport_array rasterization. Without multiViewport a pipeline may declare exactly
|
||||||
|
// one viewport (VUID-VkPipelineViewportStateCreateInfo-viewportCount-01216), so a shader's
|
||||||
|
// gl_ViewportIndex can only ever select viewport 0 and the other fifteen rectangles are
|
||||||
|
// state with nowhere to go. The GL state stays 16 wide either way - GL 4.3 core requires
|
||||||
|
// MAX_VIEWPORTS >= 16 and that is a frontend promise, not a device one; this gate decides
|
||||||
|
// only whether a DRAW can rasterize into more than one of them.
|
||||||
|
deviceFeatures.multiViewport = supportedDeviceFeatures.multiViewport;
|
||||||
|
m_multiViewportFeatureEnabled = deviceFeatures.multiViewport == VK_TRUE;
|
||||||
|
m_maxRasterizableViewports =
|
||||||
|
m_multiViewportFeatureEnabled
|
||||||
|
? std::min<Uint32>(RenderStateParameters::MAX_VIEWPORTS,
|
||||||
|
std::max<Uint32>(m_physicalDevice.properties.limits.maxViewports, 1u))
|
||||||
|
: 1u;
|
||||||
|
MGLOG_I("Vulkan: multiViewport %s; rasterizable viewports=%u (device limit %u, GL state width %u)",
|
||||||
|
m_multiViewportFeatureEnabled ? "enabled" : "UNAVAILABLE", m_maxRasterizableViewports,
|
||||||
|
m_physicalDevice.properties.limits.maxViewports,
|
||||||
|
static_cast<Uint32>(RenderStateParameters::MAX_VIEWPORTS));
|
||||||
|
if (!m_multiViewportFeatureEnabled) {
|
||||||
|
MGLOG_W("Vulkan: the device does not support the multiViewport feature; gl_ViewportIndex will always "
|
||||||
|
"select viewport 0 and per-viewport scissor/depth-range state past index 0 cannot be "
|
||||||
|
"rasterized (the state itself is still stored and queryable)");
|
||||||
|
}
|
||||||
deviceFeatures.logicOp = supportedDeviceFeatures.logicOp;
|
deviceFeatures.logicOp = supportedDeviceFeatures.logicOp;
|
||||||
deviceFeatures.shaderClipDistance = supportedDeviceFeatures.shaderClipDistance;
|
deviceFeatures.shaderClipDistance = supportedDeviceFeatures.shaderClipDistance;
|
||||||
deviceFeatures.shaderCullDistance = supportedDeviceFeatures.shaderCullDistance;
|
deviceFeatures.shaderCullDistance = supportedDeviceFeatures.shaderCullDistance;
|
||||||
|
|||||||
@@ -229,6 +229,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
|
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
|
||||||
VkImageLayout dstRestoreLayout, Bool stencilAspect);
|
VkImageLayout dstRestoreLayout, Bool stencilAspect);
|
||||||
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
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,
|
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
||||||
GLsizei width, GLsizei height, GLenum destinationFormat,
|
GLsizei width, GLsizei height, GLenum destinationFormat,
|
||||||
GLenum destinationType, SizeT destinationRowStride,
|
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
|
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
|
||||||
// honored rather than accepted-and-ignored.
|
// honored rather than accepted-and-ignored.
|
||||||
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
|
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
|
// Ensures the frame command buffer is recording (same lazy pattern as
|
||||||
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
|
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
|
||||||
// frame's pool. Null when unsupported or the pool is exhausted.
|
// 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.
|
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
||||||
Bool m_dualSrcBlendFeatureEnabled = false;
|
Bool m_dualSrcBlendFeatureEnabled = false;
|
||||||
Bool m_primitiveTopologyListRestartFeatureEnabled = 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
|
// 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
|
// because geometry/tessellation stage bits are invalid in a barrier when their
|
||||||
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
|
// 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
|
// re-resolve just the pipeline against the active pass; a change that
|
||||||
// flips it must fall back to the full path's pass selection.
|
// flips it must fall back to the full path's pass selection.
|
||||||
Bool drawUsesDepthStencil = false;
|
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};
|
IntVec2 renderPassExtent = {0, 0};
|
||||||
// colorAttachmentCount of the snapshotting draw's render pass: the
|
// colorAttachmentCount of the snapshotting draw's render pass: the
|
||||||
// pipeline-state hash input, so the fast path can refresh that hash and
|
// 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
|
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
|
||||||
// bias, line width, stencil), gated behind one render-state-parameters-version
|
// bias, line width, stencil), gated behind one render-state-parameters-version
|
||||||
// compare per command buffer - see the gate fields in DynamicStateShadow.
|
// 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,
|
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
|
|||||||
@@ -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, 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, 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, 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_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_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_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_STUB_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedfv, index, v)
|
DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_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_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_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_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_STUB_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_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_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_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, 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, 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, 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)
|
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)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.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/FramebufferEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h>
|
#include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||||
@@ -27,6 +28,11 @@
|
|||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
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 {
|
namespace {
|
||||||
enum class IndexedBufferQueryKind {
|
enum class IndexedBufferQueryKind {
|
||||||
Binding,
|
Binding,
|
||||||
@@ -339,26 +345,70 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
return sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The ARB_viewport_array indexed rectangles. MobileGL keeps exactly one viewport, one
|
// The ARB_viewport_array indexed rectangles. Each of these is genuinely per-viewport
|
||||||
// scissor box and one depth range, so every in-range index answers with that single
|
// frontend state (RenderStateParameters::Viewports / ScissorBoxes / DepthRanges), so the
|
||||||
// value - but it has to come from the frontend state the non-indexed getters read.
|
// indexed getters must read the indexed storage - the generic path at the bottom of
|
||||||
// The generic path at the bottom of GetIntegeri_v is a raw backend passthrough that
|
// GetIntegeri_v is a raw backend passthrough that has no case for them and returned
|
||||||
// has no case for these, so routing them through it returned zeros.
|
// 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) {
|
Bool IsIndexedViewportQuery(GLenum target) {
|
||||||
return target == GL_VIEWPORT || target == GL_SCISSOR_BOX || target == GL_DEPTH_RANGE;
|
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) {
|
Bool ValidateViewportQueryIndex(GLuint index, const char* caller) {
|
||||||
GLint maxViewports = 0;
|
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
|
||||||
GetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
|
||||||
if (index < static_cast<GLuint>(std::max(maxViewports, 1))) return true;
|
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Viewport index is out of range."));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Viewport index is out of range."));
|
||||||
return false;
|
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) {
|
void CopyIntsToBooleans(const GLint* src, SizeT count, GLboolean* dst) {
|
||||||
for (SizeT i = 0; i < count; ++i) {
|
for (SizeT i = 0; i < count; ++i) {
|
||||||
dst[i] = src[i] ? GL_TRUE : GL_FALSE;
|
dst[i] = src[i] ? GL_TRUE : GL_FALSE;
|
||||||
@@ -629,6 +679,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
params[1] = dynamicParameters.ViewportBoundsRangeMax;
|
params[1] = dynamicParameters.ViewportBoundsRangeMax;
|
||||||
return;
|
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_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||||
@@ -792,15 +853,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
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) {
|
switch (target) {
|
||||||
// ARB_viewport_array queries the indexed rectangles through glGetIntegeri_v as well
|
// ARB_viewport_array queries the indexed rectangles through glGetIntegeri_v as well
|
||||||
// (gl4cMultiBindTests and the viewport_array group both do). The frontend keeps one
|
// (gl4cMultiBindTests and the viewport_array group both do).
|
||||||
// viewport and one scissor box, so every in-range index reports that one.
|
|
||||||
case GL_VIEWPORT:
|
case GL_VIEWPORT:
|
||||||
case GL_SCISSOR_BOX:
|
case GL_SCISSOR_BOX:
|
||||||
|
case GL_DEPTH_RANGE: {
|
||||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
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;
|
return;
|
||||||
|
}
|
||||||
// The vertex buffer binding points of the vertex array object that is bound. Indexed by
|
// 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).
|
// binding point, not by attribute (GL 4.6 core 10.3.1).
|
||||||
case GL_VERTEX_BINDING_BUFFER:
|
case GL_VERTEX_BINDING_BUFFER:
|
||||||
@@ -927,7 +1005,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
if (IsIndexedViewportQuery(target)) {
|
if (IsIndexedViewportQuery(target)) {
|
||||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
GLint ints[4] = {};
|
GLint ints[4] = {};
|
||||||
@@ -944,7 +1025,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
if (IsIndexedViewportQuery(target)) {
|
if (IsIndexedViewportQuery(target)) {
|
||||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
GLint ints[4] = {};
|
GLint ints[4] = {};
|
||||||
@@ -1020,7 +1106,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// frontend-only value simply is not in the driver's table.
|
// frontend-only value simply is not in the driver's table.
|
||||||
GLint values[4] = {};
|
GLint values[4] = {};
|
||||||
GetIntegeri_v(target, index, values);
|
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) {
|
void GetInteger64v(GLenum pname, GLint64* params) {
|
||||||
@@ -2192,7 +2283,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
params[1] = dynamicParameters.MaxViewportHeight;
|
params[1] = dynamicParameters.MaxViewportHeight;
|
||||||
break;
|
break;
|
||||||
case GL_MAX_VIEWPORTS:
|
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;
|
break;
|
||||||
case GL_MINOR_VERSION:
|
case GL_MINOR_VERSION:
|
||||||
*params = rendererInfo.RendererGLInfo.TargetGLVersion.Minor;
|
*params = rendererInfo.RendererGLInfo.TargetGLVersion.Minor;
|
||||||
|
|||||||
@@ -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) {
|
GLboolean IsQuery(GLuint id) {
|
||||||
if (id == 0) {
|
if (id == 0) {
|
||||||
return GL_FALSE;
|
return GL_FALSE;
|
||||||
|
|||||||
@@ -13,6 +13,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void GenQueries(GLsizei n, GLuint* ids);
|
void GenQueries(GLsizei n, GLuint* ids);
|
||||||
void CreateQueries(GLenum target, GLsizei n, GLuint* ids);
|
void CreateQueries(GLenum target, GLsizei n, GLuint* ids);
|
||||||
void DeleteQueries(GLsizei n, const 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);
|
GLboolean IsQuery(GLuint id);
|
||||||
void BeginQuery(GLenum target, GLuint id);
|
void BeginQuery(GLenum target, GLuint id);
|
||||||
void EndQuery(GLenum target);
|
void EndQuery(GLenum target);
|
||||||
|
|||||||
@@ -20,28 +20,118 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return std::clamp(static_cast<Float>(value), 0.0f, 1.0f);
|
return std::clamp(static_cast<Float>(value), 0.0f, 1.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Bool ValidateIndexedBlendCapability(GLenum target, GLuint index, const char* functionName) {
|
// GL 4.6 core 17.3.2 and 22.1 give exactly two indexed capabilities: GL_BLEND, indexed by
|
||||||
if (target != GL_BLEND) {
|
// 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(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
|
if (index >= limit) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
"MG_Impl/GLImpl", functionName,
|
String(indexName) + " index " + std::to_string(index) +
|
||||||
"Buffer index " + std::to_string(index) + " is out of range. Max supported is " +
|
" is out of range. Max supported is " + std::to_string(limit - 1) +
|
||||||
std::to_string(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS - 1) + "."));
|
"."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
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,
|
static Bool TryConvertBlendEquation(GLenum mode, const char* functionName,
|
||||||
::MobileGL::BlendEquation& outEquation) {
|
::MobileGL::BlendEquation& outEquation) {
|
||||||
outEquation = MG_Util::ConvertGLEnumToBlendEquation(mode);
|
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) {
|
void Viewport_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||||
if (width < 0 || height < 0) {
|
if (!ValidateNonNegativeExtent(width, height, "Viewport_State")) return;
|
||||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Viewport_State",
|
|
||||||
"Width abd height must be non-negative."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MG_State::pGLContext->SetViewport(IntVec4(x, y, width, height));
|
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) {
|
void StencilOpSeparate_State(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
||||||
Bool applyFront = false;
|
Bool applyFront = false;
|
||||||
Bool applyBack = false;
|
Bool applyBack = false;
|
||||||
@@ -175,12 +319,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Scissor_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
void Scissor_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||||
if (width < 0 || height < 0) {
|
if (!ValidateNonNegativeExtent(width, height, "Scissor_State")) return;
|
||||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Scissor_State",
|
|
||||||
"Width abd height must be non-negative."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MG_State::pGLContext->SetScissorBox(IntVec4(x, y, width, height));
|
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) {
|
GLboolean IsEnabledi_State(GLenum target, GLuint index) {
|
||||||
if (!ValidateIndexedBlendCapability(target, index, "IsEnabledi_State")) {
|
if (!ValidateIndexedCapability(target, index, "IsEnabledi_State")) {
|
||||||
return GL_FALSE;
|
return GL_FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,7 +531,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
GLint values[4] = {};
|
GLint values[4] = {};
|
||||||
GetIntegeri_v(target, index, values);
|
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) {
|
GLboolean IsEnabled_State(GLenum cap) {
|
||||||
@@ -725,7 +871,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Disablei_State(GLenum target, GLuint index) {
|
void Disablei_State(GLenum target, GLuint index) {
|
||||||
if (!ValidateIndexedBlendCapability(target, index, "Disablei_State")) {
|
if (!ValidateIndexedCapability(target, index, "Disablei_State")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -743,7 +889,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Enablei_State(GLenum target, GLuint index) {
|
void Enablei_State(GLenum target, GLuint index) {
|
||||||
if (!ValidateIndexedBlendCapability(target, index, "Enablei_State")) {
|
if (!ValidateIndexedCapability(target, index, "Enablei_State")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,6 +943,44 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
Viewport_State(x, y, width, height);
|
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) {
|
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
||||||
StencilOpSeparate_State(face, sfail, dpfail, dppass);
|
StencilOpSeparate_State(face, sfail, dpfail, dppass);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void Enablei(GLenum target, GLuint index);
|
void Enablei(GLenum target, GLuint index);
|
||||||
void BlendFunc(GLenum sfactor, GLenum dfactor);
|
void BlendFunc(GLenum sfactor, GLenum dfactor);
|
||||||
void Viewport(GLint x, GLint y, GLsizei width, GLsizei height);
|
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 StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);
|
||||||
void StencilOp(GLenum fail, GLenum zfail, GLenum zpass);
|
void StencilOp(GLenum fail, GLenum zfail, GLenum zpass);
|
||||||
void StencilMaskSeparate(GLenum face, GLuint mask);
|
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
|
// Frontend sync object: wraps an optional backend fence handle. A null
|
||||||
// backend handle (backend has no fence support, or could not create a
|
// backend handle (backend has no fence support, or could not create a
|
||||||
// fence at call time) keeps the legacy always-signaled behavior.
|
// 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 {
|
struct SyncObject {
|
||||||
|
std::mutex mutex;
|
||||||
MG_Backend::BackendSyncHandle backendHandle = nullptr;
|
MG_Backend::BackendSyncHandle backendHandle = nullptr;
|
||||||
GLenum condition = GL_SYNC_GPU_COMMANDS_COMPLETE;
|
GLenum condition = GL_SYNC_GPU_COMMANDS_COMPLETE;
|
||||||
GLbitfield flags = 0;
|
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
|
// 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
|
// Entries left at process shutdown are simply dropped; their backend
|
||||||
// handles die with the backend.
|
// handles die with the backend.
|
||||||
std::mutex g_syncObjectsMutex;
|
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 std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||||
const auto it = g_liveSyncObjects.find(sync);
|
const auto it = g_liveSyncObjects.find(sync);
|
||||||
return it != g_liveSyncObjects.end() ? it->second : nullptr;
|
return it != g_liveSyncObjects.end() ? it->second : nullptr;
|
||||||
@@ -35,13 +54,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
GLsync FenceSync(GLenum condition, GLbitfield flags) {
|
GLsync FenceSync(GLenum condition, GLbitfield flags) {
|
||||||
auto* syncObject = new SyncObject;
|
auto syncObject = MakeShared<SyncObject>();
|
||||||
syncObject->condition = condition;
|
syncObject->condition = condition;
|
||||||
syncObject->flags = flags;
|
syncObject->flags = flags;
|
||||||
if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) {
|
if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) {
|
||||||
syncObject->backendHandle = backendFenceSync();
|
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);
|
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||||
g_liveSyncObjects[handle] = syncObject;
|
g_liveSyncObjects[handle] = syncObject;
|
||||||
return handle;
|
return handle;
|
||||||
@@ -52,24 +71,31 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
|
GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
|
||||||
const auto* syncObject = FindSyncObject(sync);
|
const SharedPtr<SyncObject> syncObject = FindSyncObject(sync);
|
||||||
if (!syncObject) {
|
if (!syncObject) {
|
||||||
return GL_WAIT_FAILED;
|
return GL_WAIT_FAILED;
|
||||||
}
|
}
|
||||||
const auto backendClientWaitSync = MG_Backend::gBackendFunctionsTable.GL.ClientWaitSync;
|
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 GL_ALREADY_SIGNALED; // legacy always-signaled fallback
|
||||||
}
|
}
|
||||||
return backendClientWaitSync(syncObject->backendHandle, flags, timeout);
|
return backendClientWaitSync(syncObject->backendHandle, flags, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
|
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
|
||||||
const auto* syncObject = FindSyncObject(sync);
|
const SharedPtr<SyncObject> syncObject = FindSyncObject(sync);
|
||||||
if (!syncObject) {
|
if (!syncObject) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync;
|
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);
|
backendWaitSync(syncObject->backendHandle, flags, timeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +104,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (sync == nullptr) {
|
if (sync == nullptr) {
|
||||||
return; // glDeleteSync(0) is silently ignored
|
return; // glDeleteSync(0) is silently ignored
|
||||||
}
|
}
|
||||||
SyncObject* syncObject = nullptr;
|
SharedPtr<SyncObject> syncObject;
|
||||||
{
|
{
|
||||||
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||||
const auto it = g_liveSyncObjects.find(sync);
|
const auto it = g_liveSyncObjects.find(sync);
|
||||||
@@ -88,15 +114,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
syncObject = it->second;
|
syncObject = it->second;
|
||||||
g_liveSyncObjects.erase(it);
|
g_liveSyncObjects.erase(it);
|
||||||
}
|
}
|
||||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
// Release the backend handle under the object lock. The local SharedPtr
|
||||||
if (backendDeleteSync && syncObject->backendHandle) {
|
// (and any reader's SharedPtr) keeps the wrapper itself alive until every
|
||||||
backendDeleteSync(syncObject->backendHandle);
|
// in-flight backend call has returned.
|
||||||
}
|
syncObject->ReleaseBackendHandle();
|
||||||
delete syncObject;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values) {
|
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 (!syncObject) {
|
||||||
if (length) {
|
if (length) {
|
||||||
*length = 0;
|
*length = 0;
|
||||||
@@ -111,7 +136,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
case GL_SYNC_STATUS: {
|
case GL_SYNC_STATUS: {
|
||||||
const auto backendGetSyncStatus = MG_Backend::gBackendFunctionsTable.GL.GetSyncStatus;
|
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);
|
backendGetSyncStatus(syncObject->backendHandle);
|
||||||
value = signaled ? GL_SIGNALED : GL_UNSIGNALED;
|
value = signaled ? GL_SIGNALED : GL_UNSIGNALED;
|
||||||
break;
|
break;
|
||||||
@@ -137,11 +163,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void DestroyAllSyncObjects() {
|
void DestroyAllSyncObjects() {
|
||||||
// Detach the registry under the lock, release outside it. Entries the app
|
// Detach the registry under the lock, release outside it. Entries the app
|
||||||
// already deleted were erased by DeleteSync, so nothing here double-frees;
|
// already deleted were erased by DeleteSync, so nothing here double-frees;
|
||||||
// a DeleteSync racing this sweep finds an empty registry and returns. A
|
// a DeleteSync racing this sweep finds an empty registry and returns.
|
||||||
// thread still blocked inside ClientWaitSync/GetSynciv during teardown
|
// Readers racing this sweep keep their SharedPtr copy alive, and each
|
||||||
// holds a raw SyncObject* these deletes invalidate - the same undefined
|
// object's own lock makes the backend-handle release wait for them.
|
||||||
// race an app-driven DeleteSync already has.
|
UnorderedMap<GLsync, SharedPtr<SyncObject>> orphans;
|
||||||
UnorderedMap<GLsync, SyncObject*> orphans;
|
|
||||||
{
|
{
|
||||||
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||||
orphans.swap(g_liveSyncObjects);
|
orphans.swap(g_liveSyncObjects);
|
||||||
@@ -153,12 +178,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// context/renderer is gone (generation/current-thread guards), so this is
|
// context/renderer is gone (generation/current-thread guards), so this is
|
||||||
// safe after the backend has released its EGL resources - but not after
|
// safe after the backend has released its EGL resources - but not after
|
||||||
// the function table itself is cleared.
|
// the function table itself is cleared.
|
||||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
|
||||||
for (const auto& [_, syncObject] : orphans) {
|
for (const auto& [_, syncObject] : orphans) {
|
||||||
if (backendDeleteSync && syncObject->backendHandle) {
|
if (syncObject) {
|
||||||
backendDeleteSync(syncObject->backendHandle);
|
syncObject->ReleaseBackendHandle();
|
||||||
}
|
}
|
||||||
delete syncObject;
|
|
||||||
}
|
}
|
||||||
MGLOG_D("DestroyAllSyncObjects: reclaimed %zu sync object(s) the app left undeleted", orphans.size());
|
MGLOG_D("DestroyAllSyncObjects: reclaimed %zu sync object(s) the app left undeleted", orphans.size());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
||||||
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
|
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
|
||||||
Scenarios/ClipDistanceScenario.cpp
|
Scenarios/ClipDistanceScenario.cpp
|
||||||
|
Scenarios/ViewportArrayScenario.cpp
|
||||||
Scenarios/SsboArrayLengthScenario.cpp
|
Scenarios/SsboArrayLengthScenario.cpp
|
||||||
Scenarios/DoublePrecisionScenario.cpp
|
Scenarios/DoublePrecisionScenario.cpp
|
||||||
Scenarios/UniformInitializerScenario.cpp
|
Scenarios/UniformInitializerScenario.cpp
|
||||||
@@ -80,6 +81,7 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||||
Scenarios/CopyImageLayeredScenario.cpp
|
Scenarios/CopyImageLayeredScenario.cpp
|
||||||
|
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||||
|
|||||||
@@ -199,5 +199,61 @@ namespace MGITest {
|
|||||||
"derived component limits are computed in";
|
"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
|
||||||
} // namespace MGITest
|
} // 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,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
|
||||||
@@ -712,10 +712,18 @@ namespace MobileGL::MG_State {
|
|||||||
m_renderState.SetViewport(viewport);
|
m_renderState.SetViewport(viewport);
|
||||||
}
|
}
|
||||||
|
|
||||||
const IntVec4& GLContext::GetViewport() const {
|
IntVec4 GLContext::GetViewport() const {
|
||||||
return m_renderState.GetViewport();
|
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) {
|
void GLContext::SetLineWidth(Float width) {
|
||||||
m_renderState.SetLineWidth(width);
|
m_renderState.SetLineWidth(width);
|
||||||
}
|
}
|
||||||
@@ -953,6 +961,14 @@ namespace MobileGL::MG_State {
|
|||||||
return m_renderState.GetDepthRange();
|
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) {
|
void GLContext::SetSampleCoverage(Float value, Bool invert) {
|
||||||
m_renderState.SetSampleCoverage(value, invert);
|
m_renderState.SetSampleCoverage(value, invert);
|
||||||
}
|
}
|
||||||
@@ -1017,6 +1033,14 @@ namespace MobileGL::MG_State {
|
|||||||
return m_renderState.GetScissorBox();
|
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
|
// Framebuffer
|
||||||
void GLContext::GenFramebufferNames(Uint number, Vector<Uint>& framebuffers) {
|
void GLContext::GenFramebufferNames(Uint number, Vector<Uint>& framebuffers) {
|
||||||
m_framebufferState.GenerateNames(number, framebuffers);
|
m_framebufferState.GenerateNames(number, framebuffers);
|
||||||
|
|||||||
@@ -198,8 +198,10 @@ namespace MobileGL {
|
|||||||
// Only the pipeline-relevant subset - see RenderState::m_pipelineStateVersion.
|
// Only the pipeline-relevant subset - see RenderState::m_pipelineStateVersion.
|
||||||
Uint GetPipelineStateVersion() const;
|
Uint GetPipelineStateVersion() const;
|
||||||
const RenderStateParameters& GetRenderStateParameters() const;
|
const RenderStateParameters& GetRenderStateParameters() const;
|
||||||
void SetViewport(IntVec4 viewport); // x, y, width, height
|
void SetViewport(IntVec4 viewport); // x, y, width, height; writes ALL viewports
|
||||||
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);
|
void SetLineWidth(Float width);
|
||||||
Float GetLineWidth() const;
|
Float GetLineWidth() const;
|
||||||
void SetPointSize(Float size);
|
void SetPointSize(Float size);
|
||||||
@@ -260,8 +262,10 @@ namespace MobileGL {
|
|||||||
Uint32 GetClearStencil() const;
|
Uint32 GetClearStencil() const;
|
||||||
void SetBlendColor(FloatVec4 color);
|
void SetBlendColor(FloatVec4 color);
|
||||||
const FloatVec4& GetBlendColor() const;
|
const FloatVec4& GetBlendColor() const;
|
||||||
void SetDepthRange(FloatVec2 range);
|
void SetDepthRange(FloatVec2 range); // writes ALL viewports' depth ranges
|
||||||
const FloatVec2& GetDepthRange() const;
|
const FloatVec2& GetDepthRange() const;
|
||||||
|
void SetDepthRangeIndexed(Uint index, FloatVec2 range);
|
||||||
|
const FloatVec2& GetDepthRangeIndexed(Uint index) const;
|
||||||
void SetSampleCoverage(Float value, Bool invert);
|
void SetSampleCoverage(Float value, Bool invert);
|
||||||
Float GetSampleCoverageValue() const;
|
Float GetSampleCoverageValue() const;
|
||||||
Bool GetSampleCoverageInvert() const;
|
Bool GetSampleCoverageInvert() const;
|
||||||
@@ -276,8 +280,10 @@ namespace MobileGL {
|
|||||||
FrontFaceMode GetFrontFaceMode() const;
|
FrontFaceMode GetFrontFaceMode() const;
|
||||||
void SetProvokingVertexMode(ProvokingVertexMode mode);
|
void SetProvokingVertexMode(ProvokingVertexMode mode);
|
||||||
ProvokingVertexMode GetProvokingVertexMode() const;
|
ProvokingVertexMode GetProvokingVertexMode() const;
|
||||||
void SetScissorBox(IntVec4 box); // x, y, width, height
|
void SetScissorBox(IntVec4 box); // x, y, width, height; writes ALL rectangles
|
||||||
const IntVec4& GetScissorBox() const; // x, y, width, height
|
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
|
// Transform feedback. The fields below are the state of the transform
|
||||||
// feedback object currently bound to GL_TRANSFORM_FEEDBACK; see the object
|
// feedback object currently bound to GL_TRANSFORM_FEEDBACK; see the object
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ namespace MobileGL {
|
|||||||
return 0;
|
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
|
} // namespace
|
||||||
|
|
||||||
RenderState::RenderState() {
|
RenderState::RenderState() {
|
||||||
@@ -32,6 +38,15 @@ namespace MobileGL {
|
|||||||
for (auto& mask : m_parameters.ColorMasks) {
|
for (auto& mask : m_parameters.ColorMasks) {
|
||||||
mask = BoolVec4(true, true, true, true);
|
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 {
|
Uint RenderState::GetVersion() const {
|
||||||
@@ -47,15 +62,47 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Rasterization --------------------
|
// -------------------- 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) {
|
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;
|
++m_version;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IntVec4& RenderState::GetViewport() const {
|
const FloatVec4& RenderState::GetViewportIndexed(Uint index) const {
|
||||||
return m_parameters.Viewport;
|
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) {
|
void RenderState::SetLineWidth(Float width) {
|
||||||
@@ -223,7 +270,6 @@ namespace MobileGL {
|
|||||||
SET_CAPABILITY(SampleAlphaToOne, enabled);
|
SET_CAPABILITY(SampleAlphaToOne, enabled);
|
||||||
SET_CAPABILITY(SampleCoverage, enabled);
|
SET_CAPABILITY(SampleCoverage, enabled);
|
||||||
SET_CAPABILITY(SampleMask, enabled);
|
SET_CAPABILITY(SampleMask, enabled);
|
||||||
SET_CAPABILITY(ScissorTest, enabled);
|
|
||||||
SET_CAPABILITY(StencilTest, enabled);
|
SET_CAPABILITY(StencilTest, enabled);
|
||||||
SET_CAPABILITY(ProgramPointSize, enabled);
|
SET_CAPABILITY(ProgramPointSize, enabled);
|
||||||
case CapabilityInput::Blend: {
|
case CapabilityInput::Blend: {
|
||||||
@@ -236,6 +282,17 @@ namespace MobileGL {
|
|||||||
if (stateChanged) BumpVersions();
|
if (stateChanged) BumpVersions();
|
||||||
break;
|
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::ClipDistance0:
|
||||||
case CapabilityInput::ClipDistance1:
|
case CapabilityInput::ClipDistance1:
|
||||||
case CapabilityInput::ClipDistance2:
|
case CapabilityInput::ClipDistance2:
|
||||||
@@ -287,11 +344,14 @@ namespace MobileGL {
|
|||||||
RETURN_CAPABILITY(SampleAlphaToOne);
|
RETURN_CAPABILITY(SampleAlphaToOne);
|
||||||
RETURN_CAPABILITY(SampleCoverage);
|
RETURN_CAPABILITY(SampleCoverage);
|
||||||
RETURN_CAPABILITY(SampleMask);
|
RETURN_CAPABILITY(SampleMask);
|
||||||
RETURN_CAPABILITY(ScissorTest);
|
|
||||||
RETURN_CAPABILITY(StencilTest);
|
RETURN_CAPABILITY(StencilTest);
|
||||||
RETURN_CAPABILITY(ProgramPointSize);
|
RETURN_CAPABILITY(ProgramPointSize);
|
||||||
case CapabilityInput::Blend:
|
case CapabilityInput::Blend:
|
||||||
return m_parameters.BlendStates[0].Enabled;
|
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::ClipDistance0:
|
||||||
case CapabilityInput::ClipDistance1:
|
case CapabilityInput::ClipDistance1:
|
||||||
case CapabilityInput::ClipDistance2:
|
case CapabilityInput::ClipDistance2:
|
||||||
@@ -307,13 +367,29 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RenderState::SetCapabilityIndexed(CapabilityInput cap, Uint index, Bool enabled) {
|
void RenderState::SetCapabilityIndexed(CapabilityInput cap, Uint index, Bool enabled) {
|
||||||
// Only for BlendState currently. The GL entry points (glEnablei/glDisablei) already
|
// GL_BLEND (indexed by draw buffer) and GL_SCISSOR_TEST (indexed by viewport) are
|
||||||
// reject every non-GL_BLEND target with GL_INVALID_ENUM before reaching here, so this
|
// the only indexed capabilities in GL 4.6 core. The GL entry points
|
||||||
// is a backstop - but it must stay a backstop: THROW_UNIMPL_EXCEPTION unwinds a C++
|
// (glEnablei/glDisablei) already reject every other target with GL_INVALID_ENUM
|
||||||
// exception through the C GL ABI and terminates the process.
|
// 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) {
|
if (cap != CapabilityInput::Blend) {
|
||||||
MGLOG_I("RenderState::SetCapabilityIndexed: indexed capability state exists only for "
|
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);
|
static_cast<int>(cap), index);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -328,9 +404,17 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Bool RenderState::IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
|
Bool RenderState::IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
|
||||||
// Only for BlendState currently - same backstop reasoning as SetCapabilityIndexed:
|
// GL_BLEND and GL_SCISSOR_TEST only - same backstop reasoning as
|
||||||
// glIsEnabledi has already answered GL_INVALID_ENUM/GL_FALSE for anything else, and a
|
// SetCapabilityIndexed: glIsEnabledi has already answered
|
||||||
// query must never be able to terminate the process.
|
// 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) {
|
if (cap != CapabilityInput::Blend) {
|
||||||
MGLOG_I("RenderState::IsCapabilityEnabledIndexed: indexed capability state exists only "
|
MGLOG_I("RenderState::IsCapabilityEnabledIndexed: indexed capability state exists only "
|
||||||
"for GL_BLEND (cap=%d, index=%u); reporting disabled",
|
"for GL_BLEND (cap=%d, index=%u); reporting disabled",
|
||||||
@@ -591,15 +675,39 @@ namespace MobileGL {
|
|||||||
return m_parameters.BlendColor;
|
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) {
|
void RenderState::SetDepthRange(FloatVec2 range) {
|
||||||
if (m_parameters.DepthRange == range) return;
|
Bool stateChanged = false;
|
||||||
|
for (auto& stored : m_parameters.DepthRanges) {
|
||||||
m_parameters.DepthRange = range;
|
if (stored == range) continue;
|
||||||
++m_version;
|
stored = range;
|
||||||
|
stateChanged = true;
|
||||||
|
}
|
||||||
|
if (stateChanged) ++m_version;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FloatVec2& RenderState::GetDepthRange() const {
|
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) {
|
void RenderState::SetSampleCoverage(Float value, Bool invert) {
|
||||||
@@ -726,15 +834,39 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --------------------- Scissor ---------------------
|
// --------------------- 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) {
|
void RenderState::SetScissorBox(IntVec4 box) {
|
||||||
if (m_parameters.ScissorBox == box) return;
|
Bool stateChanged = false;
|
||||||
|
for (auto& stored : m_parameters.ScissorBoxes) {
|
||||||
m_parameters.ScissorBox = box;
|
if (stored == box) continue;
|
||||||
++m_version;
|
stored = box;
|
||||||
|
stateChanged = true;
|
||||||
|
}
|
||||||
|
if (stateChanged) ++m_version;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IntVec4& RenderState::GetScissorBox() const {
|
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 GLState
|
||||||
} // namespace MG_State
|
} // namespace MG_State
|
||||||
|
|||||||
@@ -220,8 +220,22 @@ namespace MobileGL {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct RenderStateParameters {
|
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
|
// 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 LineWidth = 1.0f;
|
||||||
Float PointSize = 1.0f;
|
Float PointSize = 1.0f;
|
||||||
// GL_PATCH_VERTICES: how many vertices one tessellation patch consumes.
|
// GL_PATCH_VERTICES: how many vertices one tessellation patch consumes.
|
||||||
@@ -247,7 +261,13 @@ namespace MobileGL {
|
|||||||
Float ClearDepth = 1.0f;
|
Float ClearDepth = 1.0f;
|
||||||
Uint32 ClearStencil = 0;
|
Uint32 ClearStencil = 0;
|
||||||
FloatVec4 BlendColor = FloatVec4(0.0f, 0.0f, 0.0f, 0.0f);
|
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;
|
Float SampleCoverageValue = 1.0f;
|
||||||
Bool SampleCoverageInvert = false;
|
Bool SampleCoverageInvert = false;
|
||||||
Uint32 SampleMaskValue = 0xffffffffu;
|
Uint32 SampleMaskValue = 0xffffffffu;
|
||||||
@@ -299,10 +319,15 @@ namespace MobileGL {
|
|||||||
Bool SampleAlphaToOneEnabled = false;
|
Bool SampleAlphaToOneEnabled = false;
|
||||||
Bool SampleCoverageEnabled = false;
|
Bool SampleCoverageEnabled = false;
|
||||||
Bool SampleMaskEnabled = false;
|
Bool SampleMaskEnabled = false;
|
||||||
Bool ScissorTestEnabled = false;
|
|
||||||
Bool StencilTestEnabled = false;
|
Bool StencilTestEnabled = false;
|
||||||
Bool ProgramPointSizeEnabled = 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
|
// 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
|
// 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
|
// the SYNC_CAPABILITY/SET_CAPABILITY macros key off a "<Name>Enabled" field name that
|
||||||
@@ -323,8 +348,14 @@ namespace MobileGL {
|
|||||||
const RenderStateParameters& GetAllParameters() const;
|
const RenderStateParameters& GetAllParameters() const;
|
||||||
|
|
||||||
// Rasterization
|
// 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
|
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);
|
void SetLineWidth(Float width);
|
||||||
Float GetLineWidth() const;
|
Float GetLineWidth() const;
|
||||||
void SetPointSize(Float size);
|
void SetPointSize(Float size);
|
||||||
@@ -400,8 +431,12 @@ namespace MobileGL {
|
|||||||
Uint32 GetClearStencil() const;
|
Uint32 GetClearStencil() const;
|
||||||
void SetBlendColor(FloatVec4 color);
|
void SetBlendColor(FloatVec4 color);
|
||||||
const FloatVec4& GetBlendColor() const;
|
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);
|
void SetDepthRange(FloatVec2 range);
|
||||||
const FloatVec2& GetDepthRange() const;
|
const FloatVec2& GetDepthRange() const;
|
||||||
|
void SetDepthRangeIndexed(Uint index, FloatVec2 range);
|
||||||
|
const FloatVec2& GetDepthRangeIndexed(Uint index) const;
|
||||||
void SetSampleCoverage(Float value, Bool invert);
|
void SetSampleCoverage(Float value, Bool invert);
|
||||||
Float GetSampleCoverageValue() const;
|
Float GetSampleCoverageValue() const;
|
||||||
Bool GetSampleCoverageInvert() const;
|
Bool GetSampleCoverageInvert() const;
|
||||||
@@ -421,9 +456,12 @@ namespace MobileGL {
|
|||||||
void SetProvokingVertexMode(ProvokingVertexMode mode);
|
void SetProvokingVertexMode(ProvokingVertexMode mode);
|
||||||
ProvokingVertexMode GetProvokingVertexMode() const;
|
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
|
void SetScissorBox(IntVec4 box); // x, y, width, height
|
||||||
const IntVec4& GetScissorBox() const; // 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:
|
private:
|
||||||
// Bump both: any state change invalidates the draw snapshot, and this one also
|
// Bump both: any state change invalidates the draw snapshot, and this one also
|
||||||
|
|||||||
@@ -8,9 +8,28 @@
|
|||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#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) {
|
TEST(DirectVulkanSanity, ExtensionEnumeration) {
|
||||||
uint32_t extensionCount = 0;
|
uint32_t extensionCount = 0;
|
||||||
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
|
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
|
||||||
|
|||||||
@@ -725,22 +725,57 @@ TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSu
|
|||||||
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
|
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_EXT_texture_filter_anisotropic));
|
||||||
EXPECT_FALSE(contains(without, MobileGL::E_GL_ARB_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_EXT_texture_filter_anisotropic));
|
||||||
EXPECT_TRUE(contains(with, MobileGL::E_GL_ARB_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.
|
// 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));
|
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_EXT_texture_filter_anisotropic));
|
||||||
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_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) {
|
TEST(TextureAnisotropyCapabilities, MaxAnisotropyIsQueriedOnlyWhenTheExtensionIsPresent) {
|
||||||
ResetFakeDriver();
|
ResetFakeDriver();
|
||||||
g_fake.maxVertexSsboBlocks = 0;
|
g_fake.maxVertexSsboBlocks = 0;
|
||||||
@@ -836,3 +871,63 @@ TEST(MultiDrawCapabilities, ExtensionWithoutResolvedPointerIsNotSupport) {
|
|||||||
EXPECT_FALSE(caps.SupportsMultiDrawIndirect);
|
EXPECT_FALSE(caps.SupportsMultiDrawIndirect);
|
||||||
EXPECT_FALSE(caps.SupportsMultiDrawElementsBaseVertex);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ add_executable(
|
|||||||
PipelineQuirkTest
|
PipelineQuirkTest
|
||||||
PipelineQuirkTest.cpp
|
PipelineQuirkTest.cpp
|
||||||
PassthroughTessControlTest.cpp
|
PassthroughTessControlTest.cpp
|
||||||
|
ViewportIndexReflectionTest.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(PipelineQuirkTest PRIVATE
|
target_include_directories(PipelineQuirkTest PRIVATE
|
||||||
|
|||||||
@@ -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) {
|
TEST_F(ParallelShaderCompileTest, BothBackendsAdvertiseTheExtensionIffAsyncIsEnabled) {
|
||||||
{
|
{
|
||||||
const AsyncModeScope async(true);
|
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));
|
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));
|
E_GL_KHR_parallel_shader_compile));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const AsyncModeScope async(false);
|
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))
|
E_GL_KHR_parallel_shader_compile))
|
||||||
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
|
<< "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))
|
E_GL_KHR_parallel_shader_compile))
|
||||||
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
|
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2692,11 +2692,13 @@ out vec4 fragColor;
|
|||||||
float fma
|
float fma
|
||||||
(float a, float b, float c) { return a * b + c; }
|
(float a, float b, float c) { return a * b + c; }
|
||||||
float sinh(float x, float y) { return x * y; }
|
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 round(float x) { return floor(x + 0.5); }
|
||||||
float min3(float a, float b, float c) { return min(min(a, b), c); }
|
float min3(float a, float b, float c) { return min(min(a, b), c); }
|
||||||
|
|
||||||
void main() {
|
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);
|
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
|
||||||
@@ -2707,6 +2709,7 @@ void main() {
|
|||||||
if (essl.find("fragColor") == String::npos) continue; // fragment module only
|
if (essl.find("fragColor") == String::npos) continue; // fragment module only
|
||||||
EXPECT_NE(essl.find("mg_fma("), String::npos) << essl;
|
EXPECT_NE(essl.find("mg_fma("), String::npos) << essl;
|
||||||
EXPECT_NE(essl.find("mg_sinh("), 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_round("), String::npos) << essl;
|
||||||
EXPECT_NE(essl.find("mg_min3("), String::npos) << essl;
|
EXPECT_NE(essl.find("mg_min3("), String::npos) << essl;
|
||||||
EXPECT_EQ(essl.find("float fma("), String::npos) << essl;
|
EXPECT_EQ(essl.find("float fma("), String::npos) << essl;
|
||||||
|
|||||||
@@ -52,20 +52,24 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
|||||||
OpEntryPoint Fragment %main "main" %outColor
|
OpEntryPoint Fragment %main "main" %outColor
|
||||||
OpExecutionMode %main OriginUpperLeft
|
OpExecutionMode %main OriginUpperLeft
|
||||||
OpName %globalSampler "sampler"
|
OpName %globalSampler "sampler"
|
||||||
|
OpName %globalNew "new"
|
||||||
OpName %paramSampler "sampler"
|
OpName %paramSampler "sampler"
|
||||||
|
OpName %paramNew "new"
|
||||||
OpName %main "main"
|
OpName %main "main"
|
||||||
OpDecorate %outColor Location 0
|
OpDecorate %outColor Location 0
|
||||||
%void = OpTypeVoid
|
%void = OpTypeVoid
|
||||||
%float = OpTypeFloat 32
|
%float = OpTypeFloat 32
|
||||||
%v4float = OpTypeVector %float 4
|
%v4float = OpTypeVector %float 4
|
||||||
%mainFn = OpTypeFunction %void
|
%mainFn = OpTypeFunction %void
|
||||||
%paramFn = OpTypeFunction %void %float
|
%paramFn = OpTypeFunction %void %float %float
|
||||||
%outV4Ptr = OpTypePointer Output %v4float
|
%outV4Ptr = OpTypePointer Output %v4float
|
||||||
%privatePtr = OpTypePointer Private %float
|
%privatePtr = OpTypePointer Private %float
|
||||||
%outColor = OpVariable %outV4Ptr Output
|
%outColor = OpVariable %outV4Ptr Output
|
||||||
%globalSampler = OpVariable %privatePtr Private
|
%globalSampler = OpVariable %privatePtr Private
|
||||||
|
%globalNew = OpVariable %privatePtr Private
|
||||||
%helper = OpFunction %void None %paramFn
|
%helper = OpFunction %void None %paramFn
|
||||||
%paramSampler = OpFunctionParameter %float
|
%paramSampler = OpFunctionParameter %float
|
||||||
|
%paramNew = OpFunctionParameter %float
|
||||||
%helperBody = OpLabel
|
%helperBody = OpLabel
|
||||||
OpReturn
|
OpReturn
|
||||||
OpFunctionEnd
|
OpFunctionEnd
|
||||||
@@ -91,6 +95,7 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
|||||||
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
|
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
|
||||||
|
|
||||||
EXPECT_NE(outputText.find("\"MGL_COMPAT_sampler\""), String::npos);
|
EXPECT_NE(outputText.find("\"MGL_COMPAT_sampler\""), String::npos);
|
||||||
|
EXPECT_NE(outputText.find("\"MGL_COMPAT_new\""), String::npos);
|
||||||
|
|
||||||
SizeT exactSamplerNameCount = 0;
|
SizeT exactSamplerNameCount = 0;
|
||||||
SizeT searchOffset = 0;
|
SizeT searchOffset = 0;
|
||||||
@@ -99,6 +104,14 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
|||||||
searchOffset += std::strlen("\"sampler\"");
|
searchOffset += std::strlen("\"sampler\"");
|
||||||
}
|
}
|
||||||
EXPECT_EQ(exactSamplerNameCount, 1u);
|
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) {
|
TEST_F(ProgramUtilTest, UnformattedFloatStorageImagesKeepIntegerAtomicImagesTyped) {
|
||||||
|
|||||||
@@ -448,6 +448,39 @@ TEST_F(QueryTest, BackendResultsPropagateThroughFrontend) {
|
|||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
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
|
// Environment-agnostic property test for the env -> ConfigLoader -> Features
|
||||||
// chain: whatever MOBILEGL_DISABLE_TIMERQUERY is set to in the environment of
|
// chain: whatever MOBILEGL_DISABLE_TIMERQUERY is set to in the environment of
|
||||||
// this test process, MG_ConfigLoader::Init must have parsed it with the
|
// this test process, MG_ConfigLoader::Init must have parsed it with the
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
|
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||||
|
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
||||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||||
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
|
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
|
||||||
@@ -936,6 +937,45 @@ TEST(DirectVulkanSanity, ReadbackUsesTheSourceFormatTexelSize) {
|
|||||||
EXPECT_EQ(VulkanRenderer::GetReadbackTexelSize(VK_FORMAT_R32G32B32A32_SFLOAT), 16u);
|
EXPECT_EQ(VulkanRenderer::GetReadbackTexelSize(VK_FORMAT_R32G32B32A32_SFLOAT), 16u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(DirectVulkanSanity, DefaultFramebufferQuarterTurnReadbackMapsRectAndPixels) {
|
||||||
|
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
|
||||||
|
using MobileGL::Uint8;
|
||||||
|
|
||||||
|
VkOffset2D offset{};
|
||||||
|
VkExtent2D copyExtent{};
|
||||||
|
ASSERT_TRUE(VulkanRenderer::MapDefaultFramebufferReadbackRect(
|
||||||
|
1, 0, 2, 1, VkExtent2D{2, 3}, VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR,
|
||||||
|
&offset, ©Extent));
|
||||||
|
EXPECT_EQ(offset.x, 0);
|
||||||
|
EXPECT_EQ(offset.y, 1);
|
||||||
|
EXPECT_EQ(copyExtent.width, 1u);
|
||||||
|
EXPECT_EQ(copyExtent.height, 2u);
|
||||||
|
|
||||||
|
ASSERT_TRUE(VulkanRenderer::MapDefaultFramebufferReadbackRect(
|
||||||
|
1, 0, 2, 1, VkExtent2D{2, 3}, VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR,
|
||||||
|
&offset, ©Extent));
|
||||||
|
EXPECT_EQ(offset.x, 1);
|
||||||
|
EXPECT_EQ(offset.y, 0);
|
||||||
|
EXPECT_EQ(copyExtent.width, 1u);
|
||||||
|
EXPECT_EQ(copyExtent.height, 2u);
|
||||||
|
|
||||||
|
// Logical GL rows, bottom to top, are abc / def. The display-oriented swapchain blocks are
|
||||||
|
// transposed in opposite directions for 90 and 270 degrees.
|
||||||
|
const Uint8 raw90[] = {'a', 'd', 'b', 'e', 'c', 'f'};
|
||||||
|
const Uint8 raw270[] = {'f', 'c', 'e', 'b', 'd', 'a'};
|
||||||
|
const Uint8 expected[] = {'a', 'b', 'c', 'd', 'e', 'f'};
|
||||||
|
Uint8 result[sizeof(expected)]{};
|
||||||
|
|
||||||
|
ASSERT_TRUE(VulkanRenderer::RemapDefaultFramebufferReadback(
|
||||||
|
raw90, 3, 2, VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR, 1, result));
|
||||||
|
EXPECT_TRUE(std::equal(std::begin(expected), std::end(expected), std::begin(result)));
|
||||||
|
|
||||||
|
std::fill(std::begin(result), std::end(result), 0);
|
||||||
|
ASSERT_TRUE(VulkanRenderer::RemapDefaultFramebufferReadback(
|
||||||
|
raw270, 3, 2, VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR, 1, result));
|
||||||
|
EXPECT_TRUE(std::equal(std::begin(expected), std::end(expected), std::begin(result)));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(DirectVulkanSanity, ReadbackConvertsRgba8AndRgba16fPixels) {
|
TEST(DirectVulkanSanity, ReadbackConvertsRgba8AndRgba16fPixels) {
|
||||||
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
|
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
|
||||||
using MobileGL::MG_Util::EncodeFloatToHalfBits;
|
using MobileGL::MG_Util::EncodeFloatToHalfBits;
|
||||||
@@ -1932,6 +1972,9 @@ namespace {
|
|||||||
MobileGL::Vector<GLuint> framebuffers;
|
MobileGL::Vector<GLuint> framebuffers;
|
||||||
MobileGL::Vector<GLuint> renderbuffers;
|
MobileGL::Vector<GLuint> renderbuffers;
|
||||||
MobileGL::Vector<GLuint> samplers;
|
MobileGL::Vector<GLuint> samplers;
|
||||||
|
MobileGL::Vector<GLuint> vertexArrays;
|
||||||
|
MobileGL::Vector<GLuint> programs;
|
||||||
|
MobileGL::Vector<GLuint> buffers;
|
||||||
};
|
};
|
||||||
|
|
||||||
TwinDeletionSinks* g_twinDeletionSinks = nullptr;
|
TwinDeletionSinks* g_twinDeletionSinks = nullptr;
|
||||||
@@ -1958,6 +2001,24 @@ namespace {
|
|||||||
if (!g_twinDeletionSinks) return;
|
if (!g_twinDeletionSinks) return;
|
||||||
for (GLsizei i = 0; i < count; ++i) g_twinDeletionSinks->samplers.push_back(ids[i]);
|
for (GLsizei i = 0; i < count; ++i) g_twinDeletionSinks->samplers.push_back(ids[i]);
|
||||||
}
|
}
|
||||||
|
void TW_GenVertexArrays(GLsizei count, GLuint* ids) {
|
||||||
|
for (GLsizei i = 0; i < count; ++i) ids[i] = g_nextTwinDriverId++;
|
||||||
|
}
|
||||||
|
void TW_DeleteVertexArrays(GLsizei count, const GLuint* ids) {
|
||||||
|
if (!g_twinDeletionSinks) return;
|
||||||
|
for (GLsizei i = 0; i < count; ++i) g_twinDeletionSinks->vertexArrays.push_back(ids[i]);
|
||||||
|
}
|
||||||
|
GLuint TW_CreateProgram() { return g_nextTwinDriverId++; }
|
||||||
|
void TW_DeleteProgram(GLuint program) {
|
||||||
|
if (g_twinDeletionSinks) g_twinDeletionSinks->programs.push_back(program);
|
||||||
|
}
|
||||||
|
void TW_GenBuffers(GLsizei count, GLuint* ids) {
|
||||||
|
for (GLsizei i = 0; i < count; ++i) ids[i] = g_nextTwinDriverId++;
|
||||||
|
}
|
||||||
|
void TW_DeleteBuffers(GLsizei count, const GLuint* ids) {
|
||||||
|
if (!g_twinDeletionSinks) return;
|
||||||
|
for (GLsizei i = 0; i < count; ++i) g_twinDeletionSinks->buffers.push_back(ids[i]);
|
||||||
|
}
|
||||||
void TW_BindFramebuffer(GLenum target, GLuint framebuffer) {
|
void TW_BindFramebuffer(GLenum target, GLuint framebuffer) {
|
||||||
SG_Log("BindFramebuffer:" + std::to_string(target) + ":" + std::to_string(framebuffer));
|
SG_Log("BindFramebuffer:" + std::to_string(target) + ":" + std::to_string(framebuffer));
|
||||||
}
|
}
|
||||||
@@ -1979,6 +2040,12 @@ namespace {
|
|||||||
functions.glGenSamplers = TW_GenSamplers;
|
functions.glGenSamplers = TW_GenSamplers;
|
||||||
functions.glDeleteSamplers = TW_DeleteSamplers;
|
functions.glDeleteSamplers = TW_DeleteSamplers;
|
||||||
functions.glBindSampler = TW_BindSampler;
|
functions.glBindSampler = TW_BindSampler;
|
||||||
|
functions.glGenVertexArrays = TW_GenVertexArrays;
|
||||||
|
functions.glDeleteVertexArrays = TW_DeleteVertexArrays;
|
||||||
|
functions.glCreateProgram = TW_CreateProgram;
|
||||||
|
functions.glDeleteProgram = TW_DeleteProgram;
|
||||||
|
functions.glGenBuffers = TW_GenBuffers;
|
||||||
|
functions.glDeleteBuffers = TW_DeleteBuffers;
|
||||||
functions.glGetError = SG_NoError;
|
functions.glGetError = SG_NoError;
|
||||||
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(functions);
|
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(functions);
|
||||||
g_twinDeletionSinks = &sinks;
|
g_twinDeletionSinks = &sinks;
|
||||||
@@ -2078,6 +2145,145 @@ TEST(DirectGLESBackendSampler, DestructorDeletesIdAndScrubsUnitCache) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(DirectGLESBackendVertexArray, DestructorDeletesIdAndHonorsContextGeneration) {
|
||||||
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
|
ScopedBackendTwinMocks mocks;
|
||||||
|
|
||||||
|
GLuint id = 0;
|
||||||
|
{
|
||||||
|
auto backendVao = MobileGL::MakeShared<VertexArrayImpl::BackendVertexArrayObject>();
|
||||||
|
id = backendVao->GetBackendVertexArrayId();
|
||||||
|
ASSERT_NE(id, 0u);
|
||||||
|
}
|
||||||
|
ASSERT_EQ(mocks.sinks.vertexArrays.size(), 1u);
|
||||||
|
EXPECT_EQ(mocks.sinks.vertexArrays[0], id);
|
||||||
|
|
||||||
|
// A twin whose context died must NOT delete a VAO name a successor context
|
||||||
|
// may already have recycled (both contexts restart GL names at 1).
|
||||||
|
{
|
||||||
|
auto backendVao = MobileGL::MakeShared<VertexArrayImpl::BackendVertexArrayObject>();
|
||||||
|
++g_backendContextGeneration;
|
||||||
|
backendVao.reset();
|
||||||
|
--g_backendContextGeneration; // restore for later tests
|
||||||
|
EXPECT_EQ(mocks.sinks.vertexArrays.size(), 1u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectGLESBackendProgram, DestructorDeletesIdAndHonorsContextGeneration) {
|
||||||
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
|
ScopedBackendTwinMocks mocks;
|
||||||
|
|
||||||
|
GLuint id = 0;
|
||||||
|
{
|
||||||
|
auto backendProgram = MobileGL::MakeShared<PrgramImpl::BackendProgramObjectImpl>();
|
||||||
|
id = backendProgram->GetBackendProgramId();
|
||||||
|
ASSERT_NE(id, 0u);
|
||||||
|
}
|
||||||
|
ASSERT_EQ(mocks.sinks.programs.size(), 1u);
|
||||||
|
EXPECT_EQ(mocks.sinks.programs[0], id);
|
||||||
|
|
||||||
|
{
|
||||||
|
auto backendProgram = MobileGL::MakeShared<PrgramImpl::BackendProgramObjectImpl>();
|
||||||
|
++g_backendContextGeneration;
|
||||||
|
backendProgram.reset();
|
||||||
|
--g_backendContextGeneration;
|
||||||
|
EXPECT_EQ(mocks.sinks.programs.size(), 1u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectGLESBackendProgram, GlobalUboDeletionHonorsContextGeneration) {
|
||||||
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
|
ScopedBackendTwinMocks mocks;
|
||||||
|
|
||||||
|
MobileGL::Uint id = 123;
|
||||||
|
PrgramImpl::DeleteBackendProgramGlobalUbo(id, g_backendContextGeneration);
|
||||||
|
EXPECT_EQ(id, 0u);
|
||||||
|
ASSERT_EQ(mocks.sinks.buffers.size(), 1u);
|
||||||
|
EXPECT_EQ(mocks.sinks.buffers[0], 123u);
|
||||||
|
|
||||||
|
// A buffer belonging to a dead context must be abandoned, never deleted as a
|
||||||
|
// recycled name in the successor context.
|
||||||
|
id = 124;
|
||||||
|
PrgramImpl::DeleteBackendProgramGlobalUbo(id, g_backendContextGeneration - 1);
|
||||||
|
EXPECT_EQ(id, 0u);
|
||||||
|
EXPECT_EQ(mocks.sinks.buffers.size(), 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct SyncDeleteRacePayload {
|
||||||
|
std::atomic<MobileGL::Bool> alive{true};
|
||||||
|
};
|
||||||
|
std::atomic<MobileGL::Int> g_syncRaceDeleteCount{0};
|
||||||
|
|
||||||
|
MobileGL::MG_Backend::BackendSyncHandle SyncRaceFenceSync() {
|
||||||
|
return new SyncDeleteRacePayload();
|
||||||
|
}
|
||||||
|
|
||||||
|
GLenum SyncRaceClientWaitSync(MobileGL::MG_Backend::BackendSyncHandle handle, GLbitfield, GLuint64) {
|
||||||
|
auto* payload = static_cast<SyncDeleteRacePayload*>(handle);
|
||||||
|
// Keep the backend call in flight while the GL thread runs DeleteSync. The
|
||||||
|
// frontend must not release the backend handle (or the SyncObject wrapper)
|
||||||
|
// until this call has returned.
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||||
|
return payload->alive.load(std::memory_order_acquire) ? GL_ALREADY_SIGNALED : GL_WAIT_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncRaceWaitSync(MobileGL::MG_Backend::BackendSyncHandle, GLbitfield, GLuint64) {}
|
||||||
|
|
||||||
|
void SyncRaceDeleteSync(MobileGL::MG_Backend::BackendSyncHandle handle) {
|
||||||
|
auto* payload = static_cast<SyncDeleteRacePayload*>(handle);
|
||||||
|
payload->alive.store(false, std::memory_order_release);
|
||||||
|
delete payload;
|
||||||
|
g_syncRaceDeleteCount.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
MobileGL::Bool SyncRaceGetSyncStatus(MobileGL::MG_Backend::BackendSyncHandle) { return true; }
|
||||||
|
|
||||||
|
struct ScopedSyncRaceBackend {
|
||||||
|
ScopedSyncRaceBackend(): previous(MobileGL::MG_Backend::gBackendFunctionsTable) {
|
||||||
|
MobileGL::MG_Backend::GlobalBackendFunctionsTable functions{};
|
||||||
|
functions.GL.FenceSync = SyncRaceFenceSync;
|
||||||
|
functions.GL.ClientWaitSync = SyncRaceClientWaitSync;
|
||||||
|
functions.GL.WaitSync = SyncRaceWaitSync;
|
||||||
|
functions.GL.DeleteSync = SyncRaceDeleteSync;
|
||||||
|
functions.GL.GetSyncStatus = SyncRaceGetSyncStatus;
|
||||||
|
MobileGL::MG_Backend::gBackendFunctionsTable = functions;
|
||||||
|
g_syncRaceDeleteCount.store(0, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
~ScopedSyncRaceBackend() {
|
||||||
|
MobileGL::MG_Impl::GLImpl::DestroyAllSyncObjects();
|
||||||
|
MobileGL::MG_Backend::gBackendFunctionsTable = previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedSyncRaceBackend(const ScopedSyncRaceBackend&) = delete;
|
||||||
|
ScopedSyncRaceBackend& operator=(const ScopedSyncRaceBackend&) = delete;
|
||||||
|
|
||||||
|
MobileGL::MG_Backend::GlobalBackendFunctionsTable previous;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(SyncLifetime, DeleteWaitsForInFlightClientWait) {
|
||||||
|
ScopedSyncRaceBackend backend;
|
||||||
|
|
||||||
|
const GLsync sync = MobileGL::MG_Impl::GLImpl::FenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||||
|
ASSERT_NE(sync, nullptr);
|
||||||
|
|
||||||
|
GLenum clientResult = GL_WAIT_FAILED;
|
||||||
|
std::thread waiter([sync, &clientResult] {
|
||||||
|
clientResult = MobileGL::MG_Impl::GLImpl::ClientWaitSync(sync, 0, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Give the worker a head start so ClientWaitSync is already inside the stub
|
||||||
|
// (and therefore holds the per-object lock) when DeleteSync runs.
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||||
|
MobileGL::MG_Impl::GLImpl::DeleteSync(sync);
|
||||||
|
waiter.join();
|
||||||
|
|
||||||
|
EXPECT_EQ(clientResult, GL_ALREADY_SIGNALED);
|
||||||
|
EXPECT_EQ(g_syncRaceDeleteCount.load(std::memory_order_relaxed), 1);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(DirectGLESStateGuards, DefaultFramebufferBindGoesThroughShadow) {
|
TEST(DirectGLESStateGuards, DefaultFramebufferBindGoesThroughShadow) {
|
||||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
ScopedStateGuardMocks mocks;
|
ScopedStateGuardMocks mocks;
|
||||||
|
|||||||
@@ -6,11 +6,20 @@
|
|||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
//
|
//
|
||||||
// Indexed capability state (glEnablei/glDisablei/glIsEnabledi) exists only for GL_BLEND in this
|
// Indexed capability state (glEnablei/glDisablei/glIsEnabledi) exists for exactly two
|
||||||
// stack. Every other capability must come back as GL_INVALID_ENUM per GL 4.6 sec. 17.3.3 - and,
|
// capabilities: GL_BLEND, indexed by draw buffer, and GL_SCISSOR_TEST, indexed by viewport
|
||||||
// far more importantly, must come back at all: RenderState::SetCapabilityIndexed and
|
// (ARB_viewport_array). Every other capability must come back as GL_INVALID_ENUM per GL 4.6
|
||||||
// IsCapabilityEnabledIndexed used to answer a non-blend capability with THROW_UNIMPL_EXCEPTION,
|
// sec. 17.3.3 - and, far more importantly, must come back at all: RenderState::SetCapabilityIndexed
|
||||||
|
// and IsCapabilityEnabledIndexed used to answer a non-blend capability with THROW_UNIMPL_EXCEPTION,
|
||||||
// which unwinds a C++ exception through the C GL ABI and terminates the process.
|
// which unwinds a C++ exception through the C GL ABI and terminates the process.
|
||||||
|
//
|
||||||
|
// The second half of this file is the ARB_viewport_array indexed rectangle state. Every one of
|
||||||
|
// glViewportArrayv/glViewportIndexedf(v)/glScissorArrayv/glScissorIndexed(v)/glDepthRangeArrayv/
|
||||||
|
// glDepthRangeIndexed was a MGLOG_W_ONCE stub that raised no error and stored nothing, and the
|
||||||
|
// indexed getters answered EVERY index with viewport 0's value, so a set/get round trip silently
|
||||||
|
// reported the initial state. The assertions below are deliberately state-shaped rather than
|
||||||
|
// render-shaped: this IS the state machine, and the rendering half (gl_ViewportIndex routing) is
|
||||||
|
// asserted separately in MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp.
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
@@ -21,6 +30,7 @@
|
|||||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||||
|
#include <MG_State/GLState/RenderState/RenderState.h>
|
||||||
|
|
||||||
using namespace MobileGL;
|
using namespace MobileGL;
|
||||||
|
|
||||||
@@ -50,10 +60,11 @@ namespace {
|
|||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST_F(RenderStateTest, IndexedCapabilityTogglesRejectNonBlendCapabilities) {
|
TEST_F(RenderStateTest, IndexedCapabilityTogglesRejectNonIndexedCapabilities) {
|
||||||
// GL_CLIP_DISTANCE0 is a real capability, just not an indexed one - the shape an application or
|
// GL_CLIP_DISTANCE0 is a real capability, just not an indexed one - the shape an application or
|
||||||
// a CTS negative test would hit.
|
// a CTS negative test would hit. GL_SCISSOR_TEST used to be in this list and is not any more:
|
||||||
for (const GLenum cap : {GL_CLIP_DISTANCE0, GL_DEPTH_TEST, GL_SCISSOR_TEST}) {
|
// ARB_viewport_array makes it the second indexed capability (see the tests below).
|
||||||
|
for (const GLenum cap : {GL_CLIP_DISTANCE0, GL_DEPTH_TEST, GL_STENCIL_TEST}) {
|
||||||
MG_Impl::GLImpl::Enablei(cap, 0);
|
MG_Impl::GLImpl::Enablei(cap, 0);
|
||||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||||
|
|
||||||
@@ -89,3 +100,462 @@ TEST_F(RenderStateTest, IndexedBlendTogglesStillWork) {
|
|||||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, 1), GL_FALSE);
|
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, 1), GL_FALSE);
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// ARB_viewport_array: indexed viewport / scissor / depth-range state
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr GLuint kMaxViewports = RenderStateParameters::MAX_VIEWPORTS;
|
||||||
|
|
||||||
|
Array<Array<GLfloat, 4>, kMaxViewports> ReadAllViewports() {
|
||||||
|
Array<Array<GLfloat, 4>, kMaxViewports> out{};
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, i, out[i].data());
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Array<Array<GLdouble, 2>, kMaxViewports> ReadAllDepthRanges() {
|
||||||
|
Array<Array<GLdouble, 2>, kMaxViewports> out{};
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, i, out[i].data());
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, ScissorTestIsIndexedByViewport) {
|
||||||
|
// The exact shape of KHR-GL43.viewport_array.scissor_test_state_api's toggle loop: one index
|
||||||
|
// is flipped and EVERY index is read back, so a broadcast masquerading as an indexed write
|
||||||
|
// cannot pass.
|
||||||
|
MG_Impl::GLImpl::Disable(GL_SCISSOR_TEST);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
|
||||||
|
for (GLuint toggled = 0; toggled < kMaxViewports; ++toggled) {
|
||||||
|
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, toggled);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "index " << toggled;
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, i), i == toggled ? GL_TRUE : GL_FALSE)
|
||||||
|
<< "enabled index " << toggled << ", read index " << i;
|
||||||
|
}
|
||||||
|
MG_Impl::GLImpl::Disablei(GL_SCISSOR_TEST, toggled);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, toggled), GL_FALSE);
|
||||||
|
}
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, NonIndexedScissorTestEnableWritesEveryViewport) {
|
||||||
|
// GL 4.6 core 17.3.2: Enable/Disable(SCISSOR_TEST) is "for all viewports". Reading only
|
||||||
|
// index 0 back would let a broadcast-less implementation through, so every index is checked.
|
||||||
|
MG_Impl::GLImpl::Enable(GL_SCISSOR_TEST);
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, i), GL_TRUE) << "index " << i;
|
||||||
|
}
|
||||||
|
// ... and the non-indexed query answers for viewport 0 (GL 4.6 core 22.1).
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_TRUE);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::Disable(GL_SCISSOR_TEST);
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, i), GL_FALSE) << "index " << i;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_FALSE);
|
||||||
|
|
||||||
|
// An indexed enable on a NON-zero index must not move the non-indexed answer.
|
||||||
|
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, 3);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_FALSE);
|
||||||
|
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, 0);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_TRUE);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::Disable(GL_SCISSOR_TEST);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, ScissorTestEnableRejectsAnOutOfRangeViewportIndex) {
|
||||||
|
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, kMaxViewports);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::Disablei(GL_SCISSOR_TEST, kMaxViewports);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, kMaxViewports), GL_FALSE);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
// MAX_VIEWPORTS - 1 is the last LEGAL index and must stay silent.
|
||||||
|
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, kMaxViewports - 1);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
MG_Impl::GLImpl::Disablei(GL_SCISSOR_TEST, kMaxViewports - 1);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, MaxViewportsMatchesTheIndexedStateWidth) {
|
||||||
|
// The advertised limit and the width of the state arrays are the same number by
|
||||||
|
// construction; a divergence would make some index simultaneously legal to the CTS and
|
||||||
|
// out of range to the setters.
|
||||||
|
GLint maxViewports = 0;
|
||||||
|
MG_Impl::GLImpl::GetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
EXPECT_EQ(maxViewports, static_cast<GLint>(kMaxViewports));
|
||||||
|
EXPECT_GE(maxViewports, 16) << "GL 4.3 core requires MAX_VIEWPORTS >= 16";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, ViewportArrayvRoundTripsThroughEveryGetterWidth) {
|
||||||
|
Array<GLfloat, kMaxViewports * 4> written{};
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
written[i * 4 + 0] = static_cast<GLfloat>(i) + 0.125f;
|
||||||
|
written[i * 4 + 1] = static_cast<GLfloat>(i) + 0.25f;
|
||||||
|
written[i * 4 + 2] = static_cast<GLfloat>(64 + i);
|
||||||
|
written[i * 4 + 3] = static_cast<GLfloat>(32 + i);
|
||||||
|
}
|
||||||
|
MG_Impl::GLImpl::ViewportArrayv(0, kMaxViewports, written.data());
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
GLfloat asFloat[4] = {};
|
||||||
|
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, i, asFloat);
|
||||||
|
// Bit-exact: the fractional origin is the whole point of float viewport state, and the
|
||||||
|
// CTS compares with == (0.125 and 0.25 are exact binary fractions, so this is fair).
|
||||||
|
EXPECT_EQ(asFloat[0], written[i * 4 + 0]) << "index " << i << " must round-trip verbatim";
|
||||||
|
EXPECT_EQ(asFloat[1], written[i * 4 + 1]) << "index " << i;
|
||||||
|
EXPECT_EQ(asFloat[2], written[i * 4 + 2]) << "index " << i;
|
||||||
|
EXPECT_EQ(asFloat[3], written[i * 4 + 3]) << "index " << i;
|
||||||
|
|
||||||
|
GLdouble asDouble[4] = {};
|
||||||
|
MG_Impl::GLImpl::GetDoublei_v(GL_VIEWPORT, i, asDouble);
|
||||||
|
for (int c = 0; c < 4; ++c) {
|
||||||
|
EXPECT_EQ(asDouble[c], static_cast<GLdouble>(written[i * 4 + c])) << "index " << i << " component " << c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The integer widths round to nearest rather than truncate; the .5+ case is pinned by
|
||||||
|
// ViewportRoundsRatherThanTruncatesForIntegerQueries below.
|
||||||
|
GLint asInt[4] = {};
|
||||||
|
MG_Impl::GLImpl::GetIntegeri_v(GL_VIEWPORT, i, asInt);
|
||||||
|
EXPECT_EQ(asInt[2], static_cast<GLint>(64 + i)) << "index " << i;
|
||||||
|
EXPECT_EQ(asInt[3], static_cast<GLint>(32 + i)) << "index " << i;
|
||||||
|
|
||||||
|
GLint64 asInt64[4] = {};
|
||||||
|
MG_Impl::GLImpl::GetInteger64i_v(GL_VIEWPORT, i, asInt64);
|
||||||
|
for (int c = 0; c < 4; ++c) {
|
||||||
|
EXPECT_EQ(asInt64[c], static_cast<GLint64>(asInt[c])) << "index " << i << " component " << c;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLboolean asBool[4] = {};
|
||||||
|
MG_Impl::GLImpl::GetBooleani_v(GL_VIEWPORT, i, asBool);
|
||||||
|
EXPECT_EQ(asBool[2], GL_TRUE) << "index " << i << ": a non-zero width is GL_TRUE";
|
||||||
|
}
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, ViewportRoundsRatherThanTruncatesForIntegerQueries) {
|
||||||
|
MG_Impl::GLImpl::ViewportIndexedf(2, 0.0f, 0.0f, 255.875f, 63.5f);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
|
||||||
|
GLint asInt[4] = {};
|
||||||
|
MG_Impl::GLImpl::GetIntegeri_v(GL_VIEWPORT, 2, asInt);
|
||||||
|
EXPECT_EQ(asInt[2], 256);
|
||||||
|
EXPECT_EQ(asInt[3], 64);
|
||||||
|
|
||||||
|
GLfloat asFloat[4] = {};
|
||||||
|
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, 2, asFloat);
|
||||||
|
EXPECT_EQ(asFloat[2], 255.875f) << "the integer query must not disturb the stored float";
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, ViewportIndexedWritesTouchExactlyOneIndex) {
|
||||||
|
MG_Impl::GLImpl::Viewport(0, 0, 8, 8);
|
||||||
|
const auto before = ReadAllViewports();
|
||||||
|
|
||||||
|
for (GLuint target = 0; target < kMaxViewports; ++target) {
|
||||||
|
const GLfloat value[4] = {0.375f, 0.375f, 0.625f, 0.625f};
|
||||||
|
// Alternate the two indexed entry points so both are covered by the isolation claim.
|
||||||
|
if (target % 2 == 0) {
|
||||||
|
MG_Impl::GLImpl::ViewportIndexedf(target, value[0], value[1], value[2], value[3]);
|
||||||
|
} else {
|
||||||
|
MG_Impl::GLImpl::ViewportIndexedfv(target, value);
|
||||||
|
}
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
|
||||||
|
const auto after = ReadAllViewports();
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
if (i == target) {
|
||||||
|
EXPECT_EQ(after[i][0], value[0]) << "index " << i;
|
||||||
|
EXPECT_EQ(after[i][2], value[2]) << "index " << i;
|
||||||
|
} else {
|
||||||
|
EXPECT_EQ(after[i], before[i]) << "write to " << target << " disturbed index " << i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MG_Impl::GLImpl::ViewportIndexedf(target, before[target][0], before[target][1], before[target][2],
|
||||||
|
before[target][3]);
|
||||||
|
}
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, ClassicViewportWritesEveryIndexAndIsVisibleThroughIndexZero) {
|
||||||
|
// Both directions of the aliasing. ARB_viewport_array defines glViewport as ViewportIndexedf
|
||||||
|
// on every index, and glGetIntegerv(GL_VIEWPORT) as viewport 0.
|
||||||
|
MG_Impl::GLImpl::ViewportIndexedf(5, 1.0f, 2.0f, 3.0f, 4.0f);
|
||||||
|
MG_Impl::GLImpl::Viewport(0, 0, 1, 1);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
GLfloat data[4] = {};
|
||||||
|
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, i, data);
|
||||||
|
EXPECT_EQ(data[0], 0.0f) << "index " << i;
|
||||||
|
EXPECT_EQ(data[2], 1.0f) << "glViewport must overwrite index " << i;
|
||||||
|
}
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::ViewportIndexedf(0, 4.0f, 5.0f, 6.0f, 7.0f);
|
||||||
|
GLint classic[4] = {};
|
||||||
|
MG_Impl::GLImpl::GetIntegerv(GL_VIEWPORT, classic);
|
||||||
|
EXPECT_EQ(classic[0], 4);
|
||||||
|
EXPECT_EQ(classic[2], 6);
|
||||||
|
GLfloat classicFloat[4] = {};
|
||||||
|
MG_Impl::GLImpl::GetFloatv(GL_VIEWPORT, classicFloat);
|
||||||
|
EXPECT_EQ(classicFloat[2], 6.0f);
|
||||||
|
// Index 5 keeps its own value: writing index 0 is not a broadcast.
|
||||||
|
GLfloat other[4] = {};
|
||||||
|
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, 5, other);
|
||||||
|
EXPECT_EQ(other[2], 1.0f);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, ScissorBoxRoundTripsPerIndexAndAliasesIndexZero) {
|
||||||
|
Array<GLint, kMaxViewports * 4> written{};
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
written[i * 4 + 0] = static_cast<GLint>(i);
|
||||||
|
written[i * 4 + 1] = static_cast<GLint>(i * 2);
|
||||||
|
written[i * 4 + 2] = static_cast<GLint>(16 + i);
|
||||||
|
written[i * 4 + 3] = static_cast<GLint>(8 + i);
|
||||||
|
}
|
||||||
|
MG_Impl::GLImpl::ScissorArrayv(0, kMaxViewports, written.data());
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
GLint readBack[4] = {};
|
||||||
|
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, i, readBack);
|
||||||
|
for (int c = 0; c < 4; ++c) {
|
||||||
|
EXPECT_EQ(readBack[c], written[i * 4 + c]) << "index " << i << " component " << c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indexed writes stay indexed; both spellings.
|
||||||
|
MG_Impl::GLImpl::ScissorIndexed(4, 4, 4, 8, 8);
|
||||||
|
const GLint indexedV[4] = {9, 9, 12, 12};
|
||||||
|
MG_Impl::GLImpl::ScissorIndexedv(7, indexedV);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
GLint probe[4] = {};
|
||||||
|
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 4, probe);
|
||||||
|
EXPECT_EQ(probe[2], 8);
|
||||||
|
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 7, probe);
|
||||||
|
EXPECT_EQ(probe[2], 12);
|
||||||
|
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 5, probe);
|
||||||
|
EXPECT_EQ(probe[2], static_cast<GLint>(16 + 5)) << "index 5 must be untouched";
|
||||||
|
|
||||||
|
// glScissor writes every rectangle, and glGetIntegerv(GL_SCISSOR_BOX) reports rectangle 0.
|
||||||
|
MG_Impl::GLImpl::Scissor(2, 3, 5, 6);
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, i, probe);
|
||||||
|
EXPECT_EQ(probe[0], 2) << "index " << i;
|
||||||
|
EXPECT_EQ(probe[2], 5) << "index " << i;
|
||||||
|
}
|
||||||
|
GLint classic[4] = {};
|
||||||
|
MG_Impl::GLImpl::GetIntegerv(GL_SCISSOR_BOX, classic);
|
||||||
|
EXPECT_EQ(classic[2], 5);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, DepthRangeRoundTripsPerIndexAndAliasesIndexZero) {
|
||||||
|
Array<GLdouble, kMaxViewports * 2> written{};
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
// Exact binary fractions, like the CTS uses: a float-backed store round-trips them.
|
||||||
|
written[i * 2 + 0] = static_cast<GLdouble>(i) / 16.0;
|
||||||
|
written[i * 2 + 1] = 1.0 - static_cast<GLdouble>(i) / 16.0;
|
||||||
|
}
|
||||||
|
MG_Impl::GLImpl::DepthRangeArrayv(0, kMaxViewports, written.data());
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
|
||||||
|
const auto readBack = ReadAllDepthRanges();
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
EXPECT_EQ(readBack[i][0], written[i * 2 + 0]) << "index " << i;
|
||||||
|
EXPECT_EQ(readBack[i][1], written[i * 2 + 1]) << "index " << i;
|
||||||
|
}
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::DepthRangeIndexed(9, 0.25, 0.75);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
GLdouble probe[2] = {};
|
||||||
|
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, 9, probe);
|
||||||
|
EXPECT_EQ(probe[0], 0.25);
|
||||||
|
EXPECT_EQ(probe[1], 0.75);
|
||||||
|
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, 8, probe);
|
||||||
|
EXPECT_EQ(probe[0], 8.0 / 16.0) << "index 8 must be untouched";
|
||||||
|
|
||||||
|
GLfloat asFloat[2] = {};
|
||||||
|
MG_Impl::GLImpl::GetFloati_v(GL_DEPTH_RANGE, 9, asFloat);
|
||||||
|
EXPECT_EQ(asFloat[0], 0.25f);
|
||||||
|
EXPECT_EQ(asFloat[1], 0.75f);
|
||||||
|
|
||||||
|
// glDepthRange writes every range; glGetDoublev(GL_DEPTH_RANGE) reports range 0.
|
||||||
|
MG_Impl::GLImpl::DepthRange(0.0, 1.0);
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, i, probe);
|
||||||
|
EXPECT_EQ(probe[0], 0.0) << "index " << i;
|
||||||
|
EXPECT_EQ(probe[1], 1.0) << "index " << i;
|
||||||
|
}
|
||||||
|
MG_Impl::GLImpl::DepthRangeIndexed(0, 0.125, 0.875);
|
||||||
|
GLdouble classic[2] = {};
|
||||||
|
MG_Impl::GLImpl::GetDoublev(GL_DEPTH_RANGE, classic);
|
||||||
|
EXPECT_EQ(classic[0], 0.125);
|
||||||
|
EXPECT_EQ(classic[1], 0.875);
|
||||||
|
MG_Impl::GLImpl::DepthRange(0.0, 1.0);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, IndexedRectangleSettersRejectAnOutOfRangeIndex) {
|
||||||
|
const GLfloat viewport[4] = {0.0f, 0.0f, 1.0f, 1.0f};
|
||||||
|
const GLint scissor[4] = {0, 0, 1, 1};
|
||||||
|
|
||||||
|
for (const GLuint index : {kMaxViewports, kMaxViewports + 1}) {
|
||||||
|
MG_Impl::GLImpl::ViewportIndexedf(index, 0.0f, 0.0f, 1.0f, 1.0f);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::ViewportIndexedfv(index, viewport);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::ScissorIndexed(index, 0, 0, 1, 1);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::ScissorIndexedv(index, scissor);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::DepthRangeIndexed(index, 0.0, 1.0);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The last legal index must stay silent - api_errors checks both sides of the boundary.
|
||||||
|
MG_Impl::GLImpl::ViewportIndexedf(kMaxViewports - 1, 0.0f, 0.0f, 1.0f, 1.0f);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
MG_Impl::GLImpl::ScissorIndexed(kMaxViewports - 1, 0, 0, 1, 1);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
MG_Impl::GLImpl::DepthRangeIndexed(kMaxViewports - 1, 0.0, 1.0);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, ArraySettersRejectAnOutOfRangeRangeButAcceptAnExactlyFullOne) {
|
||||||
|
Array<GLfloat, kMaxViewports * 4> viewports{};
|
||||||
|
Array<GLint, kMaxViewports * 4> scissors{};
|
||||||
|
Array<GLdouble, kMaxViewports * 2> depths{};
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
viewports[i * 4 + 2] = 1.0f;
|
||||||
|
viewports[i * 4 + 3] = 1.0f;
|
||||||
|
scissors[i * 4 + 2] = 1;
|
||||||
|
scissors[i * 4 + 3] = 1;
|
||||||
|
depths[i * 2 + 1] = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// first == MAX_VIEWPORTS, and first + count > MAX_VIEWPORTS.
|
||||||
|
MG_Impl::GLImpl::ViewportArrayv(kMaxViewports, 1, viewports.data());
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::ViewportArrayv(1, kMaxViewports, viewports.data());
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::ScissorArrayv(kMaxViewports, 1, scissors.data());
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::ScissorArrayv(1, kMaxViewports, scissors.data());
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::DepthRangeArrayv(kMaxViewports, 1, depths.data());
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::DepthRangeArrayv(1, kMaxViewports, depths.data());
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
// first + count == MAX_VIEWPORTS is LEGAL - the off-by-one an ">=" bound would get wrong,
|
||||||
|
// and one KHR-GL43.viewport_array.api_errors asserts explicitly.
|
||||||
|
MG_Impl::GLImpl::ViewportArrayv(1, kMaxViewports - 1, viewports.data());
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
MG_Impl::GLImpl::ScissorArrayv(1, kMaxViewports - 1, scissors.data());
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
MG_Impl::GLImpl::DepthRangeArrayv(1, kMaxViewports - 1, depths.data());
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
|
||||||
|
// A negative count is GL_INVALID_VALUE and must not be read as a huge unsigned length.
|
||||||
|
MG_Impl::GLImpl::ViewportArrayv(0, -1, viewports.data());
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::ScissorArrayv(0, -1, scissors.data());
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::DepthRangeArrayv(0, -1, depths.data());
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, NegativeExtentsAreRejectedWithoutDisturbingState) {
|
||||||
|
MG_Impl::GLImpl::Viewport(0, 0, 4, 4);
|
||||||
|
MG_Impl::GLImpl::Scissor(0, 0, 4, 4);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::Viewport(0, 0, -1, 1);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::Viewport(0, 0, 1, -1);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::Scissor(0, 0, -1, 1);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::Scissor(0, 0, 1, -1);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
for (GLuint index = 0; index < kMaxViewports; ++index) {
|
||||||
|
MG_Impl::GLImpl::ViewportIndexedf(index, 0.0f, 0.0f, -1.0f, 1.0f);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::ViewportIndexedf(index, 0.0f, 0.0f, 1.0f, -1.0f);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
const GLfloat badW[4] = {0.0f, 0.0f, -1.0f, 1.0f};
|
||||||
|
MG_Impl::GLImpl::ViewportIndexedfv(index, badW);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::ScissorIndexed(index, 0, 0, -1, 1);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
const GLint badH[4] = {0, 0, 1, -1};
|
||||||
|
MG_Impl::GLImpl::ScissorIndexedv(index, badH);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
// The array form must reject the WHOLE call for one bad element, exactly once, and
|
||||||
|
// leave every rectangle alone - api_errors submits a full 16-element array with a
|
||||||
|
// single negative extent and then requires the error queue to hold one entry.
|
||||||
|
Array<GLfloat, kMaxViewports * 4> viewports{};
|
||||||
|
Array<GLint, kMaxViewports * 4> scissors{};
|
||||||
|
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||||
|
viewports[i * 4 + 2] = 1.0f;
|
||||||
|
viewports[i * 4 + 3] = 1.0f;
|
||||||
|
scissors[i * 4 + 2] = 1;
|
||||||
|
scissors[i * 4 + 3] = 1;
|
||||||
|
}
|
||||||
|
viewports[index * 4 + 2] = -1.0f;
|
||||||
|
scissors[index * 4 + 3] = -1;
|
||||||
|
MG_Impl::GLImpl::ViewportArrayv(0, kMaxViewports, viewports.data());
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::ScissorArrayv(0, kMaxViewports, scissors.data());
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing above may have landed.
|
||||||
|
GLint viewport[4] = {};
|
||||||
|
MG_Impl::GLImpl::GetIntegeri_v(GL_VIEWPORT, 0, viewport);
|
||||||
|
EXPECT_EQ(viewport[2], 4);
|
||||||
|
EXPECT_EQ(viewport[3], 4);
|
||||||
|
GLint scissor[4] = {};
|
||||||
|
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 0, scissor);
|
||||||
|
EXPECT_EQ(scissor[2], 4);
|
||||||
|
EXPECT_EQ(scissor[3], 4);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, IndexedRectangleQueriesRejectAnOutOfRangeIndex) {
|
||||||
|
GLint ints[4] = {};
|
||||||
|
GLfloat floats[4] = {};
|
||||||
|
GLdouble doubles[4] = {};
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, kMaxViewports, ints);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, kMaxViewports, floats);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, kMaxViewports, doubles);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, kMaxViewports - 1, ints);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, kMaxViewports - 1, floats);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, kMaxViewports - 1, doubles);
|
||||||
|
ExpectSingleGlError(GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|||||||
@@ -955,6 +955,8 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);
|
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);
|
||||||
const Bool esAtLeast31 = caps.GLESVersion.Major > 3 ||
|
const Bool esAtLeast31 = caps.GLESVersion.Major > 3 ||
|
||||||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1);
|
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1);
|
||||||
|
caps.SupportsDrawIndirect = esAtLeast31 && glesFuncs.glDrawArraysIndirect != nullptr &&
|
||||||
|
glesFuncs.glDrawElementsIndirect != nullptr;
|
||||||
caps.SupportsDrawElementsBaseVertex = (esAtLeast32 || hasDrawElementsBaseVertexExtension) &&
|
caps.SupportsDrawElementsBaseVertex = (esAtLeast32 || hasDrawElementsBaseVertexExtension) &&
|
||||||
glesFuncs.glDrawElementsBaseVertex != nullptr;
|
glesFuncs.glDrawElementsBaseVertex != nullptr;
|
||||||
caps.SupportsComputeShader = esAtLeast31 && glesFuncs.glDispatchCompute != nullptr &&
|
caps.SupportsComputeShader = esAtLeast31 && glesFuncs.glDispatchCompute != nullptr &&
|
||||||
@@ -976,6 +978,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
MGLOG_I(" indexed glColorMaski: %s", caps.SupportsIndexedColorMask ? "yes" : "no");
|
MGLOG_I(" indexed glColorMaski: %s", caps.SupportsIndexedColorMask ? "yes" : "no");
|
||||||
MGLOG_I(" dual-source blend (EXT_blend_func_extended): %s",
|
MGLOG_I(" dual-source blend (EXT_blend_func_extended): %s",
|
||||||
caps.SupportsDualSourceBlend ? "yes" : "no");
|
caps.SupportsDualSourceBlend ? "yes" : "no");
|
||||||
|
MGLOG_I(" draw indirect (ES 3.1 core): %s", caps.SupportsDrawIndirect ? "yes" : "no");
|
||||||
MGLOG_I(" multi-draw indirect (EXT_multi_draw_indirect): %s",
|
MGLOG_I(" multi-draw indirect (EXT_multi_draw_indirect): %s",
|
||||||
caps.SupportsMultiDrawIndirect ? "yes" : "no");
|
caps.SupportsMultiDrawIndirect ? "yes" : "no");
|
||||||
MGLOG_I(" multi-draw base vertex (EXT/OES_draw_elements_base_vertex + EXT_multi_draw_arrays): %s",
|
MGLOG_I(" multi-draw base vertex (EXT/OES_draw_elements_base_vertex + EXT_multi_draw_arrays): %s",
|
||||||
@@ -1000,7 +1003,11 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
GLfloat smoothLineWidthRange[2] = {1.0f, 1.0f};
|
GLfloat smoothLineWidthRange[2] = {1.0f, 1.0f};
|
||||||
GLfloat smoothLineWidthGranularity = 1.0f;
|
GLfloat smoothLineWidthGranularity = 1.0f;
|
||||||
GLfloat aliasedPointSizeRange[2] = {1.0f, 1.0f};
|
GLfloat aliasedPointSizeRange[2] = {1.0f, 1.0f};
|
||||||
GLfloat viewportBoundsRange[2] = {0.0f, 0.0f};
|
// GL 4.6 core table 23.60 sets the MINIMUM VIEWPORT_BOUNDS_RANGE at [-32768, 32767], and
|
||||||
|
// KHR-GL43.viewport_array.queries asserts exactly that floor. GLES has no such query, so
|
||||||
|
// the glGetFloatv below raises GL_INVALID_ENUM and leaves this untouched - starting it at
|
||||||
|
// {0, 0} advertised a range that admits no viewport origin at all.
|
||||||
|
GLfloat viewportBoundsRange[2] = {-32768.0f, 32767.0f};
|
||||||
GLint maxViewportDims[2] = {16384, 16384};
|
GLint maxViewportDims[2] = {16384, 16384};
|
||||||
GLint viewportSubpixelBits = 0;
|
GLint viewportSubpixelBits = 0;
|
||||||
GLint max3DTextureSize = 16384;
|
GLint max3DTextureSize = 16384;
|
||||||
@@ -1289,8 +1296,12 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.MaxViewports = maxViewports;
|
caps.MaxViewports = maxViewports;
|
||||||
caps.MaxViewportWidth = maxViewportDims[0];
|
caps.MaxViewportWidth = maxViewportDims[0];
|
||||||
caps.MaxViewportHeight = maxViewportDims[1];
|
caps.MaxViewportHeight = maxViewportDims[1];
|
||||||
caps.ViewportBoundsRangeMin = viewportBoundsRange[0];
|
// Only ever WIDER than the core minimum: a driver that answered the query is allowed to
|
||||||
caps.ViewportBoundsRangeMax = viewportBoundsRange[1];
|
// exceed the floor but never to sit inside it, and a driver that rejected the query left
|
||||||
|
// the floor in place. Written as a clamp rather than a plain assignment so a partial
|
||||||
|
// write (one component answered, the other not) cannot narrow the range either.
|
||||||
|
caps.ViewportBoundsRangeMin = std::min(viewportBoundsRange[0], -32768.0f);
|
||||||
|
caps.ViewportBoundsRangeMax = std::max(viewportBoundsRange[1], 32767.0f);
|
||||||
caps.ViewportSubpixelBits = viewportSubpixelBits;
|
caps.ViewportSubpixelBits = viewportSubpixelBits;
|
||||||
caps.MinFragmentInterpolationOffset =
|
caps.MinFragmentInterpolationOffset =
|
||||||
std::isfinite(minFragmentInterpolationOffset) && minFragmentInterpolationOffset <= -0.5f
|
std::isfinite(minFragmentInterpolationOffset) && minFragmentInterpolationOffset <= -0.5f
|
||||||
|
|||||||
@@ -1149,6 +1149,10 @@ namespace MobileGL {
|
|||||||
// GLES 3.2 core or GL_OES_shader_multisample_interpolation exposes
|
// GLES 3.2 core or GL_OES_shader_multisample_interpolation exposes
|
||||||
// interpolateAtOffset and the three fragment-offset limit queries.
|
// interpolateAtOffset and the three fragment-offset limit queries.
|
||||||
Bool SupportsShaderMultisampleInterpolation = false;
|
Bool SupportsShaderMultisampleInterpolation = false;
|
||||||
|
// ES 3.1+ exposes glDrawArraysIndirect / glDrawElementsIndirect in core. Keep the
|
||||||
|
// version and both entry-point checks together so extension advertisement and the
|
||||||
|
// DirectGLES dispatch path cannot disagree on whether native indirect draws exist.
|
||||||
|
Bool SupportsDrawIndirect = false;
|
||||||
// GL_EXT_multi_draw_indirect is present AND glMultiDrawArraysIndirectEXT /
|
// GL_EXT_multi_draw_indirect is present AND glMultiDrawArraysIndirectEXT /
|
||||||
// glMultiDrawElementsIndirectEXT both resolved. Multi-draw is not core in any ES
|
// glMultiDrawElementsIndirectEXT both resolved. Multi-draw is not core in any ES
|
||||||
// version, and eglGetProcAddress may return a live-looking stub on drivers without
|
// version, and eglGetProcAddress may return a live-looking stub on drivers without
|
||||||
|
|||||||
@@ -1168,7 +1168,9 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
backendApiVersionString = MG_Backend::DirectGLES::FormatBackendAPIVersionString(
|
backendApiVersionString = MG_Backend::DirectGLES::FormatBackendAPIVersionString(
|
||||||
summary.caps.GLESRendererString, summary.caps.GLESVersion.Major, summary.caps.GLESVersion.Minor);
|
summary.caps.GLESRendererString, summary.caps.GLESVersion.Major, summary.caps.GLESVersion.Minor);
|
||||||
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectGLES::BuildAdvertisedExtensions(
|
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectGLES::BuildAdvertisedExtensions(
|
||||||
summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy));
|
summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy,
|
||||||
|
summary.caps.SupportsDrawIndirect,
|
||||||
|
summary.caps.SupportsDrawIndirect && summary.caps.SupportsBaseInstance));
|
||||||
}
|
}
|
||||||
AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString,
|
AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString,
|
||||||
advertisedExtensions);
|
advertisedExtensions);
|
||||||
@@ -1461,6 +1463,8 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
Bool shaderSubgroupUsable = false;
|
Bool shaderSubgroupUsable = false;
|
||||||
Bool timerQueriesSupported = false;
|
Bool timerQueriesSupported = false;
|
||||||
Bool samplerAnisotropySupported = false;
|
Bool samplerAnisotropySupported = false;
|
||||||
|
Bool drawIndirectFirstInstanceSupported = false;
|
||||||
|
Bool shaderDrawParametersSupported = false;
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -1744,6 +1748,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
VkPhysicalDeviceFeatures features{};
|
VkPhysicalDeviceFeatures features{};
|
||||||
vkGetPhysicalDeviceFeaturesFn(physicalDevice, &features);
|
vkGetPhysicalDeviceFeaturesFn(physicalDevice, &features);
|
||||||
summary.samplerAnisotropySupported = features.samplerAnisotropy == VK_TRUE;
|
summary.samplerAnisotropySupported = features.samplerAnisotropy == VK_TRUE;
|
||||||
|
summary.drawIndirectFirstInstanceSupported = features.drawIndirectFirstInstance == VK_TRUE;
|
||||||
if (features.multiDrawIndirect == VK_TRUE) {
|
if (features.multiDrawIndirect == VK_TRUE) {
|
||||||
builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands");
|
builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands");
|
||||||
} else {
|
} else {
|
||||||
@@ -1910,6 +1915,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
builder.Warn("shaderDrawParameters",
|
builder.Warn("shaderDrawParameters",
|
||||||
"unavailable; shaders using gl_DrawID/gl_BaseInstance will not work");
|
"unavailable; shaders using gl_DrawID/gl_BaseInstance will not work");
|
||||||
}
|
}
|
||||||
|
summary.shaderDrawParametersSupported = shaderDrawParameters;
|
||||||
|
|
||||||
Bool provokingVertexLast = false;
|
Bool provokingVertexLast = false;
|
||||||
Bool transformFeedbackPreservesProvokingVertex = false;
|
Bool transformFeedbackPreservesProvokingVertex = false;
|
||||||
@@ -2108,7 +2114,8 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
backendApiVersionString = MG_Backend::DirectVulkan::FormatBackendAPIVersionString(
|
backendApiVersionString = MG_Backend::DirectVulkan::FormatBackendAPIVersionString(
|
||||||
summary.deviceName, summary.apiVersionString, summary.driverVersionString);
|
summary.deviceName, summary.apiVersionString, summary.driverVersionString);
|
||||||
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(
|
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(
|
||||||
summary.shaderSubgroupUsable, summary.timerQueriesSupported, summary.samplerAnisotropySupported));
|
summary.shaderSubgroupUsable, summary.timerQueriesSupported, summary.samplerAnisotropySupported,
|
||||||
|
summary.drawIndirectFirstInstanceSupported && summary.shaderDrawParametersSupported));
|
||||||
}
|
}
|
||||||
AppendMobileGLReportedRows(builder, MG_Backend::DirectVulkan::GetRendererIdentity(), backendApiVersionString,
|
AppendMobileGLReportedRows(builder, MG_Backend::DirectVulkan::GetRendererIdentity(), backendApiVersionString,
|
||||||
advertisedExtensions);
|
advertisedExtensions);
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ namespace MobileGL {
|
|||||||
"imageAtomicXor", "imageLoad", "imageSize", "imageStore", "imulExtended",
|
"imageAtomicXor", "imageLoad", "imageSize", "imageStore", "imulExtended",
|
||||||
"intBitsToFloat", "interpolateAtCentroid", "interpolateAtOffset",
|
"intBitsToFloat", "interpolateAtCentroid", "interpolateAtOffset",
|
||||||
"interpolateAtSample", "inverse", "inversesqrt", "isinf", "isnan",
|
"interpolateAtSample", "inverse", "inversesqrt", "isinf", "isnan",
|
||||||
"ldexp", "length", "lessThan", "lessThanEqual", "log", "log2",
|
"ldexp", "length", "length_squared", "lessThan", "lessThanEqual", "log", "log2",
|
||||||
"matrixCompMult", "max", "max3", "memoryBarrier",
|
"matrixCompMult", "max", "max3", "memoryBarrier",
|
||||||
"memoryBarrierAtomicCounter", "memoryBarrierBuffer", "memoryBarrierImage",
|
"memoryBarrierAtomicCounter", "memoryBarrierBuffer", "memoryBarrierImage",
|
||||||
"memoryBarrierShared", "mid3", "min", "min3", "mix", "mod", "modf",
|
"memoryBarrierShared", "mid3", "min", "min3", "mix", "mod", "modf",
|
||||||
|
|||||||
+15
-10
@@ -19,23 +19,27 @@ namespace MobileGL {
|
|||||||
namespace MG_Util {
|
namespace MG_Util {
|
||||||
namespace ShaderTranspiler {
|
namespace ShaderTranspiler {
|
||||||
namespace {
|
namespace {
|
||||||
constexpr const char* kConflictingName = "sampler";
|
const char* GetCompatName(StringView name) {
|
||||||
constexpr const char* kCompatName = "MGL_COMPAT_sampler";
|
if (name == "sampler") return "MGL_COMPAT_sampler";
|
||||||
|
if (name == "new") return "MGL_COMPAT_new";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
Bool IsNamedSamplerFunctionParameter(spvtools::opt::IRContext* context,
|
const char* GetConflictingFunctionParameterCompatName(spvtools::opt::IRContext* context,
|
||||||
spvtools::opt::Instruction& nameInst) {
|
spvtools::opt::Instruction& nameInst) {
|
||||||
if (nameInst.opcode() != spv::Op::OpName || nameInst.NumInOperands() < 2) {
|
if (nameInst.opcode() != spv::Op::OpName || nameInst.NumInOperands() < 2) {
|
||||||
return false;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameInst.GetInOperand(1).AsString() != kConflictingName) {
|
const char* compatName = GetCompatName(nameInst.GetInOperand(1).AsString());
|
||||||
return false;
|
if (compatName == nullptr) {
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* defUseMgr = context->get_def_use_mgr();
|
auto* defUseMgr = context->get_def_use_mgr();
|
||||||
const Uint32 targetId = nameInst.GetSingleWordInOperand(0);
|
const Uint32 targetId = nameInst.GetSingleWordInOperand(0);
|
||||||
const auto* target = defUseMgr->GetDef(targetId);
|
const auto* target = defUseMgr->GetDef(targetId);
|
||||||
return target != nullptr && target->opcode() == spv::Op::OpFunctionParameter;
|
return target != nullptr && target->opcode() == spv::Op::OpFunctionParameter ? compatName : nullptr;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -44,12 +48,13 @@ namespace MobileGL {
|
|||||||
auto* irContext = context();
|
auto* irContext = context();
|
||||||
|
|
||||||
for (auto& debugInst : irContext->debugs2()) {
|
for (auto& debugInst : irContext->debugs2()) {
|
||||||
if (!IsNamedSamplerFunctionParameter(irContext, debugInst)) {
|
const char* compatName = GetConflictingFunctionParameterCompatName(irContext, debugInst);
|
||||||
|
if (compatName == nullptr) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugInst.SetInOperand(
|
debugInst.SetInOperand(
|
||||||
1, spvtools::utils::MakeVector<spvtools::opt::Operand::OperandData>(kCompatName));
|
1, spvtools::utils::MakeVector<spvtools::opt::Operand::OperandData>(compatName));
|
||||||
modified = true;
|
modified = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,22 @@ namespace MobileGL {
|
|||||||
inline UniquePtr<T> MakeUnique(Args&&... args) {
|
inline UniquePtr<T> MakeUnique(Args&&... args) {
|
||||||
return std::make_unique<T>(std::forward<Args>(args)...);
|
return std::make_unique<T>(std::forward<Args>(args)...);
|
||||||
}
|
}
|
||||||
|
// RAII owner for the one-shot XXH64 state used by the Vulkan cache hashers.
|
||||||
|
// The previous `static inline XXH64_state_t*` form allocated five states per
|
||||||
|
// process and never called XXH64_freeState; a destructor here is independent of
|
||||||
|
// Vulkan/glslang teardown, so it is safe at static destruction time.
|
||||||
|
class XXH64State {
|
||||||
|
public:
|
||||||
|
XXH64State() : m_state(XXH64_createState()) {}
|
||||||
|
~XXH64State() { XXH64_freeState(m_state); }
|
||||||
|
XXH64State(const XXH64State&) = delete;
|
||||||
|
XXH64State& operator=(const XXH64State&) = delete;
|
||||||
|
|
||||||
|
XXH64_state_t* Get() const { return m_state; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
XXH64_state_t* m_state = nullptr;
|
||||||
|
};
|
||||||
using SizeT = std::size_t;
|
using SizeT = std::size_t;
|
||||||
template <typename T, SizeT N>
|
template <typename T, SizeT N>
|
||||||
using Array = std::array<T, N>;
|
using Array = std::array<T, N>;
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ The bundled fixtures cover:
|
|||||||

|

|
||||||
- minecraft-1.21.4-main-menu: captured from Minecraft 1.21.4's main menu.
|
- minecraft-1.21.4-main-menu: captured from Minecraft 1.21.4's main menu.
|
||||||

|

|
||||||
|
- minecraft-1.21.11-main-menu: captured from Minecraft 1.21.11's main menu on a Pixel 8 Pro through FCL MobileGL.
|
||||||
|

|
||||||
- minecraft-1.17-main-menu-854: captured from Minecraft 1.17's 854x480 main menu through FCL MobileGL capture.
|
- minecraft-1.17-main-menu-854: captured from Minecraft 1.17's 854x480 main menu through FCL MobileGL capture.
|
||||||

|

|
||||||
- minecraft-1.21.4-in-world: captured from Minecraft 1.21.4 after entering a singleplayer world.
|
- minecraft-1.21.4-in-world: captured from Minecraft 1.21.4 after entering a singleplayer world.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -40,6 +40,13 @@
|
|||||||
"target_call": 481787,
|
"target_call": 481787,
|
||||||
"timeout_seconds": 180
|
"timeout_seconds": 180
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "minecraft-1.21.11-main-menu",
|
||||||
|
"trace_archive": "minecraft-1.21.11-main-menu.tgz",
|
||||||
|
"golden": "minecraft-1.21.11-main-menu.0000205347.png",
|
||||||
|
"target_call": 205347,
|
||||||
|
"timeout_seconds": 180
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "minecraft-1.17-main-menu-854",
|
"name": "minecraft-1.17-main-menu-854",
|
||||||
"trace_archive": "minecraft-1.17-main-menu-854.tgz",
|
"trace_archive": "minecraft-1.17-main-menu-854.tgz",
|
||||||
|
|||||||
Reference in New Issue
Block a user