99 lines
5.1 KiB
Python
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")
|