"""Check real MASM framebuffer captures, packed decor and tested/shipping parity.""" import argparse import struct from pathlib import Path from PIL import Image, ImageChops, ImageStat from check_card_exit_build import Coff ROOT = Path(__file__).resolve().parents[2] OUT = ROOT / "build/actions" FUNCTIONS = { "LoadWorldDressing3D": 0, "FreeWorldDressing3D": 0, "ResolveDressingSurface3D": 4, "RenderHallDressing3D": 0, "RenderStreetDressing3D": 0, "HallLightVisible3D": 16, "StreetGroundHeight3D": 8, "UpdateStreetGround3D": 0, "AddHallLight3D": 16, "AddHallContact3D": 12, "BuildHallLighting3D": 0, "ApplyHallLighting3D": 0, "InitHall3D": 0, "UpdateHall3D": 0, "RenderHall3D": 0, "LoadHallAssets3D": 0, "FreeHallAssets3D": 0, "PositionCityCamera3D": 0, "RenderCityArrival3D": 0, "DrawCityHorizontal3D": 24, "ResolveCitySurface3D": 4, "RenderWalls": 0, } def assets(): raw = (ROOT / "assets/animations/hall/world-dressing-v6.bin").read_bytes() assert struct.unpack_from("<4s4I", raw) == (b"ANM1", 128, 128, 3, 196608) assert len(raw) == 196628 for index in range(3): start = 20 + index * 65536 frame = Image.frombytes("RGBA", (128, 128), raw[start:start+65536], "raw", "BGRA") assert set(frame.getchannel("A").tobytes()) <= {0, 255} assert frame.getbbox() == (0, 0, 128, 128) assert len(frame.convert("RGB").getcolors(16384)) > (100 if index < 2 else 5) print("PASS: two framed paintings and a drain material; valid dimensions, length and binary alpha") def capture(name): frame = Image.open(OUT / (name + ".bmp")).convert("RGB") assert frame.size == (640, 480), name assert len(frame.getcolors(307200)) > 1000, name frame.save(OUT / (name + ".png")) return frame def captures(): unlit = capture("world-hall-unlit") lit = capture("world-hall-lit") difference = ImageChops.difference(unlit, lit) assert difference.getbbox() == (0, 0, 640, 480), "Light pass did not cover the world" values = [sum(pixel) for pixel in difference.get_flattened_data()] assert sum(value > 6 for value in values) > 170000, "No visible lighting change" # Lighting changes channel gains, not the underlying texture resolution. for channel in range(3): gains = [new[channel] / old[channel] for old, new in zip(unlit.get_flattened_data(), lit.get_flattened_data()) if 32 <= old[channel] <= 180] assert min(gains) > .4 and max(gains) < 1.5, channel regions = [(0, 0, 160, 160), (240, 240, 400, 400), (400, 300, 560, 460)] means = [ImageStat.Stat(difference.crop(region)).mean for region in regions] assert max(sum(mean) for mean in means) - min(sum(mean) for mean in means) > 3 front = capture("world-wall-front") angle = capture("world-wall-angle") assert ImageChops.difference(front, angle).getbbox(), "World-mounted decor did not reproject" frames = [capture("world-street-" + phase) for phase in ("early", "middle", "late")] assert all(ImageChops.difference(a, b).getbbox() for a, b in zip(frames, frames[1:])) print("PASS: native lit/unlit comparison, preserved texture detail, wall parallax and three street positions") def parity(): tested = Coff(OUT / "actions-test.obj") shipped = Coff(ROOT / "build/graphics/poker.obj") for name, arguments in FUNCTIONS.items(): symbol = f"_{name}@{arguments}" assert tested.signature(symbol) == shipped.signature(symbol), name # Test-only pixel ownership counters wrap this renderer. Compare the complete # production body after its register prologue, excluding only its epilogue. symbol = "_RenderCityObjects3D@0" actual = tested.signature(symbol) expected = shipped.signature(symbol) begin = next(i for i, item in enumerate(expected) if item[2] == "_AnimateCityCars3D@0") end = len(expected) epilogue = {b"\x5f", b"\x5e", b"\x5b", b"\x5d", b"\xc9", b"\xc3", b"\x8b\xe5"} while end > begin and expected[end-1][0] in epilogue: end -= 1 core = expected[begin:end] assert len(core) > 35 and len(expected)-end >= 4 offset = next(i for i, item in enumerate(actual) if item[2] == "_AnimateCityCars3D@0") assert actual[offset:offset+len(core)] == core, "Street object production body" assert any(item[2] == "_UpdateStreetGround3D@0" for item in core) print("PASS: shipped hall/street dressing, light baker, shader and camera match native-tested MASM") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--assets-only", action="store_true") parser.add_argument("--parity", action="store_true") options = parser.parse_args() assets() if not options.assets_only: captures() if options.parity: parity()