From bf2bcfd393d4b3c023c6ad0a2ac5ad290769da3b Mon Sep 17 00:00:00 2001 From: Xget7 Date: Sat, 12 Sep 2026 18:06:17 -0300 Subject: [PATCH 1/4] Fix Adreno 640 radix histogram corruption; prepare alpha06 --- README.md | 2 +- packages/splatkit-android/README.md | 4 ++-- packages/splatkit-android/build.gradle.kts | 2 +- packages/splatkit-android/docs/VULKAN.md | 11 ++++++++++- .../src/main/cpp/rendering/vulkan/RadixSort.cpp | 2 +- .../src/main/cpp/shaders/radix_histogram.comp | 16 +++------------- .../src/main/cpp/tests/RadixSortTest.cpp | 2 +- 7 files changed, 19 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 2baa4da..28e5686 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Experimental alpha: APIs and quality/performance tradeoffs are still evolving. Android: API 29+, Vulkan 1.1, arm64-v8a. The GPU path additionally checks subgroup and memory limits. See the [Android releases](https://github.com/Xget7/splatkit-android/releases) for artifacts. -Maven `0.1.0-alpha04` predates GPU ordering; `0.1.0-alpha05` is the new GPU integration. +Maven `0.1.0-alpha04` predates GPU ordering; `0.1.0-alpha06` fixes Adreno sorting in alpha05. To build current source: ```sh diff --git a/packages/splatkit-android/README.md b/packages/splatkit-android/README.md index 62cc27f..5a50965 100644 --- a/packages/splatkit-android/README.md +++ b/packages/splatkit-android/README.md @@ -7,12 +7,12 @@ An arm64 emulator can test functionality; it is not a phone performance measurem ## Install -New GPU integration: `0.1.0-alpha05`. Check [releases](https://github.com/Xget7/splatkit-android/releases) for publication status. +Adreno-corrected GPU integration: `0.1.0-alpha06`. Check [releases](https://github.com/Xget7/splatkit-android/releases) for publication status. Maven `0.1.0-alpha04` is the older CPU-ordering artifact. ```kotlin dependencies { - implementation("io.github.xget7:splatkit-android:0.1.0-alpha05") + implementation("io.github.xget7:splatkit-android:0.1.0-alpha06") } ``` diff --git a/packages/splatkit-android/build.gradle.kts b/packages/splatkit-android/build.gradle.kts index 0856707..de52276 100644 --- a/packages/splatkit-android/build.gradle.kts +++ b/packages/splatkit-android/build.gradle.kts @@ -43,7 +43,7 @@ dependencies { // publishing machine (ORG_GRADLE_PROJECT_mavenCentralUsername, mavenCentralPassword, // signingInMemoryKey, signingInMemoryKeyPassword); local builds need none of it. mavenPublishing { - coordinates("io.github.xget7", "splatkit-android", "0.1.0-alpha05") + coordinates("io.github.xget7", "splatkit-android", "0.1.0-alpha06") publishToMavenCentral(automaticRelease = true) if (project.findProperty("signingInMemoryKey") != null) signAllPublications() pom { diff --git a/packages/splatkit-android/docs/VULKAN.md b/packages/splatkit-android/docs/VULKAN.md index d2d7c14..9fe1bb2 100644 --- a/packages/splatkit-android/docs/VULKAN.md +++ b/packages/splatkit-android/docs/VULKAN.md @@ -2,7 +2,8 @@ Public API: `SplatSurfaceView`; JNI and Vulkan classes are internal. Current source enables `VulkanFrameCompute`: GPU LOD → visibility → stable radix → indirect draw. -Alpha05 integrates this path; Maven alpha04 predates it. Hybrid tiles and RN GPU options remain pending. +Alpha06 fixes Adreno histogram corruption in alpha05; Maven alpha04 predates GPU ordering. +Hybrid tiles and RN GPU options remain pending. Contracts live beside code: [VisibilityPass.h](../src/main/cpp/rendering/vulkan/VisibilityPass.h), [shader ABI](../src/main/cpp/rendering/vulkan/VulkanShaderTypes.h). The context outlives allocations; reuse slots after fences, resize/destroy after all consumers finish. @@ -21,6 +22,14 @@ native suites had layers off. Kitchen 500k loaded/drew with layers on and no cap SwiftShader passes smaller suites but crashes on large mapped uploads, also reproduced without SDK code. No physical Android performance or reference-image quality acceptance follows. +Physical Mi 9 / Adreno 640, Vulkan 1.1.128, driver 0x801f6000: +alpha05 ballot grouping repeatedly lost duplicate keys at 31 inputs. +Workgroup-local atomic histogram fixes this without global atomics. +Alpha06 passes 152 stable-sort cases through 3M (including 63/64/65 tails), visibility, +LOD, complete indirect ordering and upload-pressure tests; standalone layers were off. +Kitchen 500k and house 2M render with app validation on; background/resume also passed. +This is device correctness evidence, not lossless LOD or universal FPS acceptance. + ## Metal mapping | Metal | Vulkan / GLSL | diff --git a/packages/splatkit-android/src/main/cpp/rendering/vulkan/RadixSort.cpp b/packages/splatkit-android/src/main/cpp/rendering/vulkan/RadixSort.cpp index c488f1e..013c979 100644 --- a/packages/splatkit-android/src/main/cpp/rendering/vulkan/RadixSort.cpp +++ b/packages/splatkit-android/src/main/cpp/rendering/vulkan/RadixSort.cpp @@ -60,7 +60,7 @@ RadixSort::Capabilities RadixSort::queryCapabilities(const VulkanContext& ctx) { result.reason = "descriptor/range/dispatch limits too small"; } else { result.supported = true; - result.reason = "stable subgroup histogram and workgroup bit-mask scatter available"; + result.reason = "shared histogram and stable workgroup bit-mask scatter available"; } return result; } diff --git a/packages/splatkit-android/src/main/cpp/shaders/radix_histogram.comp b/packages/splatkit-android/src/main/cpp/shaders/radix_histogram.comp index af7d421..8127838 100644 --- a/packages/splatkit-android/src/main/cpp/shaders/radix_histogram.comp +++ b/packages/splatkit-android/src/main/cpp/shaders/radix_histogram.comp @@ -15,15 +15,6 @@ layout(set=0,binding=8,std430) buffer CountOut { uint countOut; }; layout(set=0,binding=9,std430) buffer Status { uint status; }; layout(push_constant) uniform Push { uint capacity, shift, maxGroupsX; } pc; -uvec4 matchDigit(uint digit, bool valid) { - uvec4 peers = subgroupBallot(valid); - for (uint bit=0; bit<8; ++bit) { - bool set = ((digit >> bit) & 1u) != 0; - uvec4 vote = subgroupBallot(set); - peers &= set ? vote : ~vote; - } - return peers; -} uint element(uint block, uint row) { return block*2048u + row*128u + gl_LocalInvocationIndex; } @@ -39,10 +30,9 @@ void main() { uint e=element(block,row); bool valid=e> pc.shift)&255u : 0u; - uvec4 peers=matchDigit(digit,valid); - // One shared atomic per distinct digit in a subgroup, no global counter contention. - if(valid && subgroupBallotFindLSB(peers)==gl_SubgroupInvocationID) - atomicAdd(bins[digit],subgroupBallotBitCount(peers)); + // Workgroup-local atomics only; global bins have one writer per block. + // Ballot digit grouping loses duplicate keys on the Adreno 640 / 1.1.128 driver. + if(valid) atomicAdd(bins[digit],1u); } barrier(); histogram[t*blocks+block]=bins[t]; diff --git a/packages/splatkit-android/src/main/cpp/tests/RadixSortTest.cpp b/packages/splatkit-android/src/main/cpp/tests/RadixSortTest.cpp index 8daed2d..d280667 100644 --- a/packages/splatkit-android/src/main/cpp/tests/RadixSortTest.cpp +++ b/packages/splatkit-android/src/main/cpp/tests/RadixSortTest.cpp @@ -95,7 +95,7 @@ int main() { bad.keys = sort->output(1).keys; require(!sort->encode(cmd, 0, bad), "reject owned output alias"); }); - std::vector sizes{0, 1, 31, 32, 33, 67, 127, 128, 129, 257}; + std::vector sizes{0, 1, 31, 32, 33, 63, 64, 65, 67, 127, 128, 129, 257}; if (!tiny) sizes.insert(sizes.end(), {2047, 2048, 2049, 4097, 262145}); if (large && !tiny) sizes.push_back(capacity); std::mt19937 random(0x51a7u); From 15113a4e8bb1ded9eba91b59f37577ce400927df Mon Sep 17 00:00:00 2001 From: Xget7 Date: Sat, 12 Sep 2026 18:10:35 -0300 Subject: [PATCH 2/4] Report measured GPU visibility time in native diagnostics --- .../src/main/cpp/rendering/vulkan/VulkanFrameCompute.cpp | 1 + .../src/main/cpp/rendering/vulkan/VulkanFrameCompute.h | 2 +- .../src/main/cpp/rendering/vulkan/VulkanSplatRenderer.h | 1 + .../splatkit-engine/include/splatkit/rendering/SplatRenderer.h | 3 ++- packages/splatkit-engine/src/engine/SplatEngine.cpp | 2 +- 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanFrameCompute.cpp b/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanFrameCompute.cpp index 5102a7d..9e774a8 100644 --- a/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanFrameCompute.cpp +++ b/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanFrameCompute.cpp @@ -141,6 +141,7 @@ void VulkanFrameCompute::collect(uint32_t slot) { : (uint64_t{1} << timestampBits_) - 1; const double millis = static_cast(timestampPeriod_) * 1e-6; stats_.selectMillis = lod_ ? ((ticks[1] - ticks[0]) & mask) * millis : 0; + stats_.cullMillis = ((ticks[2] - ticks[1]) & mask) * millis; stats_.sortMillis = ((ticks[3] - ticks[2]) & mask) * millis; } diff --git a/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanFrameCompute.h b/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanFrameCompute.h index de6f911..077b2fd 100644 --- a/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanFrameCompute.h +++ b/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanFrameCompute.h @@ -33,7 +33,7 @@ class VulkanFrameCompute { }; struct Stats { uint32_t drawn = 0, selected = 0, limited = 0, evaluated = 0, status = 0; - double sortMillis = 0, selectMillis = 0; + double sortMillis = 0, selectMillis = 0, cullMillis = 0; }; // Nullopt means encoding failed; caller must still submit/end its acquired frame. std::optional encode(VkCommandBuffer cmd, uint32_t slot, VkBuffer camera, diff --git a/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanSplatRenderer.h b/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanSplatRenderer.h index 7151150..fc0574a 100644 --- a/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanSplatRenderer.h +++ b/packages/splatkit-android/src/main/cpp/rendering/vulkan/VulkanSplatRenderer.h @@ -65,6 +65,7 @@ class VulkanSplatRenderer final : public SplatRenderer { bool draw(const Frame& frame) override; double lastGpuMillis() const override { return frameLoop_.lastGpuMillis(); } double lastSortMillis() const override { return compute_ ? compute_->stats().sortMillis : 0; } + double lastCullMillis() const override { return compute_ ? compute_->stats().cullMillis : 0; } double lastSelectMillis() const override { return compute_ ? compute_->stats().selectMillis : 0; } uint32_t lastDrawCount() const override { return compute_ ? compute_->stats().drawn : 0; } uint32_t lastSelectedCount() const override { return compute_ ? compute_->stats().selected : 0; } diff --git a/packages/splatkit-engine/include/splatkit/rendering/SplatRenderer.h b/packages/splatkit-engine/include/splatkit/rendering/SplatRenderer.h index 155e532..5a85d69 100644 --- a/packages/splatkit-engine/include/splatkit/rendering/SplatRenderer.h +++ b/packages/splatkit-engine/include/splatkit/rendering/SplatRenderer.h @@ -105,8 +105,9 @@ class SplatRenderer { // GPU time of the most recently completed frame, from timestamps at both ends of it. // Zero until the first frame completes or if unsupported. virtual double lastGpuMillis() const = 0; - // GPU time of the last visibility pass, when the renderer sorts on the GPU. + // GPU times of completed sort and visibility passes; zero when unavailable. virtual double lastSortMillis() const { return 0; } + virtual double lastCullMillis() const { return 0; } // Splats the last frame drew, when the renderer sorts on the GPU. virtual uint32_t lastDrawCount() const { return 0; } virtual uint32_t lastSelectedCount() const { return 0; } diff --git a/packages/splatkit-engine/src/engine/SplatEngine.cpp b/packages/splatkit-engine/src/engine/SplatEngine.cpp index bb40591..fa82a78 100644 --- a/packages/splatkit-engine/src/engine/SplatEngine.cpp +++ b/packages/splatkit-engine/src/engine/SplatEngine.cpp @@ -243,7 +243,7 @@ void SplatEngine::streamTiles(const FrameCamera& camera, float pixelScale, void SplatEngine::takeSortResult() { if (gpuSort_) { lastSort_.sortMillis = renderer_->lastSortMillis(); - lastSort_.cullMillis = 0; + lastSort_.cullMillis = renderer_->lastCullMillis(); lastSort_.selectMillis = renderer_->lastSelectMillis(); lastSort_.selected = streamer_ ? streamer_->drawnSplats() : sourceCount_; drawCount_ = renderer_->lastDrawCount(); From c2b63db54c6aaf51e2300f10d982c4c714e45e50 Mon Sep 17 00:00:00 2001 From: Xget7 Date: Sat, 12 Sep 2026 18:16:06 -0300 Subject: [PATCH 3/4] Skip empty LOD visibility workgroups without changing survivors --- .../src/main/cpp/shaders/visibility.comp | 5 ++++ .../src/main/cpp/tests/VisibilityPassTest.cpp | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/packages/splatkit-android/src/main/cpp/shaders/visibility.comp b/packages/splatkit-android/src/main/cpp/shaders/visibility.comp index 668c58f..91f42e3 100644 --- a/packages/splatkit-android/src/main/cpp/shaders/visibility.comp +++ b/packages/splatkit-android/src/main/cpp/shaders/visibility.comp @@ -226,6 +226,11 @@ bool resolveCandidate(uint logical, out uint source, inout uint errors) { void main() { uint groupIndex = gl_WorkGroupID.x + gl_WorkGroupID.y * constants.dispatchGroupsX; uint logical = groupIndex * gl_WorkGroupSize.x + gl_LocalInvocationID.x; + // Uniform workgroup exit: indexed LOD usually fills only a fraction of its capacity. + // Keep group zero for empty-input/projection diagnostics, and never hide invalid counts. + if (constants.mode == 1u && groupIndex > 0u && + candidateCount <= constants.candidateCapacity && + groupIndex * gl_WorkGroupSize.x >= candidateCount) return; uint sourceIndex = 0u, errors = 0u, key = 0u; if (constants.mode == 2u && logical < constants.rangeCount && !validRange(logical)) errors |= 8u; bool candidate = resolveCandidate(logical, sourceIndex, errors); diff --git a/packages/splatkit-android/src/main/cpp/tests/VisibilityPassTest.cpp b/packages/splatkit-android/src/main/cpp/tests/VisibilityPassTest.cpp index 13396fb..7846d57 100644 --- a/packages/splatkit-android/src/main/cpp/tests/VisibilityPassTest.cpp +++ b/packages/splatkit-android/src/main/cpp/tests/VisibilityPassTest.cpp @@ -159,6 +159,34 @@ void tests(const test::VulkanTestContext& gpu) { } std::puts("PASS prefix, empty, subgroup tails and descriptor slots"); + // LOD reserves far more entries than it emits. Poison inactive groups so reading + // a capacity tail instead of the GPU count becomes a visible diagnostic failure. + std::vector sparseIndices(4097, UINT32_MAX); + std::iota(sparseIndices.begin(), sparseIndices.begin() + 300, 0); + auto sparseBuffer = upload(gpu, sparseIndices); + auto sparseCount = upload(gpu, std::vector{0}); + auto sparse = f.input(); + indexed(sparse, *sparseBuffer, *sparseCount, 4097); + for (uint32_t n : {0u, 1u, 127u, 128u, 129u, 257u}) { + require(sparseCount->upload(&n, 4), "sparse GPU count"); + std::vector expected(n); + std::iota(expected.begin(), expected.end(), 0); + membership(run(gpu, *pass, sparse), expected); + membership(run(gpu, *pass, sparse, 1), expected); + } + uint32_t sparseInvalid = 4098; + require(sparseCount->upload(&sparseInvalid, 4), "sparse invalid count"); + failed(run(gpu, *pass, sparse), VisibilityPass::kInvalidCount); + sparseInvalid = 0; + require(sparseCount->upload(&sparseInvalid, 4), "sparse empty count"); + auto invalidProjection = camera(); + invalidProjection.proj = splat::Mat4::identity(); + f.setCamera(invalidProjection); + sparse.keyBits = VisibilityPass::KeyBits::low16; + failed(run(gpu, *pass, sparse), VisibilityPass::kInvalidProjection); + f.setCamera(camera()); + std::puts("PASS sparse indexed workgroups, poisoned tails and empty diagnostics"); + require(pass->reserve(3), "small output capacity"); auto indexBuffer = upload(gpu, std::vector{299, 0, 128}); auto countBuffer = upload(gpu, std::vector{3}); From 8075ee74f0d9312b77a9656585cf63ba3600b797 Mon Sep 17 00:00:00 2001 From: Xget7 Date: Sat, 12 Sep 2026 18:23:03 -0300 Subject: [PATCH 4/4] Record physical Mi 9 benchmarks and validate LOD residency correctly --- docs/benchmarks/2026-09-12-mi9-vulkan.json | 128 +++++++++++++++++++++ packages/splatkit-android/docs/VULKAN.md | 12 ++ scripts/sdk_harness.py | 21 +++- scripts/tests/test_sdk_harness.py | 17 +++ 4 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 docs/benchmarks/2026-09-12-mi9-vulkan.json diff --git a/docs/benchmarks/2026-09-12-mi9-vulkan.json b/docs/benchmarks/2026-09-12-mi9-vulkan.json new file mode 100644 index 0000000..aa3d2b3 --- /dev/null +++ b/docs/benchmarks/2026-09-12-mi9-vulkan.json @@ -0,0 +1,128 @@ +{ + "date": "2026-09-12", + "environment": "physical Xiaomi Mi 9", + "gpu": "Adreno 640", + "vulkan": "1.1.128", + "driver": "0x801f6000", + "android": "11 / MIUI 12.5.1", + "build": "minified Release, validation off", + "resolution": [ + 1080, + 2261 + ], + "render_scale": 1, + "sh_degree": 0, + "sort_bits": 32, + "source": "modern_house_with_lush_landscaping_2m.spz", + "source_splats": 2000000, + "source_sha256": "fe32a5a66b7f5caaad793ab90a7ff7d7948f52ba360044170b4da04c3950ebad", + "collider_sha256": "a9ab04ad30947f85000fad460f7bcf7488dfb4b995a449a4124342c4b53d5467", + "camera": [ + 0, + 0.73, + 0, + 0, + 0 + ], + "benchmark": "30-second turn in place after world-ready; gyro off; mailbox presentation; Android host frame scheduling remains", + "lod": "load-time default hierarchy, 2970075 resident nodes, selection capacity 1200000; unchanged quality settings across tail-skip A/B", + "stage_timings": "Means of ~2-second completed-frame log samples, not per-frame averages; null means not instrumented", + "process_pss_kib": 579266, + "process_rss_kib": 686792, + "android_thermal_status": 0, + "acceptance": "No perceptual metric/reference-image acceptance or sustained thermal certification. LOD parents/subpixel rejection remain approximate; no universal FPS guarantee.", + "runs": [ + { + "variant": "full_source", + "local_start": "09-12 18:06:16.349", + "frames": 578, + "fps_mean": 19.2, + "frame_mean_ms": 52, + "frame_p95_ms": 66.8, + "gpu_mean_ms": 51.9, + "gpu_p95_ms": 59.6, + "sort_sample_mean_ms": 3.147, + "cull_sample_mean_ms": null, + "select_sample_mean_ms": 0, + "sample_count": 15, + "drawn_sample_range": [ + 44983, + 125217 + ], + "selected_sample_range": [ + 2000000, + 2000000 + ], + "end_gpu_celsius": 58.3 + }, + { + "variant": "lod_before_tail_skip", + "local_start": "09-12 18:10:34.401", + "frames": 721, + "fps_mean": 24, + "frame_mean_ms": 41.7, + "frame_p95_ms": 50.1, + "gpu_mean_ms": 41.6, + "gpu_p95_ms": 50, + "sort_sample_mean_ms": 3.173, + "cull_sample_mean_ms": 19.147, + "select_sample_mean_ms": 3.64, + "sample_count": 15, + "drawn_sample_range": [ + 45310, + 121828 + ], + "selected_sample_range": [ + 50909, + 132148 + ], + "end_gpu_celsius": 58.7 + }, + { + "variant": "lod_after_tail_skip", + "local_start": "09-12 18:15:00.531", + "frames": 1243, + "fps_mean": 41.4, + "frame_mean_ms": 24.1, + "frame_p95_ms": 33.4, + "gpu_mean_ms": 23.9, + "gpu_p95_ms": 32.6, + "sort_sample_mean_ms": 3.18, + "cull_sample_mean_ms": 1.807, + "select_sample_mean_ms": 3.653, + "sample_count": 15, + "drawn_sample_range": [ + 44999, + 124889 + ], + "selected_sample_range": [ + 52411, + 136761 + ], + "end_gpu_celsius": 64.5 + }, + { + "variant": "lod_after_tail_skip", + "local_start": "09-12 18:16:05.115", + "frames": 1244, + "fps_mean": 41.4, + "frame_mean_ms": 24.1, + "frame_p95_ms": 33.4, + "gpu_mean_ms": 23.9, + "gpu_p95_ms": 32.5, + "sort_sample_mean_ms": 3.173, + "cull_sample_mean_ms": 1.813, + "select_sample_mean_ms": 3.64, + "sample_count": 15, + "drawn_sample_range": [ + 44920, + 124804 + ], + "selected_sample_range": [ + 50891, + 135651 + ], + "end_gpu_celsius": 63.3 + } + ] +} diff --git a/packages/splatkit-android/docs/VULKAN.md b/packages/splatkit-android/docs/VULKAN.md index 9fe1bb2..7a0a444 100644 --- a/packages/splatkit-android/docs/VULKAN.md +++ b/packages/splatkit-android/docs/VULKAN.md @@ -30,6 +30,18 @@ LOD, complete indirect ordering and upload-pressure tests; standalone layers wer Kitchen 500k and house 2M render with app validation on; background/resume also passed. This is device correctness evidence, not lossless LOD or universal FPS acceptance. +Mi 9 house 2M, SH0, 1080x2261, Release, 30-second turns: + +| Configuration | Mean FPS | GPU ms | Visibility ms* | +|---|---:|---:|---:| +| Full source | 19.2 | 51.9 | unavailable | +| LOD before empty-group skip | 24.0 | 41.6 | 19.15 | +| Same LOD, empty groups skipped (two runs) | 41.4 | 23.9 | 1.81 | + +*Stage means use ~2-second samples. Sort stayed ~3.18 ms; source 2M, drawn ~45k–125k. +LOD quality/resolution were unchanged by this optimization. PSS snapshot: ~566 MiB. +[Settings, hashes and measurements](../../../docs/benchmarks/2026-09-12-mi9-vulkan.json). + ## Metal mapping | Metal | Vulkan / GLSL | diff --git a/scripts/sdk_harness.py b/scripts/sdk_harness.py index 70a4383..f29811c 100644 --- a/scripts/sdk_harness.py +++ b/scripts/sdk_harness.py @@ -116,11 +116,24 @@ def android_log(contents, pid, expected, environment): if (match := line_pattern.match(line)) and int(match[1]) == pid] text = "\n".join(messages) errors = [line for line in messages if re.search( - r"Fatal signal|FATAL EXCEPTION|VUID-|Validation Error|World failed:|VK_ERROR_", line)] + r"Fatal signal|FATAL EXCEPTION|VUID-|Validation Error|World failed:|VK_ERROR_|Vulkan GPU frame rejected:", line)] device = re.search(r"Vulkan device: (.+)", text) - draws = re.findall(r"(\d+) drawn of (\d+) selected of (\d+)", text) - valid_draw = any(0 < int(drawn) <= int(selected) <= int(loaded) == expected - for drawn, selected, loaded in draws) + # Source readiness identifies the world; an uploaded LOD hierarchy has more + # resident records than original splats. Require a draw after its ready event. + resident = expected + ready = False + valid_draw = False + for message in messages: + upload = re.search(r"uploaded (\d+) splats", message) + if upload: + resident, ready = int(upload[1]), False + world = re.fullmatch(r"world ready: (\d+) splats", message) + if world: + ready = int(world[1]) == expected + draw = re.search(r"(\d+) drawn of (\d+) selected of (\d+)", message) + if ready and draw: + drawn, selected, records = map(int, draw.groups()) + valid_draw |= 0 < drawn <= selected <= records == resident checks = {"vulkan_initialized": device is not None, "world_ready": f"world ready: {expected} splats" in messages, "nonempty_draw": valid_draw, "no_logged_errors": not errors} diff --git a/scripts/tests/test_sdk_harness.py b/scripts/tests/test_sdk_harness.py index d86aa5a..2500ef5 100644 --- a/scripts/tests/test_sdk_harness.py +++ b/scripts/tests/test_sdk_harness.py @@ -51,6 +51,23 @@ def test_empty_wrong_world_and_incomplete_logs_fail(self): def test_validation_error_fails(self): self.assertEqual(self.verdict(self.ready() + log("VUID-vkCmdDraw-None-02700"))["status"], "failed") + def test_lod_residency_is_not_original_source_count(self): + text = (log("Vulkan device: Adreno 640") + + log("uploaded 750000 splats in 10 ms, sh degree 0") + + log("world ready: 500000 splats") + + log("100 drawn of 150 selected of 750000")) + self.assertEqual(self.verdict(text)["status"], "passed") + self.assertEqual(self.verdict(text.replace("of 750000", "of 800000"))["status"], "failed") + + def test_previous_world_draw_cannot_validate_new_load(self): + text = (log("Vulkan device: Adreno 640") + + log("100 drawn of 500000 selected of 500000") + + log("world ready: 500000 splats")) + self.assertEqual(self.verdict(text)["status"], "failed") + + def test_gpu_rejection_fails_even_after_a_valid_draw(self): + self.assertEqual(self.verdict(self.ready() + log("Vulkan GPU frame rejected: diagnostic bits 0x1"))["status"], "failed") + def test_plan_never_installs_or_launches(self): steps = harness.plan("android", 2) self.assertEqual(steps[0]["argv"], ["./gradlew", "--console=plain",