From 99652d894b40dd54587cac3460d93aed7017bacd Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 3 Jul 2026 16:22:35 -0500 Subject: [PATCH] Phase 3d: game world decodes and renders -- offline and LIVE The full DPL hierarchy the game uses (vs flyk's flat scene) is now decoded and rendered: - stride-aware set_geom_verts (header word 3 = floats/vertex: 3/4/5/8/9; mech meshes carry normals + UVs) - instances are list_add children of DCS nodes; instance flush field 4 -> object; object->lod->geogroup->geometry; dcs_link builds the articulation tree of 4x4s (payload floats 4..19, row-major, row 3 = translation) - game world is y-down (DCS matrices carry a reflection); projection flips x (Division mirror) and y render_game.py reconstructs a captured game stream offline: the mission arena (10km, 246 instances, 330 geometries), the player's Thor at the camera, six enemy mechs 1.5km north -- game-mech-decoded.png shows one with real hull/armor/glass materials; game-cockpit-decoded.png the cockpit view. The live backend (vpxlog.cpp) gained the same traversal and now draws the game's out-the-window view in real time (game-live-gl.png): sky, arena floor to the horizon, own gun barrels at frame bottom. Next: texturing (action-26 texel maps + UVs), lighting from wire normals, per-frame articulation once the RIO drives the sim. Co-Authored-By: Claude Opus 4.8 --- emulator/.gitignore | 3 + emulator/PHASE3-PROGRESS.md | 48 +++++ emulator/game-cockpit-decoded.png | Bin 0 -> 27147 bytes emulator/game-live-gl.png | Bin 0 -> 12145 bytes emulator/game-mech-decoded.png | Bin 0 -> 25912 bytes emulator/render_game.py | 307 ++++++++++++++++++++++++++++++ emulator/vpx-device/vpxlog.cpp | 194 +++++++++++++++---- 7 files changed, 512 insertions(+), 40 deletions(-) create mode 100644 emulator/game-cockpit-decoded.png create mode 100644 emulator/game-live-gl.png create mode 100644 emulator/game-mech-decoded.png create mode 100644 emulator/render_game.py diff --git a/emulator/.gitignore b/emulator/.gitignore index 4dcd35b..e1eddc1 100644 --- a/emulator/.gitignore +++ b/emulator/.gitignore @@ -6,6 +6,9 @@ image/ !divrgb-decoded.png !divrgb-frame0.png !divrgb-live-gl.png +!game-cockpit-decoded.png +!game-mech-decoded.png +!game-live-gl.png dbx_out.txt vpx*.txt sweep_*.txt diff --git a/emulator/PHASE3-PROGRESS.md b/emulator/PHASE3-PROGRESS.md index ff9eb11..8cb40bd 100644 --- a/emulator/PHASE3-PROGRESS.md +++ b/emulator/PHASE3-PROGRESS.md @@ -112,6 +112,54 @@ before its first receive, which the POLL_THRESHOLD gating stalls. This is not a 3b issue (the `flyk` clean-launch path renders fine); it is the same production-sync item still open from Phase 2. +## 3d. The game's own world decodes and renders — LIVE + +![live GL window: BattleTech cockpit view](game-live-gl.png) + +The live backend now draws the game's actual out-the-window view in real +time: sky, the arena floor receding to the horizon, and the player's own gun +barrels at frame bottom (static scene — without a RIO the sim doesn't +advance). Same frame decoded offline below. + +![decoded BattleTech mech from the game's wire stream](game-mech-decoded.png) + +That is an enemy mech (object 1048, 488 verts) standing in the mission arena — +reconstructed **entirely from the game's captured FIFO stream** by +`render_game.py` (real hull/armor/cockpit-glass materials from the wire; the +half-buried look is the offline painter's-algorithm artifact, which the live +GL depth buffer doesn't have). `game-cockpit-decoded.png` is the actual +cockpit camera: the arena floor to the horizon with the player's own gun +barrels rising at frame bottom. + +What the game adds over flyk's flat scene (all now handled, offline + +live backend): + +- **Stride-aware vertices.** `set_geom_verts` header word 3 = floats per + vertex: 3 (xyz), 4, 5 (xyz+uv), 8 (xyz+normal+uv), 9. Mech meshes carry + normals and texture coordinates — lighting/texturing data is on the wire. +- **Full DPL hierarchy.** Instances are `list_add` children of DCS nodes + (dcs→instance); instance flush field 4 references the object (type 7); + object→lod→geogroup→geometry via list_add. `dcs_link` (action 7) builds the + articulation tree (mech torso/arms/legs), each DCS a 4×4 at payload floats + 4–19 (row-major, row 3 = translation). +- **The world is y-down** (the DCS matrices carry a y reflection; game coords + vs Division's). The renderer flips both x (Division mirror) and y. +- **Mission scene scale**: 10 km arena of 1000-unit ground tiles, 246 placed + instances, 330 geometries, 280 materials; the player's Thor (644 verts, + articulated sub-parts as sibling instances) sits exactly at the camera; + six enemy mechs stand ~1.5 km north. +- Camera: action 31 (rotation + eye at the cockpit position); with the sim + stalled (no RIO) it arrives once and the scene is static. + +Offline tool: `render_game.py ` (near-plane clipping, hierarchy +traversal). Fixture: a 954-frame capture. The live backend (`vpxlog.cpp`) +gained the same traversal: stride-aware geometry, dcs/instance/object link +decode, cached world transforms per DCS, y-down projection. + +Still to come: texturing (stride-8 UVs + action-26 texel maps), lighting from +the wire normals, per-frame articulation once the RIO drives the sim, LOD +selection by distance. + ## 3c. The full game runs through the live renderer (sync abort fixed) **The production `vr_sync` abort is fixed, and BattleTech v4.10 now runs diff --git a/emulator/game-cockpit-decoded.png b/emulator/game-cockpit-decoded.png new file mode 100644 index 0000000000000000000000000000000000000000..70a8fb3bc7c9d12f93b3738854e2cf6e8c0d422d GIT binary patch literal 27147 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 Tgz7Ei?;S2`8)%hk+64bUC;;q_ literal 0 HcmV?d00001 diff --git a/emulator/game-live-gl.png b/emulator/game-live-gl.png new file mode 100644 index 0000000000000000000000000000000000000000..f49cc7ac1f308b7d2fde4cc0366b944e8e18049d GIT binary patch literal 12145 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 literal 0 HcmV?d00001 diff --git a/emulator/game-mech-decoded.png b/emulator/game-mech-decoded.png new file mode 100644 index 0000000000000000000000000000000000000000..753c36da813d23123fe8ed6ce0fcc681f6374a2f GIT binary patch literal 25912 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

