mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
1fe4570860
The chunk planner (Morton sort + greedy pack) and instance composition (federation × placement matrix chain + world-AABB derive) were inline helpers in ViewportWindow.cpp. Pulled both out as free-function modules so the math + lookup logic can be exercised without a Qt window or a wgpu device. ViewportWindow now delegates; InstanceLookup is a using- alias to InstanceCompose::InstanceLookup. Also added an addSubBufferForTesting / clearSubPoolsForTesting seam to BufferPool so the sub-allocator invariants can be pinned with fake WGPUBuffer handles. The fakes are never dereferenced; the guard drops the sub-pools before destructor would call wgpuBufferRelease. Three new test binaries under src/ifcviewer/tests/, 33 cases / 173 assertions: BufferPool first-fit + alignment + coalescing + multi- sub-pool isolation; ChunkPlanner Morton split / interleave / stable sort / greedy-pack monotonicity and single-mesh-oversize; InstanceCompose identity / translation / order-of-multiplication / large-placement cancellation against federation false origin / column-major writeback / findInstance lookup paths.
255 lines
9.5 KiB
C++
255 lines
9.5 KiB
C++
/********************************************************************************
|
|
* *
|
|
* This file is part of IfcOpenShell. *
|
|
* *
|
|
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
|
* it under the terms of the Lesser GNU General Public License as published by *
|
|
* the Free Software Foundation, either version 3.0 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* IfcOpenShell 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 *
|
|
* Lesser GNU General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the Lesser GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
* *
|
|
********************************************************************************/
|
|
|
|
// Tier-1 coverage of BufferPool's sub-allocator. The pool's free-list +
|
|
// coalescing logic is pure CPU bookkeeping; wgpu calls only happen inside
|
|
// addSubBuffer() during growth. We pre-seed sub-pools via the test-only
|
|
// addSubBufferForTesting() seam so the tests don't need a real device,
|
|
// then exercise alloc / free / alignment / coalescing / multi-sub-pool
|
|
// behaviour against the public API.
|
|
//
|
|
// The fake handles below are never dereferenced — BufferPool treats
|
|
// WGPUBuffer as an opaque token it just hands back inside Slice. Using
|
|
// `reinterpret_cast<WGPUBuffer>(0x100)` etc. gives us stable identities
|
|
// for cross-pool sub_idx assertions.
|
|
|
|
#include "BufferPool.h"
|
|
|
|
#include <catch2/catch_all.hpp>
|
|
|
|
#include <cstdint>
|
|
|
|
namespace {
|
|
|
|
WGPUBuffer fake_handle(uintptr_t id) {
|
|
// Any non-null pointer works; the value is only used for == comparisons
|
|
// and never dereferenced. Adding an offset by id keeps multiple fakes
|
|
// visibly distinct in failure messages.
|
|
return reinterpret_cast<WGPUBuffer>(static_cast<uintptr_t>(0x1000) + id);
|
|
}
|
|
|
|
// RAII guard so the pool's destructor doesn't try to wgpuBufferRelease()
|
|
// our fake handles. Drops the sub-pools via the test seam first.
|
|
struct FakePoolGuard {
|
|
BufferPool& p;
|
|
~FakePoolGuard() { p.clearSubPoolsForTesting(); }
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("empty pool reports zero capacity and refuses allocs", "[buffer_pool]") {
|
|
BufferPool pool;
|
|
FakePoolGuard guard{pool};
|
|
|
|
REQUIRE(pool.sub_buffer_count() == 0);
|
|
REQUIRE(pool.total_capacity_bytes() == 0);
|
|
REQUIRE(pool.total_used_bytes() == 0);
|
|
REQUIRE(pool.total_free_bytes() == 0);
|
|
REQUIRE(pool.largest_free_run_bytes() == 0);
|
|
|
|
// No sub-pool exists yet; without a configured device addSubBuffer
|
|
// can't grow, so alloc returns an invalid Slice rather than UB.
|
|
auto s = pool.alloc(64, 16);
|
|
REQUIRE_FALSE(s.valid());
|
|
REQUIRE(s.size == 0);
|
|
}
|
|
|
|
TEST_CASE("single alloc returns a valid aligned slice", "[buffer_pool]") {
|
|
BufferPool pool;
|
|
FakePoolGuard guard{pool};
|
|
pool.addSubBufferForTesting(fake_handle(1), 1024);
|
|
|
|
REQUIRE(pool.sub_buffer_count() == 1);
|
|
REQUIRE(pool.total_capacity_bytes() == 1024);
|
|
REQUIRE(pool.total_used_bytes() == 0);
|
|
REQUIRE(pool.largest_free_run_bytes() == 1024);
|
|
|
|
auto s = pool.alloc(/*size=*/100, /*align=*/256);
|
|
REQUIRE(s.valid());
|
|
REQUIRE(s.buffer == fake_handle(1));
|
|
REQUIRE(s.size == 100);
|
|
REQUIRE((s.offset % 256) == 0);
|
|
REQUIRE(s.sub_idx == 0);
|
|
|
|
// `used` tracks alloc sizes (excludes pad). Free space drops by both.
|
|
REQUIRE(pool.total_used_bytes() == 100);
|
|
REQUIRE(pool.total_free_bytes() == 1024 - 100);
|
|
}
|
|
|
|
TEST_CASE("alloc-then-free round-trip returns the slot to the pool", "[buffer_pool]") {
|
|
BufferPool pool;
|
|
FakePoolGuard guard{pool};
|
|
pool.addSubBufferForTesting(fake_handle(1), 1024);
|
|
|
|
auto s = pool.alloc(256, 1);
|
|
REQUIRE(s.valid());
|
|
REQUIRE(pool.total_used_bytes() == 256);
|
|
|
|
pool.free(s);
|
|
REQUIRE(pool.total_used_bytes() == 0);
|
|
REQUIRE(pool.largest_free_run_bytes() == 1024);
|
|
|
|
// After the free-with-coalesce the pool is byte-identical to its
|
|
// initial state, so an alloc of the original size can reuse the
|
|
// same offset.
|
|
auto s2 = pool.alloc(256, 1);
|
|
REQUIRE(s2.valid());
|
|
REQUIRE(s2.offset == s.offset);
|
|
}
|
|
|
|
TEST_CASE("alignment padding is reclaimable by smaller allocs", "[buffer_pool]") {
|
|
BufferPool pool;
|
|
FakePoolGuard guard{pool};
|
|
pool.addSubBufferForTesting(fake_handle(1), 1024);
|
|
|
|
// First alloc requests 256-byte alignment; offset 0 already satisfies
|
|
// it, so no pad. Second alloc of size 100 follows at offset 256.
|
|
auto a = pool.alloc(100, 256);
|
|
auto b = pool.alloc(100, 256);
|
|
REQUIRE(a.offset == 0);
|
|
REQUIRE(b.offset == 256);
|
|
REQUIRE(b.offset >= a.offset + a.size);
|
|
|
|
// Used = sum of allocation sizes only. The 156 bytes of pad inside the
|
|
// first 256-byte slot remain in free_ranges and are reclaimable by an
|
|
// alloc small enough to fit them.
|
|
REQUIRE(pool.total_used_bytes() == 200);
|
|
auto c = pool.alloc(50, 1);
|
|
REQUIRE(c.valid());
|
|
REQUIRE(c.offset >= 100); // lands in the leading pad of slot 0
|
|
REQUIRE(c.offset < 256);
|
|
}
|
|
|
|
TEST_CASE("free coalesces adjacent ranges in the same sub-pool", "[buffer_pool]") {
|
|
BufferPool pool;
|
|
FakePoolGuard guard{pool};
|
|
pool.addSubBufferForTesting(fake_handle(1), 1024);
|
|
|
|
auto a = pool.alloc(256, 1);
|
|
auto b = pool.alloc(256, 1);
|
|
auto c = pool.alloc(256, 1);
|
|
REQUIRE(a.offset + a.size == b.offset);
|
|
REQUIRE(b.offset + b.size == c.offset);
|
|
|
|
// Free in non-adjacent order: a, then c, leaves a hole around b.
|
|
pool.free(a);
|
|
pool.free(c);
|
|
// largest_free_run can be a (256), b (still alloc'd, no), c+tail
|
|
// (256 + remaining = at least 256). It's not 768 because b is in
|
|
// the middle.
|
|
REQUIRE(pool.largest_free_run_bytes() < 768);
|
|
|
|
pool.free(b);
|
|
// Now all three runs collapse into one contiguous free block, plus
|
|
// the tail. largest_free_run is the entire sub-pool again.
|
|
REQUIRE(pool.largest_free_run_bytes() == 1024);
|
|
REQUIRE(pool.total_used_bytes() == 0);
|
|
}
|
|
|
|
TEST_CASE("alloc fails gracefully when no sub-pool can fit", "[buffer_pool]") {
|
|
BufferPool pool;
|
|
FakePoolGuard guard{pool};
|
|
pool.addSubBufferForTesting(fake_handle(1), 512);
|
|
|
|
auto big = pool.alloc(512, 1);
|
|
REQUIRE(big.valid());
|
|
REQUIRE(pool.total_used_bytes() == 512);
|
|
|
|
// Pool is now full and can_grow() is false (we never configure'd
|
|
// a device, so per_sub_buffer_capacity_ is 0). alloc returns
|
|
// an invalid Slice rather than asserting or growing into garbage.
|
|
REQUIRE_FALSE(pool.can_grow());
|
|
auto fail = pool.alloc(1, 1);
|
|
REQUIRE_FALSE(fail.valid());
|
|
}
|
|
|
|
TEST_CASE("multi sub-pool alloc spans pools and reports correct sub_idx", "[buffer_pool]") {
|
|
BufferPool pool;
|
|
FakePoolGuard guard{pool};
|
|
pool.addSubBufferForTesting(fake_handle(1), 256);
|
|
pool.addSubBufferForTesting(fake_handle(2), 256);
|
|
|
|
REQUIRE(pool.sub_buffer_count() == 2);
|
|
REQUIRE(pool.total_capacity_bytes() == 512);
|
|
|
|
// First alloc fits in sub-pool 0.
|
|
auto a = pool.alloc(256, 1);
|
|
REQUIRE(a.valid());
|
|
REQUIRE(a.buffer == fake_handle(1));
|
|
REQUIRE(a.sub_idx == 0);
|
|
|
|
// Second alloc can't fit in sub-pool 0 (full); first-fit moves to
|
|
// sub-pool 1.
|
|
auto b = pool.alloc(256, 1);
|
|
REQUIRE(b.valid());
|
|
REQUIRE(b.buffer == fake_handle(2));
|
|
REQUIRE(b.sub_idx == 1);
|
|
|
|
REQUIRE(pool.total_used_bytes() == 512);
|
|
REQUIRE(pool.total_free_bytes() == 0);
|
|
}
|
|
|
|
TEST_CASE("free routes by sub_idx — no cross-pool coalescing", "[buffer_pool]") {
|
|
BufferPool pool;
|
|
FakePoolGuard guard{pool};
|
|
pool.addSubBufferForTesting(fake_handle(1), 256);
|
|
pool.addSubBufferForTesting(fake_handle(2), 256);
|
|
|
|
auto a = pool.alloc(256, 1); // sub 0
|
|
auto b = pool.alloc(256, 1); // sub 1
|
|
REQUIRE(a.sub_idx == 0);
|
|
REQUIRE(b.sub_idx == 1);
|
|
|
|
pool.free(a);
|
|
pool.free(b);
|
|
|
|
// Both sub-pools are individually empty, but they are distinct
|
|
// buffers — largest_free_run is per-sub-buffer, not summed across.
|
|
REQUIRE(pool.total_used_bytes() == 0);
|
|
REQUIRE(pool.largest_free_run_bytes() == 256);
|
|
}
|
|
|
|
TEST_CASE("free with invalid slice is a no-op", "[buffer_pool]") {
|
|
BufferPool pool;
|
|
FakePoolGuard guard{pool};
|
|
pool.addSubBufferForTesting(fake_handle(1), 512);
|
|
|
|
auto a = pool.alloc(128, 1);
|
|
REQUIRE(a.valid());
|
|
const uint64_t used_before = pool.total_used_bytes();
|
|
|
|
// Default-constructed Slice has size=0 + null buffer + sub_idx=-1.
|
|
// free() should silently ignore it (this is the path real callers
|
|
// hit when an alloc failed earlier and they unconditionally free).
|
|
BufferPool::Slice junk;
|
|
REQUIRE_FALSE(junk.valid());
|
|
pool.free(junk);
|
|
REQUIRE(pool.total_used_bytes() == used_before);
|
|
|
|
// Sub-index out of range is also ignored.
|
|
BufferPool::Slice bad_idx = a;
|
|
bad_idx.sub_idx = 99;
|
|
pool.free(bad_idx);
|
|
REQUIRE(pool.total_used_bytes() == used_before);
|
|
|
|
// Real free still works after these no-ops.
|
|
pool.free(a);
|
|
REQUIRE(pool.total_used_bytes() == 0);
|
|
}
|