winpin: Pinball Soccer '98 - #1
Conversation
|
Wowwwww this is incredible! It will take me a bit to get to this but at a skim it is great and I want it all. I am fine with code that partially works as long is it is in the right direction, so the missing things you mentioned sound fine. I am extra impressed you navigated through all of the random places I did something quite hacky, sorry for those! A few first-glance comments:
|
|
Oh and re the |
6628a53 to
136259a
Compare
|
Sorry for the slow reply, I was away on holiday. Thanks for such a generous response. I have rebased onto main and acted on your points; the PR is now about 6k lines instead of 385k. Generated code. Dropped, along with the game's I left BlockMap. Gone. Your instinct was right and by more than I expected, so I replaced it with a 64-entry direct-mapped cache in front of your binary search. I instrumented A one-entry cache gets 96.8%, so your retrowin32 number reproduces here and whatever sits behind the cache barely matters. The other figure that stands out is 935: that is every indirect target this program ever reaches, out of 24114 blocks, so a perfect hash looks very reachable when you want it. Caveat: that is the intro and menus, since I was not driving input, so real table play would widen the working set. Two runs agreed to within a percent. I can measure during a game if it would be useful. To answer the original question anyway: it was an open-addressed table of u32 indices, and there was no good reason it was not a Since you now take Compile time. Agreed, and your second idea is the one I would bet on. 24114 blocks become 24114 functions and I think per-function overhead is most of the cost. Sharding took a cold build from 60s to 24s purely by handing LLVM parallel units, which is the same lever your dominator graph idea pulls harder. nightly. It was not only the wasm bits: catch_unwind. Thanks, I am already using |
evmar
left a comment
There was a problem hiding this comment.
Sorry for the delay, my son had a week off from summer camp so I also didn't have time to look at this! I read through and merged a bunch of the first commit so if you rebase you shouldn't (I think?) conflict. I left comments on some of the other bits.
Big picture, this would be easier to review if it were smaller separate commits. Is this LLM-generated? If it's easy for you to split it I would appreciate it. Otherwise I can try to continue to split as I've done already.
| encode_env(&mut encoder, &state.env); | ||
| encoder.status().unwrap(); | ||
| */ | ||
| // TODO: if available, this ends up hitting a jmp table when parsing |
There was a problem hiding this comment.
I failed at writing a better note to myself, but my recollection is one of my test programs failed when this function returned any data due to other missing functions. It's plausible you implemented all the missing functions in this branch though so I will just figure it out and remember to write a better comment next time. :)
|
|
||
| #[win32_derive::dllexport] | ||
| pub fn ExitThread(_ctx: &mut Context, dwExitCode: u32) { | ||
| // The only x86 thread is the main one. |
There was a problem hiding this comment.
This isn't true, but I guess the warning here will help me track it down if it matters.
| pub fn GetCPInfo(_ctx: &mut Context, _CodePage: u32, _lpCPInfo: Ptr<()>) -> bool { | ||
| stub!(false) // fail | ||
| pub fn GetCPInfo(ctx: &mut Context, _CodePage: u32, lpCPInfo: Ptr<()>) -> bool { | ||
| // CPINFO { MaxCharSize: u32, DefaultChar: [u8; 2], LeadByte: [u8; 12] } |
There was a problem hiding this comment.
I would rather define structures than poke at offsets in a buffer like this.
|
|
||
| #[win32_derive::dllexport] | ||
| pub fn lstrlenA(ctx: &mut Context, lpString: Ptr<u8>) -> i32 { | ||
| if lpString.addr == 0 { |
There was a problem hiding this comment.
Is this legal? I think better to fail here
| } | ||
|
|
||
| struct StaticState(OnceCell<State>); | ||
| unsafe impl Sync for StaticState {} |
There was a problem hiding this comment.
I think this should instead be a Mutex and then no mutex within State. I'll merge for now though.
| pub const RETURN_FROM_X86_ADDR16: SegOfs = SegOfs::new(0xffff, 0xfffe); | ||
|
|
||
| /// Record a code address the static analysis missed, so it can be fed back | ||
| /// into tc via --entry-points-file. Set THESEUS_MISSING_ADDRS to a file path. |
There was a problem hiding this comment.
I worry this approach will mean it's easy to get into a state where we miss the code pointers from an unlikely branch. Like if there's a vtable with 5 entries but when you run in this mode and you only hit three methods, things will seem ok but then we'll crash when one of the other methods gets hit.
What I've been doing so far is when I hit one of these, I disassemble the source program and try to understand where the code pointer came from, so I can proactively collect all the relevant addresses. This approach doesn't scale well though. :(
I guess I'm trying to say I'm not sure how I feel about this approach, maybe it's fine to merge for now.
| todo!() | ||
| } | ||
|
|
||
| fn full_rect(width: u32, height: u32) -> RECT { |
There was a problem hiding this comment.
These should be methods on RECT I think
| opts.write(true); | ||
| } | ||
| match dwCreationDisposition { | ||
| 1 => { |
There was a problem hiding this comment.
Can this be an enum using derive(ABIEnum) macro?
| lDistanceToMove as i64 | ||
| }; | ||
| let from = match dwMoveMethod { | ||
| 0 => SeekFrom::Start(distance as u64), // FILE_BEGIN |
| let c = c as u8; | ||
| let mut t = 0u16; | ||
| if c.is_ascii_uppercase() { | ||
| t |= 0x1; // C1_UPPER |
|
Also, push_mut is now in stable Rust: https://doc.rust-lang.org/std/vec/struct.Vec.html#method.push_mut |
136259a to
fc68592
Compare
|
No apology needed, and thank you for splitting and merging as much as you did. Rebased onto main; the merged parts dropped out cleanly. Yes, it is LLM-assisted. I worked through this with Claude. I read and tested everything that went in, and I am on the hook for it, but you should know that when you weigh how much to trust the parts you have not read. It also explains the volume and probably the comment style. If that changes how you want to take this, or if you would rather I mark which parts got the least human scrutiny, say so. Split. The three big commits are now 18, one per subsystem, largest 681 lines: They are split by file, so where one file holds two ideas they stayed together: Also removed a Your comments. Fixed:
On On THESEUS_MISSING_ADDRS. Your worry is right and I do not think it can be argued away: it only finds pointers on paths that actually run, so a vtable entry never exercised stays missing until it crashes. It is a net to catch what the static scanning misses, not a substitute for understanding where the pointer came from, and it works because the crash is loud and the fix is one line in a file. If you would rather not carry it, the scanning commits stand on their own without it. I would rather drop it than have it paper over gaps you would otherwise fix properly.
|
|
Went through the whole list of things I had flagged as known-bad, so this is one message rather than a trickle. 30 commits now, still one subsystem each. catch_unwind is gone. Worth reporting the measurement: it fired zero times on this game. Coverage, block count and generated output are identical without it, and 4 blocks still get dropped by your wsprintfA, in the version you merged, had three problems:
All three fixed. wildcard_match was recursive with backtracking, so The rest, one commit each:
What I have not done: Verified on the game after all of this: same coverage, no panics, and the screen renders the same as before the blit change. |
First of all, thank you for this project. I found it through your blog post and it is easily the most fun thing I have read about in a long time. Watching an exe turn into Rust that just runs is quite something. After spending a while in the code I decided the best way to say thanks was to actually pitch in rather than watch from the sidelines, so I picked a game and worked until it ran.
This adds a new target: Pinball Soccer '98, a 1998 Windows pinball game. It gets to actual gameplay, both natively and in the browser.
Per your comment, the generated code is not checked in.
out/winpinis just the crate scaffolding plus atranslate.shrecipe, so it builds aftertranslate.sh winpinif you own a copy of the game, and the diff here is about 6k lines of hand-written code.tc
Most of it already worked. The gaps were all around finding code:
sub ecx, 4; jb; jmp [ecx*4 + table])andmask, so a table whose first slot is padding does not cut the scan shortTHESEUS_MISSING_ADDRS), which you feed back in with--entry-points-fileTogether those take
.textcoverage on this exe from 6% to 93.9% (24114 blocks).Generated output is now split into
part_NN.rsfiles of roughly 1MB, which took a cold build of this target from 60s to 24s.winapi
New files: dinput (keyboard and mouse), the mmio family in winmm, ole32 and msacm32 stubs, and a shared input state in user32 that both dinput and the message pump read from.
Filled in: a software mixer for dsound (resampling, volume, pan), colorkey blits and palette handling in ddraw, LoadLibrary/GetProcAddress backed by a module registry, and a fair amount of kernel32's file and NLS surface.
runtime
indirectgets a 64-entry direct-mapped cache in front of your binary search, which measured 99.7% hits on this game. Numbers are in the comment thread below.Two pieces worth separating
runtime/src/ops/misc.rsfixes a bug that predates this branch:setgetestedZF == OFinstead ofSF == OF. It is buried in b23a8d9 along with a lot of unrelated work, so happy to lift it into its own commit.tc: avoid unstable push_mutis a two-line change that makes tc build on stable. Independent of everything else here.Things I am unsure you will want
catch_unwindaround each instruction in codegen turns an unhandled instruction into atodo!()in the output instead of stopping the build. The aggressive scanning turns up junk blocks that would otherwise be fatal, but the cost is that a genuine mistranslation gets logged rather than failing loudly. See the thread below, I think the honest fix is upstream of codegen.static-server.gogrew/logand/frameendpoints so a script can watch a page it cannot see. Reasonable to want those out, or behind a flag.translate.shhas my local path to the game.Known gaps
I went over this before sending and there is a list of things I know are wrong but have not fixed, nearly all in code this branch adds. The main ones:
wsprintfAdoes not cap output at 1024 the way the real one does,wildcard_matchin kernel32 backtracks exponentially, the wasm filesystem leaks a string per path component, and refcounts on ddraw and dsound objects are fake, so a balanced AddRef/Release frees a live object. I am happy to work through them. I just did not want to keep sitting on the branch without knowing whether you want it at all.State of the game
Intro, language select, table select, and gameplay with working flippers. Sound works in the browser. Under WSL it cuts out after a few seconds, but that is a WSLg bug (microsoft/wslg#1392) rather than anything here.