mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
[Feat] (Pipe): apply the shader CSO's artefacts and the default uniform block without ever re-linking on the server
This commit is contained in:
@@ -16,6 +16,16 @@
|
||||
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// THE ONE PLACE THE PROGRAM ARCHIVE'S CODEC IS CALLED, and it is compiled into the VERIFY
|
||||
// build only. In monolith the archive does not travel - MGPProgramDesc's seven blob refs are
|
||||
// declared with Size 0 and the two structs ride beside the record through the entry point's
|
||||
// companion pointers - so a push build pays nothing for it and the header keeps its forward
|
||||
// declarations. The verify build serialises, deserialises and compares before storing, which
|
||||
// makes the codec LIVE CODE WITH A GATE rather than dead code with a unit test.
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
@@ -909,6 +919,91 @@ namespace MobileGL::MG_Pipe {
|
||||
destinationCount = count;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// P4a: the ShaderCso band split, mirrored from the allocator's.
|
||||
//
|
||||
// The composite band starts at 983040, so ONE program-pipeline composite in a
|
||||
// slot-indexed vector would grow that vector to ~983k records of ~240 bytes each - a
|
||||
// 236 MB spike on the first pipeline draw, which would be a defect this commit
|
||||
// introduced rather than one it inherited. Both spaces are kept dense against their OWN
|
||||
// high-water mark instead, exactly as MGPipeSlotAllocator keeps the band in a table of
|
||||
// its own.
|
||||
//
|
||||
// THE SERVER STILL NEVER LEARNS A HANDLE IS A COMPOSITE. The split is an indexing detail
|
||||
// on this side of the wire: create / bind / delete_shader_state and set_global_constants
|
||||
// name a composite exactly as they name any other program, and none of them branches on
|
||||
// it outside these two functions.
|
||||
// ----------------------------------------------------------------------------
|
||||
MGPipeShaderCsoRecord* ShaderCsoRecordAt(Uint32 slot) {
|
||||
if (MGPipeIsCompositeShaderSlot(slot)) {
|
||||
return RecordAt(g_applier.CompositeShaderCsos, slot - kMGPipeShaderCsoCompositeSlotBase,
|
||||
kMGPipeShaderCsoSlotLimit - kMGPipeShaderCsoCompositeSlotBase);
|
||||
}
|
||||
// The ordinary table is bounded by the BAND'S BASE and not by the slot limit: an
|
||||
// ordinary program can never be handed a band slot (the allocator refuses it), so a
|
||||
// slot at or above the base that is not a composite is out of range by definition.
|
||||
return RecordAt(g_applier.ShaderCsos, slot, kMGPipeShaderCsoCompositeSlotBase);
|
||||
}
|
||||
|
||||
MGPipeShaderCsoRecord* FindShaderCso(MGPipeHandle cso) {
|
||||
if (MGPipeIsCompositeShaderSlot(cso.Slot)) {
|
||||
return FindIn(g_applier.CompositeShaderCsos,
|
||||
MGPipeHandle{cso.Slot - kMGPipeShaderCsoCompositeSlotBase, cso.Gen});
|
||||
}
|
||||
return FindIn(g_applier.ShaderCsos, cso);
|
||||
}
|
||||
|
||||
MGPipeShaderCsoRecord* ResolveShaderCso(const char* call, MGPipeHandle cso) {
|
||||
MGPipeShaderCsoRecord* record = FindShaderCso(cso);
|
||||
MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, cso.Slot, cso.Gen,
|
||||
kResourceRefusalNote);
|
||||
if (record == nullptr) ++g_applier.RefusedObjectCalls;
|
||||
return record;
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// THE ARCHIVE'S ROUND TRIP, and it is a VERIFY-ONLY gate on a path that does not
|
||||
// serialise anything in a shipped build. What it asks is the question a transport will
|
||||
// ask on the first day it exists and nobody can ask afterwards: does the archive this
|
||||
// client is publishing survive being turned into bytes and back? A field the codec
|
||||
// skips in BOTH directions round-trips perfectly, which is why the unit suite reads the
|
||||
// members back explicitly; what THIS adds is that it runs over every real program the
|
||||
// verify lane links rather than over one hand-built instance.
|
||||
void PinProgramArchiveRoundTrip(const MGPProgramDesc& desc,
|
||||
const MG_State::GLState::LinkArtifacts& link,
|
||||
const MG_State::GLState::SpirvArtifacts& spirv) {
|
||||
Vector<Uint8> encoded;
|
||||
MG_State::GLState::EncodeProgramArtifacts(link, spirv, encoded);
|
||||
MG_State::GLState::LinkArtifacts decodedLink;
|
||||
MG_State::GLState::SpirvArtifacts decodedSpirv;
|
||||
Vector<Uint8> reencoded;
|
||||
const char* fault = nullptr;
|
||||
if (encoded.empty()) {
|
||||
fault = "the encoder produced no bytes for a program that has artefacts";
|
||||
} else if (!MG_State::GLState::DecodeProgramArtifacts(encoded.data(), encoded.size(),
|
||||
decodedLink, decodedSpirv)) {
|
||||
fault = "the archive this client would put on the wire does not decode";
|
||||
} else {
|
||||
MG_State::GLState::EncodeProgramArtifacts(decodedLink, decodedSpirv, reencoded);
|
||||
if (reencoded != encoded) {
|
||||
fault = "the archive does not survive its own round trip";
|
||||
} else if (decodedLink.program != nullptr) {
|
||||
// The live glslang TProgram is the one member the tables deliberately omit;
|
||||
// a decode that reconstructed one would be carrying the compiler front end
|
||||
// across a boundary that exists to keep it on this side.
|
||||
fault = "the decoded archive carries a live program object";
|
||||
}
|
||||
}
|
||||
if (fault == nullptr) return;
|
||||
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("PipeVerifyDiffer")
|
||||
" program-archive create_shader_state {slot=%u, gen=%u}: %s (%llu bytes "
|
||||
"encoded, %llu re-encoded)",
|
||||
desc.Cso.Slot, desc.Cso.Gen, fault,
|
||||
static_cast<unsigned long long>(encoded.size()),
|
||||
static_cast<unsigned long long>(reencoded.size()));
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
MGPipeApplierState& MGPipeApplier() { return g_applier; }
|
||||
@@ -2094,25 +2189,220 @@ namespace MobileGL::MG_Pipe {
|
||||
++g_applier.ShaderImagesSerial;
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// w3: the shader CSO, the two program bindings and the default uniform block
|
||||
//
|
||||
// AND THE SERVER NEVER RE-LINKS. glslang lives entirely on the client and SPIRV-Cross
|
||||
// entirely on the server, a file-level cut: what crosses is the per-stage SPIR-V and the
|
||||
// reflection archive, never source, and there is no server-side compile pool to hand a
|
||||
// failure back from. Link failure needs no synchronous return either - it is one error log
|
||||
// plus a bind-program-0 empty draw today, GL_LINK_STATUS is never withdrawn, and the
|
||||
// synchronous query is answered by the client out of its own ProgramObject.
|
||||
//
|
||||
// WHAT THE SERVER STILL SPECIALISES IS NOT THIS RECORD'S BUSINESS. A backend program
|
||||
// depends on eight more inputs than the artefacts - the draw framebuffer's clamp masks and
|
||||
// fragColor broadcast count, the storage-block binding signature, the live image formats,
|
||||
// the patch parameters - and every one of them now arrives through ANOTHER record this
|
||||
// applier holds. create_shader_state publishes the artefacts; the verb specialises.
|
||||
// ================================================================================
|
||||
|
||||
void MGPipeApplyCreateShaderState(const MGPProgramDesc& desc,
|
||||
const MG_State::GLState::LinkArtifacts* link,
|
||||
const MG_State::GLState::SpirvArtifacts* spirv) {
|
||||
(void)desc;
|
||||
(void)link;
|
||||
(void)spirv;
|
||||
MOBILEGL_ASSERT(desc.Cso.Slot >= kMGPipeFirstAllocatableSlot,
|
||||
"create_shader_state named the reserved slot 0");
|
||||
if (desc.Cso.Slot < kMGPipeFirstAllocatableSlot) return;
|
||||
|
||||
// THE ONE BLOB RULE, over seven blob refs at once. All seven are declared with Size 0 in
|
||||
// monolith - "this record does not declare its blob" - and the artefacts ride beside the
|
||||
// record through the two companion pointers, so a record that declares nothing AND
|
||||
// carries nothing describes a program with no reflection and no SPIR-V, which is not a
|
||||
// program. The declared lengths themselves cross-check nothing here: no other field of
|
||||
// MGPProgramDesc describes how long a module or an archive is, so the rule is inert on
|
||||
// this record by construction rather than by omission, and it becomes a real one when
|
||||
// the transport that fills those refs in lands with the splitter that owns them.
|
||||
const char* fault = nullptr;
|
||||
if (link == nullptr || spirv == nullptr) {
|
||||
fault = "the record declares no blobs and carries no artefacts";
|
||||
} else if (desc.GlobalUboSize > kMGPipeMaxGlobalConstantsBytes) {
|
||||
fault = "the default uniform block is larger than any program may declare";
|
||||
}
|
||||
if (fault != nullptr) {
|
||||
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption")
|
||||
" create_shader_state {slot=%u, gen=%u}: %s (stages=0x%x, "
|
||||
"globalUboSize=%u, bound %u)",
|
||||
desc.Cso.Slot, desc.Cso.Gen, fault, desc.StageMask, desc.GlobalUboSize,
|
||||
kMGPipeMaxGlobalConstantsBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
MGPipeShaderCsoRecord* recordAt = ShaderCsoRecordAt(desc.Cso.Slot);
|
||||
if (recordAt == nullptr) {
|
||||
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption")
|
||||
" create_shader_state {slot=%u, gen=%u}: the slot is outside the record "
|
||||
"table's bound (%u)",
|
||||
desc.Cso.Slot, desc.Cso.Gen, kMGPipeMaxShaderCsoSlots);
|
||||
return;
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// BEFORE STORING, so a lane that aborts here aborts on the record that was wrong rather
|
||||
// than on the next one that reads it.
|
||||
PinProgramArchiveRoundTrip(desc, *link, *spirv);
|
||||
#endif
|
||||
|
||||
MGPipeShaderCsoRecord& record = *recordAt;
|
||||
// A RE-ISSUE ON THE SAME HANDLE IS HOW A RELINK TRAVELS: the handle is minted per
|
||||
// frontend program and Gen moves only on slot reuse, so an existing record of the same
|
||||
// identity keeps its serial and counts up from it, and a record of a different identity
|
||||
// is a recycled slot and starts over.
|
||||
if (!record.Live || record.Gen != desc.Cso.Gen) {
|
||||
record = MGPipeShaderCsoRecord{};
|
||||
record.Gen = desc.Cso.Gen;
|
||||
} else {
|
||||
++record.Serial;
|
||||
// A RELINK REPLACES THE DEFAULT UNIFORM BLOCK'S LAYOUT, so the image and the version
|
||||
// keyed to the old one go with it - keeping them would leave a block sized to a
|
||||
// layout that no longer exists, and the sentinel is exactly the value that says
|
||||
// "nothing has been uploaded for this program". The client re-emits against the
|
||||
// layout that now exists; the serial announces the clearing so a twin cannot match
|
||||
// what it uploaded before the relink.
|
||||
record.GlobalConstants.clear();
|
||||
record.GlobalConstantsVersion = ~Uint32{0};
|
||||
++record.GlobalConstantsSerial;
|
||||
}
|
||||
record.Live = true;
|
||||
// The DESCRIPTOR is what is stored, and the artefacts are not: in monolith the backend
|
||||
// reads the frontend's own archive, and under split the descriptor is what the
|
||||
// deserialised archive is attached to. Either way the applier owns identity, extent and
|
||||
// order, and never content.
|
||||
record.Desc = desc;
|
||||
}
|
||||
|
||||
void MGPipeApplyBindShaderState(const MGPHandleOnly& handle) { (void)handle; }
|
||||
void MGPipeApplyBindShaderState(const MGPHandleOnly& handle) {
|
||||
MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::ShaderCso),
|
||||
"bind_shader_state on kind %u", handle.Kind);
|
||||
// The null handle is legal and means "nothing bound", which is a GL state and not an
|
||||
// error; a DEAD handle leaves the previous binding untouched and is counted, which is
|
||||
// bind_render_state's precedent for the same question.
|
||||
if (MGPipeHandleIsNull(handle.Handle)) {
|
||||
g_applier.BoundShaderCso = kMGPipeNullHandle;
|
||||
++g_applier.ProgramBindingSerial;
|
||||
return;
|
||||
}
|
||||
const MGPipeShaderCsoRecord* record = ResolveShaderCso("bind_shader_state", handle.Handle);
|
||||
if (record == nullptr) return;
|
||||
g_applier.BoundShaderCso = handle.Handle;
|
||||
++g_applier.ProgramBindingSerial;
|
||||
}
|
||||
|
||||
void MGPipeApplyDeleteShaderState(const MGPHandleOnly& handle) { (void)handle; }
|
||||
void MGPipeApplyDeleteShaderState(const MGPHandleOnly& handle) {
|
||||
MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::ShaderCso),
|
||||
"delete_shader_state on kind %u", handle.Kind);
|
||||
// A COMPOSITE'S SLOT HAS TWO INDEPENDENT RELEASE PATHS - the pipeline cache's eviction
|
||||
// and the composite program's own destructor - and both arrive here through one client
|
||||
// helper. The second is a refusal: the record is already gone, so it is counted and
|
||||
// dropped, which is the same defined no-op every other death notice gets and is why the
|
||||
// double free is a proven no-op rather than a race.
|
||||
MGPipeShaderCsoRecord* record = ResolveShaderCso("delete_shader_state", handle.Handle);
|
||||
if (record == nullptr) return;
|
||||
|
||||
void MGPipeApplySetDrawProgram(const MGPHandleOnly& handle) { (void)handle; }
|
||||
const Uint32 gen = record->Gen;
|
||||
*record = MGPipeShaderCsoRecord{};
|
||||
record->Gen = gen;
|
||||
|
||||
void MGPipeApplySetDispatchProgram(const MGPHandleOnly& handle) { (void)handle; }
|
||||
// UNLIKE THE UNIT SETS, THE THREE PROGRAM BINDINGS ARE CLEARED. They are single handles
|
||||
// rather than a window of "the last set as received", and each of them is resolved
|
||||
// against the record that has just been dropped - leaving one behind would mean the
|
||||
// next verb resolving a binding to a record this applier no longer has, which is the
|
||||
// refusal counter firing for a state the applier itself created.
|
||||
Bool cleared = false;
|
||||
if (g_applier.BoundShaderCso == handle.Handle) {
|
||||
g_applier.BoundShaderCso = kMGPipeNullHandle;
|
||||
cleared = true;
|
||||
}
|
||||
if (g_applier.DrawProgram == handle.Handle) {
|
||||
g_applier.DrawProgram = kMGPipeNullHandle;
|
||||
cleared = true;
|
||||
}
|
||||
if (g_applier.DispatchProgram == handle.Handle) {
|
||||
g_applier.DispatchProgram = kMGPipeNullHandle;
|
||||
cleared = true;
|
||||
}
|
||||
if (cleared) ++g_applier.ProgramBindingSerial;
|
||||
}
|
||||
|
||||
// TWO CALLS AND NOT ONE, because the frontend has two joins and two PipeInputs slots: a
|
||||
// draw resolves GetProgramForDraw and a dispatch resolves GetProgramForDispatch, and a
|
||||
// pipeline object flattened into a composite is the first of those and never the second.
|
||||
void MGPipeApplySetDrawProgram(const MGPHandleOnly& handle) {
|
||||
MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::ShaderCso),
|
||||
"set_draw_program on kind %u", handle.Kind);
|
||||
if (MGPipeHandleIsNull(handle.Handle)) {
|
||||
g_applier.DrawProgram = kMGPipeNullHandle;
|
||||
++g_applier.ProgramBindingSerial;
|
||||
return;
|
||||
}
|
||||
const MGPipeShaderCsoRecord* record = ResolveShaderCso("set_draw_program", handle.Handle);
|
||||
if (record == nullptr) return;
|
||||
g_applier.DrawProgram = handle.Handle;
|
||||
++g_applier.ProgramBindingSerial;
|
||||
}
|
||||
|
||||
void MGPipeApplySetDispatchProgram(const MGPHandleOnly& handle) {
|
||||
MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::ShaderCso),
|
||||
"set_dispatch_program on kind %u", handle.Kind);
|
||||
if (MGPipeHandleIsNull(handle.Handle)) {
|
||||
g_applier.DispatchProgram = kMGPipeNullHandle;
|
||||
++g_applier.ProgramBindingSerial;
|
||||
return;
|
||||
}
|
||||
const MGPipeShaderCsoRecord* record = ResolveShaderCso("set_dispatch_program", handle.Handle);
|
||||
if (record == nullptr) return;
|
||||
g_applier.DispatchProgram = handle.Handle;
|
||||
++g_applier.ProgramBindingSerial;
|
||||
}
|
||||
|
||||
void MGPipeApplySetGlobalConstants(const MGPGlobalConstants& record, const void* bytes) {
|
||||
(void)record;
|
||||
(void)bytes;
|
||||
// ON THE PROGRAM'S RECORD, not in the working state, and that is what makes it survive a
|
||||
// make-current: the block is (ShaderCso, Version) keyed and belongs to the program, not
|
||||
// to the context that last uploaded it.
|
||||
MGPipeShaderCsoRecord* stored = ResolveShaderCso("set_global_constants", record.ShaderCso);
|
||||
if (stored == nullptr) return;
|
||||
|
||||
// THE LENGTH IS THE PROGRAM'S OWN GlobalUboSize, which arrived on the create and was
|
||||
// bounded there - so this call can only ever allocate what the create already declared,
|
||||
// and the blob rule has a real field to cross-check against for once.
|
||||
const Uint32 size = stored->Desc.GlobalUboSize;
|
||||
const char* fault = nullptr;
|
||||
if (record.Version == ~Uint32{0}) {
|
||||
fault = "the version is the backends' never-uploaded sentinel, which no record may carry";
|
||||
} else if (record.Blob.Size != 0 && record.Blob.Size != size) {
|
||||
fault = "the declared blob length is not the program's own default uniform block size";
|
||||
} else if (size > kMGPipeMaxGlobalConstantsBytes) {
|
||||
fault = "the program's default uniform block is larger than any program may declare";
|
||||
} else if (size != 0 && bytes == nullptr) {
|
||||
fault = "a non-empty block carries no bytes";
|
||||
}
|
||||
if (fault != nullptr) {
|
||||
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption")
|
||||
" set_global_constants {slot=%u, gen=%u}: %s (version=%u, the program "
|
||||
"declares %u bytes, the blob declares %llu)",
|
||||
record.ShaderCso.Slot, record.ShaderCso.Gen, fault, record.Version, size,
|
||||
static_cast<unsigned long long>(record.Blob.Size));
|
||||
return;
|
||||
}
|
||||
|
||||
if (size == 0) {
|
||||
stored->GlobalConstants.clear();
|
||||
} else {
|
||||
const Uint8* image = static_cast<const Uint8*>(bytes);
|
||||
stored->GlobalConstants.assign(image, image + size);
|
||||
}
|
||||
stored->GlobalConstantsVersion = record.Version;
|
||||
// Its OWN serial, beside the record's: a block upload is not a relink, and a twin that
|
||||
// memoises "which block image have I staged" must not be told the program moved.
|
||||
++stored->GlobalConstantsSerial;
|
||||
}
|
||||
|
||||
// THE MONOLITH BODY IS A NO-OP AND THAT IS THE WHOLE OF IT: the emulation this names still
|
||||
|
||||
Reference in New Issue
Block a user