[Refactor] (P8-F): reclassify what our client cannot send - the multi-draw client-indices/instanced latches and Magma's client-vertex-array become ProtocolCorruption, Magma's mipmap-shader-format-or-shape becomes a named decline decided before the chain grows (with a knob-forced 3D case on split and spawn), and the three unpublished cap bits and the index-mirror sentinel are marked reserved

This commit is contained in:
2026-09-29 22:15:29 -04:00
parent 8d3e2317a6
commit 67890f523a
17 changed files with 230 additions and 103 deletions
@@ -104,3 +104,9 @@ MGL_WIRE_DECLINE(ShapeCreateImageFailed)
// Appended by the B3 review round: the preserve-copy arm of a reallocation was the one exit in
// SyncWireTextureShape still leaving no trace at all.
MGL_WIRE_DECLINE(ShapePreserveCopyFailed)
// ---- VulkanRenderer::GenerateWireMipmap (P8-F) -----------------------------------------
// `mipmap-shader-format-or-shape` was a Fatal on this arm and a decline on the monolith arm; it
// is the monolith's decline now. Counted once per refused glGenerateMipmap, whose chain is left
// as it was.
MGL_WIRE_DECLINE(MipmapShaderFormatOrShape)
@@ -294,7 +294,14 @@
const Uint32 location = input.bindingAttributeLocations[binding];
const auto& attr = vao.Attributes[location];
const auto& resource = state.VertexBuffers[location];
if (MG_Pipe::MGPipeHandleIsNull(resource.Res)) MagmaWireFatal("client-vertex-array@P8");
// P8-F: A RECORD INVARIANT, NOT AN UNMIGRATED VERB (it carried a P8 phase tag).
// The client turns every enabled client-memory array into an owned buffer before the
// draw record is built, or refuses the draw itself (MG_Impl/Pipe/OwnedDrawInputs.h),
// so an attribute the program reads with no buffer handle is a foreign peer's record.
if (MG_Pipe::MGPipeHandleIsNull(resource.Res))
MG_Pipe::MGPipeSessionFail(MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"Magma:client-vertex-array\"} location=%u: an "
"attribute the program reads carries no buffer handle", location);
BufferSlice slice{};
if (!m_bufferManager.AcquireWireSlice(BufferKind::Vertex, resource.Res, slice))
MagmaWireFatal("vertex-buffer-resource");
@@ -20,6 +20,8 @@
// missing WHICH feature bit - was the one thing it did not. The census, the retrace
// refusal tally and every log grep key on the leading word, so it is unchanged and
// unprefixed; everything after the first space is for the reader of the next device run.
// P8-F: the refusal is a named decline now (WireDeclines.def MipmapShaderFormatOrShape);
// the line it logs keeps this word and this detail.
static String MagmaWireShapeDetail(const char* name, VkFormat format, VkFormat storageFormat,
const VkFormatProperties& props, VkImageAspectFlags aspect,
VkImageViewType viewType, VkImageUsageFlags usage,
@@ -1400,8 +1402,10 @@
base + 1, base + count - 1);
return;
}
if (!m_textureManager->GrowWireTextureMipChain(handle,base+count,frame.commandBuffer))
MagmaWireFatal("mipmap-growth");
// P8-F: WHICH ARM TAKES THIS CHAIN IS DECIDED BEFORE THE CHAIN IS GROWN, for the
// depth-stencil decline's reason above. Nothing here depends on the growth: the format,
// its features, the knob tier, the aspect, view type, usage and layer window are the
// same before and after it (GrowWireTextureMipChain carries every one of them over).
VkFormatProperties props{};
const VkFormat format = viewFormat == VK_FORMAT_UNDEFINED ? resource->format : viewFormat;
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle,format,&props);
@@ -1411,6 +1415,50 @@
(props.optimalTilingFeatures & required) == required;
const VkFilter filter = props.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT ?
VK_FILTER_LINEAR : VK_FILTER_NEAREST;
const Bool layersInRange = baseLayer < resource->arrayLayers &&
layerCount <= resource->arrayLayers - baseLayer;
// The depth arm's shape: the layer window, and what WireDepthMipmap.inc's pass refuses
// for an image this function hands it - a format that does not both sample and attach
// as depth (its other refusals, 3D / multisample / a default image, are never built here).
const auto depthPassFeatures =
VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT;
const Bool depthArmTakesShape =
layersInRange && (props.optimalTilingFeatures & depthPassFeatures) == depthPassFeatures;
// The colour arm's shape (the two shader shapes are explained at the colour arm below).
const Bool colour = resource->aspect == VK_IMAGE_ASPECT_COLOR_BIT;
const Bool planar = resource->viewType != VK_IMAGE_VIEW_TYPE_1D &&
resource->viewType != VK_IMAGE_VIEW_TYPE_1D_ARRAY &&
resource->viewType != VK_IMAGE_VIEW_TYPE_3D;
// The shader arm samples through a sampler2D and writes a vec4, which is what
// BlitWireColorImage's own float check enforces; naming it here rather than letting the
// blit return false is what puts the reason in the decline line.
const Bool sampledFloat = vkuFormatIsSampledFloat(format);
const Bool renderableFormat =
(props.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
const Bool attachable = forcedShaderTier < 2 &&
(resource->usageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) != 0;
const Bool viaScratch = !attachable && renderableFormat &&
(resource->usageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0;
const Bool colourArmTakesShape =
colour && planar && layersInRange && sampledFloat && (attachable || viaScratch);
// P8-F: `mipmap-shader-format-or-shape` IS A NAMED DECLINE, NOT A FATAL (rule I (a)).
// The monolith arm has always answered this chain with a decline (VulkanRenderer.cpp's
// GenerateMipmap: a format without blit support is skipped and the chain left unwritten),
// and a shape no shader arm takes is a coverage gap in this backend, not a broken record.
// The leading word stays, so every device-log grep keyed on it still finds the line.
const Bool shaderArmTakesShape =
resource->aspect == VK_IMAGE_ASPECT_DEPTH_BIT ? depthArmTakesShape : colourArmTakesShape;
if (!nativeBlit && !shaderArmTakesShape) {
const String detail = MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
resource->format, props, resource->aspect, resource->viewType, resource->usageFlags,
resource->extent, resource->depth, resource->mipLevels, baseLayer, layerCount,
resource->arrayLayers);
MGL_WIRE_DECLINE_AT(MipmapShaderFormatOrShape, "%s; no mip arm takes this shape, levels %u..%u left as they are",
detail.c_str(), base + 1, base + count - 1);
return;
}
if (!m_textureManager->GrowWireTextureMipChain(handle,base+count,frame.commandBuffer))
MagmaWireFatal("mipmap-growth");
if (!nativeBlit && resource->aspect == VK_IMAGE_ASPECT_DEPTH_BIT) {
// P7 wave 2-B, CONTRACT-P7 §5.1 (A): THE DEPTH ARM, and it is first because a
// depth format without BLIT_DST is the ordinary case, not the exotic one -
@@ -1422,12 +1470,7 @@
// The per-level pass is WireDepthMipmap.inc's, which is the monolith's fragment
// shader with the monolith's arithmetic on a baked pipeline that owns no frontend
// object - §3.2's "observably the monolith's" without the GL program the monolith
// needs to get there.
if (baseLayer >= resource->arrayLayers || layerCount > resource->arrayLayers - baseLayer)
MagmaWireFatal(MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
resource->format, props, resource->aspect, resource->viewType,
resource->usageFlags, resource->extent, resource->depth, resource->mipLevels,
baseLayer, layerCount, resource->arrayLayers).c_str());
// needs to get there. Its layer window and format were checked before the growth.
for (Uint32 level = base + 1; level < base + count; ++level) {
for (Uint32 layer = baseLayer; layer < baseLayer + layerCount; ++layer) {
WireImage source{};
@@ -1445,15 +1488,19 @@
destination.level = level;
destination.extent = {std::max(resource->extent.width >> level, 1u),
std::max(resource->extent.height >> level, 1u)};
// The pass reports the SHAPE it cannot take (1D, 3D, multisample, a
// format without sampled or depth-attachment support), so the refusal
// keeps its name and gains the reason.
if (!GenerateWireDepthMipLevel(source, destination))
MagmaWireFatal(MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
// The pass reports the SHAPE it cannot take. depthArmTakesShape above asks
// its questions first, so this is reached only if the two lists drift apart
// - and then it is the same named decline (P8-F), not a session's end. It
// can only refuse the first level: the shape does not change down the chain.
if (!GenerateWireDepthMipLevel(source, destination)) {
const String detail = MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
resource->format, props, resource->aspect, resource->viewType,
resource->usageFlags, resource->extent, resource->depth,
resource->mipLevels, baseLayer, layerCount,
resource->arrayLayers).c_str());
resource->mipLevels, baseLayer, layerCount, resource->arrayLayers);
MGL_WIRE_DECLINE_AT(MipmapShaderFormatOrShape, "%s; the depth pass refused level %u",
detail.c_str(), level);
return;
}
}
}
} else if (!nativeBlit) {
@@ -1470,27 +1517,9 @@
// cannot, an owned scratch attachment of the destination level's size renders
// the mip and vkCmdCopyImage moves it in: a raw texel move, no conversion, and
// the texels still come from GPU contents rather than from a staged shadow.
const Bool colour = resource->aspect == VK_IMAGE_ASPECT_COLOR_BIT;
const Bool planar = resource->viewType != VK_IMAGE_VIEW_TYPE_1D &&
resource->viewType != VK_IMAGE_VIEW_TYPE_1D_ARRAY &&
resource->viewType != VK_IMAGE_VIEW_TYPE_3D;
const Bool layersInRange = baseLayer < resource->arrayLayers &&
layerCount <= resource->arrayLayers - baseLayer;
// The shader arm samples through a sampler2D and writes a vec4, which is what
// BlitWireColorImage's own float check enforces; naming it here rather than
// letting the blit return false is what puts the reason in the fatal line.
const Bool sampledFloat = vkuFormatIsSampledFloat(format);
const Bool renderableFormat =
(props.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
const Bool attachable = forcedShaderTier < 2 &&
(resource->usageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) != 0;
const Bool viaScratch = !attachable && renderableFormat &&
(resource->usageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0;
if (!colour || !planar || !layersInRange || !sampledFloat || (!attachable && !viaScratch))
MagmaWireFatal(MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
resource->format, props, resource->aspect, resource->viewType,
resource->usageFlags, resource->extent, resource->depth, resource->mipLevels,
baseLayer, layerCount, resource->arrayLayers).c_str());
//
// Which of the two (`attachable` or `viaScratch`), and whether either takes the
// shape at all, was decided before the growth (colourArmTakesShape, P8-F).
for (Uint32 level = base + 1; level < base + count; ++level) {
for (Uint32 layer = baseLayer; layer < baseLayer + layerCount; ++layer) {
WireImage source{};
+3 -3
View File
@@ -186,9 +186,9 @@ namespace MobileGL::MG_Backend {
// ZERO IS THE EXPLICIT ANSWER FOR P5, not an omission (ServerSession.h's block):
// every optional capability bit belongs to the package that owns its question, and
// withholding one leaves the legacy path running, which is the safe direction.
// kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes must be 0 for the whole of P5
// by ruling - they are the only two things that ask for an MGHostSpan, and 0 is
// what keeps every one of them out of the first IPC frame (contract table 0).
// kCapNeedsHostIndexBytes, kCapNeedsHostUboBytes and kCapViewportArray are RESERVED
// (P8-F) and never set here: the first two are the only things that ask for an
// MGHostSpan, and 0 keeps every one of them off the wire (contract table 0).
//
// P5b t2 (CONTRACT-P5B.md §6.5) PUBLISHES THE ONE BIT P5b ADDS, and this is the
// only place that can: the question kCapBackendOwnsXfbCapture answers is "does the
+4 -4
View File
@@ -247,10 +247,10 @@ namespace MobileGL::MG_Pipe {
for (Uint32 w = 0; w < kMGPipeShaderBufferWritableMaskWords; ++w) {
header.WritableMask[w] = writableMask[w];
}
// HostSpanCount IS 0 ALWAYS on Espryt, and that is a ruling rather than an omission:
// the second var-tail exists for kCapNeedsHostUboBytes (Magma's named-UBO ring) and
// that bit is 0 for the whole of P5 (CONTRACT-P5.md table 0). The codec's host-span
// honesty pass is the guard that says so out loud if a backend ever publishes it.
// HostSpanCount IS 0 ALWAYS, and that is a ruling rather than an omission: the
// second var-tail exists for kCapNeedsHostUboBytes (a Magma named-UBO ring that was
// never built) and that bit is RESERVED (P8-F): no server publishes it. The codec's
// host-span honesty pass is the guard that says so out loud if a peer sends one.
header.HostSpanCount = 0;
header.ContentHash = hash;
MGPipeRouteSetShaderBuffers(header, m_entries.data());
+2 -2
View File
@@ -263,8 +263,8 @@ namespace MobileGL::MG_Pipe {
if (vao) {
if (const auto& bound = vao->GetIndexBufferBindingSlot().GetBoundObject()) {
m_lastIndex.Res = MGPipeSlots().Acquire(MGPipeKind::Buffer, bound->GetLifetimeId());
// The ELEMENT_ARRAY bit, and it is the one the split path keys on
// (kCapNeedsHostIndexBytes -> restart rewriting, multi-draw flattening).
// The INDEX|ELEMENT_ARRAY bind bits. ELEMENT_ARRAY's D-B7 reader
// (kCapNeedsHostIndexBytes) is RESERVED (P8-F) and was never built.
// Noted at every draw for RefreshBindMask's reason: an EBO defined through
// DSA and unbound before its last respecify would otherwise never publish
// it, and getting that bit wrong is invisible in monolith.
@@ -3354,6 +3354,26 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
"${CMAKE_CURRENT_SOURCE_DIR}/Harness/split_log_paths.py"
check "${CMAKE_CTEST_COMMAND}" "${CMAKE_BINARY_DIR}")
set_tests_properties(SplitLogPaths.PrivateAndDistinct PROPERTIES LABELS "integration-split")
# P8-F: `Magma:mipmap-shader-format-or-shape` is a named DECLINE on the wire arm now
# (WireDeclines.def MipmapShaderFormatOrShape), as the monolith arm's GenerateMipmap already
# declines. The shape is a 3D chain with the native blit forced off - neither shader mip arm
# takes it - and the knob is read BY THE SERVER, so split + spawn only (the ShaderMip entries'
# tcp note below). The `.ShaderMip1.` tail is already MAGMA_SERVER_ENV_KNOB_NO_TCP in
# scripts/ci/spawn_lane_parity.py. Environment: the Fm lane's flavour plus the arm.
foreach(mglItestArm IN ITEMS Split Spawn)
mgl_itest_magma_arm_shape(${mglItestArm})
mgl_itest_join_environment(mglItestP8fMipEnvironment
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ITEST_REQUIRE_GPU=1" "MGITEST_SPLIT_LANE=1"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_VULKAN_ENV} ${mglItestArmTransportEnv})
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.${mglItestArm}.ShaderMip1."
TEST_FILTER "F1WireScenario.GenerateMipmapWithoutAShaderArmShapeDeclinesAndKeepsTheSession"
DISCOVERY_TIMEOUT 30
PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT}
${mglItestArmProperties}
ENVIRONMENT "${mglItestP8fMipEnvironment}\;MGITEST_MAGMA_FORCE_SHADER_MIPMAP=1\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p8f-shadermip-decline${mglItestArmLogTag}.log")
endforeach()
endif()
# f1: split-only source keeps the monolith registry unchanged.
@@ -675,6 +675,65 @@ TEST_F(F1WireScenario, GenerateMipmapDepthStencilDeclinesAndKeepsTheSession) {
glClearDepth(1.0);
}
// P8-F: `Magma:mipmap-shader-format-or-shape` IS A NAMED DECLINE ON THE WIRE ARM NOW, which is
// the monolith arm's answer for a chain it cannot build (VulkanRenderer.cpp's GenerateMipmap
// declines a format without blit support and leaves the chain unwritten). It used to end the
// session. A 3D texture is a shape neither wire shader arm takes (the colour pass samples a
// sampler2D), so with the native blit forced off (MGITEST_MAGMA_FORCE_SHADER_MIPMAP) it is what
// reaches the refusal on this host - lavapipe blits a 3D RGBA8 chain natively otherwise.
//
// What this case pins: no GL error, the session still answering (two readback round trips), level
// 0 untouched, and level 1 still holding the application's own texels - the decline wrote nothing.
// Magma only, and knob-gated, like GenerateMipmapWithoutNativeBlitPixels above.
// Red once (executed, reverted): restore the MagmaWireFatal and this case dies with
// Fatal{UnmigratedVerb, "Magma:mipmap-shader-format-or-shape ..."}.
TEST_F(F1WireScenario, GenerateMipmapWithoutAShaderArmShapeDeclinesAndKeepsTheSession) {
if (!Ready()) return;
if (Gl().BackendName() != "DirectVulkan")
GTEST_SKIP() << "Magma wire-arm decline (P8-F, mipmap-shader-format-or-shape): the knob and "
"the shader mip arms it forces are Magma's; Espryt ("
<< Gl().BackendName() << ") generates this chain itself";
const std::string tier = SplitLane::MarkerValue("MGITEST_MAGMA_FORCE_SHADER_MIPMAP");
if (tier.empty())
GTEST_SKIP() << "MGITEST_MAGMA_FORCE_SHADER_MIPMAP is unset: this driver blits a 3D RGBA8 "
"chain natively, so the decline under test is not entered";
GLuint volume = 0;
glGenTextures(1, &volume);
glBindTexture(GL_TEXTURE_3D, volume);
glTexStorage3D(GL_TEXTURE_3D, 3, GL_RGBA8, 4, 4, 4);
const std::array<GLubyte, 4> base{200, 40, 80, 255};
const std::array<GLubyte, 4> sentinel{10, 220, 30, 255};
const auto fill = [](int side, const std::array<GLubyte, 4>& texel) {
std::vector<GLubyte> pixels(static_cast<size_t>(side * side * side * 4));
for (size_t at = 0; at < pixels.size(); at += 4) std::copy(texel.begin(), texel.end(), pixels.begin() + at);
return pixels;
};
const auto level0 = fill(4, base);
const auto level1 = fill(2, sentinel);
glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 4, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, level0.data());
glTexSubImage3D(GL_TEXTURE_3D, 1, 0, 0, 0, 2, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, level1.data());
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ShaderMipDecline.setup";
const auto before = PeekSplitRuntime().emitSeq;
glGenerateMipmap(GL_TEXTURE_3D);
ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ShaderMipDecline.wire";
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ShaderMipDecline.error";
for (const int level : {0, 1}) {
const int side = 4 >> level;
std::vector<GLubyte> pixels(static_cast<size_t>(side * side * side * 4), 0x5a);
glGetTexImage(GL_TEXTURE_3D, level, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ShaderMipDecline.readback level " << level;
const std::array<GLubyte, 4>& expected = level == 0 ? base : sentinel;
size_t wrong = 0;
for (size_t at = 0; at < pixels.size(); at += 4)
if (!std::equal(expected.begin(), expected.end(), pixels.begin() + at)) ++wrong;
EXPECT_EQ(wrong, 0u) << "F1.ShaderMipDecline.level" << level << " tier=" << tier
<< ": a declined mip must leave every level as the application wrote it";
}
glBindTexture(GL_TEXTURE_3D, 0);
glDeleteTextures(1, &volume);
glBindTexture(GL_TEXTURE_2D, texture);
}
TEST_F(F1WireScenario, GenerateMipmapHonorsMutableBaseAndMaxWithoutChangingOtherLevels) {
if (!Ready()) return;
const std::array<GLubyte, 4> colors[5] = {
+7 -7
View File
@@ -13,16 +13,16 @@
//
// Monolith: Ptr addresses the frontend shadow or the application's own memory and the
// accessor is one predictable branch. Split: Ptr is null and the bytes live in a staging
// segment named by Seg/Offset, or - for the index bytes a server-side primitive-restart
// rewrite or multi-draw flattening consumes - in the server's own index host mirror, which
// costs no wire traffic at all (D-B7).
// segment named by Seg/Offset. D-B7's third shape - index bytes a server-side restart
// rewrite reads out of its own index host mirror, at no wire cost - was never built; its
// sentinel below is RESERVED (P8-F).
namespace MobileGL::MG_Pipe {
// Seg sentinels. Anything else is a real SEG_STAGE id assigned by the transport.
inline constexpr Uint32 kMGHostSpanSegNone = 0;
// "The bytes are already on your side": the server reads them out of the index host
// mirror it maintains for every resource created with the ELEMENT_ARRAY bind bit while
// kCapNeedsHostIndexBytes is set. When the mirror is over budget the tracker degrades
// to per-draw staging and counts the bytes in index-bytes-shipped.
// RESERVED (P8-F). It was to mean "the bytes are already on your side", read out of an
// index host mirror armed by kCapNeedsHostIndexBytes. Neither was built (client indices
// travel as owned element buffers; the cap is reserved), so no encoder writes this value
// and SegmentTable::Resolve answers it "unknown". Kept so the value is never reused.
inline constexpr Uint32 kMGHostSpanSegFromServerIndexMirror = 0xFFFFFFFFu;
struct MGHostSpan {
+20 -20
View File
@@ -107,19 +107,19 @@ namespace MobileGL::MG_Pipe {
// flattening - is not expressible as a capability (D-B7).
enum MGPCapBit : Uint64 {
kCapNone = 0,
kCapViewportArray = 1ull << 0,
kCapViewportArray = 1ull << 0, // RESERVED (P8-F): no producer, no consumer; never renumber
kCapFloat64VertexAttrib = 1ull << 1,
kCapResidentSubData = 1ull << 2,
kCapCpuXfbPrimitiveAccounting = 1ull << 3,
kCapTimerQuery = 1ull << 4,
kCapOcclusionQuery = 1ull << 5,
kCapXfbPrimitivesQuery = 1ull << 6,
// The server rewrites restart indices / flattens multi-draws itself and therefore
// needs the index bytes on its side: under split this arms the index host mirror
// (D-B7).
// RESERVED (P8-F): never published, never read. The index host mirror it would arm
// (D-B7) was not built: client indices travel as owned element buffers and the server
// reads its own staged bytes. The bit position is wire format: never reuse/renumber it.
kCapNeedsHostIndexBytes = 1ull << 7,
// The server packs named uniform blocks into its own ring and therefore needs the
// host bytes of a set_shader_buffers(Uniform) range (D-B8).
// RESERVED (P8-F): never published, never read - the named-UBO host span (D-B8) has
// no producer. The bit position is wire format: never reuse or renumber it.
kCapNeedsHostUboBytes = 1ull << 8,
// P5b t2 (CONTRACT-P5B.md §6.5), the one cap bit P5b adds. The SERVER's backend owns
// the transform-feedback capture, i.e. its own table registers EndTransformFeedback.
@@ -312,11 +312,11 @@ namespace MobileGL::MG_Pipe {
kMGPipeBindDepthStencil = 1u << 8,
kMGPipeBindStreamOutput = 1u << 9,
kMGPipeBindAtomic = 1u << 10,
// THE D-B7 SWITCH. With kCapNeedsHostIndexBytes set the server mirrors this
// resource's bytes so it can rewrite restart indices and flatten multi-draws
// (ARCHITECTURE.md 10.3). Getting it wrong is invisible in monolith and silently
// disables both under split, which is why it is set from a table rather than from a
// special case at the emission site.
// Set beside INDEX on every element-array buffer, from a table (ResourceTracker.h).
// It was THE D-B7 SWITCH: with kCapNeedsHostIndexBytes the server would mirror this
// resource's bytes for restart rewriting and multi-draw flattening (ARCHITECTURE.md
// 10.3). That cap is RESERVED (P8-F) and the mirror was never built, so no reader
// consumes this bit today; it stays on the wire, at this position.
kMGPipeBindElementArray = 1u << 11,
};
@@ -331,8 +331,8 @@ namespace MobileGL::MG_Pipe {
Uint8 StorageKind; // == TextureStorageType (Mipmap | Buffer)
// MGPipeBindBit, above: VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|
// SHADER_IMAGE|RENDER_TARGET|DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY. The
// ELEMENT_ARRAY bit is the D-B7 switch: with kCapNeedsHostIndexBytes set the server
// mirrors this resource.
// ELEMENT_ARRAY bit was the D-B7 switch; kCapNeedsHostIndexBytes is RESERVED (P8-F)
// and no server mirrors this resource.
Uint16 BindMask;
Uint32 InternalFormat; // already resolved to an uncompressed fallback by the client
Uint32 Width, Height, Depth;
@@ -992,11 +992,11 @@ namespace MobileGL::MG_Pipe {
MGP_ASSERT_POD(MGPShaderImages, 16);
// One bound buffer range: 24 bytes, no inline host span. The named-UBO host bytes a
// backend needs under kCapNeedsHostUboBytes (D-B8) travel as an OPTIONAL second var-tail,
// MGHostSpan[HostSpanCount] behind the ranges, announced by MGPShaderBuffers below. An
// inline span would have cost every SSBO, atomic-counter and XFB range 32 dead bytes, and
// D-B8 says not to freeze that payload's shape before the stage-ubo-named counter has
// produced numbers.
// backend would need under kCapNeedsHostUboBytes (D-B8; that cap is RESERVED since P8-F
// and nothing sends the tail) travel as an OPTIONAL second var-tail, MGHostSpan[HostSpanCount]
// behind the ranges, announced by MGPShaderBuffers below. An inline span would have cost
// every SSBO, atomic-counter and XFB range 32 dead bytes, and D-B8 says not to freeze that
// payload's shape before the stage-ubo-named counter has produced numbers.
struct MGPBufferRange {
MGPipeHandle Res;
Uint64 Offset;
@@ -1005,8 +1005,8 @@ namespace MobileGL::MG_Pipe {
MGP_ASSERT_POD(MGPBufferRange, 24);
// Var-tail header: MGPBufferRange[Count], then MGHostSpan[HostSpanCount]. HostSpanCount is
// 0, or Count for the Uniform class under kCapNeedsHostUboBytes (a range with nothing to
// ship carries an empty span, so the two arrays stay index-aligned).
// 0, or Count for the Uniform class under kCapNeedsHostUboBytes (RESERVED, P8-F: 0 today;
// a range with nothing to ship carries an empty span, so the two arrays stay aligned).
// MGPShaderBuffers::Class's three values, and the ONLY spelling of them (P5e,
// MG_Remote/CONTRACT-P5E.md §1). The payload's comment has named them since P4a and
// nothing numbered them, so the emitter and the applier were one literal each away from
+3 -3
View File
@@ -44,9 +44,9 @@ namespace MobileGL::MG_Remote {
// (MGPipeTypes.h:108-124) and has no per-family bit at all, so "read the CallMask" was
// not yet an implementable instruction. It is now:
//
// bits 0..8 MGPCapBit, unchanged. kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes
// are BOTH ZERO in P5 by ruling (table 0), which is what keeps every
// MGHostSpan out of the first IPC frame.
// bits 0..8 MGPCapBit, unchanged. kCapViewportArray (0), kCapNeedsHostIndexBytes (7)
// and kCapNeedsHostUboBytes (8) are RESERVED (P8-F): never published, and
// their positions are never reused, which keeps every MGHostSpan off the wire.
// bits 9..31 reserved for further MGPCapBits.
// bits 32..47 THE CONSUMER MASK: bit (32 + n) means "the server has a consumer for
// MGPipe subsystem bit n" - i.e. the server's own subsystem mask, shifted.
+1 -1
View File
@@ -251,7 +251,7 @@ namespace MobileGL::MG_Remote::Client {
// P5e (sb, CONTRACT-P5E.md §5.6): the indexed buffer binding points, one record per
// class. The generic tail wrapper carries the FIRST tail only, which is the whole of
// the record on Espryt - the optional MGHostSpan tail exists for kCapNeedsHostUboBytes
// and that bit is 0 for the whole of P5.
// and that bit is RESERVED (P8-F): no server publishes it.
MGP_WIRE_TAIL(SetShaderBuffers, MGPShaderBuffers, MGPBufferRange)
MGP_WIRE_TAIL(SetVertexAttribDefaults, MGPVertexAttribDefaults, MGPAttribValue)
+19 -16
View File
@@ -766,17 +766,20 @@ namespace MobileGL::MG_Remote::Server {
return LandReadbackInBuffer(record, bytes.data(), bpp);
}
// P5b's server-side stub shape (CONTRACT-P5B.md): the same line the client's class-C table
// raises (EmitTables.cpp UnmigratedVerbFatal) and the same family the census greps, so a
// slot flipped on the client ahead of its server half aborts BY NAME on the apply thread
// rather than rendering nothing. Named "(server sink)" in the message so a log reader can
// tell which half is missing.
// 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 ServerUnmigratedVerbLatch(const char* slot) {
return SessionLatch(MGFatalFamily::UnmigratedVerb, "MGPipe: Fatal{UnmigratedVerb, \"%s\"} (server sink: the record crossed and "
"ServerVerbSink has no body for it yet - CONTRACT-P5B.md names the package)",
static Bool MultiDrawShapeLatch(const char* slot) {
return SessionLatch(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);
}
@@ -802,10 +805,10 @@ namespace MobileGL::MG_Remote::Server {
// 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 (the census's own grep family): a multi-draw that arrived
// with a span (the client refuses "MultiDrawElements+CLIENT_INDICES" first; P8's
// HostResolve.cpp flattens it) and a multi-draw that claims instancing (no GL entry point
// produces one; the client never sends it).
// 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_DISAGGREGATED
namespace {
// P5e (tx2), CONTRACT-P5E §5.3 / ruling 19 (ID-95, A8 closed). THE TWO UNIT WINDOWS MUST
@@ -970,12 +973,12 @@ namespace MobileGL::MG_Remote::Server {
// ---- the multi-draws: the two arrays rebuilt from the ranges (rule C) --------------
if (info.NumDraws != 1) {
if (userIndices != nullptr) {
return ServerUnmigratedVerbLatch(info.IndexSize == 0 ? "MultiDrawArrays+CLIENT_INDICES"
: "MultiDrawElements+CLIENT_INDICES");
return MultiDrawShapeLatch(info.IndexSize == 0 ? "MultiDrawArrays+CLIENT_INDICES"
: "MultiDrawElements+CLIENT_INDICES");
}
if (instanced) {
return ServerUnmigratedVerbLatch(info.IndexSize == 0 ? "MultiDrawArrays+INSTANCED"
: "MultiDrawElements+INSTANCED");
return MultiDrawShapeLatch(info.IndexSize == 0 ? "MultiDrawArrays+INSTANCED"
: "MultiDrawElements+INSTANCED");
}
const auto n = static_cast<SizeT>(info.NumDraws);
m_multiCounts.resize(n);
+3 -3
View File
@@ -162,9 +162,9 @@ namespace MobileGL::MG_Remote::Server {
// that owns the question (kCapTimerQuery to the query family, kCapResidentSubData to
// b1, and so on). `SetCapabilityBits(0)` is a legitimate and explicit answer - "this
// server offers no optional capability" - and is the right call while those packages
// land. kCapNeedsHostIndexBytes / kCapNeedsHostUboBytes must stay 0 for the whole of
// P5 by ruling (CONTRACT-P5 table 0): they are the only two things that ask for an
// MGHostSpan, and 0 is what keeps every one of them out of the first IPC frame.
// land. kCapNeedsHostIndexBytes / kCapNeedsHostUboBytes / kCapViewportArray are RESERVED
// (P8-F; CONTRACT-P5 table 0 first held the two host-byte bits at 0): the first two are
// the only things that ask for an MGHostSpan, and 0 keeps every one of them off the wire.
void SetCapabilityBits(Uint64 capBits);
// BITS 32..47, THE CONSUMER MASK (R-8 / C-4): which MGPipe subsystems this server has
+4 -3
View File
@@ -456,8 +456,9 @@ namespace MobileGL::MG_Remote::Wire {
}
const void* SegmentTable::Resolve(Uint32 seg, Uint64 offset, Uint64 size) const {
// 0 is "no segment", ALWAYS (table 0), and 0xFFFFFFFF is P8's index-mirror sentinel
// which this phase does not resolve. Both land here as "unknown", and the CALLER
// 0 is "no segment", ALWAYS (table 0), and 0xFFFFFFFF is the RESERVED index-mirror
// sentinel (P8-F: the mirror was never built, so nothing resolves it and no client of
// ours sends it). Both land here as "unknown", and the CALLER
// escalates to Fatal - a unit case has to be able to exercise this arithmetic without
// dying.
if (seg == kSegNone || seg > static_cast<Uint32>(kSegAdopt)) {
@@ -2121,7 +2122,7 @@ namespace MobileGL::MG_Remote::Wire {
// entire point is that the server stops walking the client's binding-point table.
//
// THE HOST-SPAN PASS STILL RUNS FIRST, and it runs whether or not the record is
// applied: kCapNeedsHostUboBytes is 0 for the whole of P5 (table 0), so the second
// applied: kCapNeedsHostUboBytes is RESERVED (P8-F, never published), so the second
// tail is always absent here, and this pass is what says so out loud if it ever is
// not. A span that was going to be refused must be refused BEFORE the applier has
// stored the window it rides with.
+3 -3
View File
@@ -148,9 +148,9 @@ namespace MobileGL::MG_Remote::Wire {
// declared. Fatal on an absent blob, then CheckBlobIsHonest on a present one.
Bool RequireDeclaredBlob(MG_Pipe::MGPWireOp op, const MG_Pipe::MGPBlobRef& blob,
const SegmentTable& segments);
// R-2.3 arms 1 and 3 for MGHostSpan. P5's reduced path should produce ZERO host spans
// (kCapNeedsHostIndexBytes / kCapNeedsHostUboBytes are both 0 in P5, table 0), so this
// firing at all is a finding, not just a corruption check.
// R-2.3 arms 1 and 3 for MGHostSpan. Our client produces ZERO host spans
// (kCapNeedsHostIndexBytes / kCapNeedsHostUboBytes are RESERVED since P8-F, never set), so
// this firing at all is a finding, not just a corruption check.
//
// IT CANNOT DO ARM 4 - it has no segment table - so a span that names a real segment and a
// run PAST THE END OF IT passes this function. Use the overload below on any path that has
+3 -1
View File
@@ -1159,8 +1159,10 @@ namespace {
p.FrameSerial = 0;
return Forge(c, r, P::MGPWireOp::Present, Bytes(p));
}},
// P8-F: ProtocolCorruption, no longer UnmigratedVerb - our client cannot send this shape
// (nor +CLIENT_INDICES, the same site), so only a foreign peer like this one reaches it.
{"MultiDrawInstanced", "PipeApplier.cpp", "MultiDraw*+INSTANCED", Outcome::Latched,
"Fatal{UnmigratedVerb, \"MultiDrawArrays+INSTANCED\"}", false,
"Fatal{ProtocolCorruption, \"MultiDrawArrays+INSTANCED\"}", false,
[](Client::ClientSession& c, PeerReport& r) {
P::MGPDrawInfo info{};
info.Mode = GL_TRIANGLES;