mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +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)
|
set(MOBILEGL_BUILD_BENCHMARK OFF CACHE BOOL "Build MobileGL benchmarks" FORCE)
|
||||||
endif()
|
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
|
# Check if ThinLTO or LTO is suppported
|
||||||
include(CheckIPOSupported)
|
include(CheckIPOSupported)
|
||||||
include(CheckCCompilerFlag)
|
include(CheckCCompilerFlag)
|
||||||
@@ -248,6 +250,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
|
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
|
||||||
MobileGL/MG_Backend/DirectGLES/Utils.cpp
|
MobileGL/MG_Backend/DirectGLES/Utils.cpp
|
||||||
MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||||
|
MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
|
||||||
|
|
||||||
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
|
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
|
||||||
MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp
|
MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp
|
||||||
|
|||||||
@@ -39,6 +39,23 @@ namespace MobileGL::MG_Config {
|
|||||||
Unroll, // one vkCmdDraw* per sub-draw
|
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()
|
// Feature toggles parsed once from environment variables in MG_ConfigLoader::Init()
|
||||||
// (ConfigLoader.cpp), before the accepted-env map is destroyed. All Bool fields share
|
// (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"
|
// 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;
|
// ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support;
|
||||||
// unset picks the best supported tier.
|
// unset picks the best supported tier.
|
||||||
MultiDrawMode MagmaMultiDrawMode = MultiDrawMode::Auto;
|
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;
|
extern FeaturesTable Features;
|
||||||
} // namespace MobileGL::MG_Config
|
} // namespace MobileGL::MG_Config
|
||||||
|
|||||||
@@ -116,6 +116,28 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
return MG_Config::MultiDrawMode::Auto;
|
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) {
|
inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
|
||||||
auto it = acceptedEnvVariablesMap->find(key);
|
auto it = acceptedEnvVariablesMap->find(key);
|
||||||
if (it == acceptedEnvVariablesMap->end()) {
|
if (it == acceptedEnvVariablesMap->end()) {
|
||||||
@@ -158,6 +180,7 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||||
features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
|
features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
|
||||||
|
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void InitBackendType() {
|
inline void InitBackendType() {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "MG_Util/Types.h"
|
#include "MG_Util/Types.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
#include "Managers.h"
|
#include "Managers.h"
|
||||||
|
#include "MultiDraw.h"
|
||||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Classifiers/TextureEnumClassifier.h>
|
#include <MG_Util/Classifiers/TextureEnumClassifier.h>
|
||||||
#include <MG_Util/Metrics/TextureMetrics.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() {
|
SamplerImpl::BackendSamplerObject* GetRawDepthFetchSampler() {
|
||||||
if (!g_rawDepthFetchSamplerState) {
|
if (!g_rawDepthFetchSamplerState) {
|
||||||
g_rawDepthFetchSamplerState = MakeShared<MG_State::GLState::SamplerObject>(0);
|
g_rawDepthFetchSamplerState = MakeShared<MG_State::GLState::SamplerObject>(0);
|
||||||
@@ -787,6 +757,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_GLESFuncs.glTransformFeedbackVaryings != nullptr;
|
g_GLESFuncs.glTransformFeedbackVaryings != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool IsCaptureSpanOpen() {
|
||||||
|
const auto& xfb = CurrentXfb();
|
||||||
|
return (xfb.pending || xfb.started) && !xfb.paused;
|
||||||
|
}
|
||||||
|
|
||||||
void BeginTransformFeedback(GLenum primitiveMode) {
|
void BeginTransformFeedback(GLenum primitiveMode) {
|
||||||
if (!AreTransformFeedbacksSupported()) return;
|
if (!AreTransformFeedbacksSupported()) return;
|
||||||
auto& xfb = CurrentXfb();
|
auto& xfb = CurrentXfb();
|
||||||
@@ -2071,7 +2046,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
static void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys,
|
static void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys,
|
||||||
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram);
|
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram);
|
||||||
|
|
||||||
void PrepareForDraw(DrawSyncBit syncBit) {
|
void PrepareForDraw(DrawSyncFlags syncBit) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
@@ -2774,6 +2749,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool CurrentProgramReadsDrawID() {
|
||||||
|
const auto program = GetCurrentBackendProgram();
|
||||||
|
return program != nullptr && program->ReadsDrawID();
|
||||||
|
}
|
||||||
|
|
||||||
static Bool SupportsNativeIndirectDraws() {
|
static Bool SupportsNativeIndirectDraws() {
|
||||||
const auto& version = g_GLESCapabilities.GLESVersion;
|
const auto& version = g_GLESCapabilities.GLESVersion;
|
||||||
const Bool esVersionOk = version.Major > 3 || (version.Major == 3 && version.Minor >= 1);
|
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
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
||||||
DebugImpl::OpenGLScopeMarker marker(__func__);
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
||||||
#endif
|
#endif
|
||||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer;
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
||||||
PrepareForDraw(syncBit);
|
PrepareForDraw(syncBit);
|
||||||
CheckPrimitiveRestartSupported(type);
|
CheckPrimitiveRestartSupported(type);
|
||||||
g_GLESFuncs.glDrawElements(mode, count, type, indices);
|
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
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
||||||
DebugImpl::OpenGLScopeMarker marker(__func__);
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
||||||
#endif
|
#endif
|
||||||
DrawSyncBit syncBit = DrawSyncBit::None;
|
DrawSyncFlags syncBit = DrawSyncBit::None;
|
||||||
PrepareForDraw(syncBit);
|
PrepareForDraw(syncBit);
|
||||||
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
||||||
if (currentVAO) {
|
if (currentVAO) {
|
||||||
@@ -3106,7 +3086,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
||||||
DebugImpl::OpenGLScopeMarker marker(__func__);
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
||||||
#endif
|
#endif
|
||||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer;
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
||||||
PrepareForDraw(syncBit);
|
PrepareForDraw(syncBit);
|
||||||
CheckPrimitiveRestartSupported(type);
|
CheckPrimitiveRestartSupported(type);
|
||||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count, type, indices, basevertex);
|
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
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
||||||
DebugImpl::OpenGLScopeMarker marker(__func__);
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
||||||
#endif
|
#endif
|
||||||
DrawSyncBit syncBit = DrawSyncBit::None;
|
DrawSyncFlags syncBit = DrawSyncBit::None;
|
||||||
PrepareForDraw(syncBit);
|
PrepareForDraw(syncBit);
|
||||||
|
|
||||||
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
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,
|
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
GLsizei drawcount) {
|
GLsizei drawcount) {
|
||||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
||||||
DebugImpl::OpenGLScopeMarker marker(__func__);
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
||||||
#endif
|
#endif
|
||||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer;
|
MultiDrawImpl::DrawElementsBatch(mode, count, type, indices, drawcount, nullptr);
|
||||||
PrepareForDraw(syncBit);
|
|
||||||
CheckPrimitiveRestartSupported(type);
|
|
||||||
|
|
||||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
|
||||||
g_GLESFuncs.glDrawElements(mode, count[i], type, indices[i]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
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
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
||||||
DebugImpl::OpenGLScopeMarker marker(__func__);
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
||||||
#endif
|
#endif
|
||||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer;
|
MultiDrawImpl::DrawElementsBatch(mode, count, type, indices, drawcount, basevertex);
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||||
@@ -3183,7 +3148,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
||||||
PrepareForDraw(syncBit);
|
PrepareForDraw(syncBit);
|
||||||
|
|
||||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||||
@@ -3223,7 +3188,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
||||||
PrepareForDraw(syncBit);
|
PrepareForDraw(syncBit);
|
||||||
|
|
||||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||||
@@ -3282,7 +3247,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawSyncBit syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
||||||
PrepareForDraw(syncBit);
|
PrepareForDraw(syncBit);
|
||||||
|
|
||||||
const auto* commandBytes = ResolveIndirectCommandBytes(
|
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,
|
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||||
const void* indices, GLint basevertex) {
|
const void* indices, GLint basevertex) {
|
||||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer;
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
||||||
PrepareForDraw(syncBit);
|
PrepareForDraw(syncBit);
|
||||||
g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, type, indices, basevertex);
|
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) {
|
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);
|
PrepareForDraw(syncBit);
|
||||||
g_GLESFuncs.glDrawRangeElements(mode, start, end, count, type, indices);
|
g_GLESFuncs.glDrawRangeElements(mode, start, end, count, type, indices);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||||
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
|
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
|
||||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||||
PrepareForDraw(syncBit);
|
PrepareForDraw(syncBit);
|
||||||
SetCurrentBaseInstance(baseinstance);
|
SetCurrentBaseInstance(baseinstance);
|
||||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
|
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,
|
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||||
GLsizei instancecount, GLint basevertex) {
|
GLsizei instancecount, GLint basevertex) {
|
||||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||||
PrepareForDraw(syncBit);
|
PrepareForDraw(syncBit);
|
||||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
|
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||||
GLsizei instancecount, GLuint baseinstance) {
|
GLsizei instancecount, GLuint baseinstance) {
|
||||||
DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||||
PrepareForDraw(syncBit);
|
PrepareForDraw(syncBit);
|
||||||
SetCurrentBaseInstance(baseinstance);
|
SetCurrentBaseInstance(baseinstance);
|
||||||
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
|
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) {
|
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);
|
PrepareForDraw(syncBit);
|
||||||
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
|
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
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);
|
PrepareForDraw(syncBit);
|
||||||
|
|
||||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
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,
|
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||||
GLuint baseinstance) {
|
GLuint baseinstance) {
|
||||||
DrawSyncBit syncBit = DrawSyncBit::Instancing;
|
DrawSyncFlags syncBit = DrawSyncBit::Instancing;
|
||||||
PrepareForDraw(syncBit);
|
PrepareForDraw(syncBit);
|
||||||
SetCurrentBaseInstance(baseinstance);
|
SetCurrentBaseInstance(baseinstance);
|
||||||
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
|
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) {
|
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
|
||||||
DrawSyncBit syncBit = DrawSyncBit::Instancing;
|
DrawSyncFlags syncBit = DrawSyncBit::Instancing;
|
||||||
PrepareForDraw(syncBit);
|
PrepareForDraw(syncBit);
|
||||||
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
|
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
||||||
DrawSyncBit syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
||||||
PrepareForDraw(syncBit);
|
PrepareForDraw(syncBit);
|
||||||
|
|
||||||
const auto* commandBytes =
|
const auto* commandBytes =
|
||||||
@@ -7251,6 +7216,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void DestroyEGLContext() {
|
void DestroyEGLContext() {
|
||||||
BufferImpl::OnBackendContextDestroyed();
|
BufferImpl::OnBackendContextDestroyed();
|
||||||
XfbImpl::OnBackendContextDestroyed();
|
XfbImpl::OnBackendContextDestroyed();
|
||||||
|
MultiDrawImpl::OnBackendContextDestroyed();
|
||||||
ScratchFBOImpl::OnBackendContextDestroyed();
|
ScratchFBOImpl::OnBackendContextDestroyed();
|
||||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||||
VertexArrayImpl::InvalidateVAOBindingCache();
|
VertexArrayImpl::InvalidateVAOBindingCache();
|
||||||
|
|||||||
@@ -182,6 +182,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
namespace XfbImpl {
|
namespace XfbImpl {
|
||||||
Bool AreTransformFeedbacksSupported();
|
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 BeginTransformFeedback(GLenum primitiveMode);
|
||||||
void EndTransformFeedback();
|
void EndTransformFeedback();
|
||||||
void PauseTransformFeedback();
|
void PauseTransformFeedback();
|
||||||
|
|||||||
@@ -36,6 +36,53 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Bool InProcessTeardown();
|
Bool InProcessTeardown();
|
||||||
void EnsureProcessTeardownSentinel();
|
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>
|
template <typename StateObject, typename BackendObject>
|
||||||
class StateBackendObjectRegistry {
|
class StateBackendObjectRegistry {
|
||||||
public:
|
public:
|
||||||
@@ -866,6 +913,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void SetBaseInstance(Uint32 baseInstance) const;
|
void SetBaseInstance(Uint32 baseInstance) const;
|
||||||
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
|
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
|
||||||
void SetDrawID(Uint32 drawId) 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; }
|
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
||||||
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
||||||
// False when the last SyncToBackend could not produce a usable program (a
|
// 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/OrientationScenario.cpp
|
||||||
Scenarios/CrossFrameBufferScenario.cpp
|
Scenarios/CrossFrameBufferScenario.cpp
|
||||||
Scenarios/ResidentIndexScenario.cpp
|
Scenarios/ResidentIndexScenario.cpp
|
||||||
|
Scenarios/MultiDrawScenario.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
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 &&
|
caps.SupportsMultiDrawElementsBaseVertex = hasDrawElementsBaseVertexExtension &&
|
||||||
hasMultiDrawArraysExtension &&
|
hasMultiDrawArraysExtension &&
|
||||||
glesFuncs.glMultiDrawElementsBaseVertexEXT != nullptr;
|
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.SupportsShaderMultisampleInterpolation || caps.GLESVersion.Major > 3 ||
|
caps.SupportsShaderMultisampleInterpolation || caps.GLESVersion.Major > 3 ||
|
||||||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);
|
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);
|
||||||
@@ -907,6 +919,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.SupportsMultiDrawIndirect ? "yes" : "no");
|
caps.SupportsMultiDrawIndirect ? "yes" : "no");
|
||||||
MGLOG_I(" multi-draw base vertex (EXT/OES_draw_elements_base_vertex + EXT_multi_draw_arrays): %s",
|
MGLOG_I(" multi-draw base vertex (EXT/OES_draw_elements_base_vertex + EXT_multi_draw_arrays): %s",
|
||||||
caps.SupportsMultiDrawElementsBaseVertex ? "yes" : "no");
|
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:");
|
MGLOG_I("OpenGL ES capabilities:");
|
||||||
glesFuncs.glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &caps.UniformBufferOffsetAlignment);
|
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
|
// 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.
|
// AND a resolved pointer; callers must gate on it, never on the pointer.
|
||||||
Bool SupportsMultiDrawElementsBaseVertex = false;
|
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".
|
// GL_RENDERER contains "ANGLE".
|
||||||
Bool IsAngleRenderer = false;
|
Bool IsAngleRenderer = false;
|
||||||
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <MGGitHash.h>
|
#include <MGGitHash.h>
|
||||||
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
|
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
|
||||||
|
#include <MG_Backend/DirectGLES/MultiDraw.h>
|
||||||
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
|
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
|
||||||
// Only for the compile-time MAX_VERTEX_ATTRIBS constant asserted below. The POST still executes no
|
// 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().
|
// MG_State code: it runs standalone, before MG_State::Init().
|
||||||
@@ -319,9 +320,46 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
} else {
|
} else {
|
||||||
builder.Info("Multi-draw base vertex",
|
builder.Info("Multi-draw base vertex",
|
||||||
"glMultiDrawElementsBaseVertexEXT not supported (needs EXT/OES_"
|
"glMultiDrawElementsBaseVertexEXT not supported (needs EXT/OES_"
|
||||||
"draw_elements_base_vertex plus GL_EXT_multi_draw_arrays); "
|
"draw_elements_base_vertex plus GL_EXT_multi_draw_arrays); the batch "
|
||||||
"glMultiDrawElementsBaseVertex falls back to a per-draw loop with "
|
"takes the next emulation tier instead, with identical output - see "
|
||||||
"identical output");
|
"\"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) {
|
if (caps.SupportsTextureBorderClamp) {
|
||||||
builder.Pass("Texture border clamp",
|
builder.Pass("Texture border clamp",
|
||||||
|
|||||||
Reference in New Issue
Block a user