Merge branch 'p5f/fm' of /mnt/c/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/.git/modules/MobileGL into codex/p5f-wave2

# Conflicts:
#	MobileGL/MG_Pipe/PipeApply.h
#	MobileGL/MG_Remote/Server/PipeApplier.cpp
This commit is contained in:
2026-09-19 22:36:56 -04:00
24 changed files with 2863 additions and 168 deletions
+6 -31
View File
@@ -1391,7 +1391,7 @@ jobs:
tail -5 "${RUNNER_TEMP}/dualblock-split.txt"
echo "dual-block census run: rc=${rc}"
rows=$(grep -cvE '^\s*(#|$)' "${expected}")
rows=$(awk '!/^[[:space:]]*(#|$)/ { n++ } END { print n+0 }' "${expected}")
if [ "${rc}" -eq 0 ] && [ "${rows}" -gt 0 ]; then
echo "::error::the dual-block census lane went GREEN with ${rows} expected pair(s) outstanding - the knob stopped arming, or the fields retired and dualblock-expected-fatals.txt was not emptied in the same commit"
exit 1
@@ -1407,42 +1407,17 @@ jobs:
# ---- P5e (gl), CONTRACT-P5E §7: MAGMA'S OWN LANE, EXPECTED RED -----------------------
#
# The two DirectVulkan.Split.NamedBlit entries abort on GetFramebufferBindingSlot@Clear
# (VulkanRenderer.cpp:7648). That field retires in P5e ON ESPRYT and in P7 ON MAGMA, and
# FieldOwnership.def carries ONE retiring-phase string for both - so admitting the pair to
# keep them in the Espryt lane would forgive an Espryt Clear reading the frontend's binding
# slot again, which is exactly the regression fb's handle arm exists to prevent. Splitting
# the lane costs two entries; admitting the row would cost the gate.
#
# THE LABEL IS `integration-magma-split` AND THE SPELLING MATTERS: `ctest -L` is a REGEX, so
# a label named `integration-split-magma` is still matched by `-L integration-split` and the
# entries would never have left. Measured before the rename.
#
# IT IS ASSERTED RED, WITH ITS MARKER. "Magma is red" must not be allowed to decay into
# "Magma did not run": the step fails both when the run goes green (P7 landed early, or the
# entries stopped drawing) and when it goes red without the named abort in a private log.
- name: Integration-magma-split under strict - expected red until P7 (CONTRACT-P5E section 7)
# P5f fm: Magma's framebuffer, copy and mipmap verbs consume server records.
# The label remains separate so both backends' coverage stays explicit.
- name: Integration-magma-split under strict and dual-block state (P5f fm)
working-directory: build-split
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
MOBILEGL_IPC_STRICT_ERRORS: "1"
MOBILEGL_IPC_ROLE_SPLIT_STATE: "1"
MOBILEGL_TRANSPORT: "inproc"
run: |
ulimit -c unlimited
rc=0
ctest -L integration-magma-split --no-tests=error -j 2 > "${RUNNER_TEMP}/strict-magma.txt" 2>&1 || rc=$?
tail -5 "${RUNNER_TEMP}/strict-magma.txt"
if [ "${rc}" -eq 0 ]; then
echo "::error::integration-magma-split went GREEN under strict. Magma is in lockstep for the whole of P5e (ID-90) and GetFramebufferBindingSlot retires there in P7, so these entries are expected to abort. If P7 has landed, delete this step and fold the entries back into integration-split."
exit 1
fi
marker='Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot@Clear"}'
if ! grep -rqF "${marker}" MobileGL/MG_IntegrationTest/p5b-DirectVulkan-*.log 2>/dev/null; then
echo "::error::integration-magma-split is red but no private log carries ${marker} - the lane failed for some OTHER reason, which is a defect rather than the expected-red control"
grep -rhoE '(Fatal|Admitted)\{[A-Za-z]*, "[^"]*"\}' MobileGL/MG_IntegrationTest/p5b-DirectVulkan-*.log 2>/dev/null | sort | uniq -c | sort -rn | head
exit 1
fi
echo "integration-magma-split is red by design on ${marker}"
ctest -L integration-magma-split --no-tests=error -j 2 --output-on-failure
# ---- P5e (ra2), ID-134 / ID-82: THE CLIENT-ARRAY LANE, EXPECTED RED UNDER RUN-AHEAD ----
#
@@ -20,6 +20,7 @@
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#include <MG_Remote/Server/ServerLoop.h>
#include <MG_Pipe/PipeApply.h>
#endif
#include <atomic>
#include <bit>
@@ -712,6 +713,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
auto& state = MG_Pipe::MGPipeApplier();
const auto handle = state.VerbStorageBlockProgram;
if (handle.Slot >= state.ShaderCsos.size() || !state.ShaderCsos[handle.Slot].Live ||
state.ShaderCsos[handle.Slot].Gen != handle.Gen || !storageBlockName) {
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"Magma:storage-block-program-record\"}");
std::abort();
}
auto& record = state.ShaderCsos[handle.Slot];
auto found = std::find_if(record.StorageOverrides.begin(), record.StorageOverrides.end(),
[&](const auto& entry) { return entry.Name == storageBlockName; });
if (found == record.StorageOverrides.end())
record.StorageOverrides.push_back({storageBlockName, static_cast<Int32>(storageBlockBinding)});
else found->Binding = static_cast<Int32>(storageBlockBinding);
++record.BindingsSerial;
return;
}
#endif
auto* programObject = TryGetDirectVulkanProgram(program);
if (!programObject || storageBlockName == nullptr) return;
const Int maxBindings =
@@ -0,0 +1,236 @@
// MobileGL - server-owned program input for Magma (P5f fm).
#pragma once
#include <MG_State/GLState/ProgramState/ProgramObject.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/PipeApply.h>
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
#include "../DirectVulkanResourceState.h"
#include <spirv_reflect.h>
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
#if MOBILEGL_BUILD_DISAGGREGATED
// A borrowed view, valid only while its frontend object or server record lives.
// It never constructs a ProgramObject on the server. Post-link mutable values
// come from the record tails, not from the archive's link-time defaults.
class MagmaProgramSource {
public:
using Program = MG_State::GLState::ProgramObject;
MagmaProgramSource(const Program& program) : m_frontend(&program) {}
MagmaProgramSource(MG_Pipe::MGPipeHandle handle, const MG_Pipe::MGPipeShaderCsoRecord& record)
: m_handle(handle), m_record(&record) {
MOBILEGL_ASSERT(record.Archive != nullptr, "Magma program record has no archive");
}
Bool IsWire() const { return m_record != nullptr; }
MG_Pipe::MGPipeHandle Handle() const { return m_handle; }
const Program* Frontend() const { return m_frontend; }
Uint GetExternalIndex() const { return IsWire() ? m_handle.Slot : m_frontend->GetExternalIndex(); }
Uint64 GetLifetimeId() const {
return IsWire() ? (Uint64(m_handle.Gen) << 32) | m_handle.Slot : m_frontend->GetLifetimeId();
}
Bool GetLinkStatus() const { return IsWire() ? m_record->Desc.LinkStatus != 0 : m_frontend->GetLinkStatus(); }
Bool GetSpirvStatus() const { return IsWire() ? m_record->Desc.SpirvStatus != 0 : m_frontend->GetSpirvStatus(); }
Bool PointSizeDemoted() const { return IsWire() ? m_record->Desc.PointSizeDemoted != 0 : m_frontend->PointSizeDemoted(); }
Bool GetSpirvValidationEnabled() const {
return IsWire() ? m_record->Desc.EnableSpirvValidation != 0 : m_frontend->GetSpirvValidationEnabled();
}
const Vector<Vector<Uint>>& GetGeneratedSpirv() const {
return IsWire() ? m_record->Archive->Spirv.generatedSpirv : m_frontend->GetGeneratedSpirv();
}
Vector<ShaderStage> GetLinkedShaderStages() const {
if (!IsWire()) return m_frontend->GetLinkedShaderStages();
Vector<ShaderStage> result;
for (const auto stage : m_record->Archive->LinkedStages) result.push_back(static_cast<ShaderStage>(stage));
return result;
}
Bool HasLinkedShaderStage(ShaderStage stage) const {
if (!IsWire()) return m_frontend->HasLinkedShaderStage(stage);
const auto& stages = m_record->Archive->LinkedStages;
return std::find(stages.begin(), stages.end(), static_cast<Uint32>(stage)) != stages.end();
}
Bool GetBackendHashMemo(Uint flags, Uint64& hash) const {
// A view has no lifetime in which a memo can safely persist. The server
// factory's content cache still owns compiled programs; hash from current
// archive + binding tails rather than borrowing the client's mutable memo.
return !IsWire() && m_frontend->GetBackendHashMemo(flags, hash);
}
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
if (!IsWire()) m_frontend->SetBackendHashMemo(flags, hash);
}
Uint64 GetBackendStateVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetBackendStateVersion(); }
Uint64 GetBlockBindingVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetBlockBindingVersion(); }
Uint64 GetImageUnitVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetImageUnitVersion(); }
Uint64 GetLinkVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetLinkVersion(); }
const void* GetUBOData() const { return IsWire() ? m_record->GlobalConstants.data() : m_frontend->GetUBOData(); }
Uint GetUBOSize() const { return IsWire() ? static_cast<Uint>(m_record->GlobalConstants.size()) : m_frontend->GetUBOSize(); }
Uint32 GetUBOContentVersion() const { return IsWire() ? m_record->GlobalConstantsVersion : m_frontend->GetUBOContentVersion(); }
Int GetActiveUniformBlocksCount() const {
return IsWire() ? static_cast<Int>(Link().glBlockIndexToTProgram.size()) : m_frontend->GetActiveUniformBlocksCount();
}
Uint GetUniformBlockIndex(const char* name) const {
if (!IsWire()) return m_frontend->GetUniformBlockIndex(name);
auto found = Link().uniformBlockIndexByName.find(name);
if (found == Link().uniformBlockIndexByName.end()) found = Link().uniformBlockIndexByName.find(String(name) + "[0]");
return found == Link().uniformBlockIndexByName.end() ? GL_INVALID_INDEX : found->second;
}
Uint GetUniformBlockBinding(Uint index) const {
if (!IsWire()) return m_frontend->GetUniformBlockBinding(index);
return index < m_record->BlockBindings.size() ? static_cast<Uint>(m_record->BlockBindings[index]) : 0;
}
const String& GetUniformBlockName(Uint index) const {
if (!IsWire()) return m_frontend->GetUniformBlockName(index);
const auto* block = Block(index);
static const String empty;
return block ? block->name : empty;
}
Uint GetUBOSizeAt(Uint index) const {
if (!IsWire()) return m_frontend->GetUBOSizeAt(index);
const auto* block = Block(index);
return block ? (static_cast<Uint>(block->size) + 15u) & ~15u : 0;
}
Bool IsValidUniformLocation(Int location) const {
return IsWire() ? Program::IsValidUniformLocation(Link(), location) : m_frontend->IsValidUniformLocation(location);
}
Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
if (!IsWire()) return m_frontend->UniformLocationsAliasSameUniform(a, b);
return IsValidUniformLocation(a) && IsValidUniformLocation(b) &&
Link().uniformIndexInTProgram[a] == Link().uniformIndexInTProgram[b];
}
GLenum GetUniformType(Uint location) const {
if (!IsWire()) return m_frontend->GetUniformType(location);
return IsValidUniformLocation(static_cast<Int>(location))
? Program::UniformAtIn(Link(), Link().uniformIndexInTProgram[location]).glDefineType : 0;
}
Int GetUniformSamplerOrImageUnitIndex(Uint location) const {
if (!IsWire()) return m_frontend->GetUniformSamplerOrImageUnitIndex(location);
const auto& units = m_record->SamplerUnits;
const auto found = std::lower_bound(units.begin(), units.end(), location,
[](const MG_Pipe::MGPProgramSamplerUnit& unit, Uint value) { return unit.Location < value; });
return found != units.end() && found->Location == location ? found->Unit : -1;
}
Int GetUniformLocation(const String& name) const {
if (!IsWire()) return m_frontend->GetUniformLocation(name);
const auto& locations = Link().uniformLocations;
auto found = locations.find(name);
if (found != locations.end()) return static_cast<Int>(found->second);
if (name.empty()) return -1;
found = locations.find(name + "[0]");
if (found != locations.end()) return static_cast<Int>(found->second);
if (name.back() != ']' || name.size() < 4) return -1;
const SizeT bracket = name.rfind('[');
if (bracket == String::npos || bracket + 1 >= name.size() - 1) return -1;
Uint element = 0;
for (SizeT i = bracket + 1; i < name.size() - 1; ++i) {
if (name[i] < '0' || name[i] > '9') return -1;
element = element * 10 + static_cast<Uint>(name[i] - '0');
if (element > 0x0fffffffu) return -1;
}
found = locations.find(name.substr(0, bracket) + "[0]");
if (found == locations.end()) found = locations.find(name.substr(0, bracket));
if (found == locations.end()) return -1;
const Int base = static_cast<Int>(found->second);
if (!IsValidUniformLocation(base)) return -1;
const Int tIndex = Link().uniformIndexInTProgram[base];
if (!Program::UniformAtIn(Link(), tIndex).type.isArray ||
static_cast<GLint>(element) >= Program::GetUniformArraySizeByTIndex(Link(), tIndex)) return -1;
const Int location = base + static_cast<Int>(element);
return UniformLocationsAliasSameUniform(base, location) ? location : -1;
}
SizeT GetTransformFeedbackVaryingCount() const { return IsWire() ? Link().xfbVaryings.size() : m_frontend->GetTransformFeedbackVaryingCount(); }
const Vector<MG_State::GLState::XfbVarying>& GetTransformFeedbackVaryings() const {
return IsWire() ? Link().xfbVaryings : m_frontend->GetTransformFeedbackVaryings();
}
SizeT GetTransformFeedbackBufferCount() const { return IsWire() ? Link().xfbStrides.size() : m_frontend->GetTransformFeedbackBufferCount(); }
Uint32 GetTransformFeedbackStride(Uint index) const {
if (!IsWire()) return m_frontend->GetTransformFeedbackStride(index);
return index < Link().xfbStrides.size() ? Link().xfbStrides[index] : 0;
}
GLenum GetTransformFeedbackBufferMode() const {
return IsWire() ? Link().xfbBufferMode : m_frontend->GetTransformFeedbackBufferMode();
}
Uint GetShaderStorageBlockIndex(const String& name) const {
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockIndex(*m_frontend, name);
EnsureStorageBlocks();
String key = name;
auto find = [&] {
return std::find_if(m_storageBlocks.begin(), m_storageBlocks.end(),
[&](const StorageBlock& block) { return block.Name == key; });
};
auto found = find();
if (found == m_storageBlocks.end() && !name.empty() && name.back() == ']') {
const auto bracket = name.rfind('[');
if (bracket != String::npos) { key = name.substr(0, bracket); found = find(); }
}
return found == m_storageBlocks.end() ? GL_INVALID_INDEX : static_cast<Uint>(found - m_storageBlocks.begin());
}
Uint GetShaderStorageBlockBinding(Uint index) const {
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockBinding(*m_frontend, index);
EnsureStorageBlocks();
if (index >= m_storageBlocks.size()) return 0;
const auto& block = m_storageBlocks[index];
for (const auto& override : m_record->StorageOverrides) {
if (override.Name == block.Name) return override.Binding;
}
return block.Binding;
}
private:
const MG_State::GLState::LinkArtifacts& Link() const { return m_record->Archive->Link; }
const MG_State::GLState::BlockReflection* Block(Uint index) const {
if (index >= Link().glBlockIndexToTProgram.size()) return nullptr;
const Int tIndex = Link().glBlockIndexToTProgram[index];
return tIndex >= 0 && static_cast<SizeT>(tIndex) < Link().blockReflection.size()
? &Link().blockReflection[tIndex] : nullptr;
}
struct StorageBlock { String Name; Uint Binding; };
void EnsureStorageBlocks() const {
if (m_storageBlocksReady) return;
m_storageBlocksReady = true;
// Match the legacy resource-query index order: stage order, then
// normalized descriptor name/binding, deduplicated across stages.
for (const auto& spirv : GetGeneratedSpirv()) {
if (spirv.empty()) continue;
SpvReflectShaderModule module{};
if (spvReflectCreateShaderModule(spirv.size() * sizeof(Uint), spirv.data(), &module) != SPV_REFLECT_RESULT_SUCCESS) continue;
Uint32 count = 0;
if (spvReflectEnumerateDescriptorBindings(&module, &count, nullptr) == SPV_REFLECT_RESULT_SUCCESS) {
Vector<SpvReflectDescriptorBinding*> bindings(count);
if (spvReflectEnumerateDescriptorBindings(&module, &count, bindings.data()) == SPV_REFLECT_RESULT_SUCCESS) {
auto name = [](const SpvReflectDescriptorBinding* binding) {
const char* raw = binding->name;
if (binding->type_description && binding->type_description->type_name) raw = binding->type_description->type_name;
String result = raw ? raw : "";
const auto suffix = result.find("[0]");
return suffix == String::npos ? result : result.substr(0, suffix);
};
std::sort(bindings.begin(), bindings.end(), [&](const auto* a, const auto* b) {
const String an = a ? name(a) : String(), bn = b ? name(b) : String();
return an != bn ? an < bn : (a && b && a->binding < b->binding);
});
for (const auto* binding : bindings) {
if (!binding || binding->descriptor_type != SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_BUFFER) continue;
const String key = name(binding);
if (key.empty() || std::any_of(m_storageBlocks.begin(), m_storageBlocks.end(),
[&](const StorageBlock& block) { return block.Name == key; })) continue;
m_storageBlocks.push_back({key, binding->binding});
}
}
}
spvReflectDestroyShaderModule(&module);
}
}
const Program* m_frontend = nullptr;
MG_Pipe::MGPipeHandle m_handle = MG_Pipe::kMGPipeNullHandle;
const MG_Pipe::MGPipeShaderCsoRecord* m_record = nullptr;
mutable Bool m_storageBlocksReady = false;
mutable Vector<StorageBlock> m_storageBlocks;
};
#else
using MagmaProgramSource = MG_State::GLState::ProgramObject;
#endif
}
@@ -1891,7 +1891,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Bool TransformSpirvForXfbCapture(const Vector<Uint>& input, Vector<Uint>& output,
const MG_State::GLState::ProgramObject& program) {
const MagmaProgramSource& program) {
if (input.empty()) {
output.clear();
return true;
@@ -2466,7 +2466,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program,
ProgramFactory::HashType ProgramFactory::ComputeHash(const MagmaProgramSource& program,
CompileOptionFlags flags) const {
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
// We expect shader stages in program object are sorted
@@ -2832,7 +2832,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// key and every element of an array of arrays seeds texture unit 0. Resolving those elements
// would then paint silently-wrong pixels with no diagnostic at all - strictly worse than
// declining. The decline goes away together with the seeding fix, not before it.
static Uint32 DescriptorCountForOpaqueUniformArray(const MG_State::GLState::ProgramObject& program,
static Uint32 DescriptorCountForOpaqueUniformArray(const MagmaProgramSource& program,
const String& uniformName, Uint32 binding, Int baseLocation,
Uint32 reflectedCount, Uint32 maxBindings,
const char* kindLabel, Bool& outDeclined) {
@@ -2868,7 +2868,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return count;
}
void ProgramFactory::ReflectLayout(const MG_State::GLState::ProgramObject& program,
void ProgramFactory::ReflectLayout(const MagmaProgramSource& program,
const Vector<Vector<Uint>>& spirv, VkProgramObject& entry) const {
// Initialize layout vectors
entry.bindingKinds.assign(m_maxBindings, DescriptorBindingKind::None);
@@ -3068,7 +3068,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.bindingKinds[binding] = descriptorKind;
if (descriptorKind == DescriptorBindingKind::StorageBuffer) {
const GLuint blockIndex = GetShaderStorageBlockIndex(program, uniformName);
const GLuint blockIndex =
#if MOBILEGL_BUILD_DISAGGREGATED
program.GetShaderStorageBlockIndex(uniformName);
#else
GetShaderStorageBlockIndex(program, uniformName);
#endif
if (blockIndex == GL_INVALID_INDEX) {
MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive SSBO '%s' at binding %u",
uniformName.c_str(), binding);
@@ -3426,7 +3431,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const ProgramFactory::VkProgramObject& ProgramFactory::GetOrCreateProgram(
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) {
const MagmaProgramSource& program, CompileOptionFlags flags) {
// Hashing the full SPIR-V of every stage is far too expensive to repeat per draw;
// reuse the program's memoized hash while its backend state version is unchanged.
// The memo keys on the flags word, which ComputeHash is no longer a pure function of:
@@ -10,7 +10,7 @@
#include "../VkIncludes.h"
#include "PipelineFactory.h"
#include "MG_State/GLState/ProgramState/ProgramObject.h"
#include "MagmaProgramSource.h"
#include "MG_State/GLState/ProgramState/ShaderObject.h"
#include "MG_State/GLState/TextureState/TextureEnum.h"
@@ -471,9 +471,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
~ProgramFactory();
ProgramFactory(const ProgramFactory&) = delete;
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
HashType ComputeHash(const MagmaProgramSource& program, CompileOptionFlags flags) const;
const VkProgramObject& GetOrCreateProgram(
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
const MagmaProgramSource& program, CompileOptionFlags flags);
// The default framebuffer's current image height, baked as a literal into every
// FragCoordYFlip variant (there is no push-constant or specialization channel here, and
@@ -600,7 +600,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void ReflectFragmentOutputs(const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
void ReflectLayout(const MagmaProgramSource& program, const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
// Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked
// modules. Const and reflection-only: it decides nothing about the pipeline, it only
@@ -29,6 +29,9 @@
#include "MG_Util/Metrics/TextureMetrics.h"
#include "MG_Util/ShaderTranspiler/Types.h"
#include <Config.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/PipeApply.h>
#endif
#include <vulkan/utility/vk_format_utils.h>
#include <algorithm>
#include <cstdio>
@@ -37,6 +40,162 @@
#include <limits>
namespace MobileGL::MG_Backend::DirectVulkan {
#if MOBILEGL_BUILD_DISAGGREGATED
[[noreturn]] static void WireDescriptorFatal(const char* detail) {
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"Magma:%s\"}", detail);
std::abort();
}
[[noreturn]] static void WireBufferDescriptorDebt() {
MGLOG_F("MGPipe: Fatal{RoleViolation, \"buffer-legacy-arm\"} - Magma buffer descriptors remain P7");
std::abort();
}
static VkImageViewType WireViewType(MG_Pipe::MGPipeResourceTarget target) {
using T = MG_Pipe::MGPipeResourceTarget;
switch (target) {
case T::Tex1D: return VK_IMAGE_VIEW_TYPE_1D;
case T::Tex1DArray: return VK_IMAGE_VIEW_TYPE_1D_ARRAY;
case T::Tex3D: return VK_IMAGE_VIEW_TYPE_3D;
case T::TexCube: return VK_IMAGE_VIEW_TYPE_CUBE;
case T::TexCubeArray: return VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
case T::Tex2DArray: case T::Tex2DMSArray: return VK_IMAGE_VIEW_TYPE_2D_ARRAY;
default: return VK_IMAGE_VIEW_TYPE_2D;
}
}
static VkComponentSwizzle WireSwizzle(Uint8 value) {
switch (static_cast<TextureSwizzleParam>(value)) {
case TextureSwizzleParam::Red: return VK_COMPONENT_SWIZZLE_R;
case TextureSwizzleParam::Green: return VK_COMPONENT_SWIZZLE_G;
case TextureSwizzleParam::Blue: return VK_COMPONENT_SWIZZLE_B;
case TextureSwizzleParam::Alpha: return VK_COMPONENT_SWIZZLE_A;
case TextureSwizzleParam::Zero: return VK_COMPONENT_SWIZZLE_ZERO;
case TextureSwizzleParam::One: return VK_COMPONENT_SWIZZLE_ONE;
default: WireDescriptorFatal("sampler-swizzle");
}
}
Bool UniformManager::ResolveWireImageDescriptor(
VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element,
Bool storage, VkDescriptorImageInfo& out) const {
out = {};
auto& state = MG_Pipe::MGPipeApplier();
const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
const Int location = baseLocation + static_cast<Int>(element);
if (baseLocation < 0 || !program.UniformLocationsAliasSameUniform(baseLocation, location)) return false;
const Int unit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
if (unit < 0 || static_cast<Uint32>(unit) >=
(storage ? MG_Pipe::kMGPipeMaxImageUnits : MG_Pipe::kMGPipeMaxTextureUnits)) return false;
const auto handle = storage ? state.BoundShaderImages[unit].Res : state.BoundSamplerViews[unit].Texture;
// The old placeholder constructor enters the guarded client allocator. Keep that
// P7 boundary explicit instead of constructing a frontend texture on the server.
if (MG_Pipe::MGPipeHandleIsNull(handle)) WireDescriptorFatal("unbound-image-placeholder@P7");
if (handle.Slot >= state.TextureResources.size()) WireDescriptorFatal("image-record");
const auto& record = state.TextureResources[handle.Slot];
if (!record.Live || record.Gen != handle.Gen) WireDescriptorFatal("image-record-generation");
auto* resource = m_textureManager->SyncTextureResourceByHandle(handle);
if (!resource) WireDescriptorFatal("image-resource");
m_textureManager->FlushPendingUploads();
Uint32 level = storage ? state.BoundShaderImages[unit].Level : record.Params.BaseLevel;
Uint32 layer = storage && !state.BoundShaderImages[unit].Layered ? state.BoundShaderImages[unit].Layer : 0;
const Uint32 localLevel = level;
const Uint32 localLayer = layer;
Uint32 layers = 0;
VkFormat aliasFormat = VK_FORMAT_UNDEFINED;
const auto root = m_textureManager->ResolveWireTextureStorage(handle, level, layer, &aliasFormat, &layers);
if (MG_Pipe::MGPipeHandleIsNull(root)) WireDescriptorFatal("image-view-window");
const auto target = static_cast<MG_Pipe::MGPipeResourceTarget>(record.Desc.Target);
VkImageViewType type = WireViewType(target);
if (storage && !state.BoundShaderImages[unit].Layered) {
layers = 1;
type = target == MG_Pipe::MGPipeResourceTarget::Tex1DArray ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D;
}
if (type == VK_IMAGE_VIEW_TYPE_3D) layers = 1;
Uint32 levels = 1;
if (!storage) {
if (record.Params.MaxLevel < localLevel || record.Desc.Levels <= localLevel) return false;
levels = std::min<Uint32>(record.Desc.Levels - localLevel,
record.Params.MaxLevel - localLevel + 1);
const auto viewHandle = state.BoundSamplerViews[unit].View;
if (!MG_Pipe::MGPipeHandleIsNull(viewHandle) && viewHandle.Slot < state.SamplerViewCsos.size()) {
const auto& view = state.SamplerViewCsos[viewHandle.Slot];
if (!view.Live || view.Gen != viewHandle.Gen || view.View.NumLevels <= localLevel)
WireDescriptorFatal("sampler-view-record");
levels = std::min<Uint32>(levels, view.View.NumLevels - localLevel);
}
levels = std::min(levels, resource->mipLevels - level);
}
VkFormat format = aliasFormat == VK_FORMAT_UNDEFINED ? resource->format : aliasFormat;
const auto domain = programObj.samplerNumericDomainByBinding[binding];
if (storage) {
format = ResolveStorageImageViewFormat(programObj.storageImageFormatByBinding[binding],
state.BoundShaderImages[unit].InternalFormat, format,
programObj.storageImageUsesBindingFormatByBinding[binding]);
} else if (resource->aspect == VK_IMAGE_ASPECT_COLOR_BIT) {
format = VkTextureManager::ResolveSampledImageViewFormat(format, domain);
}
if (format == VK_FORMAT_UNDEFINED) return false;
VkImageAspectFlags aspect = resource->aspect;
if (!storage && (aspect & VK_IMAGE_ASPECT_DEPTH_BIT))
aspect = record.Params.DepthStencilMode == MG_Pipe::kMGPipeDepthStencilModeStencil
? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT;
// All wire image descriptors use GENERAL. A texture sampled and image-bound in
// the same shader then has one truthful layout, including views of the same owner.
if (!VkTextureManager::TransitionImageLayout(commandBuffer, resource->image, resource->layout,
VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
resource->layout == VK_IMAGE_LAYOUT_UNDEFINED ? 0 : VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT,
VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, resource->aspect, 0, resource->mipLevels))
WireDescriptorFatal("image-descriptor-transition");
// A second descriptor/draw can keep GENERAL after a shader write. A layout
// equality fast return is not a memory dependency for that prior write.
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
0, 1, &memory, 0, nullptr, 0, nullptr);
VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
info.image = resource->image;
info.viewType = type;
info.format = format;
info.subresourceRange = {aspect, level, levels, layer, layers};
if (!storage) {
const Bool alphaIsOne = ResolveTextureFormatInfo(
static_cast<TextureInternalFormat>(record.Desc.InternalFormat)).expandRgbToRgba;
const auto swizzle = [&](Uint32 channel) {
const auto value = record.Params.Swizzle[channel];
return alphaIsOne && static_cast<TextureSwizzleParam>(value) == TextureSwizzleParam::Alpha
? VK_COMPONENT_SWIZZLE_ONE : WireSwizzle(value);
};
info.components = {swizzle(0), swizzle(1), swizzle(2), swizzle(3)};
}
VkImageView view = VK_NULL_HANDLE;
if (vkCreateImageView(m_device, &info, nullptr, &view) != VK_SUCCESS) return false;
m_frames[m_wireFrameIndex].wireImageViews.push_back(view);
out.imageView = view;
out.imageLayout = VK_IMAGE_LAYOUT_GENERAL;
if (storage) {
if (state.BoundShaderImages[unit].Access != kMGPipeImageAccessReadOnly)
m_textureManager->MarkWireTextureGpuWritten(handle, localLevel, localLayer, layers);
} else {
auto samplerHandle = state.BoundSamplerStates[unit];
if (MG_Pipe::MGPipeHandleIsNull(samplerHandle)) samplerHandle = record.Params.BuiltinSampler;
if (MG_Pipe::MGPipeHandleIsNull(samplerHandle) || samplerHandle.Slot >= state.SamplerCsos.size())
WireDescriptorFatal("sampler-record");
const auto& sampler = state.SamplerCsos[samplerHandle.Slot];
if (!sampler.Live || sampler.Gen != samplerHandle.Gen) WireDescriptorFatal("sampler-record-generation");
out.sampler = m_samplerManager->GetOrCreateSamplerFromParameters(sampler.Params,
static_cast<TextureInternalFormat>(record.Desc.InternalFormat),
domain == SamplerNumericDomain::SignedInteger || domain == SamplerNumericDomain::UnsignedInteger,
levels);
if (!out.sampler) return false;
}
return true;
}
#endif
namespace {
constexpr Uint kFallbackTexture2DExternalIndex = 0xFFFFFF00u;
// One id for every storage-image placeholder. They are never reachable through GL - no
@@ -234,7 +393,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// per array element, so the element's location is the base plus its index - bounded by the
// array's real extent so a descriptorCount that outran the reflection cannot walk onto the
// next uniform. Element 0 is the ordinary non-array case and costs nothing extra.
static Int ResolveDescriptorElementLocation(const MG_State::GLState::ProgramObject& program, Int baseLocation,
static Int ResolveDescriptorElementLocation(const MagmaProgramSource& program, Int baseLocation,
Uint32 element) {
if (baseLocation < 0 || element == 0) {
return baseLocation;
@@ -252,7 +411,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
: 1u;
}
static Int ResolveSamplerUnitIndex(const MG_State::GLState::ProgramObject& program, Int location, Uint32 binding) {
static Int ResolveSamplerUnitIndex(const MagmaProgramSource& program, Int location, Uint32 binding) {
MOBILEGL_ASSERT(location >= -1, "ResolveSamplerUnitIndex: invalid sampler location for binding %u", binding);
if (location < 0) {
return 0;
@@ -349,6 +508,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_unboundStorageImageTextures.clear();
for (auto& frame : m_frames) {
if (m_device != VK_NULL_HANDLE) {
#if MOBILEGL_BUILD_DISAGGREGATED
for (const auto view : frame.wireImageViews) vkDestroyImageView(m_device, view, nullptr);
frame.wireImageViews.clear();
#endif
for (auto& view : frame.texelBufferViews) {
if (view != VK_NULL_HANDLE) {
vkDestroyBufferView(m_device, view, nullptr);
@@ -390,6 +553,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void UniformManager::BeginFrame(Uint32 frameIndex) {
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "UniformDescriptorBinder::BeginFrame invalid frame index");
auto& frame = m_frames[frameIndex];
#if MOBILEGL_BUILD_DISAGGREGATED
m_wireFrameIndex = frameIndex;
for (const auto view : frame.wireImageViews) vkDestroyImageView(m_device, view, nullptr);
frame.wireImageViews.clear();
#endif
for (auto& view : frame.texelBufferViews) {
if (view != VK_NULL_HANDLE) {
vkDestroyBufferView(m_device, view, nullptr);
@@ -467,11 +635,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 binding, Uint32 element,
VkDescriptorImageInfo& outImageInfo,
Bool trustUnchangedHint) const {
#if MOBILEGL_BUILD_DISAGGREGATED
if (program.IsWire()) return ResolveWireImageDescriptor(commandBuffer, program, programObj, binding, element, false, outImageInfo);
#endif
MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveSamplerDescriptor: texture manager is null");
MOBILEGL_ASSERT(m_samplerManager != nullptr, "ResolveSamplerDescriptor: sampler manager is null");
// The whole-descriptor memo below is keyed by binding alone, so it describes a binding
@@ -753,7 +924,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Bool UniformManager::ProgramSamplesOnlySingleLevelTextures(
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) {
const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (program.IsWire()) return false; // Conservative optimization gate; no frontend probe.
#endif
// A declined program never draws (see VkProgramObject::declinedDescriptors), and its
// declined binding has no resolvable uniform location - so there is nothing to prove
// about the textures it would have sampled.
@@ -810,7 +984,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return sawSampler;
}
Bool UniformManager::ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
Bool UniformManager::ResolveSamplerTexture(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
outTexture.reset();
@@ -837,7 +1011,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
MG_State::GLState::ITextureObject* UniformManager::ResolveSamplerTextureRaw(
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj,
const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj,
Uint32 binding, Uint32 element) {
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveSamplerTextureRaw: GL context is null");
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
@@ -864,10 +1038,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return texture;
}
Bool UniformManager::ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
Bool UniformManager::ResolveTexelBufferDescriptor(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 binding, Uint32 frameIndex,
VkBufferView& outBufferView) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (program.IsWire()) {
const Int location = programObj.samplerUniformLocationByBinding[binding];
const Int unit = location < 0 ? 0 : program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
const auto& state = MG_Pipe::MGPipeApplier();
if (unit < 0 || static_cast<Uint32>(unit) >= MG_Pipe::kMGPipeMaxTextureUnits) return false;
const auto handle = state.BoundSamplerViews[unit].Texture;
if (!MG_Pipe::MGPipeHandleIsNull(handle)) {
if (handle.Slot >= state.TextureResources.size()) WireDescriptorFatal("texel-buffer-record");
const auto& record = state.TextureResources[handle.Slot];
if (!record.Live || record.Gen != handle.Gen) WireDescriptorFatal("texel-buffer-generation");
if (!MG_Pipe::MGPipeHandleIsNull(record.Desc.BufferForTexBuffer)) WireBufferDescriptorDebt();
}
outBufferView = AcquireUnboundTexelBufferView(VK_FORMAT_UNDEFINED,
programObj.samplerNumericDomainByBinding[binding], false);
return outBufferView != VK_NULL_HANDLE;
}
#endif
outBufferView = VK_NULL_HANDLE;
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveTexelBufferDescriptor: buffer manager is null");
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "ResolveTexelBufferDescriptor: frame index out of range");
@@ -988,10 +1180,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// binding at all for a uniform the shader still read, and lavapipe segfaulted inside pipeline
// creation on the JIT worker thread. KHR-GL44.multi_bind.dispatch_bind_image_textures is the
// case that carries it.
Bool UniformManager::ResolveStorageTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
Bool UniformManager::ResolveStorageTexelBufferDescriptor(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 binding, Uint32 frameIndex,
VkBufferView& outBufferView) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (program.IsWire()) {
const Int location = programObj.samplerUniformLocationByBinding[binding];
const Int unit = location < 0 ? 0 : program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
const auto& state = MG_Pipe::MGPipeApplier();
if (unit < 0 || static_cast<Uint32>(unit) >= MG_Pipe::kMGPipeMaxImageUnits) return false;
const auto handle = state.BoundShaderImages[unit].Res;
if (!MG_Pipe::MGPipeHandleIsNull(handle)) {
if (handle.Slot >= state.TextureResources.size()) WireDescriptorFatal("texel-buffer-record");
const auto& record = state.TextureResources[handle.Slot];
if (!record.Live || record.Gen != handle.Gen) WireDescriptorFatal("texel-buffer-generation");
if (!MG_Pipe::MGPipeHandleIsNull(record.Desc.BufferForTexBuffer)) WireBufferDescriptorDebt();
}
outBufferView = AcquireUnboundTexelBufferView(programObj.storageImageFormatByBinding[binding],
programObj.samplerNumericDomainByBinding[binding], true);
return outBufferView != VK_NULL_HANDLE;
}
#endif
outBufferView = VK_NULL_HANDLE;
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageTexelBufferDescriptor: buffer manager is null");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveStorageTexelBufferDescriptor: GL context is null");
@@ -1154,7 +1364,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
Bool UniformManager::ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
Bool UniformManager::ResolveStorageBufferDescriptor(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 binding, Uint32 element,
VkDescriptorBufferInfo& outBufferInfo) const {
@@ -1195,7 +1405,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const GLuint frontendBinding =
isAtomicCounterBlock
? static_cast<GLuint>(atomicCounterBinding)
: GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex)) + element;
:
#if MOBILEGL_BUILD_DISAGGREGATED
(program.IsWire() ? program.GetShaderStorageBlockBinding(static_cast<GLuint>(blockIndex)) :
GetShaderStorageBlockBinding(*program.Frontend(), static_cast<GLuint>(blockIndex))) + element;
#else
GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex)) + element;
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
if (program.IsWire()) {
const auto& state = MG_Pipe::MGPipeApplier();
const Uint32 cls = isAtomicCounterBlock ? 2u : 1u;
if (frontendBinding < state.ShaderBufferCount[cls] &&
!MG_Pipe::MGPipeHandleIsNull(state.BoundShaderBuffers[cls][frontendBinding].Res))
WireBufferDescriptorDebt();
const BufferSlice placeholder = m_bufferManager->AcquireUnboundStorageDescriptor();
if (!placeholder.IsValid()) return false;
outBufferInfo = {placeholder.buffer, placeholder.offset, placeholder.size};
return true;
}
#endif
const Uint32 bindingPointCount =
static_cast<Uint32>(MGB_CTX->GetBufferBindingPointCount(bufferTarget));
MOBILEGL_ASSERT(frontendBinding < bindingPointCount,
@@ -1274,10 +1503,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Bool UniformManager::ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 binding, Uint32 element,
VkDescriptorImageInfo& outImageInfo) const {
#if MOBILEGL_BUILD_DISAGGREGATED
if (program.IsWire()) return ResolveWireImageDescriptor(commandBuffer, program, programObj, binding, element, true, outImageInfo);
#endif
outImageInfo = {};
MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveStorageImageDescriptor: texture manager is null");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveStorageImageDescriptor: GL context is null");
@@ -1657,7 +1889,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return texture;
}
Bool UniformManager::ResolveSampledBinding(const MG_State::GLState::ProgramObject& program,
Bool UniformManager::ResolveSampledBinding(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 binding, Uint32 element,
MG_State::GLState::ITextureObject*& outTexture,
@@ -1726,10 +1958,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
Bool UniformManager::CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
Bool UniformManager::CollectSampledTextures(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures,
Vector<SampledBindingRecord>* outBindingRecords) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (program.IsWire()) { outTextures.clear(); if (outBindingRecords) outBindingRecords->clear(); return true; }
#endif
outTextures.clear();
if (outBindingRecords != nullptr) {
outBindingRecords->clear();
@@ -1773,9 +2008,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
Bool UniformManager::SampledBindingsUnchanged(const MG_State::GLState::ProgramObject& program,
Bool UniformManager::SampledBindingsUnchanged(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
const Vector<SampledBindingRecord>& previousRecords) const {
#if MOBILEGL_BUILD_DISAGGREGATED
if (program.IsWire()) return false;
#endif
// A declined program takes the full path every time and is refused there.
if (programObj.declinedDescriptors) {
return false;
@@ -1816,9 +2054,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Bool UniformManager::CollectStorageImageTextures(
const MG_State::GLState::ProgramObject& program,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures) const {
#if MOBILEGL_BUILD_DISAGGREGATED
if (program.IsWire()) { outTextures.clear(); return true; }
#endif
outTextures.clear();
MOBILEGL_ASSERT(MGB_CTX_LIVE,
"CollectStorageImageTextures: GL context is null");
@@ -1898,9 +2139,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Bool UniformManager::CollectSamplerImageFeedback(
const MG_State::GLState::ProgramObject& program,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<SamplerImageFeedbackBinding>& outBindings) const {
#if MOBILEGL_BUILD_DISAGGREGATED
if (program.IsWire()) { outBindings.clear(); return true; }
#endif
outBindings.clear();
MOBILEGL_ASSERT(MGB_CTX_LIVE,
"CollectSamplerImageFeedback: GL context is null");
@@ -1976,7 +2220,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
Bool UniformManager::ResolveUniformBufferPayload(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 arrayElement, UboBindResult& out) const {
const void* outData = nullptr;
@@ -2002,6 +2246,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return outData != nullptr && outSize > 0;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// Global uniforms above are record bytes; named UBOs retain P7's buffer boundary.
if (program.IsWire()) WireBufferDescriptorDebt();
#endif
MOBILEGL_ASSERT(binding < programObj.uniformBlockIndexByBinding.size(),
"ResolveUniformBufferPayload: UBO mapping binding %u out of range", binding);
Int blockIndex = programObj.uniformBlockIndexByBinding[binding];
@@ -2265,7 +2514,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return VK_SUCCESS;
}
Bool UniformManager::ResolveDynamicUboDescriptor(const MG_State::GLState::ProgramObject& program,
Bool UniformManager::ResolveDynamicUboDescriptor(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 binding, Uint32 arrayElement, Uint32 frameIndex,
VkBuffer& outBuffer, VkDeviceSize& outRange,
@@ -2361,7 +2610,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Bool UniformManager::BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 frameIndex,
VkPipelineBindPoint bindPoint,
@@ -9,6 +9,7 @@
#pragma once
#include "ProgramFactory.h"
#include "MagmaProgramSource.h"
#include "VkBufferManager.h"
#include "VkSamplerManager.h"
#include "VkTextureManager.h"
@@ -78,7 +79,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 textureLifetimeId = 0;
Uint64 samplerLifetimeId = 0;
};
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
Bool CollectSampledTextures(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures,
Vector<SampledBindingRecord>* outBindingRecords = nullptr);
@@ -87,14 +88,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// (texture, effective sampler) pair. A texture bind generation bump alone (e.g. a
// redundant glBindSampler, which always bumps it) does not prove the sampled set
// moved; this walk does, without rebuilding the set or falling off the fast path.
Bool SampledBindingsUnchanged(const MG_State::GLState::ProgramObject& program,
Bool SampledBindingsUnchanged(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
const Vector<SampledBindingRecord>& previousRecords) const;
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
Bool CollectStorageImageTextures(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
Bool CollectSamplerImageFeedback(
const MG_State::GLState::ProgramObject& program,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<SamplerImageFeedbackBinding>& outBindings) const;
static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
@@ -106,7 +107,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// epochs AND per-resource layout values - so the per-binding cached
// VkDescriptorImageInfo may be reused without re-running the resolve chain.
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 frameIndex,
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
@@ -127,7 +128,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
// only GL state, so a texture that ends up single-level for another reason (one uploaded
// level under a wide level range) merely misses the rewrite.
static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program,
static Bool ProgramSamplesOnlySingleLevelTextures(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj);
private:
@@ -155,12 +156,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<DescriptorPoolBucket> descriptorPools;
UnorderedMap<VkDescriptorSetLayout, DescriptorSetCacheEntry> descriptorSetCacheByLayout;
Vector<VkBufferView> texelBufferViews;
#if MOBILEGL_BUILD_DISAGGREGATED
mutable Vector<VkImageView> wireImageViews;
#endif
Uint32 activeDescriptorPoolIndex = 0;
Uint32 allocatedSetsThisFrame = 0;
Uint32 peakAllocatedSetsThisFrame = 0;
};
static Bool ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
static Bool ResolveSamplerTexture(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
// Shared per-binding resolution for CollectSampledTextures and
@@ -169,7 +173,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// exists), effective sampler = unit override else the texture's own sampler.
// False = the binding is skipped (unbound with a non-2D fallback target).
// `element` indexes a sampler array inside the binding; see ResolveSamplerDescriptor.
Bool ResolveSampledBinding(const MG_State::GLState::ProgramObject& program,
Bool ResolveSampledBinding(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element,
MG_State::GLState::ITextureObject*& outTexture,
const MG_State::GLState::SamplerObject*& outSampler) const;
@@ -177,7 +181,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// the bound texture stays alive through the draw via GL binding state, so callers that
// only need the pointer skip the SharedPtr copy's atomic refcount churn.
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
const MG_State::GLState::ProgramObject& program,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
// `numericDomain` is the sampler's class, and it matters only for the multisample arm -
// see GetFallbackMultisampleTexture for why the single-sampled fallback can ignore it.
@@ -224,13 +228,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof
// obligations the caller carries). The cache is keyed by binding alone, so it is
// used ONLY for single-descriptor bindings - see m_samplerResolveMemo.
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program,
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 element, VkDescriptorImageInfo& outImageInfo,
Bool trustUnchangedHint = false) const;
#if MOBILEGL_BUILD_DISAGGREGATED
Bool ResolveWireImageDescriptor(VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 element, Bool storage, VkDescriptorImageInfo& out) const;
Uint32 m_wireFrameIndex = 0;
#endif
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
VkDescriptorImageInfo& outImageInfo) const;
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
Bool ResolveTexelBufferDescriptor(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 frameIndex, VkBufferView& outBufferView);
// GLSL `imageBuffer`: the same VkBufferView descriptor as the sampled texel buffer above,
@@ -238,19 +248,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// made GPU-resident-writable because the shader may store to it. No `element` parameter:
// an imageBuffer ARRAY is refused at program creation, so a binding is always one
// descriptor (see the array gate in RemapDescriptorBindingsForVulkan).
Bool ResolveStorageTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
Bool ResolveStorageTexelBufferDescriptor(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 frameIndex, VkBufferView& outBufferView);
// `element` indexes a block INSTANCE array's descriptors; it is 0 for every ordinary
// block. Each element resolves through its own GL storage block, and so its own GL
// binding point, buffer and glBindBufferRange window.
Bool ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
Bool ResolveStorageBufferDescriptor(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 element, VkDescriptorBufferInfo& outBufferInfo) const;
// `element` indexes an image ARRAY inside one binding; each element carries its own
// independently assigned GL image unit.
Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 element, VkDescriptorImageInfo& outImageInfo) const;
// Result of resolving a UBO binding: either a zero-copy direct bind to the app's resident
@@ -263,14 +273,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const void* payload = nullptr; // fallback UploadTransient path
VkDeviceSize payloadSize = 0;
};
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
Bool ResolveUniformBufferPayload(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 arrayElement, UboBindResult& out) const;
// Shared resolution of one dynamic-UBO binding element into the
// (buffer, range, dynamicOffset) triple the descriptor consumes: direct
// bind, global-slice reuse, or transient upload. Used by the full walk
// and by the dynamic-offset-only rebind (see FastRebindMemo).
Bool ResolveDynamicUboDescriptor(const MG_State::GLState::ProgramObject& program,
Bool ResolveDynamicUboDescriptor(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 arrayElement, Uint32 frameIndex, VkBuffer& outBuffer,
VkDeviceSize& outRange, Uint32& outDynamicOffset);
@@ -15,7 +15,37 @@
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
#if MOBILEGL_BUILD_DISAGGREGATED
// An accessor view over the wire's POD values, not a GL object with constructor,
// lifetime ID, allocator hooks or mutation callbacks. Both sources below use
// one sampler policy, including format-sensitive border-color conversion.
struct SamplerParametersSource {
const SamplerParameters& Params;
SamplerWrapMode GetWrapS() const { return Params.wrapS; }
SamplerWrapMode GetWrapT() const { return Params.wrapT; }
SamplerWrapMode GetWrapR() const { return Params.wrapR; }
SamplerFilterMode GetMinFilter() const { return Params.minFilter; }
SamplerFilterMode GetMagFilter() const { return Params.magFilter; }
SamplerMipmapMode GetMipmapMode() const { return Params.mipmapMode; }
Float GetMinLod() const { return Params.minLod; }
Float GetMaxLod() const { return Params.maxLod; }
Float GetLodBias() const { return Params.lodBias; }
Float GetMaxAnisotropy() const { return Params.maxAnisotropy; }
SamplerCompareMode GetCompareMode() const { return Params.compareMode; }
SamplerCompareFunc GetSamplerCompareFunc() const { return Params.compareFunc; }
const FloatVec4& GetBorderColor() const { return Params.borderColor; }
const IntVec4& GetBorderColorI() const { return Params.borderColorI; }
const UintVec4& GetBorderColorUI() const { return Params.borderColorUI; }
// Cache identity is entirely content-based; these fields are diagnostics.
Uint GetExternalIndex() const { return 0; }
Uint16 GetVersion() const { return 0; }
};
template <class SamplerSource>
Bool UsesBorderColor(const SamplerSource& sampler) {
#else
Bool UsesBorderColor(const MG_State::GLState::SamplerObject& sampler) {
#endif
return sampler.GetWrapS() == SamplerWrapMode::ClampToBorder ||
sampler.GetWrapT() == SamplerWrapMode::ClampToBorder ||
sampler.GetWrapR() == SamplerWrapMode::ClampToBorder;
@@ -191,14 +221,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return std::fabs(lhs - rhs) <= 1e-6f;
}
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
Float ResolveEffectiveMaxLod(const SamplerSource& sampler) {
#else
Float ResolveEffectiveMaxLod(const MG_State::GLState::SamplerObject& sampler) {
#endif
if (sampler.GetMipmapMode() == SamplerMipmapMode::None) {
return 0.0f;
}
return sampler.GetMaxLod();
}
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
Float ResolveEffectiveMinLod(const SamplerSource& sampler, Float effectiveMaxLod) {
#else
Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) {
#endif
return std::min(sampler.GetMinLod(), effectiveMaxLod);
}
@@ -209,7 +249,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// GL's non-mipmapped minification - large enough for lambda to stay positive, small enough
// that a NEAREST mip mode still rounds down to level 0. Clamped rather than assigned, so a
// texture whose GL_TEXTURE_MAX_LOD really is 0 keeps magnifying as GL says it must.
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
Float ResolveSingleLevelMaxLod(const SamplerSource& sampler, Bool singleLevelView) {
#else
Float ResolveSingleLevelMaxLod(const MG_State::GLState::SamplerObject& sampler, Bool singleLevelView) {
#endif
const Float maxLod = ResolveEffectiveMaxLod(sampler);
return singleLevelView ? std::min(maxLod, 0.25f) : maxLod;
}
@@ -230,7 +275,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const SamplerSource& sampler,
#else
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler,
#endif
Bool forceNearestFiltering) const {
if (!m_samplerAnisotropySupported) return 1.0f;
if (forceNearestFiltering) return 1.0f;
@@ -289,7 +339,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
Uint64 VkSamplerManager::BuildSamplerKey(const SamplerSource& sampler,
#else
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
#endif
Bool forceNearestFiltering, Bool singleLevelView,
const ResolvedBorderColor& borderColor) const {
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
@@ -335,8 +390,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return XXH64_digest(m_hashState);
}
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
VkSampler VkSamplerManager::GetOrCreateSamplerImpl(const SamplerSource& sampler,
TextureInternalFormat format,
#else
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
#endif
Bool forceNearestFiltering, Uint32 viewLevelCount) {
// A view that exposes a single mip level has no second level to blend with, so GL's
// *_MIPMAP_* minification filters degenerate to plain filtering on the base level -
@@ -348,7 +409,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// the default-framebuffer blit shader had to work around with an explicit-LOD sample.
const Bool singleLevelView = viewLevelCount == 1;
// Resolved once and used for both the key and the create-info; see ResolvedBorderColor.
#if MOBILEGL_BUILD_DISAGGREGATED
const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, format);
#else
const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, texture);
#endif
const Uint64 key = BuildSamplerKey(sampler, forceNearestFiltering, singleLevelView, borderColor);
auto it = m_samplers.find(key);
if (it != m_samplers.end()) {
@@ -408,6 +473,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return vkSampler;
}
#if MOBILEGL_BUILD_DISAGGREGATED
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering, Uint32 viewLevelCount) {
return GetOrCreateSamplerImpl(sampler, texture.GetFormat(), forceNearestFiltering, viewLevelCount);
}
VkSampler VkSamplerManager::GetOrCreateSamplerFromParameters(const SamplerParameters& parameters,
TextureInternalFormat format, Bool forceNearestFiltering, Uint32 viewLevelCount) {
return GetOrCreateSamplerImpl(SamplerParametersSource{parameters}, format,
forceNearestFiltering, viewLevelCount);
}
#endif
VkFilter VkSamplerManager::ToVkFilter(SamplerFilterMode mode) {
return mode == SamplerFilterMode::Nearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR;
}
@@ -463,8 +542,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
const SamplerSource& sampler, TextureInternalFormat format) const {
#else
VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture) const {
#endif
ResolvedBorderColor resolved{};
if (!UsesBorderColor(sampler)) {
return resolved; // FLOAT_TRANSPARENT_BLACK, never sampled
@@ -472,7 +557,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Border colour is sampler state: a bound sampler object supplies its own, and a texture
// with none reaches the very same value through the sampler object it owns.
#if !MOBILEGL_BUILD_DISAGGREGATED
const auto format = texture.GetFormat();
#endif
const auto domain = ResolveBorderColorDomain(format);
const Bool canUseCustom = m_customBorderColorSupported && m_maxCustomBorderColorSamplers > 0 &&
m_customBorderColorSamplerCount < m_maxCustomBorderColorSamplers;
@@ -46,6 +46,12 @@ public:
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering = false,
Uint32 viewLevelCount = 0);
#if MOBILEGL_BUILD_DISAGGREGATED
// The applier owns these values: effective bound/builtin sampler parameters and
// the sampled texture view's format/range. No frontend object is constructed.
VkSampler GetOrCreateSamplerFromParameters(const SamplerParameters& parameters,
TextureInternalFormat format, Bool forceNearestFiltering = false, Uint32 viewLevelCount = 0);
#endif
// Frame boundary hook: ages the sampler cache and destroys samplers not used
// for many frames. The key hashes continuous float state (lodBias, LOD clamps,
// anisotropy), so an app animating those would otherwise mint an unbounded
@@ -86,19 +92,37 @@ private:
Bool usesCustomBorderColor = false;
};
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
VkSampler GetOrCreateSamplerImpl(const SamplerSource& sampler, TextureInternalFormat format,
Bool forceNearestFiltering, Uint32 viewLevelCount);
template <class SamplerSource>
Uint64 BuildSamplerKey(const SamplerSource& sampler, Bool forceNearestFiltering,
#else
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Bool forceNearestFiltering,
#endif
Bool singleLevelView, const ResolvedBorderColor& borderColor) const;
static VkFilter ToVkFilter(SamplerFilterMode mode);
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
ResolvedBorderColor ResolveBorderColor(const SamplerSource& sampler, TextureInternalFormat format) const;
#else
ResolvedBorderColor ResolveBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture) const;
#endif
// The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
// that) while Vulkan forbids anisotropyEnable there, so the GL value must never be forwarded raw.
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
Float ResolveEffectiveMaxAnisotropy(const SamplerSource& sampler,
#else
Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler,
#endif
Bool forceNearestFiltering) const;
VkDevice m_device = VK_NULL_HANDLE;
File diff suppressed because it is too large Load Diff
@@ -17,9 +17,21 @@
#include <unordered_map>
#include <unordered_set>
#include <Config.h>
#if MOBILEGL_BUILD_DISAGGREGATED
// The handle-keyed arm's key type. Header-only POD, disagg-only, exactly the shape
// MagmaPipeArms.h already gives this directory.
#include <MG_Pipe/MGPipeHandles.h>
#endif
namespace MobileGL::MG_State::GLState {
class ITextureObject;
}
#if MOBILEGL_BUILD_DISAGGREGATED
namespace MobileGL::MG_Pipe {
struct MGPipeResourceRecord;
}
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8;
@@ -333,6 +345,13 @@ public:
// shape version and nothing else. Without this in the early-out key the image, its views
// and therefore imageSize() all keep answering with the texture's PREVIOUS shape.
Uint64 syncedShapeVersion = 0;
// The handle-keyed arm's (P5f fm) early-out key: the applier record's server-owned
// Serial at the last successful sync. 0 also means "this resource has only ever been
// synced by the frontend arm" - a wire record's Serial starts at 0 on a create and moves
// on every mutation, so the first handle sync of a fresh image always re-checks.
#if MOBILEGL_BUILD_DISAGGREGATED
Uint64 syncedWireSerial = 0;
#endif
TextureResource() = default;
TextureResource(const TextureResource&) = delete;
@@ -365,6 +384,9 @@ public:
std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
#if MOBILEGL_BUILD_DISAGGREGATED
std::swap(this->syncedWireSerial, that.syncedWireSerial);
#endif
}
void Reset() {
@@ -429,6 +451,9 @@ public:
syncedContentVersion = 0;
syncedMipLevelCount = 0;
syncedShapeVersion = 0;
#if MOBILEGL_BUILD_DISAGGREGATED
syncedWireSerial = 0;
#endif
}
~TextureResource() {
@@ -514,6 +539,39 @@ public:
TextureResource* SyncTextureAndGetDescriptor(
MG_State::GLState::ITextureObject& texture);
#if MOBILEGL_BUILD_DISAGGREGATED
// P5f (fm): THE HANDLE-KEYED ARM of SyncTextureAndGetDescriptor. Under an active transport
// the apply thread may not name the client's ITextureObject (rule E), so the clear / blit /
// readback / mipmap verbs resolve their attachment textures from the wire handle the
// framebuffer or mip record carried: shape from the applier's resource record
// (MGPipeApplier().TextureResources), texels from the server's staged-texture store
// (P5c tx), pending-upload bookkeeping consumed from the same record. Returns null - never
// a frontend fallback - when the record or the shape does not resolve; the caller declines
// loudly, which is the honest answer a missing record deserves.
//
// The resource lives in m_wireTextureResources keyed by StagedTextureStore::KeyForHandle -
// a NAMESPACE THE FRONTEND-KEYED m_textureResources NEVER MEETS, so a recycled {slot, gen}
// can never inherit its predecessor's image. Texture views resolve the storage owner from
// Desc.ViewOf and the window from ViewCso; buffer-backed textures remain P7's buffer arm.
TextureResource* SyncTextureResourceByHandle(MG_Pipe::MGPipeHandle handle, Bool renderbuffer = false);
// Map a texture/view-relative subresource to its live storage owner. layerCount, when
// supplied, receives the number of accessible layers starting at the input layer.
// viewFormat is UNDEFINED for an ordinary texture, or the outer view's format.
MG_Pipe::MGPipeHandle ResolveWireTextureStorage(MG_Pipe::MGPipeHandle handle, Uint32& level,
Uint32& layer, VkFormat* viewFormat = nullptr, Uint32* layerCount = nullptr);
// Grows the live image of a handle-keyed resource to `requiredMipLevels`, carrying the
// existing levels' content across with an in-command-buffer copy. The caller
// (GenerateMipmap's record arm) has flushed every pending submission first, so the old
// image is GPU-idle and the copy may be recorded into `commandBuffer` directly; the old
// image goes to the deferred-release ring. False when the chain cannot grow (the resource
// stays as it was).
Bool GrowWireTextureMipChain(MG_Pipe::MGPipeHandle handle, Uint32 requiredMipLevels,
VkCommandBuffer commandBuffer);
// Records that GPU writes superseded the staged CPU snapshot of this level. Layer
// ranges select cube faces; array and 3D levels share one staged level shadow.
void MarkWireTextureGpuWritten(MG_Pipe::MGPipeHandle handle, Uint32 mipLevel,
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
#endif
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
VkImageView GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 baseArrayLayer, Uint32 layerCount,
@@ -670,6 +728,21 @@ private:
void EraseTrackedTexture(const TextureIdentity& identity);
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
SizeT PruneDeadTextures();
#if MOBILEGL_BUILD_DISAGGREGATED
// The two halves of SyncTextureResourceByHandle: shape from the resource record's
// descriptor (create / recreate / compatibility), then the pending-upload walk against the
// server's staged store. UploadPendingWireLevels mutates the applier's record (consumed
// entries are dropped), which is why the record is a non-const reference.
Bool SyncWireTextureShape(const MG_Pipe::MGPipeResourceRecord& record, TextureResource& resource);
Bool UploadPendingWireLevels(MG_Pipe::MGPipeHandle handle, MG_Pipe::MGPipeResourceRecord& record,
TextureResource& resource);
// The handle-keyed twin of m_textureResources, keyed by StagedTextureStore::KeyForHandle.
// Node-based for the same reason m_renderbufferResources is: callers hold TextureResource*
// across map-touching calls.
std::unordered_map<Uint64, TextureResource> m_wireTextureResources;
// Handles are per-kind; a renderbuffer and texture may have identical slot/gen.
std::unordered_map<Uint64, TextureResource> m_wireRenderbufferResources;
#endif
VkDevice m_device = VK_NULL_HANDLE;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
@@ -3154,9 +3154,15 @@ void main() {
inline ProgramFactory::CompileOptionFlags GetShaderTransformFlags(VkSurfaceTransformFlagBitsKHR preTransform) {
ProgramFactory::CompileOptionFlags flags = ProgramFactory::CompileOptionBit::PositionZRemap;
const auto& currentDrawFBO =
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
if (currentDrawFBO != nullptr && currentDrawFBO->IsDefaultFramebuffer()) {
#if MOBILEGL_BUILD_DISAGGREGATED
const auto* wireFbo = MG_Config::Transport != MG_Config::TransportMode::Monolith ? MG_Pipe::MGPipeApplier().DrawFramebuffer() : nullptr;
const Bool isDefault = wireFbo ? wireFbo->IsDefault :
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject()->IsDefaultFramebuffer();
#else
const auto& currentDrawFBO = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
const Bool isDefault = currentDrawFBO != nullptr && currentDrawFBO->IsDefaultFramebuffer();
#endif
if (isDefault) {
flags |= ProgramFactory::CompileOptionBit::PositionYFlip;
// gl_FragCoord follows the same rule the default-framebuffer RECTANGLES follow
// (GetDefaultFramebufferRectMapping): flipped for identity/180, left alone under a
@@ -3420,6 +3426,9 @@ void main() {
if (m_device != VK_NULL_HANDLE) {
VK_VERIFY(vkDeviceWaitIdle(m_device));
}
#if MOBILEGL_BUILD_DISAGGREGATED
DestroyWireDrawPass();
#endif
OnSubmitsCompletedUpTo(m_submitCounter);
DestroySubmitFencePool();
@@ -5210,7 +5219,7 @@ void main() {
// A program that runs a geometry shader AND captures transform feedback. Both halves are
// link-time properties, so this is safe to fold into a pipeline keyed on the program hash.
static Bool ProgramCapturesXfbFromGeometryStage(const MG_State::GLState::ProgramObject& program) {
static Bool ProgramCapturesXfbFromGeometryStage(const MagmaProgramSource& program) {
if (program.GetTransformFeedbackVaryingCount() == 0) return false;
// Both halves are link-time properties, so both are asked of the LAST LINK. Reading the
// live attach list would let a glAttachShader that has not been linked in yet - which GL
@@ -5249,12 +5258,22 @@ void main() {
VkPipeline VulkanRenderer::GetOrCreatePipeline(
GLenum mode,
const MG_State::GLState::ProgramObject& program,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
ProgramFactory::CompileOptionFlags transformFlags,
const MG_State::GLState::VertexArrayObject& vao,
const RenderPassEntry& renderPassEntry,
Bool primitiveRestartEnable) {
#if MOBILEGL_BUILD_DISAGGREGATED
return GetOrCreatePipelineWithInput(mode, program, programObj, transformFlags,
m_vertexInputStateFactory->GetOrCreateVertexInputState(vao), renderPassEntry, primitiveRestartEnable);
}
VkPipeline VulkanRenderer::GetOrCreatePipelineWithInput(GLenum mode, const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, ProgramFactory::CompileOptionFlags transformFlags,
const VertexInputStateFactory::BackendVertexInputState& vis, const RenderPassEntry& renderPassEntry,
Bool primitiveRestartEnable) {
#endif
Bool invertClockwise = transformFlags & ProgramFactory::CompileOptionBit::PositionYFlip;
if (programObj.stages.empty()) {
MGLOG_D("GetOrCreatePipeline skipped: program has no shader stages");
@@ -5272,7 +5291,9 @@ void main() {
// payload key on the resolved LAYOUT hash instead, so draws over identical
// layouts share one pipeline.
// The one-arg fetch rides the VAO's state-pointer memo (no hash, no map).
#if !MOBILEGL_BUILD_DISAGGREGATED
auto& vis = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao);
#endif
const Uint64 vertexLayoutHash = vis.layoutHash;
const Uint64 renderPassHash = renderPassEntry.hash;
// The pipeline-relevant subset only: glViewport / glScissor / glBlendColor / glStencilMask
@@ -5506,6 +5527,16 @@ void main() {
// (stencil) test always passes and nothing is written - even when the bound
// image is a packed depth-stencil texture attached through only one half.
{
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer();
if (fbo && !fbo->IsDefault) {
if (fbo->Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone) depthTestEnabled = false;
if (fbo->Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone) stencilTestEnabled = false;
}
} else
#endif
{
const auto& gatingFbo =
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
if (gatingFbo != nullptr && !gatingFbo->IsDefaultFramebuffer()) {
@@ -5519,6 +5550,7 @@ void main() {
}
}
}
}
const StencilFaceState& frontStencil = MGB_CTX->GetStencilState(StencilFace::Front);
const StencilFaceState& backStencil = MGB_CTX->GetStencilState(StencilFace::Back);
const VkPolygonMode requestedPolygonMode =
@@ -5726,12 +5758,31 @@ void main() {
MOBILEGL_ASSERT(payload.colorAttachmentCount <= PipelineFactory::PipelineCreatePayload::kMaxColorAttachments,
"GetOrCreatePipeline: colorAttachmentCount=%u exceeds payload capacity",
payload.colorAttachmentCount);
#if MOBILEGL_BUILD_DISAGGREGATED
const auto* wireFbo = MG_Config::Transport != MG_Config::TransportMode::Monolith ?
MG_Pipe::MGPipeApplier().DrawFramebuffer() : nullptr;
const auto drawFboBinding = wireFbo ? SharedPtr<MG_State::GLState::FramebufferObject>{} :
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
#else
const auto& drawFboBinding =
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
#endif
MOBILEGL_ASSERT(drawFboBinding != nullptr, "GetOrCreatePipeline: draw framebuffer is null");
#if MOBILEGL_BUILD_DISAGGREGATED
const Bool isDefaultDrawFbo = wireFbo ? wireFbo->IsDefault : drawFboBinding->IsDefaultFramebuffer();
Array<FramebufferAttachmentType, MG_Pipe::kMGPipeMaxColorAttachments> wireDrawBuffers{};
if (wireFbo) for (SizeT i = 0; i < wireDrawBuffers.size(); ++i)
wireDrawBuffers[i] = wireFbo->DrawBuffers[i] < 0 ? FramebufferAttachmentType::None :
static_cast<FramebufferAttachmentType>(static_cast<Int>(FramebufferAttachmentType::Color0) + wireFbo->DrawBuffers[i]);
const auto& drawBuffers = wireFbo ? wireDrawBuffers : drawFboBinding->GetDrawBuffers();
#else
const Bool isDefaultDrawFbo = drawFboBinding->IsDefaultFramebuffer();
const auto& drawBuffers = drawFboBinding->GetDrawBuffers();
#endif
auto resolveCompleteColorAttachmentTexture = [&](Uint32 drawBufferIndex) -> MG_State::GLState::ITextureObject* {
#if MOBILEGL_BUILD_DISAGGREGATED
if (wireFbo) return nullptr;
#endif
if (isDefaultDrawFbo || drawBufferIndex >= drawBuffers.size()) {
return nullptr;
}
@@ -5782,6 +5833,15 @@ void main() {
attachmentColorWriteMask = 0;
effectiveBlendEnabled = false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (wireFbo && !isDefaultDrawFbo) {
const Int32 slot = wireFbo->DrawBuffers[i];
if (slot < 0 || wireFbo->Color[slot].Kind == MG_Pipe::kMGPipeSurfaceKindNone) {
attachmentColorWriteMask = 0;
effectiveBlendEnabled = false;
}
} else
#endif
if (!isDefaultDrawFbo && i < drawBuffers.size()) {
const auto drawBuffer = drawBuffers[i];
colorAttachmentTexture = resolveCompleteColorAttachmentTexture(i);
@@ -5877,6 +5937,12 @@ void main() {
VkFormat colorAttachmentFormat = VK_FORMAT_UNDEFINED;
Int textureExternalIndex = -1;
#if MOBILEGL_BUILD_DISAGGREGATED
if (wireFbo && !isDefaultDrawFbo) {
const Int32 slot = wireFbo->DrawBuffers[i];
colorAttachmentFormat = ResolveWireImage(*wireFbo, wireFbo->Color[slot], VK_IMAGE_ASPECT_COLOR_BIT).format;
} else
#endif
if (isDefaultDrawFbo) {
colorAttachmentFormat = m_swapchainObject.GetSurfaceFormat().format;
} else if (colorAttachmentRenderbuffer != nullptr) {
@@ -6004,7 +6070,7 @@ void main() {
Bool VulkanRenderer::PrepareStorageImageTextures(
FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj) {
if (!programObj.hasStorageImages) {
return true;
@@ -6100,7 +6166,7 @@ void main() {
}
Bool VulkanRenderer::PrepareSamplerImageFeedbackSnapshots(
FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
VkPipelineStageFlags consumerShaderStageMask) {
auto& feedbackBindings = m_samplerImageFeedbackScratch;
@@ -6801,6 +6867,10 @@ void main() {
Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
const DrawCmdParam& drawParams,
const IndexBufferView* pIndexBufferView) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith)
return SetupWireDraw(frame, mode, aspects, drawParams);
#endif
// Sync each sampled texture at most once across this whole draw: the layout
// probe loop, the post-transition loop, and ResolveSamplerDescriptor would
// otherwise each re-run the full SyncTexture path on the same textures.
@@ -7417,6 +7487,12 @@ void main() {
}
void VulkanRenderer::DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
DispatchWireCompute(numGroupsX, numGroupsY, numGroupsZ);
return;
}
#endif
m_textureManager->CollectGarbage();
auto& frame = m_frameContext.GetCurrent();
// The DISPATCH accessor: with a pipeline bound this is its compute stage program
@@ -7636,7 +7712,23 @@ void main() {
return ScissoredClearPrep::Ready;
}
#include "WireFramebuffer.inc"
#include "WireDraw.inc"
void VulkanRenderer::Clear(GLbitfield mask) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer();
if (!fbo) MagmaWireFatal("clear-framebuffer-record");
ClearAttachmentPayload payload{};
payload.mask = mask;
payload.color = MGB_CTX->GetClearColor();
payload.depth = MGB_CTX->GetClearDepth();
payload.stencil = MGB_CTX->GetClearStencil();
ClearWireFramebuffer(*fbo, payload);
return;
}
#endif
m_clearManager->CollectGarbage();
if ((mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) == 0) {
return;
@@ -8013,6 +8105,14 @@ void main() {
void VulkanRenderer::QueueClearBufferPayload(GLenum buffer, GLint drawbuffer,
const ClearAttachmentPayload& clearPayload) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer();
if (!fbo) MagmaWireFatal("clear-buffer-framebuffer-record");
ClearWireFramebuffer(*fbo,clearPayload,buffer == GL_COLOR ? drawbuffer : -1);
return;
}
#endif
auto* fbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();
if (!fbo) {
return;
@@ -8971,55 +9071,42 @@ void main() {
void VulkanRenderer::BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §3.3/§5.4): MAGMA'S NAMED ARM. A blit record whose
// ReadFbo/DrawFbo are non-null is a glBlitNamedFramebuffer: both framebuffers resolve
// from the record's handles, and the sink is told the pair was consumed - a backend
// that leaves the flag clear has no named arm and the verb declines there, loudly.
// Magma has no FBO twin registry (it reads the frontend object wherever it syncs), so
// the resolution here is frontend-keyed - the handle was minted over the frontend
// object's lifetime id, and the two probes below (the client allocator's slot entry
// and the frontend context's framebuffer pool) are named debt inside the scope, the
// same shape as Espryt's StateForHandle arm: P7 retires it by carrying the object
// identity in the record. P5e (id), ruling 12: the third of Magma's four debts, so the
// scope it rides in is MagmaP7AllocatorDebtScope. Magma stays lockstep for the whole of
// P5e (CONTRACT-P5E §6.1), so the record being applied here is always barriered and the
// exemption still holds.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
auto& applierState = MG_Pipe::MGPipeApplier();
const MG_Pipe::MGPipeHandle readHandle = applierState.VerbBlitReadFbo;
const MG_Pipe::MGPipeHandle drawHandle = applierState.VerbBlitDrawFbo;
if (!MG_Pipe::MGPipeHandleIsNull(readHandle) || !MG_Pipe::MGPipeHandleIsNull(drawHandle)) {
const auto resolveEndpoint = [](MG_Pipe::MGPipeHandle handle)
-> SharedPtr<MG_State::GLState::FramebufferObject> {
const MG_Pipe::MagmaP7AllocatorDebtScope magmaP7AllocatorDebt;
if (handle == MG_Pipe::kMGPipeDefaultFramebuffer) {
return MG_State::pGLContext ? MG_State::pGLContext->GetFramebufferObject(0) : nullptr;
}
if (!MG_Pipe::MGPipeSlots().IsLive(MG_Pipe::MGPipeKind::Framebuffer, handle)) {
return nullptr;
}
const Uint64 lifetimeId =
MG_Pipe::MGPipeSlots().LifetimeIdOfSlot(MG_Pipe::MGPipeKind::Framebuffer, handle.Slot);
if (lifetimeId == 0 || MG_State::pGLContext == nullptr) return nullptr;
return MG_State::pGLContext->FindFramebufferObjectByLifetimeId(lifetimeId);
};
auto readFbo = resolveEndpoint(readHandle);
auto drawFbo = resolveEndpoint(drawHandle);
if (!readFbo || !drawFbo) {
// Leave the pair UNCONSUMED: the sink's decline is the loud answer a
// missing endpoint deserves, not a silent blit of whatever is bound.
MGLOG_E_ONCE("MGPipe: Magma's named blit could not resolve an endpoint (read {%u, %u}, draw "
"{%u, %u}) to a frontend framebuffer; the verb declines at the sink",
readHandle.Slot, readHandle.Gen, drawHandle.Slot, drawHandle.Gen);
return;
// The scissor test clips blit writes: intersect the destination rectangle with
// the scissor box and shrink the source proportionally.
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
const IntVec4& scissor = MGB_CTX->GetScissorBox();
const auto clipAxis = [](GLint& d0, GLint& d1, GLint& s0, GLint& s1, GLint clipLo, GLint clipHi) -> Bool {
const Bool dstFlipped = d1 < d0;
GLint lo = dstFlipped ? d1 : d0;
GLint hi = dstFlipped ? d0 : d1;
const GLint newLo = std::max(lo, clipLo);
const GLint newHi = std::min(hi, clipHi);
if (newLo >= newHi) {
return false;
}
applierState.VerbBlitNamedConsumed = true;
BlitNamedFramebuffer(readFbo, drawFbo, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0,
dstX1, dstY1, mask, filter);
return;
const double srcSpan = static_cast<double>(s1 - s0);
const double dstSpan = static_cast<double>(d1 - d0);
const double scale = dstSpan != 0.0 ? srcSpan / dstSpan : 0.0;
const GLint origD0 = d0;
const GLint clippedD0 = dstFlipped ? newHi : newLo;
const GLint clippedD1 = dstFlipped ? newLo : newHi;
s0 = s0 + static_cast<GLint>(std::lround((clippedD0 - origD0) * scale));
s1 = s0 + static_cast<GLint>(std::lround((clippedD1 - clippedD0) * scale));
d0 = clippedD0;
d1 = clippedD1;
return true;
};
if (!clipAxis(dstX0, dstX1, srcX0, srcX1, scissor.x(), scissor.x() + scissor.z()) ||
!clipAxis(dstY0, dstY1, srcY0, srcY1, scissor.y(), scissor.y() + scissor.w())) {
return; // fully scissored out
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
BlitWireFramebuffers(srcX0,srcY0,srcX1,srcY1,dstX0,dstY0,dstX1,dstY1,mask,filter);
return;
}
#endif
auto readFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
auto drawFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
@@ -9052,37 +9139,6 @@ void main() {
return;
}
// The scissor test clips blit writes: intersect the destination rectangle with
// the scissor box and shrink the source proportionally.
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
const IntVec4& scissor = MGB_CTX->GetScissorBox();
const auto clipAxis = [](GLint& d0, GLint& d1, GLint& s0, GLint& s1, GLint clipLo, GLint clipHi) -> Bool {
const Bool dstFlipped = d1 < d0;
GLint lo = dstFlipped ? d1 : d0;
GLint hi = dstFlipped ? d0 : d1;
const GLint newLo = std::max(lo, clipLo);
const GLint newHi = std::min(hi, clipHi);
if (newLo >= newHi) {
return false;
}
const double srcSpan = static_cast<double>(s1 - s0);
const double dstSpan = static_cast<double>(d1 - d0);
const double scale = dstSpan != 0.0 ? srcSpan / dstSpan : 0.0;
const GLint origD0 = d0;
const GLint clippedD0 = dstFlipped ? newHi : newLo;
const GLint clippedD1 = dstFlipped ? newLo : newHi;
s0 = s0 + static_cast<GLint>(std::lround((clippedD0 - origD0) * scale));
s1 = s0 + static_cast<GLint>(std::lround((clippedD1 - clippedD0) * scale));
d0 = clippedD0;
d1 = clippedD1;
return true;
};
if (!clipAxis(dstX0, dstX1, srcX0, srcX1, scissor.x(), scissor.x() + scissor.z()) ||
!clipAxis(dstY0, dstY1, srcY0, srcY1, scissor.y(), scissor.y() + scissor.w())) {
return; // fully scissored out
}
}
MOBILEGL_ASSERT(readFbo != nullptr, "VulkanRenderer::BlitFramebuffer: read framebuffer is null");
MOBILEGL_ASSERT(drawFbo != nullptr, "VulkanRenderer::BlitFramebuffer: draw framebuffer is null");
if (IsUnsupportedFramebufferForDirectVulkan(*readFbo) ||
@@ -9633,6 +9689,12 @@ void main() {
void VulkanRenderer::CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
GLint x, GLint y, GLsizei width, GLsizei height) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
CopyWireFramebufferToTexture(target,level,xoffset,yoffset,x,y,width,height);
return;
}
#endif
if (width <= 0 || height <= 0) {
return;
}
@@ -9952,6 +10014,10 @@ void main() {
const CopyImageEndpoint& dstEndpoint,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
#if MOBILEGL_BUILD_DISAGGREGATED
const Bool wire = MG_Config::Transport != MG_Config::TransportMode::Monolith;
if (wire && HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("copy-image-flush");
#endif
MOBILEGL_ASSERT(srcEndpoint.Exists() && dstEndpoint.Exists(),
"CopyImageSubData requires valid source and destination images.");
// The frontend already declines a zero or negative extent, so anything else here is a
@@ -9978,7 +10044,11 @@ void main() {
srcEndpoint.Texture ? &VkTextureManager::StorageTextureOf(*srcEndpoint.Texture) : nullptr;
const auto* dstStorageTexture =
dstEndpoint.Texture ? &VkTextureManager::StorageTextureOf(*dstEndpoint.Texture) : nullptr;
if (srcStorageTexture == dstStorageTexture && srcEndpoint.Renderbuffer == dstEndpoint.Renderbuffer) {
if (
#if MOBILEGL_BUILD_DISAGGREGATED
!wire &&
#endif
srcStorageTexture == dstStorageTexture && srcEndpoint.Renderbuffer == dstEndpoint.Renderbuffer) {
MGLOG_E_ONCE("%s: in-place copy on objectId=%u is not supported; declining the copy", __func__,
CopyImageEndpointName(srcEndpoint));
return;
@@ -9997,6 +10067,21 @@ void main() {
// SyncTextureAndGetDescriptor the copy always used; the renderbuffer arm goes through the
// render-pass manager, which is where a renderbuffer's VkImage lives.
const auto resolveImage = [this](const CopyImageEndpoint& endpoint, CopyImageVkImage& out) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
auto* resource = m_textureManager->SyncTextureResourceByHandle(endpoint.TextureHandle);
if (!resource) MagmaWireFatal("copy-image-resource");
out.image = resource->image;
out.trackedLayout = &resource->layout;
out.aspect = resource->aspect;
out.mipLevels = resource->mipLevels;
out.extent = resource->extent;
out.depth = resource->depth;
out.arrayLayers = resource->arrayLayers;
out.format = resource->format;
return out.image != VK_NULL_HANDLE;
}
#endif
if (endpoint.IsRenderbuffer()) {
auto* resource = m_renderPassManager->GetOrCreateRenderbufferResource(endpoint.Renderbuffer);
if (resource == nullptr) return false;
@@ -10031,6 +10116,12 @@ void main() {
CopyImageVkImage dstImage{};
const Bool srcResolved = resolveImage(srcEndpoint, srcImage);
const Bool dstResolved = resolveImage(dstEndpoint, dstImage);
#if MOBILEGL_BUILD_DISAGGREGATED
if (wire) {
if (srcImage.image == dstImage.image) MagmaWireFatal("copy-image-in-place@P7");
m_textureManager->FlushPendingUploads();
}
#endif
// Real checks, not MOBILEGL_ASSERT: the assertions this replaces compile to nothing in
// a release build, which is where both observed failures happened - a null resource
// dereferenced right below (lavapipe) and a mip level the VkImage does not have handed
@@ -10159,6 +10250,9 @@ void main() {
}
const auto materializeClear = [this, &frame](const CopyImageEndpoint& endpoint) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return true;
#endif
if (endpoint.IsRenderbuffer()) {
return MaterializePendingClearForRenderbuffer(frame.commandBuffer, endpoint.Renderbuffer);
}
@@ -10270,6 +10364,10 @@ void main() {
srcImage.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dstImage.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &copyRegion);
#if MOBILEGL_BUILD_DISAGGREGATED
if (wire) m_textureManager->MarkWireTextureGpuWritten(dstEndpoint.TextureHandle,
dstMipLevel, dstSlices.BaseArrayLayer(), dstSlices.slicesAreDepth ? 1u : copySliceCount);
#endif
VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags srcRestoreAccessMask = 0;
@@ -10364,6 +10462,12 @@ void main() {
void VulkanRenderer::ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
void* pixels) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
ReadWirePixels(x,y,width,height,format,type,pixels);
return;
}
#endif
if (width <= 0 || height <= 0) {
return;
}
@@ -11376,6 +11480,12 @@ void main() {
}
void VulkanRenderer::GenerateMipmap(GLenum target) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
GenerateWireMipmap();
return;
}
#endif
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// Whatever is left here is a coverage gap in this backend, not a broken invariant, so it
// declines (leaving the mip chain unwritten) rather than asserting the process down. What
@@ -433,6 +433,44 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
void QueueClearBufferPayload(GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload);
#if MOBILEGL_BUILD_DISAGGREGATED
struct WireImage {
VkImage image = VK_NULL_HANDLE;
VkFormat format = VK_FORMAT_UNDEFINED;
VkExtent2D extent{};
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkImageLayout* trackedLayout = nullptr;
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
VkImageAspectFlags aspect = 0;
Uint32 level = 0, layer = 0, layers = 1, levels = 1;
Bool isDefault = false;
Bool is3D = false;
MG_Pipe::MGPipeHandle storage = MG_Pipe::kMGPipeNullHandle;
};
WireImage ResolveWireImage(const MG_Pipe::MGPFramebufferState& fbo,
const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect);
void TransitionWireImage(WireImage& image, VkImageLayout layout);
void ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo,
const ClearAttachmentPayload& payload, GLint drawbuffer = -1);
void BlitWireFramebuffers(GLint sx0, GLint sy0, GLint sx1, GLint sy1,
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLbitfield mask, GLenum filter);
void ReadWirePixels(GLint x, GLint y, GLsizei width, GLsizei height,
GLenum format, GLenum type, void* pixels);
void GenerateWireMipmap();
Bool SetupWireDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
const DrawCmdParam& drawParams);
void DestroyWireDrawPass();
void DispatchWireCompute(GLuint x, GLuint y, GLuint z);
UniquePtr<RenderPassEntry> m_wireDrawPass;
Vector<VkImageView> m_wireDrawViews;
VkPipeline GetOrCreatePipelineWithInput(GLenum mode, const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, ProgramFactory::CompileOptionFlags transformFlags,
const VertexInputStateFactory::BackendVertexInputState& vis, const RenderPassEntry& renderPassEntry,
Bool primitiveRestartEnable);
void CopyWireFramebufferToTexture(GLenum target, GLint level, GLint xoffset, GLint yoffset,
GLint x, GLint y, GLsizei width, GLsizei height);
#endif
void QueueClearBufferPayloadForFramebuffer(const MG_State::GLState::FramebufferObject& framebuffer,
GLenum buffer, GLint drawbuffer,
const ClearAttachmentPayload& clearPayload);
@@ -1508,7 +1546,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline GetOrCreatePipeline(
GLenum mode,
const MG_State::GLState::ProgramObject& program,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
ProgramFactory::CompileOptionFlags transformFlags,
const MG_State::GLState::VertexArrayObject& vao,
@@ -1520,14 +1558,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// flush the pending recording (see the body), which retires the current command buffer.
Bool PrepareStorageImageTextures(
FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj);
// Vulkan forbids a sampled descriptor and writable storage descriptor from naming the
// same image subresource in one shader operation. Snapshot only the sampler side; the
// storage descriptor continues to name the application texture.
Bool PrepareSamplerImageFeedbackSnapshots(
FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
VkPipelineStageFlags consumerShaderStageMask);
@@ -0,0 +1,179 @@
// Server-owned draw/dispatch setup. The legacy setup remains the monolith implementation.
#if MOBILEGL_BUILD_DISAGGREGATED
static MagmaProgramSource WireProgramSource(MG_Pipe::MGPipeHandle handle) {
auto& state = MG_Pipe::MGPipeApplier();
const Bool composite = handle.Slot >= MG_Pipe::kMGPipeShaderCsoCompositeSlotBase;
const auto& records = composite ? state.CompositeShaderCsos : state.ShaderCsos;
const SizeT index = composite ? handle.Slot - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase : handle.Slot;
if (MG_Pipe::MGPipeHandleIsNull(handle) || index >= records.size() ||
!records[index].Live || records[index].Gen != handle.Gen || !records[index].Archive)
MagmaWireFatal("program-record");
return MagmaProgramSource(handle, records[index]);
}
[[noreturn]] static void WireBufferLegacyFatal() {
MGLOG_F("MGPipe: Fatal{RoleViolation, \"buffer-legacy-arm\"} (Magma P7 buffer consumer)");
std::abort();
}
void VulkanRenderer::DestroyWireDrawPass() {
m_wireDrawPass.reset();
for (const auto view : m_wireDrawViews) vkDestroyImageView(m_device, view, nullptr);
m_wireDrawViews.clear();
}
Bool VulkanRenderer::SetupWireDraw(FrameContext::FrameData& frame, GLenum mode,
Flags<DrawSetupAspect> aspects, const DrawCmdParam& drawParams) {
auto& state = MG_Pipe::MGPipeApplier();
const auto program = WireProgramSource(state.BoundShaderCso);
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) return false;
if (aspects & DrawSetupAspect::IndexBuffer || aspects & DrawSetupAspect::IndirectDrawBuffer ||
MGB_CTX->IsTransformFeedbackActive()) WireBufferLegacyFatal();
const auto* fbo = state.DrawFramebuffer();
if (!fbo) MagmaWireFatal("draw-framebuffer-record");
if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("draw-flush");
DestroyWireDrawPass();
ProgramFactory::CompileOptionFlags flags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform());
const auto* programObject = &m_programFactory->GetOrCreateProgram(program, flags);
if (programObject->readsBaseVertexBuiltin) {
flags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex;
programObject = &m_programFactory->GetOrCreateProgram(program, flags);
}
if (programObject->pointSizeCapabilityUnsupported) return false;
const auto& programObj = *programObject;
const auto vaoHandle = state.BoundVertexElements;
if (!MG_Pipe::MGPipeHandleIsNull(vaoHandle)) {
if (vaoHandle.Slot >= state.VertexElementsCsos.size()) MagmaWireFatal("vertex-elements-record");
const auto& vao = state.VertexElementsCsos[vaoHandle.Slot];
if (!vao.Live || vao.Gen != vaoHandle.Gen) MagmaWireFatal("vertex-elements-generation");
for (Uint32 i = 0; i < vao.AttributeCount; ++i)
if (vao.Attributes[i].Enabled && (programObj.activeVertexInputLocationMask & (1u << i)))
WireBufferLegacyFatal();
}
Vector<VkAttachmentDescription> attachments;
Vector<VkImageView> views;
Vector<VkAttachmentReference> colors;
VkAttachmentReference depth{VK_ATTACHMENT_UNUSED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
VkExtent2D extent{~0u, ~0u};
Uint32 layers = ~0u;
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
Uint64 passHash = 0;
const auto add = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Uint32 {
WireImage image = ResolveWireImage(*fbo, surface, aspect);
if (!image.image) return VK_ATTACHMENT_UNUSED;
m_textureManager->FlushPendingUploads();
const VkImageLayout layout = aspect == VK_IMAGE_ASPECT_COLOR_BIT ?
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
TransitionWireImage(image, layout);
VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
info.image = image.image;
info.viewType = image.layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D;
info.format = image.format;
info.subresourceRange = {aspect, image.level, 1, image.layer, image.layers};
if (image.format == VK_FORMAT_D24_UNORM_S8_UINT || image.format == VK_FORMAT_D32_SFLOAT_S8_UINT)
info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
VkImageView view = VK_NULL_HANDLE;
VK_VERIFY(vkCreateImageView(m_device, &info, nullptr, &view));
m_wireDrawViews.push_back(view);
views.push_back(view);
VkAttachmentDescription description{};
description.format = image.format;
description.samples = image.samples;
description.loadOp = description.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
description.storeOp = description.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
description.initialLayout = description.finalLayout = layout;
attachments.push_back(description);
extent.width = std::min(extent.width, image.extent.width);
extent.height = std::min(extent.height, image.extent.height);
layers = std::min(layers, image.layers);
samples = image.samples;
passHash = XXH64(&description, sizeof(description), passHash);
if (!image.isDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture)
m_textureManager->MarkWireTextureGpuWritten(image.storage, image.level, image.layer, image.layers);
if (image.isDefault) {
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, true);
else m_swapchainObject.SetDepthStencilContentDefined(m_imageIndexAcquired, true);
}
return static_cast<Uint32>(attachments.size() - 1);
};
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
const Int32 slot = fbo->DrawBuffers[i];
const Uint32 index = slot < 0 ? VK_ATTACHMENT_UNUSED : add(fbo->Color[slot], VK_IMAGE_ASPECT_COLOR_BIT);
colors.push_back({index, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL});
}
while (!colors.empty() && colors.back().attachment == VK_ATTACHMENT_UNUSED) colors.pop_back();
if (fbo->IsDefault || fbo->Depth.Kind != MG_Pipe::kMGPipeSurfaceKindNone)
depth.attachment = add(fbo->Depth, VK_IMAGE_ASPECT_DEPTH_BIT);
else if (fbo->Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone)
depth.attachment = add(fbo->Stencil, VK_IMAGE_ASPECT_STENCIL_BIT);
if (attachments.empty()) return false;
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = static_cast<Uint32>(colors.size());
subpass.pColorAttachments = colors.data();
subpass.pDepthStencilAttachment = depth.attachment != VK_ATTACHMENT_UNUSED ? &depth : nullptr;
VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
passInfo.attachmentCount = static_cast<Uint32>(attachments.size());
passInfo.pAttachments = attachments.data();
passInfo.subpassCount = 1;
passInfo.pSubpasses = &subpass;
m_wireDrawPass = MakeUnique<RenderPassEntry>();
auto& pass = *m_wireDrawPass;
VK_VERIFY(vkCreateRenderPass(m_device, &passInfo, nullptr, &pass.renderPass));
VkFramebufferCreateInfo fbInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
fbInfo.renderPass = pass.renderPass;
fbInfo.attachmentCount = static_cast<Uint32>(views.size());
fbInfo.pAttachments = views.data();
fbInfo.width = extent.width; fbInfo.height = extent.height; fbInfo.layers = layers;
VK_VERIFY(vkCreateFramebuffer(m_device, &fbInfo, nullptr, &pass.framebuffer));
pass.attachmentCount = static_cast<Uint32>(attachments.size());
pass.colorAttachmentCount = static_cast<Uint32>(colors.size());
pass.hasDepthStencilAttachment = depth.attachment != VK_ATTACHMENT_UNUSED;
pass.sampleCount = samples;
pass.extent = {static_cast<Int>(extent.width), static_cast<Int>(extent.height)};
pass.layers = layers;
pass.hash = pass.compatibilityHash = XXH64(colors.data(), colors.size() * sizeof(colors[0]), passHash);
VertexInputStateFactory::BackendVertexInputState input{};
const auto pipeline = GetOrCreatePipelineWithInput(mode, program, programObj, flags, input, pass, false);
if (pipeline == VK_NULL_HANDLE) return false;
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
if (!m_uniformManager->BindProgramUniformBuffers(frame.commandBuffer, program, programObj,
m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_GRAPHICS)) return false;
Uint32 binding = 0;
for (Uint32 location = 0; location < kMaxVertexAttribs; ++location) {
if (!(programObj.activeVertexInputLocationMask & (1u << location))) continue;
VkFormat format = VK_FORMAT_UNDEFINED;
const void* data = nullptr;
VkDeviceSize size = 0;
if (!TryGetCurrentVertexAttributeUploadPayload(MGB_CTX->GetCurrentVertexAttribute(location),
programObj.vertexInputTypes[location], format, data, size)) return false;
BufferSlice slice{};
if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(),
data, size, 16, slice)) return false;
vkCmdBindVertexBuffers(frame.commandBuffer, binding++, 1, &slice.buffer, &slice.offset);
}
if (!VkRenderPassManager::BeginRenderPass(frame.commandBuffer, pass)) return false;
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
ApplyDynamicDrawStateTail(frame, pass.extent, fbo->IsDefault,
ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin));
(void)drawParams;
return true;
}
void VulkanRenderer::DispatchWireCompute(GLuint x, GLuint y, GLuint z) {
if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("dispatch-flush");
auto& frame = m_frameContext.GetCurrent();
const auto program = WireProgramSource(MG_Pipe::MGPipeApplier().DispatchProgram);
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) return;
const auto& programObj = m_programFactory->GetOrCreateProgram(program, {});
const auto pipeline = GetOrCreateComputePipeline(programObj);
if (!pipeline) return;
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
if (!m_uniformManager->BindProgramUniformBuffers(frame.commandBuffer, program, programObj,
m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_COMPUTE)) return;
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
vkCmdDispatch(frame.commandBuffer, x, y, z);
}
#endif
@@ -0,0 +1,426 @@
// P5f fm: included by VulkanRenderer.cpp inside the backend namespace. All inputs here
// belong to the applier; no frontend object, lifetime-id lookup or allocator is used.
#if MOBILEGL_BUILD_DISAGGREGATED
[[noreturn]] static void MagmaWireFatal(const char* detail) {
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"Magma:%s\"}", detail);
std::abort();
}
VulkanRenderer::WireImage VulkanRenderer::ResolveWireImage(
const MG_Pipe::MGPFramebufferState& fbo, const MG_Pipe::MGPSurface& surface,
VkImageAspectFlags aspect) {
WireImage out{};
out.aspect = aspect;
if (fbo.IsDefault) {
out.isDefault = true;
out.extent = m_swapchainObject.GetExtent();
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) {
out.image = m_swapchainObject.GetImage(m_imageIndexAcquired);
out.format = m_swapchainObject.GetSurfaceFormat().format;
out.layout = m_swapchainObject.GetImageLayout(m_imageIndexAcquired);
} else {
out.image = m_swapchainObject.GetDepthStencilImage(m_imageIndexAcquired);
out.format = m_swapchainObject.GetDepthStencilFormat();
out.layout = m_swapchainObject.GetDepthStencilImageLayout(m_imageIndexAcquired);
}
return out;
}
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) return out;
const Bool renderbuffer = surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer;
Uint32 level = surface.Level, layer = surface.Layer, availableLayers = 1;
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
out.storage = renderbuffer ? surface.Res : m_textureManager->ResolveWireTextureStorage(
surface.Res, level, layer, &viewFormat, &availableLayers);
if (MG_Pipe::MGPipeHandleIsNull(out.storage)) MagmaWireFatal("framebuffer-view-record");
auto* resource = m_textureManager->SyncTextureResourceByHandle(out.storage, renderbuffer);
if (!resource) MagmaWireFatal("framebuffer-resource");
out.image = resource->image;
out.format = viewFormat != VK_FORMAT_UNDEFINED ? viewFormat : resource->format;
out.extent = {std::max(resource->extent.width >> level, 1u),
std::max(resource->extent.height >> level, 1u)};
out.layout = resource->layout;
out.trackedLayout = &resource->layout;
out.samples = resource->sampleCount;
out.level = level;
out.levels = resource->mipLevels;
out.layer = layer;
out.is3D = resource->depth > 1;
out.layers = surface.Layered ? (out.is3D ? std::max(resource->depth >> level, 1u) : availableLayers) : 1;
if (out.level >= resource->mipLevels || out.layer + out.layers >
(out.is3D ? std::max(resource->depth >> level, 1u) : resource->arrayLayers))
MagmaWireFatal("framebuffer-subresource");
return out;
}
void VulkanRenderer::TransitionWireImage(WireImage& image, VkImageLayout layout) {
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
if (VkRenderPassManager::GetActiveRenderPass()) VkRenderPassManager::EndRenderPass(frame.commandBuffer);
VkPipelineStageFlags stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags access = 0;
GetImageTransitionSourceState(image.layout, stage, access);
// One tracked layout per image: move the complete mip chain, never just the surface.
VkImageAspectFlags barrierAspect = image.aspect;
if (image.format == VK_FORMAT_D24_UNORM_S8_UINT || image.format == VK_FORMAT_D32_SFLOAT_S8_UINT)
barrierAspect = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer, image.image, image.layout, layout,
stage, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, access,
VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, barrierAspect, 0, image.levels))
MagmaWireFatal("image-transition");
if (image.trackedLayout) *image.trackedLayout = image.layout;
if (image.isDefault) {
if (image.aspect == VK_IMAGE_ASPECT_COLOR_BIT)
m_swapchainObject.SetImageLayout(m_imageIndexAcquired, image.layout);
else m_swapchainObject.SetDepthStencilImageLayout(m_imageIndexAcquired, image.layout);
}
}
void VulkanRenderer::ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo,
const ClearAttachmentPayload& payload, GLint drawbuffer) {
if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("wire-verb-flush");
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) return;
const auto clearSurface = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect,
Uint32 colorIndex) {
if (!fbo.IsDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) return;
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) {
const auto mask = MGB_CTX->GetColorMaskIndexed(colorIndex);
if (!mask.r() && !mask.g() && !mask.b() && !mask.a()) return;
if (!mask.r() || !mask.g() || !mask.b() || !mask.a()) {
MGLOG_W_ONCE("DirectVulkan: glClear with a partial color mask is not supported");
return;
}
}
if (aspect & VK_IMAGE_ASPECT_DEPTH_BIT) {
if (!MGB_CTX->GetDepthMask()) aspect &= ~VK_IMAGE_ASPECT_DEPTH_BIT;
}
if (aspect & VK_IMAGE_ASPECT_STENCIL_BIT) {
const Uint32 mask = MGB_CTX->GetStencilState(StencilFace::Front).WriteMask & 255u;
if (!mask) aspect &= ~VK_IMAGE_ASPECT_STENCIL_BIT;
else if (mask != 255u) {
MGLOG_W_ONCE("DirectVulkan: glClear with a partial stencil write mask is not supported");
aspect &= ~VK_IMAGE_ASPECT_STENCIL_BIT;
}
}
if (!aspect) return;
WireImage image = ResolveWireImage(fbo, surface, aspect);
if (!image.image) return;
m_textureManager->FlushPendingUploads();
VkRect2D area{{0, 0}, image.extent};
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
const auto& box = MGB_CTX->GetScissorBox();
const Int32 x0 = std::max(box.x(), 0), y0 = std::max(box.y(), 0);
const Int32 x1 = std::min(box.x() + box.z(), static_cast<Int32>(image.extent.width));
const Int32 y1 = std::min(box.y() + box.w(), static_cast<Int32>(image.extent.height));
if (x0 >= x1 || y0 >= y1) return;
area = {{x0, y0}, {static_cast<Uint32>(x1-x0), static_cast<Uint32>(y1-y0)}};
if (image.isDefault && !MapDefaultFramebufferReadbackRect(x0, y0, x1-x0, y1-y0,
image.extent, m_swapchainObject.GetPreTransform(), &area.offset, &area.extent))
MagmaWireFatal("clear-surface-transform");
}
const Bool color = aspect == VK_IMAGE_ASPECT_COLOR_BIT;
const auto layout = color ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL :
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
TransitionWireImage(image, layout);
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = image.image;
viewInfo.viewType = image.layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = image.format;
viewInfo.subresourceRange = {color ? VK_IMAGE_ASPECT_COLOR_BIT :
(image.format == VK_FORMAT_D24_UNORM_S8_UINT || image.format == VK_FORMAT_D32_SFLOAT_S8_UINT ? VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT : image.aspect), image.level, 1, image.layer, image.layers};
VkImageView view = VK_NULL_HANDLE;
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &view));
VkAttachmentDescription attachment{};
attachment.format = image.format;
attachment.samples = image.samples;
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
attachment.initialLayout = layout;
attachment.finalLayout = layout;
VkAttachmentReference ref{0, layout};
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = color ? 1 : 0;
subpass.pColorAttachments = color ? &ref : nullptr;
subpass.pDepthStencilAttachment = color ? nullptr : &ref;
VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
passInfo.attachmentCount = 1; passInfo.pAttachments = &attachment;
passInfo.subpassCount = 1; passInfo.pSubpasses = &subpass;
VkRenderPass pass = VK_NULL_HANDLE;
VK_VERIFY(vkCreateRenderPass(m_device, &passInfo, nullptr, &pass));
VkFramebufferCreateInfo fbInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
fbInfo.renderPass = pass; fbInfo.attachmentCount = 1; fbInfo.pAttachments = &view;
fbInfo.width = image.extent.width; fbInfo.height = image.extent.height; fbInfo.layers = image.layers;
VkFramebuffer fb = VK_NULL_HANDLE;
VK_VERIFY(vkCreateFramebuffer(m_device, &fbInfo, nullptr, &fb));
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
begin.renderPass = pass; begin.framebuffer = fb; begin.renderArea = area;
auto clearPayload = payload;
if (color) PreCompensateSrgbClearColor(clearPayload, image.format);
VkClearAttachment clear{};
clear.aspectMask = aspect;
clear.clearValue.color = MakeVkClearColorValue(clearPayload, false);
if (!color) clear.clearValue.depthStencil = {payload.depth, payload.stencil};
VkClearRect rect{area, 0, image.layers};
auto& frame = m_frameContext.GetCurrent();
vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
vkCmdClearAttachments(frame.commandBuffer, 1, &clear, 1, &rect);
vkCmdEndRenderPass(frame.commandBuffer);
// P5f changes ownership, not Magma's wait contract. Synchronous retirement keeps
// these temporary server resources bounded and makes their lifetime explicit.
if (!SubmitReadbackCommandsAndWait(frame)) MagmaWireFatal("clear-submit");
vkDestroyFramebuffer(m_device, fb, nullptr);
vkDestroyRenderPass(m_device, pass, nullptr);
vkDestroyImageView(m_device, view, nullptr);
if (!image.isDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture)
m_textureManager->MarkWireTextureGpuWritten(image.storage, image.level, image.layer, image.layers);
if (image.isDefault) {
if (color) m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, true);
else m_swapchainObject.SetDepthStencilContentDefined(m_imageIndexAcquired, true);
}
};
if (payload.mask & GL_COLOR_BUFFER_BIT) {
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
if (drawbuffer >= 0 && static_cast<Uint32>(drawbuffer) != i) continue;
const Int32 attachment = fbo.DrawBuffers[i];
if (attachment >= 0 && attachment < static_cast<Int32>(MG_Pipe::kMGPipeMaxColorAttachments))
clearSurface(fbo.Color[attachment], VK_IMAGE_ASPECT_COLOR_BIT, i);
}
}
if (payload.mask & GL_DEPTH_BUFFER_BIT) clearSurface(fbo.Depth, VK_IMAGE_ASPECT_DEPTH_BIT, 0);
if (payload.mask & GL_STENCIL_BUFFER_BIT) clearSurface(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT, 0);
}
void VulkanRenderer::ReadWirePixels(GLint x, GLint y, GLsizei width, GLsizei height,
GLenum format, GLenum type, void* pixels) {
if (width <= 0 || height <= 0) return;
if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("wire-verb-flush");
const auto* fbo = MG_Pipe::MGPipeApplier().ReadFramebuffer();
if (!fbo) MagmaWireFatal("read-framebuffer-record");
VkImageAspectFlags aspect = format == GL_DEPTH_COMPONENT ? VK_IMAGE_ASPECT_DEPTH_BIT :
format == GL_STENCIL_INDEX ? VK_IMAGE_ASPECT_STENCIL_BIT :
VK_IMAGE_ASPECT_COLOR_BIT;
if (format == GL_DEPTH_STENCIL) aspect = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
const auto& surface = (aspect & VK_IMAGE_ASPECT_DEPTH_BIT) ? fbo->Depth :
aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? fbo->Stencil : fbo->ReadSurface;
WireImage source = ResolveWireImage(*fbo, surface, aspect);
if (!source.image) return;
m_textureManager->FlushPendingUploads();
TransitionWireImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
if (aspect != VK_IMAGE_ASPECT_COLOR_BIT) {
ReadDepthStencilImageToClient(source.image,source.format,
source.trackedLayout ? source.trackedLayout : &source.layout,aspect,
source.level,source.layer,x,y,width,height,format,type,pixels,source.isDefault);
if (source.isDefault) m_swapchainObject.SetDepthStencilImageLayout(m_imageIndexAcquired,source.layout);
return;
}
const SizeT texelSize = aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? 1 : GetReadbackTexelSize(source.format);
if (!texelSize) MagmaWireFatal("read-format");
const VkDeviceSize size = static_cast<VkDeviceSize>(width) * height * texelSize;
VkBufferObject buffer;
if (!buffer.Create({.allocator=m_allocator, .size=size, .usage=VK_BUFFER_USAGE_TRANSFER_DST_BIT,
.memoryUsage=VMA_MEMORY_USAGE_AUTO, .allocationFlags=VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}))
MagmaWireFatal("read-buffer-allocation");
VkOffset2D offset{x,y};
VkExtent2D extent{static_cast<Uint32>(width), static_cast<Uint32>(height)};
if (source.isDefault && !MapDefaultFramebufferReadbackRect(x,y,width,height,source.extent,
m_swapchainObject.GetPreTransform(),&offset,&extent)) MagmaWireFatal("read-surface-transform");
VkBufferImageCopy region{};
region.imageSubresource = {aspect,source.level,source.is3D ? 0u : source.layer,1};
region.imageOffset = {offset.x,offset.y,source.is3D ? static_cast<Int32>(source.layer) : 0};
region.imageExtent = {extent.width,extent.height,1};
auto& frame = m_frameContext.GetCurrent();
vkCmdCopyImageToBuffer(frame.commandBuffer,source.image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
buffer.GetHandle(),1,&region);
if (!SubmitReadbackCommandsAndWait(frame)) MagmaWireFatal("read-submit");
const auto* bytes = static_cast<const Uint8*>(buffer.Map());
if (!bytes || !buffer.Invalidate(size)) MagmaWireFatal("read-map");
Vector<Uint8> remapped;
if (source.isDefault) {
remapped.resize(static_cast<SizeT>(size));
if (!RemapDefaultFramebufferReadback(bytes,width,height,m_swapchainObject.GetPreTransform(),
texelSize,remapped.data())) MagmaWireFatal("read-remap");
bytes = remapped.data();
}
if (!PackReadbackToClientOrPbo(bytes,source.format,width,height,1,format,type,pixels,false,true))
MagmaWireFatal("read-conversion");
}
void VulkanRenderer::BlitWireFramebuffers(GLint sx0, GLint sy0, GLint sx1, GLint sy1,
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLbitfield mask, GLenum filter) {
if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("wire-verb-flush");
auto& state = MG_Pipe::MGPipeApplier();
const Bool named = !MG_Pipe::MGPipeHandleIsNull(state.VerbBlitReadFbo) ||
!MG_Pipe::MGPipeHandleIsNull(state.VerbBlitDrawFbo);
const auto* read = named ? state.FramebufferRecordFor(state.VerbBlitReadFbo) : state.ReadFramebuffer();
const auto* draw = named ? state.FramebufferRecordFor(state.VerbBlitDrawFbo) : state.DrawFramebuffer();
if (!read || !draw) MagmaWireFatal("blit-framebuffer-record");
state.VerbBlitNamedConsumed = named;
const auto blit = [&](const MG_Pipe::MGPSurface& src, const MG_Pipe::MGPSurface& dst,
VkImageAspectFlags aspect) {
WireImage source = ResolveWireImage(*read,src,aspect);
WireImage destination = ResolveWireImage(*draw,dst,aspect);
if (!source.image || !destination.image) return;
m_textureManager->FlushPendingUploads();
const Bool sameImage = source.image == destination.image;
const VkImageLayout srcLayout = sameImage ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
const VkImageLayout dstLayout = sameImage ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
TransitionWireImage(source,srcLayout);
if (sameImage) destination.layout = source.layout;
else TransitionWireImage(destination,dstLayout);
VkImageBlit region{};
region.srcSubresource = {aspect,source.level,source.layer,1};
region.dstSubresource = {aspect,destination.level,destination.layer,1};
region.srcOffsets[0] = {sx0,sy0,0}; region.srcOffsets[1] = {sx1,sy1,1};
region.dstOffsets[0] = {dx0,dy0,0}; region.dstOffsets[1] = {dx1,dy1,1};
// Headless and identity surfaces store GL's bottom row at Vulkan's last row.
if (source.isDefault) {
region.srcOffsets[0].y = static_cast<GLint>(source.extent.height)-sy0;
region.srcOffsets[1].y = static_cast<GLint>(source.extent.height)-sy1;
}
if (destination.isDefault) {
region.dstOffsets[0].y = static_cast<GLint>(destination.extent.height)-dy0;
region.dstOffsets[1].y = static_cast<GLint>(destination.extent.height)-dy1;
}
vkCmdBlitImage(m_frameContext.GetCurrent().commandBuffer,source.image,srcLayout,
destination.image,dstLayout,1,&region,
filter == GL_LINEAR ? VK_FILTER_LINEAR : VK_FILTER_NEAREST);
if (!destination.isDefault && dst.Kind == MG_Pipe::kMGPipeSurfaceKindTexture)
m_textureManager->MarkWireTextureGpuWritten(destination.storage, destination.level, destination.layer, destination.layers);
if (destination.isDefault) {
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired,true);
else m_swapchainObject.SetDepthStencilContentDefined(m_imageIndexAcquired,true);
}
};
if (mask & GL_COLOR_BUFFER_BIT) {
for (Uint32 i=0;i<MG_Pipe::kMGPipeMaxColorAttachments;++i) {
const Int32 attachment = draw->DrawBuffers[i];
if (attachment>=0 && attachment<static_cast<Int32>(MG_Pipe::kMGPipeMaxColorAttachments))
blit(read->ReadSurface,draw->Color[attachment],VK_IMAGE_ASPECT_COLOR_BIT);
}
}
if (mask & GL_DEPTH_BUFFER_BIT) blit(read->Depth,draw->Depth,VK_IMAGE_ASPECT_DEPTH_BIT);
if (mask & GL_STENCIL_BUFFER_BIT) blit(read->Stencil,draw->Stencil,VK_IMAGE_ASPECT_STENCIL_BIT);
}
void VulkanRenderer::CopyWireFramebufferToTexture(GLenum target, GLint level, GLint xoffset, GLint yoffset,
GLint x, GLint y, GLsizei width, GLsizei height) {
if (width <= 0 || height <= 0) return;
if (target != GL_TEXTURE_2D) {
RecordTextureCopyError(__func__,ErrorCode::InvalidOperation,
"CopyTexSubImage2D currently only supports GL_TEXTURE_2D destinations.");
return;
}
if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("wire-verb-flush");
auto& state = MG_Pipe::MGPipeApplier();
const auto* read = state.ReadFramebuffer();
if (!read) MagmaWireFatal("copy-framebuffer-record");
const auto* texture = m_textureManager->SyncTextureResourceByHandle(state.VerbCopyTexDst);
if (!texture) MagmaWireFatal("copy-destination-record");
const VkImageAspectFlags aspect = texture->aspect;
MG_Pipe::MGPFramebufferState fake{};
MG_Pipe::MGPSurface surface{};
surface.Kind = MG_Pipe::kMGPipeSurfaceKindTexture;
surface.Res = state.VerbCopyTexDst;
surface.Level = static_cast<Uint16>(level);
WireImage source = ResolveWireImage(*read,aspect & VK_IMAGE_ASPECT_DEPTH_BIT ? read->Depth :
aspect & VK_IMAGE_ASPECT_STENCIL_BIT ? read->Stencil : read->ReadSurface,aspect);
WireImage destination = ResolveWireImage(fake,surface,aspect);
if (!source.image || !destination.image) return;
m_textureManager->FlushPendingUploads();
TransitionWireImage(source,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
TransitionWireImage(destination,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
VkImageBlit region{};
region.srcSubresource = {aspect,source.level,source.layer,1};
region.dstSubresource = {aspect,destination.level,destination.layer,1};
region.srcOffsets[0] = {x,y,0}; region.srcOffsets[1] = {x+width,y+height,1};
region.dstOffsets[0] = {xoffset,yoffset,0}; region.dstOffsets[1] = {xoffset+width,yoffset+height,1};
if (source.isDefault) {
region.srcOffsets[0].y = static_cast<Int32>(source.extent.height)-y;
region.srcOffsets[1].y = static_cast<Int32>(source.extent.height)-y-height;
}
vkCmdBlitImage(m_frameContext.GetCurrent().commandBuffer,source.image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
destination.image,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,1,&region,VK_FILTER_NEAREST);
m_textureManager->MarkWireTextureGpuWritten(destination.storage,destination.level,destination.layer);
}
void VulkanRenderer::GenerateWireMipmap() {
if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("wire-verb-flush");
auto& state = MG_Pipe::MGPipeApplier();
const auto handle = state.VerbMipRes;
auto* resource = m_textureManager->SyncTextureResourceByHandle(handle);
if (!resource) MagmaWireFatal("mipmap-resource");
m_textureManager->FlushPendingUploads();
if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("mipmap-flush");
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
const Uint32 base = state.VerbMipBaseLevel;
const Uint32 count = state.VerbMipLevelCount;
if (count <= 1) return;
if (!m_textureManager->GrowWireTextureMipChain(handle,base+count,frame.commandBuffer))
MagmaWireFatal("mipmap-growth");
if (resource->aspect != VK_IMAGE_ASPECT_COLOR_BIT && resource->aspect != VK_IMAGE_ASPECT_DEPTH_BIT)
MagmaWireFatal("depth-stencil-mipmap@P7");
VkFormatProperties props{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle,resource->format,&props);
const auto required = VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT;
if ((props.optimalTilingFeatures & required) != required) MagmaWireFatal("mipmap-native-format@P7");
const VkFilter filter = props.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT ?
VK_FILTER_LINEAR : VK_FILTER_NEAREST;
VkPipelineStageFlags stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags access = 0;
GetImageTransitionSourceState(resource->layout,stage,access);
if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,resource->layout,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,stage,VK_PIPELINE_STAGE_TRANSFER_BIT,access,
VK_ACCESS_TRANSFER_WRITE_BIT,resource->aspect,0,resource->mipLevels)) MagmaWireFatal("mipmap-transition");
for (Uint32 level=base+1;level<base+count;++level) {
VkImageLayout previous = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,previous,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_ACCESS_TRANSFER_WRITE_BIT,VK_ACCESS_TRANSFER_READ_BIT,resource->aspect,level-1,1);
VkImageBlit blit{};
blit.srcSubresource = {resource->aspect,level-1,0,resource->arrayLayers};
blit.dstSubresource = {resource->aspect,level,0,resource->arrayLayers};
blit.srcOffsets[1] = {static_cast<Int32>(std::max(resource->extent.width>>(level-1),1u)),
static_cast<Int32>(std::max(resource->extent.height>>(level-1),1u)),
static_cast<Int32>(std::max(resource->depth>>(level-1),1u))};
blit.dstOffsets[1] = {static_cast<Int32>(std::max(resource->extent.width>>level,1u)),
static_cast<Int32>(std::max(resource->extent.height>>level,1u)),
static_cast<Int32>(std::max(resource->depth>>level,1u))};
vkCmdBlitImage(frame.commandBuffer,resource->image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
resource->image,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,1,&blit,filter);
VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,previous,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_ACCESS_TRANSFER_READ_BIT,VK_ACCESS_TRANSFER_WRITE_BIT,resource->aspect,level-1,1);
}
resource->layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
auto& record = state.TextureResources[handle.Slot];
record.Desc.Levels = std::max<Uint16>(record.Desc.Levels,static_cast<Uint16>(base+count));
resource->syncedWireSerial = ++record.Serial;
auto& store = MG_Remote::Server::ServerStagedTexture();
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(handle);
const auto target = static_cast<MG_Pipe::MGPipeResourceTarget>(record.Desc.Target);
// The resource descriptor determines the upload-target set; no texture unit or
// client upload-target vector participates in defining the generated shadow.
for (Uint32 face=0;face<(target==MG_Pipe::MGPipeResourceTarget::TexCube ? 6u : 1u);++face) {
TextureUploadTarget upload = TextureUploadTarget::Texture2D;
switch (target) {
case MG_Pipe::MGPipeResourceTarget::Tex1D: upload=TextureUploadTarget::Texture1D; break;
case MG_Pipe::MGPipeResourceTarget::Tex3D: upload=TextureUploadTarget::Texture3D; break;
case MG_Pipe::MGPipeResourceTarget::Tex1DArray: upload=TextureUploadTarget::Texture1DArray; break;
case MG_Pipe::MGPipeResourceTarget::Tex2DArray: upload=TextureUploadTarget::Texture2DArray; break;
case MG_Pipe::MGPipeResourceTarget::TexCubeArray: upload=TextureUploadTarget::CubeMapArray; break;
case MG_Pipe::MGPipeResourceTarget::TexCube:
upload=static_cast<TextureUploadTarget>(static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX)+face); break;
default: break;
}
for (Uint32 level=base+1;level<base+count;++level) {
const IntVec3 extent{static_cast<Int>(std::max(resource->extent.width>>level,1u)),
static_cast<Int>(std::max(resource->extent.height>>level,1u)),
static_cast<Int>(std::max(resource->depth>>level,1u))};
store.NoteLevelDefined(key,static_cast<Uint16>(upload),static_cast<Uint16>(level),extent);
store.MarkLevelGpuDirty(key,static_cast<Uint16>(upload),static_cast<Uint16>(level),true);
}
}
}
#endif
+2 -1
View File
@@ -113,7 +113,8 @@ namespace MobileGL::MG_Backend {
switch (type) {
case BackendType::DirectGLES: return MG_Pipe::kMGPipeSubsystemsMigratedAtP5e;
case BackendType::DirectVulkan:
return MG_Pipe::kMGPipeSubsystemsMigratedAtP4a & ~MG_Pipe::kMGPipeSubsystemResources;
return (MG_Pipe::kMGPipeSubsystemsMigratedAtP4a | MG_Pipe::kMGPipeSubsystemBufferBindings) &
~MG_Pipe::kMGPipeSubsystemResources;
default: return 0;
}
}
@@ -6077,6 +6077,10 @@ namespace MobileGL::MG_Impl::GLImpl {
viewObject->SetInternalFormat(viewInternalFormat);
viewObject->SetSamples(storageOwner->GetSamples());
viewObject->SetFixedSampleLocations(storageOwner->HasFixedSampleLocations());
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith)
MG_Pipe::MGPipeEmitSamplerViewCreate(*viewObject);
#endif
}
void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width) {
+16 -2
View File
@@ -1392,8 +1392,10 @@ namespace MobileGL::MG_Pipe {
// family here would be a NEW rule, and a client that withheld more than the server
// refuses leaves the server's handle arm live with no records to read.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
// P5f fm: texture/framebuffer records have their own server consumers;
// Magma consumes them without claiming the still-unmigrated buffer family.
return MG_Remote::Client::CapsMirrorInstance().ServerConsumes(
kMGPipeSubsystemResources);
subsystem & kMGPipeP4aFamilySubsystems);
}
#endif
return MGPipeGetResourceOps() != nullptr;
@@ -2832,6 +2834,9 @@ namespace MobileGL::MG_Pipe {
Bool ApplierDerives = false;
Bool ContextValuesWireLive = false;
Bool P4aConsumer = false;
#if MOBILEGL_BUILD_DISAGGREGATED
Uint64 CapsGeneration = 0;
#endif
// One bit per FIELD - not per verb class. The class mask is applied at the walk
// exactly as it always was, so "does this verb read this field" stays the walk's
// business and this stays a statement about EMISSION alone.
@@ -2857,7 +2862,12 @@ namespace MobileGL::MG_Pipe {
if (g_fillPlan.Valid && g_fillPlan.PushMask == pushMask &&
g_fillPlan.ApplierDerives == applierDerives &&
g_fillPlan.ContextValuesWireLive == contextValuesWireLive &&
g_fillPlan.P4aConsumer == p4aConsumer) {
g_fillPlan.P4aConsumer == p4aConsumer
#if MOBILEGL_BUILD_DISAGGREGATED
&& g_fillPlan.CapsGeneration == (MG_Config::Transport != MG_Config::TransportMode::Monolith ?
MG_Remote::Client::CapsMirrorInstance().Generation() : 0)
#endif
) {
return g_fillPlan.Supplied;
}
MGPipeFieldMask built{};
@@ -2898,6 +2908,10 @@ namespace MobileGL::MG_Pipe {
g_fillPlan.ApplierDerives = applierDerives;
g_fillPlan.ContextValuesWireLive = contextValuesWireLive;
g_fillPlan.P4aConsumer = p4aConsumer;
#if MOBILEGL_BUILD_DISAGGREGATED
g_fillPlan.CapsGeneration = MG_Config::Transport != MG_Config::TransportMode::Monolith ?
MG_Remote::Client::CapsMirrorInstance().Generation() : 0;
#endif
g_fillPlan.Supplied = built;
return built;
}
@@ -2378,6 +2378,21 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p5b-f1-${f1Slot}.log"
)
endforeach()
# P5f fm: the framebuffer/copy/mipmap verbs now consume server records on Magma too.
mgl_itest_join_environment(MGL_ITEST_MAGMA_WIRE_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_TRANSPORT=inproc"
"MOBILEGL_ITEST_REQUIRE_GPU=1" "MGITEST_SPLIT_LANE=1"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
foreach(fmSlot PartialTextureUploadPreservesGpuClear TextureViewClearTargetsItsRootMipAndLayer ClearBufferfi ClearBufferfv ClearBufferiv ClearBufferuiv ClearNamedFramebufferfi ClearNamedFramebufferfv ClearNamedFramebufferiv ClearNamedFramebufferuiv CopyTexImage2D CopyTexSubImage2D GenerateMipmap GenerateMipmapPackedFloat GenerateMipmapDepth)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.Split.Fm."
TEST_FILTER "F1WireScenario.${fmSlot}Pixels"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-magma-split"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_MAGMA_WIRE_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p5f-fm-${fmSlot}.log")
endforeach()
endif()
# P5c ct (MG_Remote/CONTRACT-P5C.md §5): the two control records, split-only source for the
@@ -259,6 +259,84 @@ TEST_F(F1WireScenario, GenerateMipmapDepthPixels) {
EXPECT_NEAR(pixel, 0.375f, 0.00001f);
}
TEST_F(F1WireScenario, PartialTextureUploadPreservesGpuClearPixels) {
if (!Ready()) return;
Attach(GL_RGBA8);
// The client shadow is red. The GPU then changes every texel to green, so
// applying a later one-pixel upload as a whole shadow loses observable data.
std::array<GLubyte, 8 * 8 * 4> red{};
for (size_t i = 0; i < red.size(); i += 4) {
red[i] = 255;
red[i + 3] = 255;
}
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
glClearColor(0, 1, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
const std::array<GLubyte, 4> blue{0, 0, 255, 255};
glTexSubImage2D(GL_TEXTURE_2D, 0, 1, 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
std::array<GLubyte, 8 * 8 * 4> pixels{};
glReadPixels(0, 0, 8, 8, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
for (int y = 0; y < 8; ++y) {
for (int x = 0; x < 8; ++x) {
const std::array<GLubyte, 4> expected = x == 1 && y == 2
? blue : std::array<GLubyte, 4>{0, 255, 0, 255};
const size_t at = static_cast<size_t>(y * 8 + x) * 4;
const std::array<GLubyte, 4> actual{pixels[at], pixels[at + 1], pixels[at + 2], pixels[at + 3]};
EXPECT_EQ(actual, expected) << "GPU clear survived outside upload at " << x << ", " << y;
}
}
}
TEST_F(F1WireScenario, TextureViewClearTargetsItsRootMipAndLayer) {
if (!Ready()) return;
GLuint root = 0, view = 0;
glGenTextures(1, &root);
glBindTexture(GL_TEXTURE_2D_ARRAY, root);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 2, GL_RGBA8, 8, 8, 2);
std::array<GLubyte, 8 * 8 * 2 * 4> red{};
for (size_t i = 0; i < red.size(); i += 4) {
red[i] = 255;
red[i + 3] = 255;
}
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, 8, 8, 2,
GL_RGBA, GL_UNSIGNED_BYTE, red.data());
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 1, 0, 0, 0, 4, 4, 2,
GL_RGBA, GL_UNSIGNED_BYTE, red.data());
std::array<GLubyte, 4 * 4 * 4> blue{};
for (size_t i = 0; i < blue.size(); i += 4) {
blue[i + 2] = 255;
blue[i + 3] = 255;
}
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 1, 0, 0, 0, 4, 4, 1,
GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
glGenTextures(1, &view);
glTextureView(view, GL_TEXTURE_2D, root, GL_RGBA8, 1, 1, 1, 1);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "view construction";
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
glClearColor(0, 1, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
std::array<GLubyte, 4> pixel{};
glReadPixels(1, 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data());
EXPECT_EQ(pixel, (std::array<GLubyte, 4>{0, 255, 0, 255})) << "view reads its cleared window";
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, root, 1, 1);
glReadPixels(1, 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data());
EXPECT_EQ(pixel, (std::array<GLubyte, 4>{0, 255, 0, 255})) << "view clear reaches root mip 1, layer 1";
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, root, 1, 0);
glReadPixels(1, 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data());
EXPECT_EQ(pixel, (std::array<GLubyte, 4>{0, 0, 255, 255})) << "the neighboring root layer is preserved";
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, root, 0, 1);
glReadPixels(1, 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data());
EXPECT_EQ(pixel, (std::array<GLubyte, 4>{255, 0, 0, 255})) << "the neighboring root mip is preserved";
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 0, 0);
glDeleteTextures(1, &view);
glDeleteTextures(1, &root);
}
TEST_F(F1WireScenario, NamedBlitPreservesBindingsAndRestoresNextVerbsPixels) {
if (!Ready()) return;
Attach(GL_RGBA8);
+9
View File
@@ -44,6 +44,8 @@
// server-role-only fixture: there the direct call is the only reset that exists).
#include <MG_Remote/Client/ClientSession.h>
#include <MG_Remote/Server/ServerLoop.h>
#include <MG_Remote/Server/ServerSession.h>
#include <MG_Remote/CapsCodec.h>
#endif
#include <algorithm>
@@ -1235,6 +1237,13 @@ namespace MobileGL::MG_Pipe {
// site: ApplyUnitWindow validates and writes in one step, so the question has to be
// asked in front of it.
Bool NoP4aConsumer() {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const auto* session = MG_Remote::Server::ServerSession::Active();
if (session && session->CallMaskIsSet() && MG_Remote::MGCapsServerConsumes(
session->CallMask(), kMGPipeSubsystemTextureResources)) return false;
}
#endif
if (g_resourceOps != nullptr) return false;
++g_applier.RefusedNoConsumer;
return true;
+3
View File
@@ -851,6 +851,8 @@ namespace MobileGL::MG_Pipe {
UnorderedMap<Uint64, MGPStreamOutputBegin> StreamOutputSpans;
// generate_mipmap's texture (MGPMipPlan::Res).
MGPipeHandle VerbMipRes = kMGPipeNullHandle;
Uint16 VerbMipBaseLevel = 0;
Uint16 VerbMipLevelCount = 0;
// The current indirect draw's command buffer and (for the *Count forms) parameter
// buffer (MGPDrawIndirect::Buffer / ParameterBuffer).
MGPipeHandle VerbIndirectBuffer = kMGPipeNullHandle;
@@ -875,6 +877,7 @@ namespace MobileGL::MG_Pipe {
VerbCopyTexDst = kMGPipeNullHandle;
VerbStorageBlockProgram = kMGPipeNullHandle;
VerbMipRes = kMGPipeNullHandle;
VerbMipBaseLevel = VerbMipLevelCount = 0;
VerbIndirectBuffer = kMGPipeNullHandle;
VerbIndirectParameterBuffer = kMGPipeNullHandle;
VerbDispatchIndirectBuffer = kMGPipeNullHandle;
@@ -1047,6 +1047,8 @@ namespace MobileGL::MG_Remote::Server {
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;
@@ -44,6 +44,9 @@
#include <Config.h>
#include <MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h>
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Backend/DirectVulkan/Renderer/MagmaProgramSource.h>
#endif
using namespace MobileGL;
@@ -258,4 +261,99 @@ namespace {
"kCapRunAheadApply moved bit; CONTRACT-P5E table 0 names bit 10");
}
#endif // MOBILEGL_PIPE_PUSH
TEST(MagmaProgramSourceTest, ServerBindingTailsReplaceLinkTimeDefaults) {
#if MOBILEGL_BUILD_DISAGGREGATED
auto archive = MakeShared<MG_State::GLState::ProgramArchive>();
archive->Link.glBlockIndexToTProgram = {0};
archive->Link.blockReflection.resize(1);
archive->Link.blockReflection[0].name = "Block";
archive->Link.blockReflection[0].size = 12;
archive->Link.uniformBlockBinding = {9};
archive->Link.uniformSamplerOrImageUnitIndex = {12, 13};
archive->Spirv.globalUboScratch = {99, 98};
MG_Pipe::MGPipeShaderCsoRecord record{};
record.Archive = archive;
record.BlockBindings = {3};
record.SamplerUnits = {{0, 4}};
record.GlobalConstants = {5, 6};
record.GlobalConstantsVersion = 11;
const MagmaProgramSource source({7, 2}, record);
EXPECT_TRUE(source.IsWire());
EXPECT_EQ(source.Frontend(), nullptr);
EXPECT_EQ(source.GetUniformBlockBinding(0), 3u);
EXPECT_EQ(source.GetUniformBlockName(0), "Block");
EXPECT_EQ(source.GetUBOSizeAt(0), 16u);
EXPECT_EQ(source.GetUniformSamplerOrImageUnitIndex(0), 4);
EXPECT_EQ(source.GetUniformSamplerOrImageUnitIndex(1), -1)
<< "an absent tail entry must not resurrect the archive's stale unit";
ASSERT_EQ(source.GetUBOSize(), 2u);
EXPECT_EQ(static_cast<const Uint8*>(source.GetUBOData())[0], 5u);
EXPECT_EQ(source.GetUBOContentVersion(), 11u);
record.BlockBindings[0] = 6;
record.SamplerUnits[0].Unit = 8;
record.GlobalConstants[0] = 42;
record.GlobalConstantsVersion = 12;
EXPECT_EQ(source.GetUniformBlockBinding(0), 6u);
EXPECT_EQ(source.GetUniformSamplerOrImageUnitIndex(0), 8);
EXPECT_EQ(static_cast<const Uint8*>(source.GetUBOData())[0], 42u);
EXPECT_EQ(source.GetUBOContentVersion(), 12u);
#else
GTEST_SKIP() << "server program sources require the disaggregated build";
#endif
}
TEST(MagmaProgramSourceTest, ArrayUniformNamesResolveWithinTheArchivedUniform) {
#if MOBILEGL_BUILD_DISAGGREGATED
auto archive = MakeShared<MG_State::GLState::ProgramArchive>();
auto& link = archive->Link;
link.maxUniformLocation = 2;
link.uniformLocations["arr[0]"] = 0;
link.uniformIndexInTProgram = {0, 0, 0};
link.tProgramUniformIndexToGl = {0};
link.uniformReflection.resize(1);
link.uniformReflection[0].type.isArray = true;
link.uniformReflection[0].arraySize = 3;
link.uniformReflection[0].glDefineType = GL_SAMPLER_2D;
MG_Pipe::MGPipeShaderCsoRecord record{};
record.Archive = archive;
const MagmaProgramSource source({8, 1}, record);
EXPECT_EQ(source.GetUniformLocation("arr"), 0);
EXPECT_EQ(source.GetUniformLocation("arr[2]"), 2);
EXPECT_EQ(source.GetUniformLocation("arr[3]"), -1);
EXPECT_EQ(source.GetUniformLocation("arr[-1]"), -1);
EXPECT_EQ(source.GetUniformLocation("arr[999999999999999]"), -1);
EXPECT_EQ(source.GetUniformLocation("unknown"), -1);
EXPECT_TRUE(source.UniformLocationsAliasSameUniform(0, 2));
EXPECT_FALSE(source.UniformLocationsAliasSameUniform(0, 3));
EXPECT_EQ(source.GetUniformType(2), GL_SAMPLER_2D);
#else
GTEST_SKIP() << "server program sources require the disaggregated build";
#endif
}
TEST(MagmaProgramSourceTest, ReusedHandlesAndRecordVersionsHaveDistinctCacheIdentity) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeShaderCsoRecord record{};
record.Archive = MakeShared<MG_State::GLState::ProgramArchive>();
record.Serial = 17;
record.BindingsSerial = 21;
record.GlobalConstantsVersion = 25;
const MagmaProgramSource first({9, 2}, record);
const MagmaProgramSource recycled({9, 3}, record);
EXPECT_NE(first.GetLifetimeId(), recycled.GetLifetimeId());
EXPECT_EQ(first.Handle(), (MG_Pipe::MGPipeHandle{9, 2}));
const auto identity = first.GetLifetimeId();
record.Serial = 18;
record.BindingsSerial = 22;
record.GlobalConstantsVersion = 26;
EXPECT_EQ(first.GetLifetimeId(), identity) << "content changes do not mint object identities";
EXPECT_EQ(first.GetBackendStateVersion(), 18u);
EXPECT_EQ(first.GetBlockBindingVersion(), 22u);
EXPECT_EQ(first.GetImageUnitVersion(), 22u);
EXPECT_EQ(first.GetUBOContentVersion(), 26u);
#else
GTEST_SKIP() << "server program sources require the disaggregated build";
#endif
}
} // namespace