From dca3eb868e7d06516d403a3d51ff1ded30fe8b66 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 8 Sep 2026 12:48:26 -0400 Subject: [PATCH] [Feat] (Pipe): apply sampler states, sampler views and the three unit sets into the server's own working state --- MobileGL/MG_Pipe/PipeApply.cpp | 224 +++++++++++++++++++++++++++++++-- 1 file changed, 213 insertions(+), 11 deletions(-) diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index 15465d47..cf389f95 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -862,6 +862,53 @@ namespace MobileGL::MG_Pipe { // record was accumulated, so the texels are the server's now. ++stored->Serial; } + + // ---------------------------------------------------------------------------- + // P4a: the three kVarTail unit sets share one body. + // + // They differ in exactly one thing - what an entry IS - and in nothing else: the same + // window rule, the same bound, the same "the entries outside the window are not + // cleared", the same serial discipline. set_vertex_buffers wrote this arithmetic once + // already; a second, third and fourth copy of it would be three more places to get the + // Start + Count overflow wrong. + // + // THE WINDOW IS THE BOUND AND ENTRIES OUTSIDE IT ARE NOT CLEARED. The record is "the + // last set as received": a set that names four units has said nothing about the other + // 188, and clearing them would unbind textures the client never mentioned. + // + // THE ENTRY'S OWN Unit FIELD IS NOT POLICED, deliberately and for MGPVertexBuffer:: + // BindingIndex's reason: the DESTINATION is Start + i, which is the window this + // function has already bounded, and the field beside it is the client's own label for + // the entry. Policing it would hand the emitter a contract this applier cannot justify + // - the two agree by construction or the emitter is broken in a way a unit test on the + // emitter's side is the right place to catch. + template + Bool ApplyUnitWindow(const char* call, Uint32 start, Uint32 count, Uint64 contentHash, + const Entry* tail, ArrayT& destination, Uint32& destinationStart, + Uint32& destinationCount) { + const Uint64 capacity = destination.size(); + const Uint64 end = Uint64{start} + Uint64{count}; + const char* fault = nullptr; + if (end > capacity) { + fault = "the window runs past the merged texture-unit space"; + } else if (count != 0 && tail == nullptr) { + fault = "a non-empty set carries no entries"; + } + if (fault != nullptr) { + MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption") + " %s {start=%u, count=%u, hash=%llu}: %s (the applier holds %llu " + "entries)", + call, start, count, static_cast(contentHash), + fault, static_cast(capacity)); + return false; + } + for (Uint32 i = 0; i < count; ++i) { + destination[start + i] = tail[i]; + } + destinationStart = start; + destinationCount = count; + return true; + } } // namespace MGPipeApplierState& MGPipeApplier() { return g_applier; } @@ -1828,16 +1875,144 @@ namespace MobileGL::MG_Pipe { ++g_applier.FramebufferSerial; } + // ================================================================================ + // w2: sampler CSOs, sampler views and the three unit sets + // ================================================================================ + void MGPipeApplyCreateSamplerState(const MGPSamplerDesc& desc, const SamplerParameters* parameters) { - (void)desc; - (void)parameters; + MOBILEGL_ASSERT(desc.Cso.Slot >= kMGPipeFirstAllocatableSlot, + "create_sampler_state named the reserved slot 0"); + if (desc.Cso.Slot < kMGPipeFirstAllocatableSlot) return; + + // THE ONE BLOB RULE, on the family's own blob: a non-zero Parameters.Size must be + // exactly one SamplerParameters, and a zero means "this record does not declare its + // blob" - which is what a monolith emission is, because the value rides beside the + // record through the companion pointer. Either way the bytes read are bounded by the + // TYPE and not by the declared length, so the length is a cross-check and never the + // safety property. + const char* fault = nullptr; + if (desc.Parameters.Size != 0 && desc.Parameters.Size != sizeof(SamplerParameters)) { + fault = "the declared blob length is not one SamplerParameters"; + } else if (parameters == nullptr) { + fault = "the record declares no parameters and carries none"; + } + if (fault != nullptr) { + MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption") + " create_sampler_state {slot=%u, gen=%u}: %s (one SamplerParameters is " + "%llu bytes, the blob declares %llu)", + desc.Cso.Slot, desc.Cso.Gen, fault, + static_cast(sizeof(SamplerParameters)), + static_cast(desc.Parameters.Size)); + return; + } + + MGPipeSamplerCsoRecord* recordAt = + RecordAt(g_applier.SamplerCsos, desc.Cso.Slot, kMGPipeMaxSamplerCsoSlots); + if (recordAt == nullptr) { + MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption") + " create_sampler_state {slot=%u, gen=%u}: the slot is outside the record " + "table's bound (%u)", + desc.Cso.Slot, desc.Cso.Gen, kMGPipeMaxSamplerCsoSlots); + return; + } + MGPipeSamplerCsoRecord& record = *recordAt; + // A CREATE STARTS THE RECORD OVER AND LEAVES Serial AT 0; A RE-ISSUE ON A LIVE IDENTITY + // COUNTS UP. The first half is what stops a recycled slot contributing one field of its + // predecessor, and it is why a fresh backend twin starting its own synced serial at 0 + // agrees with a fresh record without either side publishing anything. The second is how + // a value change travels on a handle whose Gen moves only on slot reuse - and it is a + // mutation, so it moves the serial. + if (!record.Live || record.Gen != desc.Cso.Gen) { + record = MGPipeSamplerCsoRecord{}; + record.Gen = desc.Cso.Gen; + } else { + ++record.Serial; + } + record.Live = true; + // BY VALUE, INCLUDING borderColorForm. All three border representations are always + // numerically populated, so the value alone cannot say which driver entry point to use, + // and the backend's redundancy filter compares all four. The three trailing padding + // bytes are why MOBILEGL_PIPE_VERIFY compares this FIELD BY FIELD through + // PipeFields.def's MGP_FIELDS_SamplerParameters rather than as bytes. + record.Params = *parameters; } - void MGPipeApplyDeleteSamplerState(const MGPHandleOnly& handle) { (void)handle; } + void MGPipeApplyDeleteSamplerState(const MGPHandleOnly& handle) { + MOBILEGL_ASSERT(handle.Kind == static_cast(MGPipeKind::SamplerCso), + "delete_sampler_state on kind %u", handle.Kind); + MGPipeSamplerCsoRecord* record = + ResolveObject(g_applier.SamplerCsos, "delete_sampler_state", handle.Handle); + if (record == nullptr) return; - void MGPipeApplyCreateSamplerView(const MGPSamplerView& view) { (void)view; } + // Dropped whole, generation kept, for resource_destroy's reason: the client allocator + // owns the Gen bump and takes it on the next handout of the slot, so a server-side bump + // here would put the two identities out of step. + // + // AND THE UNIT SET IS NOT SWEPT. bind_sampler_states is "the last set as received" and + // the client re-emits the whole window from its own resolved set at the next verb, so + // walking 192 entries here to blank a handle the next set is about to overwrite would + // buy nothing and would break the one rule the window has. + const Uint32 gen = record->Gen; + *record = MGPipeSamplerCsoRecord{}; + record->Gen = gen; + } - void MGPipeApplyDeleteSamplerView(const MGPHandleOnly& handle) { (void)handle; } + void MGPipeApplyCreateSamplerView(const MGPSamplerView& view) { + MOBILEGL_ASSERT(view.Cso.Slot >= kMGPipeFirstAllocatableSlot, + "create_sampler_view named the reserved slot 0"); + if (view.Cso.Slot < kMGPipeFirstAllocatableSlot) return; + + MGPipeSamplerViewRecord* recordAt = + RecordAt(g_applier.SamplerViewCsos, view.Cso.Slot, kMGPipeMaxSamplerViewSlots); + if (recordAt == nullptr) { + MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption") + " create_sampler_view {slot=%u, gen=%u}: the slot is outside the record " + "table's bound (%u)", + view.Cso.Slot, view.Cso.Gen, kMGPipeMaxSamplerViewSlots); + return; + } + MGPipeSamplerViewRecord& record = *recordAt; + // RE-ISSUING ON THE SAME HANDLE IS HOW A RESTRICTION CHANGE TRAVELS - a view is + // identity-addressed one per texture object, minted off that object's lifetime id, and + // Gen moves only on slot reuse - so it bumps the serial and starts nothing over. AND IT + // DOES NOT REBIND: a re-issue on a view some unit is holding changes what that unit + // sees, which the serial already says; it must not make some other unit see it. + if (!record.Live || record.Gen != view.Cso.Gen) { + record = MGPipeSamplerViewRecord{}; + record.Gen = view.Cso.Gen; + } else { + ++record.Serial; + } + record.Live = true; + record.View = view; + + // THE TEXTURE'S OWN ViewCso IS WRITTEN HERE, and it is a SILENT lookup rather than a + // resolved one: the texture-resource bit and the sampler bit are independent, so a view + // arriving before - or without - the texture record is an ordering fact and not a + // refusal. When the record is there this is the back-pointer that lets a sync reach a + // texture's view without walking every view the applier holds. + MGPipeResourceRecord* texture = FindIn(g_applier.TextureResources, view.Texture); + if (texture != nullptr) texture->ViewCso = view.Cso; + } + + void MGPipeApplyDeleteSamplerView(const MGPHandleOnly& handle) { + MOBILEGL_ASSERT(handle.Kind == static_cast(MGPipeKind::SamplerViewCso), + "delete_sampler_view on kind %u", handle.Kind); + MGPipeSamplerViewRecord* record = + ResolveObject(g_applier.SamplerViewCsos, "delete_sampler_view", handle.Handle); + if (record == nullptr) return; + + // The back-pointer goes first, while the record that names the texture still exists, + // and only if that texture still names THIS view - a texture whose view has already + // been re-minted must not have the new handle cleared out from under it. + MGPipeResourceRecord* texture = FindIn(g_applier.TextureResources, record->View.Texture); + if (texture != nullptr && texture->ViewCso == handle.Handle) { + texture->ViewCso = kMGPipeNullHandle; + } + const Uint32 gen = record->Gen; + *record = MGPipeSamplerViewRecord{}; + record->Gen = gen; + } void MGPipeApplySetTextureParams(const MGPTextureParams& params) { // ADDRESSED BY RESOURCE AND BY NOTHING ELSE, which is the whole point of the call: a @@ -1877,19 +2052,46 @@ namespace MobileGL::MG_Pipe { ++record->ParamsSerial; } + // The three of them, and NO STAGE DIMENSION on any of them: MobileGL's texture-unit space + // is one merged array of 192, the same unit may be sampled from two stages, and stage is + // derived server-side from the reflection archive only where the target API needs it. + // + // A NULL HANDLE IN A TAIL ENTRY IS LEGAL EVERYWHERE HERE and is not a refusal: a unit the + // program does not resolve carries a null view, a unit with no sampler object carries a + // null sampler CSO (the texture's built-in sampler applies then, exactly as today), and a + // unit with no texture carries a null resource. None of the three is resolved against a + // record either - a set is WORKING STATE, the records it names are OBJECT state, and the + // backend resolves the pair at its own sync point where both are current. void MGPipeApplySetSamplerViews(const MGPSamplerViews& hdr, const MGPBoundView* tail) { - (void)hdr; - (void)tail; + if (!ApplyUnitWindow("set_sampler_views", hdr.Start, hdr.Count, hdr.ContentHash, tail, + g_applier.BoundSamplerViews, g_applier.SamplerViewStart, + g_applier.SamplerViewCount)) { + return; + } + ++g_applier.SamplerViewsSerial; } void MGPipeApplyBindSamplerStates(const MGPSamplerStates& hdr, const MGPipeHandle* tail) { - (void)hdr; - (void)tail; + if (!ApplyUnitWindow("bind_sampler_states", hdr.Start, hdr.Count, hdr.ContentHash, tail, + g_applier.BoundSamplerStates, g_applier.SamplerStateStart, + g_applier.SamplerStateCount)) { + return; + } + ++g_applier.SamplerStatesSerial; } void MGPipeApplySetShaderImages(const MGPShaderImages& hdr, const MGPImageView* tail) { - (void)hdr; - (void)tail; + // The image set carries InternalFormat and Access per entry and BOTH are live + // glBindImageTexture state the format-less image bake keys on, so they are stored as + // sent and recast on the server - the record carries the application's format, and the + // bind-format recast that turns a GL_RG32F bind into something 19 of 26 non-core + // formats on Adreno will accept is the backend's, not this applier's. + if (!ApplyUnitWindow("set_shader_images", hdr.Start, hdr.Count, hdr.ContentHash, tail, + g_applier.BoundShaderImages, g_applier.ShaderImageStart, + g_applier.ShaderImageCount)) { + return; + } + ++g_applier.ShaderImagesSerial; } void MGPipeApplyCreateShaderState(const MGPProgramDesc& desc,