OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / blender / test_depth.py

158 lines · python ref:
"""
Tests for the nearest-sample depth channel that lets scene geometry occlude the
live overlay.

The failure this suite exists to catch is not "depth is wrong by a bit", it is
"depth is silently absent for SOME pixels". The kernel calls `accumulate_hit` from
two places — the direct plot and the density-estimation filter, which splats into
NEIGHBOURING pixels — and an implementation that writes depth at the call site
instead of inside the function leaves every filtered pixel at the far plane. That
renders correctly in every still where filtering is off, and un-occludes exactly the
blurred regions when it is on. So the two-call-site property is asserted directly.

Run: python3 -m unittest discover -s blender
"""

import os
import re
import unittest

from ifs_codegen import apply_portability_fixes, generate_kernel, load_library
from ifs_layout import DEPTH_CLEAR, DEPTH_EXTRA_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")


def kernel_source() -> str:
    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)
    return kernel


class DepthRewrite(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        if not os.path.isfile(TEMPLATE):
            raise unittest.SkipTest("kernel template not found")
        cls.raw = kernel_source()
        cls.body, _typedefs, cls.report = rewrite(cls.raw, depth=True)
        cls.plain, _t, _r = rewrite(cls.raw)

    def test_rewrite_is_complete(self):
        self.assertEqual([], self.report.missing)

    def test_depth_substitutions_applied(self):
        for label in ("rewrite accumulate_hit with depth",
                      "inject set_depth at the top of the plot block"):
            self.assertIn(label, self.report.applied)

    # ---------------------------------------------------------------- the real risk

    def test_the_kernel_really_has_two_accumulate_hit_call_sites(self):
        """
        Guards the PREMISE of the design. If upstream collapses these to one, or
        adds a third, the reasoning below needs revisiting rather than silently
        still passing.
        """
        calls = re.findall(r"\baccumulate_hit\s*\(", self.raw)
        # One declaration + two call sites.
        self.assertEqual(3, len(calls), "kernel's accumulate_hit call shape changed")

    def test_depth_is_written_inside_accumulate_hit_not_at_call_sites(self):
        body = self.body
        match = re.search(r"void accumulate_hit\(ivec2 proj, vec4 color\)\s*\{.*?\n\}",
                          body, re.S)
        self.assertIsNotNone(match, "accumulate_hit not found after rewrite")
        self.assertIn("imageAtomicMin(depth_img, proj, g_depth)", match.group(0),
                      "depth must be written INSIDE accumulate_hit, so that the "
                      "density-estimation filter's neighbour splats carry it too")
        # ...and nowhere else, which is what would reintroduce the per-call-site bug.
        self.assertEqual(1, body.count("imageAtomicMin(depth_img"))

    def test_set_depth_precedes_both_arms(self):
        """`set_depth` must run once per sample, before the branch that chooses
        between the filtered and direct plot — not inside either arm."""
        body = self.body
        set_at = body.index("set_depth(p.pos.xyz)")
        for call in re.finditer(r"accumulate_hit\((?:nb|proj),", body):
            self.assertLess(set_at, call.start(),
                            "set_depth must precede every accumulate_hit call")

    # ------------------------------------------------------------------- soundness

    def test_depth_uses_blenders_matrix_not_the_engines(self):
        """
        The engine's `view_proj_mat` is mirrored in x and its NDC z is not what
        Blender's depth buffer holds. Comparing against the buffer with the engine's
        z would be wrong in a way that still renders a plausible picture.
        """
        helper = re.search(r"void set_depth\(vec3 world\)\s*\{.*?\n\}", self.body, re.S)
        self.assertIsNotNone(helper)
        self.assertIn("blender_view_proj", helper.group(0))
        self.assertNotIn("view_proj_mat", helper.group(0))

    def test_behind_camera_never_occludes(self):
        helper = re.search(r"void set_depth\(vec3 world\)\s*\{.*?\n\}", self.body, re.S)
        self.assertIn("h.w <= 0.0", helper.group(0),
                      "points behind the camera must fall back to the far plane")

    def test_depth_clear_is_the_far_plane(self):
        """The clear value is a float bit pattern, not a fixed-point count. If it
        stops meaning 1.0, empty pixels start occluding scene geometry."""
        import struct
        as_float = struct.unpack("<f", struct.pack("<I", DEPTH_CLEAR))[0]
        self.assertEqual(1.0, as_float)

    def test_atomic_min_over_float_bits_is_monotonic(self):
        """
        imageAtomicMin on floatBitsToUint is only an exact minimum because the IEEE
        bit pattern of a NON-NEGATIVE float is monotonic. set_depth clamps to [0,1],
        so this holds — but assert the property rather than trust the comment.
        """
        import struct

        def bits(f):
            return struct.unpack("<I", struct.pack("<f", f))[0]

        samples = [i / 64.0 for i in range(65)]
        self.assertEqual(sorted(samples, key=bits), samples)

    # ------------------------------------------------------------------- isolation

    def test_plain_rewrite_is_untouched(self):
        """The still and volume paths must not pay for a viewport-only feature —
        a declared-but-unused image costs one of Blender's eight slots."""
        self.assertNotIn("depth_img", self.plain)
        self.assertNotIn("set_depth", self.plain)
        self.assertNotIn("blender_view_proj", self.plain)

    def test_depth_adds_exactly_two_image_slots(self):
        """
        Depth mode costs TWO slots, and the count is asserted because Blender only
        has eight and a declared-but-unused image consumes one anyway.

        `depth_img` is the per-pixel nearest depth. `depth_range_img` is the two
        texels holding the fractal's own min and max depth, which is what lets the
        bands span the range the fractal actually occupies — window depth is
        nonlinear enough that a scene at distance 5..20 lives in 0.980..0.995, so
        bands spread over [0,1] put every one of them behind real geometry.

        Running total with depth on: histogram 0, state 1, depth 2, range 3.
        """
        names = {r.name: r.slot for r in DEPTH_EXTRA_LAYOUT}
        self.assertEqual(2, len(DEPTH_EXTRA_LAYOUT), f"slots changed: {names}")
        self.assertEqual({"depth_img": 2, "depth_range_img": 3}, names)

    def test_volume_and_depth_are_rejected_together(self):
        with self.assertRaises(ValueError):
            rewrite(self.raw, volume=True, depth=True)


if __name__ == "__main__":
    unittest.main()