EBLAN_ENGINE/tools/test_preview_ipc.py

99 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Exercise the actual windowless ASM renderer through its public mailbox."""
import ctypes as C
import mmap
import os
from pathlib import Path
import struct
import subprocess
import time
ROOT = Path(__file__).resolve().parents[1]
SIZE, TEXTURES, PIXELS = 1362408, 2016, 1050592
def check_preview(engine, expected=None, output=None, level_path=None):
name = f"Local\\EblanPreview-test-{os.getpid()}-{time.monotonic_ns()}"
with mmap.mmap(-1, SIZE, tagname=name) as packet:
base = C.addressof(C.c_char.from_buffer(packet))
state = C.cast(base+8, C.POINTER(C.c_long))
struct.pack_into("<4I3i6I", packet, 0, 0x56504245, 2, 0, os.getpid(), 160, 1024, 0,
0x383838, 0x222222, 0, 0, 0, 1)
struct.pack_into("<8I",packet,52,0,40,0,0,384,240,45,0)
packet[1358312:1358312+4096]=struct.pack("<I",0x282828)*1024
for y in range(32): packet[128+y*32+10] = 1
# Flat red wall, other slots black; proves texture upload rather than fallback.
packet[TEXTURES+65536:TEXTURES+2*65536] = struct.pack("<I",0xE02010)*16384
proc = subprocess.Popen([str(engine), "--preview:"+name], cwd=ROOT,
creationflags=subprocess.CREATE_NO_WINDOW)
def request():
# Aligned DWORD stores are atomic and release-ordered on x86/x64.
# Payload writes above finish before publishing the request.
state[0]=1
deadline = time.monotonic()+5
while state[0] != 2:
if proc.poll() is not None: raise AssertionError(f"Preview process exited: {proc.returncode}")
if time.monotonic()>deadline: raise AssertionError("Preview renderer did not answer")
time.sleep(.005)
return packet[PIXELS:PIXELS+307200]
try:
frame=request()
middle=struct.unpack_from("<I",frame,(120*320+160)*4)[0]
assert middle not in (0,0x383838,0x222222), "Wall was not rendered"
packet[128+16*32+5]=4
changed=request()
assert changed!=frame, "Unsaved wall change did not reach the renderer"
packet[128+16*32+5]=0
struct.pack_into("<2I",packet,40,1,16)
assert not any(request()), "Full cinematic fade is not black"
struct.pack_into("<2I",packet,40,1,0)
packet[1920:1920+7]="Привет".encode("cp1251")+b"\0"
subtitle=request()
assert not any(subtitle[:8*320*4]) and not any(subtitle[188*320*4:]), "Letterbox missing"
assert subtitle!=frame, "Subtitle overlay missing"
# Compare the live IPC output with a direct Render3DScene regression dump.
if expected:
data=Path(level_path or ROOT/"assets/map.dat").read_bytes()
h=struct.unpack_from("<I7HII",data)
version=h[1]; count=h[7]
struct.pack_into("<3i",packet,16,h[4]*64+32,h[5]*64+32,h[6])
struct.pack_into("<5I",packet,28,h[8],h[9],count,0,0)
packet[128:1152]=data[38:1062]
sprites=bytearray(data[1062:1062+count*24])
if version==1:
for i in range(count):
x,y=struct.unpack_from("<ii",sprites,i*24)
struct.pack_into("<ii",sprites,i*24,x>>16,y>>16)
packet[1152:1152+len(sprites)]=sprites
if version>=5:
transitions,events,strings=struct.unpack_from("<3H",data,26)
lights=struct.unpack_from("<H",data,34)[0]
offset=1062+count*24+transitions*42+events*16+strings*96
mode,ambient,flash,radius,intensity,cone,_,_=struct.unpack_from("<8I",data,offset)
struct.pack_into("<7I",packet,52,mode,ambient,flash,lights,radius,intensity,cone)
offset+=32
packet[1357792:1357792+lights*40]=data[offset:offset+lights*40]
offset+=lights*40
packet[1358312:1358312+4096]=data[offset:offset+4096]
pack=(ROOT/"assets/textures/texturepack.bin").read_bytes()
packet[TEXTURES:TEXTURES+len(pack)-8]=pack[8:]
struct.pack_into("<I",packet,48,2)
actual=request()
assert actual==Path(expected).read_bytes(), "Viewport differs from the standalone ASM renderer"
if output:
header=struct.pack("<2sIHHI",b"BM",307254,0,0,54)
header+=struct.pack("<IiiHHIIiiII",40,320,-240,1,32,0,307200,0,0,0,0)
Path(output).write_bytes(header+actual)
state[0]=3
assert proc.wait(timeout=5)==0, "Preview did not exit cleanly"
print("PASS live ASM viewport: map edits, texture upload, letterbox, subtitles, fade and clean shutdown")
if expected: print("PASS live viewport pixels exactly match the standalone ASM renderer")
finally:
if proc.poll() is None: proc.terminate(); proc.wait(timeout=5)
del state
if __name__ == "__main__":
check_preview(ROOT/"engine.exe")