3OtU)N9aHgW+I`qT zGBu+4%?b6`zwzqXfwfD<;qT=h%44R8qV+!i8vBx{o37Bt=lGPt=aPrTNk=`w@WfrZ z9nPipz;I@! zq3lHRSiXgG-d5p?QSF`e4@33lqp`zO6Qkq5Z}p*st{ndw4)*xpEGHQBQiFg9NLNJ}zy&uj%{b_Nx0hR{$qa+!R?^ zOE?tUR9>Y0ZZrK@9!uMdn?$+_S__{82ISCIUt1oy7Jv-7jnkypnj zE#ebFTc4KviyFkP#MFK2Z6GQ7irPE_wVL|5=o|zbK8gDKJns7HZpo;)niw+3+I4S^ z=HJdA98V2zDCeo;%rNt5T(^eNR>LmtZEOs-DP_nog0(L|)AWQmzRrkI(V46N*i(?j ztB~_&LQG+rQ$3lU((X$5;u+T-0njQy70BL;UduHRq9*NKrNGX^GYV(G=q2DSqu- zI6Y|HThgHmb>SI@)$OMb#LoWO@fhNHju5h)9!|d~>JJRuSv>kUf4ChLd<<(gy*iSi zzkbj2QjHGHX;fI{8=ZfD&IgB%$t;EU-XE7wM)xO8PJV#8y76RrosxJ~irrVJb@ofq zne&ePVz%5;H(mI0J^B$bDk$ISSvbv)XN#2qr(G`B9bsj6p_8J@#k82iFHi*@a97{s zG}fBf>P>=s#KzO72~9`uc*L@sHZM!pJV#V`PtVOj!vA0T!P%8e2UtDYb-N-Yb;o^h zy>Jj&l*~pFp-{PLKQ%j~wBs0?WU~>GPzrrOd54=Q3qv`&|2JPJWoT1|h>0td%{J(wHt51=IGF`gvVJU|DVP)6KLga&EjA9qO;{XU zW@karfqU2>|E`+wA0m<`Qo)<3#p~Z&rVsvajy7Z`8U}3Iz%&M*)jqrF>%mQ(o`Pl> zYw#Ky6W_-(crgZ=`_jdek}za~sXWsvIIYWq!eB*?Xai(u#GtIeRR6VtmEV=>ba z&XNg`uy7r_QUP4Eq_(UunS>ZoS4LK9n($Zws9@`sF+dtmdL*t9_P9uitqbVfq@hiG zh<7*bB6(-Lc=t=tZ`+GfjXa=dHnD_)tL0=bzokbm{u%GD@w(xu@;Bq=i?p!zPEdv5 zG#Wg|ku@!yT6Gp4d>wv!!k0C7F-L*=Ep8)^5Yi>lYw}Ic==(m^SW_H)%P}1EcQUwf zYX0~%E_fFec2Bijg8(oG_=gAjD*XWvx9=vw)w4Y$k{vAL1=Pa4ue?G0z=X~=$5loA zhN^oYrda9p-{!MMJ0hrb2_Gg$DD-w6!nyCFkdT5^&4X#KXZ@RvgwFY46z(*Qy`nZ_ zN(MU{wHxSLeq;chbb3>$(>DJd3TA^#ZnslWyIemNYB&-sC*D?l6->s0NvJwb<-Bg|$?E-o zdbkcpHruYR^|T&Ot7=zK6{~7fq*`%_kDjr?(5Edo$FlZ{Ki2!$>AZk_f`~9&jbKV*M=z0^L(pWWtT301B}@I z7PhByPF3=}Z_tOa#62Xwxk%&j5Wo$%!m;d&*(N0H%*V6U8I@CaK0EiQKsBjkghvkw zPL}n4NT{ZT_i}kkF(dV-EHIRG7Y%wH(GBsDKrK>q=uUJ>PMq&)hGM7L?@Nj)Xgd+t zt4t!Z_KSpP_utOseT*)Alq)X;?&NZolykGL(+un-R#+{}(Gi|mY?QOpsVD`t_F~`h-<3VsXbCU#Er;DCe zVwMs*Z>ri@wtoKYtEN>>I$rB)>6HOaHv}a7{WrldSJ#2L67^u=o2m$cL(Y!6|K@tb z_c0Xjcqt+ngh7l?8h}Tqo4i*abq?RJXv8{@NTdgOV~x}$7cRYdHKoU5wnp_1z^NGi zS+7KVwA<)5OQhyexoMra>j;=7w z{4@c}oe!xfG93?_>8*vjR};Zk6(ln$?K_vMd^abn31itvcI-Tx@7k2Tt%D|LR*XqO zhCenMenI!Vg@42*oa?6QfU}0;@y@R`nMKQy`9!l&o{YZ4k&di%Y|Jr0awOJ!G-RM) zFh~T%(v<6gK=y}vw2nlO!#wz|th5%Z+@WbjIPxDEfLuCQ3&_ee8T54M?2+r#RXYQx zA|dEEA6c1V&^{&^n*qIj=Bwj(+*zTO(LiGJ{8J1JKZ=@sNk-A#0t!L{>q%4r&k*I+ zB`Kj;l*M>z&}!3fx(Chi`c;U2l-RT9{Sf=y8h)+YW#R+GPHl{+`uZl&Hz;8uK z%GBd%C0OVzKks&kO_hs>J5NgtCg|kknhiK)=eOe<2yaKgLWL;DOybZ=h{Y&pXHJIL_yl|Z`3A4)`39q| zkMTb;{vq=jxb(Wd$^6;dnYt^S8OR;l+@4Xn8@*!k@<$8eZO1C6&M|XZlWl_X5-9NS znDrQe?rE2|?SR3o<3%4kd#8uSD@jF(-6lI{s;cTJJ&ETaNWt8^6v8SR;BR61$6=Yd!tWi=^uq{ZXEMRwVw6O}?FJC!Nc!74NTnD; zfYxBP1EZm}G^=F12M__fY9YimZ@$ z7f|#S|I7h@lqL2_Dq#=Zp{4ZM`3Ws=D@VNxD*Vq0z)tokdZ%OS3^szL;#qne`n667 zA*(13wrb2h+26i;L$iXrx2#E&VGhIctd((d<;Gh-@bea63%)% z_l0n0#P!cX-?A|A{FQNl6Pg$j;+V9}%}o3GILc%Gx(ok%+o8xGk(ahrRSC&<%+*x{ zIji7>&l%Q-fMST5yu`wBl9;zSaE!Ogg>*yaXO~XfPRJQkHCY|DbVjc^|6r{{SMNnz zgp5rjQDaYcq2I?_ZQnw=4et8+l$Z*E2JV(fJ9pBK2r=dMWLtWy)cm87I|Ki(LuqtM~0cD|=jeA$AANn>I%&KK* zLE%>!tMgv{K2~@(eFYkJG-aZ)iLD(qV&V?AOszf0nZF2mDa6ugmkZ{6WoNef$#h92 z@L%_#5QWc-T1xlKm~To$2E_DOqp|jd@H8je#EKh}M5`?&CBrxab6}o|_2Hgy+X3U* zv*PJr7im@i`H+`vl|FM<8P|r@&giD{?qI&gzBtc8dQT~>tVVv~7w2U}eo&>cg2s=; zZYhVWM=6V<3k~`%^HcWU%q>M+GT1n3WSCVMjPfu=>G9S?{KtP-oj@RT5C7`d%7Qs5 zQ7%=Ev-K;#Yd!+_w&v{Njux~t?S0SUD%8_tMDMLYVLTr+0x5Sz)LkniQlOn=XF!hP z3iIQW{rqK+iK*z-YExto)OvYMoGy>^t}p-r=Lb^IbCS4*1w_)-`1*_O*qO`n{g+>_ z-d_J(X7+au7_n= za?54Uu%`TNB>p*k0T0nUGLK2PX<^dW8);kH@1@cA% zaqW#}&4^1VR?xic7gMcvQHN6}TqqyY!BDIKV*W^tCoA^3=KiiH?3hoE1M?Lef6Vt# zF}ou##Qbzg>^ZSA$-V*~XgwIC;tv29x;Nn!Dvk+iQl{~)9WfQ7Bj z97p{*q?l6X%eN?J=l*r0FOGZ`o{r5iAN4S&?lVS^GKldi1$Ha$d2d*+_aC7$BdRy4a!du=--x z&;ZE5wruWfIw0v`)zY@VV1UC6Y!h|R&7ub#gK(BhP-=a6eDec?+NTpQTZp6}D;L=k zc|q_dV#+sGS*A@DgkUVjh04}F0SO@iDC=DI`Ic{}>tssX_5(l{lZ!6z(}?1SqX%~L zkK;b+CqJMpUXwBaRK_04za33;K8s$@vMRgl0XV@J?8@f!3lKx22$h}PUPjZB58<6n zyhlg(86V!HAcSjXe_e*5t+>;5c@3<2%1q1o`I1S(5+imw+XLf?@+mNM(OKU8$y-(j zFqMi_=EY|v2O(<*Q8K6o@X(acJn^GHb&f3R&K(Q1RlppNUvNz!TA<3j&URv!^#Pi5 z#TBUUGu@_D`Q$20*>c5SvRpKYn$G*9(PO7ek^(v(%u4KYNVc4Wdhg+T z{?l>!N5cSxXy$zN!$oZ_0Ls)!`>O#)H*3e2^%Sp3bP4$L!}E@F9YSn^yuP>1X#0|4s4IJ<6Istx!D;aVwtI@Ph-AmBidV zc2@wxli13Z$-^eB2X_MS9oB8!hvG?F7)}yXvj-=oma<=w3_ecjpW2FLp}7%ZGeo11 zzeAl7rO7lJCr{Sek*rjXKF@OxbkvyuEdW_|Fd`^lgi;> zI6=N%$bBZWD*M8V?Ei+uQIVhRi!xQqNzy^Zd5n#ZE#+p?An-*THr|q}_M^{IzzT{u zhACuB_eiPJcSMnN3fQ4V724wXFYOcqmFa4odvh?l&9Yoo&&`D3nRPaUZ{CsS+=!Ji zciu&lkFj^?F{T4F-~Y_u$`Q`mW>-H|XRO>z%J%8N2qaF(lN454$0jucsKZ8#XiHOB z;?xB1@gU|Nw6&FOdA<0$9=>M8=e$6vm(jyM=bmu&!F-w)&VlSCl$h(=KiV7ElViorQzK@o(b0- z9gqSD$HXM7US*+J;oLJ-Y<@%ds5i#4EkZ@+8xA`>;_#;5H~-kTHj*~;GF6_gp(mAB zyVdED+d~|G&8S{gnA6M?D_5;;zQB}9eZ_{y7T#AsX4Ye}oBtJeEQ7Uf#65|2Y+&#V z1SNi*lWeV=<2`4b_ZOIMb!&}t(g(NWJq9ifIp6-dq(LV;mBM>+Nb(Gs zOfwL+Ic^^OSDgFN(?8)?t*7$~dD%~a_izyG(jbt&_k0BY4SG5jo`*xEl7(#kJP}@J z3}7c4HZ}j2iFl2YUaP%+%DI69lHKI=fznCq{%?Z^*0ylK4!8oCtG!YZTrLWO)=q#Z z15nmkfO08C*cj*8gnjzot6;x%G~qy#TcOe`oO5}d(4nojHj#a?GgwC@)Z1=-B#fX} zt!TT}7JgN@gF2lQLd~nkt=6cV3`r2rIVv9gmz)(~NidJSqjHDa)!#M&Dd2*+*k#^z zirc*vVE1Egli5{$Y=5QkZUet`3J(9nfQilC%=PyPM$UCWlVPp?!9H)YsTRl%R<%tR zdcg9zPH8VJbVEgE(Cdog>&di6A_p+P#SU#*rbzhYAQPbo#t*k&z$R)%j_O#9opuk_ z=s#t^)2w)lSFm~OUdn3SCrMzESw<%!M-~wN3T37W$B1ri!irm{*Dg7{O|Op66fpI|8!FR*6GS(h9}A8 zs&WoZ@oNS8c7(v}t3q5)R!q0O4-1=esx(kU8u~jx?p#wi>#vZjn0^FFV91kUL@~XX zfEd0TAx`}|cdYWZlwg+r(cQ+73CKGqaJ!f6isu-xD{$c zYbmJ_828yoI1xWw_r9~BlZDQ`QA(EuoLp&MWWK($@3{;Vv1jF)dSyRrXIlGXDOkJk zG#9r=+7X%VWp6;-mMq>fK5QbZ+sY0QZF%*^hfK-WMFi!A9LK`uP0TkZ;tRrF2fpC2 zeZ2}Yd-PnfX)~woysF5txth54No}{ETg^{3r#{eDYhEqbTH3(PVKK`ZG*Q*i8Ik<&yPu!4DV?Y~)fr@+AAsG&$;nB2Gbj0FIcAAPpy8TZ_SFtvI3BMJ3t-`QLD zq}B9+)J(kDeIJ(1nX>p2-$w?>Yp9Lt(VK(X&3-~pG>a;(Pdx&jGe*-^aRHwCx2PZ? z_w=V@Ctj#KwYixCV@~-3r542TbE(pbK2=5es(r2t{w3tnDp*)t`jI~7N6WdPiKC~P zt00S4`ZOz(n(=2}vLjAa>f2FiiL|oh$`=PZ~!$0~mn%?||8<@5fG# z>)6(x@1|+s*MPi>)l`lSob*SmznXTfA;1ec>MxSBd8_e zLBtB=;vlNkuwmR_CwcnCLZvjI%SO!MxWbN$YBs|J+JX~0^q2upa#lL~;6d?u;8Wle z==}k}?Qi3Frb6ZimCI$be(-wsyS2=mXCRTwxVEG15k@7;mG#^gT^@cQGqM;eH*Out zq|daS&@^tK7*cZm)xu5uiwkP{rG9AN{g4XsA#WHiU|opW+04KGifg|ZB(E@Z$Or9) zTc!j6Lr*iEfdz*$g0BIi8A9fvVbQ6DGY{;fo9|qO59XX7E3hz$${jig31f!+sY~iq z?O#z0dI?`)cfDw@aQrg@sN&+fDcP`V{sJ*`X}pIs2$lUSJz5!=o%fB+QHN?)BC3+g zr(etW4Kl9CkzFe{`ZQ<%26hMzgp78Eug3cNR{q&F=Yfgu<2+jGM!()yh^PvPDv)d7 zlxitVLi1mbk!zWkO8dn?!V>#&@APHcyli{xe7%D7dq?lO=hR66BtcdtBy}6_o_}6TMG4eg#sFQI%0!*_=Q!90h1Xd? zTl^ioXez>?-gUmh7!xtjvFvV_68v6p@;$+Wa81XXg^4_ri~9{!R)dxmdALvQq{d7u zQy4mmVd;vj)r#n*5`eTu{nHv7leN<0^?oYz)nJb(`MCLfZnoi!1UT_Eft_Sv<^xnh z<2I+ZmLj5(vI%=?5N36jkW3&ilBjiVcm-X^e==g<2^7IO@TSDmOX+ITCI}+-VL!hG z(jObJBqxk;Z1|F6wNgH@UlFo><$T?w$Ytby^i?`3ejsz)3m{<-RSGx6`tx3-J&%TO~jDGny%yrdGRB!*CZ55Qt=hWCns2iCPeqv z)iI*MKA!74)Vf8ZdzV#Vz@qX%)Fzm+n{Q;^j3>*`NCBR^NBoeEiMjlv^YXP<1lL^g zp;8@K;8TC9iHe2ts=K6hLpc0LSsxGAb$%{At E19wHPy#N3J literal 0 HcmV?d00001 diff --git a/emulator/render_game.py b/emulator/render_game.py new file mode 100644 index 0000000..9c1b08e --- /dev/null +++ b/emulator/render_game.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 +"""Phase 3d: render a captured *game* VPX FIFO stream (full DPL hierarchy). + +Unlike flyk's flat DIVRGB scene (render_capture.py), the game builds the real +DPL graph: instances reference objects and DCS transform nodes; objects hold +LODs holding geogroups holding geometries; dcs_link builds an articulation +tree of 4x4 matrices (mech torso/legs/arms). This reconstructs that graph +from a VPX_FIFODUMP capture and renders the scene each draw_scene commits. + +Node types (empirical, Rel4.10 wire): 2=? 3=view 4=instance 5=dcs 6=material +(old) 7=object 8=lod 9=geogroup 10=geometry 11=material 12=texmap 13=? 14=? + +Usage: render_game.py [-o out.png] [--frame N] [--eye x,y,z] +""" +import struct +import sys + +from PIL import Image, ImageDraw + + +def read_messages(path): + msgs = [] + with open(path, "rb") as f: + while True: + hdr = f.read(8) + if len(hdr) < 8: + break + if hdr[:4] != b"VPXM": + raise SystemExit("bad magic") + d = f.read(struct.unpack("= 4: + msgs.append((struct.unpack(" [children] + self.inst_object = {} # instance -> object + self.inst_dcs = {} # instance -> dcs + self.dcs_mat = {} # dcs -> 4x4 local matrix + self.dcs_parent = {} # dcs child -> parent (dcs_link) + self.view = None + self.background = (0, 0, 0) + self.frames = [] + + +def parse_dcs_matrix(f): + """The 132-byte dcs flush payload: [name][type] then fields; the 4x4 is + the 16 floats starting at float index 4 (rows [x,y,z,0], row 3 = T).""" + m = [f[4 + r * 4: 8 + r * 4] for r in range(4)] + # sanity: last column should be ~(0,0,0,1) + if abs(m[3][3] - 1.0) > 0.5 and abs(m[0][3]) < 0.01: + # some builds put w=1 elsewhere; force affine + m[3][3] = 1.0 + for r in range(3): + m[r][3] = 0.0 + m[3][3] = 1.0 + return m + + +def reconstruct(msgs): + sc = Scene() + camera = None + geom_pend = None + conn_pend = None + for action, d in msgs: + if action == 1: + w = U(d) + if len(w) >= 2: + sc.types[w[1]] = w[0] + elif action == 3: + w = U(d) + if len(w) < 2: + continue + name, t = w[0], w[1] + if t == 11 and len(d) >= 92: + f = F(d) + sc.material[name] = tuple(f[12:15]) + elif t == 9 and len(d) >= 80: + sc.gg_material[name] = w[16] + elif t == 3 and len(d) >= 104: + f = F(d) + sc.view = (f[6], f[7], f[8], f[9], f[10], + f[11], f[12], f[13], f[14]) + sc.background = tuple(f[15:18]) + elif t == 5 and len(d) >= 132: + sc.dcs_mat[name] = parse_dcs_matrix(F(d)) + elif t == 4: + # instance: find object/dcs refs by node-type lookup + for val in w[2:]: + if val and val != 0xFFFFFFFF: + vt = sc.types.get(val) + if vt == 7: + sc.inst_object[name] = val + elif vt == 5: + sc.inst_dcs[name] = val + elif action == 7 and len(d) >= 8: # dcs_link parent -> child + w = U(d) + sc.dcs_parent[w[1]] = w[0] + elif action == 11 and len(d) >= 8: + w = U(d) + sc.children.setdefault(w[0], []).append(w[1]) + elif action == 23: + if geom_pend is None: + w = U(d) + if len(d) >= 36: + # header: [name][0][n_verts][stride_floats][n_msgs]... + geom_pend = [w[0], w[2], w[3], []] + sc.verts[w[0]] = [] + else: + name, n, stride, acc = geom_pend + acc.extend(F(d)) + if len(acc) >= n * stride: + sc.verts[name] = [ + (acc[i], acc[i + 1], acc[i + 2]) + for i in range(0, n * stride, stride)] + geom_pend = None + elif action == 25: + if conn_pend is None: + w = U(d) + if len(d) >= 16: + conn_pend = (w[0], w[1], w[2]) + sc.polys[w[0]] = [] + else: + name, n_polys, loop = conn_pend + idx = U(d) + pl = sc.polys[name] + if loop >= 2: + for i in range(0, len(idx), loop): + pl.append(idx[i:i + loop - 1]) + if len(pl) >= n_polys: + conn_pend = None + elif action == 31: + f = F(d) + camera = ([f[2:5], f[5:8], f[8:11]], f[11:14]) + elif action == 9: + sc.frames.append(camera) + return sc + + +def dcs_world(sc, dcs, cache, depth=0): + if dcs in cache: + return cache[dcs] + m = sc.dcs_mat.get(dcs, mat_id()) + p = sc.dcs_parent.get(dcs) + if p is not None and p != dcs and depth < 64: + m = mat_mul(m, dcs_world(sc, p, cache, depth + 1)) + cache[dcs] = m + return m + + +def gather_polys(sc): + """instance -> object -> lod -> geogroup -> geometry, transformed. + + Placement: instances are list_add children of DCS nodes (dcs -> instance); + dcs_link builds the dcs->dcs articulation tree above them.""" + inst_parent = {} + for parent, kids in sc.children.items(): + if sc.types.get(parent) == 5: + for k in kids: + if sc.types.get(k) == 4: + inst_parent[k] = parent + out = [] + cache = {} + for inst, obj in sc.inst_object.items(): + world = mat_id() + d = inst_parent.get(inst, sc.inst_dcs.get(inst)) + if d is not None: + world = dcs_world(sc, d, cache) + for lod in sc.children.get(obj, []): + # use only the first (highest-detail) LOD child set + ggs = sc.children.get(lod, []) + if not ggs: + continue + for gg in ggs: + rgb = sc.material.get(sc.gg_material.get(gg, -1), + (0.8, 0.2, 0.8)) + col = tuple(max(0, min(255, int(c * 255 + .5))) for c in rgb) + for geo in sc.children.get(gg, []): + vl = sc.verts.get(geo) + if not vl: + continue + wv = [xform(v, world) for v in vl] + for poly in sc.polys.get(geo, []): + pts = [wv[i] for i in poly if i < len(wv)] + if len(pts) >= 3: + out.append((pts, col)) + break # first LOD only + return out + + +def render(sc, frame, out, ss=2): + if sc.view: + wl, wb, wr, wt, wd, vw, vh, near, far = sc.view + vw, vh = int(vw), int(vh) + else: + wl, wb, wr, wt, wd = -1, -0.6154, 1, 0.6154, 1.732 + vw, vh, near, far = 832, 512, 0.25, 1150 + cam = sc.frames[frame] + if cam is None: + raise SystemExit("no camera at that frame") + rot, eye = cam + W, H = vw * ss, vh * ss + bg = tuple(max(0, min(255, int(c * 255 + .5))) for c in sc.background) + img = Image.new("RGB", (W, H), bg) + draw = ImageDraw.Draw(img) + + def project(p): + x, y, z = (p[0] - eye[0], p[1] - eye[1], p[2] - eye[2]) + ex = rot[0][0] * x + rot[0][1] * y + rot[0][2] * z + ey = rot[1][0] * x + rot[1][1] * y + rot[1][2] * z + ez = rot[2][0] * x + rot[2][1] * y + rot[2][2] * z + return ex, ey, ez + + def clip_near(evs, zn): + """Sutherland-Hodgman clip against eye-space plane z = -zn.""" + out = [] + n = len(evs) + for i in range(n): + a, b = evs[i], evs[(i + 1) % n] + ain, bin_ = a[2] <= -zn, b[2] <= -zn + if ain: + out.append(a) + if ain != bin_: + t = (-zn - a[2]) / (b[2] - a[2]) + out.append((a[0] + t * (b[0] - a[0]), + a[1] + t * (b[1] - a[1]), -zn)) + return out + + polys = gather_polys(sc) + print(f"gathered {len(polys)} world polys") + items = [] + for pts, col in polys: + evs = clip_near([project(p) for p in pts], near) + if len(evs) < 3: + continue + depth = sum(p[2] for p in evs) / len(evs) + scr = [] + for ex, ey, ez in evs: + # game world is y-down (DCS matrices carry a y reflection), so + # screen-up = -eye_y; screen-x mirrored as with flyk. + ndc_x = (-ex * wd / -ez - wl) / (wr - wl) + ndc_y = (-ey * wd / -ez - wb) / (wt - wb) + scr.append((ndc_x * W, (1 - ndc_y) * H)) + items.append((depth, scr, col)) + items.sort(key=lambda it: it[0]) + for _, scr, col in items: + draw.polygon(scr, fill=col) + if ss > 1: + img = img.resize((vw, vh), Image.LANCZOS) + img.save(out) + print(f"rendered frame {frame}: {len(items)} polys -> {out} ({vw}x{vh})") + + +def main(): + path = sys.argv[1] + out = "game_frame.png" + frame = -1 + if "-o" in sys.argv: + out = sys.argv[sys.argv.index("-o") + 1] + if "--frame" in sys.argv: + frame = int(sys.argv[sys.argv.index("--frame") + 1]) + msgs = read_messages(path) + sc = reconstruct(msgs) + print(f"{len(msgs)} msgs: {len(sc.verts)} geoms, {len(sc.material)} mats, " + f"{len(sc.inst_object)} placed instances, {len(sc.dcs_mat)} dcs, " + f"{len(sc.frames)} frames, view={sc.view}") + if "--eye" in sys.argv: + eye = [float(v) for v in sys.argv[sys.argv.index("--eye") + 1].split(",")] + sc.frames = [([[1, 0, 0], [0, 1, 0], [0, 0, 1]], eye)] + frame = 0 + render(sc, frame, out) + + +if __name__ == "__main__": + main() diff --git a/emulator/vpx-device/vpxlog.cpp b/emulator/vpx-device/vpxlog.cpp index 4cec028..ec98559 100644 --- a/emulator/vpx-device/vpxlog.cpp +++ b/emulator/vpx-device/vpxlog.cpp @@ -443,16 +443,19 @@ struct VFrame { float nearp, farp; int vw, vh; bool has_cam; + bool ydown; /* game world is y-down (DCS reflections) */ float rot[9], eye[3]; /* row-major rotation; eye = R*(p - e) */ std::vector polys; VFrame() : valid(false), nearp(2), farp(12000), vw(832), vh(512), - has_cam(false) { + has_cam(false), ydown(false) { bg[0] = bg[1] = bg[2] = 0; win[0] = -1; win[1] = -0.6153846f; win[2] = 1; win[3] = 0.6153846f; win[4] = 1.3f; } }; +struct M16 { float m[16]; }; /* row-major 4x4, row 3 = translation */ + static struct VScene { std::map type; /* name -> node type */ std::map > verts; /* geometry -> xyz */ @@ -460,12 +463,53 @@ static struct VScene { std::map mat; /* material -> RGB */ std::map ggmat; /* geogroup -> material */ std::map > children; + /* game (full DPL) hierarchy: instance placement + articulation */ + std::map inst_object; /* instance -> object */ + std::map dcs_mat; /* dcs -> local matrix */ + std::map dcs_parent; /* dcs_link child->parent */ VFrame view; /* view/bg/camera state */ - /* multi-burst assembly */ - unsigned geom_node; size_t geom_need; bool geom_active; + /* multi-burst assembly (stride-aware: game verts are 3..9 floats each) */ + unsigned geom_node; size_t geom_need, geom_stride; bool geom_active; + std::vector geom_acc; unsigned conn_node, conn_npolys, conn_loop; bool conn_active; } S; +static void m16_id(M16 &o) { + for (int i = 0; i < 16; i++) o.m[i] = (i % 5 == 0) ? 1.0f : 0.0f; +} +static void m16_mul(const M16 &a, const M16 &b, M16 &o) { /* o = a * b */ + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) { + float s = 0; + for (int k = 0; k < 4; k++) s += a.m[r * 4 + k] * b.m[k * 4 + c]; + o.m[r * 4 + c] = s; + } +} +static void m16_xform(const M16 &w, const float *v, float *o) { + for (int c = 0; c < 3; c++) + o[c] = v[0] * w.m[c] + v[1] * w.m[4 + c] + v[2] * w.m[8 + c] + w.m[12 + c]; +} +/* world transform of a dcs: local * parent_world (row-vector convention) */ +static void dcs_world(unsigned dcs, std::map &cache, M16 &out, + int depth = 0) { + std::map::iterator ci = cache.find(dcs); + if (ci != cache.end()) { out = ci->second; return; } + M16 local; + std::map::iterator mi = S.dcs_mat.find(dcs); + if (mi != S.dcs_mat.end()) local = mi->second; else m16_id(local); + std::map::iterator pi = S.dcs_parent.find(dcs); + if (pi != S.dcs_parent.end() && pi->second != dcs && depth < 64) { + M16 pw; + dcs_world(pi->second, cache, pw, depth + 1); + M16 w; + m16_mul(local, pw, w); + out = w; + } else { + out = local; + } + cache[dcs] = out; +} + /* ---- render thread ------------------------------------------------------ */ static HANDLE rt_thread = NULL, rt_event = NULL; static CRITICAL_SECTION rt_lock; @@ -484,8 +528,9 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) { glMatrixMode(GL_PROJECTION); glLoadIdentity(); /* Division screen x runs opposite to GL eye x (SMPTE pattern comes - * out mirrored otherwise) -- flip x after projection. */ - glScalef(-1.0f, 1.0f, 1.0f); + * out mirrored otherwise) -- flip x after projection. The game world + * is additionally y-down (its DCS matrices carry a reflection). */ + glScalef(-1.0f, f.ydown ? -1.0f : 1.0f, 1.0f); glFrustum(f.win[0] * n / wd, f.win[2] * n / wd, f.win[1] * n / wd, f.win[3] * n / wd, n, fa); glMatrixMode(GL_MODELVIEW); @@ -565,40 +610,79 @@ static DWORD WINAPI rt_main(LPVOID) { } /* ---- scene-graph message decode ----------------------------------------- */ +static void emit_geogroup(VFrame &f, unsigned gg, const M16 *world) { + VCol col = { { 1.0f, 0.0f, 1.0f } }; /* missing = magenta */ + std::map::const_iterator gmi = S.ggmat.find(gg); + if (gmi != S.ggmat.end()) { + std::map::const_iterator mi = S.mat.find(gmi->second); + if (mi != S.mat.end()) col = mi->second; + } + std::map >::const_iterator ci = + S.children.find(gg); + if (ci == S.children.end()) return; + for (size_t k = 0; k < ci->second.size(); k++) { + unsigned geo = ci->second[k]; + std::map >::const_iterator vi = + S.verts.find(geo); + std::map > >::const_iterator + pi = S.polys.find(geo); + if (vi == S.verts.end() || pi == S.polys.end()) continue; + const std::vector &vv = vi->second; + for (size_t q = 0; q < pi->second.size(); q++) { + const std::vector &idx = pi->second[q]; + VPoly poly; + memcpy(poly.rgb, col.c, sizeof poly.rgb); + for (size_t j = 0; j < idx.size(); j++) { + size_t o = (size_t)idx[j] * 3; + if (o + 2 >= vv.size()) continue; + float out[3]; + if (world) m16_xform(*world, &vv[o], out); + else { out[0] = vv[o]; out[1] = vv[o + 1]; out[2] = vv[o + 2]; } + poly.xyz.push_back(out[0]); + poly.xyz.push_back(out[1]); + poly.xyz.push_back(out[2]); + } + if (poly.xyz.size() >= 9) f.polys.push_back(poly); + } + } +} + static void scene_publish_frame(void) { VFrame f = S.view; f.valid = true; - for (std::map::const_iterator gi = S.ggmat.begin(); - gi != S.ggmat.end(); ++gi) { - VCol col = { { 1.0f, 0.0f, 1.0f } }; /* missing = magenta */ - std::map::const_iterator mi = S.mat.find(gi->second); - if (mi != S.mat.end()) col = mi->second; - std::map >::const_iterator ci = - S.children.find(gi->first); - if (ci == S.children.end()) continue; - for (size_t k = 0; k < ci->second.size(); k++) { - unsigned geo = ci->second[k]; - std::map >::const_iterator vi = - S.verts.find(geo); - std::map > >::const_iterator - pi = S.polys.find(geo); - if (vi == S.verts.end() || pi == S.polys.end()) continue; - const std::vector &vv = vi->second; - for (size_t q = 0; q < pi->second.size(); q++) { - const std::vector &idx = pi->second[q]; - VPoly poly; - memcpy(poly.rgb, col.c, sizeof poly.rgb); - for (size_t j = 0; j < idx.size(); j++) { - size_t o = (size_t)idx[j] * 3; - if (o + 2 >= vv.size()) continue; - poly.xyz.push_back(vv[o]); - poly.xyz.push_back(vv[o + 1]); - poly.xyz.push_back(vv[o + 2]); - } - if (poly.xyz.size() >= 9) f.polys.push_back(poly); + f.ydown = !S.dcs_mat.empty(); /* game path: world is y-down */ + /* Game (full DPL) path: instances are list_add children of DCS nodes; + * instance -> object -> lod -> geogroup -> geometry, transformed by the + * dcs_link articulation tree. */ + std::map cache; + std::map gg_done; + for (std::map >::const_iterator di = + S.children.begin(); di != S.children.end(); ++di) { + if (S.type.count(di->first) == 0 || S.type[di->first] != 5) continue; + M16 world; + dcs_world(di->first, cache, world); + for (size_t i = 0; i < di->second.size(); i++) { + unsigned inst = di->second[i]; + std::map::const_iterator oi = + S.inst_object.find(inst); + if (oi == S.inst_object.end()) continue; + std::map >::const_iterator li = + S.children.find(oi->second); + if (li == S.children.end() || li->second.empty()) continue; + unsigned lod = li->second[0]; /* highest LOD */ + std::map >::const_iterator ggi = + S.children.find(lod); + if (ggi == S.children.end()) continue; + for (size_t g = 0; g < ggi->second.size(); g++) { + emit_geogroup(f, ggi->second[g], &world); + gg_done[ggi->second[g]] = true; } } } + /* flyk (flat) path: geogroups with geometry directly, no instance */ + for (std::map::const_iterator gi = S.ggmat.begin(); + gi != S.ggmat.end(); ++gi) + if (!gg_done.count(gi->first)) emit_geogroup(f, gi->first, NULL); EnterCriticalSection(&rt_lock); rt_pending = f; rt_new = true; @@ -614,13 +698,20 @@ static void scene_burst(const unsigned char *p, size_t n) { /* multi-burst payload continuations take priority over new headers */ if (S.geom_active && action == 23) { - std::vector &vl = S.verts[S.geom_node]; - for (size_t o = 0; o + 11 < nb; o += 12) { - vl.push_back(rd_f32(d + o)); - vl.push_back(rd_f32(d + o + 4)); - vl.push_back(rd_f32(d + o + 8)); + for (size_t o = 0; o + 3 < nb; o += 4) + S.geom_acc.push_back(rd_f32(d + o)); + if (S.geom_acc.size() >= S.geom_need * S.geom_stride) { + std::vector &vl = S.verts[S.geom_node]; + vl.clear(); + for (size_t i = 0; i + 2 < S.geom_need * S.geom_stride; + i += S.geom_stride) { + vl.push_back(S.geom_acc[i]); + vl.push_back(S.geom_acc[i + 1]); + vl.push_back(S.geom_acc[i + 2]); + } + S.geom_acc.clear(); + S.geom_active = false; } - if (vl.size() / 3 >= S.geom_need) S.geom_active = false; return; } if (S.conn_active && action == 25) { @@ -656,17 +747,38 @@ static void scene_burst(const unsigned char *p, size_t n) { S.view.nearp = rd_f32(d + 52); S.view.farp = rd_f32(d + 56); S.view.bg[0] = rd_f32(d + 60); S.view.bg[1] = rd_f32(d + 64); S.view.bg[2] = rd_f32(d + 68); + } else if (t == 5 && nb >= 132) { /* dcs: 4x4 at f[4..19] */ + M16 &mm = S.dcs_mat[name]; + for (int i = 0; i < 16; i++) mm.m[i] = rd_f32(d + 16 + i * 4); + for (int r = 0; r < 3; r++) mm.m[r * 4 + 3] = 0.0f; + mm.m[15] = 1.0f; + } else if (t == 4) { /* instance: object ref */ + for (size_t o = 8; o + 3 < nb; o += 4) { + unsigned val = rd_u32(d + o); + if (val && val != 0xFFFFFFFFu) { + std::map::const_iterator ti = + S.type.find(val); + if (ti != S.type.end() && ti->second == 7) + S.inst_object[name] = val; + } + } } break; } + case 7: /* dcs_link [parent][child]: articulation tree */ + if (nb >= 8) S.dcs_parent[rd_u32(d + 4)] = rd_u32(d); + break; case 11: /* list_add [parent][child] */ if (nb >= 8) S.children[rd_u32(d)].push_back(rd_u32(d + 4)); break; - case 23: /* set_geom_verts header */ + case 23: /* set_geom_verts header: [name][0][n_verts][stride_floats].. */ if (nb >= 36) { S.geom_node = rd_u32(d); S.geom_need = rd_u32(d + 8); + S.geom_stride = rd_u32(d + 12); + if (S.geom_stride < 3 || S.geom_stride > 16) S.geom_stride = 3; S.geom_active = S.geom_need > 0; + S.geom_acc.clear(); S.verts[S.geom_node].clear(); } break; @@ -700,8 +812,10 @@ static void scene_burst(const unsigned char *p, size_t n) { static void scene_reset(void) { S.type.clear(); S.verts.clear(); S.polys.clear(); S.mat.clear(); S.ggmat.clear(); S.children.clear(); + S.inst_object.clear(); S.dcs_mat.clear(); S.dcs_parent.clear(); S.view = VFrame(); S.geom_active = false; S.conn_active = false; + S.geom_acc.clear(); } static void vpx_render_start(void) {