From 08922b6f78b0cd79a3bc8ce80da6dd16d8a26475 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 8 Sep 2026 14:32:28 -0400 Subject: [PATCH] [Fix] (Pipe): read a sub-data record's target halves through the contract's own accessors, answer the emitter whether a create or a respecify was accepted, and apply a respecify that restates the stored storage as a metadata update rather than a redefinition --- MobileGL/MG_Pipe/PipeApply.cpp | 154 +++++++++++++++++----- MobileGL/MG_Pipe/PipeApply.h | 51 ++++++- MobileGL/MG_Test/Pipe/TextureEmitTest.cpp | 31 ++++- 3 files changed, 197 insertions(+), 39 deletions(-) diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index 11bb40bb..530fb141 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -512,21 +512,73 @@ namespace MobileGL::MG_Pipe { return record.Target == kMGPipeResourceTargetBuffer; } - // THE VALUE SPACE OF MGPSubData::Target, written down here because the record itself does - // not say it and package B is about to encode cube faces into the same field. It is - // PACKED (integrator ruling ID-12, the DV-3 seam): the LOW byte is the MGPipeResourceTarget - // that owns the storage and the HIGH byte is the TextureUploadTarget the upload names - - // which is what makes the packing necessary at all, since TextureUploadTarget::Texture1D - // is 0 and the bare enumerator would collide with Buffer. Both halves are therefore - // free to take any value their own enum defines and neither may be read without the mask. + // THE VALUE SPACE OF MGPSubData::Target IS THE CONTRACT'S, AND THIS FILE NO LONGER + // RESTATES IT. c0c (ID-12, the DV-3 seam) moved the encoding into MGPipeTypes.h as + // MGPipePackSubDataTarget / MGPipeSubDataResourceTargetOf / MGPipeSubDataUploadTargetOf, + // so wire v2's local `SubDataResourceTargetOf` - written because those helpers were not + // on that round's base - is gone and every half-read below goes through the contract's + // accessor. Nothing here open-codes `& 0xFF`. // - // MGPipeTypes.h IS SUPPOSED TO OWN THE THREE HELPERS (MGPipePackSubDataTarget / - // MGPipeSubDataResourceTargetOf / MGPipeSubDataUploadTargetOf, contract commit c0c); - // they are NOT on this package's base (feat/disaggregated = c0 + c0b), so this is the - // local decode and it is one line to retire the day c0c lands. The applier still matches - // the WHOLE field for the buffer question above, exactly as ID-12 says it does. - Uint16 SubDataResourceTargetOf(const MGPSubData& record) { - return static_cast(record.Target & 0x00FFu); + // The applier still matches the WHOLE field for the buffer question above, exactly as + // ID-12 says it does, and c0c's own static_assert is what keeps the two readings in + // step: a buffer record's Target is exactly kMGPipeResourceTargetBuffer. + + // ID-18 M4: DOES THIS RESPECIFY REDEFINE ANY STORAGE, OR IS IT CARRYING METADATA? + // + // The question exists because a sticky BindMask / ImageBindableHint bit has exactly one + // way onto the wire - a respecify - and an IMMUTABLE texture has no further respecify to + // ride on. So B re-publishes the descriptor when the mask moves, and the applier has to + // tell that call apart from a real redefinition: the first must NOT drop the pending + // uploads standing against the storage (nothing replaced them), the second must. + // + // THE COMPARISON IS FIELD BY FIELD AND NOT A memcmp. MGPResourceDesc carries Pad0/Pad1 + // and neither side of this comparison is guaranteed to have written them - the client + // fills a stack descriptor field by field - so a byte comparison of 88 bytes is a coin + // flip on padding, which is the defect clientsp's SamplerParameters cache already paid + // for once. + // + // WHAT IS NOT COMPARED IS THE SHORT LIST AND IT IS THE DEFINITION: BindMask and + // ImageBindableHint (the two this call exists to carry), GlNameForDiag (diagnostics, and + // never an identity - section 4.2.1) and the two padding words. EVERYTHING ELSE IS + // COMPARED, which is deliberately WIDER than "the fields that define an allocation": + // being wide can only mis-classify a metadata update as a redefinition, whose cost is + // one level's pending upload dropped by a call B does not emit; being narrow would + // mis-classify a REDEFINITION as metadata and keep boxes in a coordinate system that no + // longer exists, which is an upload past the end of the new level. The asymmetry decides + // the direction. + // + // A BUFFER IS NEVER METADATA-ONLY, and that is a rule rather than a consequence. + // glBufferData at an unchanged size is the canonical ORPHANING idiom - a real + // reallocation whose whole purpose is that the old store is gone - and glBufferStorage + // is the one entry point in the catalogue allowed a synchronous acknowledgement + // (MGPipeResourceRespecifyNeedsAck). Excluding the target here is what makes ID-18 M4's + // "no reallocation ack" true by construction instead of by a second suppression rule, + // and it costs nothing: the mask that M4 is about is a TEXTURE's (and a renderbuffer's), + // and a buffer has no pending upload for the arm to protect anyway. + Bool RespecifyRedefinesNoStorage(const MGPResourceDesc& stored, const MGPResourceDesc& next) { + if (next.Target == kMGPipeResourceTargetBuffer) return false; + if (stored.Target != next.Target) return false; + if (stored.StorageKind != next.StorageKind) return false; + if (stored.InternalFormat != next.InternalFormat) return false; + if (stored.Width != next.Width || stored.Height != next.Height || + stored.Depth != next.Depth) { + return false; + } + if (stored.ArrayLayers != next.ArrayLayers || stored.Levels != next.Levels || + stored.Samples != next.Samples) { + return false; + } + if (stored.FixedSampleLocations != next.FixedSampleLocations) return false; + if (stored.Immutable != next.Immutable) return false; + if (stored.HasDefinedContent != next.HasDefinedContent) return false; + if (stored.Usage != next.Usage || stored.StorageFlags != next.StorageFlags) return false; + // The view and buffer-texture fields: a texture view's storage OWNER and a buffer + // texture's backing range are its storage exactly as an extent is one, and + // re-pointing either is a redefinition however unchanged the extent looks. + if (!(stored.ViewOf == next.ViewOf)) return false; + if (!(stored.BufferForTexBuffer == next.BufferForTexBuffer)) return false; + if (stored.BufOffset != next.BufOffset || stored.BufSize != next.BufSize) return false; + return true; } // WHY A DEAD HANDLE IS NOT A TRIP WIRE HERE, and the bounds faults below are - and @@ -1469,10 +1521,10 @@ namespace MobileGL::MG_Pipe { // is why they are incremented here rather than carried in a payload. // ================================================================================ - void MGPipeApplyResourceCreate(const MGPResourceDesc& desc) { + Bool MGPipeApplyResourceCreate(const MGPResourceDesc& desc) { MOBILEGL_ASSERT(desc.Resource.Slot >= kMGPipeFirstAllocatableSlot, "resource_create named the reserved slot 0"); - if (desc.Resource.Slot < kMGPipeFirstAllocatableSlot) return; + if (desc.Resource.Slot < kMGPipeFirstAllocatableSlot) return false; // P4a: THE TABLE IS CHOSEN BY THE DESCRIPTOR'S TARGET, and getting that wrong is the one // way this call can damage an object it was not about - slot 7 is a live Buffer, a live @@ -1483,7 +1535,7 @@ namespace MobileGL::MG_Pipe { " resource_create {slot=%u, gen=%u, glName=%u}: the descriptor names no " "resource target (%u)", desc.Resource.Slot, desc.Resource.Gen, desc.GlNameForDiag, desc.Target); - return; + return false; } // A CREATE STARTS THE RECORD OVER rather than editing it. The slot it names may be a @@ -1499,7 +1551,7 @@ namespace MobileGL::MG_Pipe { "record table's bound (%u)", desc.Resource.Slot, desc.Resource.Gen, desc.GlNameForDiag, kMGPipeMaxResourceSlots); - return; + return false; } *record = MGPipeResourceRecord{}; record->Gen = desc.Resource.Gen; @@ -1514,13 +1566,14 @@ namespace MobileGL::MG_Pipe { // lazily inside SyncMipmapsToBackend and a renderbuffer's inside its own SyncToBackend, // so there is no GL-call-time hook to dispatch to and P4a adds none: the record IS the // publication, and the backend reads it at the sync point it already has. - if (desc.Target != kMGPipeResourceTargetBuffer) return; + if (desc.Target != kMGPipeResourceTargetBuffer) return true; if (g_resourceOps != nullptr && g_resourceOps->Create != nullptr) { g_resourceOps->Create(desc.Resource, desc); } + return true; } - void MGPipeApplyResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes, + Bool MGPipeApplyResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes, const MGPRespecifiedLevel* level) { Vector* table = ResourceTableForTarget(desc.Target); if (table == nullptr) { @@ -1528,17 +1581,28 @@ namespace MobileGL::MG_Pipe { " resource_respecify {slot=%u, gen=%u, glName=%u}: the descriptor names " "no resource target (%u)", desc.Resource.Slot, desc.Resource.Gen, desc.GlNameForDiag, desc.Target); - return; + return false; } MGPipeResourceRecord* record = ResolveResourceIn(*table, "resource_respecify", desc.Resource); - if (record == nullptr) return; + if (record == nullptr) return false; PinNoLiveHostWrites(*record, desc.Resource, "resource_respecify"); + // IS THIS A REDEFINITION AT ALL? Asked BEFORE the descriptor is replaced, because the + // stored one is the only thing there is to compare against (ID-18 M4). See + // RespecifyRedefinesNoStorage: true means every storage-defining field is unchanged and + // the call exists to carry a BindMask / ImageBindableHint that moved after the + // allocation - the immutable-texture case, where there is no later respecify to ride on. + const Bool metadataOnly = RespecifyRedefinesNoStorage(record->Desc, desc); + // The descriptor is replaced WHOLE, because that is what a respecify is: the store's // extent, usage, storage flags, immutability and defined-content flag are all restated // by the call that redefines it, and the backend reads them from here instead of - // asking a frontend object for them. + // asking a frontend object for them. A metadata update replaces it too - that is how + // the mask arrives - and by construction only the non-storage fields differ. record->Desc = desc; + // THE SERIAL MOVES EITHER WAY, and for a metadata update it is the entire publication: + // the twin re-derives its storage flags from the new mask at its next sync and decides + // for itself whether the backend needs a recreate. ++record->Serial; // A RESPECIFY REDEFINES A STORE, SO THE PENDING UPLOADS AGAINST THE STORE IT REPLACES @@ -1561,8 +1625,21 @@ namespace MobileGL::MG_Pipe { // the arm this set exists for) -> glTexImage2D(1, data), which under a blanket // clear destroys level 0's entry before anything ever uploaded it. // - // A buffer never has a pending upload at all, so both arms are inert for P3a's half. - if (level == nullptr) { + // - and a METADATA update (ID-18 M4) drops NOTHING, whatever `level` says. It is the + // third arm and it refines the first two rather than contradicting them: the rule + // is "the uploads against the storage this call REPLACES go with it", and a call + // whose storage-defining fields all equal the stored descriptor replaces no + // storage, so no level's coordinate system has moved and every pending box is still + // described in the space it was accumulated in. B re-emits the descriptor when a + // sticky bind bit moves, which can land between a glTexSubImage2D and the sync that + // consumes it; eating those texels there would be C1's bug with a different + // trigger, and just as silent. + // + // A buffer never has a pending upload at all, so all three arms are inert for P3a's + // half - which is also why a buffer is never classified as metadata-only (below). + if (metadataOnly) { + // nothing to drop, deliberately. + } else if (level == nullptr) { record->PendingUploads.clear(); } else { // The keys are unique by AccumulatePendingUpload's construction - it looks for the @@ -1583,10 +1660,19 @@ namespace MobileGL::MG_Pipe { // branch whose arms were identical would be dead code the transport would then have to // find and remove. PipeCatalogueTest.ResourceRespecifyAcksOnlyImmutableStorage is what // keeps the predicate honest until the doorbell reads it. - if (desc.Target != kMGPipeResourceTargetBuffer) return; + // + // ID-18 M4 ASKS FOR "NO REALLOCATION ACK" ON A METADATA UPDATE, AND IT IS TRUE HERE BY + // CONSTRUCTION RATHER THAN BY A BRANCH: the predicate is false for every target that is + // not a buffer, and RespecifyRedefinesNoStorage refuses the buffer target outright, so + // MGPipeResourceRespecifyNeedsAck(desc) is false for every record this applier + // classifies as metadata-only. That is why the classification excludes buffers rather + // than suppressing the ack afterwards - a suppression would have been a second rule the + // transport would then have to learn. + if (desc.Target != kMGPipeResourceTargetBuffer) return true; if (g_resourceOps != nullptr && g_resourceOps->Respecify != nullptr) { g_resourceOps->Respecify(desc.Resource, desc, initialBytes); } + return true; } Bool MGPipeApplyResourceSubData(const MGPSubData& record, const void* bytes, @@ -1609,14 +1695,20 @@ namespace MobileGL::MG_Pipe { // accumulate a pending upload onto whatever TEXTURE holds slot N in the texture slot // space. Renderbuffers have no sub-data path at all, so no correct client can produce // one and this is a protocol fault rather than a dropped call. - const Uint16 resourceTarget = SubDataResourceTargetOf(record); + const Uint8 resourceTarget = MGPipeSubDataResourceTargetOf(record.Target); if (resourceTarget == kMGPipeResourceTargetBuffer || - resourceTarget == static_cast(MGPipeResourceTarget::Renderbuffer) || - resourceTarget >= static_cast(MGPipeResourceTarget::Count)) { + resourceTarget == static_cast(MGPipeResourceTarget::Renderbuffer) || + resourceTarget >= static_cast(MGPipeResourceTarget::Count)) { + // BOTH HALVES GO IN THE LINE. The packed field alone leaves a reader doing the + // arithmetic the encoding exists to stop anyone doing by hand, and the upload half + // is what says whether the emitter packed the pair the wrong way round. MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption") " resource_subdata {slot=%u, gen=%u}: the record's resource target names " - "no texture to upload into (target=%u, resource target=%u)", - record.Res.Slot, record.Res.Gen, record.Target, resourceTarget); + "no texture to upload into (target=%u, resource target=%u, upload " + "target=%u)", + record.Res.Slot, record.Res.Gen, record.Target, + static_cast(resourceTarget), + static_cast(MGPipeSubDataUploadTargetOf(record.Target))); return false; } return ApplyTextureUpload(record, bytes, regions); diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index 7c31f145..bf571940 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -774,10 +774,30 @@ namespace MobileGL::MG_Pipe { Uint16 Level = 0; }; + // THE THREE ACCEPTANCE RETURNS, AND WHY ALL THREE (ID-18 M3, clientfb review M3). D-D5 + // step 1 says the client clears a level's dirty flags "for the levels whose record the + // applier ACCEPTED", and the emitter cannot answer that for itself: an `if constexpr` that + // discarded the call, a dead or stale handle (a counted no-op) and a corrupt record (a Fatal + // that deliberately moves no counter) are all invisible from the call site, so a client that + // clears on the strength of having EMITTED drops those texels for good. resource_subdata + // returns it, and so must the two calls that DEFINE the storage a subsequent upload lands + // in - a create or a respecify the applier refused leaves no record for the upload to + // accumulate onto, and B's own bookkeeping (its per-entry descriptor dedupe, its drain list) + // must not advance past a call that never landed. + // + // ALL THREE ARE SOURCE-COMPATIBLE: a Bool return is ignorable, P3a's call sites in + // MG_Impl/Pipe/PipeFill.cpp discard it, and gen_pipe.py never parses this header - the wire + // path calls no MGPipeApply* at all (wire review W1), so PipeCalls.def and + // MobileGL/MG_Pipe/generated do not move. + // resource_create: mints the record and marks the slot Live. Emitted from the buffer // object's CONSTRUCTOR, so a resource exists before anything can name it; storage is // defined lazily by the first respecify and a backend tolerates a resource with none. - void MGPipeApplyResourceCreate(const MGPResourceDesc& desc); + // + // Returns true when the record was minted. False for the three refusals: the reserved slot + // 0, a descriptor whose target names no resource kind, and a slot at or above + // kMGPipeMaxResourceSlots. + Bool MGPipeApplyResourceCreate(const MGPResourceDesc& desc); // resource_respecify: replaces the stored descriptor and bumps Serial. `initialBytes` is // the shadow when desc.HasDefinedContent, else null. kNeedsAck on the call, // MGPipeResourceRespecifyNeedsAck(desc) per record - only an immutable store acks. @@ -798,7 +818,34 @@ namespace MobileGL::MG_Pipe { // Trailing and defaulted for W1's reason: P3a's buffer call site (PipeFill.cpp:691) and // every existing case compile unchanged. PACKAGE B PASSES THE PAIR IT JUST ALLOCATED at // every per-level respecify; it has both halves in hand at the AllocateStorage call site. - void MGPipeApplyResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes, + // + // A METADATA RESPECIFY IS A RESPECIFY THAT REDEFINES NO STORAGE (ID-18 M4). A sticky + // BindMask / ImageBindableHint bit reaches the applier only on a respecify, and an + // IMMUTABLE texture has no further one - that is what immutable means - so the canonical + // order (glTexStorage2D, then glBindImageTexture or an FBO attachment) would leave the + // record's hint at 0 for ever, and the hint is the PREVENTION half of the texture-remint + // stall class. So B re-emits the descriptor when the mask moves, and a record whose + // STORAGE-DEFINING fields all equal the stored descriptor's is applied as a metadata + // update: + // + // - the descriptor is replaced, so BindMask and ImageBindableHint take their new values; + // - NO pending upload is dropped, whatever `level` says. This REFINES the rule above + // rather than contradicting it: that rule drops the uploads against the storage a + // respecify REPLACES, and a call that replaces no storage replaces no coordinate system + // either, so there is nothing to drop. A mask change arriving between a + // glTexSubImage2D and the sync that consumes it must not eat the texels; + // - the serial advances, which is the whole publication - the twin re-derives its storage + // flags from the new mask at its next sync and recreates only where the backend needs + // it (D's side); + // - and MGPipeResourceRespecifyNeedsAck is false for it BY CONSTRUCTION, because a buffer + // is never classified this way (see the body: glBufferData at an unchanged size is a + // real orphaning reallocation, and glBufferStorage is the one entry point allowed a + // synchronous ack). + // + // Returns true when the descriptor was stored - metadata updates included, since the record + // did move - and false when the call was refused: a descriptor whose target names no + // resource kind, or a handle this applier has no live record for at that generation. + Bool MGPipeApplyResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes, const MGPRespecifiedLevel* level = nullptr); // resource_subdata, buffer half: the destination range rides in the record's box through // MGPipeSetSubDataBufferRange, and a false from that helper is where the EMITTER split. diff --git a/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp b/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp index abe3ebb2..75bab9a2 100644 --- a/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp @@ -479,17 +479,32 @@ TEST(TextureEmit, ARespecifyOfOneLevelKeepsThePendingUploadsOfTheOthers) { const Uint8 texels[4096] = {}; MGPipeApplyResourceCreate(TextureDesc(texture, 0, 121)); + // EVERY DESCRIPTOR BELOW CARRIES THE LEVEL COUNT THE CALL IT MODELS WOULD CARRY, and that + // is not decoration. A mutable mip build grows MipmapStorage's level count as it defines + // levels, so package B's descriptor (TextureEmit.h: `desc.Levels = + // mipmap->GetMipmapLevelCount()`) MOVES on the glTexImage2D that adds level 1 - which is + // also why B's own memcmp dedupe emits that respecify at all. A respecify whose + // storage-defining fields are all unchanged is a METADATA update (ID-18 M4) and drops + // nothing whatever level it names, so a case that fed the same descriptor three times would + // be exercising that arm rather than this one. + auto levelDesc = [&](Uint16 levels, Uint32 internalFormat) { + MGPResourceDesc desc = TextureDesc(texture, 64, 121); + desc.Levels = levels; + desc.InternalFormat = internalFormat; + return desc; + }; + // glTexImage2D(level 0, data): the respecify names the level it defines, and the drain then // emits level 0's shape, which the applier accepts. const MGPRespecifiedLevel levelZero{kTex2D, 0}; - MGPipeApplyResourceRespecify(TextureDesc(texture, 64, 121), nullptr, &levelZero); + MGPipeApplyResourceRespecify(levelDesc(1, 0x8058u /*GL_RGBA8*/), nullptr, &levelZero); ASSERT_TRUE(MGPipeApplyResourceSubData(TextureUpload(texture, 0, MGPBox{0, 0, 0, 64, 64, 1}, 0), texels)); // A SECOND FACE OF THE SAME LEVEL, keyed the way the packed Target keys it (ID-12: high // byte = the cube-face upload target, low byte = the resource target), so what survives is // a SET and not one lucky entry - and so that the level number alone cannot be what matched. - const Uint16 secondFace = static_cast((1u << 8) | kTex2D); + const Uint16 secondFace = MGPipePackSubDataTarget(kTex2D, 1u); const MGPRespecifiedLevel faceOfLevelZero{secondFace, 0}; - MGPipeApplyResourceRespecify(TextureDesc(texture, 64, 121), nullptr, &faceOfLevelZero); + MGPipeApplyResourceRespecify(levelDesc(1, 0x8058u), nullptr, &faceOfLevelZero); MGPSubData otherFace = TextureUpload(texture, 0, MGPBox{0, 0, 0, 64, 64, 1}, 0); otherFace.Target = secondFace; ASSERT_TRUE(MGPipeApplyResourceSubData(otherFace, texels)); @@ -498,9 +513,10 @@ TEST(TextureEmit, ARespecifyOfOneLevelKeepsThePendingUploadsOfTheOthers) { // Espryt BAILS - the texture is not mipmap-complete for its min filter - so both entries // are still owed when the next GL call arrives. // - // glTexImage2D(level 1, data): this redefines level 1 of the (kTex2D, *) face only. + // glTexImage2D(level 1, data): this redefines level 1 of the (kTex2D, *) face only, and the + // level count moves 1 -> 2 with it. const MGPRespecifiedLevel levelOne{kTex2D, 1}; - MGPipeApplyResourceRespecify(TextureDesc(texture, 64, 121), nullptr, &levelOne); + MGPipeApplyResourceRespecify(levelDesc(2, 0x8058u), nullptr, &levelOne); ASSERT_EQ(TextureRecordOf(10).PendingUploads.size(), 2u) << "a respecify of level 1 dropped the pending uploads of levels it never redefined - " @@ -511,9 +527,12 @@ TEST(TextureEmit, ARespecifyOfOneLevelKeepsThePendingUploadsOfTheOthers) { EXPECT_EQ(TextureRecordOf(10).PendingUploads[1].UploadTarget, secondFace); // And the key it DOES name goes, because that level's coordinate system has been replaced. + // The redefinition modelled here is glTexImage2D(level 1) with a NEW internal format - a + // legal thing to do to a mutable texture, and a real redefinition of that level's storage, + // so the descriptor moves and the metadata arm does not claim it. ASSERT_TRUE(MGPipeApplyResourceSubData(TextureUpload(texture, 1, MGPBox{0, 0, 0, 32, 32, 1}, 0), texels)); ASSERT_EQ(TextureRecordOf(10).PendingUploads.size(), 3u); - MGPipeApplyResourceRespecify(TextureDesc(texture, 64, 121), nullptr, &levelOne); + MGPipeApplyResourceRespecify(levelDesc(2, 0x8051u /*GL_RGB8*/), nullptr, &levelOne); ASSERT_EQ(TextureRecordOf(10).PendingUploads.size(), 2u) << "the level the respecify DOES redefine kept its box across the redefinition"; for (const auto& entry : TextureRecordOf(10).PendingUploads) {