WriteBufferDescriptor / WriteSampledImageDescriptor / RegisterSampler / WriteSwapchainDescriptors each FlushDevice()'d the entire multi-KB per-frame descriptor heap after writing one few-byte descriptor. Add a ranged VulkanBuffer::FlushDevice(offset, bytes) that flushes only the touched bytes, rounding the range outward to nonCoherentAtomSize as vkFlushMappedMemoryRanges requires (the WHOLE_SIZE path sidestepped that). The coherent-memory gate from issue #60 is preserved — coherent memory still skips the flush entirely. The descriptor writers now self-flush their written range, so the redundant whole-heap flushes in UIRenderer::Initialize and the resize callback are gone. Rounding lives in AlignMappedFlushRange (pure math) and is unit-tested without a device in the new VulkanBufferRangedFlush test; the change was also verified end-to-end by running HelloUI on a real GPU with validation layers (font-atlas glyphs, sampler and storage-image descriptors all flush correctly, no VUIDs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
124 lines
5.1 KiB
C++
124 lines
5.1 KiB
C++
/*
|
|
Crafter®.Graphics
|
|
Copyright (C) 2026 Catcrafts®
|
|
catcrafts.net
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License version 3.0 as published by the Free Software Foundation;
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with this library; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
// Regression test for the ranged FlushDevice (UI descriptor registration):
|
|
// WriteBufferDescriptor / WriteSampledImageDescriptor / RegisterSampler used to
|
|
// FlushDevice() the whole multi-KB descriptor heap after writing one few-byte
|
|
// descriptor. They now flush only the written byte range via
|
|
// VulkanBuffer::FlushDevice(offset, bytes), which rounds the range outward to
|
|
// VkPhysicalDeviceLimits::nonCoherentAtomSize (vkFlushMappedMemoryRanges rejects
|
|
// an unaligned sub-buffer offset/size).
|
|
//
|
|
// The rounding lives in AlignMappedFlushRange — pure math with no Vulkan
|
|
// dependency — so this drives it directly with no GPU device, and also confirms
|
|
// the ranged FlushDevice keeps the issue-#60 coherent-memory gate (a real flush
|
|
// would fault with Device::device == VK_NULL_HANDLE).
|
|
|
|
#include <cstdlib>
|
|
#include "vulkan/vulkan.h"
|
|
|
|
import Crafter.Graphics;
|
|
import std;
|
|
using namespace Crafter;
|
|
|
|
namespace {
|
|
|
|
int failures = 0;
|
|
|
|
void Check(bool ok, std::string_view what) {
|
|
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
|
if (!ok) ++failures;
|
|
}
|
|
|
|
constexpr auto HOST_VISIBLE = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT;
|
|
constexpr auto HOST_COHERENT = VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
|
|
constexpr auto DEVICE_LOCAL = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
Check(Device::device == VK_NULL_HANDLE,
|
|
"no Vulkan device created — a real flush would fault");
|
|
|
|
// ── AlignMappedFlushRange: the spec demands offset and size be multiples
|
|
// of nonCoherentAtomSize (atom). Start rounds down, end rounds up. ──
|
|
{
|
|
// A descriptor wholly inside one atom expands to cover that atom.
|
|
auto r = AlignMappedFlushRange(/*offset*/ 80, /*bytes*/ 16,
|
|
/*atom*/ 64, /*mappingSize*/ 4096);
|
|
Check(r.offset == 64 && r.size == 64,
|
|
"sub-atom range rounds out to the enclosing atom [64,128)");
|
|
Check(r.offset % 64 == 0 && r.size % 64 == 0,
|
|
"rounded offset and size are atom multiples");
|
|
}
|
|
{
|
|
// A range straddling an atom boundary expands both ways.
|
|
auto r = AlignMappedFlushRange(/*offset*/ 60, /*bytes*/ 8,
|
|
/*atom*/ 64, /*mappingSize*/ 4096);
|
|
Check(r.offset == 0 && r.size == 128,
|
|
"straddling range rounds out to [0,128)");
|
|
}
|
|
{
|
|
// Already-aligned range is left exactly as is.
|
|
auto r = AlignMappedFlushRange(/*offset*/ 128, /*bytes*/ 64,
|
|
/*atom*/ 64, /*mappingSize*/ 4096);
|
|
Check(r.offset == 128 && r.size == 64,
|
|
"already atom-aligned range is unchanged");
|
|
}
|
|
{
|
|
// atom == 1 (no coherence granularity): range passes through verbatim.
|
|
auto r = AlignMappedFlushRange(/*offset*/ 80, /*bytes*/ 16,
|
|
/*atom*/ 1, /*mappingSize*/ 4096);
|
|
Check(r.offset == 80 && r.size == 16,
|
|
"atom==1 leaves the range exact");
|
|
}
|
|
{
|
|
// Near the end of a non-atom-aligned mapping: the rounded-up end is
|
|
// clamped to mappingSize so offset+size == mapping size (the spec's
|
|
// exception that permits a non-atom-multiple size).
|
|
auto r = AlignMappedFlushRange(/*offset*/ 4030, /*bytes*/ 8,
|
|
/*atom*/ 64, /*mappingSize*/ 4040);
|
|
Check(r.offset == 3968 && r.offset + r.size == 4040,
|
|
"tail range clamps its end to the mapping size");
|
|
Check(r.offset % 64 == 0,
|
|
"clamped range still has an atom-aligned offset");
|
|
}
|
|
|
|
// ── Ranged FlushDevice keeps the coherent-memory gate (issue #60). ──
|
|
{
|
|
VulkanBuffer<std::uint8_t, true> buf;
|
|
buf.memoryPropertyFlagsChosen = HOST_VISIBLE | HOST_COHERENT;
|
|
buf.mappedSize = 4096;
|
|
buf.FlushDevice(/*offset*/ 80, /*bytes*/ 16); // must early-return
|
|
Check(true, "ranged FlushDevice skips the Vulkan call on coherent memory");
|
|
}
|
|
{
|
|
VulkanBuffer<std::uint8_t, true> buf;
|
|
buf.memoryPropertyFlagsChosen = HOST_VISIBLE | DEVICE_LOCAL;
|
|
Check(!(buf.memoryPropertyFlagsChosen & HOST_COHERENT),
|
|
"non-coherent chosen type is not gated as coherent (ranged flush still required)");
|
|
}
|
|
|
|
if (failures != 0) {
|
|
std::println("{} check(s) failed", failures);
|
|
return EXIT_FAILURE;
|
|
}
|
|
std::println("all checks passed");
|
|
return EXIT_SUCCESS;
|
|
}
|