"""
Tests for the POINTS variant of the kernel rewrite.
Volume mode swaps the plot block for a voxel write; points mode swaps it for an
append to a flat list. The list is the interesting part, because Blender's GPU
module has no SSBOs (`ifs_layout`'s module docstring, reason 1) — so there is no
buffer to append to. The substitute is an RGBA32F image used as an array, one
texel per point, indexed by an atomic counter in a 1x1 R32UI image. That is the
same trick the viewport's auto-exposure max pass uses, turned from a reduction
into an allocator.
What can go wrong here is not that the arithmetic is subtly off; it is that a
substitution silently stops matching and leaves the 2D projection in place, so
the kernel still compiles and still runs and quietly plots to a histogram nobody
reads. So these assert what the body must and must NOT contain, and that the
three modes stay mutually exclusive — a kernel that tried to be two of them at
once would bind images under two different meanings.
Run: python3 -m unittest discover -s blender
"""
import os
import unittest
from ifs_codegen import apply_portability_fixes, generate_kernel, load_library
from ifs_layout import POINTS_LAYOUT, rewrite
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TRANSFORMS = os.path.join(REPO, "Transforms")
INCLUDES = os.path.join(REPO, "WpfDisplay", "Library", "Includes")
TEMPLATE = os.path.join(REPO, "IFSEngine", "Rendering", "Shaders", "ifs_kernel.comp.shader")
class PointsRewrite(unittest.TestCase):
@classmethod
def setUpClass(cls):
if not os.path.isfile(TEMPLATE):
raise unittest.SkipTest("kernel template not found")
library = load_library(TRANSFORMS, INCLUDES)
with open(TEMPLATE, "r", encoding="utf-8-sig") as handle:
kernel = generate_kernel(handle.read(), library)
kernel, _ = apply_portability_fixes(kernel)
cls.body, cls.typedefs, cls.report = rewrite(kernel, points=True)
def test_every_substitution_applied(self):
self.assertTrue(self.report.ok,
"rewrite incomplete: " + "; ".join(self.report.missing))
def test_the_projection_is_gone_entirely(self):
"""
Not bypassed — REPLACED. `project()` can `continue` on out-of-frame, so a
points mode that merely ignored the result would still drop every sample
outside the current view and emit a view-dependent cloud. That is the same
reasoning volume mode is built on.
"""
self.assertNotIn("projf = project(", self.body)
self.assertNotIn("accumulate_hit", self.body)
def test_points_are_appended_through_the_atomic_counter(self):
self.assertIn("emit_point", self.body)
self.assertIn("imageAtomicAdd(counter_img", self.body)
self.assertIn("imageStore(points_img", self.body)
self.assertIn("imageStore(colors_img", self.body)
def test_the_emit_is_bounded_by_capacity(self):
"""
The counter keeps counting past the end of the image on purpose, so the host
can report how many points were WANTED. Without a capacity guard that would
be an out-of-bounds imageStore on every sample past the end.
"""
self.assertIn("point_capacity", self.body)
self.assertIn("points_width", self.body)
def test_colour_is_not_premultiplied(self):
"""
The volume path multiplies colour by opacity because it accumulates a
density-weighted sum and divides it back out. A point is not accumulated —
it carries its own colour — so premultiplying would darken every point by
its iterator's opacity and there is nothing to divide it out again.
"""
self.assertNotIn("color.xyz *= color.w", self.body)
def test_the_modes_are_mutually_exclusive(self):
with self.assertRaises(ValueError):
rewrite("", points=True, volume=True)
with self.assertRaises(ValueError):
rewrite("", points=True, depth=True)
def test_the_layout_fits_blenders_image_budget(self):
"""Blender caps images at 8 and the budget is STATIC — see ifs_layout."""
images = [r for r in POINTS_LAYOUT if r.kind == "image"]
self.assertLessEqual(len(images), 8)
self.assertEqual(len(images), len({r.slot for r in images}),
"two images share a binding slot")
def test_the_layout_names_match_the_glsl(self):
"""A declared name that the body never uses binds nothing and wastes a slot."""
for resource in POINTS_LAYOUT:
if resource.kind == "image":
self.assertIn(resource.name, self.body,
f"{resource.name} is declared but never used")
if __name__ == "__main__":
unittest.main()