mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
1500 lines
86 KiB
C++
1500 lines
86 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESDraw.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 "DirectGLESInternal.h"
|
|
|
|
namespace MobileGL::MG_Backend::DirectGLES {
|
|
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
|
|
// P5c (hd, CONTRACT-P5C §3.5): under an active transport the command buffer resolves
|
|
// from the handle the verb record carried, and its CPU bytes are the SERVER's staged
|
|
// shadow (rule E) - the client's GL_DRAW_INDIRECT_BUFFER binding slot, the frontend
|
|
// object's MappedData()/GetSize() (B3) and the client slot allocator (T2) are never
|
|
// read. Coverage is asserted: commands read from bytes no record staged would be
|
|
// zero-filled, which is a missing record, not a valid command stream.
|
|
const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
|
|
if (!MG_Pipe::MGPipeHandleIsNull(handle)) {
|
|
auto* resource = BufferImpl::FindBufferResourceForHandle(handle);
|
|
const Uint8* base = resource ? BufferImpl::SplitHostBytes(*resource) : nullptr;
|
|
// P8-C: a store nothing ever staged but a shader wrote (the GPU-culling shape)
|
|
// takes its first copy from this server's own GL buffer. A store that HAS a copy
|
|
// is not refreshed here: most indirect draws execute natively and read no field
|
|
// on the CPU, so ExecuteIndexed/ArraysIndirectCommands refresh the fields only
|
|
// when they consume them.
|
|
if (base == nullptr && resource != nullptr) {
|
|
base = BufferImpl::SplitHostBytesForCpuRead(*resource, handle, "indirect_command_bytes");
|
|
}
|
|
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
|
if (base == nullptr) {
|
|
MGLOG_E_ONCE("%s skipped: the verb's indirect buffer {%u, %u} has no staged "
|
|
"shadow on this side and no GPU write to take one from",
|
|
label, handle.Slot, handle.Gen);
|
|
return nullptr;
|
|
}
|
|
if (commandOffset + requiredBytes > BufferImpl::ResourceWidthForHandle(handle)) {
|
|
MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
|
|
return nullptr;
|
|
}
|
|
BufferImpl::RequireStagedCoverage(*resource, base, commandOffset, commandOffset + requiredBytes,
|
|
"indirect_command_bytes");
|
|
return base + commandOffset;
|
|
}
|
|
// No buffer was bound: the frontend already raised the GL error; keep the old
|
|
// fall-through shape (a null indirect pointer is the same skip it was).
|
|
if (!indirect) {
|
|
MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
|
|
return nullptr;
|
|
}
|
|
MGLOG_E_ONCE("%s skipped: the verb carried no indirect buffer handle", label);
|
|
return nullptr;
|
|
}
|
|
|
|
// TODO: deletion for deleted objects
|
|
|
|
namespace BufferImpl {
|
|
// P5e (mv): THE ARM SELECTOR of this family - VertexInputReadsRecords() - moved to
|
|
// Managers.h, unchanged. MultiDraw.cpp is a second translation unit on the same draw
|
|
// path and has to select the same arm; a copy of the conjunction there would be two
|
|
// selectors for one family. Its rationale traveled with it.
|
|
|
|
// P5e (vi): "does this draw fetch any attribute out of the application's own memory",
|
|
// answered FROM THE RECORD instead of from the frontend VAO. The emitter publishes
|
|
// Res == kMGPipeNullHandle for a client-memory array (VertexInputEmit.h says so in as
|
|
// many words: "A client-memory array is Res == kMGPipeNullHandle, and that is not a
|
|
// hole"), and the window is truncated at the highest ENABLED attribute, so a null Res
|
|
// inside [Start, Start+Count) is exactly the client-sourced case and nothing else.
|
|
//
|
|
// BOTH VIEWS ARE NEEDED, and taking only the buffer one would be wrong in the common
|
|
// direction: the window covers [0, highest enabled + 1), so a VAO whose attribute 0 is
|
|
// DISABLED and whose attribute 1 feeds from a VBO publishes entry[0].Res == null too.
|
|
// A null Res is "client-sourced" only for an attribute the CONFIGURATION has enabled,
|
|
// which is what the vertex-elements record answers. Reading one view and not the other
|
|
// would send every such ordinary draw back to the frontend VAO and keep the row this
|
|
// package is retiring alive for no reason.
|
|
//
|
|
// A prefix scan rather than a memo: Count is 1-3 on the measured workload, not 32, and
|
|
// this replaces a cross-TU accessor call plus a SharedPtr dereference per DrawArrays.
|
|
Bool AnyClientSideVertexArrayInRecord() {
|
|
const auto& st = MG_Pipe::MGPipeApplier();
|
|
if (st.VertexBufferCount == 0) return false;
|
|
const MG_Pipe::MGPipeHandle elements = st.BoundVertexElements;
|
|
if (MG_Pipe::MGPipeHandleIsNull(elements) || elements.Slot >= st.VertexElementsCsos.size()) {
|
|
return false;
|
|
}
|
|
const auto& record = st.VertexElementsCsos[elements.Slot];
|
|
if (!record.Live || record.Gen != elements.Gen) return false;
|
|
|
|
const Uint32 begin = st.VertexBufferStart;
|
|
const Uint32 end = begin + st.VertexBufferCount;
|
|
for (Uint32 i = begin; i < end && i < MG_Pipe::kMGPipeMaxVertexAttribs; ++i) {
|
|
if (!record.Attributes[i].Enabled) continue;
|
|
if (MG_Pipe::MGPipeHandleIsNull(st.VertexBuffers[i].Res)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
} // namespace BufferImpl
|
|
|
|
namespace VertexArrayImpl {
|
|
// The indirect executors' form: the command's own words ARE the draw's fetch, and a
|
|
// command block a shader wrote this frame is exactly what these shapes exist for - so the
|
|
// command buffer's shadow is reconciled before it is read, and `commandBytes` (the
|
|
// caller's possibly stale copy) is only the fallback for a client-authored block.
|
|
static Bool SyncClientSideVertexArraysForIndirectFetch(
|
|
const Uint8* commandBytes, SizeT commandOffset, GLsizei stride, GLsizei index, Uint8 indexSize,
|
|
const SharedPtr<MG_State::GLState::BufferObject>& commandBuffer) {
|
|
return true;
|
|
}
|
|
|
|
// The indexed entry points' form. `indices` is a byte offset into the bound element buffer
|
|
// when one is bound and the application's own array otherwise, which is the same
|
|
// distinction every one of them already makes when it issues the draw.
|
|
static Bool SyncClientSideVertexArraysForIndexedFetch(GLenum type, GLsizei count, const void* indices,
|
|
GLsizei instanceCount, GLint baseVertex,
|
|
GLuint baseInstance) {
|
|
if (count <= 0 || instanceCount <= 0) return true;
|
|
// MONOLITH ONLY - see SyncClientSideVertexArraysForFetch; this wrapper's own read of
|
|
// the bound VAO is the frontend read that must not happen on an apply thread.
|
|
return true;
|
|
}
|
|
|
|
static void SyncClientSideVertexArraysForDrawArrays(GLint first, GLsizei count) {
|
|
if (!BufferImpl::AnyClientSideVertexArrayInRecord()) return;
|
|
// P13: client-memory arrays are monolith-only - under a transport the client refuses
|
|
// them (Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"}) before any record goes out,
|
|
// so a record naming one here means the bytes below belong to another process.
|
|
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
|
MGLOG_F("MGPipe: Fatal{RoleViolation, \"ClientArrays\"} - a draw carrying a "
|
|
"client-memory vertex array reached the backend under a transport; the "
|
|
"client's refusal exists to make this unreachable");
|
|
std::abort();
|
|
}
|
|
auto* twin = ResolveVaoTwin(MG_Pipe::MGPipeApplier().BoundVertexElements);
|
|
if (twin == nullptr) return;
|
|
const auto& currentVAO = MG_Pipe::gPipeInputs->GetBoundVertexArray();
|
|
if (!currentVAO) return;
|
|
twin->SyncClientSideAttributesForDrawArrays(currentVAO, first, count);
|
|
return;
|
|
}
|
|
} // namespace VertexArrayImpl
|
|
|
|
// ---- gl_ViewportIndex routing emulation, draw half ---------------------------------------
|
|
// See the block comment in Managers.h for what this is and why. Here is the state half: one
|
|
// replay pass per DISTINCT viewport state, each pushing that state onto the ES context's one
|
|
// viewport / one scissor / one depth range and telling the fragment gate which indices it
|
|
// serves.
|
|
|
|
namespace ViewportRoutingImpl {
|
|
// One replay pass: the state to push, and the set of gl_ViewportIndex values whose
|
|
// fragments this pass is allowed to keep.
|
|
struct RoutingPass {
|
|
IntVec4 viewport{};
|
|
IntVec4 scissorBox{};
|
|
FloatVec2 depthRange{};
|
|
Bool scissorTest = false;
|
|
Uint32 indexMask = 0;
|
|
};
|
|
|
|
static constexpr Uint32 kAllViewportsMask = (RenderStateParameters::MAX_VIEWPORTS >= 32)
|
|
? 0xFFFFFFFFu
|
|
: ((1u << RenderStateParameters::MAX_VIEWPORTS) - 1u);
|
|
|
|
// The plan for the draw currently being issued. A file-scope buffer rather than a return
|
|
// value because Begin/Apply/End are three calls around a draw the caller writes, and a
|
|
// fixed array of 16 keeps it allocation-free on a path that is per draw. NOT re-entrant,
|
|
// which is a property of the call sites and not an accident: every wrap in this file and
|
|
// in MultiDraw.cpp is around the innermost native glDraw*, so no replay can begin inside
|
|
// another - and a multi-draw tier that replayed its whole loop would be nesting.
|
|
static Array<RoutingPass, RenderStateParameters::MAX_VIEWPORTS> g_passes{};
|
|
static Uint g_passCount = 0;
|
|
static PrgramImpl::BackendProgramObjectImpl* g_routedProgram = nullptr;
|
|
|
|
// What index `i` actually rasterizes against, resolved exactly the way SyncRenderState
|
|
// resolves index 0 - including both substitutions it makes, which are not cosmetic:
|
|
//
|
|
// * a viewport of zero extent means "the application has never called glViewport", and
|
|
// GL's initial viewport is the whole surface, which the frontend cannot spell before
|
|
// a surface exists;
|
|
// * a scissor rectangle is read through the WRITTEN flag and not through its extent,
|
|
// because glScissor(0, 0, 0, 0) is a legal request meaning "reject every fragment"
|
|
// and is byte-identical to the never-written default that means the opposite.
|
|
//
|
|
// Resolving them here rather than deferring to SyncRenderState is what makes the grouping
|
|
// below correct: two indices that differ only in a field that resolves to the same
|
|
// rectangle really do rasterize identically and must share one pass.
|
|
static RoutingPass ResolveIndexState(const RenderStateParameters& parameters, Uint index, Int surfaceWidth,
|
|
Int surfaceHeight) {
|
|
RoutingPass pass;
|
|
const FloatVec4& viewport = parameters.Viewports[index];
|
|
pass.viewport =
|
|
IntVec4(static_cast<Int>(std::lround(viewport.x())), static_cast<Int>(std::lround(viewport.y())),
|
|
static_cast<Int>(std::lround(viewport.z())), static_cast<Int>(std::lround(viewport.w())));
|
|
if ((pass.viewport.z() <= 0 || pass.viewport.w() <= 0) && surfaceWidth > 0 && surfaceHeight > 0) {
|
|
pass.viewport = IntVec4(0, 0, surfaceWidth, surfaceHeight);
|
|
}
|
|
pass.scissorBox = parameters.ScissorBoxes[index];
|
|
if ((parameters.ScissorBoxWrittenMask & (1u << index)) == 0 && surfaceWidth > 0 && surfaceHeight > 0) {
|
|
pass.scissorBox = IntVec4(0, 0, surfaceWidth, surfaceHeight);
|
|
}
|
|
pass.depthRange = parameters.DepthRanges[index];
|
|
pass.scissorTest = (parameters.ScissorTestEnabledMask & (1u << index)) != 0;
|
|
return pass;
|
|
}
|
|
|
|
static Bool SameState(const RoutingPass& a, const RoutingPass& b) {
|
|
return a.viewport == b.viewport && a.scissorBox == b.scissorBox && a.depthRange == b.depthRange &&
|
|
a.scissorTest == b.scissorTest;
|
|
}
|
|
} // namespace ViewportRoutingImpl
|
|
|
|
Uint BeginViewportRoutingPasses() {
|
|
using namespace ViewportRoutingImpl;
|
|
g_passCount = 1;
|
|
g_routedProgram = nullptr;
|
|
|
|
// The whole emulation behind one static load, for every application that has never built
|
|
// a program writing gl_ViewportIndex - which is all of them but the conformance suite.
|
|
// Without it every draw in the process would pay GetCurrentBackendProgram's chain of
|
|
// frontend lookups for an answer that cannot change.
|
|
if (!g_anyProgramRoutesViewportIndex) {
|
|
return 1;
|
|
}
|
|
|
|
auto* program = GetCurrentBackendProgram();
|
|
if (program == nullptr || !program->RoutesViewportIndex()) {
|
|
return 1;
|
|
}
|
|
g_routedProgram = program;
|
|
// The gate reads zero until something writes it, and a zero mask discards every fragment.
|
|
// So this is not an optimization that can be skipped in the one-pass case - it is what
|
|
// keeps a routing program drawing at all.
|
|
program->SetViewportPassMask(kAllViewportsMask);
|
|
|
|
// Replaying multiplies every side effect the vertex and geometry stages have, and the
|
|
// fragment gate can only undo the ones that happen in the FRAGMENT stage. Transform
|
|
// feedback records per emitted primitive, so a replayed draw would write its vertices N
|
|
// times; rasterizer discard means there are no fragments to gate at all, so replaying
|
|
// would be pure cost with nothing to show for it. Both fall back to a single pass with an
|
|
// open gate, i.e. to the pre-emulation behavior, rather than to wrong data.
|
|
if (MG_Pipe::gPipeInputs->IsTransformFeedbackActive() ||
|
|
MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
|
|
return 1;
|
|
}
|
|
|
|
const auto& parameters = MG_Pipe::gPipeInputs->GetRenderStateParameters();
|
|
Int surfaceWidth = 0;
|
|
Int surfaceHeight = 0;
|
|
if (!QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) {
|
|
surfaceWidth = 0;
|
|
surfaceHeight = 0;
|
|
}
|
|
|
|
Uint count = 0;
|
|
for (Uint index = 0; index < RenderStateParameters::MAX_VIEWPORTS; ++index) {
|
|
const RoutingPass resolved = ResolveIndexState(parameters, index, surfaceWidth, surfaceHeight);
|
|
Uint existing = 0;
|
|
for (; existing < count; ++existing) {
|
|
if (SameState(g_passes[existing], resolved)) break;
|
|
}
|
|
if (existing == count) {
|
|
g_passes[count] = resolved;
|
|
++count;
|
|
}
|
|
g_passes[existing].indexMask |= (1u << index);
|
|
}
|
|
|
|
// One group is the overwhelmingly common case - it is what glViewport, glScissor and
|
|
// glDepthRange leave behind, because ARB_viewport_array defines all three as writing
|
|
// EVERY index. The mask is already open and index 0's state is what SyncRenderState
|
|
// pushed, so there is nothing to replay and nothing to restore.
|
|
if (count <= 1) {
|
|
g_passCount = 1;
|
|
return 1;
|
|
}
|
|
g_passCount = count;
|
|
return count;
|
|
}
|
|
|
|
void ApplyViewportRoutingPass(Uint pass) {
|
|
using namespace ViewportRoutingImpl;
|
|
if (pass >= g_passCount || g_routedProgram == nullptr) {
|
|
return;
|
|
}
|
|
const RoutingPass& entry = g_passes[pass];
|
|
g_GLESFuncs.glViewport(entry.viewport.x(), entry.viewport.y(), entry.viewport.z(), entry.viewport.w());
|
|
g_GLESFuncs.glScissor(entry.scissorBox.x(), entry.scissorBox.y(), entry.scissorBox.z(), entry.scissorBox.w());
|
|
// ES has one scissor-test enable where GL has sixteen, so the per-index bit becomes a
|
|
// per-pass glEnable/glDisable. This is the half DirectVulkan cannot do at all (Vulkan has
|
|
// no per-viewport scissor toggle either and has to widen a disabled index's rectangle to
|
|
// the whole framebuffer instead); here the rectangle stays honest.
|
|
entry.scissorTest ? g_GLESFuncs.glEnable(GL_SCISSOR_TEST) : g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
|
g_GLESFuncs.glDepthRangef(entry.depthRange.x(), entry.depthRange.y());
|
|
g_routedProgram->SetViewportPassMask(entry.indexMask);
|
|
}
|
|
|
|
void EndViewportRoutingPasses(Uint passCount) {
|
|
using namespace ViewportRoutingImpl;
|
|
if (passCount <= 1) {
|
|
// Nothing was pushed and the mask is already open; leaving the shadow alone here is
|
|
// what keeps a non-routing draw at exactly its previous cost.
|
|
g_routedProgram = nullptr;
|
|
return;
|
|
}
|
|
if (g_routedProgram != nullptr) {
|
|
// Any draw that reaches the driver without going through a replay - an internal blit,
|
|
// or a path this emulation has not been taught about - must not inherit the last
|
|
// pass's mask and paint nothing.
|
|
g_routedProgram->SetViewportPassMask(kAllViewportsMask);
|
|
}
|
|
g_routedProgram = nullptr;
|
|
g_passCount = 0;
|
|
// The viewport, scissor, scissor-test enable and depth range now on the ES context belong
|
|
// to the last replay pass, and the shadow SyncRenderState diffs against does not know it.
|
|
// A full resync is the honest repair and costs one state push on the next draw, which
|
|
// only a viewport-routing workload ever pays.
|
|
RenderStateImpl::InvalidateSyncedRenderState();
|
|
}
|
|
|
|
static Bool SupportsNativeIndirectDraws() {
|
|
return g_GLESCapabilities.SupportsDrawIndirect;
|
|
}
|
|
|
|
// P8-C: the command bytes an indirect executor is about to CONSUME on the CPU, made current.
|
|
// Consumed means: every field on the CPU path (no native indirect), or on the native path the
|
|
// uniforms fed from the command - gl_BaseVertex whenever the program reads it, and
|
|
// gl_BaseInstance when the program reads it without the mg_IndirectParams storage view that
|
|
// would give it the GPU's word. A native draw that feeds neither reads nothing and keeps its
|
|
// pointer, so a compute-culled indirect buffer drawn every frame costs no readback.
|
|
static const Uint8* CurrentIndirectCommandBytes(MG_Pipe::MGPipeHandle handle, const Uint8* commandBytes,
|
|
SizeT commandOffset, Bool useNative, Bool indexed) {
|
|
if (MG_Pipe::MGPipeHandleIsNull(handle)) return commandBytes;
|
|
if (useNative) {
|
|
const auto program = GetCurrentBackendProgram();
|
|
if (program == nullptr) return commandBytes;
|
|
const Bool baseVertexFromCpu = indexed && program->ReadsBaseVertex();
|
|
const Bool baseInstanceFromCpu = program->GetIndirectParamsBinding() < 0 && program->ReadsBaseInstance();
|
|
if (!baseVertexFromCpu && !baseInstanceFromCpu) return commandBytes;
|
|
}
|
|
auto* resource = BufferImpl::FindBufferResourceForHandle(handle);
|
|
if (resource == nullptr) return commandBytes;
|
|
const Uint8* base = BufferImpl::SplitHostBytesForCpuRead(*resource, handle, "indirect_command_fields");
|
|
return base != nullptr ? base + commandOffset : commandBytes;
|
|
}
|
|
|
|
// Runs an (indexed) indirect multi-draw. When a GL_DRAW_INDIRECT_BUFFER is bound the draws
|
|
// execute natively on the GPU so commands written by compute shaders (e.g. Flywheel's
|
|
// culling pipeline updating instanceCount) are honored; the CPU shadow is still consulted
|
|
// for the per-command baseInstance, which is CPU-authored, to feed the mg_BaseInstance
|
|
// shader emulation. Without GL_EXT_base_instance a non-zero baseInstance in the command is
|
|
// technically undefined in ES; mobile drivers ignore the reserved word, instanced-array
|
|
// fetches were never baseInstance-offset here anyway, and the CPU fallback cannot see
|
|
// GPU-written command fields at all - so native is never worse. ANGLE-on-Vulkan instead
|
|
// leaks the word into gl_InstanceID (it becomes vkCmdDrawIndexedIndirect's firstInstance);
|
|
// the shader rewrite compensates by rebasing gl_InstanceID during these draws when
|
|
// IndirectDrawInstanceIdIncludesBaseInstance is set (PromoteDrawParameterGlobalsToUniforms).
|
|
// Only client-memory commands take the CPU per-command loop.
|
|
static void ExecuteIndexedIndirectCommands(GLenum mode, GLenum type, SizeT indexSize, const Uint8* commandBytes,
|
|
SizeT commandOffset,
|
|
const SharedPtr<MG_State::GLState::BufferObject>& drawIndirectBuffer,
|
|
GLsizei drawcount, GLsizei stride, const char* label) {
|
|
(void)label;
|
|
// An indirect command's firstIndex/count live in GPU memory, so the substitution has
|
|
// to rewrite the whole element array buffer rather than this draw's range - which is
|
|
// exactly what it does when no CPU-known count is handed to it. Held for the whole
|
|
// command loop so every command in the batch reads the rewritten copy.
|
|
//
|
|
// firstIndex counts ELEMENTS, so it survives a widened copy untouched; what does not
|
|
// survive is the type and the element size, which are re-taken from the substitution
|
|
// below for both the native and the CPU-unrolled path.
|
|
const ScopedRestartIndexSubstitution restart(type, /*count=*/0, /*indices=*/nullptr);
|
|
if (!restart.DrawIsValid()) return;
|
|
type = restart.IndexType();
|
|
indexSize = MG_Util::GetGLTypeSize(type);
|
|
// P5c (hd, CONTRACT-P5C §3.5): under an active transport the command buffer is the
|
|
// handle the verb record carried (MGPipeApplier's verb stash) and the SSBO view's
|
|
// resource resolves from it by handle - the frontend binding slot and the client slot
|
|
// allocator are never read (T2).
|
|
const MG_Pipe::MGPipeHandle verbBufferHandle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
|
|
const Bool useNative = !MG_Pipe::MGPipeHandleIsNull(verbBufferHandle) && SupportsNativeIndirectDraws();
|
|
commandBytes = CurrentIndirectCommandBytes(verbBufferHandle, commandBytes, commandOffset, useNative,
|
|
/*indexed=*/true);
|
|
if (useNative) {
|
|
// gl_BaseInstance must observe GPU-written command fields; expose the indirect
|
|
// buffer to the program's mg_IndirectParams SSBO view and address it per draw.
|
|
const auto backendProgram = GetCurrentBackendProgram();
|
|
const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1;
|
|
if (paramsBinding >= 0) {
|
|
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, verbBufferHandle);
|
|
if (resource && resource->id != 0) {
|
|
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(paramsBinding),
|
|
resource->id);
|
|
}
|
|
}
|
|
// gl_BaseVertex has no SSBO view of its own: the command's baseVertex word is read
|
|
// from the CPU shadow, so a command whose baseVertex a compute shader wrote this
|
|
// frame is not observable here (baseInstance is, through the view above). Feeding
|
|
// the stale-but-usually-correct shadow beats leaving the uniform at the previous
|
|
// draw's value, which is what a program reading gl_BaseVertex saw before.
|
|
const Bool feedBaseVertex = CurrentProgramReadsBaseVertex();
|
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
|
const SizeT cmdByteOffset = commandOffset + static_cast<SizeT>(i) * stride;
|
|
SetCurrentDrawID(static_cast<Uint32>(i));
|
|
if (paramsBinding >= 0 && backendProgram) {
|
|
// baseInstance is the 5th word of DrawElementsIndirectCommand.
|
|
backendProgram->SetBaseInstanceWordIndex(static_cast<Int32>((cmdByteOffset + 16) / 4));
|
|
if (feedBaseVertex) {
|
|
DrawElementsIndirectCommand cmd{};
|
|
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
|
SetCurrentBaseVertex(cmd.baseVertex);
|
|
}
|
|
} else {
|
|
DrawElementsIndirectCommand cmd{};
|
|
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
|
SetCurrentBaseInstance(cmd.baseInstance);
|
|
SetCurrentBaseVertex(cmd.baseVertex);
|
|
}
|
|
ForEachViewportRoutingPass([&] {
|
|
if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch(
|
|
commandBytes, commandOffset, stride, i, static_cast<Uint8>(indexSize), drawIndirectBuffer))
|
|
return;
|
|
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(cmdByteOffset));
|
|
});
|
|
}
|
|
} else {
|
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
|
DrawElementsIndirectCommand cmd{};
|
|
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
|
if (cmd.count == 0 || cmd.instanceCount == 0) {
|
|
continue;
|
|
}
|
|
SetCurrentDrawID(static_cast<Uint32>(i));
|
|
SetCurrentBaseInstance(cmd.baseInstance);
|
|
SetCurrentBaseVertex(cmd.baseVertex);
|
|
const auto indexByteOffset = static_cast<SizeT>(cmd.firstIndex) * indexSize;
|
|
ForEachViewportRoutingPass([&] {
|
|
if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch(
|
|
commandBytes, commandOffset, stride, i, static_cast<Uint8>(indexSize), drawIndirectBuffer))
|
|
return;
|
|
g_GLESFuncs.glDrawElementsInstancedBaseVertex(
|
|
mode, static_cast<GLsizei>(cmd.count), type, reinterpret_cast<const GLvoid*>(indexByteOffset),
|
|
static_cast<GLsizei>(cmd.instanceCount), cmd.baseVertex);
|
|
});
|
|
}
|
|
}
|
|
SetCurrentDrawID(0);
|
|
SetCurrentBaseInstance(0);
|
|
SetCurrentBaseVertex(0);
|
|
}
|
|
|
|
static void ExecuteArraysIndirectCommands(GLenum mode, const Uint8* commandBytes, SizeT commandOffset,
|
|
const SharedPtr<MG_State::GLState::BufferObject>& drawIndirectBuffer,
|
|
GLsizei drawcount, GLsizei stride, const char* label) {
|
|
(void)label;
|
|
// DrawArraysIndirectCommand has no baseVertex word, so gl_BaseVertex is zero for every
|
|
// command here. Written BEFORE the draws, not merely restored after them: the previous
|
|
// draw is what leaves a stale value, and restoring afterwards would only protect the
|
|
// NEXT draw while these commands ran with the stale one.
|
|
SetCurrentBaseVertex(0);
|
|
// P5c (hd, CONTRACT-P5C §3.5): see ExecuteIndexedIndirectCommands - the verb's handle,
|
|
// never the frontend binding slot or the client allocator.
|
|
const MG_Pipe::MGPipeHandle verbBufferHandle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
|
|
const Bool useNative = !MG_Pipe::MGPipeHandleIsNull(verbBufferHandle) && SupportsNativeIndirectDraws();
|
|
commandBytes = CurrentIndirectCommandBytes(verbBufferHandle, commandBytes, commandOffset, useNative,
|
|
/*indexed=*/false);
|
|
if (useNative) {
|
|
const auto backendProgram = GetCurrentBackendProgram();
|
|
const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1;
|
|
if (paramsBinding >= 0) {
|
|
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, verbBufferHandle);
|
|
if (resource && resource->id != 0) {
|
|
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(paramsBinding),
|
|
resource->id);
|
|
}
|
|
}
|
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
|
const SizeT cmdByteOffset = commandOffset + static_cast<SizeT>(i) * stride;
|
|
SetCurrentDrawID(static_cast<Uint32>(i));
|
|
if (paramsBinding >= 0 && backendProgram) {
|
|
// baseInstance is the 4th word of DrawArraysIndirectCommand.
|
|
backendProgram->SetBaseInstanceWordIndex(static_cast<Int32>((cmdByteOffset + 12) / 4));
|
|
} else {
|
|
DrawArraysIndirectCommand cmd{};
|
|
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
|
SetCurrentBaseInstance(cmd.baseInstance);
|
|
}
|
|
ForEachViewportRoutingPass([&] {
|
|
if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch(commandBytes, commandOffset,
|
|
stride, i, 0, drawIndirectBuffer))
|
|
return;
|
|
g_GLESFuncs.glDrawArraysIndirect(mode, reinterpret_cast<const void*>(cmdByteOffset));
|
|
});
|
|
}
|
|
} else {
|
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
|
DrawArraysIndirectCommand cmd{};
|
|
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
|
if (cmd.count == 0 || cmd.instanceCount == 0) {
|
|
continue;
|
|
}
|
|
SetCurrentDrawID(static_cast<Uint32>(i));
|
|
SetCurrentBaseInstance(cmd.baseInstance);
|
|
ForEachViewportRoutingPass([&] {
|
|
if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch(commandBytes, commandOffset,
|
|
stride, i, 0, drawIndirectBuffer))
|
|
return;
|
|
g_GLESFuncs.glDrawArraysInstanced(mode, static_cast<GLint>(cmd.first),
|
|
static_cast<GLsizei>(cmd.count),
|
|
static_cast<GLsizei>(cmd.instanceCount));
|
|
});
|
|
}
|
|
}
|
|
SetCurrentDrawID(0);
|
|
SetCurrentBaseInstance(0);
|
|
}
|
|
|
|
void Clear(GLbitfield mask) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
TextureImpl::SyncNeccessaryTextures();
|
|
FramebufferImpl::SyncCurrentFBO();
|
|
// A color clear is exactly the operation that is allowed to write a widened
|
|
// attachment's alpha - it is what puts the 1.0 there that every later draw is masked
|
|
// away from. SyncCurrentFBO ran first, so g_alphaWidenedDrawBufferMask already describes
|
|
// the framebuffer this clear will land on.
|
|
RenderStateImpl::SyncRenderState(/*forColorClear=*/(mask & GL_COLOR_BUFFER_BIT) != 0);
|
|
|
|
BindCurrentFBO(FramebufferTarget::Draw);
|
|
|
|
// Debug-only diagnostics: is each offscreen clear complete and unscissored?
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
|
{
|
|
static int diagCount = 0;
|
|
GLint fboId = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &fboId);
|
|
if (fboId != 0 && diagCount++ < 900) {
|
|
GLint color0 = 0, depthName = 0, box[4] = {0};
|
|
GLboolean scissor = g_GLESFuncs.glIsEnabled(GL_SCISSOR_TEST);
|
|
GLboolean cmask[4] = {0};
|
|
GLfloat cc[4] = {0};
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
|
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &color0);
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
|
|
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &depthName);
|
|
g_GLESFuncs.glGetIntegerv(GL_SCISSOR_BOX, box);
|
|
g_GLESFuncs.glGetBooleanv(GL_COLOR_WRITEMASK, cmask);
|
|
g_GLESFuncs.glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
|
|
MGLOG_D("CLEAR fbo=%d color0=%d depth=%d mask=0x%x scissor=%d box=(%d,%d,%d,%d) cmask=%d%d%d%d "
|
|
"cc=(%g,%g,%g,%g)",
|
|
fboId, color0, depthName, mask, (int)scissor, box[0], box[1], box[2], box[3], (int)cmask[0],
|
|
(int)cmask[1], (int)cmask[2], (int)cmask[3], cc[0], cc[1], cc[2], cc[3]);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// GLES clamps the glClearColor state to [0,1] (desktop GL keeps it unclamped
|
|
// for float color buffers). Minecraft's OIT clears its depth-bounds RGBA32F
|
|
// target to -FLT_MAX as the MAX-blend identity, so an out-of-range clear
|
|
// color must go through glClearBufferfv, which GLES does not clamp.
|
|
GLbitfield remainingMask = mask;
|
|
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
|
|
const FloatVec4& cc = MG_Pipe::gPipeInputs->GetRenderStateParameters().ClearColor;
|
|
const Bool outOfRange = cc.x() < 0.f || cc.x() > 1.f || cc.y() < 0.f || cc.y() > 1.f || cc.z() < 0.f ||
|
|
cc.z() > 1.f || cc.w() < 0.f || cc.w() > 1.f;
|
|
// A widened attachment's stored alpha has to end up 1.0, and glClear applies ONE
|
|
// clear color to every draw buffer - so a framebuffer that mixes a widened
|
|
// attachment with a native one cannot be served by doctoring glClearColor. Take the
|
|
// same per-draw-buffer glClearBufferfv route the out-of-range case already uses and
|
|
// substitute the alpha only where it belongs. Scissor and the color write mask apply
|
|
// to glClearBufferfv exactly as they do to glClear, so a scissored clear stays
|
|
// scissored and an application that masked alpha off still gets its way (the storage
|
|
// then keeps the 1.0 an earlier clear left, which is the same answer).
|
|
//
|
|
// glClearBufferfv on an INTEGER color buffer is GL_INVALID_OPERATION, so a
|
|
// framebuffer with one of those as a draw buffer keeps plain glClear - which ES
|
|
// leaves undefined for integer color buffers anyway, and which an application that
|
|
// wants a defined answer must replace with glClearBufferuiv/iv (those DO substitute
|
|
// the widened alpha). The out-of-range trigger is left exactly as it was.
|
|
const Uint32 widenedDrawBuffers = FramebufferImpl::g_alphaWidenedDrawBufferMask;
|
|
const Bool widenedColorClear =
|
|
widenedDrawBuffers != 0 && FramebufferImpl::g_integerColorDrawBufferMask == 0;
|
|
GLint clearDrawFbo = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &clearDrawFbo);
|
|
if ((outOfRange || widenedColorClear) && clearDrawFbo != 0) {
|
|
GLint maxDrawBuffers = 0;
|
|
GLint clearedCount = 0;
|
|
GLint firstDb = -1;
|
|
g_GLESFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
|
|
for (GLint i = 0; i < maxDrawBuffers; ++i) {
|
|
GLint db = GL_NONE;
|
|
g_GLESFuncs.glGetIntegerv(GL_DRAW_BUFFER0 + static_cast<GLenum>(i), &db);
|
|
if (i == 0) firstDb = db;
|
|
if (db != GL_NONE) {
|
|
const Bool widened = i < 32 && (widenedDrawBuffers & (1u << i)) != 0;
|
|
const GLfloat value[4] = {cc.x(), cc.y(), cc.z(), widened ? 1.0f : cc.w()};
|
|
g_GLESFuncs.glClearBufferfv(GL_COLOR, i, value);
|
|
++clearedCount;
|
|
}
|
|
}
|
|
remainingMask &= ~static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT);
|
|
(void)clearedCount;
|
|
(void)firstDb;
|
|
// Debug-only diagnostics: verify the unclamped clear actually landed.
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
|
{
|
|
static int diagCount = 0;
|
|
if (diagCount++ < 20) {
|
|
const GLenum clrErr = g_GLESFuncs.glGetError();
|
|
GLint prevRead = 0, prevPbo = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevRead);
|
|
g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPbo);
|
|
g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
|
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)clearDrawFbo);
|
|
GLint prevReadBuf = GL_COLOR_ATTACHMENT0;
|
|
g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &prevReadBuf);
|
|
g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0);
|
|
GLfloat rb[4] = {0};
|
|
g_GLESFuncs.glReadPixels(100, 100, 1, 1, GL_RGBA, GL_FLOAT, rb);
|
|
const GLenum rbErr = g_GLESFuncs.glGetError();
|
|
int feDb0 = -1, feDb1 = -1;
|
|
Uint feIdx = 0, feVer = 0;
|
|
// P5e (fb): DEBUG-ONLY, and still a BARRIER_PULLED read. A diagnostic
|
|
// is not a reason to touch the binding slot on an apply thread, so
|
|
// under a transport the same four numbers come off the draw record -
|
|
// the external index has no record counterpart and stays 0, which reads
|
|
// as "the record did not say".
|
|
if (const auto* rec = BoundFramebufferRecord(FramebufferTarget::Draw)) {
|
|
feVer = static_cast<Uint>(rec->ContentHash);
|
|
feDb0 = (int)rec->DrawBuffers[0];
|
|
feDb1 = (int)rec->DrawBuffers[1];
|
|
}
|
|
MGLOG_D("CLEARV fbo=%d clrErr=0x%x rbErr=0x%x cc.x=%g cleared=%d firstDb=0x%x prevReadBuf=0x%x "
|
|
"feFbo=%u feVer=%u feDb=[%d,%d] stored=(%g,%g,%g,%g)",
|
|
clearDrawFbo, clrErr, rbErr, cc.x(), clearedCount, firstDb, prevReadBuf, feIdx, feVer,
|
|
feDb0, feDb1, rb[0], rb[1], rb[2], rb[3]);
|
|
g_GLESFuncs.glReadBuffer(static_cast<GLenum>(prevReadBuf));
|
|
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)prevRead);
|
|
g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, (GLuint)prevPbo);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
if (remainingMask != 0) {
|
|
g_GLESFuncs.glClear(remainingMask);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Arbitrary-index primitive restart
|
|
//
|
|
// Desktop GL restarts on whatever index glPrimitiveRestartIndex named; GLES core only
|
|
// ever restarts on the all-ones value of the index type. When the two agree - which
|
|
// includes every GL_PRIMITIVE_RESTART_FIXED_INDEX user - the render state push at
|
|
// SyncRenderState is the whole implementation and nothing here does any work. When they
|
|
// disagree the index DATA is rewritten into a scratch element array buffer.
|
|
//
|
|
// This used to throw instead. A throw here unwinds a C++ exception through the C GL ABI
|
|
// and takes the process down - the same hazard GL_Texture.cpp and RenderState.cpp
|
|
// already call out - so an application that merely asked for a legal desktop feature
|
|
// died rather than got an error.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
namespace {
|
|
struct RestartScratchBuffer {
|
|
Uint id = 0;
|
|
SizeT capacity = 0;
|
|
};
|
|
|
|
RestartScratchBuffer g_restartIndices;
|
|
Vector<Uint8> g_restartStaging;
|
|
|
|
// Past this the rewrite would stage and re-upload hundreds of megabytes on EVERY
|
|
// draw (the copy is not memoised, exactly as on the Vulkan side). Decline instead of
|
|
// trying: a draw that renders nothing is recoverable, a stall of that size is not.
|
|
constexpr SizeT kMaxRestartRewriteBytes = SizeT{1} << 26; // 64 MiB
|
|
|
|
// The index type one step wider than this one, or 0 when there is none. Widening is how
|
|
// an all-ones value that is a REAL vertex index keeps its meaning while the all-ones
|
|
// value of the destination type serves as the restart sentinel: a source that cannot
|
|
// spell 0xFFFF cannot collide with a 16-bit sentinel, and likewise 8 -> 16.
|
|
GLenum WiderIndexType(GLenum indexType) {
|
|
switch (indexType) {
|
|
case GL_UNSIGNED_BYTE:
|
|
return GL_UNSIGNED_SHORT;
|
|
case GL_UNSIGNED_SHORT:
|
|
return GL_UNSIGNED_INT;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
Uint32 ReadIndex(const Uint8* source, SizeT i, SizeT indexSize) {
|
|
switch (indexSize) {
|
|
case 1:
|
|
return source[i];
|
|
case 2: {
|
|
Uint16 narrow = 0;
|
|
std::memcpy(&narrow, source + i * 2, sizeof(narrow));
|
|
return narrow;
|
|
}
|
|
default: {
|
|
Uint32 wide = 0;
|
|
std::memcpy(&wide, source + i * 4, sizeof(wide));
|
|
return wide;
|
|
}
|
|
}
|
|
}
|
|
|
|
void WriteIndex(Uint8* destination, SizeT i, SizeT indexSize, Uint32 value) {
|
|
switch (indexSize) {
|
|
case 1:
|
|
destination[i] = static_cast<Uint8>(value);
|
|
break;
|
|
case 2: {
|
|
const Uint16 narrow = static_cast<Uint16>(value);
|
|
std::memcpy(destination + i * 2, &narrow, sizeof(narrow));
|
|
break;
|
|
}
|
|
default:
|
|
std::memcpy(destination + i * 4, &value, sizeof(value));
|
|
break;
|
|
}
|
|
}
|
|
|
|
// True when any index in the range already holds the type's all-ones value, i.e. when
|
|
// that value is doing double duty as a real vertex index and so cannot also be the
|
|
// restart sentinel. Only asked on the rare substitution path.
|
|
Bool ContainsFixedRestartIndex(const Uint8* source, SizeT indexCount, SizeT indexSize, Uint32 fixedMax) {
|
|
for (SizeT i = 0; i < indexCount; ++i) {
|
|
if (ReadIndex(source, i, indexSize) == fixedMax) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Copies index data, replacing every occurrence of the application's restart index with
|
|
// the all-ones value of the DESTINATION type - the only one GLES restarts on. The
|
|
// destination may be wider than the source, which is what makes the copy lossless: a
|
|
// source index equal to the source's all-ones value zero-extends to something the wider
|
|
// sentinel can never equal, so it stays the vertex it was.
|
|
//
|
|
// Same width in and out is the degenerate case, used when the source contains no
|
|
// all-ones index at all (nothing to protect) or when there is no wider type to move to.
|
|
// In that last case only - a GL_UNSIGNED_INT stream that really does use index
|
|
// 0xFFFFFFFF while asking to restart on a different one - a legal index has to be
|
|
// nudged to 0xFFFFFFFE, because 32 bits cannot hold both meanings. The caller logs it;
|
|
// it is the one input this feature cannot represent.
|
|
void RewriteRestartIndices(const Uint8* source, SizeT indexCount, SizeT sourceIndexSize,
|
|
SizeT destinationIndexSize, Uint32 applicationRestartIndex,
|
|
Uint32 destinationFixedMax, Vector<Uint8>& output) {
|
|
output.resize(indexCount * destinationIndexSize);
|
|
for (SizeT i = 0; i < indexCount; ++i) {
|
|
Uint32 value = ReadIndex(source, i, sourceIndexSize);
|
|
if (value == applicationRestartIndex) {
|
|
value = destinationFixedMax;
|
|
} else if (value == destinationFixedMax) {
|
|
// Only reachable when no widening was possible; see above.
|
|
value = destinationFixedMax - 1;
|
|
}
|
|
WriteIndex(output.data(), i, destinationIndexSize, value);
|
|
}
|
|
}
|
|
|
|
// Whole-buffer respecify through the manager-wide staging target, so binding it
|
|
// disturbs no VAO state. glBufferData orphans the previous store, so the upload
|
|
// never waits on a draw still reading the old contents out of the same name.
|
|
Bool UploadRestartScratch(SizeT bytes, const void* data) {
|
|
if (g_restartIndices.id == 0) {
|
|
GLuint id = 0;
|
|
g_GLESFuncs.glGenBuffers(1, &id);
|
|
if (id == 0) return false;
|
|
g_restartIndices.id = id;
|
|
g_restartIndices.capacity = 0;
|
|
}
|
|
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_restartIndices.id);
|
|
SizeT capacity = g_restartIndices.capacity == 0 ? bytes : g_restartIndices.capacity;
|
|
while (capacity < bytes)
|
|
capacity *= 2;
|
|
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(capacity), nullptr,
|
|
GL_STREAM_DRAW);
|
|
g_restartIndices.capacity = capacity;
|
|
if (data != nullptr && bytes != 0) {
|
|
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
|
|
if (MG_Util::PipeStats::Enabled()) {
|
|
// Rewritten index list staged on the draw path: in a split build these
|
|
// bytes are the index-mirror-versus-ship decision of section 8.
|
|
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndexClient,
|
|
static_cast<Uint64>(bytes));
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
} // namespace
|
|
|
|
RestartSubstitutionKind ResolveRestartSubstitution(GLenum indexType) {
|
|
if (!MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
|
MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) {
|
|
return RestartSubstitutionKind::None;
|
|
}
|
|
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType);
|
|
if (fixedMax == 0) return RestartSubstitutionKind::None;
|
|
const Uint32 restartIndex = MG_Pipe::gPipeInputs->GetPrimitiveRestartIndex();
|
|
if (restartIndex == fixedMax) return RestartSubstitutionKind::None;
|
|
// Strictly greater, never truncated. GL 4.6 core 10.3.6 compares the fetched index
|
|
// zero-extended against the full 32-bit state, so an index this type cannot hold matches
|
|
// nothing. Truncating instead - glPrimitiveRestartIndex(0x100) over GL_UNSIGNED_BYTE data
|
|
// becoming "restart on 0" - turns the most common index in any mesh into a restart.
|
|
if (restartIndex > fixedMax) return RestartSubstitutionKind::SuppressRestart;
|
|
return RestartSubstitutionKind::RewriteIndices;
|
|
}
|
|
|
|
void OnRestartSubstitutionContextDestroyed() {
|
|
g_restartIndices = {};
|
|
g_restartStaging.clear();
|
|
g_restartStaging.shrink_to_fit();
|
|
}
|
|
|
|
ScopedSuppressedPrimitiveRestart::ScopedSuppressedPrimitiveRestart(RestartSubstitutionKind kind) {
|
|
if (kind != RestartSubstitutionKind::SuppressRestart) return;
|
|
// SyncRenderState turned the driver's fixed-index restart on because GL_PRIMITIVE_RESTART
|
|
// is enabled; for this draw's index type it would restart on a value the application
|
|
// never named. Toggled directly rather than through the render-state shadow, and put back
|
|
// in the destructor, so the shadow stays true and the next draw pays nothing.
|
|
g_GLESFuncs.glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
|
m_suppressed = true;
|
|
}
|
|
|
|
ScopedSuppressedPrimitiveRestart::~ScopedSuppressedPrimitiveRestart() {
|
|
if (!m_suppressed) return;
|
|
g_GLESFuncs.glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
|
}
|
|
|
|
ScopedRestartIndexSubstitution::ScopedRestartIndexSubstitution(GLenum indexType, GLsizei count, const void* indices)
|
|
: m_kind(ResolveRestartSubstitution(indexType)), m_capOverride(m_kind), m_indices(indices),
|
|
m_indexType(indexType) {
|
|
if (m_kind != RestartSubstitutionKind::RewriteIndices) {
|
|
return;
|
|
}
|
|
const SizeT sourceIndexSize = MG_Util::GetGLTypeSize(indexType);
|
|
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType);
|
|
const Uint32 applicationRestartIndex = MG_Pipe::gPipeInputs->GetPrimitiveRestartIndex();
|
|
|
|
const Uint8* source = nullptr;
|
|
SizeT indexCount = 0;
|
|
SizeT sourceByteOffset = 0;
|
|
Bool hasElementBuffer = false;
|
|
// -1: monolith, or this arm never resolved a backend id, and the tail asks
|
|
// BoundElementArrayBufferId() exactly as it always did.
|
|
Int serverElementBinding = -1;
|
|
|
|
// P5c (hd, CONTRACT-P5C §3.5): with an active transport this constructor runs on the
|
|
// apply thread, where the frontend BufferObject's legacy accessors are Fatal by name
|
|
// (BufferObject.cpp's "buffer-legacy-arm" refusal). The element buffer therefore
|
|
// resolves from the applier's set_index_buffer state (T2) and the bytes read from the
|
|
// server's own staged shadow - "the backend's ScopedRestartIndexSubstitution reads the
|
|
// index bytes it needs on its side" (CONTRACT-P5B d1), the same arm
|
|
// MultiDrawElementsIndirectCount takes for its command buffer. A draw whose record
|
|
// carried client indices (no index buffer bound) takes the client-pointer arm below,
|
|
// `indices` already resolved through the server's segment resolver. Monolith takes
|
|
// the original body, verbatim.
|
|
const auto& applierState = MG_Pipe::MGPipeApplier();
|
|
const MG_Pipe::MGPipeHandle elements = applierState.IndexBuffer.Res;
|
|
serverElementBinding = 0;
|
|
if (!MG_Pipe::MGPipeHandleIsNull(elements)) {
|
|
auto* elementResource = BufferImpl::FindBufferResourceForHandle(elements);
|
|
// The WHOLE buffer is rewritten, not just this draw's range, so that every
|
|
// index keeps its position: an indirect draw's firstIndex lives in GPU memory
|
|
// and cannot be adjusted from here. It is an ELEMENT index, so it survives
|
|
// widening unchanged.
|
|
const SizeT sizeBytes = BufferImpl::ResourceWidthForHandle(elements);
|
|
if (sizeBytes < sourceIndexSize) {
|
|
return; // Nothing to restart on; let the driver see the draw unchanged.
|
|
}
|
|
if (sizeBytes > kMaxRestartRewriteBytes) {
|
|
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs the %zu-byte element "
|
|
"array buffer rewritten every draw, which is past the %zu-byte ceiling. Use "
|
|
"GL_PRIMITIVE_RESTART_FIXED_INDEX, or set glPrimitiveRestartIndex to the all-ones "
|
|
"value of the index type.",
|
|
applicationRestartIndex, sizeBytes, kMaxRestartRewriteBytes);
|
|
m_valid = false;
|
|
return;
|
|
}
|
|
// The staged shadow is the source of truth for CPU reads on this side: a
|
|
// persistent map's blocks were consumed by the applier before this verb ran, and
|
|
// (P8-C) a store a shader wrote on this side is read back from this server's own
|
|
// GL buffer first - no writeback ever reaches the server's copy.
|
|
const Uint8* hostBytes =
|
|
elementResource
|
|
? BufferImpl::SplitHostBytesForCpuRead(*elementResource, elements, "primitive_restart_substitution")
|
|
: nullptr;
|
|
if (hostBytes == nullptr) {
|
|
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs a CPU-readable copy of "
|
|
"the bound element array buffer and none is available.",
|
|
applicationRestartIndex);
|
|
m_valid = false;
|
|
return;
|
|
}
|
|
// The M-3 rule for a whole-store reader (ResourceContentIsDeclared in ManagersBuffers.cpp): for a store
|
|
// whose content the application SUPPLIED a coverage gap is a missing record and is
|
|
// Fatal by name; for one it ORPHANED the gap is its own undefined content and
|
|
// reading the shadow's zero-fill is exactly what the monolith arm's
|
|
// MappedData() answers.
|
|
const MG_Pipe::MGPipeResourceRecord* elementRecord = nullptr;
|
|
if (elements.Slot < applierState.Resources.size()) {
|
|
const auto& candidate = applierState.Resources[elements.Slot];
|
|
if (candidate.Live && candidate.Gen == elements.Gen) elementRecord = &candidate;
|
|
}
|
|
if (elementRecord != nullptr && elementRecord->Desc.HasDefinedContent != 0) {
|
|
BufferImpl::RequireStagedCoverage(*elementResource, hostBytes, 0, sizeBytes,
|
|
"primitive_restart_substitution");
|
|
}
|
|
hasElementBuffer = true;
|
|
serverElementBinding = static_cast<Int>(elementResource->id);
|
|
source = hostBytes;
|
|
indexCount = sizeBytes / sourceIndexSize;
|
|
sourceByteOffset = reinterpret_cast<SizeT>(indices);
|
|
}
|
|
if (!hasElementBuffer) {
|
|
// No element array buffer: `indices` is a client pointer, so only the draw's own
|
|
// range is readable and an indirect draw has nothing to read at all.
|
|
if (count <= 0 || indices == nullptr || sourceIndexSize == 0) {
|
|
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs either a bound element "
|
|
"array buffer or a client index array with a CPU-known count.",
|
|
applicationRestartIndex);
|
|
m_valid = false;
|
|
return;
|
|
}
|
|
if (static_cast<SizeT>(count) * sourceIndexSize > kMaxRestartRewriteBytes) {
|
|
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART index rewrite of %zu bytes is past the %zu-byte "
|
|
"ceiling.",
|
|
static_cast<SizeT>(count) * sourceIndexSize, kMaxRestartRewriteBytes);
|
|
m_valid = false;
|
|
return;
|
|
}
|
|
source = static_cast<const Uint8*>(indices);
|
|
indexCount = static_cast<SizeT>(count);
|
|
}
|
|
|
|
// Widen only when the source really does use the all-ones value as a vertex index -
|
|
// otherwise the sentinel is free and the copy stays the caller's width, which keeps the
|
|
// common substitution allocation-for-allocation identical to the narrow form.
|
|
GLenum destinationType = indexType;
|
|
SizeT destinationIndexSize = sourceIndexSize;
|
|
if (ContainsFixedRestartIndex(source, indexCount, sourceIndexSize, fixedMax)) {
|
|
const GLenum wider = WiderIndexType(indexType);
|
|
// An element-array offset that is not a whole number of indices cannot be rescaled
|
|
// into the widened copy, so such a draw keeps the narrow (lossy) form.
|
|
const Bool offsetIsWholeIndices = sourceIndexSize != 0 && (sourceByteOffset % sourceIndexSize) == 0;
|
|
if (wider != 0 && offsetIsWholeIndices &&
|
|
indexCount * MG_Util::GetGLTypeSize(wider) <= kMaxRestartRewriteBytes) {
|
|
destinationType = wider;
|
|
destinationIndexSize = MG_Util::GetGLTypeSize(wider);
|
|
} else {
|
|
MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over index data that also uses the "
|
|
"all-ones index %u: this index type cannot spell both, so every all-ones index is "
|
|
"drawn as %u instead. Use GL_PRIMITIVE_RESTART_FIXED_INDEX, or keep the all-ones "
|
|
"value out of the index data.",
|
|
applicationRestartIndex, fixedMax, fixedMax - 1);
|
|
}
|
|
}
|
|
|
|
const Uint32 destinationFixedMax = MG_Util::FixedRestartIndexForGLType(destinationType);
|
|
RewriteRestartIndices(source, indexCount, sourceIndexSize, destinationIndexSize, applicationRestartIndex,
|
|
destinationFixedMax, g_restartStaging);
|
|
|
|
if (!UploadRestartScratch(g_restartStaging.size(), g_restartStaging.data())) {
|
|
MGLOG_E_ONCE("Draw skipped: could not allocate the scratch element array buffer for GL_PRIMITIVE_RESTART "
|
|
"index substitution.");
|
|
m_valid = false;
|
|
return;
|
|
}
|
|
m_previousBinding = 0;
|
|
if (serverElementBinding >= 0) m_previousBinding = static_cast<Uint>(serverElementBinding);
|
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, g_restartIndices.id);
|
|
m_substituted = true;
|
|
m_indexType = destinationType;
|
|
// The rewritten copy starts at byte 0 of the scratch buffer and holds one
|
|
// destination-width element per source element, so an EBO-sourced draw keeps its ELEMENT
|
|
// offset (rescaled to the new width) and a client-memory draw reads from the front.
|
|
m_indices = hasElementBuffer
|
|
? reinterpret_cast<const void*>((sourceByteOffset / sourceIndexSize) * destinationIndexSize)
|
|
: nullptr;
|
|
}
|
|
|
|
ScopedRestartIndexSubstitution::~ScopedRestartIndexSubstitution() {
|
|
if (!m_substituted) return;
|
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, m_previousBinding);
|
|
}
|
|
|
|
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
|
if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, 1, 0, 0)) return;
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
|
PrepareForDraw(syncBit);
|
|
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
|
if (!restart.DrawIsValid()) return;
|
|
ForEachViewportRoutingPass(
|
|
[&] { g_GLESFuncs.glDrawElements(mode, count, restart.IndexType(), restart.Indices()); });
|
|
}
|
|
|
|
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
DrawSyncFlags syncBit = DrawSyncBit::None;
|
|
PrepareForDraw(syncBit);
|
|
VertexArrayImpl::SyncClientSideVertexArraysForDrawArrays(first, count);
|
|
ForEachViewportRoutingPass([&] { g_GLESFuncs.glDrawArrays(mode, first, count); });
|
|
}
|
|
|
|
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
|
|
if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, 1, basevertex, 0)) return;
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
|
PrepareForDraw(syncBit);
|
|
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
|
if (!restart.DrawIsValid()) return;
|
|
SetCurrentBaseVertex(basevertex);
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawElementsBaseVertex(mode, count, restart.IndexType(), restart.Indices(), basevertex);
|
|
});
|
|
SetCurrentBaseVertex(0);
|
|
}
|
|
|
|
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
DrawSyncFlags syncBit = DrawSyncBit::None;
|
|
PrepareForDraw(syncBit);
|
|
|
|
// This loop IS the emulation - there is no batched tier for the non-indexed form -
|
|
// so each sub-draw has to be given its own gl_DrawID here, exactly as the indexed
|
|
// ladder and the indirect executors do. Without it every sub-draw of a
|
|
// glMultiDrawArrays read draw index 0.
|
|
const Bool feedDrawID = CurrentProgramReadsDrawID();
|
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
|
// Client-side arrays are uploaded per sub-draw range, like the single DrawArrays path.
|
|
VertexArrayImpl::SyncClientSideVertexArraysForDrawArrays(first[i], count[i]);
|
|
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
|
ForEachViewportRoutingPass([&] { g_GLESFuncs.glDrawArrays(mode, first[i], count[i]); });
|
|
}
|
|
if (feedDrawID) SetCurrentDrawID(0);
|
|
}
|
|
|
|
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
|
GLsizei drawcount, const GLint* basevertex) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
MultiDrawImpl::DrawElementsBatch(mode, count, type, indices, drawcount, basevertex);
|
|
}
|
|
|
|
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
if (drawcount <= 0) {
|
|
return;
|
|
}
|
|
if (stride == 0) {
|
|
stride = sizeof(DrawElementsIndirectCommand);
|
|
}
|
|
if (stride < static_cast<GLsizei>(sizeof(DrawElementsIndirectCommand))) {
|
|
MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: stride %d is smaller than command size %zu", stride,
|
|
sizeof(DrawElementsIndirectCommand));
|
|
return;
|
|
}
|
|
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
|
|
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
|
if (indexSize == 0) {
|
|
MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: unsupported index type 0x%x", type);
|
|
return;
|
|
}
|
|
|
|
const auto* commandBytes = ResolveIndirectCommandBytes(
|
|
indirect,
|
|
static_cast<SizeT>(stride) * static_cast<SizeT>(drawcount - 1) + sizeof(DrawElementsIndirectCommand),
|
|
"MultiDrawElementsIndirect");
|
|
if (!commandBytes) {
|
|
return;
|
|
}
|
|
|
|
const auto& drawIndirectBuffer =
|
|
// P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot
|
|
// is never read - the Execute* helpers resolve the buffer from the verb record's
|
|
// handle (T2). Monolith reads the slot as it always did.
|
|
SharedPtr<MG_State::GLState::BufferObject>(nullptr);
|
|
ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast<SizeT>(indirect),
|
|
drawIndirectBuffer, drawcount, stride, "MultiDrawElementsIndirect");
|
|
}
|
|
|
|
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
|
GLsizei maxdrawcount, GLsizei stride) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
if (maxdrawcount <= 0) {
|
|
return;
|
|
}
|
|
if (stride == 0) {
|
|
stride = sizeof(DrawElementsIndirectCommand);
|
|
}
|
|
if (stride < static_cast<GLsizei>(sizeof(DrawElementsIndirectCommand))) {
|
|
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: stride %d is smaller than command size %zu", stride,
|
|
sizeof(DrawElementsIndirectCommand));
|
|
return;
|
|
}
|
|
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
|
|
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
|
if (indexSize == 0) {
|
|
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: unsupported index type 0x%x", type);
|
|
return;
|
|
}
|
|
|
|
// P5c (hd, CONTRACT-P5C §3.5): with an active transport both buffers resolve from the
|
|
// verb record's handles (MGPDrawIndirect::Buffer / ParameterBuffer) and the count
|
|
// reads from the SERVER's staged shadow - the frontend binding slots, the frontend
|
|
// accessors (B3) and the client allocator (T2) are never read. Monolith takes the
|
|
// original body below, verbatim.
|
|
const auto& applierState = MG_Pipe::MGPipeApplier();
|
|
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
|
const SizeT commandBytes = commandOffset + static_cast<SizeT>(stride) * static_cast<SizeT>(maxdrawcount - 1) +
|
|
sizeof(DrawElementsIndirectCommand);
|
|
auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer);
|
|
auto* paramResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer);
|
|
// P8-C: the count is CONSUMED here, so the parameter buffer is made current first
|
|
// (a store a shader wrote on this side is read back); the command buffer only gets a
|
|
// first copy when it has none - its fields are made current by the executor, and
|
|
// only when it consumes them (CurrentIndirectCommandBytes).
|
|
const Uint8* const parameterBytes =
|
|
paramResource ? BufferImpl::SplitHostBytesForCpuRead(
|
|
*paramResource, applierState.VerbIndirectParameterBuffer, "indirect_count_parameter")
|
|
: nullptr;
|
|
const Uint8* drawBytes = drawResource ? BufferImpl::SplitHostBytes(*drawResource) : nullptr;
|
|
if (drawBytes == nullptr && drawResource != nullptr) {
|
|
drawBytes = BufferImpl::SplitHostBytesForCpuRead(*drawResource, applierState.VerbIndirectBuffer,
|
|
"indirect_count_commands");
|
|
}
|
|
if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) {
|
|
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
|
|
return;
|
|
}
|
|
if (drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) >
|
|
BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectParameterBuffer)) {
|
|
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
|
return;
|
|
}
|
|
// No staged shadow means no count to read, not a wrong one - the monolith body's
|
|
// own rule for a buffer with no CPU shadow.
|
|
if (parameterBytes == nullptr || drawBytes == nullptr) {
|
|
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: CPU fallback cannot read the parameter or "
|
|
"draw-indirect buffer");
|
|
return;
|
|
}
|
|
BufferImpl::RequireStagedCoverage(*drawResource, drawBytes, commandOffset, commandBytes,
|
|
"multidraw_elements_indirect_count_commands");
|
|
BufferImpl::RequireStagedCoverage(*paramResource, parameterBytes, static_cast<SizeT>(drawcount),
|
|
static_cast<SizeT>(drawcount) + sizeof(Uint32),
|
|
"multidraw_elements_indirect_count_parameter");
|
|
Uint32 actualDrawCount = 0;
|
|
std::memcpy(&actualDrawCount, parameterBytes + drawcount, sizeof(actualDrawCount));
|
|
actualDrawCount = std::min<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
|
|
ExecuteIndexedIndirectCommands(mode, type, indexSize, drawBytes + commandOffset, commandOffset, nullptr,
|
|
static_cast<GLsizei>(actualDrawCount), stride, "MultiDrawElementsIndirectCount");
|
|
return;
|
|
}
|
|
|
|
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
if (drawcount <= 0) {
|
|
return;
|
|
}
|
|
if (stride == 0) {
|
|
stride = sizeof(DrawArraysIndirectCommand);
|
|
}
|
|
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
|
MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu", stride,
|
|
sizeof(DrawArraysIndirectCommand));
|
|
return;
|
|
}
|
|
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
|
|
const auto* commandBytes = ResolveIndirectCommandBytes(
|
|
indirect,
|
|
static_cast<SizeT>(stride) * static_cast<SizeT>(drawcount - 1) + sizeof(DrawArraysIndirectCommand),
|
|
"MultiDrawArraysIndirect");
|
|
if (!commandBytes) {
|
|
return;
|
|
}
|
|
|
|
const auto& drawIndirectBuffer =
|
|
// P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot
|
|
// is never read - the Execute* helpers resolve the buffer from the verb record's
|
|
// handle (T2). Monolith reads the slot as it always did.
|
|
SharedPtr<MG_State::GLState::BufferObject>(nullptr);
|
|
ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast<SizeT>(indirect), drawIndirectBuffer,
|
|
drawcount, stride, "MultiDrawArraysIndirect");
|
|
}
|
|
|
|
// The non-indexed twin of MultiDrawElementsIndirectCount, and structurally identical to it:
|
|
// ES has no GL_PARAMETER_BUFFER at all, so the draw count is read from the CPU shadow of the
|
|
// bound one and the batch degenerates into an ordinary indirect multi-draw of that many
|
|
// commands. Missing from the backend table until now, which made every
|
|
// glMultiDrawArraysIndirectCount an INVALID_OPERATION ("backend does not support
|
|
// indirect-parameter array draws") on DirectGLES while the extension was advertised.
|
|
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount,
|
|
GLsizei stride) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
if (maxdrawcount <= 0) {
|
|
return;
|
|
}
|
|
if (stride == 0) {
|
|
stride = sizeof(DrawArraysIndirectCommand);
|
|
}
|
|
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
|
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu", stride,
|
|
sizeof(DrawArraysIndirectCommand));
|
|
return;
|
|
}
|
|
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
|
|
// P5c (hd, CONTRACT-P5C §3.5): see the indexed twin - the verb record's handles and
|
|
// the SERVER's staged shadow, never the frontend bindings or the client allocator.
|
|
const auto& applierState = MG_Pipe::MGPipeApplier();
|
|
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
|
const SizeT commandBytes = commandOffset + static_cast<SizeT>(stride) * static_cast<SizeT>(maxdrawcount - 1) +
|
|
sizeof(DrawArraysIndirectCommand);
|
|
auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer);
|
|
auto* paramResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer);
|
|
// P8-C: the count is CONSUMED here, so the parameter buffer is made current first
|
|
// (a store a shader wrote on this side is read back); the command buffer only gets a
|
|
// first copy when it has none - its fields are made current by the executor, and
|
|
// only when it consumes them (CurrentIndirectCommandBytes).
|
|
const Uint8* const parameterBytes =
|
|
paramResource ? BufferImpl::SplitHostBytesForCpuRead(
|
|
*paramResource, applierState.VerbIndirectParameterBuffer, "indirect_count_parameter")
|
|
: nullptr;
|
|
const Uint8* drawBytes = drawResource ? BufferImpl::SplitHostBytes(*drawResource) : nullptr;
|
|
if (drawBytes == nullptr && drawResource != nullptr) {
|
|
drawBytes = BufferImpl::SplitHostBytesForCpuRead(*drawResource, applierState.VerbIndirectBuffer,
|
|
"indirect_count_commands");
|
|
}
|
|
if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) {
|
|
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
|
|
return;
|
|
}
|
|
if (drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) >
|
|
BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectParameterBuffer)) {
|
|
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
|
return;
|
|
}
|
|
if (parameterBytes == nullptr || drawBytes == nullptr) {
|
|
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read the parameter or "
|
|
"draw-indirect buffer");
|
|
return;
|
|
}
|
|
BufferImpl::RequireStagedCoverage(*drawResource, drawBytes, commandOffset, commandBytes,
|
|
"multidraw_arrays_indirect_count_commands");
|
|
BufferImpl::RequireStagedCoverage(*paramResource, parameterBytes, static_cast<SizeT>(drawcount),
|
|
static_cast<SizeT>(drawcount) + sizeof(Uint32),
|
|
"multidraw_arrays_indirect_count_parameter");
|
|
Uint32 actualDrawCount = 0;
|
|
std::memcpy(&actualDrawCount, parameterBytes + drawcount, sizeof(actualDrawCount));
|
|
actualDrawCount = std::min<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
|
|
ExecuteArraysIndirectCommands(mode, drawBytes + commandOffset, commandOffset, nullptr,
|
|
static_cast<GLsizei>(actualDrawCount), stride, "MultiDrawArraysIndirectCount");
|
|
return;
|
|
}
|
|
|
|
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
|
const void* indices, GLint basevertex) {
|
|
if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, 1, basevertex, 0)) return;
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
|
PrepareForDraw(syncBit);
|
|
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
|
if (!restart.DrawIsValid()) return;
|
|
SetCurrentBaseVertex(basevertex);
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, restart.IndexType(), restart.Indices(),
|
|
basevertex);
|
|
});
|
|
SetCurrentBaseVertex(0);
|
|
}
|
|
|
|
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
|
|
if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, 1, 0, 0)) return;
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
|
PrepareForDraw(syncBit);
|
|
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
|
if (!restart.DrawIsValid()) return;
|
|
ForEachViewportRoutingPass(
|
|
[&] { g_GLESFuncs.glDrawRangeElements(mode, start, end, count, restart.IndexType(), restart.Indices()); });
|
|
}
|
|
|
|
// True when the driver will apply baseInstance to the vertex fetch itself, in which case the
|
|
// attribute-offset emulation must stay out of the way. SetCurrentBaseInstance is orthogonal
|
|
// and runs either way - it feeds the shader's gl_BaseInstance, not the fetch.
|
|
inline Bool UseNativeBaseInstance() {
|
|
return g_GLESCapabilities.SupportsBaseInstance;
|
|
}
|
|
|
|
// The emulated shift has to be in place before PrepareForDraw, because that is what syncs the
|
|
// VAO; a zero here is what un-shifts the arrays for the next ordinary draw.
|
|
//
|
|
// P3a (D-H2): this is the LEGACY arm's carrier. On the handle arm the draw's raw base
|
|
// instance rides in MGPVertexBuffers::BaseInstance - a ContentHash input, so a base-instance
|
|
// change that moves no buffer is still emitted rather than suppressed - and the server
|
|
// decides whether to shift, because emulation ownership is the server's. The scopes below
|
|
// are therefore compiled only where the pre-handle arm is (a pull build always).
|
|
inline Uint32 EmulatedFetchBaseInstance(GLuint baseinstance) {
|
|
return UseNativeBaseInstance() ? 0u : static_cast<Uint32>(baseinstance);
|
|
}
|
|
|
|
#define MGL_SCOPED_FETCH_BASE_INSTANCE(name, value) ((void)(value))
|
|
|
|
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
|
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
|
|
if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, instancecount, basevertex,
|
|
baseinstance))
|
|
return;
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
|
MGL_SCOPED_FETCH_BASE_INSTANCE(fetchScope, EmulatedFetchBaseInstance(baseinstance));
|
|
PrepareForDraw(syncBit);
|
|
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
|
if (!restart.DrawIsValid()) return;
|
|
SetCurrentBaseInstance(baseinstance);
|
|
SetCurrentBaseVertex(basevertex);
|
|
ForEachViewportRoutingPass([&] {
|
|
if (UseNativeBaseInstance()) {
|
|
g_GLESFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT(
|
|
mode, count, restart.IndexType(), restart.Indices(), instancecount, basevertex, baseinstance);
|
|
} else {
|
|
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, restart.IndexType(), restart.Indices(),
|
|
instancecount, basevertex);
|
|
}
|
|
});
|
|
SetCurrentBaseVertex(0);
|
|
SetCurrentBaseInstance(0);
|
|
}
|
|
|
|
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
|
GLsizei instancecount, GLint basevertex) {
|
|
if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, instancecount, basevertex,
|
|
0))
|
|
return;
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
|
if (!restart.DrawIsValid()) return;
|
|
SetCurrentBaseVertex(basevertex);
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, restart.Indices(), instancecount,
|
|
basevertex);
|
|
});
|
|
SetCurrentBaseVertex(0);
|
|
}
|
|
|
|
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
|
GLsizei instancecount, GLuint baseinstance) {
|
|
if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, instancecount, 0,
|
|
baseinstance))
|
|
return;
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
|
MGL_SCOPED_FETCH_BASE_INSTANCE(fetchScope, EmulatedFetchBaseInstance(baseinstance));
|
|
PrepareForDraw(syncBit);
|
|
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
|
if (!restart.DrawIsValid()) return;
|
|
SetCurrentBaseInstance(baseinstance);
|
|
ForEachViewportRoutingPass([&] {
|
|
if (UseNativeBaseInstance()) {
|
|
g_GLESFuncs.glDrawElementsInstancedBaseInstanceEXT(mode, count, restart.IndexType(), restart.Indices(),
|
|
instancecount, baseinstance);
|
|
} else {
|
|
g_GLESFuncs.glDrawElementsInstanced(mode, count, restart.IndexType(), restart.Indices(), instancecount);
|
|
}
|
|
});
|
|
SetCurrentBaseInstance(0);
|
|
}
|
|
|
|
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
|
|
if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, instancecount, 0, 0))
|
|
return;
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
|
if (!restart.DrawIsValid()) return;
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawElementsInstanced(mode, count, restart.IndexType(), restart.Indices(), instancecount);
|
|
});
|
|
}
|
|
|
|
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
|
|
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
|
if (indexSize == 0) {
|
|
MGLOG_E_ONCE("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
|
|
return;
|
|
}
|
|
|
|
const auto* commandBytes =
|
|
ResolveIndirectCommandBytes(indirect, sizeof(DrawElementsIndirectCommand), "DrawElementsIndirect");
|
|
if (!commandBytes) {
|
|
return;
|
|
}
|
|
|
|
const auto& drawIndirectBuffer =
|
|
// P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot
|
|
// is never read - the Execute* helpers resolve the buffer from the verb record's
|
|
// handle (T2). Monolith reads the slot as it always did.
|
|
SharedPtr<MG_State::GLState::BufferObject>(nullptr);
|
|
ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast<SizeT>(indirect),
|
|
drawIndirectBuffer, 1, sizeof(DrawElementsIndirectCommand),
|
|
"DrawElementsIndirect");
|
|
}
|
|
|
|
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
|
GLuint baseinstance) {
|
|
DrawSyncFlags syncBit = DrawSyncBit::Instancing;
|
|
MGL_SCOPED_FETCH_BASE_INSTANCE(fetchScope, EmulatedFetchBaseInstance(baseinstance));
|
|
PrepareForDraw(syncBit);
|
|
SetCurrentBaseInstance(baseinstance);
|
|
ForEachViewportRoutingPass([&] {
|
|
if (UseNativeBaseInstance()) {
|
|
g_GLESFuncs.glDrawArraysInstancedBaseInstanceEXT(mode, first, count, instancecount, baseinstance);
|
|
} else {
|
|
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
|
|
}
|
|
});
|
|
SetCurrentBaseInstance(0);
|
|
}
|
|
|
|
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
|
|
DrawSyncFlags syncBit = DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
ForEachViewportRoutingPass([&] { g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount); });
|
|
}
|
|
|
|
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
|
|
const auto* commandBytes =
|
|
ResolveIndirectCommandBytes(indirect, sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect");
|
|
if (!commandBytes) {
|
|
return;
|
|
}
|
|
|
|
const auto& drawIndirectBuffer =
|
|
// P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot
|
|
// is never read - the Execute* helpers resolve the buffer from the verb record's
|
|
// handle (T2). Monolith reads the slot as it always did.
|
|
SharedPtr<MG_State::GLState::BufferObject>(nullptr);
|
|
ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast<SizeT>(indirect), drawIndirectBuffer, 1,
|
|
sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect");
|
|
}
|
|
|
|
// Empties the ES driver's error queue, BOUNDED. A driver that never answers GL_NO_ERROR - a
|
|
// lost context is the usual way, and GL_CONTEXT_LOST is allowed to keep coming back - would
|
|
// otherwise spin an unbounded drain forever inside whichever GL entry point happened to be
|
|
// cleaning up, which is how a GPU reset reads as an unkillable process whose log simply
|
|
// stops. A healthy context cannot queue anywhere near the cap, so reaching it IS the
|
|
// diagnostic. Every drain in this backend goes through here so the bound cannot drift apart
|
|
// between them.
|
|
static constexpr Int kMaxDrainedGLErrors = 32;
|
|
|
|
void DrainDriverErrors(const char* site) {
|
|
Int drained = 0;
|
|
while (drained < kMaxDrainedGLErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
|
++drained;
|
|
}
|
|
if (drained == kMaxDrainedGLErrors) {
|
|
MGLOG_E_ONCE("%s: the ES driver still reported errors after %d drains - the context is most likely lost",
|
|
site, kMaxDrainedGLErrors);
|
|
}
|
|
}
|
|
} // namespace MobileGL::MG_Backend::DirectGLES
|