mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Perf] (Magma): converted wire vertex streams are cached across frames and default-framebuffer clears stay standalone (BSL Magma 19.5 -> 77.2 fps, MobileGlues 78.2).
This commit is contained in:
@@ -125,6 +125,17 @@ namespace MobileGL::MG_Config {
|
||||
// (vkCmdClearAttachments) rather than in a render pass of its own built for the clear.
|
||||
// Auto is ON; ForceOff restores the standalone clear for every surface.
|
||||
QuirkOverride MagmaInPassClear = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_INPASS_CLEAR_DEFAULT: the same for the default framebuffer. Auto is OFF
|
||||
// (an unexplained openra regression on Adreno 750, perf-p15/HANDOFF.md); ForceOn enables it.
|
||||
QuirkOverride MagmaInPassClearDefault = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_PASS_CACHE: wire draw passes (render pass, framebuffer, views) are kept
|
||||
// across frames while their key's image epochs hold. Auto is ON; ForceOff drops every
|
||||
// cached pass whose last use completed at each frame begin, as before P15.
|
||||
QuirkOverride MagmaPassCache = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_VERTEX_CONVERSION_CACHE: a wire vertex stream that needs a CPU format
|
||||
// conversion is converted once per frame slot per (buffer content, layout) and reused by
|
||||
// every draw that names it, instead of once per draw. Auto is ON; ForceOff converts per draw.
|
||||
QuirkOverride MagmaVertexConversionCache = QuirkOverride::Auto;
|
||||
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
||||
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
||||
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
||||
|
||||
@@ -346,6 +346,9 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
|
||||
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
|
||||
features.MagmaInPassClear = QueryEnvQuirkOverride("MOBILEGL_MAGMA_INPASS_CLEAR");
|
||||
features.MagmaInPassClearDefault = QueryEnvQuirkOverride("MOBILEGL_MAGMA_INPASS_CLEAR_DEFAULT");
|
||||
features.MagmaPassCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PASS_CACHE");
|
||||
features.MagmaVertexConversionCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_VERTEX_CONVERSION_CACHE");
|
||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
|
||||
|
||||
@@ -108,7 +108,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkCommandBufferBeginInfo beginInfo{};
|
||||
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
beginInfo.flags = flags;
|
||||
// P15: a frame recording is submitted once and then reset, so the driver may skip whatever
|
||||
// it keeps for re-submission.
|
||||
beginInfo.flags = flags | VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
beginInfo.pInheritanceInfo = pInheritanceInfo;
|
||||
VK_VERIFY(vkBeginCommandBuffer(frame.commandBuffer, &beginInfo), "BeginCommandRecording, vkBeginCommandBuffer");
|
||||
|
||||
@@ -137,6 +139,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_VERIFY(vkResetCommandBuffer(frame.preCommandBuffer, 0), "BeginPreCommandRecording, vkResetCommandBuffer");
|
||||
VkCommandBufferBeginInfo beginInfo{};
|
||||
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
VK_VERIFY(vkBeginCommandBuffer(frame.preCommandBuffer, &beginInfo),
|
||||
"BeginPreCommandRecording, vkBeginCommandBuffer");
|
||||
frame.isPreCommandRecording = true;
|
||||
|
||||
@@ -225,6 +225,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// to wait for a submission (DeferredWireRelease), and three lines from now the record
|
||||
// no longer carries it.
|
||||
const Uint64 orphanedUseSerial = resource->lastUseSerial;
|
||||
resource->contentSerial = ++m_wireContentSerial;
|
||||
// P11 B2: a T0 store is ALWAYS released through the serial-gated list (never "at once"),
|
||||
// so the client's AHardwareBuffer outlives every submission that can still name it however
|
||||
// the client's own release (at this respecify) raced the frames in flight.
|
||||
@@ -312,6 +313,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
std::abort();
|
||||
}
|
||||
if (size == 0) return;
|
||||
resource->contentSerial = ++m_wireContentSerial;
|
||||
Bool uploaded = false;
|
||||
const Uint64 probeLastUse = resource->lastUseSerial;
|
||||
const Uint64 probeCompleted = GetCompletedSerial();
|
||||
@@ -417,12 +419,91 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// ordered after earlier uses. There is no stale shadow to replay here.
|
||||
}
|
||||
|
||||
Bool VkBufferManager::LookupConvertedStream(const ConvertedStreamKey& key, SizeT elementCount,
|
||||
BufferSlice& outSlice) {
|
||||
for (auto& entry : m_convertedStreams) {
|
||||
if (!(entry.key == key) || entry.elementCount < elementCount) continue;
|
||||
entry.lastUseFrameSerial = m_frameSerial;
|
||||
outSlice = entry.buffer.GetSlice();
|
||||
outSlice.mapped = nullptr;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void VkBufferManager::RetireConvertedStream(SizeT index) {
|
||||
auto& entry = m_convertedStreams[index];
|
||||
m_convertedStreamBytes -= static_cast<SizeT>(entry.buffer.GetSize());
|
||||
// Its last use may be this frame: the slot's next BeginFrame is after this frame completed.
|
||||
if (m_currentFrameIndex < m_deferredBufferReleases.size())
|
||||
m_deferredBufferReleases[m_currentFrameIndex].push_back(std::move(entry.buffer));
|
||||
m_convertedStreams.erase(m_convertedStreams.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
}
|
||||
|
||||
Bool VkBufferManager::StoreConvertedStream(const ConvertedStreamKey& key, MG_Pipe::MGPipeHandle source,
|
||||
SizeT elementCount, const void* bytes, SizeT size,
|
||||
BufferSlice& outSlice) {
|
||||
constexpr SizeT kConvertedStreamBudget = 192ull << 20;
|
||||
if (size == 0 || size > kConvertedStreamBudget / 2 || m_initInfo.allocator == nullptr ||
|
||||
m_deferredBufferReleases.empty())
|
||||
return false;
|
||||
// A newer conversion of the same store and layout supersedes the old one (its content
|
||||
// serial can never come back); an entry unused for many frames (a deleted store's, a
|
||||
// layout no longer drawn) goes too.
|
||||
constexpr Uint64 kConvertedStreamIdleFrames = 256;
|
||||
for (SizeT i = m_convertedStreams.size(); i-- > 0;) {
|
||||
const auto& entry = m_convertedStreams[i];
|
||||
if (entry.lastUseFrameSerial + kConvertedStreamIdleFrames < m_frameSerial) {
|
||||
RetireConvertedStream(i);
|
||||
continue;
|
||||
}
|
||||
if (entry.source == source && entry.key.baseOffset == key.baseOffset && entry.key.stride == key.stride &&
|
||||
entry.key.type == key.type && entry.key.size == key.size && entry.key.conversion == key.conversion)
|
||||
RetireConvertedStream(i);
|
||||
}
|
||||
while (!m_convertedStreams.empty() && m_convertedStreamBytes + size > kConvertedStreamBudget) {
|
||||
SizeT oldest = 0;
|
||||
for (SizeT i = 1; i < m_convertedStreams.size(); ++i)
|
||||
if (m_convertedStreams[i].lastUseFrameSerial < m_convertedStreams[oldest].lastUseFrameSerial) oldest = i;
|
||||
RetireConvertedStream(oldest);
|
||||
}
|
||||
ConvertedStream entry;
|
||||
entry.key = key;
|
||||
entry.source = source;
|
||||
entry.elementCount = elementCount;
|
||||
entry.lastUseFrameSerial = m_frameSerial;
|
||||
if (!entry.buffer.Create({
|
||||
.allocator = m_initInfo.allocator,
|
||||
.size = static_cast<VkDeviceSize>(size),
|
||||
.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
|
||||
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
|
||||
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT,
|
||||
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||
}) || entry.buffer.Map() == nullptr || !entry.buffer.Upload(bytes, static_cast<VkDeviceSize>(size), 0))
|
||||
return false;
|
||||
m_convertedStreamBytes += size;
|
||||
outSlice = entry.buffer.GetSlice();
|
||||
outSlice.mapped = nullptr;
|
||||
m_convertedStreams.push_back(std::move(entry));
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VkBufferManager::GetWireBufferContentSerial(MG_Pipe::MGPipeHandle res, Uint64& serial) {
|
||||
auto* resource = FindWireBuffer(res);
|
||||
if (!resource || resource->imported || !resource->buffer.IsValid()) return false;
|
||||
serial = resource->contentSerial;
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VkBufferManager::AcquireWireSlice(BufferKind kind, MG_Pipe::MGPipeHandle res, BufferSlice& outSlice) {
|
||||
(void)kind; // The store carries every buffer usage, so changing roles never orphans it.
|
||||
// The store carries every buffer usage, so changing roles never orphans it.
|
||||
outSlice = {};
|
||||
auto* resource = FindWireBuffer(res);
|
||||
if (!resource || !resource->buffer.IsValid() || resource->size == 0) return false;
|
||||
resource->lastUseSerial = m_frameSerial;
|
||||
// A slice a shader may write through: whatever it writes is a new content state.
|
||||
if (kind == BufferKind::ShaderStorage || kind == BufferKind::TextureBuffer)
|
||||
resource->contentSerial = ++m_wireContentSerial;
|
||||
// B3: the draw this slice is being acquired for is normally recorded into the NEXT
|
||||
// submission - normally, not always: a mid-draw flush (SyncWireTextureShape's preserve
|
||||
// path) can submit this index without the draw. This stamp is the defence term's input
|
||||
@@ -581,6 +662,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!resource || offset > resource->size) return;
|
||||
if (size == MG_Pipe::kMGPipeWholeBuffer) size = resource->size - offset;
|
||||
if (size == 0 || size > resource->size - offset) return;
|
||||
resource->contentSerial = ++m_wireContentSerial;
|
||||
resource->lastUseSerial = m_frameSerial;
|
||||
resource->stagedCoverage.clear();
|
||||
resource->gpuWritesPending = true;
|
||||
@@ -721,6 +803,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void* mapped = resource->buffer.GetMappedData();
|
||||
if (mapped == nullptr) return nullptr;
|
||||
if (resource->imported) return mapped; // already donated: the same pointer
|
||||
resource->contentSerial = ++m_wireContentSerial;
|
||||
if (!WaitForWireBufferHostAccess(*resource)) return nullptr;
|
||||
if (seedBytes != nullptr) std::memcpy(mapped, seedBytes, static_cast<SizeT>(size));
|
||||
resource->imported = true;
|
||||
@@ -1448,6 +1531,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void VkBufferManager::DestroyAllDeferredReleases() {
|
||||
// Both callers (Shutdown, RecreateTransientArenas) have proven the device idle.
|
||||
DestroyAllDeferredWireReleases();
|
||||
for (auto& stream : m_convertedStreams) stream.buffer.Destroy();
|
||||
m_convertedStreams.clear();
|
||||
m_convertedStreamBytes = 0;
|
||||
for (auto& releases : m_deferredBufferReleases) {
|
||||
for (auto& buffer : releases) {
|
||||
buffer.Destroy();
|
||||
|
||||
@@ -158,6 +158,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// that memo names can have been freed and re-minted in between, which is the one
|
||||
// fact a handle-keyed memo of wire descriptors needs and cannot read off the handle.
|
||||
Uint64 GetWireStoreDestroyEpoch() const { return m_wireStoreDestroyEpoch; }
|
||||
// P15 S0: the store's content state, as a process-wide monotonic serial: it moves on every
|
||||
// host write, respecify and GPU-write mark, and on every bind a shader may write through
|
||||
// (storage / texture-buffer slices). False for a store whose bytes the client writes
|
||||
// directly (an imported or donated store), which no cache may key on.
|
||||
Bool GetWireBufferContentSerial(MG_Pipe::MGPipeHandle res, Uint64& serial);
|
||||
// P15 S0: CONVERTED WIRE VERTEX STREAMS, KEPT ACROSS FRAMES (the frontend arm's
|
||||
// ConvertedVertexStream cache, lost with it, and longer-lived than it: that one was per
|
||||
// frame). Key: the source store's content serial - process-wide monotonic, so no store or
|
||||
// handle identity can alias - and the layout. Each entry owns a vertex buffer holding the
|
||||
// converted elements; a newer conversion of the same store and layout replaces it, and the
|
||||
// whole set is bounded by bytes (least recently used first). A replaced or evicted buffer
|
||||
// is released when the frame slot that last used it comes round again.
|
||||
struct ConvertedStreamKey {
|
||||
Uint64 contentSerial = 0;
|
||||
Uint64 baseOffset = 0;
|
||||
Uint32 stride = 0, type = 0, size = 0, conversion = 0;
|
||||
Bool normalized = false, isInteger = false, isBgra = false;
|
||||
Bool operator==(const ConvertedStreamKey&) const = default;
|
||||
};
|
||||
Bool LookupConvertedStream(const ConvertedStreamKey& key, SizeT elementCount, BufferSlice& outSlice);
|
||||
Bool StoreConvertedStream(const ConvertedStreamKey& key, MG_Pipe::MGPipeHandle source, SizeT elementCount,
|
||||
const void* bytes, SizeT size, BufferSlice& outSlice);
|
||||
// Highest frame serial whose GPU work is known complete; serials at or
|
||||
// below it may be considered signaled. Drives IsResourceBusy and the
|
||||
// backend GL fence objects.
|
||||
@@ -173,6 +195,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// unsubmitted index as incomplete, but the stamp is taken before the draw is
|
||||
// recorded and a mid-draw flush can submit it without the draw.
|
||||
Uint64 lastUseSubmitIndex = 0;
|
||||
// P15 S0: see GetWireBufferContentSerial.
|
||||
Uint64 contentSerial = 0;
|
||||
Bool gpuWritesPending = false;
|
||||
// Only ranges actually submitted by resource_subdata are covered. No
|
||||
// shadow is retained: flush cannot replay stale bytes over GPU writes.
|
||||
@@ -329,6 +353,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// so that a memo taken before a re-initialize must not match
|
||||
// a handle minted after it.
|
||||
Uint64 m_wireStoreDestroyEpoch = 0;
|
||||
Uint64 m_wireContentSerial = 0;
|
||||
struct ConvertedStream {
|
||||
ConvertedStreamKey key;
|
||||
MG_Pipe::MGPipeHandle source = MG_Pipe::kMGPipeNullHandle;
|
||||
SizeT elementCount = 0;
|
||||
VkBufferObject buffer;
|
||||
Uint64 lastUseFrameSerial = 0;
|
||||
};
|
||||
Vector<ConvertedStream> m_convertedStreams;
|
||||
SizeT m_convertedStreamBytes = 0;
|
||||
void RetireConvertedStream(SizeT index);
|
||||
Uint32 m_currentFrameIndex = 0;
|
||||
Uint64 m_frameSerial = 1;
|
||||
Uint64 m_completedSerialFloor = 0;
|
||||
|
||||
@@ -105,6 +105,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VkRenderPassManager::BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry,
|
||||
VkRenderPass renderPass, const VkClearValue* clearValues,
|
||||
Uint32 clearValueCount) {
|
||||
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
begin.renderPass = renderPass;
|
||||
begin.framebuffer = renderPassEntry.framebuffer;
|
||||
begin.renderArea = {{0, 0}, {static_cast<Uint32>(renderPassEntry.extent.x()),
|
||||
static_cast<Uint32>(renderPassEntry.extent.y())}};
|
||||
begin.clearValueCount = clearValueCount;
|
||||
begin.pClearValues = clearValues;
|
||||
vkCmdBeginRenderPass(commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
s_activeRenderPass.Get().commandBuffer = commandBuffer;
|
||||
s_activeRenderPass.Get().hash = renderPassEntry.hash;
|
||||
s_activeRenderPass.Get().compatibilityHash = renderPassEntry.compatibilityHash;
|
||||
s_activeRenderPass.Get().extent = renderPassEntry.extent;
|
||||
s_hasActiveRenderPass.Get() = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VkRenderPassManager::EndRenderPass(VkCommandBuffer commandBuffer) {
|
||||
BumpWireImageWriteEpoch();
|
||||
vkCmdEndRenderPass(commandBuffer);
|
||||
|
||||
@@ -47,6 +47,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 layers = 1;
|
||||
// Frame counter value of the last GetOrCreateRenderPass hit; drives cache eviction.
|
||||
Uint64 lastUsedFrame = 0;
|
||||
// P15 S0 (wire passes only): what the render pass was created from, and the render
|
||||
// passes that differ from it only in loading some attachments with LOAD_OP_CLEAR. Load ops
|
||||
// are not part of render-pass compatibility, so a variant begins with this entry's own
|
||||
// framebuffer and runs every pipeline created against `renderPass`.
|
||||
Vector<VkAttachmentDescription> wireAttachments;
|
||||
Vector<VkAttachmentReference> wireColors;
|
||||
VkAttachmentReference wireDepth{VK_ATTACHMENT_UNUSED, VK_IMAGE_LAYOUT_GENERAL};
|
||||
Vector<std::pair<Uint64, VkRenderPass>> clearVariants;
|
||||
|
||||
RenderPassEntry() = default;
|
||||
RenderPassEntry(const RenderPassEntry&) = delete;
|
||||
@@ -62,6 +70,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
std::swap(extent, that.extent);
|
||||
std::swap(layers, that.layers);
|
||||
std::swap(lastUsedFrame, that.lastUsedFrame);
|
||||
std::swap(wireAttachments, that.wireAttachments);
|
||||
std::swap(wireColors, that.wireColors);
|
||||
std::swap(wireDepth, that.wireDepth);
|
||||
std::swap(clearVariants, that.clearVariants);
|
||||
}
|
||||
// Move ASSIGNMENT, not just construction. The move constructor above and the
|
||||
// destructor below each independently suppress the implicit one, which left the
|
||||
@@ -94,10 +106,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
std::swap(extent, that.extent);
|
||||
std::swap(layers, that.layers);
|
||||
std::swap(lastUsedFrame, that.lastUsedFrame);
|
||||
std::swap(wireAttachments, that.wireAttachments);
|
||||
std::swap(wireColors, that.wireColors);
|
||||
std::swap(wireDepth, that.wireDepth);
|
||||
std::swap(clearVariants, that.clearVariants);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
~RenderPassEntry() {
|
||||
for (const auto& variant : clearVariants)
|
||||
if (variant.second != VK_NULL_HANDLE) vkDestroyRenderPass(s_device.Get(), variant.second, nullptr);
|
||||
if (renderPass != VK_NULL_HANDLE) {
|
||||
vkDestroyRenderPass(s_device.Get(), renderPass, nullptr);
|
||||
}
|
||||
@@ -147,6 +165,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_defaultFramebufferTarget = serial;
|
||||
}
|
||||
static Bool BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry);
|
||||
// P15 S0: begins `renderPassEntry` through `renderPass` (one of its clear-load variants)
|
||||
// with these clear values; it is active as the entry itself, so a draw continues it.
|
||||
static Bool BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry,
|
||||
VkRenderPass renderPass, const VkClearValue* clearValues, Uint32 clearValueCount);
|
||||
static Bool EndRenderPass(VkCommandBuffer commandBuffer);
|
||||
static ActiveRenderPassInfo* GetActiveRenderPass();
|
||||
private:
|
||||
|
||||
@@ -2371,6 +2371,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
WireDeclineTally::Dump("shutdown");
|
||||
// The SPIR-V screen's engagement proof: a decline-free session above reads as "screened
|
||||
// and valid" only with this count beside it (distinct modules, process-wide).
|
||||
MGLOG_I("MGWIRE-VCONV[shutdown] converted=%llu reused=%llu", static_cast<unsigned long long>(m_wireStreamsConverted),
|
||||
static_cast<unsigned long long>(m_wireStreamsReused));
|
||||
MGLOG_I("MGWIRE-SPIRV-SCREEN[shutdown] modulesValidated=%llu",
|
||||
static_cast<unsigned long long>(ProgramFactory::DriverModuleValidatorRuns()));
|
||||
DestroyWireDrawPass();
|
||||
|
||||
@@ -880,6 +880,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool WirePassMemoHolds(const MG_Pipe::MGPFramebufferState& fbo, const FrameContext::FrameData& frame) const;
|
||||
Uint64 m_wirePassMarksClearGeneration = 0;
|
||||
WireDrawScratch m_wireDrawScratch;
|
||||
Uint64 m_wireStreamsConverted = 0, m_wireStreamsReused = 0;
|
||||
Bool PrepareWireDrawPass(FrameContext::FrameData& frame, const MG_Pipe::MGPFramebufferState& fbo,
|
||||
WireDrawScratch& scratch, Bool passContinues, Bool& continuing);
|
||||
// P15: glClear / glClearBuffer* into the attachments of the wire draw pass, recorded inside it
|
||||
|
||||
@@ -173,10 +173,14 @@
|
||||
if (m_wireColorBlitResources) ResetWireColorBlitFramePool(frameIndex);
|
||||
const Uint64 imageEpoch = m_textureManager->GetTextureImageEpoch();
|
||||
const Uint64 eraseEpoch = m_textureManager->GetResourceEraseEpoch();
|
||||
const Bool keepAcrossFrames = MG_Config::Features.MagmaPassCache != MG_Config::QuirkOverride::ForceOff;
|
||||
SizeT kept = 0;
|
||||
for (SizeT i = 0; i < m_wireDrawPassCache.size(); ++i) {
|
||||
auto& entry = m_wireDrawPassCache[i];
|
||||
if (entry.key.textureImageEpoch == imageEpoch && entry.key.resourceEraseEpoch == eraseEpoch) {
|
||||
const Bool live = keepAcrossFrames ? entry.key.textureImageEpoch == imageEpoch &&
|
||||
entry.key.resourceEraseEpoch == eraseEpoch
|
||||
: entry.lastUseSubmit > m_completedSubmitCounter;
|
||||
if (live) {
|
||||
if (kept != i) m_wireDrawPassCache[kept] = Move(entry);
|
||||
++kept;
|
||||
continue;
|
||||
@@ -426,6 +430,9 @@
|
||||
created.extent = {static_cast<Int>(extent.width), static_cast<Int>(extent.height)};
|
||||
created.layers = layers;
|
||||
created.hash = created.compatibilityHash = XXH64(colors.data(), colors.size() * sizeof(colors[0]), passHash);
|
||||
created.wireAttachments.assign(attachments.begin(), attachments.end());
|
||||
created.wireColors.assign(colors.begin(), colors.end());
|
||||
created.wireDepth = depth;
|
||||
}
|
||||
// The attachments' GPU-write marks (MarkWireTextureGpuWritten): idempotent while nothing
|
||||
// clears a GPU-dirty mark, so a draw that continues a pass whose draws already marked the
|
||||
@@ -641,6 +648,19 @@
|
||||
MGL_WIRE_DECLINE_AT(VertexStreamCountZero, "the converted vertex stream would hold no elements");
|
||||
return false;
|
||||
}
|
||||
// One conversion per (source content, layout), kept across frames until the source
|
||||
// changes (VkBufferManager's converted-stream cache): a multi-draw over a chunk arena
|
||||
// names the same stream for every chunk, frame after frame.
|
||||
Uint64 contentSerial = 0;
|
||||
const Bool cacheable = MG_Config::Features.MagmaVertexConversionCache != MG_Config::QuirkOverride::ForceOff &&
|
||||
m_bufferManager.GetWireBufferContentSerial(resource.Res, contentSerial);
|
||||
const VkBufferManager::ConvertedStreamKey cacheKey{contentSerial, offset, static_cast<Uint32>(stride),
|
||||
static_cast<Uint32>(attr.Type), static_cast<Uint32>(attr.Size), static_cast<Uint32>(conversion),
|
||||
attr.Normalized != 0, attr.IsInteger != 0, attr.IsBgra != 0};
|
||||
if (cacheable && m_bufferManager.LookupConvertedStream(cacheKey, count, vertexSlices[binding])) {
|
||||
++m_wireStreamsReused;
|
||||
continue;
|
||||
}
|
||||
Vector<Uint8> source((count - 1) * stride + elementSize);
|
||||
if (!m_bufferManager.ReadWireBuffer(resource.Res, offset, source.size(), source.data())) {
|
||||
MGL_WIRE_DECLINE_AT(VertexStreamRead, "the vertex bytes to convert could not be read back");
|
||||
@@ -664,7 +684,10 @@
|
||||
WireDeclineTally::Count(WireDeclineSite::VertexStreamConverter);
|
||||
return false;
|
||||
}
|
||||
if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(),
|
||||
++m_wireStreamsConverted;
|
||||
if (!(cacheable && m_bufferManager.StoreConvertedStream(cacheKey, resource.Res, count, converted.data(),
|
||||
converted.size(), vertexSlices[binding])) &&
|
||||
!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(),
|
||||
converted.data(), converted.size(), 16, vertexSlices[binding])) {
|
||||
MGL_WIRE_DECLINE_AT(VertexTransientUpload, "the converted vertex stream did not fit the transient arena");
|
||||
return false;
|
||||
@@ -865,29 +888,40 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
// P15 S0: A CLEAR INTO THE OPEN PASS'S ATTACHMENTS IS RECORDED INSIDE THE PASS.
|
||||
// P15 S0: A CLEAR INTO THE ATTACHMENTS OF THE DRAW PASS BECOMES PART OF THAT PASS.
|
||||
//
|
||||
// The standalone path (ClearWireFramebuffer's per-surface lambda) closes the draw pass, then
|
||||
// builds an image view, a render pass and a framebuffer for every cleared surface, runs a pass
|
||||
// of its own and retires all three: about 70 us a clear on Adreno 750, and every following draw
|
||||
// to the same framebuffer opens a new pass (FCL MC 1.21.5: 6 clears a frame, 0.42 ms). Here the
|
||||
// clear takes the pass a draw to this framebuffer would use (PrepareWireDrawPass: the open one,
|
||||
// a cached one, or a new one through the same walk) and becomes a vkCmdClearAttachments in it,
|
||||
// so the draws after it continue the same pass.
|
||||
// a cached one, or a new one through the same walk), so the draws after it continue that pass:
|
||||
//
|
||||
// vkCmdClearAttachments inside the subpass is ordered with the pass's draws as a draw is, writes
|
||||
// through the same attachment views (the UNORM twin of an sRGB image while GL_FRAMEBUFFER_SRGB
|
||||
// is off, which is exactly GL's raw clear - no pre-compensation), and needs no layout change:
|
||||
// wire attachments live in GENERAL. What it cannot express goes back to the standalone path,
|
||||
// per GL buffer bit: a partial colour or stencil write mask (that path warns and skips, as
|
||||
// before), an attachment whose extent or layer count differs from the pass's (the pass covers the
|
||||
// common area only), and a stencil clear the pass's depth attachment does not carry.
|
||||
// * A clear of the whole render area that would BEGIN the pass begins it with LOAD_OP_CLEAR for
|
||||
// the cleared attachments (a clear-load variant of the pass's render pass). Nothing is loaded
|
||||
// from memory into the tile for them, which is also the cheapest form on a tiler.
|
||||
// * Any other clear - into the open pass, or scissored - is vkCmdClearAttachments in the pass:
|
||||
// ordered with the pass's draws as a draw is.
|
||||
//
|
||||
// Every write goes through the pass's attachment views (the UNORM twin of an sRGB image while
|
||||
// GL_FRAMEBUFFER_SRGB is off, exactly GL's raw clear - no pre-compensation), and wire attachments
|
||||
// live in GENERAL. What none of this expresses goes back to the standalone path, per GL buffer
|
||||
// bit: a partial colour or stencil write mask (that path warns and skips, as before), an
|
||||
// attachment whose extent or layer count differs from the pass's (the pass covers the common area
|
||||
// only), and a stencil clear the pass's depth attachment does not carry.
|
||||
GLbitfield VulkanRenderer::ClearWireFramebufferInPass(const MG_Pipe::MGPFramebufferState& fbo,
|
||||
const ClearAttachmentPayload& payload, GLint drawbuffer) {
|
||||
GLbitfield remaining = payload.mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
if (!remaining || MG_Config::Features.MagmaInPassClear == MG_Config::QuirkOverride::ForceOff) return remaining;
|
||||
// Nested inside a draw's preparation the scratch and the pass belong to that draw.
|
||||
if (m_wirePreparationDepth != 0) return remaining;
|
||||
// NOT THE DEFAULT FRAMEBUFFER (measured, not understood): on Adreno 750 the openra retrace
|
||||
// loses its palette-sampled sprites (SSIM 0.956) when a colour+depth clear of the default
|
||||
// framebuffer becomes part of its draw pass - with LOAD_OP_CLEAR as well as with
|
||||
// vkCmdClearAttachments - while the same clear split across the two paths, or left
|
||||
// standalone, renders correctly. Default-framebuffer clears keep the standalone path until
|
||||
// that is explained (perf-p15/HANDOFF.md).
|
||||
if (fbo.IsDefault && MG_Config::Features.MagmaInPassClearDefault != MG_Config::QuirkOverride::ForceOn) return remaining;
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
WirePreparationScope preparing(m_wirePreparationDepth);
|
||||
// As SetupWireDraw: resolve every attachment (a lazy upload or promotion may submit the
|
||||
@@ -992,16 +1026,66 @@
|
||||
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>(key.width));
|
||||
const Int32 y1 = std::min(box.y() + box.w(), static_cast<Int32>(key.height));
|
||||
if (x0 >= x1 || y0 >= y1) return remaining & ~(handled | GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT |
|
||||
GL_STENCIL_BUFFER_BIT); // an empty box clears nothing
|
||||
// An empty box clears nothing, on either path.
|
||||
if (x0 >= x1 || y0 >= y1) return 0;
|
||||
area = {{x0, y0}, {static_cast<Uint32>(x1 - x0), static_cast<Uint32>(y1 - y0)}};
|
||||
if (fbo.IsDefault && !MapDefaultFramebufferReadbackRect(x0, y0, x1 - x0, y1 - y0, VkExtent2D{key.width, key.height},
|
||||
m_swapchainObject.GetPreTransform(), &area.offset, &area.extent))
|
||||
MagmaWireFatal("clear-surface-transform");
|
||||
}
|
||||
const Bool wholeArea = area.offset.x == 0 && area.offset.y == 0 && area.extent.width == key.width &&
|
||||
area.extent.height == key.height;
|
||||
const auto* active = VkRenderPassManager::GetActiveRenderPass();
|
||||
const Bool open = continuing && active != nullptr && active->commandBuffer == frame.commandBuffer &&
|
||||
active->hash == pass.hash;
|
||||
Bool open = continuing && active != nullptr && active->commandBuffer == frame.commandBuffer &&
|
||||
active->hash == pass.hash;
|
||||
if (wholeArea && !open) {
|
||||
if (active != nullptr) VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
auto& begun = *m_wireDrawPass;
|
||||
Uint64 variantKey = 0;
|
||||
VkClearValue values[MG_Pipe::kMGPipeMaxColorAttachments + 1]{};
|
||||
const Uint32 attachmentCount = static_cast<Uint32>(begun.wireAttachments.size());
|
||||
if (attachmentCount == 0 || attachmentCount > MG_Pipe::kMGPipeMaxColorAttachments + 1)
|
||||
return remaining; // not a wire-built pass: cannot happen, but never guess
|
||||
for (Uint32 c = 0; c < count; ++c) {
|
||||
const auto& clear = clears[c];
|
||||
if (clear.aspectMask & VK_IMAGE_ASPECT_COLOR_BIT) {
|
||||
const Uint32 a = begun.wireColors[clear.colorAttachment].attachment;
|
||||
variantKey |= 1ull << (2 * a);
|
||||
values[a].color = clear.clearValue.color;
|
||||
} else {
|
||||
const Uint32 a = begun.wireDepth.attachment;
|
||||
if (clear.aspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) variantKey |= 1ull << (2 * a);
|
||||
if (clear.aspectMask & VK_IMAGE_ASPECT_STENCIL_BIT) variantKey |= 1ull << (2 * a + 1);
|
||||
values[a].depthStencil = clear.clearValue.depthStencil;
|
||||
}
|
||||
}
|
||||
VkRenderPass variant = VK_NULL_HANDLE;
|
||||
for (const auto& entry : begun.clearVariants)
|
||||
if (entry.first == variantKey) variant = entry.second;
|
||||
if (variant == VK_NULL_HANDLE) {
|
||||
auto descriptions = begun.wireAttachments;
|
||||
for (Uint32 a = 0; a < attachmentCount; ++a) {
|
||||
if (variantKey >> (2 * a) & 1) descriptions[a].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||
if (variantKey >> (2 * a + 1) & 1) descriptions[a].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||
}
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.colorAttachmentCount = static_cast<Uint32>(begun.wireColors.size());
|
||||
subpass.pColorAttachments = begun.wireColors.data();
|
||||
subpass.pDepthStencilAttachment =
|
||||
begun.wireDepth.attachment != VK_ATTACHMENT_UNUSED ? &begun.wireDepth : nullptr;
|
||||
VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
info.attachmentCount = attachmentCount;
|
||||
info.pAttachments = descriptions.data();
|
||||
info.subpassCount = 1;
|
||||
info.pSubpasses = &subpass;
|
||||
VK_VERIFY(vkCreateRenderPass(m_device, &info, nullptr, &variant));
|
||||
begun.clearVariants.push_back({variantKey, variant});
|
||||
}
|
||||
if (!VkRenderPassManager::BeginRenderPass(frame.commandBuffer, begun, variant, values, attachmentCount))
|
||||
return remaining;
|
||||
return remaining & ~handled;
|
||||
}
|
||||
if (!open && active != nullptr) VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
if (!open && !VkRenderPassManager::BeginRenderPass(frame.commandBuffer, pass)) return remaining;
|
||||
const VkClearRect rect{area, 0, key.layers};
|
||||
|
||||
@@ -98,6 +98,8 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||
# P15 S0: a clear between draws into the same framebuffer (DirectVulkan records it in the pass).
|
||||
Scenarios/InPassClearScenario.cpp
|
||||
# P15 S0: a CPU-converted vertex stream reused and re-written within a frame (DirectVulkan caches it).
|
||||
Scenarios/ConvertedVertexStreamScenario.cpp
|
||||
Scenarios/SampleVariablesScenario.cpp
|
||||
Scenarios/DepthStencilReadbackScenario.cpp
|
||||
Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
||||
@@ -2455,6 +2457,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
|
||||
mgl_itest_register_split_arms("ClearThenReadPixelsScenario.*" "")
|
||||
mgl_itest_register_split_arms("InPassClearScenario.*" "")
|
||||
mgl_itest_register_split_arms("ConvertedVertexStreamScenario.*" "")
|
||||
# GL_ARB_shader_objects (P13): ARB-created shader / program objects travel the record arm like
|
||||
# core-created ones, so the scenario runs on every split arm as well as both monoliths.
|
||||
mgl_itest_register_split_arms("ArbShaderObjectsScenario.*" "")
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ConvertedVertexStreamScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A VERTEX STREAM THE BACKEND CONVERTS ON THE CPU, REUSED AND RE-WRITTEN WITHIN A FRAME.
|
||||
//
|
||||
// P15 S0: DirectVulkan's wire arm converts a vertex stream whose layout the device cannot fetch
|
||||
// (here a 16-bit attribute at an odd stride, which it repacks) once per (buffer content, layout)
|
||||
// and reuses the converted copy for every draw that names it, frame after frame - a chunk
|
||||
// renderer's multi-draws all name one arena. What that cache must get right:
|
||||
//
|
||||
// * REUSE: two indexed draws in one frame over different base vertices of one converted stream
|
||||
// both read their own vertices out of the shared conversion.
|
||||
// * INVALIDATION: a glBufferSubData of the source between two draws in the SAME frame is seen by
|
||||
// the second draw (the store's content serial moved, so the cached conversion is not reused).
|
||||
// * ACROSS FRAMES: an unchanged stream drawn over more frames than there are frame slots keeps
|
||||
// drawing right, and a glBufferSubData between frames is seen by the next frame's draw.
|
||||
//
|
||||
// DirectGLES is the control; MOBILEGL_MAGMA_VERTEX_CONVERSION_CACHE=0 converts per draw.
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
layout(location = 0) in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos * 2.0 - 1.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
uniform vec4 uColor;
|
||||
out vec4 o0;
|
||||
void main() { o0 = uColor; }
|
||||
)";
|
||||
|
||||
constexpr int kSize = 64;
|
||||
// 5-byte stride: the 4-byte (2 x GL_UNSIGNED_SHORT) attribute sits at odd offsets, which
|
||||
// no device fetches directly, so Magma repacks the stream on the CPU.
|
||||
constexpr int kStride = 5;
|
||||
|
||||
class ConvertedVertexStreamScenario : public ScenarioTest {};
|
||||
|
||||
// Writes vertex `index` of the interleaved stream: (x, y) in [0, 1] as normalized shorts.
|
||||
void PutVertex(std::vector<unsigned char>& bytes, int index, float x, float y) {
|
||||
const unsigned short sx = static_cast<unsigned short>(x * 65535.0f + 0.5f);
|
||||
const unsigned short sy = static_cast<unsigned short>(y * 65535.0f + 0.5f);
|
||||
std::memcpy(bytes.data() + index * kStride, &sx, 2);
|
||||
std::memcpy(bytes.data() + index * kStride + 2, &sy, 2);
|
||||
}
|
||||
|
||||
// Four vertices of an axis-aligned quad as a triangle strip.
|
||||
void PutQuad(std::vector<unsigned char>& bytes, int first, float x0, float y0, float x1, float y1) {
|
||||
PutVertex(bytes, first + 0, x0, y0);
|
||||
PutVertex(bytes, first + 1, x1, y0);
|
||||
PutVertex(bytes, first + 2, x0, y1);
|
||||
PutVertex(bytes, first + 3, x1, y1);
|
||||
}
|
||||
|
||||
::testing::AssertionResult Near(const Rgba8& got, int r, int g, int b) {
|
||||
const auto close = [](int v, int want) { return v >= want - 3 && v <= want + 3; };
|
||||
if (close(got.r, r) && close(got.g, g) && close(got.b, b)) return ::testing::AssertionSuccess();
|
||||
return ::testing::AssertionFailure() << "got (" << int(got.r) << ", " << int(got.g) << ", " << int(got.b)
|
||||
<< "), want (" << r << ", " << g << ", " << b << ")";
|
||||
}
|
||||
|
||||
struct Target {
|
||||
GLuint fbo = 0, color = 0;
|
||||
};
|
||||
|
||||
Target MakeTarget() {
|
||||
Target t;
|
||||
glGenTextures(1, &t.color);
|
||||
glBindTexture(GL_TEXTURE_2D, t.color);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize);
|
||||
glGenFramebuffers(1, &t.fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, t.fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, t.color, 0);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
return t;
|
||||
}
|
||||
|
||||
Rgba8 At(int x, int y) {
|
||||
const Image image = ReadPixelsRect(x, y, 1, 1);
|
||||
return image.At(0, 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(ConvertedVertexStreamScenario, TwoIndexedDrawsShareOneConversionAndReadTheirOwnVertices) {
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
Target target = MakeTarget();
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
std::vector<unsigned char> bytes(8 * kStride, 0);
|
||||
PutQuad(bytes, 0, 0.0f, 0.0f, 0.5f, 1.0f); // left half
|
||||
PutQuad(bytes, 4, 0.5f, 0.0f, 1.0f, 1.0f); // right half
|
||||
const unsigned short indices[] = {0, 1, 2, 3};
|
||||
GLuint vao = 0, vbo = 0, ibo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(bytes.size()), bytes.data(), GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_UNSIGNED_SHORT, GL_TRUE, kStride, nullptr);
|
||||
glGenBuffers(1, &ibo);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
|
||||
|
||||
glUseProgram(program);
|
||||
const GLint color = glGetUniformLocation(program, "uColor");
|
||||
glUniform4f(color, 1.0f, 0.0f, 0.0f, 1.0f);
|
||||
glDrawElementsBaseVertex(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_SHORT, nullptr, 0);
|
||||
glUniform4f(color, 0.0f, 1.0f, 0.0f, 1.0f);
|
||||
glDrawElementsBaseVertex(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_SHORT, nullptr, 4);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
EXPECT_TRUE(Near(At(kSize / 4, kSize / 2), 255, 0, 0)) << "left half: the first draw's vertices";
|
||||
EXPECT_TRUE(Near(At(kSize * 3 / 4, kSize / 2), 0, 255, 0)) << "right half: base vertex 4 of the same stream";
|
||||
|
||||
glDeleteBuffers(1, &ibo);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &target.fbo);
|
||||
glDeleteTextures(1, &target.color);
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
|
||||
TEST_F(ConvertedVertexStreamScenario, ASubDataBetweenTwoDrawsInOneFrameIsSeenByTheSecond) {
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
Target target = MakeTarget();
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
std::vector<unsigned char> bytes(4 * kStride, 0);
|
||||
PutQuad(bytes, 0, 0.0f, 0.0f, 0.5f, 1.0f); // left half
|
||||
const unsigned short indices[] = {0, 1, 2, 3};
|
||||
GLuint vao = 0, vbo = 0, ibo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(bytes.size()), bytes.data(), GL_DYNAMIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_UNSIGNED_SHORT, GL_TRUE, kStride, nullptr);
|
||||
glGenBuffers(1, &ibo);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
|
||||
|
||||
glUseProgram(program);
|
||||
const GLint color = glGetUniformLocation(program, "uColor");
|
||||
glUniform4f(color, 1.0f, 0.0f, 0.0f, 1.0f);
|
||||
glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_SHORT, nullptr);
|
||||
// Same buffer, same layout, new bytes: the quad moves to the right half.
|
||||
PutQuad(bytes, 0, 0.5f, 0.0f, 1.0f, 1.0f);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, 0, static_cast<GLsizeiptr>(bytes.size()), bytes.data());
|
||||
glUniform4f(color, 0.0f, 0.0f, 1.0f, 1.0f);
|
||||
glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_SHORT, nullptr);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
EXPECT_TRUE(Near(At(kSize / 4, kSize / 2), 255, 0, 0)) << "left half: the draw before the sub-data";
|
||||
EXPECT_TRUE(Near(At(kSize * 3 / 4, kSize / 2), 0, 0, 255))
|
||||
<< "right half: the draw after the sub-data must read the new bytes, not the cached conversion";
|
||||
|
||||
glDeleteBuffers(1, &ibo);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &target.fbo);
|
||||
glDeleteTextures(1, &target.color);
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
|
||||
TEST_F(ConvertedVertexStreamScenario, AConversionKeptAcrossFramesIsReplacedByASubDataBetweenFrames) {
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
Target target = MakeTarget();
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
std::vector<unsigned char> bytes(4 * kStride, 0);
|
||||
PutQuad(bytes, 0, 0.0f, 0.0f, 0.5f, 1.0f); // left half
|
||||
GLuint vao = 0, vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(bytes.size()), bytes.data(), GL_DYNAMIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_UNSIGNED_SHORT, GL_TRUE, kStride, nullptr);
|
||||
glUseProgram(program);
|
||||
const GLint color = glGetUniformLocation(program, "uColor");
|
||||
|
||||
// Several frames - more than any frame-slot count - drawing the one unchanged stream: every
|
||||
// frame after the first reuses the conversion the first one made.
|
||||
for (int frame = 0; frame < 5; ++frame) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUniform4f(color, 1.0f, static_cast<float>(frame) / 4.0f, 0.0f, 1.0f);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
EXPECT_TRUE(Near(At(kSize / 4, kSize / 2), 255, (frame * 255 + 2) / 4, 0)) << "frame " << frame;
|
||||
EXPECT_TRUE(Near(At(kSize * 3 / 4, kSize / 2), 0, 0, 0)) << "frame " << frame;
|
||||
Gl().EndFrame();
|
||||
}
|
||||
// New bytes between frames: the next frame's draw must not reuse the kept conversion.
|
||||
PutQuad(bytes, 0, 0.5f, 0.0f, 1.0f, 1.0f);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, 0, static_cast<GLsizeiptr>(bytes.size()), bytes.data());
|
||||
for (int frame = 0; frame < 3; ++frame) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUniform4f(color, 0.0f, 0.0f, 1.0f, 1.0f);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
EXPECT_TRUE(Near(At(kSize / 4, kSize / 2), 0, 0, 0))
|
||||
<< "frame " << frame << " after the sub-data: the old conversion was reused";
|
||||
EXPECT_TRUE(Near(At(kSize * 3 / 4, kSize / 2), 0, 0, 255)) << "frame " << frame << " after the sub-data";
|
||||
Gl().EndFrame();
|
||||
}
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &target.fbo);
|
||||
glDeleteTextures(1, &target.color);
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -292,4 +292,68 @@ void main() { o0 = uColor; o1 = uColor; }
|
||||
glDeleteTextures(1, &color);
|
||||
}
|
||||
|
||||
// The openra shape on the DEFAULT framebuffer (P15 S0's open item, perf-p15/HANDOFF.md): every
|
||||
// frame starts with glClear(COLOR | DEPTH), then re-specifies a palette texture
|
||||
// (glTexImage2D - a recording flush on Magma) before the first draw samples it, and the
|
||||
// sprites are depth-tested. Two frames, so the clear lands on a freshly acquired image.
|
||||
TEST_F(InPassClearScenario, ADefaultFramebufferClearThenAPaletteUploadKeepsTheDepthTestedDraw) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width(), height = gl.Height();
|
||||
constexpr const char* kPaletteVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
uniform float uDepth;
|
||||
void main() { gl_Position = vec4(aPos, uDepth, 1.0); }
|
||||
)";
|
||||
constexpr const char* kPaletteFS = R"(#version 330 core
|
||||
uniform sampler2D uPalette;
|
||||
out vec4 o0;
|
||||
void main() { o0 = texture(uPalette, vec2(0.5, 0.5)); }
|
||||
)";
|
||||
std::string error;
|
||||
const unsigned int paletteProgram = CompileProgram(kPaletteVS, kPaletteFS, &error);
|
||||
ASSERT_NE(paletteProgram, 0u) << error;
|
||||
Build();
|
||||
GLuint palette = 0;
|
||||
glGenTextures(1, &palette);
|
||||
for (int frame = 0; frame < 3; ++frame) {
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClearDepth(1.0);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
// The palette changes every frame; this frame's colour is what the sprite must show.
|
||||
const unsigned char texel[4] = {static_cast<unsigned char>(frame == 2 ? 0 : 255), 255, 0, 255};
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, palette);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, texel);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
// Terrain: no depth test, red.
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
Quad(1.0f, 0.0f, 0.0f, 1.0f, 0.5f);
|
||||
// Sprites: depth-tested against the cleared depth, palette-coloured.
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
glUseProgram(paletteProgram);
|
||||
glUniform1i(glGetUniformLocation(paletteProgram, "uPalette"), 0);
|
||||
glUniform1f(glGetUniformLocation(paletteProgram, "uDepth"), 0.0f);
|
||||
glBindVertexArray(vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
if (frame == 2) {
|
||||
const Image image = ReadPixels(width, height);
|
||||
EXPECT_TRUE(Near(image.At(width / 2, height / 2), 0, 255, 0, 255))
|
||||
<< "frame 2: the depth-tested sprite shows this frame's palette (green)";
|
||||
}
|
||||
gl.EndFrame();
|
||||
}
|
||||
glDeleteTextures(1, &palette);
|
||||
glDeleteProgram(paletteProgram);
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
|
||||
Reference in New Issue
Block a user