mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
1158 lines
69 KiB
C++
1158 lines
69 KiB
C++
// MobileGL - MobileGL/MG_Backend/Record/RecordVerbSink.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
|
|
|
|
// P13 W5: the record verbs' server half, moved verbatim from MG_Remote/Server/PipeApplier.cpp
|
|
// (ServerVerbSink) so a library without a transport applies them too. Deaths and latches go
|
|
// through MG_Pipe's record seam (PipeSessionFail.h), which is SessionFail / SessionLatch wherever
|
|
// MG_Remote is linked; the wire's user-index span is checked by the subclass that has a wire.
|
|
|
|
#include "RecordVerbSink.h"
|
|
#include <MG_Backend/Record/StagedTextureStore.h>
|
|
|
|
#include <Config.h>
|
|
#include <MG_Backend/MGPipe/PipeInputs.h>
|
|
#include <MG_Backend/DirectGLES/Managers.h>
|
|
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
|
#include <MG_Backend/DirectVulkan/DirectVulkan.h>
|
|
#include <MG_Pipe/PipeApply.h>
|
|
#include <MG_Pipe/PipeSessionFail.h>
|
|
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
|
#include <MG_Util/Debug/Log.h>
|
|
#include <MG_Util/Metrics/TextureMetrics.h>
|
|
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <limits>
|
|
|
|
namespace MobileGL::MG_Record {
|
|
void RecordVerbSink::SetBackend(MG_Backend::BackendObject* backend) {
|
|
if (m_backend != backend) OnBackendChanged();
|
|
m_backend = backend;
|
|
if (backend != nullptr) {
|
|
const auto& limits = backend->GetDynamicParameters();
|
|
MG_Record::ServerStagedTexture().SetDeviceLimits(
|
|
limits.MaxTextureSize, limits.Max3DTextureSize, limits.MaxCubeMapTextureSize,
|
|
limits.MaxArrayTextureLayers, limits.MaxTextureBufferSize);
|
|
}
|
|
}
|
|
|
|
const MG_Backend::GlobalBackendFunctionsTable* RecordVerbSink::Table(const char* verb) const {
|
|
if (m_backend == nullptr) {
|
|
// DECLINE BY NAME, DO NOT DEREFERENCE. A verb that arrives before
|
|
// ServerLoop::CreateBackend has run means the hook order changed under us, and the
|
|
// honest answer is "this build did not apply it" - which DecodeAndApply reports as
|
|
// false and the lane sees as a record that did not render, rather than as a crash
|
|
// with no line saying which verb was first.
|
|
MGLOG_E_ONCE("MG_Remote server: %s arrived with no backend object; the verb is "
|
|
"DECLINED. ServerLoop::CreateBackend runs from MG_Backend::Init()'s "
|
|
"hook, before ClientSession::Start",
|
|
verb);
|
|
return nullptr;
|
|
}
|
|
return &m_backend->GetBackendFunctions();
|
|
}
|
|
|
|
Bool RecordVerbSink::OnClear(const MG_Pipe::MGPClear& clear) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("clear");
|
|
if (table == nullptr) return false;
|
|
const MG_Backend::GLFunctionsTable& gl = table->GL;
|
|
|
|
// The named framebuffer record already crossed before this verb. Scope
|
|
// only the server's resolved draw target; the next verb still observes
|
|
// the application's unchanged binding. Both backends sync from this
|
|
// handle and never need the frontend FramebufferObject.
|
|
auto& applier = MG_Pipe::MGPipeApplier();
|
|
struct ScopedClearTarget {
|
|
MG_Pipe::MGPipeApplierState& State;
|
|
MG_Pipe::MGPipeHandle Saved;
|
|
~ScopedClearTarget() { State.BoundFramebuffer[0] = Saved; }
|
|
} clearTarget{applier, applier.BoundFramebuffer[0]};
|
|
if (!MG_Pipe::MGPipeHandleIsNull(clear.Fbo)) {
|
|
if (!applier.FramebufferRecordFor(clear.Fbo))
|
|
MG_Pipe::MGPipeProtocolFatal("Clear.Fbo", "missing named framebuffer record");
|
|
applier.BoundFramebuffer[0] = clear.Fbo;
|
|
}
|
|
switch (clear.Kind) {
|
|
case MG_Pipe::kMGPipeClearKindWhole:
|
|
if (gl.Clear == nullptr) return false;
|
|
gl.Clear(static_cast<GLbitfield>(clear.BufferMask));
|
|
break;
|
|
case MG_Pipe::kMGPipeClearKindColor:
|
|
switch (clear.ValueClass) {
|
|
case MG_Pipe::kMGPipeClearValueClassFloat:
|
|
if (gl.ClearBufferfv == nullptr) return false;
|
|
gl.ClearBufferfv(GL_COLOR, clear.DrawBufferIndex,
|
|
reinterpret_cast<const GLfloat*>(clear.ColorValue));
|
|
break;
|
|
case MG_Pipe::kMGPipeClearValueClassInt:
|
|
if (gl.ClearBufferiv == nullptr) return false;
|
|
gl.ClearBufferiv(GL_COLOR, clear.DrawBufferIndex,
|
|
reinterpret_cast<const GLint*>(clear.ColorValue));
|
|
break;
|
|
case MG_Pipe::kMGPipeClearValueClassUint:
|
|
if (gl.ClearBufferuiv == nullptr) return false;
|
|
gl.ClearBufferuiv(GL_COLOR, clear.DrawBufferIndex,
|
|
reinterpret_cast<const GLuint*>(clear.ColorValue));
|
|
break;
|
|
default:
|
|
// A value class outside the three is a wire fault, not a fallback: all three
|
|
// representations of a clear colour are numerically populated by the frontend
|
|
// and only this field says which one the backend must use, so guessing renders
|
|
// a plausible wrong colour.
|
|
MG_Pipe::MGPipeProtocolFatalAt("MGPClear::ValueClass", clear.ValueClass, 3);
|
|
}
|
|
break;
|
|
case MG_Pipe::kMGPipeClearKindDepth:
|
|
if (gl.ClearBufferfv == nullptr) return false;
|
|
gl.ClearBufferfv(GL_DEPTH, 0, &clear.DepthValue);
|
|
break;
|
|
case MG_Pipe::kMGPipeClearKindStencil:
|
|
if (gl.ClearBufferiv == nullptr) return false;
|
|
gl.ClearBufferiv(GL_STENCIL, 0, &clear.StencilValue);
|
|
break;
|
|
case MG_Pipe::kMGPipeClearKindDepthStencil:
|
|
if (gl.ClearBufferfi == nullptr) return false;
|
|
gl.ClearBufferfi(GL_DEPTH_STENCIL, 0, clear.DepthValue, clear.StencilValue);
|
|
break;
|
|
default:
|
|
MG_Pipe::MGPipeProtocolFatalAt("MGPClear::Kind", clear.Kind, MG_Pipe::kMGPipeClearKindDepthStencil + 1);
|
|
}
|
|
++m_clears;
|
|
return true;
|
|
}
|
|
|
|
Bool RecordVerbSink::OnBlit(const MG_Pipe::MGPBlit& blit) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("blit");
|
|
if (table == nullptr) return false;
|
|
if (table->GL.BlitFramebuffer == nullptr) return false;
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// P5c (hd, CONTRACT-P5C §3.3): the record's handles cross to the backend as the verb's
|
|
// own state. The bound form carries two nulls and nothing changes; the named form's
|
|
// pair is what the backend's named-blit arm resolves - the sink no longer relies on
|
|
// "the read and draw framebuffers are already bound" (the client's ScopedBlitBindings
|
|
// staging is deleted with this), and a backend that does not consume the pair has no
|
|
// named arm, which is a loud decline rather than a blit of whatever is bound.
|
|
auto& applierState = MG_Pipe::MGPipeApplier();
|
|
applierState.ClearVerbHandles();
|
|
applierState.VerbBlitReadFbo = blit.ReadFbo;
|
|
applierState.VerbBlitDrawFbo = blit.DrawFbo;
|
|
const Bool named = !MG_Pipe::MGPipeHandleIsNull(blit.ReadFbo) ||
|
|
!MG_Pipe::MGPipeHandleIsNull(blit.DrawFbo);
|
|
#endif
|
|
table->GL.BlitFramebuffer(blit.SrcX0, blit.SrcY0, blit.SrcX1, blit.SrcY1, blit.DstX0,
|
|
blit.DstY0, blit.DstX1, blit.DstY1,
|
|
static_cast<GLbitfield>(blit.Mask),
|
|
static_cast<GLenum>(blit.Filter));
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (named && !applierState.VerbBlitNamedConsumed) {
|
|
MGLOG_E_ONCE("MGPipe: a named blit (read {%u, %u}, draw {%u, %u}) reached a backend "
|
|
"with no named-blit arm; the verb is DECLINED rather than applied to "
|
|
"the bound framebuffers",
|
|
blit.ReadFbo.Slot, blit.ReadFbo.Gen, blit.DrawFbo.Slot, blit.DrawFbo.Gen);
|
|
applierState.VerbBlitReadFbo = MG_Pipe::kMGPipeNullHandle;
|
|
applierState.VerbBlitDrawFbo = MG_Pipe::kMGPipeNullHandle;
|
|
return false;
|
|
}
|
|
#endif
|
|
++m_blits;
|
|
return true;
|
|
}
|
|
|
|
Bool RecordVerbSink::OnGetTextureImage(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
|
|
MG_Pipe::MGPipeReplySink* replies) {
|
|
if (!replies) MG_Pipe::MGPipeProtocolFatal("GetTextureImage.reply", "missing reply sink");
|
|
const SizeT bpp = MG_Util::GetInputBytesPerPixel(
|
|
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
|
|
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type));
|
|
if (!bpp || !info.Box.W || !info.Box.H || !info.Box.D || info.Box.X || info.Box.Y || info.Box.Z) {
|
|
MG_Pipe::MGPipeProtocolFatal("GetTextureImage.extent", "invalid tight image extent");
|
|
}
|
|
const Uint64 tight = static_cast<Uint64>(info.Box.W) * info.Box.H * info.Box.D * bpp;
|
|
if (tight / bpp / info.Box.W / info.Box.H != info.Box.D ||
|
|
info.DstOffset > tight || !info.DstSize || info.DstSize > tight - info.DstOffset)
|
|
MG_Pipe::MGPipeProtocolFatal("GetTextureImage.range", "invalid reply window");
|
|
auto image = info;
|
|
image.DstOffset = 0;
|
|
image.DstSize = tight;
|
|
Vector<Uint8> bytes;
|
|
const Bool ok = ReadTextureImageTight(image, bytes);
|
|
if (!ok || bytes.size() != tight) {
|
|
replies->PostReply(seq, MG_Pipe::MGPipeReplySink::kStatusError, nullptr, 0);
|
|
return false;
|
|
}
|
|
replies->PostReply(seq, MG_Pipe::MGPipeReplySink::kStatusOk, bytes.data() + info.DstOffset, info.DstSize);
|
|
m_readbackBytes += info.DstSize;
|
|
++m_readbacks;
|
|
return true;
|
|
}
|
|
|
|
Bool RecordVerbSink::OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
|
|
MG_Pipe::MGPipeReplySink* replies) {
|
|
if (replies == nullptr) {
|
|
// The decoder always passes its ReplySink; a null one means the applier was built
|
|
// without a reply pool, and answering nothing would leave the client's barrier
|
|
// waiting for a slot that never gets stamped - a hang, not a wrong picture.
|
|
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"read_pixels without a reply sink\"} - "
|
|
"the pixels' only destination in P5 is SEG_REPLY (contract table 1 row 23) "
|
|
"and a client blocked on seq %llu would never be answered",
|
|
static_cast<unsigned long long>(seq));
|
|
}
|
|
const SizeT bytesPerPixel = MG_Util::GetInputBytesPerPixel(
|
|
MG_Util::ConvertGLEnumToTextureInputFormat(static_cast<GLenum>(info.Format)),
|
|
MG_Util::ConvertGLEnumToTexturePixelDataType(static_cast<GLenum>(info.Type)));
|
|
// The server's LinkTerms.maxReplyBytes is copied from its attached ILink capabilities.
|
|
// Reject malformed shapes and oversized replies before growing m_readbackScratch. The
|
|
// client-side guard is not a trust boundary: a peer can write this record directly.
|
|
const auto rejectReadback = [&] {
|
|
replies->PostReply(seq, MG_Pipe::MGPipeReplySink::kStatusError, nullptr, 0);
|
|
return false;
|
|
};
|
|
if (bytesPerPixel == 0 || info.Box.W == 0 || info.Box.H == 0 ||
|
|
info.Box.W > static_cast<Uint32>(std::numeric_limits<GLsizei>::max()) ||
|
|
info.Box.H > static_cast<Uint32>(std::numeric_limits<GLsizei>::max()) ||
|
|
info.DstSize == 0 || m_maxReplyBytes == 0) {
|
|
return rejectReadback();
|
|
}
|
|
|
|
const Uint64 width = info.Box.W;
|
|
const Uint64 height = info.Box.H;
|
|
const Uint64 bpp = static_cast<Uint64>(bytesPerPixel);
|
|
constexpr Uint64 kUint64Max = std::numeric_limits<Uint64>::max();
|
|
if (width > kUint64Max / height) return rejectReadback();
|
|
const Uint64 pixels = width * height;
|
|
if (pixels > kUint64Max / bpp) return rejectReadback();
|
|
const Uint64 tight = pixels * bpp;
|
|
// PH-3's bound is the SERVER's answer size: w*h*bpp against its own maxReplyBytes,
|
|
// checked before the scratch grows. It is not a rule about DstSize.
|
|
if (tight > m_maxReplyBytes ||
|
|
tight > static_cast<Uint64>(std::numeric_limits<SizeT>::max())) {
|
|
return rejectReadback();
|
|
}
|
|
// ID-49: THE REPLY CROSSES TIGHT, WHATEVER DstSize THE CLIENT SENT. The server reads with
|
|
// neutral pack state into a w*h*bpp extent that IS the reply payload and the client
|
|
// scatters it per its own GL_PACK_* state, so a DstSize that disagrees with the tight
|
|
// extent (a client that sized its destination with pack padding) is logged and the tight
|
|
// extent this side owns is read and posted - never the client's number, so a wrong
|
|
// DstSize cannot make this a short read into uninitialised scratch. F2's first PH-3 draft
|
|
// refused the mismatch instead, which broke the ID-49 control
|
|
// (ServerLoopEglTest.AReadPixelsReplyIsTheTightExtentWhateverDstSizeTheClientSent); the
|
|
// bound above is what PH-3 needs and it does not depend on DstSize.
|
|
if (tight != info.DstSize) {
|
|
MGLOG_E_ONCE("MG_Remote server: read_pixels DstSize %llu != tight w*h*bpp %llu "
|
|
"(%ux%u, bpp %llu); reading the tight extent (ID-49)",
|
|
static_cast<unsigned long long>(info.DstSize),
|
|
static_cast<unsigned long long>(tight), info.Box.W, info.Box.H,
|
|
static_cast<unsigned long long>(bpp));
|
|
}
|
|
|
|
if (!ReadBoundFramebufferTight(info, tight)) {
|
|
// A well-formed call can still be unsupported by this server; return the
|
|
// established decline status after the peer-controlled size has been checked.
|
|
replies->PostReply(seq, MG_Pipe::MGPipeReplySink::kStatusDeclined, nullptr, 0);
|
|
return false;
|
|
}
|
|
|
|
// m-7: the answer is written into the slot HERE, mid-apply, while the verb stamp is still
|
|
// up - and that is safe for exactly one reason, which is the contract's and is stated so it
|
|
// is not mistaken for luck: the client reaches a reply slot ONLY through appliedSeq
|
|
// (ReplySlot.h's ORDERING clause), never by polling the slot's own stamp, and s1's
|
|
// SessionConsumer::ApplyOne publishes appliedSeq only AFTER PipeApplier::ApplyOne has run
|
|
// LeaveApplier() and (on the joint tree) dropped the ScopedApplierEntry. So by the time the
|
|
// client is allowed to look at this slot, the apply-side gPipeInputs flag is already down.
|
|
replies->PostReply(seq, MG_Pipe::MGPipeReplySink::kStatusOk, m_readbackScratch.data(), tight);
|
|
m_readbackBytes += tight;
|
|
++m_readbacks;
|
|
return true;
|
|
}
|
|
|
|
Bool RecordVerbSink::ReadBoundFramebufferTight(const MG_Pipe::MGPReadbackInfo& info, Uint64 tight) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("read_pixels");
|
|
if (table == nullptr || table->GL.ReadPixels == nullptr) return false;
|
|
if (tight > m_readbackScratch.size()) {
|
|
m_readbackScratch.resize(static_cast<SizeT>(tight));
|
|
}
|
|
|
|
// Save the server-visible pack state, force neutral for the read, restore. Both go through
|
|
// the applier's own set_pixel_pack_state entry point (MGPipeApplySetPixelPackState writes
|
|
// gPipeInputs.m_pixelStore[0], which the backend's ReadPixels reads via
|
|
// gPipeInputs->GetPixelStoreParameters); the read is synchronous on this thread, so the window
|
|
// in which the pack state is neutral does not outlive the call.
|
|
const MG_Pipe::PixelStoreParameters savedPack =
|
|
MG_Pipe::gPipeInputs.GetPixelStoreParameters(/*isUnpack=*/false);
|
|
// MG_Pipe owns the constant (MGPipeTypes.h): in a verify build the compare-at-read hook's
|
|
// oracle for the pack half inside this window is the same value, and a second hand-typed
|
|
// copy would drift without a build break.
|
|
const MG_Pipe::MGPPixelPackState neutralPack = MG_Pipe::MGPipeNeutralReadPixelsPack();
|
|
MG_Pipe::MGPipeApplySetPixelPackState(neutralPack);
|
|
|
|
table->GL.ReadPixels(info.Box.X, info.Box.Y, static_cast<GLsizei>(info.Box.W),
|
|
static_cast<GLsizei>(info.Box.H), static_cast<GLenum>(info.Format),
|
|
static_cast<GLenum>(info.Type), m_readbackScratch.data());
|
|
|
|
MG_Pipe::MGPPixelPackState restorePack{};
|
|
restorePack.Pack = savedPack;
|
|
MG_Pipe::MGPipeApplySetPixelPackState(restorePack);
|
|
// A backend that found its device lost latched this session and returned without filling
|
|
// the scratch: the read is declined rather than answered with stale bytes.
|
|
return !MG_Pipe::MGPipeRecordLatched();
|
|
}
|
|
|
|
Bool RecordVerbSink::ReadTextureImageTight(const MG_Pipe::MGPReadbackInfo& image, Vector<Uint8>& bytes) {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (m_backend && m_backend->GetBackendType() == BackendType::DirectGLES)
|
|
return MG_Backend::DirectGLES::ReadTextureImageWire(image, bytes);
|
|
if (m_backend && m_backend->GetBackendType() == BackendType::DirectVulkan &&
|
|
MG_Backend::DirectVulkan::pVulkanRenderer)
|
|
return MG_Backend::DirectVulkan::pVulkanRenderer->ReadTextureImageWire(image, bytes);
|
|
#endif
|
|
(void)image;
|
|
(void)bytes;
|
|
return false;
|
|
}
|
|
|
|
// ---- P9 (W1, MG_Remote/CONTRACT-P9.md §1): the pack-buffer readbacks ------------------------
|
|
//
|
|
// WHY THE ROWS GO THROUGH THE APPLIER'S BUFFER-WRITE GATE AND NOT THROUGH THE DRIVER'S OWN
|
|
// PBO READ. Monolith binds the driver buffer as GL_PIXEL_PACK_BUFFER and lets glReadPixels
|
|
// write it, then maps the whole store back into the frontend shadow (DirectGLES.cpp's
|
|
// "PBO used, mapping buffer to client memory"). A server has two stores per buffer - the
|
|
// backend's (GL buffer / VkBuffer) and R-11's staged shadow that every later drain uploads
|
|
// from - and a driver-side write reaches only the first: the next queued range drained from
|
|
// the shadow would then land OLD bytes over the pixels on Espryt, and Magma has no pack path
|
|
// in its ReadPixels at all. MGPipeApplyResourceSubData is the one write both backends already
|
|
// take from the server (Ops_H_SubData adopts into the staged shadow and queues the range;
|
|
// WriteWireBuffer orders the copy against in-flight reads), so the rows are exactly what a
|
|
// client glBufferSubData of the same bytes would have been - same serial bump, same epoch
|
|
// bump, same ordering against the records that follow.
|
|
//
|
|
// THE READ IS THE REPLY FORM'S, byte for byte (ReadBoundFramebufferTight /
|
|
// ReadTextureImageTight), so the two forms cannot disagree about what a pixel is; only the
|
|
// destination moved. The PACK layout is the client's arithmetic (ReadbackLayout) carried in
|
|
// the record, and the byte swap is applied to the tight bytes first, exactly where the client
|
|
// applied it to its reply.
|
|
//
|
|
// A FAILED READ IS LOUD. The reply form's failures reach the client as ERROR / DECLINED and
|
|
// end in a named Fatal there (RequireReadbackReplyComplete: "a blocking readback has no pixels
|
|
// to return"); this form has no reply to carry them, and a pack buffer left holding its old
|
|
// bytes is the same silently wrong answer. So the refusal latches here, under the same family
|
|
// words.
|
|
|
|
// Every number here is the peer's, and they are checked BEFORE the read, so a corrupt record
|
|
// costs no GPU work - and completely enough that LandReadbackInBuffer cannot meet a range it
|
|
// would have to refuse: every run it writes starts at or before the LAST row's offset and is
|
|
// no wider than the tight extent, and both are bounded here by what one MGPSubData can name
|
|
// (MGPipeSetSubDataBufferRange: an offset up to 2^31-1, a width up to 2^32-1). Whether the
|
|
// range fits the buffer is the applier's own gate, the one every buffer write meets.
|
|
static const char* ReadbackToBufferLayoutFault(const MG_Pipe::MGPReadbackToBuffer& record,
|
|
Uint64 bytesPerPixel) {
|
|
const MG_Pipe::MGPBox& box = record.Src.Box;
|
|
const Uint64 rowBytes = static_cast<Uint64>(box.W) * bytesPerPixel;
|
|
const Uint64 tightBytes = rowBytes * box.H * box.D; // the caller proved this fits
|
|
if (record.RowStride < rowBytes) return "the row stride is narrower than a tight row";
|
|
if (box.D > 1 && record.ImageStride < record.RowStride) return "the image stride is narrower than a row";
|
|
if (record.SwapGroup != 0 && record.SwapGroup != 2 && record.SwapGroup != 4 && record.SwapGroup != 8)
|
|
return "the swap group is not a byte group GL_PACK_SWAP_BYTES reverses (2, 4 or 8)";
|
|
constexpr Uint64 kMaxOffset = 0x7FFFFFFFull;
|
|
const Uint64 lastImage = box.D - 1;
|
|
const Uint64 lastRow = box.H - 1;
|
|
if (record.DstBase > kMaxOffset) return "the first row starts past 2^31-1";
|
|
if (lastImage != 0 && record.ImageStride > (kMaxOffset - record.DstBase) / lastImage)
|
|
return "the last image starts past 2^31-1";
|
|
const Uint64 imageBase = record.DstBase + lastImage * record.ImageStride;
|
|
if (lastRow != 0 && record.RowStride > (kMaxOffset - imageBase) / lastRow)
|
|
return "the last row starts past 2^31-1";
|
|
if (tightBytes > 0xFFFFFFFFull) return "the read is wider than one buffer write can name";
|
|
return nullptr;
|
|
}
|
|
|
|
Bool RecordVerbSink::LandReadbackInBuffer(const MG_Pipe::MGPReadbackToBuffer& record, Uint8* tight,
|
|
Uint64 bytesPerPixel) {
|
|
const MG_Pipe::MGPBox& box = record.Src.Box;
|
|
const Uint64 rowBytes = static_cast<Uint64>(box.W) * bytesPerPixel;
|
|
const Uint64 rows = box.H;
|
|
const Uint64 images = box.D;
|
|
const Uint64 tightBytes = rowBytes * rows * images;
|
|
if (record.SwapGroup != 0) {
|
|
const Uint64 group = record.SwapGroup;
|
|
for (Uint64 at = 0; at + group <= tightBytes; at += group) {
|
|
for (Uint64 i = 0; i < group / 2; ++i) std::swap(tight[at + i], tight[at + group - 1 - i]);
|
|
}
|
|
}
|
|
|
|
// ONE WRITE PER CONTIGUOUS RUN: the whole read when the layout is tight (the common
|
|
// case - a neutral pack state, or an image-packed read of whole images), one per image
|
|
// when only the rows are tight, one per row otherwise. The bytes between rows are the
|
|
// application's (GL_PACK_ROW_LENGTH / ALIGNMENT padding) and are never written.
|
|
const Bool rowsTight = record.RowStride == rowBytes;
|
|
const Bool imagesTight = images == 1 || record.ImageStride == rowBytes * rows;
|
|
const Uint64 runBytes = rowsTight ? (imagesTight ? tightBytes : rowBytes * rows) : rowBytes;
|
|
const Uint64 runsPerImage = rowsTight ? 1 : rows;
|
|
const Uint64 imageRuns = rowsTight && imagesTight ? 1 : images;
|
|
for (Uint64 z = 0; z < imageRuns; ++z) {
|
|
for (Uint64 r = 0; r < runsPerImage; ++r) {
|
|
// No overflow and no refusal below: ReadbackToBufferLayoutFault bounded the last
|
|
// offset and the widest run before the read.
|
|
const Uint64 offset = record.DstBase + z * record.ImageStride + r * record.RowStride;
|
|
MG_Pipe::MGPSubData write{};
|
|
write.Res = record.Dst;
|
|
write.Target = MG_Pipe::kMGPipeResourceTargetBuffer;
|
|
// HasLiveHostWrites stays 0 and that is the client's fact, not a default: the
|
|
// client takes this form only for a pack buffer it does not map
|
|
// (TextureReadbackEmit.inc, PackBufferReadbackTarget), so "no live host writer" is
|
|
// what a glBufferSubData of the same bytes would have said.
|
|
if (!MG_Pipe::MGPipeSetSubDataBufferRange(write, offset, runBytes)) return false;
|
|
const Uint8* bytes = tight + (z * runsPerImage + r) * runBytes;
|
|
++m_bufferReadbackWrites;
|
|
// The acceptance is the applier's (a dead handle is a counted no-op, a range
|
|
// past the store a trip wire), so a refused write ends the landing here.
|
|
const MG_Pipe::MGPipeServerOriginatedWriteScope serverWrite;
|
|
if (!MG_Pipe::MGPipeApplyResourceSubData(write, bytes, nullptr)) return false;
|
|
}
|
|
}
|
|
++m_bufferReadbacks;
|
|
return true;
|
|
}
|
|
|
|
Bool RecordVerbSink::OnReadPixelsToBuffer(const MG_Pipe::MGPReadbackToBuffer& record) {
|
|
const MG_Pipe::MGPReadbackInfo& info = record.Src;
|
|
const Uint64 bpp = static_cast<Uint64>(MG_Util::GetInputBytesPerPixel(
|
|
MG_Util::ConvertGLEnumToTextureInputFormat(static_cast<GLenum>(info.Format)),
|
|
MG_Util::ConvertGLEnumToTexturePixelDataType(static_cast<GLenum>(info.Type))));
|
|
// The reply form's shape checks and its PH-3 bound: the scratch is sized from the record,
|
|
// so the SERVER's own maxReplyBytes caps it exactly as it caps an answer - the client bands
|
|
// this form by the same number (PlanReadbackBands), so no correct client is refused by it.
|
|
// ONE latch site for every shape fault (PeerLatchTest's row names it once).
|
|
constexpr Uint64 kUint64Max = std::numeric_limits<Uint64>::max();
|
|
const char* fault = nullptr;
|
|
Uint64 tight = 0;
|
|
if (bpp == 0 || info.Box.W == 0 || info.Box.H == 0 || info.Box.D != 1 ||
|
|
info.Box.W > static_cast<Uint32>(std::numeric_limits<GLsizei>::max()) ||
|
|
info.Box.H > static_cast<Uint32>(std::numeric_limits<GLsizei>::max())) {
|
|
fault = "the read box or its format is invalid";
|
|
} else if (static_cast<Uint64>(info.Box.W) > kUint64Max / info.Box.H ||
|
|
static_cast<Uint64>(info.Box.W) * info.Box.H > kUint64Max / bpp) {
|
|
fault = "the tight extent overflows";
|
|
} else {
|
|
tight = static_cast<Uint64>(info.Box.W) * info.Box.H * bpp;
|
|
if (m_maxReplyBytes == 0 || tight > m_maxReplyBytes ||
|
|
tight > static_cast<Uint64>(std::numeric_limits<SizeT>::max())) {
|
|
fault = "the tight extent is past this server's maxReplyBytes (PH-3)";
|
|
} else if (info.DstOffset != 0 || info.DstSize != tight) {
|
|
fault = "Src.DstOffset / Src.DstSize are not 0 / the tight extent";
|
|
} else {
|
|
fault = ReadbackToBufferLayoutFault(record, bpp);
|
|
}
|
|
}
|
|
if (fault != nullptr) {
|
|
return MG_Pipe::MGPipeRecordLatch(MG_Pipe::MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ReadPixelsToBuffer.shape\"} %s", fault);
|
|
}
|
|
if (!ReadBoundFramebufferTight(info, tight)) {
|
|
return MG_Pipe::MGPipeRecordLatch(MG_Pipe::MGFatalFamily::ReadbackDeclined, "MGPipe: Fatal{ReadbackDeclined, \"ReadPixelsToBuffer\"} - the server "
|
|
"has no GL.ReadPixels; the pack buffer would keep bytes the application is "
|
|
"about to read as pixels, and there is no reply to carry the decline");
|
|
}
|
|
return LandReadbackInBuffer(record, m_readbackScratch.data(), bpp);
|
|
}
|
|
|
|
Bool RecordVerbSink::OnGetTextureImageToBuffer(const MG_Pipe::MGPReadbackToBuffer& record) {
|
|
const MG_Pipe::MGPReadbackInfo& info = record.Src;
|
|
const Uint64 bpp = static_cast<Uint64>(MG_Util::GetInputBytesPerPixel(
|
|
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
|
|
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)));
|
|
const char* fault = nullptr;
|
|
Uint64 tight = 0;
|
|
if (!bpp || !info.Box.W || !info.Box.H || !info.Box.D || info.Box.X || info.Box.Y || info.Box.Z) {
|
|
fault = "the image extent is not a whole level's";
|
|
} else {
|
|
tight = static_cast<Uint64>(info.Box.W) * info.Box.H * info.Box.D * bpp;
|
|
if (tight / bpp / info.Box.W / info.Box.H != info.Box.D) {
|
|
fault = "the tight extent overflows";
|
|
} else if (info.DstOffset != 0 || info.DstSize != tight) {
|
|
fault = "Src.DstOffset / Src.DstSize are not 0 / the tight extent";
|
|
} else {
|
|
fault = ReadbackToBufferLayoutFault(record, bpp);
|
|
}
|
|
}
|
|
if (fault != nullptr) {
|
|
return MG_Pipe::MGPipeRecordLatch(MG_Pipe::MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"GetTextureImageToBuffer.shape\"} %s", fault);
|
|
}
|
|
Vector<Uint8> bytes;
|
|
if (!ReadTextureImageTight(info, bytes) || bytes.size() != tight) {
|
|
return MG_Pipe::MGPipeRecordLatch(MG_Pipe::MGFatalFamily::ReplyError, "MGPipe: Fatal{ReplyError, \"GetTextureImageToBuffer\"} - the level "
|
|
"could not be read (%llu of %llu bytes); the reply form answers ERROR here, and "
|
|
"this form has no reply to carry it",
|
|
static_cast<unsigned long long>(bytes.size()),
|
|
static_cast<unsigned long long>(tight));
|
|
}
|
|
return LandReadbackInBuffer(record, bytes.data(), bpp);
|
|
}
|
|
|
|
// P8-F: A PROTOCOL GUARD, NOT AN UNMIGRATED VERB. This was P5b's server-side stub shape
|
|
// (`Fatal{UnmigratedVerb, ...}`, "ServerVerbSink has no body for it yet"), but no package is
|
|
// going to give these two multi-draw shapes a body, because our client can no longer send
|
|
// either: client index arrays become one owned element buffer before the record is built
|
|
// (EmitTables.cpp's multi-draw arm, OwnedDrawInputs.h clears kDrawHasUserIndices), and no GL
|
|
// entry point produces an instanced multi-draw. A record that carries one came from a
|
|
// foreign peer, so the family is the one every other malformed-record check uses.
|
|
//
|
|
// PH-1 (3): the shape is the PEER's (a multi-draw record it chose to send), so in an armed
|
|
// session child the refusal latches and the verb returns false; unarmed it still dies.
|
|
static Bool MultiDrawShapeLatch(const char* slot) {
|
|
return MG_Pipe::MGPipeRecordLatch(MG_Pipe::MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} a multi-draw "
|
|
"record with a shape this protocol does not carry: client indices travel as an owned "
|
|
"element buffer and no GL entry point produces an instanced multi-draw",
|
|
slot);
|
|
}
|
|
|
|
// P5b d1 (MG_Remote/CONTRACT-P5B.md §2 d1): draw_vbo's whole cross product. The record
|
|
// carries the GL call verbatim (rule D) and this body reproduces the backend call the
|
|
// monolith makes for that shape, reading only the record and the backend's own
|
|
// barrier-pulled state; the twenty GL entry points collapse onto the arms below:
|
|
//
|
|
// kDrawIsIndirect arrays: DrawArraysIndirect (DrawCount 1, Stride 0) /
|
|
// MultiDrawArraysIndirect / MultiDrawArraysIndirectCount (a parameter
|
|
// buffer named); indexed: the three Elements twins
|
|
// NumDraws != 1 MultiDrawArrays / MultiDrawElementsBaseVertex, the two arrays
|
|
// rebuilt from the ranges into bounded locals (rule C)
|
|
// arrays, one range DrawArrays / DrawArraysInstanced / DrawArraysInstancedBaseInstance
|
|
// indexed, one range DrawElementsBaseVertex (the P5 arm, unchanged, bias 0 for a plain
|
|
// DrawElements) / DrawRangeElements[BaseVertex] under
|
|
// kDrawHasIndexRange / the four DrawElementsInstanced* by whether a
|
|
// base vertex and a base instance are non-zero
|
|
// kDrawHasUserIndices the `indices` argument is the resolved SEG_STAGE run instead of an
|
|
// element-buffer offset (the client staged a client index array)
|
|
//
|
|
// "Instanced" is InstanceCount != 1 || StartInstance != 0: an instanced call with a count
|
|
// of 1 and no base instance is the plain draw it is equivalent to, and a count of 0 must
|
|
// NOT collapse onto the plain draw (it draws nothing, the plain draw would draw once).
|
|
//
|
|
// What is still refused by name, as protocol corruption (P8-F): a multi-draw that arrived
|
|
// with a span (our client stages client indices into an owned element buffer and never sets
|
|
// kDrawHasUserIndices) and a multi-draw that claims instancing (no GL entry point produces
|
|
// one; the client never sends it). Only a foreign peer reaches either.
|
|
#if MOBILEGL_BUILD_RECORD_ARM
|
|
namespace {
|
|
// P5e (tx2), CONTRACT-P5E §5.3 / ruling 19 (ID-95, A8 closed). THE TWO UNIT WINDOWS MUST
|
|
// COVER [0, MaxTouchedTextureUnit], AND THIS IS WHERE THAT PROMISE IS CHECKED.
|
|
//
|
|
// Everything the texture and sampler families do per draw now reads
|
|
// [SamplerViewStart, +SamplerViewCount) and [SamplerStateStart, +SamplerStateCount).
|
|
// Under run-ahead a window NARROWER than the frontend's high-water mark silently drops a
|
|
// sync of a texture the draw is about to sample, or leaves an earlier draw's sampler
|
|
// object on a unit - and a decline is exactly what run-ahead cannot take, because the
|
|
// client has already moved on and there is no wait in which to notice.
|
|
//
|
|
// A SERVER-SIDE RE-DERIVATION IS REFUSED, and that is the ruling's point: the client owns
|
|
// the high-water mark (it is the `count` argument SamplerEmit.h passes, Start=0 /
|
|
// Count=maxTouched+1) and the server's job is to check the promise, not to invent a
|
|
// second authority for it. So this is a CHECK and its failure is corruption.
|
|
//
|
|
// WHERE THE MARK COMES FROM: MGPContextValues::MaxTouchedTextureUnit, applied by
|
|
// set_context_values, which precedes the verb on the ring - the same ordering §2.1's
|
|
// XFB clause leans on. It is RECORD_SUPPLIED, so reading it is reading what a record
|
|
// put there and not client memory (rule F).
|
|
//
|
|
// WIDER IS FINE. A window larger than the mark costs a walk over provably-empty units;
|
|
// only SMALLER is unrepresentable. An empty window on a draw that touches no unit at all
|
|
// (mark 0 with nothing ever touched) is the correct and only possible emission, so a
|
|
// count of 0 is admitted exactly while the applied counters say no unit was touched.
|
|
//
|
|
// THE ONE ADMITTED SILENCE, and it is the A/B rather than a hole: a client whose sampler
|
|
// subsystem bit is CLEAR emits neither record, the backend then runs its pre-handle
|
|
// frontend walk, and there is no window to check because there is no window. That is
|
|
// "both counts are still 0", and it is distinguishable from the narrowing this refuses
|
|
// (a narrowed window carries a non-zero count that is merely too small). The moment
|
|
// either set has been received, the rule binds.
|
|
//
|
|
// PH-1 (3): both windows are the PEER's records, so the refusals latch in an armed
|
|
// session child (false = latched, the verb declines); unarmed they still die.
|
|
Bool CheckUnitWindows(const MG_Pipe::MGPipeApplierState& st, const char* verb) {
|
|
if (st.SamplerViewCount == 0 && st.SamplerStateCount == 0) return true;
|
|
const Int maxTouched = MG_Pipe::gPipeInputs.GetMaxTouchedTextureUnit();
|
|
if (maxTouched < 0) return true;
|
|
const Uint32 required = static_cast<Uint32>(maxTouched) + 1u;
|
|
if (st.SamplerViewStart != 0 || st.SamplerViewCount < required) {
|
|
return MG_Pipe::MGPipeRecordLatch(MG_Pipe::MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"SetSamplerViews.Count\"} - %s applies with "
|
|
"units 0..%d touched, so set_sampler_views must carry Start=0 and Count >= %u "
|
|
"(CONTRACT-P5E.md §5.3); the applied window is Start=%u Count=%u, which drops "
|
|
"the sync of at least one texture this draw samples",
|
|
verb, static_cast<int>(maxTouched), required, st.SamplerViewStart,
|
|
st.SamplerViewCount);
|
|
}
|
|
if (st.SamplerStateStart != 0 || st.SamplerStateCount < required) {
|
|
return MG_Pipe::MGPipeRecordLatch(MG_Pipe::MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - %s applies with "
|
|
"units 0..%d touched, so bind_sampler_states must carry Start=0 and Count >= %u "
|
|
"(CONTRACT-P5E.md §5.3); the applied window is Start=%u Count=%u, which leaves "
|
|
"an earlier draw's sampler object on at least one unit",
|
|
verb, static_cast<int>(maxTouched), required, st.SamplerStateStart,
|
|
st.SamplerStateCount);
|
|
}
|
|
return true;
|
|
}
|
|
} // namespace
|
|
#endif
|
|
|
|
Bool RecordVerbSink::OnDrawVbo(const MG_Pipe::MGPDrawInfo& info,
|
|
const MG_Pipe::MGPDrawRange* ranges,
|
|
const MG_Pipe::MGHostSpan* userIndices,
|
|
const MG_Pipe::MGPDrawIndirect* indirect) {
|
|
if (userIndices != nullptr && !CheckUserIndices(info, ranges, *userIndices)) {
|
|
return false; // PH-1 (3): latched (armed session child); unarmed it died inside
|
|
}
|
|
// The witness first, before the backend is consulted, so a unit process with no
|
|
// backend object still sees the wire's fields (PipeApplier.h LastDraw).
|
|
m_lastDraw = LastDrawRecord{};
|
|
m_lastDraw.Info = info;
|
|
if (ranges != nullptr && info.NumDraws != 0) m_lastDraw.FirstRange = ranges[0];
|
|
if (userIndices != nullptr) {
|
|
m_lastDraw.HadUserIndices = true;
|
|
m_lastDraw.UserIndexBytes = userIndices->Size;
|
|
}
|
|
if (indirect != nullptr) {
|
|
m_lastDraw.HadIndirect = true;
|
|
m_lastDraw.Indirect = *indirect;
|
|
}
|
|
++m_drawRecords;
|
|
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// Ruling 19 / ID-95: the window promise, checked at every draw, before the backend is
|
|
// asked to resolve anything out of the windows.
|
|
if (!CheckUnitWindows(MG_Pipe::MGPipeApplier(), "draw_vbo")) return false;
|
|
#endif
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("draw_vbo");
|
|
if (table == nullptr) return false;
|
|
const MG_Backend::GLFunctionsTable& gl = table->GL;
|
|
const auto mode = static_cast<GLenum>(info.Mode);
|
|
|
|
GLenum indexType = 0;
|
|
switch (info.IndexSize) {
|
|
case 0: break; // arrays
|
|
case 1: indexType = GL_UNSIGNED_BYTE; break;
|
|
case 2: indexType = GL_UNSIGNED_SHORT; break;
|
|
case 4: indexType = GL_UNSIGNED_INT; break;
|
|
default:
|
|
// IndexSize is "0 = arrays, else 1 / 2 / 4" (MGPipeTypes.h) and nothing else is a
|
|
// legal width; defaulting to 4 would read past the element buffer.
|
|
MG_Pipe::MGPipeProtocolFatalAt("MGPDrawInfo::IndexSize", info.IndexSize, 4);
|
|
}
|
|
|
|
// ---- the indirect family: the block is the whole description --------------------
|
|
if (indirect != nullptr) {
|
|
// The layout already refused a record that sets both flags or declares ranges
|
|
// beside the block, so NumDraws is 0 and there is no span here.
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// P5c (hd, CONTRACT-P5C §3.5): the command/parameter buffer handles cross as the
|
|
// verb's own state; the backend's indirect arm resolves the buffer twins from
|
|
// them instead of reading the client's GL_DRAW_INDIRECT_BUFFER binding slot.
|
|
auto& applierState = MG_Pipe::MGPipeApplier();
|
|
applierState.ClearVerbHandles();
|
|
applierState.VerbIndirectBuffer = indirect->Buffer;
|
|
applierState.VerbIndirectParameterBuffer = indirect->ParameterBuffer;
|
|
#endif
|
|
const auto offset = reinterpret_cast<const void*>(static_cast<std::uintptr_t>(indirect->Offset));
|
|
const auto drawCount = static_cast<GLsizei>(indirect->DrawCount);
|
|
const auto stride = static_cast<GLsizei>(indirect->Stride);
|
|
const Bool counted = !MG_Pipe::MGPipeHandleIsNull(indirect->ParameterBuffer);
|
|
const auto parameterOffset = static_cast<GLintptr>(indirect->ParameterOffset);
|
|
// glMultiDraw*Indirect with drawcount 1 and stride 0 IS glDraw*Indirect by GL's own
|
|
// definition, so the single-draw entry point is the one the monolith reaches for
|
|
// the single-draw call and nothing is lost for the multi-draw spelling of it.
|
|
const Bool single = !counted && drawCount == 1 && stride == 0;
|
|
if (info.IndexSize == 0) {
|
|
if (counted) {
|
|
if (gl.MultiDrawArraysIndirectCount == nullptr) return false;
|
|
gl.MultiDrawArraysIndirectCount(mode, offset, parameterOffset, drawCount, stride);
|
|
} else if (single) {
|
|
if (gl.DrawArraysIndirect == nullptr) return false;
|
|
gl.DrawArraysIndirect(mode, offset);
|
|
} else {
|
|
if (gl.MultiDrawArraysIndirect == nullptr) return false;
|
|
gl.MultiDrawArraysIndirect(mode, offset, drawCount, stride);
|
|
}
|
|
} else {
|
|
if (counted) {
|
|
if (gl.MultiDrawElementsIndirectCount == nullptr) return false;
|
|
gl.MultiDrawElementsIndirectCount(mode, indexType, offset, parameterOffset,
|
|
drawCount, stride);
|
|
} else if (single) {
|
|
if (gl.DrawElementsIndirect == nullptr) return false;
|
|
gl.DrawElementsIndirect(mode, indexType, offset);
|
|
} else {
|
|
if (gl.MultiDrawElementsIndirect == nullptr) return false;
|
|
gl.MultiDrawElementsIndirect(mode, indexType, offset, drawCount, stride);
|
|
}
|
|
}
|
|
++m_draws;
|
|
return true;
|
|
}
|
|
|
|
if (ranges == nullptr || info.NumDraws == 0) return false;
|
|
const Bool instanced = info.InstanceCount != 1 || info.StartInstance != 0;
|
|
const auto instanceCount = static_cast<GLsizei>(info.InstanceCount);
|
|
const GLuint baseInstance = info.StartInstance;
|
|
|
|
// ---- the multi-draws: the two arrays rebuilt from the ranges (rule C) --------------
|
|
if (info.NumDraws != 1) {
|
|
if (userIndices != nullptr) {
|
|
return MultiDrawShapeLatch(info.IndexSize == 0 ? "MultiDrawArrays+CLIENT_INDICES"
|
|
: "MultiDrawElements+CLIENT_INDICES");
|
|
}
|
|
if (instanced) {
|
|
return MultiDrawShapeLatch(info.IndexSize == 0 ? "MultiDrawArrays+INSTANCED"
|
|
: "MultiDrawElements+INSTANCED");
|
|
}
|
|
const auto n = static_cast<SizeT>(info.NumDraws);
|
|
m_multiCounts.resize(n);
|
|
if (info.IndexSize == 0) {
|
|
if (gl.MultiDrawArrays == nullptr) return false;
|
|
m_multiFirsts.resize(n);
|
|
for (SizeT i = 0; i < n; ++i) {
|
|
m_multiFirsts[i] = static_cast<GLint>(ranges[i].Start);
|
|
m_multiCounts[i] = static_cast<GLsizei>(ranges[i].Count);
|
|
}
|
|
gl.MultiDrawArrays(mode, m_multiFirsts.data(), m_multiCounts.data(),
|
|
static_cast<GLsizei>(n));
|
|
} else {
|
|
if (gl.MultiDrawElementsBaseVertex == nullptr) return false;
|
|
m_multiOffsets.resize(n);
|
|
m_multiBaseVertices.resize(n);
|
|
for (SizeT i = 0; i < n; ++i) {
|
|
m_multiOffsets[i] = reinterpret_cast<const void*>(
|
|
static_cast<std::uintptr_t>(ranges[i].Start) * info.IndexSize);
|
|
m_multiCounts[i] = static_cast<GLsizei>(ranges[i].Count);
|
|
m_multiBaseVertices[i] = ranges[i].IndexBias;
|
|
}
|
|
// MultiDrawElements is the same call with every base vertex 0, which is what
|
|
// its ranges carry (CONTRACT-P5B.md d1's MultiDrawElements row).
|
|
gl.MultiDrawElementsBaseVertex(mode, m_multiCounts.data(), indexType,
|
|
m_multiOffsets.data(), static_cast<GLsizei>(n),
|
|
m_multiBaseVertices.data());
|
|
}
|
|
++m_draws;
|
|
return true;
|
|
}
|
|
|
|
// ---- one range ----------------------------------------------------------------------
|
|
const MG_Pipe::MGPDrawRange& range = ranges[0];
|
|
const auto count = static_cast<GLsizei>(range.Count);
|
|
if (info.IndexSize == 0) {
|
|
if (!instanced) {
|
|
if (gl.DrawArrays == nullptr) return false;
|
|
gl.DrawArrays(mode, static_cast<GLint>(range.Start), count);
|
|
} else if (baseInstance != 0) {
|
|
if (gl.DrawArraysInstancedBaseInstance == nullptr) return false;
|
|
gl.DrawArraysInstancedBaseInstance(mode, static_cast<GLint>(range.Start), count,
|
|
instanceCount, baseInstance);
|
|
} else {
|
|
if (gl.DrawArraysInstanced == nullptr) return false;
|
|
gl.DrawArraysInstanced(mode, static_cast<GLint>(range.Start), count, instanceCount);
|
|
}
|
|
++m_draws;
|
|
return true;
|
|
}
|
|
|
|
// Indexed. `indices` is the element-buffer byte offset Start * IndexSize - the same
|
|
// arithmetic PipeFill's emitter inverted - or, for a client index array, the staged run
|
|
// resolved through the process resolver the server installed (rule C: the pointer is
|
|
// used for this call only). The codec proved the span lies inside SEG_STAGE (all four
|
|
// honesty arms), so a null here is the resolver missing, which is a wiring fault.
|
|
const void* indices = nullptr;
|
|
if (userIndices != nullptr) {
|
|
indices = MG_Pipe::MGPipeHostBytes(*userIndices);
|
|
if (indices == nullptr) {
|
|
MG_Pipe::MGPipeProtocolFatal("DrawVbo.userIndices",
|
|
"the user-index span passed the codec's four honesty arms "
|
|
"but MGPipeHostBytes resolved it to null; the server's "
|
|
"segment resolver is not installed");
|
|
}
|
|
} else {
|
|
indices = reinterpret_cast<const void*>(static_cast<std::uintptr_t>(range.Start) *
|
|
info.IndexSize);
|
|
}
|
|
const GLint baseVertex = range.IndexBias;
|
|
const Bool ranged = (info.Flags & MG_Pipe::kDrawHasIndexRange) != 0;
|
|
// A BASE VERTEX OF 0 IS THE PLAIN ENTRY POINT, NOT THE BaseVertex ONE WITH A 0 - d1's one
|
|
// ruling against CONTRACT-P5B.md's "DrawElementsBaseVertex(..., 0), the P5 arm,
|
|
// unchanged". Espryt's DrawElementsBaseVertex slot calls glDrawElementsBaseVertex, which
|
|
// is ES 3.2 / OES_draw_elements_base_vertex and is NOT loaded on an ES 3.1 provider
|
|
// (ANGLE on D3D11: "Failed to load GLES function: glDrawElementsBaseVertex" at bring-up),
|
|
// so the P5 arm dereferenced a null function pointer on every plain glDrawElements - the
|
|
// one call every Minecraft frame is made of - while the monolith, which calls
|
|
// GL.DrawElements for glDrawElements, was fine. Rule D says the sink reproduces the call
|
|
// the monolith makes; this is that, for all three indexed families.
|
|
if (!instanced) {
|
|
if (ranged) {
|
|
if (baseVertex != 0) {
|
|
if (gl.DrawRangeElementsBaseVertex == nullptr) return false;
|
|
gl.DrawRangeElementsBaseVertex(mode, info.MinIndex, info.MaxIndex, count, indexType,
|
|
indices, baseVertex);
|
|
} else {
|
|
if (gl.DrawRangeElements == nullptr) return false;
|
|
gl.DrawRangeElements(mode, info.MinIndex, info.MaxIndex, count, indexType, indices);
|
|
}
|
|
} else if (baseVertex != 0) {
|
|
if (gl.DrawElementsBaseVertex == nullptr) return false;
|
|
gl.DrawElementsBaseVertex(mode, count, indexType, indices, baseVertex);
|
|
} else {
|
|
if (gl.DrawElements == nullptr) return false;
|
|
gl.DrawElements(mode, count, indexType, indices);
|
|
}
|
|
} else if (baseInstance != 0) {
|
|
if (baseVertex != 0) {
|
|
if (gl.DrawElementsInstancedBaseVertexBaseInstance == nullptr) return false;
|
|
gl.DrawElementsInstancedBaseVertexBaseInstance(mode, count, indexType, indices,
|
|
instanceCount, baseVertex, baseInstance);
|
|
} else {
|
|
if (gl.DrawElementsInstancedBaseInstance == nullptr) return false;
|
|
gl.DrawElementsInstancedBaseInstance(mode, count, indexType, indices, instanceCount,
|
|
baseInstance);
|
|
}
|
|
} else if (baseVertex != 0) {
|
|
if (gl.DrawElementsInstancedBaseVertex == nullptr) return false;
|
|
gl.DrawElementsInstancedBaseVertex(mode, count, indexType, indices, instanceCount, baseVertex);
|
|
} else {
|
|
if (gl.DrawElementsInstanced == nullptr) return false;
|
|
gl.DrawElementsInstanced(mode, count, indexType, indices, instanceCount);
|
|
}
|
|
++m_draws;
|
|
return true;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------
|
|
// P5b: the stubs the four migration packages replace (MG_Remote/CONTRACT-P5B.md).
|
|
//
|
|
// Each dies by the GL slot's own name. The record has crossed and been validated by the
|
|
// codec by the time one of these runs, so the only thing missing is the backend call, and
|
|
// the package that owns the row writes it here: `Table("<row>")`, the null-slot check
|
|
// (a backend that leaves the slot null DECLINES, which is the monolith's null-slot answer
|
|
// in the same words), the call, and a tally the lane can assert moved.
|
|
// -----------------------------------------------------------------------------------
|
|
|
|
// ---- i1 ---- (MG_Remote/CONTRACT-P5B.md §2 i1; landed by package p5b/i1)
|
|
//
|
|
// RULE D IN FIVE BODIES. Each reproduces the backend call the monolith makes, from the
|
|
// record and from server state, and NOTHING ELSE: the backend goes on reading the frontend
|
|
// fields it reads today through the BARRIER-PULLED entries of its verb class, which the
|
|
// verb stamp PipeApplier::ApplyOne put up before this sink ran is exactly what makes legal.
|
|
// That is why none of these touches a backend file and why the monolith path is byte
|
|
// identical - and it is also the honest statement of the debt, which `rsp` counts.
|
|
|
|
Bool RecordVerbSink::OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// Ruling 19 / ID-95: a dispatch samples through the same unit windows a draw does.
|
|
if (!CheckUnitWindows(MG_Pipe::MGPipeApplier(), "launch_grid")) return false;
|
|
#endif
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("launch_grid");
|
|
if (table == nullptr) return false;
|
|
const MG_Backend::GLFunctionsTable& gl = table->GL;
|
|
// The compute program is NOT named by THIS record and must not be - but it IS named, by
|
|
// the set_dispatch_program record that preceded it, and the backend's PrepareForCompute
|
|
// reads MGPipeApplier().DispatchProgram from there.
|
|
//
|
|
// P5e (pa): that is what this comment used to get wrong. Until P5e the backend PULLED
|
|
// GetProgramForDispatch - GetProgramForDraw's twin - inside PrepareForCompute, and the
|
|
// field's FATAL -> BARRIER_PULLED move in FieldOwnership.def (contract §6.9, i1's one
|
|
// granted row) is what made the pull legal. pg gave the sync and the link/SPIR-V gate a
|
|
// handle arm and pa retired the pull itself, so on the handle arm the row is not read at
|
|
// all; the BARRIER_PULLED class stays because the monolith arm still reads it (ID-81).
|
|
// Block* are 0 on the wire for the unchanged reason: the local size is a link artifact
|
|
// the backend reads from the program it resolved, which on that arm is the record's
|
|
// archive.
|
|
if (grid.IsIndirect != 0) {
|
|
if (gl.DispatchComputeIndirect == nullptr) return false;
|
|
// IndirectBuffer travels for P7's sake; the BINDING is server state, put there by
|
|
// the set_buffer_bindings record that preceded this one, exactly as OnClear's Fbo
|
|
// is not re-resolved here. glDispatchComputeIndirect takes only the offset.
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// P5c (hd, CONTRACT-P5C §3.5): the buffer handle itself is now also the verb's own
|
|
// state, so the backend's dispatch-indirect arm resolves the twin from the record
|
|
// rather than from the client's GL_DISPATCH_INDIRECT_BUFFER binding slot.
|
|
auto& applierState = MG_Pipe::MGPipeApplier();
|
|
applierState.ClearVerbHandles();
|
|
applierState.VerbDispatchIndirectBuffer = grid.IndirectBuffer;
|
|
#endif
|
|
gl.DispatchComputeIndirect(static_cast<GLintptr>(grid.IndirectOffset));
|
|
} else {
|
|
if (gl.DispatchCompute == nullptr) return false;
|
|
gl.DispatchCompute(static_cast<GLuint>(grid.GridX), static_cast<GLuint>(grid.GridY),
|
|
static_cast<GLuint>(grid.GridZ));
|
|
}
|
|
++m_dispatches;
|
|
return true;
|
|
}
|
|
|
|
Bool RecordVerbSink::OnMemoryBarrier(const MG_Pipe::MGPMemoryBarrier& barrier) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("memory_barrier");
|
|
if (table == nullptr) return false;
|
|
const MG_Backend::GLFunctionsTable& gl = table->GL;
|
|
// THE BITS GO OVER VERBATIM AND ARE LOWERED HERE BY NOBODY. Espryt's atomic-counter
|
|
// lowering - the counter bit implying the storage bit, because glslang lowers every
|
|
// atomic_uint onto a storage block - lives inside its own MemoryBarrier
|
|
// (DirectGLES.cpp:8837) and is a statement about the DRIVER. Repeating it on this side
|
|
// would make the split arm and the monolith arm two different calls.
|
|
if (barrier.ByRegion != 0) {
|
|
if (gl.MemoryBarrierByRegion == nullptr) return false;
|
|
gl.MemoryBarrierByRegion(static_cast<GLbitfield>(barrier.Bits));
|
|
} else {
|
|
if (gl.MemoryBarrier == nullptr) return false;
|
|
gl.MemoryBarrier(static_cast<GLbitfield>(barrier.Bits));
|
|
}
|
|
++m_memoryBarriers;
|
|
return true;
|
|
}
|
|
|
|
Bool RecordVerbSink::OnResourceCopyRegion(const MG_Pipe::MGPCopyRegion& copy) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("resource_copy_region");
|
|
if (table == nullptr) return false;
|
|
if (table->GL.CopyImageSubData == nullptr) return false;
|
|
|
|
// Typed handles resolve both texture and renderbuffer storage on the server.
|
|
MG_Backend::CopyImageEndpoint src{};
|
|
MG_Backend::CopyImageEndpoint dst{};
|
|
if (copy.SrcTarget == GL_RENDERBUFFER) src.RenderbufferHandle = copy.Src;
|
|
else src.TextureHandle = copy.Src;
|
|
if (copy.DstTarget == GL_RENDERBUFFER) dst.RenderbufferHandle = copy.Dst;
|
|
else dst.TextureHandle = copy.Dst;
|
|
if (MG_Pipe::MGPipeHandleIsNull(copy.Src) || MG_Pipe::MGPipeHandleIsNull(copy.Dst)) {
|
|
// The monolith's own answer to this, in its own words (DirectGLES.cpp:9067
|
|
// "source or destination image failed to sync; declining the copy"): the frontend
|
|
// validator is what keeps it unreachable and what reports the INVALID_VALUE the
|
|
// application is owed. A decline here is a real answer, not a silent success.
|
|
MGLOG_E_ONCE("MG_Remote server: resource_copy_region named texture(s) %u -> %u that "
|
|
"the frontend no longer holds; declining the copy",
|
|
static_cast<unsigned>(copy.SrcGlName),
|
|
static_cast<unsigned>(copy.DstGlName));
|
|
return false;
|
|
}
|
|
|
|
// SrcBox is {origin, extent} and the extent is the copy's, spelled once by GL for both
|
|
// endpoints; the destination contributes only its origin.
|
|
table->GL.CopyImageSubData(src, static_cast<GLenum>(copy.SrcTarget),
|
|
static_cast<GLint>(copy.SrcLevel), copy.SrcBox.X, copy.SrcBox.Y,
|
|
copy.SrcBox.Z, dst, static_cast<GLenum>(copy.DstTarget),
|
|
static_cast<GLint>(copy.DstLevel), copy.DstX, copy.DstY,
|
|
copy.DstZ, static_cast<GLsizei>(copy.SrcBox.W),
|
|
static_cast<GLsizei>(copy.SrcBox.H),
|
|
static_cast<GLsizei>(copy.SrcBox.D));
|
|
++m_imageCopies;
|
|
return true;
|
|
}
|
|
|
|
Bool RecordVerbSink::OnBindShaderImage(const MG_Pipe::MGPImageBind& bind) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("bind_shader_image");
|
|
if (table == nullptr) return false;
|
|
if (table->GL.BindImageTexture == nullptr) return false;
|
|
// THE SAME CALL IS RIGHT FOR BOTH BACKENDS, which is why the record carries the whole
|
|
// argument list although neither reads all of it today: Espryt ignores everything but
|
|
// Unit and syncs that unit from the barrier-pulled GetImageTextureBinding
|
|
// (DirectGLES.cpp:9154, :2471), and Magma's slot is a no-op (DirectVulkan.cpp:665). The
|
|
// arguments travel because rule D says a verb crosses as the CALL, and because P7 is
|
|
// what makes the backend read them instead of pulling.
|
|
table->GL.BindImageTexture(static_cast<GLuint>(bind.Unit), static_cast<GLuint>(bind.GlName),
|
|
static_cast<GLint>(bind.Level),
|
|
bind.Layered != 0 ? GL_TRUE : GL_FALSE,
|
|
static_cast<GLint>(bind.Layer),
|
|
static_cast<GLenum>(bind.Access),
|
|
static_cast<GLenum>(bind.Format));
|
|
++m_imageBinds;
|
|
// P5c (rv): an image bind moves the frontend's texture bind generation, and this verb
|
|
// carries no set_shader_images alongside it - so the server-side shutter serial the
|
|
// accessors now answer with (CONTRACT-P5C.md §5.3) moves here, at the one place the
|
|
// event reaches the applier's side.
|
|
MG_Pipe::MGPipeApplierNoteTextureStateMoved();
|
|
return true;
|
|
}
|
|
|
|
Bool RecordVerbSink::OnSetStorageBlockBinding(const MG_Pipe::MGPStorageBlockBinding& binding,
|
|
const char* name) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("set_storage_block_binding");
|
|
if (table == nullptr) return false;
|
|
if (table->GL.ShaderStorageBlockBinding == nullptr) return false;
|
|
if (name == nullptr) return false;
|
|
// The NAME is the one coordinate the application, the frontend and both backends agree
|
|
// on (BackendObject.h:216-221), which is why the row carries a blob rather than the
|
|
// application's block INDEX. `name` points into the decoder's bounded local and is
|
|
// valid for this call only (rule C); the backend slot copies what it needs.
|
|
//
|
|
// P5f fe: publish the record's program handle for the backend consumer. Espryt
|
|
// resolves its server twin directly; Magma's consumer is the fm package's seam.
|
|
MG_Pipe::MGPipeApplier().ClearVerbHandles();
|
|
MG_Pipe::MGPipeApplier().VerbStorageBlockProgram = binding.ShaderCso;
|
|
table->GL.ShaderStorageBlockBinding(static_cast<GLuint>(binding.GlName), name,
|
|
static_cast<GLuint>(binding.Binding));
|
|
++m_storageBlockBindings;
|
|
return true;
|
|
}
|
|
|
|
// ---- t2 ---- (MG_Remote/CONTRACT-P5B.md §2 t2)
|
|
//
|
|
// SIX BODIES, SIX BACKEND CALLS, NO STATE OF THEIR OWN. Rule D: the record IS the call, and
|
|
// everything the backend reads around it - the capture program, the capture-buffer
|
|
// bindings, the bound XFB object, the patch state - it reads from gPipeInputs through its
|
|
// verb class's BARRIER-PULLED fields, which the client filled at the call site and the
|
|
// verb barrier holds still (R-1). That is why none of these touches m_backend beyond
|
|
// Table() and why not one of them caches anything across records.
|
|
//
|
|
// A NULL SLOT DECLINES, AND THE DECLINE IS THE MONOLITH'S ANSWER IN THE SAME WORDS. Magma
|
|
// (DirectVulkan) registers NO XFB slot and no PatchParameteri at all
|
|
// (BackendObject_DirectVulkan.cpp), and under monolith the frontend's own
|
|
// `if (const auto f = table.GL.X)` guard simply skips the call; `return false` here is that
|
|
// same skip, reported to DecodeAndApply as "this build did not apply it" rather than as a
|
|
// crash or as a silent success. Contract §2 t2 says so for PatchParameteri by name.
|
|
|
|
Bool RecordVerbSink::OnBeginStreamOutput(const MG_Pipe::MGPStreamOutputBegin& begin) {
|
|
auto& state = MG_Pipe::MGPipeApplier();
|
|
state.BoundStreamOutputLifetimeId = begin.LifetimeId;
|
|
state.StreamOutputSpans[begin.LifetimeId] = begin;
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("begin_stream_output");
|
|
if (table == nullptr) return false;
|
|
if (table->GL.BeginTransformFeedback == nullptr) return false;
|
|
// Espryt's Begin only ARMS the span (DirectGLES.cpp:1212-1220: primitiveMode, pending,
|
|
// targets cleared); the driver glBeginTransformFeedback happens in the tail of the next
|
|
// PrepareForDraw (StartPendingTransformFeedback, :1224), where the capture program and
|
|
// the buffer bindings are read through the pulls. So this record's effect is not
|
|
// visible until a DRAW crosses - which is why an XFB scenario whose draw is still class
|
|
// C moves its first blocker to that draw rather than rendering.
|
|
table->GL.BeginTransformFeedback(static_cast<GLenum>(begin.PrimitiveMode));
|
|
++m_streamOutputSpans;
|
|
return true;
|
|
}
|
|
|
|
Bool RecordVerbSink::OnEndStreamOutput(const MG_Pipe::MGPXfbAccounting& accounting) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("end_stream_output");
|
|
if (table == nullptr) return false;
|
|
if (table->GL.EndTransformFeedback == nullptr) {
|
|
auto& state = MG_Pipe::MGPipeApplier();
|
|
state.StreamOutputSpans.erase(state.BoundStreamOutputLifetimeId);
|
|
return false;
|
|
}
|
|
// THE THREE ACCOUNTING FIELDS ARE NOT READ, AND THAT IS THE RULING RATHER THAN AN
|
|
// OMISSION. glEndTransformFeedback takes no arguments; the numbers are the CLIENT's own
|
|
// per-span accounting (contract §2 t2's companions row) and the client is where they are
|
|
// consumed - by the primitive queries and by the capture-capacity clamp. A server that
|
|
// second-guessed them from its own driver would be publishing a second answer to a
|
|
// question the frontend already answers, and the second answer is the one that goes
|
|
// stale. They cross because the row has carried them since P4a and because P9's
|
|
// server-side scatter is what will need them.
|
|
(void)accounting;
|
|
table->GL.EndTransformFeedback();
|
|
auto& state = MG_Pipe::MGPipeApplier();
|
|
state.StreamOutputSpans.erase(state.BoundStreamOutputLifetimeId);
|
|
++m_streamOutputSpans;
|
|
return true;
|
|
}
|
|
|
|
Bool RecordVerbSink::OnPauseStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("pause_stream_output");
|
|
if (table == nullptr) return false;
|
|
if (table->GL.PauseTransformFeedback == nullptr) return false;
|
|
(void)control; // Reserved, and the contract says it is 0.
|
|
table->GL.PauseTransformFeedback();
|
|
++m_streamOutputControls;
|
|
return true;
|
|
}
|
|
|
|
Bool RecordVerbSink::OnResumeStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("resume_stream_output");
|
|
if (table == nullptr) return false;
|
|
if (table->GL.ResumeTransformFeedback == nullptr) return false;
|
|
(void)control;
|
|
table->GL.ResumeTransformFeedback();
|
|
++m_streamOutputControls;
|
|
return true;
|
|
}
|
|
|
|
Bool RecordVerbSink::OnBindStreamOutput(const MG_Pipe::MGPStreamOutputBind& bind) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("bind_stream_output");
|
|
if (table == nullptr) return false;
|
|
// THE GL NAME IS THE ARGUMENT, NOT THE LifetimeId BESIDE IT. Espryt keys its driver
|
|
// objects by the GL name (XfbImpl::g_xfbObjects[name], DirectGLES.cpp:1401) and creates
|
|
// the ES object on first bind; passing the lifetime id would index a map that has never
|
|
// heard of it and silently create a second driver object per bind. The lifetime id
|
|
// identifies the P5f capture snapshot on this side. Keeping it separate preserves
|
|
// the backend GL-name argument while Begin/End find the right server-owned span.
|
|
MG_Pipe::MGPipeApplier().BoundStreamOutputLifetimeId = bind.LifetimeId;
|
|
if (table->GL.BindTransformFeedback) table->GL.BindTransformFeedback(static_cast<GLuint>(bind.GlName));
|
|
++m_streamOutputBinds;
|
|
return true;
|
|
}
|
|
|
|
Bool RecordVerbSink::OnDeleteStreamOutput(const MG_Pipe::MGPStreamOutputBind& object) {
|
|
const auto* table = Table("DeleteTransformFeedback");
|
|
if (!table) return false;
|
|
if (!object.GlName || object.Pad0)
|
|
MG_Pipe::MGPipeProtocolFatal("DeleteStreamOutput.name", "default object or reserved field");
|
|
auto& state = MG_Pipe::MGPipeApplier();
|
|
if (object.LifetimeId) {
|
|
state.StreamOutputSpans.erase(object.LifetimeId);
|
|
if (state.BoundStreamOutputLifetimeId == object.LifetimeId) state.BoundStreamOutputLifetimeId = 0;
|
|
}
|
|
state.VerbDeleteStreamOutputLifetimeId = object.LifetimeId;
|
|
if (table->GL.DeleteTransformFeedback) table->GL.DeleteTransformFeedback(object.GlName);
|
|
state.VerbDeleteStreamOutputLifetimeId = 0;
|
|
return true;
|
|
}
|
|
|
|
Bool RecordVerbSink::OnPatchParameter(const MG_Pipe::MGPPatchParameter& patch) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("patch_parameter");
|
|
if (table == nullptr) return false;
|
|
// Magma registers no PatchParameteri: it compiles the patch size into its synthesized
|
|
// control stage from set_patch_state instead, so the DECLINE below is the whole of the
|
|
// right answer for that backend and not a gap (contract §2 t2).
|
|
if (table->GL.PatchParameteri == nullptr) return false;
|
|
// Pname is GL_PATCH_VERTICES and the frontend has already rejected every other spelling
|
|
// with INVALID_ENUM before the record was built, so this is a forward and not a switch.
|
|
table->GL.PatchParameteri(static_cast<GLenum>(patch.Pname), static_cast<GLint>(patch.Value));
|
|
++m_patchParameters;
|
|
return true;
|
|
}
|
|
|
|
// ---- f1 ----
|
|
Bool RecordVerbSink::OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) {
|
|
const auto* table = Table("GenerateMipmap");
|
|
if (table == nullptr || table->GL.GenerateMipmap == nullptr) return false;
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// P5c (hd, CONTRACT-P5C §3.2): the texture the client resolved at Target on the active
|
|
// unit crosses as the verb's own state; the backend's mip-descriptor check resolves
|
|
// the record from it instead of probing the client allocator for the bound object's
|
|
// lifetime id (T2's mip half).
|
|
auto& applierState = MG_Pipe::MGPipeApplier();
|
|
applierState.ClearVerbHandles();
|
|
applierState.VerbMipRes = plan.Res;
|
|
applierState.VerbMipBaseLevel = plan.BaseLevel;
|
|
applierState.VerbMipLevelCount = plan.LevelCount;
|
|
#endif
|
|
table->GL.GenerateMipmap(plan.Target);
|
|
return true;
|
|
}
|
|
|
|
Bool RecordVerbSink::OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) {
|
|
const auto* table = Table(copy.SubImage ? "CopyTexSubImage2D" : "CopyTexImage2D");
|
|
if (table == nullptr) return false;
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// P5c (hd, CONTRACT-P5C §3.4): the destination texture the client resolved at the
|
|
// active unit crosses as the verb's own state; the backend resolves its twin from the
|
|
// handle instead of reading the client's texture-unit binding slot (T4).
|
|
auto& applierState = MG_Pipe::MGPipeApplier();
|
|
applierState.ClearVerbHandles();
|
|
applierState.VerbCopyTexDst = copy.Dst;
|
|
#endif
|
|
if (copy.SubImage) {
|
|
if (table->GL.CopyTexSubImage2D == nullptr) return false;
|
|
table->GL.CopyTexSubImage2D(copy.Target, copy.Level, copy.XOffset, copy.YOffset,
|
|
copy.X, copy.Y, copy.Width, copy.Height);
|
|
} else {
|
|
if (table->GL.CopyTexImage2D == nullptr) return false;
|
|
table->GL.CopyTexImage2D(copy.Target, copy.Level, copy.InternalFormat,
|
|
copy.X, copy.Y, copy.Width, copy.Height, 0);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace MobileGL::MG_Record
|