// MobileGL - MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.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 "GL_Drawing.h" #include #include #include #include #include "../Getter/GL_Getter.h" namespace MobileGL::MG_Impl::GLImpl { static Bool ValidateProgramForExecution(const SharedPtr& currentProgram, const char* functionName) { if (!currentProgram) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", functionName, "There is no current program object.")); return false; } if (!currentProgram->GetLinkStatus()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", functionName, "The current program object is not linked.")); return false; } return true; } // Takes the ALREADY-RESOLVED draw program rather than looking it up: GLContext::GetProgramForDraw // is not a plain getter (it settles the program's link and SPIR-V jobs so every version a // backend samples during this draw describes the program it is drawing), so the draw funnel // below resolves it exactly once and hands it to both users. static Bool ValidateResolvedProgramForDraw(const SharedPtr& currentProgram, const char* functionName) { // "If there is no current program object or bound program pipeline object, the results of // a draw are UNDEFINED" - and undefined is not an error (GL 4.6 core 7.3, ES 3.1 7.3). // The draw is dropped, silently, which is one of the shapes "undefined" is allowed to // take; recording INVALID_OPERATION here is not, and es31cSeparateShaderObjsTests' // StateInteraction reads exactly that error back after useProgram(0) + bindProgramPipeline(0). // A DISPATCH is the opposite rule ("INVALID_OPERATION if there is no active program for // the compute shader stage"), which is why this lives on the draw path and not in the // shared ValidateProgramForExecution below. if (!currentProgram) return false; if (!ValidateProgramForExecution(currentProgram, functionName)) return false; // GL 4.6 core 7.4.1, the pipeline validation rule every vertex-transferring command // inherits: it is an INVALID_OPERATION when a tessellation control, tessellation // evaluation or geometry stage has an executable but no program supplies an executable // VERTEX shader. A non-separable program cannot reach this - the link rule forbids the // shape - so in practice it catches a program pipeline assembled out of stage programs, // which today draws happily and renders nothing. // // Asked of the EXECUTABLE, like the compute check below: for a pipeline the resolved // program is the graphics composite, whose linked-shader snapshot is built out of exactly // the pipeline's own graphics stage programs (GLContext::GetProgramForDraw), and the only // stage compositing ever invents is a default FRAGMENT shader. A fragment-only pipeline is // deliberately NOT rejected: the rule above names the three pre-rasterization stages, and // nothing else here should start refusing draws GL accepts. // // On the DRAW path only, never in ValidateProgramForExecution itself, so a dispatch - // which shares that helper and legitimately has no vertex stage - is untouched. const Bool hasPreRasterizationStage = currentProgram->HasLinkedShaderStage(ShaderStage::Geometry) || currentProgram->HasLinkedShaderStage(ShaderStage::TessControl) || currentProgram->HasLinkedShaderStage(ShaderStage::TessEval); if (hasPreRasterizationStage && !currentProgram->HasLinkedShaderStage(ShaderStage::Vertex)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique( "MG_Impl/GLImpl", functionName, "The program in use runs a geometry or tessellation stage but has no vertex shader stage.")); return false; } return true; } // gl_NumSamples has no SPIR-V built-in, so the source pipeline lowers it onto a reserved // default-block uniform (see InjectNumSamplesBuiltinShim). This is where that uniform is paid // for: the value is a property of the DRAW FRAMEBUFFER, not of the program, so one program // drawn into a 4x target and then into the default framebuffer must see 4 and then 1 - which // rules out baking it at link time. // // Per draw rather than on framebuffer changes because the pair (program, framebuffer) is what // decides the value and either half can move between draws. It costs a phase-A flag read for // every program that has no shim, and a 4-byte compare for the ones that do: the write only // bumps the UBO content version when the number actually changes, so a run of draws into one // framebuffer re-uploads nothing. static void PublishDrawFramebufferSampleCount(const SharedPtr& program) { if (!program || !program->UsesReservedNumSamples()) return; // GL 4.6 core 15.2.2: gl_NumSamples is the number of samples in the framebuffer, or ONE // when the target is not multisampled - where glGetIntegerv(GL_SAMPLES) answers zero. program->WriteReservedNumSamples(static_cast(std::max(ResolveDrawFramebufferSampleCount(), 1))); } // The one funnel every drawing command passes through. Order is load-bearing: validate first // (a rejected draw must leave state alone), then publish the sample count - which reads the // DRAW FRAMEBUFFER binding, so it has to run after the caller's framebuffer state is settled // and before the backend consumes the program's UBO content version. static Bool PrepareCurrentProgramForDraw(const char* functionName) { const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw(); if (!ValidateResolvedProgramForDraw(currentProgram, functionName)) return false; PublishDrawFramebufferSampleCount(currentProgram); return true; } // A dispatch resolves its program through the DISPATCH accessor: with a pipeline bound // that is the pipeline's compute stage program, not the graphics composite a draw would // build - which no longer contains a compute stage to find at all. static Bool ValidateCurrentProgramForCompute(const char* functionName) { const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch(); if (!ValidateProgramForExecution(currentProgram, functionName)) return false; // Of the EXECUTABLE, not the live attach list: attaching a compute shader to an // already-linked graphics program does not give that program a compute stage to // dispatch (GL 4.6 core 7.3), and letting the dispatch through on the strength of the // attach hands the backend a program whose SPIR-V has no compute module in it. if (!currentProgram->HasLinkedShaderStage(ShaderStage::Compute)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", functionName, "The current program object has no compute shader stage.")); return false; } return true; } // Primitives a draw of `count` vertices in `mode` assembles (0 for // incomplete primitives). Used for the CPU-side transform feedback // primitive accounting. static Uint64 CountPrimitivesForDraw(GLenum mode, GLsizei count) { if (count <= 0) return 0; switch (mode) { case GL_POINTS: return static_cast(count); case GL_LINES: return static_cast(count / 2); case GL_LINE_STRIP: return count >= 2 ? static_cast(count - 1) : 0; case GL_LINE_LOOP: return count >= 2 ? static_cast(count) : 0; case GL_TRIANGLES: return static_cast(count / 3); case GL_TRIANGLE_STRIP: case GL_TRIANGLE_FAN: return count >= 3 ? static_cast(count - 2) : 0; // Adjacency primitives (GL 4.6 core table 10.1). Only a geometry stage can consume // them, and it is the ADJACENT-free primitive count that reaches it: 4 vertices per // line, 6 per triangle, one per step for the strips. Answering 0 here - which is what // the default arm did - made AccountTransformFeedbackPrimitives bail before it had // recorded anything, so an adjacency capture advanced neither the captured-vertex // counter the scattered-capture path is bounded by nor the geometry-capture-draw flag // that routes the transform feedback queries to the driver's own counter. case GL_LINES_ADJACENCY: return static_cast(count / 4); case GL_LINE_STRIP_ADJACENCY: return count >= 4 ? static_cast(count - 3) : 0; case GL_TRIANGLES_ADJACENCY: return static_cast(count / 6); case GL_TRIANGLE_STRIP_ADJACENCY: return count >= 6 ? static_cast((count - 4) / 2) : 0; // GL_PATCHES is deliberately absent: the tessellator's amplification is not knowable // on the CPU, and answering 0 is what defers GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN // to the driver's own counter, which is the only correct source for a patch capture. default: return 0; } } // Accumulate the transform feedback primitive counter for a captured draw. // Draws without a geometry stage write exactly the primitives they assemble, // clamped by the capture buffers' remaining capacity (a full buffer stops // recording whole primitives, which is what PRIMITIVES_WRITTEN reports). // Geometry amplification is not modelled here. static void AccountTransformFeedbackPrimitives(GLenum mode, GLsizei count) { if (!MG_State::pGLContext->IsTransformFeedbackActive()) return; // A paused span captures nothing, so a draw made while paused contributes to // PRIMITIVES_GENERATED but not to TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN. if (MG_State::pGLContext->IsTransformFeedbackPaused()) { MG_State::pGLContext->AddTransformFeedbackPausedPrimitives(CountPrimitivesForDraw(mode, count)); return; } Uint64 primitives = CountPrimitivesForDraw(mode, count); if (primitives == 0) return; MG_State::pGLContext->AddTransformFeedbackInputPrimitives(primitives); Uint64 verticesPerPrimitive = 1; switch (mode) { case GL_LINES: case GL_LINE_STRIP: case GL_LINE_LOOP: // An adjacency primitive delivers the same line/triangle to the geometry stage; the // adjacent vertices are context, not part of the primitive. case GL_LINES_ADJACENCY: case GL_LINE_STRIP_ADJACENCY: verticesPerPrimitive = 2; break; case GL_TRIANGLES: case GL_TRIANGLE_STRIP: case GL_TRIANGLE_FAN: case GL_TRIANGLES_ADJACENCY: case GL_TRIANGLE_STRIP_ADJACENCY: verticesPerPrimitive = 3; break; default: break; } const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram(); if (program != nullptr) { // A geometry stage writes what it emits, not what the draw assembled, and the // amplification factor lives in the shader. Record that this span contained such // a draw so the transform feedback queries keep their backend result for it. if (program->HasLinkedShaderStage(ShaderStage::Geometry)) { MG_State::pGLContext->AddTransformFeedbackGeometryCaptureDraw(); } // Capacity in captured vertices = the tightest bound buffer. Uint64 capacityVertices = ~0ull; for (SizeT i = 0; i < program->GetTransformFeedbackBufferCount(); ++i) { const Uint32 stride = program->GetTransformFeedbackStride(static_cast(i)); if (stride == 0) continue; const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback, static_cast(i)); const Range1D range = point.GetRange(); const Uint64 bytes = range.end > range.start ? static_cast(range.end - range.start) : 0; capacityVertices = std::min(capacityVertices, bytes / stride); } if (capacityVertices != ~0ull) { const Uint64 usedVertices = MG_State::pGLContext->GetTransformFeedbackCapturedVertices(); const Uint64 remainingVertices = capacityVertices > usedVertices ? capacityVertices - usedVertices : 0; primitives = std::min(primitives, remainingVertices / verticesPerPrimitive); } } MG_State::pGLContext->AddTransformFeedbackPrimitives(primitives); MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive); // Only draws that get this far are in the written counter at all. The instanced and // indirect entry points never call this function, so a span that contains one is NOT // fully accounted, and the queries must be able to tell: they compare this counter's // delta against zero before standing in for the backend's own result. MG_State::pGLContext->AddTransformFeedbackAccountedCaptureDraw(); } // Every primitive mode a draw command accepts (GL 4.6 core table 10.1, plus // GL_PATCHES for the tessellation pipeline). Anything else is GL_INVALID_ENUM. static Bool IsAcceptedPrimitiveMode(GLenum mode) { switch (mode) { case GL_POINTS: case GL_LINES: case GL_LINE_LOOP: case GL_LINE_STRIP: case GL_LINES_ADJACENCY: case GL_LINE_STRIP_ADJACENCY: case GL_TRIANGLES: case GL_TRIANGLE_STRIP: case GL_TRIANGLE_FAN: case GL_TRIANGLES_ADJACENCY: case GL_TRIANGLE_STRIP_ADJACENCY: case GL_PATCHES: return true; default: return false; } } // The `mode` INVALID_ENUM in isolation, so a draw entry point can raise it BEFORE any of the // state-dependent INVALID_OPERATIONs below. GL 4.6 core 10.4 makes a bad mode INVALID_ENUM // unconditionally, while "no current program" is not even a spec-listed draw error - it is // MobileGL's own null-dereference guard - so it must never shadow the enum check // (KHR-GL31.api.coverage calls glDrawArraysInstanced/glDrawElementsInstanced with mode // GL_POINTS-1 against a bare context and pins GL_INVALID_ENUM). static Bool ValidatePrimitiveModeEnum(const char* functionName, GLenum mode) { if (IsAcceptedPrimitiveMode(mode)) return true; MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", functionName, "mode is not an accepted primitive type.")); return false; } static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) { if (!ValidatePrimitiveModeEnum(functionName, mode)) { return false; } const auto& activeBackendObject = MG_Backend::pActiveBackendObject; if (!activeBackendObject) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", functionName, "No active backend object.")); return false; } const auto& vao = MG_State::pGLContext->GetBoundVertexArray(); if (vao && vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", functionName, "Default vertex array object cannot be used for drawing in core profile.")); return false; } const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw(); // GL 4.6 core 10.1: the tessellation pipeline's only input primitive is GL_PATCHES, and // GL_PATCHES has no meaning without it. Both directions are INVALID_OPERATION, and // neither was implemented - which is two of the four sites // KHR-GL43.transform_feedback.api_errors_test checks with one shared message string. // The EVALUATION stage is what decides: a control stage cannot run without one, and a // program carrying only an evaluation stage still tessellates, through GL's // fixed-function pass-through control stage (11.2.2). // Asked of the LAST LINK, not the live attach list (GL 4.6 core 7.3): attaching a // tessellation evaluation shader to an already-linked program does not put it in the // executable, so reading the live list here would reject every non-GL_PATCHES draw // against a program that does not tessellate - and keep rejecting them, since a detach // is likewise deferred to the next link. const Bool tessellationActive = currentProgram && currentProgram->HasLinkedShaderStage(ShaderStage::TessEval); if (tessellationActive && mode != GL_PATCHES) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique( "MG_Impl/GLImpl", functionName, "A program with a tessellation evaluation shader can only be drawn with GL_PATCHES.")); return false; } if (!tessellationActive && mode == GL_PATCHES) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", functionName, "GL_PATCHES requires an active tessellation evaluation shader.")); return false; } // A geometry stage only accepts the primitive types that decompose into its declared // input primitive (GL 4.6 core 11.3.1); anything else is INVALID_OPERATION. GL_PATCHES // is the tessellation pipeline's input and reaches the geometry stage already // converted, so it is not constrained here. // // "Is there a geometry stage at all" has to be asked of the STAGE, never of the input // primitive: GL_NONE and GL_POINTS are both 0, so a `layout(points) in` geometry shader // is indistinguishable from no geometry shader by its reflected input type alone. The // sentinel test this replaces therefore skipped the whole rule for exactly the geometry // shaders whose input is the most restrictive one - every mode but GL_POINTS was // accepted (KHR-GL43.transform_feedback.api_errors_test draws a points-in geometry // program with GL_LINES and requires INVALID_OPERATION). // // And it has to be asked of the LAST LINK: gsInputPrimitive is a link artifact, so // pairing it with the live attach list would re-point the very same 0-aliasing rather // than remove it. In the window after glAttachShader(GS) on a linked program the live // list says "geometry present" while the artifact still reads GL_NONE == GL_POINTS, and // the switch below would silently reject every mode but GL_POINTS. const Bool geometryActive = currentProgram && currentProgram->HasLinkedShaderStage(ShaderStage::Geometry); const GLenum gsInput = geometryActive ? currentProgram->GetGeometryInputType() : GL_NONE; if (geometryActive && mode != GL_PATCHES) { Bool compatible = false; switch (gsInput) { case GL_POINTS: compatible = mode == GL_POINTS; break; case GL_LINES: compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP; break; case GL_LINES_ADJACENCY: compatible = mode == GL_LINES_ADJACENCY || mode == GL_LINE_STRIP_ADJACENCY; break; case GL_TRIANGLES: compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN; break; case GL_TRIANGLES_ADJACENCY: compatible = mode == GL_TRIANGLES_ADJACENCY || mode == GL_TRIANGLE_STRIP_ADJACENCY; break; default: break; } if (!compatible) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique( "MG_Impl/GLImpl", functionName, "Primitive mode is incompatible with the geometry shader's input primitive type.")); return false; } } // While transform feedback is active the draw's primitive type must match // the feedback primitive mode (GL 3.3 core 13.2.2). With a geometry shader // the constraint moves to the shader's output primitive type instead, so // the draw mode itself is unconstrained here - and a TESSELLATION EVALUATION // stage relocates it exactly the same way (GL 4.6 core 13.2.2 names both): // what is captured is the tessellator's output primitive, and the draw mode // can only ever be GL_PATCHES. A paused span is exempt: it captures nothing, // so there is nothing for the mode to be incompatible with (GL 4.6 core 13.2.3). const auto& feedbackProgram = MG_State::pGLContext->GetTransformFeedbackProgram(); // Both stage tests are asked of the last link, for the same reason as the two guards // above: what relocates the constraint is a stage the program actually RUNS, and an // attach that has not been linked in yet gives it none. const Bool feedbackModeIsProgramDriven = feedbackProgram && (feedbackProgram->HasLinkedShaderStage(ShaderStage::Geometry) || feedbackProgram->HasLinkedShaderStage(ShaderStage::TessEval)); if (MG_State::pGLContext->IsTransformFeedbackActive() && !MG_State::pGLContext->IsTransformFeedbackPaused() && !feedbackModeIsProgramDriven) { const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode(); Bool compatible = false; switch (feedbackMode) { case GL_POINTS: compatible = mode == GL_POINTS; break; // The adjacency modes belong here too (GL 4.6 core table 13.1, ES 3.2 table 12.1). // This arm is only reached when the program has NO geometry or tessellation // evaluation stage, and without a geometry stage the adjacent vertices are simply // ignored (GL 4.6 core 10.1) - the primitive assembled IS a plain line or triangle, // so the combination is legal and must capture. Omitting them raised a spurious // GL_INVALID_OPERATION and dropped the draw entirely, leaving the capture buffer // with its pre-draw bytes. The geometry-stage input table above already carries the // same four arms; this is the second table catching up with it. case GL_LINES: compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP || mode == GL_LINES_ADJACENCY || mode == GL_LINE_STRIP_ADJACENCY; break; case GL_TRIANGLES: compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN || mode == GL_TRIANGLES_ADJACENCY || mode == GL_TRIANGLE_STRIP_ADJACENCY; break; default: break; } if (!compatible) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique( "MG_Impl/GLImpl", functionName, "Primitive mode is incompatible with the active transform feedback primitive mode.")); return false; } } return true; } // Byte size of the command structures the indirect draws read (GL 4.6 core 10.3.10). constexpr SizeT kDrawArraysIndirectCommandBytes = 4 * sizeof(Uint32); constexpr SizeT kDrawElementsIndirectCommandBytes = 5 * sizeof(Uint32); // Shared preconditions of every *Indirect draw: `indirect` is a byte offset into the // buffer bound to GL_DRAW_INDIRECT_BUFFER, must be 4-byte aligned, and the whole // command has to lie inside that buffer. static Bool ValidateIndirectDrawSource(const char* functionName, const void* indirect, SizeT commandBytes) { const auto offset = reinterpret_cast(indirect); if (offset % 4 != 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", functionName, "indirect offset must be a multiple of 4.")); return false; } const auto& buffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); if (!buffer) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", functionName, "No buffer is bound to GL_DRAW_INDIRECT_BUFFER.")); return false; } if (offset + commandBytes > buffer->GetSize()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", functionName, "The indirect command extends past the end of the bound " "GL_DRAW_INDIRECT_BUFFER.")); return false; } return true; } // Index type accepted by the DrawElements family (GL 4.6 core 10.3.9). static Bool ValidateDrawElementsIndexType(const char* functionName, GLenum type) { switch (type) { case GL_UNSIGNED_BYTE: case GL_UNSIGNED_SHORT: case GL_UNSIGNED_INT: return true; default: MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", functionName, "type is not an accepted index type.")); return false; } } // GL 4.6 core 10.3.9: every DrawElements-family count is a sizei and "if count is negative, an // INVALID_VALUE error is generated". The same sentence covers instancecount and the // MultiDraw* drawcount, so one helper serves all of them; the parameter is named for the // caller so the message says which argument the application actually got wrong. static Bool ValidateNonNegativeDrawArgument(const char* functionName, const char* argumentName, GLsizei value) { if (value >= 0) return true; MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", functionName, String(argumentName) + " must be non-negative.")); return false; } // GL 4.6 core 10.3.9 for DrawRangeElements*: "if end < start, an INVALID_VALUE error is // generated". Both are uints, so a caller that passes -1 for start arrives here as // 0xFFFFFFFF and is caught by the same comparison - which is exactly what // KHR-GL4x.draw_elements_base_vertex_tests.invalid_count_argument checks. static Bool ValidateDrawElementsRange(const char* functionName, GLuint start, GLuint end) { if (end >= start) return true; MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", functionName, "end must not be less than start.")); return false; } // GL 4.6 core 10.9: inside a conditional block whose predicate did not pass, the drawing // commands, Clear, ClearBuffer* and the compute dispatches are DISCARDED. The gate sits on the // wrappers that ISSUE the backend call rather than at the top of each entry point, so that // everything a real driver would still do inside the block - argument validation and the // errors it raises - happens exactly as it does outside one, and only the command itself is // dropped. It is deliberately not on the frontend's transform-feedback accounting either: // that mirrors what the capture stage would have written, and a conditional block around a // capturing draw has no test coverage in either direction. static Bool ConditionalRenderDiscardsCommand() { return MG_State::pGLContext->ConditionalRenderDiscardsCommands(); } void Clear_Backend(GLbitfield mask) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.Clear(mask); } void DrawElements_Backend(GLenum mode, GLsizei count, GLenum type, const void* indices) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices); } void MultiDrawElements_Backend(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices, GLsizei drawcount) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount); } void MultiDrawElementsBaseVertex_Backend(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices, GLsizei drawcount, const GLint* basevertex) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount, basevertex); } void DrawArrays_Backend(GLenum mode, GLint first, GLsizei count) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count); } void MultiDrawArrays_Backend(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount); } void DrawElementsBaseVertex_Backend(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex); } void MultiDrawElementsIndirect_Backend(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride); } void MultiDrawArraysIndirect_Backend(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride); } void MultiDrawElementsIndirectCount_Backend(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount, maxdrawcount, stride); } void MultiDrawArraysIndirectCount_Backend(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount, stride); } void DrawRangeElementsBaseVertex_Backend(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices, GLint basevertex) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices, basevertex); } void DrawRangeElements_Backend(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices); } void DrawElementsInstancedBaseVertexBaseInstance_Backend(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance( mode, count, type, indices, instancecount, basevertex, baseinstance); } void DrawElementsInstancedBaseVertex_Backend(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex); } void DrawElementsInstancedBaseInstance_Backend(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices, instancecount, baseinstance); } void DrawElementsInstanced_Backend(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount); } void DrawElementsIndirect_Backend(GLenum mode, GLenum type, const void* indirect) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect); } void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount, baseinstance); } void DrawArraysInstanced_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount); } void DrawArraysIndirect_Backend(GLenum mode, const void* indirect) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect); } /* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */ void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) { auto dispatchCompute = MG_Backend::gBackendFunctionsTable.GL.DispatchCompute; if (!dispatchCompute) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, "Backend does not support compute dispatch.")); return; } if (!ValidateCurrentProgramForCompute(__func__)) return; // GL 4.6 core 19: each num_groups_* must be within GL_MAX_COMPUTE_WORK_GROUP_COUNT // for its dimension. GetIntegeri_v already floors that at the spec minimum. const GLuint numGroups[3] = {numGroupsX, numGroupsY, numGroupsZ}; for (GLuint dimension = 0; dimension < 3; ++dimension) { GLint maxGroups = 0; GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, dimension, &maxGroups); if (numGroups[dimension] > static_cast(std::max(maxGroups, 0))) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", __func__, "num_groups exceeds GL_MAX_COMPUTE_WORK_GROUP_COUNT for dimension " + std::to_string(dimension) + ".")); return; } } // GL 4.3 added both dispatches to the conditional-render set (GL 4.6 core 10.9), which is // exactly what KHR-GL43.compute_shader.conditional-dispatching checks. if (ConditionalRenderDiscardsCommand()) return; dispatchCompute(numGroupsX, numGroupsY, numGroupsZ); } void DispatchComputeIndirect(GLintptr indirect) { // Argument and binding validation runs FIRST. Both are properties of the call and of GL // state, so a context whose backend cannot dispatch at all must still report the // argument error the spec names rather than masking every one of them with // "unsupported" - which is what put GL_INVALID_OPERATION where // KHR-GL43.compute_shader.api-indirect expects GL_INVALID_VALUE. // // GL 4.6 core 19: `indirect` is a byte offset into GL_DISPATCH_INDIRECT_BUFFER - // negative or misaligned is INVALID_VALUE, nothing bound is INVALID_OPERATION. if (indirect < 0 || (indirect % 4) != 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", __func__, "indirect must be non-negative and a multiple of 4.")); return; } const auto& indirectBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DispatchIndirect).GetBoundObject(); if (!indirectBuffer) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, "No buffer is bound to GL_DISPATCH_INDIRECT_BUFFER.")); return; } // ...and the same INVALID_OPERATION covers "the command would source data beyond the end // of the bound buffer object" (GL 4.6 core 19): the dispatch reads three uints starting // at `indirect`. constexpr SizeT kDispatchIndirectCommandSize = 3 * sizeof(Uint32); if (static_cast(indirect) + kDispatchIndirectCommandSize > indirectBuffer->GetSize()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique( "MG_Impl/GLImpl", __func__, std::format("indirect ({}) + 12 bytes runs past the end of the {}-byte buffer bound to " "GL_DISPATCH_INDIRECT_BUFFER.", indirect, indirectBuffer->GetSize()))); return; } auto dispatchComputeIndirect = MG_Backend::gBackendFunctionsTable.GL.DispatchComputeIndirect; if (!dispatchComputeIndirect) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, "Backend does not support indirect compute dispatch.")); return; } if (!ValidateCurrentProgramForCompute(__func__)) return; if (ConditionalRenderDiscardsCommand()) return; dispatchComputeIndirect(indirect); } void PatchParameteri(GLenum pname, GLint value) { if (pname != GL_PATCH_VERTICES) { MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", __func__, "pname must be GL_PATCH_VERTICES.")); return; } GLint maxPatchVertices = 32; GetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices); if (value <= 0 || value > maxPatchVertices) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", __func__, "value must be in [1, GL_MAX_PATCH_VERTICES].")); return; } MG_State::pGLContext->SetPatchVertices(static_cast(value)); if (const auto patchParameteri = MG_Backend::gBackendFunctionsTable.GL.PatchParameteri) { patchParameteri(pname, value); } } // GL 4.6 core 11.2.2. The default tessellation levels a program with an evaluation stage and // NO control stage tessellates at; both backends have to synthesize that control stage // themselves (ES 3.2 and Vulkan both require one), and they compile these numbers into it, so // there is no backend entry point to forward to - ES has none at all. INVALID_ENUM on a bad // pname is the only error the spec lists: any float values are accepted, negatives and NaN // included, and it is the tessellator that clamps them. // // This used to be a stub, which is why the two synthesizers hardcoded 1.0. void PatchParameterfv(GLenum pname, const GLfloat* values) { if (pname != GL_PATCH_DEFAULT_OUTER_LEVEL && pname != GL_PATCH_DEFAULT_INNER_LEVEL) { MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique( "MG_Impl/GLImpl", __func__, "pname must be GL_PATCH_DEFAULT_OUTER_LEVEL or GL_PATCH_DEFAULT_INNER_LEVEL.")); return; } if (!values) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", __func__, "values pointer cannot be null")); return; } if (pname == GL_PATCH_DEFAULT_OUTER_LEVEL) { MG_State::pGLContext->SetPatchDefaultOuterLevel( FloatVec4(values[0], values[1], values[2], values[3])); } else { MG_State::pGLContext->SetPatchDefaultInnerLevel(FloatVec2(values[0], values[1])); } } namespace { // GL 4.6 core 7.11.2 (and ARB_shader_image_load_store, which introduced the call): the // barrier bitfield is INVALID_VALUE unless every bit is one of the defined ones, with // GL_ALL_BARRIER_BITS - which is 0xFFFFFFFF, not the union of the list - accepted whole. // Forwarding an undefined bit to the host driver let a caller that had computed its mask // wrongly (or reused an ES-only bit) get silence instead of the error the spec promises. constexpr GLbitfield kAllDefinedBarrierBits = GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT | GL_UNIFORM_BARRIER_BIT | GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_COMMAND_BARRIER_BIT | GL_PIXEL_BUFFER_BARRIER_BIT | GL_TEXTURE_UPDATE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT | GL_TRANSFORM_FEEDBACK_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT | GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT | GL_QUERY_BUFFER_BARRIER_BIT; Bool ValidateMemoryBarrierBits(const char* function, GLbitfield barriers) { if (barriers == GL_ALL_BARRIER_BITS) return true; if ((barriers & ~kAllDefinedBarrierBits) != 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", function, "barriers contains bits that are not defined barrier bits.")); return false; } return true; } } // namespace void MemoryBarrier(GLbitfield barriers) { if (!ValidateMemoryBarrierBits(__func__, barriers)) return; auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier; if (!memoryBarrier) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers.")); return; } memoryBarrier(barriers); } void TextureBarrier() { // GL 4.5 core 8.26 / GL_ARB_texture_barrier: order every write the fixed-function // framebuffer has already issued ahead of every subsequent texture fetch, so a shader may // read texels of a texture that is also attached to the current framebuffer. // // Both backends serve this through their existing memory-barrier hook rather than a new // entry point of their own: GL_FRAMEBUFFER_BARRIER_BIT is the source half (framebuffer // writes) and GL_TEXTURE_FETCH_BARRIER_BIT the destination half (texture fetches), which // is exactly the dependency ARB_texture_barrier defines - just expressed with the wider // scope glMemoryBarrier gives it. That is a superset of the required ordering, never a // subset, so it cannot under-synchronize. auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier; if (!memoryBarrier) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers.")); return; } memoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT); } void MemoryBarrierByRegion(GLbitfield barriers) { if (!ValidateMemoryBarrierBits(__func__, barriers)) return; auto memoryBarrierByRegion = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrierByRegion; if (!memoryBarrierByRegion) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, "Backend does not support regional memory barriers.")); return; } memoryBarrierByRegion(barriers); } void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride); } void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride); } // ARB_indirect_parameters / GL 4.6 core 10.4: `drawcount` is a byte offset into the buffer // bound to PARAMETER_BUFFER and holds one uint draw count. Three errors have to be raised // before the call reaches a backend, and none of them was // (KHR-GL46.indirect_parameters_tests.MultiDraw{Arrays,Elements}IndirectCount): // * drawcount not a multiple of four INVALID_VALUE // * nothing bound to PARAMETER_BUFFER, or the uint at `drawcount` // lies past its end INVALID_OPERATION // * maxdrawcount commands from `indirect` run past the end of the // buffer bound to DRAW_INDIRECT_BUFFER INVALID_OPERATION static Bool ValidateIndirectCountDraw(GLintptr indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride, SizeT commandSize, const char* funcName) { if (drawcount < 0 || (drawcount % 4) != 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", funcName, "drawcount must be non-negative and a multiple of four.")); return false; } const auto& parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject(); if (!parameterBuffer || static_cast(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", funcName, "No buffer is bound to GL_PARAMETER_BUFFER, or drawcount runs past " "the end of the one that is.")); return false; } if (maxdrawcount < 0 || stride < 0 || indirect < 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", funcName, "indirect, maxdrawcount and stride must all be non-negative.")); return false; } const SizeT effectiveStride = stride != 0 ? static_cast(stride) : commandSize; const auto& indirectBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); // A zero maxdrawcount sources nothing, so it cannot run past anything. const SizeT requiredBytes = maxdrawcount == 0 ? 0 : static_cast(indirect) + static_cast(maxdrawcount - 1) * effectiveStride + commandSize; if (!indirectBuffer || requiredBytes > indirectBuffer->GetSize()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", funcName, "maxdrawcount commands would be sourced from beyond the end of the " "buffer bound to GL_DRAW_INDIRECT_BUFFER.")); return false; } return true; } void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) { // Argument validation before the backend-availability check: see DispatchComputeIndirect. // DrawElementsIndirectCommand: count, instanceCount, firstIndex, baseVertex, baseInstance. if (!ValidateIndirectCountDraw(reinterpret_cast(indirect), drawcount, maxdrawcount, stride, 5 * sizeof(Uint32), __func__)) { return; } // The only two draw entry points that were missing this. Every backend draw path // dereferences GetProgramForDraw() unconditionally, so "no current program" has to be // stopped here or it is a null dereference rather than the INVALID_OPERATION the spec // asks for - reachable through a bound pipeline that supplies no graphics stage. // // AFTER the argument checks, unlike the sibling draw entry points, and deliberately: // the argument rules here are properties of the call rather than of GL state, and // NegativeApiErrorsTest.IndirectParameterDrawsCheckBothBuffers pins the INVALID_VALUE // they produce for a call made with no program bound. Same precedence decision, and // the same reason, as DispatchComputeIndirect above. if (!PrepareCurrentProgramForDraw(__func__)) return; auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount; if (!multiDrawElementsIndirectCount) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, "Backend does not support indirect-parameter indexed draws.")); return; } MultiDrawElementsIndirectCount_Backend(mode, type, indirect, drawcount, maxdrawcount, stride); } void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) { // Argument validation before the backend-availability check: see DispatchComputeIndirect. // DrawArraysIndirectCommand: count, instanceCount, first, baseInstance. if (!ValidateIndirectCountDraw(reinterpret_cast(indirect), drawcount, maxdrawcount, stride, 4 * sizeof(Uint32), __func__)) { return; } // See MultiDrawElementsIndirectCount, including why this one goes last. if (!PrepareCurrentProgramForDraw(__func__)) return; auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount; if (!multiDrawArraysIndirectCount) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, "Backend does not support indirect-parameter array draws.")); return; } MultiDrawArraysIndirectCount_Backend(mode, indirect, drawcount, maxdrawcount, stride); } void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices, GLint basevertex) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidateDrawElementsIndexType(__func__, type)) return; if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return; if (!ValidateDrawElementsRange(__func__, start, end)) return; DrawRangeElementsBaseVertex_Backend(mode, start, end, count, type, indices, basevertex); } void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; DrawRangeElements_Backend(mode, start, end, count, type, indices); } void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex, baseinstance); } void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidateDrawElementsIndexType(__func__, type)) return; if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return; if (!ValidateNonNegativeDrawArgument(__func__, "instancecount", instancecount)) return; DrawElementsInstancedBaseVertex_Backend(mode, count, type, indices, instancecount, basevertex); } void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance); } void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; DrawElementsInstanced_Backend(mode, count, type, indices, instancecount); } void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidateDrawElementsIndexType(__func__, type)) return; if (!ValidateIndirectDrawSource(__func__, indirect, kDrawElementsIndirectCommandBytes)) return; DrawElementsIndirect_Backend(mode, type, indirect); } void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance); } void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; DrawArraysInstanced_Backend(mode, first, count, instancecount); } void DrawArraysIndirect(GLenum mode, const void* indirect) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return; DrawArraysIndirect_Backend(mode, indirect); } void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidateDrawElementsIndexType(__func__, type)) return; if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return; AccountTransformFeedbackPrimitives(mode, count); DrawElementsBaseVertex_Backend(mode, count, type, indices, basevertex); } void DrawArrays(GLenum mode, GLint first, GLsizei count) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; AccountTransformFeedbackPrimitives(mode, count); DrawArrays_Backend(mode, first, count); } void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (drawcount < 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", __func__, "drawcount must be non-negative.")); return; } MultiDrawArrays_Backend(mode, first, count, drawcount); } void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices, GLsizei drawcount) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; MultiDrawElements_Backend(mode, count, type, indices, drawcount); } void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices, GLsizei drawcount, const GLint* basevertex) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidateDrawElementsIndexType(__func__, type)) return; if (!ValidateNonNegativeDrawArgument(__func__, "drawcount", drawcount)) return; // GL 4.6 core 10.5 defines MultiDrawElementsBaseVertex as drawcount separate // DrawElementsBaseVertex calls, so each element of the count array carries the same // non-negative requirement the single-draw entry point applies to its own count. The // whole call is rejected before any sub-draw is issued, which is what makes the error // observable at all - a driver that drew the valid prefix first would leave the // framebuffer half-written. if (count != nullptr) { for (GLsizei draw = 0; draw < drawcount; ++draw) { if (!ValidateNonNegativeDrawArgument(__func__, "every element of count", count[draw])) return; } } MultiDrawElementsBaseVertex_Backend(mode, count, type, indices, drawcount, basevertex); } void Clear(GLbitfield mask) { Clear_Backend(mask); } void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) { if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!PrepareCurrentProgramForDraw(__func__)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; AccountTransformFeedbackPrimitives(mode, count); DrawElements_Backend(mode, count, type, indices); } void BeginTransformFeedback(GLenum primitiveMode) { if (primitiveMode != GL_POINTS && primitiveMode != GL_LINES && primitiveMode != GL_TRIANGLES) { MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", __func__, "primitiveMode must be GL_POINTS, GL_LINES or GL_TRIANGLES.")); return; } if (MG_State::pGLContext->IsTransformFeedbackActive()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, "Transform feedback is already active.")); return; } const auto& program = MG_State::pGLContext->GetProgramForDraw(); if (!program || !program->GetLinkStatus() || program->GetTransformFeedbackVaryingCount() == 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique( "MG_Impl/GLImpl", __func__, "No program with transform feedback varyings is active.")); return; } // Every capture buffer slot the program's mode uses must have a buffer bound. A slot // of stride 0 - two consecutive gl_NextBuffer entries - captures nothing and so needs // no binding. const SizeT usedBufferCount = program->GetTransformFeedbackBufferCount(); for (SizeT i = 0; i < usedBufferCount; ++i) { if (program->GetTransformFeedbackStride(static_cast(i)) == 0) continue; const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback, static_cast(i)); if (point.GetBoundObject() == nullptr) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique( "MG_Impl/GLImpl", __func__, "Transform feedback buffer binding point " + std::to_string(i) + " has no buffer bound.")); return; } } MG_State::pGLContext->BeginTransformFeedback(primitiveMode, program); if (const auto beginXfb = MG_Backend::gBackendFunctionsTable.GL.BeginTransformFeedback) { beginXfb(primitiveMode); } } // Vulkan transform feedback captures triangle strips in plain (i, i+1, i+2) // vertex order, but GL decomposes odd strip triangles as (i+1, i, i+2) // (GL 4.6 table 10.1). With the geometry stage's statically-known strip // lengths the captured records are reordered in place: swap the first two // vertex records of every odd triangle within each emitted strip. static void FixupGsStripCaptureOrder(const SharedPtr& program, Uint64 inputPrimitives) { // Only Vulkan-order captures need this. A backend that runs the capture on its // own GL/ES driver (it owns the span, hence the EndTransformFeedback entry) has // already produced GL's vertex order, and reordering it again would corrupt it. if (MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback != nullptr) { return; } if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) { return; } const auto& stripTriangles = program->GetGsStripTriangles(); // Global triangle indices whose leading vertex pair must swap. Vector swapTriangles; Uint64 triangleBase = 0; for (Uint64 input = 0; input < inputPrimitives; ++input) { for (const Uint32 stripLength : stripTriangles) { for (Uint32 t = 1; t < stripLength; t += 2) { swapTriangles.push_back(triangleBase + t); } triangleBase += stripLength; } } if (swapTriangles.empty()) { return; } for (SizeT bufferIndex = 0; bufferIndex < program->GetTransformFeedbackBufferCount(); ++bufferIndex) { const Uint32 stride = program->GetTransformFeedbackStride(static_cast(bufferIndex)); if (stride == 0) continue; const auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback, static_cast(bufferIndex)); const auto& buffer = bindingPoint.GetBoundObject(); if (buffer == nullptr) continue; const Range1D range = bindingPoint.GetRange(); const Uint8* mapped = buffer->MappedData(); if (mapped == nullptr) continue; // The geometry stage amplifies, so the CPU vertex counter does not bound // the capture; the binding range's whole-triangle capacity does. const Uint64 rangeBytes = range.end > range.start ? static_cast(range.end - range.start) : 0; const Uint64 capturedTriangles = std::min(triangleBase, (rangeBytes / stride) / 3); // Observed Vulkan capture order for odd strip triangles is (i, i+2, i+1) // (winding preserved by swapping the trailing pair); GL wants // (i+1, i, i+2), which is one rotation away: (a,b,c) -> (c,a,b). Vector scratch(stride); for (const Uint64 triangle : swapTriangles) { if (triangle >= capturedTriangles) break; const SizeT v0Offset = static_cast(range.start) + static_cast(triangle * 3) * stride; const SizeT v1Offset = v0Offset + stride; const SizeT v2Offset = v1Offset + stride; Memcpy(scratch.data(), mapped + v2Offset, stride); buffer->WritebackFromBackend({const_cast(mapped) + v1Offset, stride}, v2Offset); buffer->WritebackFromBackend({const_cast(mapped) + v0Offset, stride}, v1Offset); buffer->WritebackFromBackend({scratch.data(), stride}, v0Offset); } } } void EndTransformFeedback(void) { if (!MG_State::pGLContext->IsTransformFeedbackActive()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, "Transform feedback is not active.")); return; } const auto capturedProgram = MG_State::pGLContext->GetTransformFeedbackProgram(); const Uint64 inputPrimitives = MG_State::pGLContext->GetTransformFeedbackInputPrimitives(); // Closed while the capture state is still active: a backend that captures // through its own driver reads the capture program and buffer bindings here. if (const auto endXfb = MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback) { endXfb(); } MG_State::pGLContext->EndTransformFeedback(); // Captured results must be visible to MapBuffer/GetBufferSubData after // End; the capture targets are host-coherent GPU memory, so completing // the GPU work is all that is required. auto& backendGL = MG_Backend::gBackendFunctionsTable.GL; if (backendGL.FenceSync && backendGL.ClientWaitSync) { if (auto sync = backendGL.FenceSync()) { backendGL.ClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, ~0ull); if (backendGL.DeleteSync) { backendGL.DeleteSync(sync); } } } FixupGsStripCaptureOrder(capturedProgram, inputPrimitives); } void PauseTransformFeedback(void) { if (!MG_State::pGLContext->IsTransformFeedbackActive() || MG_State::pGLContext->IsTransformFeedbackPaused()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, "Transform feedback is not active, or is already paused.")); return; } MG_State::pGLContext->SetTransformFeedbackPaused(true); if (const auto pauseXfb = MG_Backend::gBackendFunctionsTable.GL.PauseTransformFeedback) { pauseXfb(); } } void ResumeTransformFeedback(void) { if (!MG_State::pGLContext->IsTransformFeedbackActive() || !MG_State::pGLContext->IsTransformFeedbackPaused()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, "Transform feedback is not paused.")); return; } MG_State::pGLContext->SetTransformFeedbackPaused(false); if (const auto resumeXfb = MG_Backend::gBackendFunctionsTable.GL.ResumeTransformFeedback) { resumeXfb(); } } void GenTransformFeedbacks(GLsizei n, GLuint* ids) { if (n < 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", __func__, "n must be non-negative.")); return; } if (n == 0 || ids == nullptr) return; Vector names; MG_State::pGLContext->GenTransformFeedbackNames(static_cast(n), names); Memcpy(ids, names.data(), static_cast(n) * sizeof(GLuint)); } void CreateTransformFeedbacks(GLsizei n, GLuint* ids) { if (n < 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", __func__, "n must be non-negative.")); return; } if (n == 0 || ids == nullptr) return; Vector names; MG_State::pGLContext->GenTransformFeedbackNames(static_cast(n), names); // Unlike glGenTransformFeedbacks, the names are objects immediately: there is no bind step // to create them from (GL 4.6 core 13.2.1). for (const Uint name : names) { MG_State::pGLContext->CreateTransformFeedbackObject(name); } Memcpy(ids, names.data(), static_cast(n) * sizeof(GLuint)); } namespace { // Shared front half of the by-name transform feedback entry points: the object has to exist // (INVALID_OPERATION otherwise) before anything else about the call is looked at. Bool ValidateNamedTransformFeedback(GLuint xfb, const char* functionName) { if (!MG_State::pGLContext->IsTransformFeedbackObject(xfb)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", functionName, std::to_string(xfb) + " is not a transform feedback object.")); return false; } return true; } Bool ValidateTransformFeedbackBufferIndex(GLuint index, const char* functionName) { if (index >= MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", functionName, "index exceeds GL_MAX_TRANSFORM_FEEDBACK_BUFFERS.")); return false; } return true; } // A capture binding may not be changed while the object is capturing (GL 4.6 core 13.2.2). Bool ValidateNamedTransformFeedbackNotActive(GLuint xfb, const char* functionName) { if (MG_State::pGLContext->IsNamedTransformFeedbackActive(xfb)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", functionName, "The transform feedback object is capturing.")); return false; } return true; } SharedPtr ResolveTransformFeedbackBuffer(GLuint buffer, const char* functionName) { if (buffer == 0) return nullptr; if (!MG_State::pGLContext->ValidateBufferName(buffer)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", functionName, std::to_string(buffer) + " is not a buffer object.")); return nullptr; } return MG_State::pGLContext->GetBufferObject(buffer); } } // namespace void TransformFeedbackBufferBase(GLuint xfb, GLuint index, GLuint buffer) { if (!ValidateNamedTransformFeedback(xfb, __func__)) return; if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return; if (!ValidateNamedTransformFeedbackNotActive(xfb, __func__)) return; if (buffer != 0 && !MG_State::pGLContext->ValidateBufferName(buffer)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, std::to_string(buffer) + " is not a buffer object.")); return; } MG_State::pGLContext->SetNamedTransformFeedbackBinding(xfb, index, ResolveTransformFeedbackBuffer(buffer, __func__), {}, false); } void TransformFeedbackBufferRange(GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) { if (!ValidateNamedTransformFeedback(xfb, __func__)) return; if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return; if (!ValidateNamedTransformFeedbackNotActive(xfb, __func__)) return; if (offset < 0 || size <= 0 || (offset % 4) != 0 || (size % 4) != 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", __func__, "offset and size must be non-negative multiples of 4.")); return; } if (buffer != 0 && !MG_State::pGLContext->ValidateBufferName(buffer)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, std::to_string(buffer) + " is not a buffer object.")); return; } auto bufferObject = ResolveTransformFeedbackBuffer(buffer, __func__); const Range1D range{static_cast(offset), static_cast(offset) + static_cast(size)}; MG_State::pGLContext->SetNamedTransformFeedbackBinding(xfb, index, bufferObject, range, bufferObject != nullptr); } void GetTransformFeedbackiv(GLuint xfb, GLenum pname, GLint* param) { if (!ValidateNamedTransformFeedback(xfb, __func__)) return; if (!param) return; switch (pname) { case GL_TRANSFORM_FEEDBACK_ACTIVE: *param = MG_State::pGLContext->IsNamedTransformFeedbackActive(xfb) ? GL_TRUE : GL_FALSE; return; case GL_TRANSFORM_FEEDBACK_PAUSED: *param = MG_State::pGLContext->IsNamedTransformFeedbackPaused(xfb) ? GL_TRUE : GL_FALSE; return; default: MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", __func__, "pname must be GL_TRANSFORM_FEEDBACK_ACTIVE or _PAUSED.")); return; } } void GetTransformFeedbacki_v(GLuint xfb, GLenum pname, GLuint index, GLint* param) { if (!ValidateNamedTransformFeedback(xfb, __func__)) return; if (pname != GL_TRANSFORM_FEEDBACK_BUFFER_BINDING) { MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", __func__, "pname must be GL_TRANSFORM_FEEDBACK_BUFFER_BINDING.")); return; } if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return; if (!param) return; const auto binding = MG_State::pGLContext->GetNamedTransformFeedbackBinding(xfb, index); *param = binding.Buffer ? static_cast(binding.Buffer->GetExternalIndex()) : 0; } void GetTransformFeedbacki64_v(GLuint xfb, GLenum pname, GLuint index, GLint64* param) { if (!ValidateNamedTransformFeedback(xfb, __func__)) return; if (pname != GL_TRANSFORM_FEEDBACK_BUFFER_START && pname != GL_TRANSFORM_FEEDBACK_BUFFER_SIZE) { MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", __func__, "pname must be GL_TRANSFORM_FEEDBACK_BUFFER_START or _SIZE.")); return; } if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return; if (!param) return; const auto binding = MG_State::pGLContext->GetNamedTransformFeedbackBinding(xfb, index); // glTransformFeedbackBufferBase leaves both at zero; only the range form sets them // (GL 4.6 core table 23.48). if (!binding.Buffer || !binding.HasExplicitRange) { *param = 0; return; } *param = (pname == GL_TRANSFORM_FEEDBACK_BUFFER_START) ? static_cast(binding.Range.start) : static_cast(binding.Range.end - binding.Range.start); } void DeleteTransformFeedbacks(GLsizei n, const GLuint* ids) { if (n < 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", __func__, "n must be non-negative.")); return; } if (ids == nullptr) return; for (GLsizei i = 0; i < n; ++i) { const GLuint id = ids[i]; // Unknown names and 0 are silently ignored; an object whose capture span is // still open is not (GL 4.6 core 13.2.1). if (id == 0 || !MG_State::pGLContext->ValidateTransformFeedbackName(id)) continue; if (id == MG_State::pGLContext->GetBoundTransformFeedbackName() && MG_State::pGLContext->IsTransformFeedbackActive()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, "Cannot delete a transform feedback object whose capture is active.")); continue; } if (const auto deleteXfb = MG_Backend::gBackendFunctionsTable.GL.DeleteTransformFeedback) { deleteXfb(id); } MG_State::pGLContext->MarkTransformFeedbackObjectForDeletion(id); } } void BindTransformFeedback(GLenum target, GLuint id) { if (target != GL_TRANSFORM_FEEDBACK) { MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", __func__, "target must be GL_TRANSFORM_FEEDBACK.")); return; } // A running capture pins its object; only a paused one may be swapped out. if (MG_State::pGLContext->IsTransformFeedbackActive() && !MG_State::pGLContext->IsTransformFeedbackPaused()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, "Transform feedback is active and not paused.")); return; } if (!MG_State::pGLContext->ValidateTransformFeedbackName(id)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, std::to_string(id) + " is not a transform feedback object name.")); return; } MG_State::pGLContext->BindTransformFeedbackObject(id); if (const auto bindXfb = MG_Backend::gBackendFunctionsTable.GL.BindTransformFeedback) { bindXfb(id); } } GLboolean IsTransformFeedback(GLuint id) { // Name 0 is the default object, and a name glGenTransformFeedbacks handed out only // becomes the name of an object once it has been bound. return MG_State::pGLContext->IsTransformFeedbackObject(id) ? GL_TRUE : GL_FALSE; } // glDrawTransformFeedback[Stream][Instanced]: replays the vertices the named object // captured in its last completed span, as if by glDrawArraysInstanced with that count // (GL 4.6 core 10.3.7). static void DrawTransformFeedbackImpl(const char* functionName, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) { if (!PrepareCurrentProgramForDraw(functionName)) return; if (!ValidatePrimitiveModeForBackend(functionName, mode)) return; if (instancecount < 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", functionName, "instancecount must be non-negative.")); return; } // "id is not the name of a transform feedback object" has to mean the same thing here // as it does to glIsTransformFeedback, and the two predicates are not interchangeable: // a name glGenTransformFeedbacks handed out is only reserved until it is first bound, // and only the bind turns it into an object (GL 4.6 core 13.2.1). ValidateTransformFeedbackName // answers the reservation question - the right one for glBindTransformFeedback, which is // what turns a reserved name into an object - so using it here let a generated-but-unbound // name through to the completed-span check below and raised INVALID_OPERATION where the // spec asks for INVALID_VALUE. Name 0 is the default object and always drawable. if (id != 0 && !MG_State::pGLContext->IsTransformFeedbackObject(id)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", functionName, std::to_string(id) + " is not a transform feedback object name.")); return; } // GL 4.6 core 10.3.7 bounds `stream` by GL_MAX_VERTEX_STREAMS, which this implementation // answers as 1 - so stream 0 is the only one that exists and anything else is // INVALID_VALUE. Read from the getter rather than written as `stream != 0` so the two can // never drift: if vertex-stream support ever lands, this bound moves with the limit. GLint maxVertexStreams = 1; GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams); if (stream >= static_cast(std::max(maxVertexStreams, 1))) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", functionName, "stream must be less than GL_MAX_VERTEX_STREAMS.")); return; } // Drawing from an object whose capture is currently open is legal and deliberate: // it is how a transform feedback result is fed straight back into the next span // (ARB_transform_feedback2 lists no such restriction). if (!MG_State::pGLContext->HasTransformFeedbackCompletedSpan(id)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", functionName, "glEndTransformFeedback has never been called for this object.")); return; } // `stream` is provably 0 here (the bound above is 1), so this is stream 0's record. const Uint64 vertices = MG_State::pGLContext->GetTransformFeedbackRecordedVertices(id); if (vertices == 0) return; const auto count = static_cast(vertices); AccountTransformFeedbackPrimitives(mode, count); if (instancecount == 1) { DrawArrays_Backend(mode, 0, count); } else { DrawArraysInstanced_Backend(mode, 0, count, instancecount); } } void DrawTransformFeedback(GLenum mode, GLuint id) { DrawTransformFeedbackImpl(__func__, mode, id, 0, 1); } void DrawTransformFeedbackInstanced(GLenum mode, GLuint id, GLsizei instancecount) { DrawTransformFeedbackImpl(__func__, mode, id, 0, instancecount); } void DrawTransformFeedbackStream(GLenum mode, GLuint id, GLuint stream) { DrawTransformFeedbackImpl(__func__, mode, id, stream, 1); } void DrawTransformFeedbackStreamInstanced(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) { DrawTransformFeedbackImpl(__func__, mode, id, stream, instancecount); } } // namespace MobileGL::MG_Impl::GLImpl