mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
Compare commits
3
Commits
ebc5bff9b1
...
867fe3e0ef
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
867fe3e0ef | ||
|
|
0ec487c993 | ||
|
|
23b880c8be |
+4
-1
@@ -22,7 +22,9 @@ if (ANDROID)
|
||||
set(MOBILEGL_BUILD_BENCHMARK OFF CACHE BOOL "Build MobileGL benchmarks" FORCE)
|
||||
endif()
|
||||
|
||||
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT)
|
||||
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO" OFF)
|
||||
|
||||
if ((NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT) AND MOBILEGL_ENABLE_LTO)
|
||||
# Check if ThinLTO or LTO is suppported
|
||||
include(CheckIPOSupported)
|
||||
include(CheckCCompilerFlag)
|
||||
@@ -248,6 +250,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/Utils.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
|
||||
|
||||
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
|
||||
MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp
|
||||
|
||||
@@ -39,6 +39,23 @@ namespace MobileGL::MG_Config {
|
||||
Unroll, // one vkCmdDraw* per sub-draw
|
||||
};
|
||||
|
||||
// Preferred DirectGLES emulation tier for glMultiDrawElements(BaseVertex). GLES has no
|
||||
// such entry point in core, so every tier below is an emulation; they differ only in
|
||||
// which driver capability they lean on and how many driver calls a batch costs. Like
|
||||
// the Magma knob this is a preference, clamped at resolution time to what the ES
|
||||
// driver actually supports, with one log line when it falls back.
|
||||
enum class GLESMultiDrawMode : Uint8 {
|
||||
Auto = 0, // unset: best supported tier
|
||||
Ext, // one glMultiDrawElementsBaseVertexEXT
|
||||
MultiIndirect, // one glMultiDrawElementsIndirectEXT over a scratch command buffer
|
||||
Indirect, // one glDrawElementsIndirect per sub-draw over that same buffer
|
||||
BaseVertex, // one glDrawElementsBaseVertex per sub-draw
|
||||
DrawElements, // baseVertex folded into a scratch index buffer on the CPU, then plain
|
||||
// glDrawElements per sub-draw (for drivers with no base-vertex draw at all)
|
||||
Compute, // a compute shader flattens every sub-draw into one rebased index buffer,
|
||||
// drawn by a single glDrawElements
|
||||
};
|
||||
|
||||
// Feature toggles parsed once from environment variables in MG_ConfigLoader::Init()
|
||||
// (ConfigLoader.cpp), before the accepted-env map is destroyed. All Bool fields share
|
||||
// one truthy rule: the variable is set, non-empty, not "0", and not "false"
|
||||
@@ -105,6 +122,11 @@ namespace MobileGL::MG_Config {
|
||||
// ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support;
|
||||
// unset picks the best supported tier.
|
||||
MultiDrawMode MagmaMultiDrawMode = MultiDrawMode::Auto;
|
||||
// MOBILEGL_ESPRYT_MULTIDRAW_MODE: preferred DirectGLES glMultiDrawElements emulation
|
||||
// tier ("ext" | "multiindirect" | "indirect" | "basevertex" | "drawelements" |
|
||||
// "compute", see GLESMultiDrawMode). Clamped to driver support; unset picks the best
|
||||
// supported tier, which never includes "compute" - see the note on its resolution.
|
||||
GLESMultiDrawMode EsprytMultiDrawMode = GLESMultiDrawMode::Auto;
|
||||
};
|
||||
extern FeaturesTable Features;
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
@@ -116,6 +116,28 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
return MG_Config::MultiDrawMode::Auto;
|
||||
}
|
||||
|
||||
// Same contract as QueryEnvMultiDrawMode, over the DirectGLES tier names.
|
||||
inline MG_Config::GLESMultiDrawMode QueryEnvGLESMultiDrawMode(const String& key) {
|
||||
auto it = acceptedEnvVariablesMap->find(key);
|
||||
if (it == acceptedEnvVariablesMap->end()) {
|
||||
return MG_Config::GLESMultiDrawMode::Auto;
|
||||
}
|
||||
String lowered = it->second;
|
||||
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
if (lowered == "ext") return MG_Config::GLESMultiDrawMode::Ext;
|
||||
if (lowered == "multiindirect") return MG_Config::GLESMultiDrawMode::MultiIndirect;
|
||||
if (lowered == "indirect") return MG_Config::GLESMultiDrawMode::Indirect;
|
||||
if (lowered == "basevertex") return MG_Config::GLESMultiDrawMode::BaseVertex;
|
||||
if (lowered == "drawelements") return MG_Config::GLESMultiDrawMode::DrawElements;
|
||||
if (lowered == "compute") return MG_Config::GLESMultiDrawMode::Compute;
|
||||
if (lowered.empty() || lowered == "auto") return MG_Config::GLESMultiDrawMode::Auto;
|
||||
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected "
|
||||
"ext|multiindirect|indirect|basevertex|drawelements|compute|auto, using auto",
|
||||
key.c_str(), it->second.c_str());
|
||||
return MG_Config::GLESMultiDrawMode::Auto;
|
||||
}
|
||||
|
||||
inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
|
||||
auto it = acceptedEnvVariablesMap->find(key);
|
||||
if (it == acceptedEnvVariablesMap->end()) {
|
||||
@@ -158,6 +180,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
|
||||
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
||||
}
|
||||
|
||||
inline void InitBackendType() {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "MG_Util/Types.h"
|
||||
#include "Utils.h"
|
||||
#include "Managers.h"
|
||||
#include "MultiDraw.h"
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Classifiers/TextureEnumClassifier.h>
|
||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||
@@ -162,37 +163,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
enum class DrawSyncBit : Uint32 {
|
||||
None = 0,
|
||||
IndexBuffer = 1 << 0,
|
||||
IndirectBuffer = 1 << 1,
|
||||
Instancing = 1 << 2
|
||||
};
|
||||
|
||||
inline DrawSyncBit operator|(DrawSyncBit a, DrawSyncBit b) {
|
||||
return static_cast<DrawSyncBit>(static_cast<uint32_t>(a) | static_cast<uint32_t>(b));
|
||||
}
|
||||
|
||||
inline DrawSyncBit& operator|=(DrawSyncBit& a, DrawSyncBit b) {
|
||||
a = a | b;
|
||||
return a;
|
||||
}
|
||||
|
||||
struct DrawElementsIndirectCommand {
|
||||
Uint32 count = 0;
|
||||
Uint32 instanceCount = 0;
|
||||
Uint32 firstIndex = 0;
|
||||
Int32 baseVertex = 0;
|
||||
Uint32 baseInstance = 0;
|
||||
};
|
||||
|
||||
struct DrawArraysIndirectCommand {
|
||||
Uint32 count = 0;
|
||||
Uint32 instanceCount = 0;
|
||||
Uint32 first = 0;
|
||||
Uint32 baseInstance = 0;
|
||||
};
|
||||
|
||||
SamplerImpl::BackendSamplerObject* GetRawDepthFetchSampler() {
|
||||
if (!g_rawDepthFetchSamplerState) {
|
||||
g_rawDepthFetchSamplerState = MakeShared<MG_State::GLState::SamplerObject>(0);
|
||||
@@ -787,6 +757,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glTransformFeedbackVaryings != nullptr;
|
||||
}
|
||||
|
||||
Bool IsCaptureSpanOpen() {
|
||||
const auto& xfb = CurrentXfb();
|
||||
return (xfb.pending || xfb.started) && !xfb.paused;
|
||||
}
|
||||
|
||||
void BeginTransformFeedback(GLenum primitiveMode) {
|
||||
if (!AreTransformFeedbacksSupported()) return;
|
||||
auto& xfb = CurrentXfb();
|
||||
@@ -2071,7 +2046,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys,
|
||||
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram);
|
||||
|
||||
void PrepareForDraw(DrawSyncBit syncBit) {
|
||||
void PrepareForDraw(DrawSyncFlags syncBit) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
@@ -2774,6 +2749,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
Bool CurrentProgramReadsDrawID() {
|
||||
const auto program = GetCurrentBackendProgram();
|
||||
return program != nullptr && program->ReadsDrawID();
|
||||
}
|
||||
|
||||
static Bool SupportsNativeIndirectDraws() {
|
||||
const auto& version = g_GLESCapabilities.GLESVersion;
|
||||
const Bool esVersionOk = version.Major > 3 || (version.Major == 3 && version.Minor >= 1);
|
||||
@@ -3080,7 +3060,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
||||
DebugImpl::OpenGLScopeMarker marker(__func__);
|
||||
#endif
|
||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
||||
PrepareForDraw(syncBit);
|
||||
CheckPrimitiveRestartSupported(type);
|
||||
g_GLESFuncs.glDrawElements(mode, count, type, indices);
|
||||
@@ -3090,7 +3070,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
||||
DebugImpl::OpenGLScopeMarker marker(__func__);
|
||||
#endif
|
||||
DrawSyncBit syncBit = DrawSyncBit::None;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::None;
|
||||
PrepareForDraw(syncBit);
|
||||
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (currentVAO) {
|
||||
@@ -3106,7 +3086,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
||||
DebugImpl::OpenGLScopeMarker marker(__func__);
|
||||
#endif
|
||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
||||
PrepareForDraw(syncBit);
|
||||
CheckPrimitiveRestartSupported(type);
|
||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count, type, indices, basevertex);
|
||||
@@ -3116,7 +3096,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
||||
DebugImpl::OpenGLScopeMarker marker(__func__);
|
||||
#endif
|
||||
DrawSyncBit syncBit = DrawSyncBit::None;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::None;
|
||||
PrepareForDraw(syncBit);
|
||||
|
||||
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
||||
@@ -3132,18 +3112,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
// Both glMultiDrawElements entry points are emulated - ES has neither in core - by the
|
||||
// tier ladder in MultiDraw.cpp, which owns the draw preparation too (its compute tier
|
||||
// has to dispatch before the draw state is established). The only difference between
|
||||
// them is whether the batch carries per-sub-draw base vertices.
|
||||
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount) {
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
||||
DebugImpl::OpenGLScopeMarker marker(__func__);
|
||||
#endif
|
||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer;
|
||||
PrepareForDraw(syncBit);
|
||||
CheckPrimitiveRestartSupported(type);
|
||||
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
g_GLESFuncs.glDrawElements(mode, count[i], type, indices[i]);
|
||||
}
|
||||
MultiDrawImpl::DrawElementsBatch(mode, count, type, indices, drawcount, nullptr);
|
||||
}
|
||||
|
||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
@@ -3151,20 +3129,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
||||
DebugImpl::OpenGLScopeMarker marker(__func__);
|
||||
#endif
|
||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer;
|
||||
PrepareForDraw(syncBit);
|
||||
CheckPrimitiveRestartSupported(type);
|
||||
|
||||
// Gate on the capability flag, never on the entry-point pointer: eglGetProcAddress
|
||||
// returns a non-NULL stub for glMultiDrawElementsBaseVertexEXT on drivers without the
|
||||
// extension interaction (NVIDIA ES), and that stub silently drops every draw.
|
||||
if (g_GLESCapabilities.SupportsMultiDrawElementsBaseVertex) {
|
||||
g_GLESFuncs.glMultiDrawElementsBaseVertexEXT(mode, count, type, indices, drawcount, basevertex);
|
||||
return;
|
||||
}
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i], basevertex[i]);
|
||||
}
|
||||
MultiDrawImpl::DrawElementsBatch(mode, count, type, indices, drawcount, basevertex);
|
||||
}
|
||||
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||
@@ -3183,7 +3148,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
|
||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
@@ -3223,7 +3188,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
|
||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
@@ -3282,7 +3247,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
|
||||
DrawSyncBit syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
|
||||
const auto* commandBytes = ResolveIndirectCommandBytes(
|
||||
@@ -3301,20 +3266,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||
const void* indices, GLint basevertex) {
|
||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
||||
PrepareForDraw(syncBit);
|
||||
g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, type, indices, basevertex);
|
||||
}
|
||||
|
||||
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
|
||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
||||
PrepareForDraw(syncBit);
|
||||
g_GLESFuncs.glDrawRangeElements(mode, start, end, count, type, indices);
|
||||
}
|
||||
|
||||
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
|
||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
SetCurrentBaseInstance(baseinstance);
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
|
||||
@@ -3323,14 +3288,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLint basevertex) {
|
||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
|
||||
}
|
||||
|
||||
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLuint baseinstance) {
|
||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
SetCurrentBaseInstance(baseinstance);
|
||||
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||
@@ -3338,13 +3303,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
|
||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||
}
|
||||
|
||||
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
@@ -3368,7 +3333,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||
GLuint baseinstance) {
|
||||
DrawSyncBit syncBit = DrawSyncBit::Instancing;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
SetCurrentBaseInstance(baseinstance);
|
||||
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
|
||||
@@ -3376,13 +3341,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
|
||||
DrawSyncBit syncBit = DrawSyncBit::Instancing;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
|
||||
}
|
||||
|
||||
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
||||
DrawSyncBit syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
|
||||
const auto* commandBytes =
|
||||
@@ -7251,6 +7216,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void DestroyEGLContext() {
|
||||
BufferImpl::OnBackendContextDestroyed();
|
||||
XfbImpl::OnBackendContextDestroyed();
|
||||
MultiDrawImpl::OnBackendContextDestroyed();
|
||||
ScratchFBOImpl::OnBackendContextDestroyed();
|
||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||
VertexArrayImpl::InvalidateVAOBindingCache();
|
||||
|
||||
@@ -182,6 +182,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
namespace XfbImpl {
|
||||
Bool AreTransformFeedbacksSupported();
|
||||
// True while a capture span is open on the current transform feedback object
|
||||
// (frontend Begin seen and not paused), whether or not the deferred driver-side
|
||||
// Begin has been issued yet. Draw paths that would restructure the primitive
|
||||
// stream, or that need to dispatch compute mid-draw, decline while it is set.
|
||||
Bool IsCaptureSpanOpen();
|
||||
void BeginTransformFeedback(GLenum primitiveMode);
|
||||
void EndTransformFeedback();
|
||||
void PauseTransformFeedback();
|
||||
|
||||
@@ -36,6 +36,53 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool InProcessTeardown();
|
||||
void EnsureProcessTeardownSentinel();
|
||||
|
||||
// Which optional pieces of state a draw needs synchronized before it is issued.
|
||||
// Index/indirect buffer syncs and the instancing-related work are skipped for
|
||||
// draws that provably cannot read them.
|
||||
enum class DrawSyncBit : Uint32 {
|
||||
None = 0,
|
||||
IndexBuffer = 1 << 0,
|
||||
IndirectBuffer = 1 << 1,
|
||||
Instancing = 1 << 2
|
||||
};
|
||||
// Deliberately the shared Flags<> rather than hand-written operators for this enum:
|
||||
// a namespace-local operator| here would hide MobileGL::operator|(Bit, Bit) from
|
||||
// every other scoped-enum flag set used inside this namespace.
|
||||
using DrawSyncFlags = Flags<DrawSyncBit>;
|
||||
|
||||
// The GL-defined indirect command layouts, byte-identical to what the driver reads
|
||||
// out of a GL_DRAW_INDIRECT_BUFFER. Also the staging layout the multi-draw emulation
|
||||
// synthesizes commands into.
|
||||
struct DrawElementsIndirectCommand {
|
||||
Uint32 count = 0;
|
||||
Uint32 instanceCount = 0;
|
||||
Uint32 firstIndex = 0;
|
||||
Int32 baseVertex = 0;
|
||||
Uint32 baseInstance = 0;
|
||||
};
|
||||
|
||||
struct DrawArraysIndirectCommand {
|
||||
Uint32 count = 0;
|
||||
Uint32 instanceCount = 0;
|
||||
Uint32 first = 0;
|
||||
Uint32 baseInstance = 0;
|
||||
};
|
||||
|
||||
// Brings the whole draw-relevant frontend state onto the native ES context and binds
|
||||
// the program; every GL draw entry point calls it exactly once before issuing draws.
|
||||
void PrepareForDraw(DrawSyncFlags syncBits);
|
||||
// GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX. Throws when the app enabled
|
||||
// the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index for this index type.
|
||||
void CheckPrimitiveRestartSupported(GLenum indexType);
|
||||
// Feed the current program's gl_BaseInstance / gl_DrawID emulation uniforms. Both are
|
||||
// no-ops when the program does not read the corresponding builtin.
|
||||
void SetCurrentBaseInstance(Uint32 baseInstance);
|
||||
void SetCurrentDrawID(Uint32 drawId);
|
||||
// True when the current program actually reads gl_DrawID, i.e. when a batched
|
||||
// (single driver call) multi-draw tier would have to feed it one value for the whole
|
||||
// batch and would therefore be wrong.
|
||||
Bool CurrentProgramReadsDrawID();
|
||||
|
||||
template <typename StateObject, typename BackendObject>
|
||||
class StateBackendObjectRegistry {
|
||||
public:
|
||||
@@ -866,6 +913,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void SetBaseInstance(Uint32 baseInstance) const;
|
||||
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
|
||||
void SetDrawID(Uint32 drawId) const;
|
||||
// True when the transpiled program kept a gl_DrawID uniform, i.e. SetDrawID
|
||||
// actually reaches a shader read rather than being discarded.
|
||||
Bool ReadsDrawID() const { return m_drawIdUniformLocation >= 0; }
|
||||
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
||||
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
||||
// False when the last SyncToBackend could not produce a usable program (a
|
||||
|
||||
@@ -0,0 +1,894 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "MultiDraw.h"
|
||||
#include "Managers.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
using MG_Config::GLESMultiDrawMode;
|
||||
|
||||
namespace {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Batch shape
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
SizeT IndexTypeSize(GLenum type) {
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_BYTE: return 1;
|
||||
case GL_UNSIGNED_SHORT: return 2;
|
||||
case GL_UNSIGNED_INT: return 4;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// The all-ones value of an index type, which is what GL restarts on once
|
||||
// primitive restart is in play. CheckPrimitiveRestartSupported has already
|
||||
// rejected the arbitrary-index form of GL_PRIMITIVE_RESTART, so an enabled
|
||||
// restart always restarts here and nowhere else.
|
||||
Uint32 RestartSentinelFor(GLenum type) {
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_BYTE: return 0xFFu;
|
||||
case GL_UNSIGNED_SHORT: return 0xFFFFu;
|
||||
default: return 0xFFFFFFFFu;
|
||||
}
|
||||
}
|
||||
|
||||
Bool RestartActive() {
|
||||
return MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||
}
|
||||
|
||||
// Vertices per primitive for the modes whose sub-draws may be concatenated into a
|
||||
// single draw without changing the primitive stream. Zero for strip/loop/fan modes
|
||||
// (concatenation would weld one sub-draw's last primitive to the next sub-draw's
|
||||
// first) and for GL_PATCHES, whose primitive size is dynamic tessellation state.
|
||||
Uint32 ConcatenablePrimitiveSize(GLenum mode) {
|
||||
switch (mode) {
|
||||
case GL_POINTS: return 1;
|
||||
case GL_LINES: return 2;
|
||||
case GL_TRIANGLES: return 3;
|
||||
case GL_LINES_ADJACENCY: return 4;
|
||||
case GL_TRIANGLES_ADJACENCY: return 6;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Beyond this an emulated batch would ask for a scratch allocation measured in
|
||||
// hundreds of megabytes (and the scratch ring never shrinks again); decline and let
|
||||
// a per-sub-draw tier handle it instead of trying and failing inside the driver.
|
||||
constexpr SizeT kMaxFlattenedIndices = SizeT{1} << 24;
|
||||
|
||||
// The flattening dispatch is one invocation per output index. ES 3.1 only
|
||||
// guarantees 65535 work groups per dimension, and exceeding it makes
|
||||
// glDispatchCompute an INVALID_VALUE no-op - which would leave the draw reading an
|
||||
// uninitialised index buffer rather than failing visibly. Cap the tier there
|
||||
// instead of querying: 4.19M indices is far past any real multi-draw batch, and
|
||||
// beyond it the per-sub-draw tiers are the better answer anyway.
|
||||
constexpr SizeT kComputeWorkGroupSize = 64;
|
||||
constexpr SizeT kMaxComputeWorkGroups = 65535;
|
||||
constexpr SizeT kMaxComputeFlattenedIndices = kMaxComputeWorkGroups * kComputeWorkGroupSize;
|
||||
|
||||
Uint BoundDrawIndirectBufferId() {
|
||||
const auto& indirect =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (!indirect) return 0;
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(indirect);
|
||||
return resource ? resource->id : 0;
|
||||
}
|
||||
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& BoundIndexBuffer() {
|
||||
static const SharedPtr<MG_State::GLState::BufferObject> none;
|
||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (!vao) return none;
|
||||
return vao->GetIndexBufferBindingSlot().GetBoundObject();
|
||||
}
|
||||
|
||||
// The GL name PrepareForDraw left on GL_ELEMENT_ARRAY_BUFFER, i.e. what a tier
|
||||
// that swaps in a scratch index buffer has to put back. Restoring the exact name
|
||||
// matters beyond tidiness: the VAO twin memoises that it already synced this
|
||||
// index binding and will not re-issue it on the next draw.
|
||||
Uint BoundIndexBufferId() {
|
||||
const auto& ibo = BoundIndexBuffer();
|
||||
if (!ibo) return 0;
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
|
||||
return resource ? resource->id : 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scratch GL objects
|
||||
//
|
||||
// All of them belong to the ES context and are abandoned (not deleted) when it
|
||||
// dies, exactly like XfbImpl's scatter buffer: the names are the dead context's
|
||||
// to reclaim, and deleting them would target whatever the successor context
|
||||
// handed out for the same name.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct ScratchBuffer {
|
||||
Uint id = 0;
|
||||
SizeT capacity = 0;
|
||||
SizeT cursor = 0; // ring buffers only: next free byte
|
||||
};
|
||||
|
||||
ScratchBuffer g_indirectCommands; // synthesized DrawElementsIndirectCommand array
|
||||
ScratchBuffer g_rebasedIndices; // CPU-rebased index stream
|
||||
ScratchBuffer g_drawInfo; // compute tier: per-sub-draw descriptors
|
||||
ScratchBuffer g_flattenedIndices; // compute tier: flattened index stream
|
||||
|
||||
Uint g_computeProgram = 0;
|
||||
Bool g_computeProgramFailed = false;
|
||||
GLint g_uElementSize = -1;
|
||||
GLint g_uDrawCount = -1;
|
||||
GLint g_uTotalIndices = -1;
|
||||
|
||||
// Reused staging, so a steady stream of batches allocates nothing.
|
||||
Vector<DrawElementsIndirectCommand> g_commandStaging;
|
||||
Vector<Uint32> g_indexStaging;
|
||||
Vector<Uint32> g_drawInfoStaging;
|
||||
Vector<GLint> g_zeroBaseVertices;
|
||||
|
||||
// Everything below stages through GL_ARRAY_BUFFER, the manager-wide staging target
|
||||
// (BufferImpl::TempBufferTarget); binding it disturbs no VAO state.
|
||||
Bool EnsureScratchName(ScratchBuffer& buffer) {
|
||||
if (buffer.id != 0) return true;
|
||||
GLuint id = 0;
|
||||
g_GLESFuncs.glGenBuffers(1, &id);
|
||||
if (id == 0) return false;
|
||||
buffer.id = id;
|
||||
buffer.capacity = 0;
|
||||
buffer.cursor = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Whole-buffer upload, for the two buffers that are read from offset 0 because they
|
||||
// are bound as storage blocks. Respecifies rather than sub-updates: glBufferData
|
||||
// orphans the previous store, so the upload never waits on a dispatch still reading
|
||||
// the old contents out of the same name.
|
||||
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data) {
|
||||
if (bytes == 0) return true;
|
||||
if (!EnsureScratchName(buffer)) return false;
|
||||
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
|
||||
// Grow in powers of two so a batch that creeps up in size stops respecifying.
|
||||
SizeT capacity = buffer.capacity == 0 ? bytes : buffer.capacity;
|
||||
while (capacity < bytes) capacity *= 2;
|
||||
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(capacity), nullptr,
|
||||
GL_STREAM_DRAW);
|
||||
buffer.capacity = capacity;
|
||||
buffer.cursor = 0;
|
||||
if (data) {
|
||||
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ring upload, for the buffers whose consumers can address a byte offset (indirect
|
||||
// commands and rewritten index streams). Respecifying per batch is what an
|
||||
// orphan-every-time scheme costs, and on a desktop-class driver that allocation
|
||||
// dominated the tiers that use these buffers - a multi-draw of 32 sub-draws stages
|
||||
// 640 bytes and paid for a fresh store to hold them. Bump-allocating instead means
|
||||
// one respecify per wrap; every byte between two wraps is written exactly once, so
|
||||
// nothing in flight is overwritten, and the wrap itself orphans.
|
||||
constexpr SizeT kRingAlignment = 16; // >= 4, so both command and uint32-index offsets stay legal
|
||||
constexpr SizeT kMinRingBytes = 1u << 16;
|
||||
|
||||
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data, SizeT& outOffset) {
|
||||
outOffset = 0;
|
||||
if (bytes == 0) return true;
|
||||
if (!EnsureScratchName(buffer)) return false;
|
||||
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
|
||||
|
||||
const SizeT aligned = (bytes + kRingAlignment - 1) & ~(kRingAlignment - 1);
|
||||
if (buffer.capacity < aligned) {
|
||||
SizeT capacity = buffer.capacity == 0 ? kMinRingBytes : buffer.capacity;
|
||||
while (capacity < aligned) capacity *= 2;
|
||||
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(capacity), nullptr,
|
||||
GL_STREAM_DRAW);
|
||||
buffer.capacity = capacity;
|
||||
buffer.cursor = 0;
|
||||
} else if (buffer.cursor + aligned > buffer.capacity) {
|
||||
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(buffer.capacity),
|
||||
nullptr, GL_STREAM_DRAW);
|
||||
buffer.cursor = 0;
|
||||
}
|
||||
|
||||
outOffset = buffer.cursor;
|
||||
if (data) {
|
||||
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, static_cast<GLintptr>(outOffset),
|
||||
static_cast<GLsizeiptr>(bytes), data);
|
||||
}
|
||||
buffer.cursor += aligned;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tier resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Best-first, and measured rather than assumed. MobileGlues orders its own Auto
|
||||
// multiindirect -> indirect -> basevertex; on both ES drivers available here that
|
||||
// is backwards, because staging a command buffer per batch costs more than the
|
||||
// driver entries it saves. mc_sodium_multidraw (132 batches x 32 sub-draws),
|
||||
// ns/op, median of three:
|
||||
//
|
||||
// NVIDIA ES 3.2 Mesa llvmpipe ES 3.2
|
||||
// ext n/a 19300
|
||||
// basevertex 2500 25200
|
||||
// multiindirect 5700 27600
|
||||
// drawelements 5600 28700
|
||||
// indirect 5800 31000
|
||||
//
|
||||
// Ring-allocating the command staging (instead of respecifying per batch) was
|
||||
// tried first and moved the indirect tiers by less than noise, so the cost is the
|
||||
// indirect draw path itself, not the upload. Only "ext" - a real multi-draw entry
|
||||
// point rather than an indirect one - actually beats replaying the sub-draws.
|
||||
//
|
||||
// The compute tier is deliberately absent from the ladder: it rewrites the
|
||||
// primitive stream rather than replaying it, and it measured slowest of all here,
|
||||
// so it stays opt-in behind the env knob (the same call MobileGlues makes - its
|
||||
// Auto never selects Compute either).
|
||||
constexpr GLESMultiDrawMode kAutoLadder[] = {
|
||||
GLESMultiDrawMode::Ext, GLESMultiDrawMode::BaseVertex, GLESMultiDrawMode::MultiIndirect,
|
||||
GLESMultiDrawMode::Indirect, GLESMultiDrawMode::DrawElements,
|
||||
};
|
||||
|
||||
Bool SupportsTier(GLESMultiDrawMode tier) {
|
||||
return IsTierSupported(g_GLESCapabilities, g_GLESFuncs, tier);
|
||||
}
|
||||
|
||||
GLESMultiDrawMode g_resolvedTier = GLESMultiDrawMode::Auto;
|
||||
Bool g_tierResolved = false;
|
||||
String g_tierResolution;
|
||||
|
||||
void ResolveTierOnce() {
|
||||
if (g_tierResolved) return;
|
||||
g_tierResolved = true;
|
||||
g_resolvedTier =
|
||||
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
|
||||
&g_tierResolution);
|
||||
MGLOG_I("DirectGLES multi-draw: %s", g_tierResolution.c_str());
|
||||
}
|
||||
|
||||
// Which tiers have already announced themselves, one bit per GLESMultiDrawMode.
|
||||
// The resolution line above says which tier was CHOSEN; this says which one a
|
||||
// batch actually went through, and the two differ whenever a batch's shape
|
||||
// demotes it. Worth a line each: a multi-draw path that resolves to a tier and
|
||||
// then quietly runs a different one is exactly how "the batch drew nothing"
|
||||
// hides.
|
||||
Uint32 g_announcedTiers = 0;
|
||||
|
||||
void NoteTierExecuted(GLESMultiDrawMode tier) {
|
||||
const Uint32 bit = 1u << static_cast<Uint32>(tier);
|
||||
if (g_announcedTiers & bit) return;
|
||||
g_announcedTiers |= bit;
|
||||
MGLOG_I("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
|
||||
}
|
||||
|
||||
// The tier this particular batch can actually take. A tier is demoted here when
|
||||
// the batch's own shape - not the driver - rules it out; the compute tier keeps
|
||||
// its remaining feasibility checks inside its implementation, where the data it
|
||||
// has to walk is already in hand.
|
||||
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool hasIndexBuffer) {
|
||||
ResolveTierOnce();
|
||||
GLESMultiDrawMode tier = g_resolvedTier;
|
||||
|
||||
// Batched tiers issue one driver entry for the whole batch, so the emulated
|
||||
// gl_DrawID uniform can only hold one value across every sub-draw. A program
|
||||
// that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its
|
||||
// own index (the spec's value); nothing else observes the difference.
|
||||
const Bool batched = tier == GLESMultiDrawMode::Ext || tier == GLESMultiDrawMode::MultiIndirect ||
|
||||
tier == GLESMultiDrawMode::Compute;
|
||||
if (batched && programReadsDrawID) {
|
||||
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
|
||||
: GLESMultiDrawMode::DrawElements;
|
||||
}
|
||||
|
||||
// The indirect tiers describe each sub-draw as an element offset into the
|
||||
// bound element array buffer. A client-memory index array has no such buffer,
|
||||
// and indirect draws are not defined without one.
|
||||
if (!hasIndexBuffer &&
|
||||
(tier == GLESMultiDrawMode::MultiIndirect || tier == GLESMultiDrawMode::Indirect)) {
|
||||
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
|
||||
: GLESMultiDrawMode::DrawElements;
|
||||
}
|
||||
return tier;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Index rewriting, shared by the two tiers that fold base vertices into indices
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Both of those tiers emit GL_UNSIGNED_INT regardless of the source type. Keeping
|
||||
// the source width would be wrong, not merely tight: GL adds baseVertex to the
|
||||
// index at full precision, so a GL_UNSIGNED_SHORT index plus a base vertex past
|
||||
// 65535 addresses a vertex the source type cannot spell. Widening also gives the
|
||||
// rewritten stream a restart sentinel (0xFFFFFFFF) that survives the rebase.
|
||||
void RebaseIndices(const Uint8* source, SizeT sourceIndexCount, SizeT indexSize, Int32 baseVertex,
|
||||
Bool restartActive, Uint32 restartSentinel, Uint32* out) {
|
||||
const Uint32 baseVertexBits = static_cast<Uint32>(baseVertex);
|
||||
for (SizeT i = 0; i < sourceIndexCount; ++i) {
|
||||
Uint32 value = 0;
|
||||
switch (indexSize) {
|
||||
case 1: value = source[i]; break;
|
||||
case 2: {
|
||||
Uint16 narrow = 0;
|
||||
std::memcpy(&narrow, source + i * 2, sizeof(narrow));
|
||||
value = narrow;
|
||||
break;
|
||||
}
|
||||
default: std::memcpy(&value, source + i * 4, sizeof(value)); break;
|
||||
}
|
||||
// Unsigned wraparound is the defined behaviour for a negative base vertex.
|
||||
out[i] = (restartActive && value == restartSentinel) ? 0xFFFFFFFFu : value + baseVertexBits;
|
||||
}
|
||||
}
|
||||
|
||||
// CPU-readable bytes of one sub-draw's indices, from the frontend shadow of the
|
||||
// bound index buffer or straight from the client array. Null when the sub-draw
|
||||
// would read outside the buffer.
|
||||
const Uint8* ResolveSubDrawIndices(const SharedPtr<MG_State::GLState::BufferObject>& indexBuffer,
|
||||
const Uint8* indexBufferBytes, SizeT indexBufferSize, const void* indices,
|
||||
SizeT indexCount, SizeT indexSize) {
|
||||
if (!indexBuffer) {
|
||||
return static_cast<const Uint8*>(indices);
|
||||
}
|
||||
if (!indexBufferBytes) return nullptr;
|
||||
const SizeT byteOffset = reinterpret_cast<SizeT>(indices);
|
||||
const SizeT byteEnd = byteOffset + indexCount * indexSize;
|
||||
if (byteEnd > indexBufferSize || byteEnd < byteOffset) return nullptr;
|
||||
return indexBufferBytes + byteOffset;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tier: Ext - one glMultiDrawElementsBaseVertexEXT
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Bool RunExt(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, GLsizei drawcount,
|
||||
const GLint* basevertex) {
|
||||
if (!SupportsTier(GLESMultiDrawMode::Ext)) return false;
|
||||
const GLint* baseVertices = basevertex;
|
||||
if (!baseVertices) {
|
||||
// glMultiDrawElements: every base vertex is 0, but the entry point still
|
||||
// wants an array. One permanently-zero vector serves every such batch.
|
||||
if (g_zeroBaseVertices.size() < static_cast<SizeT>(drawcount)) {
|
||||
g_zeroBaseVertices.resize(static_cast<SizeT>(drawcount), 0);
|
||||
}
|
||||
baseVertices = g_zeroBaseVertices.data();
|
||||
}
|
||||
g_GLESFuncs.glMultiDrawElementsBaseVertexEXT(mode, count, type, indices, drawcount, baseVertices);
|
||||
NoteTierExecuted(GLESMultiDrawMode::Ext);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tiers: MultiIndirect / Indirect - synthesized indirect commands
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Bool RunIndirect(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID) {
|
||||
if (!SupportsTier(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect)) return false;
|
||||
const SizeT indexSize = IndexTypeSize(type);
|
||||
if (indexSize == 0) return false;
|
||||
// Indirect commands address indices as an element offset into the bound element
|
||||
// array buffer, and an indirect draw is not defined without one.
|
||||
const auto& indexBuffer = BoundIndexBuffer();
|
||||
if (!indexBuffer) return false;
|
||||
|
||||
g_commandStaging.resize(static_cast<SizeT>(drawcount));
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
const SizeT byteOffset = reinterpret_cast<SizeT>(indices[i]);
|
||||
// firstIndex counts elements, so an offset that is not a whole number of
|
||||
// them cannot be expressed as a command at all.
|
||||
if (byteOffset % indexSize != 0) return false;
|
||||
auto& command = g_commandStaging[static_cast<SizeT>(i)];
|
||||
command.count = count[i] > 0 ? static_cast<Uint32>(count[i]) : 0u;
|
||||
command.instanceCount = 1;
|
||||
command.firstIndex = static_cast<Uint32>(byteOffset / indexSize);
|
||||
command.baseVertex = basevertex ? basevertex[i] : 0;
|
||||
command.baseInstance = 0;
|
||||
}
|
||||
|
||||
const SizeT commandBytes = g_commandStaging.size() * sizeof(DrawElementsIndirectCommand);
|
||||
SizeT commandBase = 0;
|
||||
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(), commandBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Every synthesized command carries baseInstance 0. Say so through the direct
|
||||
// path, which also clears the indirect-params word index a preceding real
|
||||
// indirect draw may have left pointing into its own command buffer.
|
||||
SetCurrentBaseInstance(0);
|
||||
|
||||
const Uint previousIndirectBinding = BoundDrawIndirectBufferId();
|
||||
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id);
|
||||
if (batched) {
|
||||
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
|
||||
drawcount, 0);
|
||||
} else {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
|
||||
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
}
|
||||
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, previousIndirectBinding);
|
||||
NoteTierExecuted(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tier: BaseVertex - the per-sub-draw replay
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Bool RunBaseVertexLoop(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) {
|
||||
if (!SupportsTier(GLESMultiDrawMode::BaseVertex)) return false;
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] <= 0) continue;
|
||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||
basevertex ? basevertex[i] : 0);
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
NoteTierExecuted(GLESMultiDrawMode::BaseVertex);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tier: DrawElements - base vertices folded into a scratch index stream
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Bool RunRebasedDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) {
|
||||
const SizeT indexSize = IndexTypeSize(type);
|
||||
if (indexSize == 0) return false;
|
||||
|
||||
SizeT total = 0;
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] > 0) total += static_cast<SizeT>(count[i]);
|
||||
}
|
||||
if (total == 0) return true;
|
||||
if (total > kMaxFlattenedIndices) return false;
|
||||
|
||||
const auto& indexBuffer = BoundIndexBuffer();
|
||||
const Uint8* indexBufferBytes = nullptr;
|
||||
SizeT indexBufferSize = 0;
|
||||
if (indexBuffer) {
|
||||
// The shadow is the source of truth for CPU reads, but a persistent map or
|
||||
// a shader write may have moved past it since the last sync.
|
||||
indexBuffer->SyncPersistentMappedRange();
|
||||
indexBuffer->SyncGpuWrites();
|
||||
indexBufferBytes = indexBuffer->MappedData();
|
||||
indexBufferSize = indexBuffer->GetSize();
|
||||
}
|
||||
|
||||
const Bool restartActive = RestartActive();
|
||||
const Uint32 restartSentinel = RestartSentinelFor(type);
|
||||
g_indexStaging.resize(total);
|
||||
SizeT cursor = 0;
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] <= 0) continue;
|
||||
const SizeT subDrawCount = static_cast<SizeT>(count[i]);
|
||||
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
|
||||
subDrawCount, indexSize);
|
||||
if (!source) {
|
||||
MGLOG_E("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
|
||||
"buffer; skipping the batch",
|
||||
i);
|
||||
return false;
|
||||
}
|
||||
RebaseIndices(source, subDrawCount, indexSize, basevertex ? basevertex[i] : 0, restartActive,
|
||||
restartSentinel, g_indexStaging.data() + cursor);
|
||||
cursor += subDrawCount;
|
||||
}
|
||||
|
||||
SizeT indexBase = 0;
|
||||
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(), indexBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Uint previousIndexBinding = BoundIndexBufferId();
|
||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, g_rebasedIndices.id);
|
||||
cursor = 0;
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] <= 0) continue;
|
||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
|
||||
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
|
||||
cursor += static_cast<SizeT>(count[i]);
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
||||
NoteTierExecuted(GLESMultiDrawMode::DrawElements);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tier: Compute - the whole batch flattened into one rebased index stream
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// One index per invocation. The sub-draw an output slot belongs to is found by
|
||||
// binary search over the inclusive prefix sums of the sub-draw counts, which is
|
||||
// why the descriptors are sorted by construction. Sub-draws with a zero count
|
||||
// repeat the previous prefix sum and are therefore skipped by the search.
|
||||
//
|
||||
// Three storage blocks, not the five the shape suggests: ES 3.1 only guarantees
|
||||
// four per compute stage, so the per-sub-draw descriptors share one buffer.
|
||||
constexpr const char* kFlattenComputeSource = R"(#version 310 es
|
||||
layout(local_size_x = 64) in;
|
||||
|
||||
uniform uint uElementSize;
|
||||
uniform uint uDrawCount;
|
||||
uniform uint uTotalIndices;
|
||||
|
||||
layout(std430, binding = 0) readonly buffer SourceIndices { uint sourceWords[]; };
|
||||
layout(std430, binding = 1) readonly buffer DrawInfo { uint drawInfo[]; };
|
||||
layout(std430, binding = 2) writeonly buffer FlatIndices { uint flatIndices[]; };
|
||||
|
||||
uint ReadSourceIndex(uint element) {
|
||||
if (uElementSize == 4u) {
|
||||
return sourceWords[element];
|
||||
}
|
||||
if (uElementSize == 2u) {
|
||||
uint word = sourceWords[element >> 1u];
|
||||
return (word >> ((element & 1u) * 16u)) & 0xFFFFu;
|
||||
}
|
||||
uint word = sourceWords[element >> 2u];
|
||||
return (word >> ((element & 3u) * 8u)) & 0xFFu;
|
||||
}
|
||||
|
||||
void main() {
|
||||
uint outIndex = gl_GlobalInvocationID.x;
|
||||
if (outIndex >= uTotalIndices) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint low = 0u;
|
||||
uint high = uDrawCount - 1u;
|
||||
while (low < high) {
|
||||
uint mid = low + (high - low) / 2u;
|
||||
if (drawInfo[mid * 3u + 2u] > outIndex) {
|
||||
high = mid;
|
||||
} else {
|
||||
low = mid + 1u;
|
||||
}
|
||||
}
|
||||
|
||||
uint localIndex = outIndex - (low == 0u ? 0u : drawInfo[(low - 1u) * 3u + 2u]);
|
||||
// Unsigned wraparound is the defined behaviour for a negative base vertex. No
|
||||
// restart sentinel handling: the tier declines outright while restart is enabled.
|
||||
flatIndices[outIndex] = ReadSourceIndex(localIndex + drawInfo[low * 3u]) + drawInfo[low * 3u + 1u];
|
||||
}
|
||||
)";
|
||||
|
||||
struct FlattenedStream {
|
||||
Uint bufferId = 0;
|
||||
SizeT indexCount = 0;
|
||||
};
|
||||
|
||||
Bool EnsureComputeProgram() {
|
||||
if (g_computeProgram != 0) return true;
|
||||
if (g_computeProgramFailed) return false;
|
||||
g_computeProgramFailed = true; // cleared again only on a complete success
|
||||
|
||||
const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER);
|
||||
if (shader == 0) {
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
|
||||
return false;
|
||||
}
|
||||
const char* source = kFlattenComputeSource;
|
||||
g_GLESFuncs.glShaderSource(shader, 1, &source, nullptr);
|
||||
g_GLESFuncs.glCompileShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
g_GLESFuncs.glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
if (status != GL_TRUE) {
|
||||
char log[1024] = {};
|
||||
g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
|
||||
g_GLESFuncs.glDeleteShader(shader);
|
||||
return false;
|
||||
}
|
||||
|
||||
const GLuint program = g_GLESFuncs.glCreateProgram();
|
||||
if (program == 0) {
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateProgram failed");
|
||||
g_GLESFuncs.glDeleteShader(shader);
|
||||
return false;
|
||||
}
|
||||
g_GLESFuncs.glAttachShader(program, shader);
|
||||
g_GLESFuncs.glLinkProgram(program);
|
||||
g_GLESFuncs.glDeleteShader(shader);
|
||||
g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
if (status != GL_TRUE) {
|
||||
char log[1024] = {};
|
||||
g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
|
||||
g_GLESFuncs.glDeleteProgram(program);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_computeProgram = program;
|
||||
g_uElementSize = g_GLESFuncs.glGetUniformLocation(program, "uElementSize");
|
||||
g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount");
|
||||
g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices");
|
||||
g_computeProgramFailed = false;
|
||||
MGLOG_I("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Builds the flattened stream, or leaves `out` empty when this batch's shape rules
|
||||
// the tier out. Runs BEFORE PrepareForDraw - see the call site - so it may leave
|
||||
// the compute program current and the first storage points unbound; the
|
||||
// preparation that follows re-establishes both.
|
||||
void FlattenWithCompute(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex, FlattenedStream& out) {
|
||||
if (!SupportsTier(GLESMultiDrawMode::Compute)) return;
|
||||
const SizeT indexSize = IndexTypeSize(type);
|
||||
if (indexSize == 0) return;
|
||||
|
||||
// Merging sub-draws into a single draw only reproduces the original primitive
|
||||
// stream for list-shaped modes: a strip, loop or fan would gain primitives
|
||||
// spanning the seam between two sub-draws.
|
||||
const Uint32 primitiveSize = ConcatenablePrimitiveSize(mode);
|
||||
if (primitiveSize == 0) return;
|
||||
|
||||
// Primitive restart defeats the whole-multiple-of-a-primitive argument below,
|
||||
// even for a list mode. A restart ends the current primitive, so a sub-draw of
|
||||
// six GL_TRIANGLES indices with a restart after the third emits ONE triangle
|
||||
// and drops the two leftover vertices - and once concatenated those leftovers
|
||||
// find a third vertex in the next sub-draw and become a triangle that GL never
|
||||
// draws. Splicing separator sentinels into the flattened stream could fix it,
|
||||
// at the cost of a per-sub-draw offset the prefix-sum layout does not carry;
|
||||
// declining is the honest trade for a tier that is already opt-in.
|
||||
if (RestartActive()) return;
|
||||
|
||||
// The shader reads the source indices as a storage buffer, so there has to be
|
||||
// a real buffer to read - a client-memory index array has none.
|
||||
const auto& indexBuffer = BoundIndexBuffer();
|
||||
if (!indexBuffer) return;
|
||||
|
||||
// A dispatch inside an open capture span is not legal, and the span would also
|
||||
// observe one merged draw rather than the batch it asked for.
|
||||
if (XfbImpl::IsCaptureSpanOpen()) return;
|
||||
|
||||
auto* sourceResource = BufferImpl::EnsureBufferResource(indexBuffer);
|
||||
if (!sourceResource || sourceResource->id == 0) return;
|
||||
const SizeT sourceSize = indexBuffer->GetSize();
|
||||
// std430 addresses the source as uint[]; a tail shorter than a word is not
|
||||
// reachable, so a narrow index type needs a word-multiple buffer.
|
||||
if (indexSize < 4 && (sourceSize % 4) != 0) return;
|
||||
|
||||
g_drawInfoStaging.resize(3 * static_cast<SizeT>(drawcount));
|
||||
SizeT total = 0;
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
const SizeT subDrawCount = count[i] > 0 ? static_cast<SizeT>(count[i]) : 0;
|
||||
// GL drops a trailing partial primitive per sub-draw; concatenation would
|
||||
// instead splice it onto the next sub-draw's first vertices.
|
||||
if (subDrawCount % primitiveSize != 0) return;
|
||||
const SizeT byteOffset = reinterpret_cast<SizeT>(indices[i]);
|
||||
if (byteOffset % indexSize != 0) return;
|
||||
if (subDrawCount != 0) {
|
||||
const SizeT byteEnd = byteOffset + subDrawCount * indexSize;
|
||||
if (byteEnd > sourceSize || byteEnd < byteOffset) return;
|
||||
}
|
||||
total += subDrawCount;
|
||||
if (total > kMaxComputeFlattenedIndices) return;
|
||||
const SizeT slot = 3 * static_cast<SizeT>(i);
|
||||
g_drawInfoStaging[slot] = static_cast<Uint32>(byteOffset / indexSize);
|
||||
g_drawInfoStaging[slot + 1] = static_cast<Uint32>(basevertex ? basevertex[i] : 0);
|
||||
g_drawInfoStaging[slot + 2] = static_cast<Uint32>(total);
|
||||
}
|
||||
if (total == 0) return; // nothing to draw; the ordinary tiers no-op just as well
|
||||
|
||||
if (!EnsureComputeProgram()) return;
|
||||
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data())) {
|
||||
return;
|
||||
}
|
||||
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr)) return;
|
||||
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, sourceResource->id);
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 1, g_drawInfo.id);
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 2, g_flattenedIndices.id);
|
||||
|
||||
g_GLESFuncs.glUseProgram(g_computeProgram);
|
||||
PrgramImpl::g_lastUsedBackendProgramId = g_computeProgram;
|
||||
if (g_uElementSize >= 0) g_GLESFuncs.glUniform1ui(g_uElementSize, static_cast<GLuint>(indexSize));
|
||||
if (g_uDrawCount >= 0) g_GLESFuncs.glUniform1ui(g_uDrawCount, static_cast<GLuint>(drawcount));
|
||||
if (g_uTotalIndices >= 0) g_GLESFuncs.glUniform1ui(g_uTotalIndices, static_cast<GLuint>(total));
|
||||
|
||||
g_GLESFuncs.glDispatchCompute(
|
||||
static_cast<GLuint>((total + kComputeWorkGroupSize - 1) / kComputeWorkGroupSize), 1, 1);
|
||||
g_GLESFuncs.glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT);
|
||||
|
||||
// Hand the storage points back to their GL default. PrepareForDraw re-syncs
|
||||
// only the points the app has actually touched, so leaving a scratch buffer on
|
||||
// an untouched point would keep it visible to the next shader that declares one.
|
||||
for (Uint point = 0; point < 3; ++point) {
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, point, 0);
|
||||
}
|
||||
|
||||
NoteTierExecuted(GLESMultiDrawMode::Compute);
|
||||
out.bufferId = g_flattenedIndices.id;
|
||||
out.indexCount = total;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// -------------------------------------------------------------------------------
|
||||
// Public surface
|
||||
// -------------------------------------------------------------------------------
|
||||
|
||||
Bool IsTierSupported(const MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& funcs,
|
||||
GLESMultiDrawMode tier) {
|
||||
const Bool esAtLeast31 =
|
||||
caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1);
|
||||
switch (tier) {
|
||||
case GLESMultiDrawMode::Ext:
|
||||
return caps.SupportsMultiDrawElementsBaseVertex;
|
||||
case GLESMultiDrawMode::MultiIndirect:
|
||||
return caps.SupportsMultiDrawIndirect && esAtLeast31 && funcs.glDrawElementsIndirect != nullptr;
|
||||
case GLESMultiDrawMode::Indirect:
|
||||
return esAtLeast31 && funcs.glDrawElementsIndirect != nullptr;
|
||||
case GLESMultiDrawMode::BaseVertex:
|
||||
return caps.SupportsDrawElementsBaseVertex;
|
||||
case GLESMultiDrawMode::DrawElements:
|
||||
// Plain glDrawElements over a rewritten index stream: ES 2 core, so this is
|
||||
// the floor every other tier can fall back to.
|
||||
return true;
|
||||
case GLESMultiDrawMode::Compute:
|
||||
// Three storage blocks, which is inside the four ES 3.1 guarantees per stage.
|
||||
return caps.SupportsComputeShader && caps.MaxComputeShaderStorageBlocks >= 3 &&
|
||||
funcs.glBindBufferBase != nullptr;
|
||||
case GLESMultiDrawMode::Auto:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
GLESMultiDrawMode ResolveTier(const MG_External::GLESCapabilities& caps,
|
||||
const MG_External::GLESFunctionsTable& funcs, GLESMultiDrawMode requested,
|
||||
String* explanation) {
|
||||
const auto bestAuto = [&]() {
|
||||
for (const GLESMultiDrawMode tier : kAutoLadder) {
|
||||
if (IsTierSupported(caps, funcs, tier)) return tier;
|
||||
}
|
||||
return GLESMultiDrawMode::DrawElements;
|
||||
};
|
||||
|
||||
GLESMultiDrawMode resolved = GLESMultiDrawMode::DrawElements;
|
||||
String line;
|
||||
if (requested == GLESMultiDrawMode::Auto) {
|
||||
resolved = bestAuto();
|
||||
line = String("auto -> ") + TierName(resolved);
|
||||
} else if (IsTierSupported(caps, funcs, requested)) {
|
||||
resolved = requested;
|
||||
line = String("MOBILEGL_ESPRYT_MULTIDRAW_MODE=") + TierName(requested) + " -> " + TierName(resolved);
|
||||
} else {
|
||||
resolved = bestAuto();
|
||||
line = String("MOBILEGL_ESPRYT_MULTIDRAW_MODE=") + TierName(requested) +
|
||||
" requested but unsupported by this driver -> " + TierName(resolved);
|
||||
}
|
||||
|
||||
if (explanation) {
|
||||
String supported;
|
||||
for (const GLESMultiDrawMode tier : kAutoLadder) {
|
||||
if (!IsTierSupported(caps, funcs, tier)) continue;
|
||||
if (!supported.empty()) supported += ", ";
|
||||
supported += TierName(tier);
|
||||
}
|
||||
if (IsTierSupported(caps, funcs, GLESMultiDrawMode::Compute)) {
|
||||
supported += supported.empty() ? "compute (opt-in)" : ", compute (opt-in)";
|
||||
}
|
||||
*explanation = line + " (driver supports: " + supported + ")";
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
const char* TierName(GLESMultiDrawMode tier) {
|
||||
switch (tier) {
|
||||
case GLESMultiDrawMode::Auto: return "auto";
|
||||
case GLESMultiDrawMode::Ext: return "ext";
|
||||
case GLESMultiDrawMode::MultiIndirect: return "multiindirect";
|
||||
case GLESMultiDrawMode::Indirect: return "indirect";
|
||||
case GLESMultiDrawMode::BaseVertex: return "basevertex";
|
||||
case GLESMultiDrawMode::DrawElements: return "drawelements";
|
||||
case GLESMultiDrawMode::Compute: return "compute";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
GLESMultiDrawMode ResolvedTier() {
|
||||
ResolveTierOnce();
|
||||
return g_resolvedTier;
|
||||
}
|
||||
|
||||
String DescribeTierResolution() {
|
||||
ResolveTierOnce();
|
||||
return g_tierResolution;
|
||||
}
|
||||
|
||||
void OnBackendContextDestroyed() {
|
||||
g_indirectCommands = {};
|
||||
g_rebasedIndices = {};
|
||||
g_drawInfo = {};
|
||||
g_flattenedIndices = {};
|
||||
g_computeProgram = 0;
|
||||
g_computeProgramFailed = false;
|
||||
g_uElementSize = -1;
|
||||
g_uDrawCount = -1;
|
||||
g_uTotalIndices = -1;
|
||||
}
|
||||
|
||||
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex) {
|
||||
if (drawcount <= 0 || !count || !indices) return;
|
||||
// State-independent and possibly throwing, so it runs before any GL work.
|
||||
CheckPrimitiveRestartSupported(type);
|
||||
|
||||
const Bool hasIndexBuffer = BoundIndexBuffer() != nullptr;
|
||||
|
||||
// The compute tier dispatches BEFORE the draw state is established: doing it
|
||||
// afterwards would mean unpicking the program, SSBO and index bindings
|
||||
// PrepareForDraw just made, and a dispatch inside an open transform feedback
|
||||
// span is not legal at all. On success it hands back a flattened index stream.
|
||||
FlattenedStream flattened;
|
||||
if (ResolvedTier() == GLESMultiDrawMode::Compute && !CurrentProgramReadsDrawID()) {
|
||||
FlattenWithCompute(mode, count, type, indices, drawcount, basevertex, flattened);
|
||||
}
|
||||
|
||||
PrepareForDraw(DrawSyncBit::IndexBuffer);
|
||||
|
||||
if (flattened.indexCount != 0) {
|
||||
const Uint previousIndexBinding = BoundIndexBufferId();
|
||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId);
|
||||
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
|
||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
||||
return;
|
||||
}
|
||||
|
||||
const Bool feedDrawID = CurrentProgramReadsDrawID();
|
||||
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, hasIndexBuffer);
|
||||
|
||||
Bool drawn = false;
|
||||
switch (tier) {
|
||||
case GLESMultiDrawMode::Ext:
|
||||
drawn = RunExt(mode, count, type, indices, drawcount, basevertex);
|
||||
break;
|
||||
case GLESMultiDrawMode::MultiIndirect:
|
||||
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID);
|
||||
break;
|
||||
case GLESMultiDrawMode::Indirect:
|
||||
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID);
|
||||
break;
|
||||
case GLESMultiDrawMode::BaseVertex:
|
||||
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||
break;
|
||||
case GLESMultiDrawMode::DrawElements:
|
||||
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||
break;
|
||||
case GLESMultiDrawMode::Compute:
|
||||
// Its pre-pass ran above; reaching here means it declined this batch's shape.
|
||||
break;
|
||||
case GLESMultiDrawMode::Auto:
|
||||
break; // resolution never yields Auto
|
||||
}
|
||||
|
||||
// Every tier above may decline a batch whose shape it cannot express. The two
|
||||
// below are the floor: a base-vertex replay where the driver has one, and the
|
||||
// rewritten index stream where it does not. Both are safe for any batch these
|
||||
// entry points can receive.
|
||||
if (!drawn) drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||
if (!drawn) drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||
if (!drawn) {
|
||||
MGLOG_E("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
|
||||
"the batch was dropped",
|
||||
drawcount, mode, type);
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl
|
||||
@@ -0,0 +1,64 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectGLES/MultiDraw.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <Config.h>
|
||||
#include "DirectGLES.h"
|
||||
|
||||
// Emulation of the desktop glMultiDrawElements / glMultiDrawElementsBaseVertex entry
|
||||
// points on OpenGL ES, which has neither in core.
|
||||
//
|
||||
// Every strategy below is an emulation; they differ only in which driver capability
|
||||
// they lean on and in how many driver entries a batch of N sub-draws costs. The design
|
||||
// follows MobileGlues (MobileGL-Dev/MobileGlues, gl/multidraw.cpp) tier for tier, plus
|
||||
// the native GL_EXT_multi_draw_arrays interaction that MobileGL already had:
|
||||
//
|
||||
// Ext one glMultiDrawElementsBaseVertexEXT 1 driver entry
|
||||
// MultiIndirect one glMultiDrawElementsIndirectEXT 1 driver entry + 1 upload
|
||||
// Indirect N x glDrawElementsIndirect N + 1 upload
|
||||
// BaseVertex N x glDrawElementsBaseVertex N
|
||||
// DrawElements N x glDrawElements over CPU-rebased indices N + 1 upload
|
||||
// Compute 1 x glDrawElements over a GPU-flattened, 1 dispatch + 1 entry
|
||||
// rebased index stream
|
||||
//
|
||||
// Which one runs is resolved once per ES context from the driver's capabilities,
|
||||
// capped by MOBILEGL_ESPRYT_MULTIDRAW_MODE, and can additionally be demoted per batch
|
||||
// when the batch's own shape rules a tier out (see ResolveTierForBatch in the .cpp).
|
||||
namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// The tier this ES context resolved to, computed on first use and stable after.
|
||||
MG_Config::GLESMultiDrawMode ResolvedTier();
|
||||
// "multiindirect", "compute", ... - stable identifiers, also used by the POST row.
|
||||
const char* TierName(MG_Config::GLESMultiDrawMode tier);
|
||||
// One line naming the resolved tier, the tiers the driver can support, and the env
|
||||
// clamp if one applied. For DriverPost and the startup log.
|
||||
String DescribeTierResolution();
|
||||
|
||||
// The resolution itself, as a pure function of a capability set: the backend feeds
|
||||
// it the live ES context's capabilities, DriverPost feeds it the ones it probed
|
||||
// standalone, and both therefore report the same tier. `explanation`, when non-null,
|
||||
// receives the "requested -> resolved (driver supports: ...)" line.
|
||||
MG_Config::GLESMultiDrawMode ResolveTier(const MG_External::GLESCapabilities& caps,
|
||||
const MG_External::GLESFunctionsTable& funcs,
|
||||
MG_Config::GLESMultiDrawMode requested, String* explanation);
|
||||
// Whether one tier is runnable on the given capability set, for per-row POST output.
|
||||
Bool IsTierSupported(const MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& funcs,
|
||||
MG_Config::GLESMultiDrawMode tier);
|
||||
|
||||
// Runs `drawcount` indexed sub-draws as one glMultiDrawElements(BaseVertex) call
|
||||
// would. `basevertex` is null for the plain glMultiDrawElements entry point (every
|
||||
// base vertex is 0). Owns the whole draw, preparation included: callers must not
|
||||
// have run PrepareForDraw, because the compute tier has to dispatch before the
|
||||
// draw state is established.
|
||||
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex);
|
||||
|
||||
// The ES context is gone: every scratch buffer and the compute program belonged to
|
||||
// it, so drop the names without deleting them (the dead context reclaims them).
|
||||
void OnBackendContextDestroyed();
|
||||
} // namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl
|
||||
@@ -49,6 +49,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/OrientationScenario.cpp
|
||||
Scenarios/CrossFrameBufferScenario.cpp
|
||||
Scenarios/ResidentIndexScenario.cpp
|
||||
Scenarios/MultiDrawScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/MultiDrawScenario.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 D - glMultiDrawElements(BaseVertex) against the draws it stands for.
|
||||
//
|
||||
// Neither entry point exists in OpenGL ES, so DirectGLES emulates both through a
|
||||
// ladder of tiers (MG_Backend/DirectGLES/MultiDraw.cpp): a native
|
||||
// glMultiDrawElementsBaseVertexEXT, synthesized indirect commands drawn one at a
|
||||
// time or in one batch, a per-sub-draw replay, a CPU rewrite of the index stream,
|
||||
// and a compute shader that flattens the whole batch into a single draw. They
|
||||
// share nothing but their contract, which is the one thing asserted here:
|
||||
//
|
||||
// a multi-draw must paint exactly what the unrolled single draws paint.
|
||||
//
|
||||
// The reference side never enters the emulation - it is a loop of
|
||||
// glDrawElementsBaseVertex / glDrawElements - so a tier cannot make itself look
|
||||
// right by breaking both sides the same way.
|
||||
//
|
||||
// The Minecraft retraces already cover the common shape (GL_UNSIGNED_INT indices
|
||||
// in a bound element array buffer, small base vertices, GL_TRIANGLES) on every
|
||||
// tier. What they contain none of, and what these cases are for, is the set of
|
||||
// shapes where a tier has to decline or compensate rather than replay:
|
||||
//
|
||||
// * narrow index types, where a rewritten stream has to widen (BYTE/SHORT);
|
||||
// * a base vertex past the index type's range, where folding it into the
|
||||
// indices at the source width silently wraps - GL adds base vertices at full
|
||||
// precision, so `ushort index 10 + baseVertex 70000` is vertex 70010 and not
|
||||
// vertex 4474;
|
||||
// * primitive restart, where a rewritten stream must carry the sentinel across
|
||||
// unrebased or the restart is lost and the strip welds shut;
|
||||
// * client-memory index arrays, which have no buffer for the indirect tiers to
|
||||
// address or for the compute tier to read;
|
||||
// * a strip mode, which the flattening tier must decline outright because
|
||||
// concatenation would weld one sub-draw's last primitive to the next
|
||||
// sub-draw's first.
|
||||
//
|
||||
// One process is one tier (MOBILEGL_ESPRYT_MULTIDRAW_MODE is read once at
|
||||
// startup), so a single run exercises whichever tier this driver resolved to.
|
||||
// Running the binary once per mode is what covers the ladder; each run is a
|
||||
// complete, self-contained proof for the tier it landed on.
|
||||
|
||||
#include <cstdint>
|
||||
#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/glext.h>
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 330 core
|
||||
layout(location = 0) in vec2 aPos;
|
||||
layout(location = 1) in vec3 aColor;
|
||||
out vec3 vColor;
|
||||
void main() {
|
||||
vColor = aColor;
|
||||
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kFragmentSource = R"(#version 330 core
|
||||
in vec3 vColor;
|
||||
out vec4 oColor;
|
||||
void main() {
|
||||
oColor = vec4(vColor, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
struct Vertex {
|
||||
float x, y;
|
||||
float r, g, b;
|
||||
};
|
||||
|
||||
// Four column quads spanning the viewport left to right, in four colours,
|
||||
// so a sub-draw that lands in the wrong place, draws the wrong vertices or
|
||||
// does not draw at all changes the picture rather than hiding inside it.
|
||||
constexpr int kColumns = 4;
|
||||
|
||||
const Rgba8 kColumnColors[kColumns] = {
|
||||
{255, 0, 0, 255},
|
||||
{0, 255, 0, 255},
|
||||
{0, 0, 255, 255},
|
||||
{255, 255, 255, 255},
|
||||
};
|
||||
|
||||
// `padVertices` leading dummies force every sub-draw to need its own base
|
||||
// vertex: without one applied, a draw reads the padding and paints black.
|
||||
std::vector<Vertex> ColumnVertices(int padVertices) {
|
||||
std::vector<Vertex> vertices(static_cast<std::size_t>(padVertices), Vertex{0.0f, 0.0f, 0.0f, 0.0f, 0.0f});
|
||||
for (int column = 0; column < kColumns; ++column) {
|
||||
const float x0 = -1.0f + 2.0f * static_cast<float>(column) / kColumns;
|
||||
const float x1 = -1.0f + 2.0f * static_cast<float>(column + 1) / kColumns;
|
||||
const Rgba8 color = kColumnColors[column];
|
||||
const float r = color.r / 255.0f;
|
||||
const float g = color.g / 255.0f;
|
||||
const float b = color.b / 255.0f;
|
||||
vertices.push_back({x0, -1.0f, r, g, b});
|
||||
vertices.push_back({x1, -1.0f, r, g, b});
|
||||
vertices.push_back({x1, 1.0f, r, g, b});
|
||||
vertices.push_back({x0, 1.0f, r, g, b});
|
||||
}
|
||||
return vertices;
|
||||
}
|
||||
|
||||
// Every sub-draw uses the SAME six indices, 0..3 relative to its own quad;
|
||||
// only the base vertex tells the columns apart. That makes the base vertex
|
||||
// the load-bearing part of the batch.
|
||||
const std::uint32_t kQuadIndices[6] = {0, 1, 2, 0, 2, 3};
|
||||
|
||||
// One column, as a restart-separated pair of triangle strips. Two strips in
|
||||
// one sub-draw means the sentinel is genuinely interior: drop it and the two
|
||||
// halves weld into a single strip that paints across the gap between them.
|
||||
// Indices are relative to the sub-draw's own quad, like kQuadIndices.
|
||||
template <typename Index>
|
||||
std::vector<Index> RestartStripIndices(Index restartSentinel) {
|
||||
// 3,0,2,1 is the strip winding of the quad; splitting it around the
|
||||
// sentinel gives two degenerate-free halves that redraw the same area.
|
||||
return {Index{3}, Index{0}, Index{2}, restartSentinel, Index{0}, Index{2}, Index{1}};
|
||||
}
|
||||
|
||||
class MultiDrawScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "program setup left a GL error behind";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
ReleaseBuffers();
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
}
|
||||
|
||||
// VAO + VBO, and an EBO only when `indexBytes` is non-null: a null one
|
||||
// leaves GL_ELEMENT_ARRAY_BUFFER unbound so the sub-draws address client
|
||||
// memory, which is the shape that forces the buffer-reading tiers out.
|
||||
void BuildScene(int padVertices, const void* indexBytes, std::size_t indexByteCount) {
|
||||
ReleaseBuffers();
|
||||
const std::vector<Vertex> vertices = ColumnVertices(padVertices);
|
||||
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(vertices.size() * sizeof(Vertex)),
|
||||
vertices.data(), GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<const void*>(0));
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||
reinterpret_cast<const void*>(sizeof(float) * 2));
|
||||
|
||||
if (indexBytes != nullptr) {
|
||||
glGenBuffers(1, &m_ebo);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, static_cast<GLsizeiptr>(indexByteCount), indexBytes,
|
||||
GL_STATIC_DRAW);
|
||||
}
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
|
||||
}
|
||||
|
||||
void ReleaseBuffers() {
|
||||
if (m_ebo != 0) glDeleteBuffers(1, &m_ebo);
|
||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_ebo = 0;
|
||||
m_vbo = 0;
|
||||
m_vao = 0;
|
||||
}
|
||||
|
||||
GLuint m_program = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
GLuint m_ebo = 0;
|
||||
};
|
||||
|
||||
// Runs `draw`, reads the default framebuffer back and returns the image.
|
||||
template <typename DrawFn>
|
||||
Image RenderPass(GLuint program, GLuint vao, DrawFn&& draw) {
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, HeadlessGL::Get().Width(), HeadlessGL::Get().Height());
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glUseProgram(program);
|
||||
glBindVertexArray(vao);
|
||||
draw();
|
||||
return ReadPixels(HeadlessGL::Get().Width(), HeadlessGL::Get().Height());
|
||||
}
|
||||
|
||||
// The whole point of the file: two renderings of the same geometry, one
|
||||
// through the multi-draw emulation and one through the single-draw entry
|
||||
// points it stands for, must be identical to the byte.
|
||||
void ExpectSameImage(const Image& multiDraw, const Image& unrolled, const std::string& what) {
|
||||
ASSERT_FALSE(multiDraw.Empty()) << what << ": multi-draw readback was empty";
|
||||
ASSERT_FALSE(unrolled.Empty()) << what << ": reference readback was empty";
|
||||
EXPECT_EQ(multiDraw, unrolled)
|
||||
<< what << ": glMultiDraw* painted something else than the draws it stands for ("
|
||||
<< multiDraw.ByteDiffCount(unrolled) << " bytes differ; multi-draw quadrants "
|
||||
<< multiDraw.QuadrantSignature() << ", unrolled quadrants " << unrolled.QuadrantSignature() << ")";
|
||||
// A pair of blank frames would satisfy the comparison above and prove
|
||||
// nothing at all - the failure mode a multi-draw path most often has is
|
||||
// drawing NOTHING (see the shipped glMultiDrawElementsBaseVertexEXT stub
|
||||
// that silently dropped every draw). Demand the columns really landed.
|
||||
EXPECT_NE(multiDraw.QuadrantSignature(), "black,black,black,black") << what << ": nothing was drawn at all";
|
||||
}
|
||||
|
||||
// ---- GL_UNSIGNED_INT indices in a buffer, per-sub-draw base vertices ----
|
||||
|
||||
TEST_F(MultiDrawScenario, BaseVertexBatchMatchesUnrolledDraws) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kPad = 5; // odd, so nothing lines up by accident
|
||||
BuildScene(kPad, kQuadIndices, sizeof(kQuadIndices));
|
||||
|
||||
GLsizei counts[kColumns];
|
||||
const void* offsets[kColumns];
|
||||
GLint baseVertices[kColumns];
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
counts[i] = 6;
|
||||
offsets[i] = reinterpret_cast<const void*>(0);
|
||||
baseVertices[i] = kPad + i * 4;
|
||||
}
|
||||
|
||||
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets, kColumns, baseVertices);
|
||||
});
|
||||
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_INT, offsets[i], baseVertices[i]);
|
||||
}
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectSameImage(batched, unrolled, "GL_UNSIGNED_INT indices, per-sub-draw base vertices");
|
||||
}
|
||||
|
||||
// ---- glMultiDrawElements: no base vertices, distinct index offsets ----
|
||||
|
||||
TEST_F(MultiDrawScenario, PlainBatchMatchesUnrolledDraws) {
|
||||
if (!Ready()) return;
|
||||
// No padding and no base vertices: each sub-draw reaches its own column
|
||||
// through its index offset instead.
|
||||
std::vector<std::uint32_t> indices;
|
||||
for (int column = 0; column < kColumns; ++column) {
|
||||
for (const std::uint32_t index : kQuadIndices) {
|
||||
indices.push_back(index + static_cast<std::uint32_t>(column * 4));
|
||||
}
|
||||
}
|
||||
BuildScene(0, indices.data(), indices.size() * sizeof(std::uint32_t));
|
||||
|
||||
GLsizei counts[kColumns];
|
||||
const void* offsets[kColumns];
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
counts[i] = 6;
|
||||
offsets[i] = reinterpret_cast<const void*>(static_cast<std::uintptr_t>(i * 6 * sizeof(std::uint32_t)));
|
||||
}
|
||||
|
||||
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||
glMultiDrawElements(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets, kColumns);
|
||||
});
|
||||
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
glDrawElements(GL_TRIANGLES, counts[i], GL_UNSIGNED_INT, offsets[i]);
|
||||
}
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectSameImage(batched, unrolled, "glMultiDrawElements with no base vertices");
|
||||
}
|
||||
|
||||
// ---- narrow index types ----
|
||||
// A tier that rewrites the stream emits GL_UNSIGNED_INT whatever came in,
|
||||
// so these two say the widening reproduces the original draw exactly.
|
||||
|
||||
TEST_F(MultiDrawScenario, UnsignedShortBatchMatchesUnrolledDraws) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kPad = 3;
|
||||
std::uint16_t indices[6];
|
||||
for (int i = 0; i < 6; ++i)
|
||||
indices[i] = static_cast<std::uint16_t>(kQuadIndices[i]);
|
||||
BuildScene(kPad, indices, sizeof(indices));
|
||||
|
||||
GLsizei counts[kColumns];
|
||||
const void* offsets[kColumns];
|
||||
GLint baseVertices[kColumns];
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
counts[i] = 6;
|
||||
offsets[i] = reinterpret_cast<const void*>(0);
|
||||
baseVertices[i] = kPad + i * 4;
|
||||
}
|
||||
|
||||
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_SHORT, offsets, kColumns, baseVertices);
|
||||
});
|
||||
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_SHORT, offsets[i], baseVertices[i]);
|
||||
}
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectSameImage(batched, unrolled, "GL_UNSIGNED_SHORT indices");
|
||||
}
|
||||
|
||||
TEST_F(MultiDrawScenario, UnsignedByteBatchMatchesUnrolledDraws) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kPad = 3;
|
||||
std::uint8_t indices[6];
|
||||
for (int i = 0; i < 6; ++i)
|
||||
indices[i] = static_cast<std::uint8_t>(kQuadIndices[i]);
|
||||
// 24 bytes: a word multiple, which the compute tier needs of the source
|
||||
// buffer when the index type is narrower than a word.
|
||||
std::uint8_t padded[24] = {};
|
||||
for (int i = 0; i < 6; ++i)
|
||||
padded[i] = indices[i];
|
||||
BuildScene(kPad, padded, sizeof(padded));
|
||||
|
||||
GLsizei counts[kColumns];
|
||||
const void* offsets[kColumns];
|
||||
GLint baseVertices[kColumns];
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
counts[i] = 6;
|
||||
offsets[i] = reinterpret_cast<const void*>(0);
|
||||
baseVertices[i] = kPad + i * 4;
|
||||
}
|
||||
|
||||
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_BYTE, offsets, kColumns, baseVertices);
|
||||
});
|
||||
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_BYTE, offsets[i], baseVertices[i]);
|
||||
}
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectSameImage(batched, unrolled, "GL_UNSIGNED_BYTE indices");
|
||||
}
|
||||
|
||||
// ---- a base vertex the index type cannot spell ----
|
||||
// GL adds the base vertex at full precision, so folding it into a
|
||||
// GL_UNSIGNED_SHORT index stream at the source width wraps and addresses the
|
||||
// wrong vertex. The columns here start past 65535, which no ushort index can
|
||||
// reach on its own.
|
||||
|
||||
TEST_F(MultiDrawScenario, BaseVertexBeyondIndexTypeRangeMatchesUnrolledDraws) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kPad = 70000; // > 0xFFFF
|
||||
std::uint16_t indices[6];
|
||||
for (int i = 0; i < 6; ++i)
|
||||
indices[i] = static_cast<std::uint16_t>(kQuadIndices[i]);
|
||||
BuildScene(kPad, indices, sizeof(indices));
|
||||
|
||||
GLsizei counts[kColumns];
|
||||
const void* offsets[kColumns];
|
||||
GLint baseVertices[kColumns];
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
counts[i] = 6;
|
||||
offsets[i] = reinterpret_cast<const void*>(0);
|
||||
baseVertices[i] = kPad + i * 4;
|
||||
}
|
||||
|
||||
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_SHORT, offsets, kColumns, baseVertices);
|
||||
});
|
||||
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_SHORT, offsets[i], baseVertices[i]);
|
||||
}
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectSameImage(batched, unrolled, "base vertex past the GL_UNSIGNED_SHORT range");
|
||||
}
|
||||
|
||||
// ---- client-memory index arrays ----
|
||||
// No element array buffer, so the indirect tiers have nothing to address and
|
||||
// the compute tier nothing to read; both must decline and hand the batch to
|
||||
// a tier that can replay it.
|
||||
|
||||
TEST_F(MultiDrawScenario, ClientSideIndicesBatchMatchesUnrolledDraws) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kPad = 5;
|
||||
BuildScene(kPad, nullptr, 0);
|
||||
|
||||
GLsizei counts[kColumns];
|
||||
const void* offsets[kColumns];
|
||||
GLint baseVertices[kColumns];
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
counts[i] = 6;
|
||||
offsets[i] = kQuadIndices;
|
||||
baseVertices[i] = kPad + i * 4;
|
||||
}
|
||||
|
||||
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets, kColumns, baseVertices);
|
||||
});
|
||||
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_INT, offsets[i], baseVertices[i]);
|
||||
}
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectSameImage(batched, unrolled, "client-memory index arrays");
|
||||
}
|
||||
|
||||
// ---- primitive restart inside a strip ----
|
||||
// Two things at once: a strip mode, which the flattening tier must decline
|
||||
// because concatenation would weld sub-draws together, and a restart
|
||||
// sentinel, which any tier that rewrites indices must carry across without
|
||||
// adding the base vertex to it.
|
||||
|
||||
TEST_F(MultiDrawScenario, PrimitiveRestartStripBatchMatchesUnrolledDraws) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kPad = 5;
|
||||
const std::vector<std::uint32_t> indices = RestartStripIndices<std::uint32_t>(0xFFFFFFFFu);
|
||||
BuildScene(kPad, indices.data(), indices.size() * sizeof(std::uint32_t));
|
||||
|
||||
GLsizei counts[kColumns];
|
||||
const void* offsets[kColumns];
|
||||
GLint baseVertices[kColumns];
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
counts[i] = static_cast<GLsizei>(indices.size());
|
||||
offsets[i] = reinterpret_cast<const void*>(0);
|
||||
baseVertices[i] = kPad + i * 4;
|
||||
}
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
||||
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLE_STRIP, counts, GL_UNSIGNED_INT, offsets, kColumns,
|
||||
baseVertices);
|
||||
});
|
||||
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
glDrawElementsBaseVertex(GL_TRIANGLE_STRIP, counts[i], GL_UNSIGNED_INT, offsets[i],
|
||||
baseVertices[i]);
|
||||
}
|
||||
});
|
||||
glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectSameImage(batched, unrolled, "GL_TRIANGLE_STRIP with primitive restart");
|
||||
}
|
||||
|
||||
// Same, with GL_UNSIGNED_SHORT: the sentinel a rewritten stream has to
|
||||
// recognise is the index TYPE's all-ones value, not the rewritten stream's.
|
||||
TEST_F(MultiDrawScenario, PrimitiveRestartUnsignedShortBatchMatchesUnrolledDraws) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kPad = 5;
|
||||
const std::vector<std::uint16_t> indices = RestartStripIndices<std::uint16_t>(0xFFFFu);
|
||||
BuildScene(kPad, indices.data(), indices.size() * sizeof(std::uint16_t));
|
||||
|
||||
GLsizei counts[kColumns];
|
||||
const void* offsets[kColumns];
|
||||
GLint baseVertices[kColumns];
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
counts[i] = static_cast<GLsizei>(indices.size());
|
||||
offsets[i] = reinterpret_cast<const void*>(0);
|
||||
baseVertices[i] = kPad + i * 4;
|
||||
}
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
||||
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLE_STRIP, counts, GL_UNSIGNED_SHORT, offsets, kColumns,
|
||||
baseVertices);
|
||||
});
|
||||
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
glDrawElementsBaseVertex(GL_TRIANGLE_STRIP, counts[i], GL_UNSIGNED_SHORT, offsets[i],
|
||||
baseVertices[i]);
|
||||
}
|
||||
});
|
||||
glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectSameImage(batched, unrolled, "GL_TRIANGLE_STRIP with GL_UNSIGNED_SHORT primitive restart");
|
||||
}
|
||||
|
||||
// ---- a batch with holes ----
|
||||
// Zero-count sub-draws draw nothing. The flattening tier's binary search
|
||||
// finds a sub-draw by prefix sum, and a zero-count entry repeats the
|
||||
// previous sum - so a search that resolves ties the other way would attribute
|
||||
// indices to the empty draw and paint the wrong column.
|
||||
|
||||
TEST_F(MultiDrawScenario, ZeroCountSubDrawsMatchUnrolledDraws) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kPad = 5;
|
||||
BuildScene(kPad, kQuadIndices, sizeof(kQuadIndices));
|
||||
|
||||
GLsizei counts[kColumns];
|
||||
const void* offsets[kColumns];
|
||||
GLint baseVertices[kColumns];
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
// Columns 1 and 2 are skipped, leaving the outer two painted.
|
||||
counts[i] = (i == 1 || i == 2) ? 0 : 6;
|
||||
offsets[i] = reinterpret_cast<const void*>(0);
|
||||
baseVertices[i] = kPad + i * 4;
|
||||
}
|
||||
|
||||
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets, kColumns, baseVertices);
|
||||
});
|
||||
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||
for (int i = 0; i < kColumns; ++i) {
|
||||
if (counts[i] == 0) continue;
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_INT, offsets[i], baseVertices[i]);
|
||||
}
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectSameImage(batched, unrolled, "a batch with zero-count sub-draws");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -888,6 +888,18 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.SupportsMultiDrawElementsBaseVertex = hasDrawElementsBaseVertexExtension &&
|
||||
hasMultiDrawArraysExtension &&
|
||||
glesFuncs.glMultiDrawElementsBaseVertexEXT != nullptr;
|
||||
// Core from ES 3.2 on, so an extension string is not required there; below 3.2 the
|
||||
// extension is, and the pointer still has to have resolved either way.
|
||||
const Bool esAtLeast32 = caps.GLESVersion.Major > 3 ||
|
||||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);
|
||||
const Bool esAtLeast31 = caps.GLESVersion.Major > 3 ||
|
||||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1);
|
||||
caps.SupportsDrawElementsBaseVertex = (esAtLeast32 || hasDrawElementsBaseVertexExtension) &&
|
||||
glesFuncs.glDrawElementsBaseVertex != nullptr;
|
||||
caps.SupportsComputeShader = esAtLeast31 && glesFuncs.glDispatchCompute != nullptr &&
|
||||
glesFuncs.glMemoryBarrier != nullptr &&
|
||||
glesFuncs.glCreateShader != nullptr &&
|
||||
glesFuncs.glCreateProgram != nullptr;
|
||||
caps.SupportsShaderMultisampleInterpolation =
|
||||
caps.SupportsShaderMultisampleInterpolation || caps.GLESVersion.Major > 3 ||
|
||||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);
|
||||
@@ -907,6 +919,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.SupportsMultiDrawIndirect ? "yes" : "no");
|
||||
MGLOG_I(" multi-draw base vertex (EXT/OES_draw_elements_base_vertex + EXT_multi_draw_arrays): %s",
|
||||
caps.SupportsMultiDrawElementsBaseVertex ? "yes" : "no");
|
||||
MGLOG_I(" draw elements base vertex (ES 3.2 core or EXT/OES_draw_elements_base_vertex): %s",
|
||||
caps.SupportsDrawElementsBaseVertex ? "yes" : "no");
|
||||
MGLOG_I(" compute shaders (ES 3.1 core): %s", caps.SupportsComputeShader ? "yes" : "no");
|
||||
|
||||
MGLOG_I("OpenGL ES capabilities:");
|
||||
glesFuncs.glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &caps.UniformBufferOffsetAlignment);
|
||||
|
||||
@@ -1084,6 +1084,15 @@ namespace MobileGL {
|
||||
// requires (EXT or OES draw_elements_base_vertex) AND GL_EXT_multi_draw_arrays
|
||||
// AND a resolved pointer; callers must gate on it, never on the pointer.
|
||||
Bool SupportsMultiDrawElementsBaseVertex = false;
|
||||
// glDrawElementsBaseVertex is callable: ES 3.2 core, or EXT/OES_draw_elements_base_vertex
|
||||
// before that, with the pointer resolved. This is the weaker sibling of the flag above -
|
||||
// it does NOT need GL_EXT_multi_draw_arrays, only the single-draw entry point - and it
|
||||
// decides whether a multi-draw batch can carry per-sub-draw base vertices at all or has
|
||||
// to fold them into rewritten indices.
|
||||
Bool SupportsDrawElementsBaseVertex = false;
|
||||
// Compute shaders are usable: ES 3.1 core (there is no pre-3.1 extension in ES), with
|
||||
// the dispatch and barrier entry points resolved.
|
||||
Bool SupportsComputeShader = false;
|
||||
// GL_RENDERER contains "ANGLE".
|
||||
Bool IsAngleRenderer = false;
|
||||
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <Config.h>
|
||||
#include <MGGitHash.h>
|
||||
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
|
||||
#include <MG_Backend/DirectGLES/MultiDraw.h>
|
||||
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
|
||||
// Only for the compile-time MAX_VERTEX_ATTRIBS constant asserted below. The POST still executes no
|
||||
// MG_State code: it runs standalone, before MG_State::Init().
|
||||
@@ -319,9 +320,46 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
} else {
|
||||
builder.Info("Multi-draw base vertex",
|
||||
"glMultiDrawElementsBaseVertexEXT not supported (needs EXT/OES_"
|
||||
"draw_elements_base_vertex plus GL_EXT_multi_draw_arrays); "
|
||||
"glMultiDrawElementsBaseVertex falls back to a per-draw loop with "
|
||||
"identical output");
|
||||
"draw_elements_base_vertex plus GL_EXT_multi_draw_arrays); the batch "
|
||||
"takes the next emulation tier instead, with identical output - see "
|
||||
"\"Multi-draw elements tier\" below for the one that will run");
|
||||
}
|
||||
// glMultiDrawElements(BaseVertex) has no ES counterpart at all, so DirectGLES
|
||||
// emulates it; these rows say which emulation the driver leaves available and
|
||||
// which one will run. The two capabilities each tier leans on come first.
|
||||
if (caps.SupportsDrawElementsBaseVertex) {
|
||||
builder.Pass("Draw elements base vertex",
|
||||
"glDrawElementsBaseVertex available (ES 3.2 core or EXT/OES_draw_elements_base_"
|
||||
"vertex); a multi-draw batch can replay its sub-draws with their own base "
|
||||
"vertices");
|
||||
} else {
|
||||
builder.Warn("Draw elements base vertex",
|
||||
"glDrawElementsBaseVertex not supported (pre-ES 3.2 without EXT/OES_draw_"
|
||||
"elements_base_vertex); every base-vertex draw has to be emulated by rewriting "
|
||||
"the index stream on the CPU, which costs an upload per batch");
|
||||
}
|
||||
if (caps.SupportsComputeShader) {
|
||||
builder.Pass("Compute shaders",
|
||||
"available (ES 3.1 core); the opt-in \"compute\" multi-draw tier can flatten a "
|
||||
"whole batch into one draw");
|
||||
} else {
|
||||
builder.Info("Compute shaders",
|
||||
"not available (pre-ES 3.1); no impact on the default multi-draw tiers, which "
|
||||
"never use compute");
|
||||
}
|
||||
{
|
||||
// The same resolution the backend runs, over the capabilities probed here.
|
||||
// Like the Magma tier row, the preference comes from MG_Config::Features,
|
||||
// which is only populated once MobileGL::Initialize() has parsed the
|
||||
// environment - a POST executed standalone before that reports the
|
||||
// unclamped choice, so the row names the variable rather than implying it
|
||||
// was consulted.
|
||||
using MG_Backend::DirectGLES::MultiDrawImpl::ResolveTier;
|
||||
String resolution;
|
||||
ResolveTier(caps, glesFuncs, MG_Config::Features.EsprytMultiDrawMode, &resolution);
|
||||
builder.Info("Multi-draw elements tier",
|
||||
"glMultiDrawElements(BaseVertex) emulation: " + resolution +
|
||||
"; override with MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
||||
}
|
||||
if (caps.SupportsTextureBorderClamp) {
|
||||
builder.Pass("Texture border clamp",
|
||||
|
||||
Reference in New Issue
Block a user