Compare commits

..
330 Commits
Author SHA1 Message Date
SonicSwordcane ec7a89d9da chore: Release lemur version 0.15.0 2026-08-15 01:00:33 -04:00
SonicSwordcane a6c12a986c Persist watch rom setting 2026-08-15 00:59:35 -04:00
SonicSwordcane 762f43fd42 Support emulating low battery 2026-08-15 00:52:40 -04:00
SonicSwordcane 3a00497942 Fix warning on not-linux 2026-08-15 00:27:03 -04:00
SonicSwordcane 1d4c6c9e88 Support playing roms by dragging into window 2026-08-15 00:18:39 -04:00
SonicSwordcane 4f3dc85bb4 Make extensions case insensitive on linux 2026-08-15 00:11:44 -04:00
SonicSwordcane 644e99c295 Make audio much more defensive 2026-08-14 23:42:02 -04:00
SonicSwordcane 1ff4fda55b chore: Release lemur version 0.14.0 2026-08-12 20:05:12 -04:00
SonicSwordcane 76b7e9d6ec Never attach dialog to window on Windows 2026-08-12 19:59:26 -04:00
SonicSwordcane e15659f6d2 Use sync API on macos 2026-08-12 19:45:59 -04:00
SonicSwordcane a926f19f79 Try only setting the window on macos 2026-08-11 23:23:22 -04:00
SonicSwordcane 8ed1019b44 Use async file picker API 2026-08-11 22:53:31 -04:00
SonicSwordcane ff8353cacc Use recommended hardware sample rate 2026-08-09 17:30:24 -04:00
SonicSwordcane 0a8663a010 Remember power preerence and force fallback 2026-08-08 22:44:00 -04:00
SonicSwordcane 05d00eee5a Improve graphics driver support and debugging
- Support WGPU_FORCE_FALLBACK to run on CPU
 - Pass display when creating wgpu instance
 - Pass compatible surface when constructing adapter
 - Log some settings (at debug level)
2026-08-08 18:38:47 -04:00
SonicSwordcane cfec6e51bf chore: Release lemur version 0.13.1 2026-07-30 19:38:35 -04:00
SonicSwordcane 88721236e7 Show (Player 1) or (Player 2) on new windows too 2026-07-30 19:38:14 -04:00
SonicSwordcane 9824e8f475 Fix "watch rom" feature not actually reloading 2026-07-30 19:33:05 -04:00
SonicSwordcane 5d86ceddd7 chore: Release lemur version 0.13.0 2026-07-16 23:57:36 -04:00
SonicSwordcane d934c843eb Display (Player 1) or (Player 2) consistently 2026-07-16 23:56:20 -04:00
SonicSwordcane b283b69f74 Fix windows growing over time 2026-07-16 23:11:14 -04:00
SonicSwordcane 828acebb89 Fix cpal initialization on windows 2026-07-14 00:13:20 -04:00
SonicSwordcane 466cf29eb7 Update cpal 2026-07-13 23:16:27 -04:00
SonicSwordcane c7628c42d8 Update several dependencies 2026-07-13 23:03:35 -04:00
SonicSwordcane 2ce0edee76 chore: Release lemur version 0.12.4 2026-05-22 00:02:15 -04:00
SonicSwordcane d21cb4e5d0 Fix controller code 2026-05-21 23:45:00 -04:00
SonicSwordcane 6e36254203 Detect diagonals better on controllers 2026-05-21 23:03:15 -04:00
SonicSwordcane 5fd1957d74 Update world inspector too 2026-05-21 21:46:16 -04:00
SonicSwordcane 19c595dd7d Update shrooms-vb-core to address affine bug 2026-05-21 17:27:58 -04:00
SonicSwordcane bb43c9aad8 chore: Release lemur version 0.12.3 2026-04-27 22:03:08 -04:00
SonicSwordcane 9256c513da Update LLVM version 2026-04-27 22:02:33 -04:00
SonicSwordcane 218a1764bc Update deps 2026-04-27 21:21:57 -04:00
SonicSwordcane ea9ca614e9 Explicitly request redraw when needed 2026-04-27 21:17:13 -04:00
SonicSwordcane dd9e04f636 Update to rust 1.95 2026-04-18 11:23:01 -04:00
SonicSwordcane f37ee878e1 chore: Release lemur version 0.12.2 2026-04-12 14:08:44 -04:00
SonicSwordcane bd0a4294e8 Update deps 2026-04-12 14:05:49 -04:00
SonicSwordcane 9934566414 Avoid unnecessary frequent rerenders 2026-04-12 14:01:34 -04:00
SonicSwordcane 5aeb81e800 chore: Release lemur version 0.12.1 2026-04-07 22:33:24 -04:00
SonicSwordcane ec1fe6e0d3 Persist window positions when possible 2026-04-07 22:32:43 -04:00
SonicSwordcane c96368da0e chore: Release lemur version 0.12.0 2026-04-04 23:43:33 -04:00
SonicSwordcane 48c9be0967 Update dependencies once more 2026-04-04 23:42:37 -04:00
SonicSwordcane 3418664184 Persist window sizes as well 2026-04-04 23:25:02 -04:00
SonicSwordcane e75bef634b Persist window state across runs 2026-04-04 23:05:53 -04:00
SonicSwordcane f7b37caccb Rerwite everything to share an egui context 2026-04-02 23:51:01 -04:00
SonicSwordcane 615b0ab9a1 Update viewports to stop overloading root 2026-04-02 23:44:48 -04:00
SonicSwordcane 0d2d6bf863 Fix the object viewer 2026-04-02 23:44:07 -04:00
SonicSwordcane 341de60816 Rewrite VIP tools to not mutate context 2026-03-29 22:37:46 -04:00
SonicSwordcane 8f6a6f86db Use newer rust when building releases 2026-03-29 12:29:52 -04:00
SonicSwordcane 391702b3f3 Update egui 2026-03-29 12:27:23 -04:00
SonicSwordcane 2414489ef2 Update deps, including shrooms-vb-core 2026-03-29 12:26:59 -04:00
SonicSwordcane e222f1bae5 Show version 2026-02-27 00:05:53 -05:00
SonicSwordcane acd7a27c5b chore: Release lemur version 0.11.0 2026-02-20 01:40:40 -05:00
SonicSwordcane faf258bbda Fix broken LLVM install 2026-02-20 01:37:56 -05:00
SonicSwordcane 1b1fa3ced0 Update deps 2026-02-20 00:25:34 -05:00
SonicSwordcane 9dfc942dfc CLI flag to give P2 the controller 2026-02-20 00:02:04 -05:00
SonicSwordcane d269082c8b CLI flag to open player 2 (thx Elrinth) 2026-02-19 23:52:52 -05:00
SonicSwordcane 640078e9b9 chore: Release lemur version 0.10.2 2026-02-05 23:18:05 -05:00
SonicSwordcane 4187b6683e Go easy on the GPU 2026-02-05 23:16:26 -05:00
SonicSwordcane ab86c7abd2 Fix underflow when playing with speedup 2026-02-05 23:06:55 -05:00
SonicSwordcane 25b08d26af Upgrade more deps 2026-02-05 22:59:16 -05:00
SonicSwordcane 3068084b5f Upgrade rubato 2026-01-25 18:15:19 -05:00
SonicSwordcane 18dccb8a64 chore: Release lemur version 0.10.1 2026-01-23 00:13:24 -05:00
SonicSwordcane 6325a90b4f Update dependencies 2026-01-23 00:02:30 -05:00
SonicSwordcane be1133adf7 Fix hotkey detection 2026-01-22 23:15:43 -05:00
SonicSwordcane ac9d3c51d6 chore: Release lemur version 0.10.0 2025-11-23 16:48:08 -05:00
SonicSwordcane 8cf546167f Update all deps 2025-11-23 16:47:36 -05:00
SonicSwordcane 2034f7934b Support manual/automatic ROM reloading 2025-11-23 15:45:57 -05:00
SonicSwordcane 2e474df3e8 Add CLI flags to open windows at startup 2025-11-23 13:58:46 -05:00
SonicSwordcane 0c68cffc0e Use newer brighter core 2025-11-23 13:38:48 -05:00
SonicSwordcane e1d08672fb Remember whether game is muted 2025-11-23 13:37:35 -05:00
SonicSwordcane 8dd64598ed Fix inline stacks 2025-10-25 10:03:57 -04:00
SonicSwordcane c00fae79f8 Fix new warnings 2025-10-17 23:52:56 -04:00
SonicSwordcane 0a8da6bff0 chore: Release lemur version 0.9.2 2025-09-06 22:33:36 -04:00
SonicSwordcane 73e8234b5b Use latest shrooms-vb-core 2025-09-06 22:32:25 -04:00
SonicSwordcane 7b590dde14 Use one file picker filter 2025-09-06 22:25:40 -04:00
SonicSwordcane 73ccc4054a Pass window to file picker 2025-09-06 22:19:20 -04:00
SonicSwordcane efcffed848 chore: Release lemur version 0.9.1 2025-09-06 10:32:02 -04:00
SonicSwordcane 58bb34e3cc Add elf/isx to file picker 2025-09-06 10:22:17 -04:00
SonicSwordcane 132d494cc2 Delete profiles before overwriting 2025-09-06 10:13:09 -04:00
SonicSwordcane 30ee8c06f7 Fix off-by-one errors 2025-09-06 09:57:37 -04:00
SonicSwordcane 1c146590fe chore: Release lemur version 0.9.0 2025-09-06 01:29:42 -04:00
SonicSwordcane d610e3de14 Mask top 5 bits of PC for symbols 2025-09-06 01:29:11 -04:00
SonicSwordcane f7f112f7c1 Support running/profiling ISX files 2025-09-06 01:21:25 -04:00
SonicSwordcane db482b5c21 chore: Release lemur version 0.8.2 2025-09-02 09:29:52 -04:00
SonicSwordcane 3e90d7df6d Fix windows builds 2025-09-02 09:29:24 -04:00
SonicSwordcane 1cab6524a4 chore: Release lemur version 0.8.1 2025-09-02 00:34:16 -04:00
SonicSwordcane cd8ebab4bf Fix parsing vuengine elf files 2025-09-02 00:33:31 -04:00
SonicSwordcane 4f681d883e chore: Release lemur version 0.8.0 2025-09-01 17:52:04 -04:00
SonicSwordcane a00d89bc36 Merge pull request 'Profiling' (#7) from profiling into main
Reviewed-on: #7
2025-09-01 21:51:34 +00:00
SonicSwordcane 6e89a0c988 Improve profiler interface 2025-09-01 17:38:00 -04:00
SonicSwordcane 8f9922473e Support arbitrary markers 2025-09-01 00:07:55 -04:00
SonicSwordcane 841ded3bee Include inline functions when profiling 2025-08-31 23:01:50 -04:00
SonicSwordcane 04c92c1454 Parse ELF exclusively on emulation thread 2025-08-30 13:02:38 -04:00
SonicSwordcane e5abd48337 Update deps 2025-08-27 23:02:04 -04:00
SonicSwordcane 4785e5e11b Profile without ELF data 2025-08-27 22:49:44 -04:00
SonicSwordcane 2936960cc9 Restart recording when restarting game 2025-08-27 22:49:44 -04:00
SonicSwordcane 5e23df4723 Add markers to event stream 2025-08-27 22:49:44 -04:00
SonicSwordcane ed06004a60 Actually produce profiler files 2025-08-27 22:49:44 -04:00
SonicSwordcane ce15d22ab1 UI for recording 2025-08-27 22:49:44 -04:00
SonicSwordcane 8aab90f024 Track HALT 2025-08-27 22:49:44 -04:00
SonicSwordcane 7f819d080f Begin implementing profiling sessions 2025-08-27 22:49:44 -04:00
SonicSwordcane 3bfdcc9366 Start profiler UI/thread 2025-08-27 22:49:44 -04:00
SonicSwordcane 38b34c9cf9 Run elf files 2025-08-27 22:49:44 -04:00
SonicSwordcane 11c17cb246 Start implementing profiling 2025-08-27 22:49:44 -04:00
SonicSwordcane 6272f6cc21 chore: Release lemur version 0.7.3 2025-08-24 22:29:04 -04:00
SonicSwordcane bc6cc6cabc Fix build 2025-08-24 22:28:44 -04:00
SonicSwordcane f732720c31 Update rust version (required many build changes) 2025-08-24 21:02:48 -04:00
SonicSwordcane b988571674 Update shrooms-vb-core with fix for Virtual Lab 2025-08-24 12:17:42 -04:00
SonicSwordcane c52c33ce67 chore: Release lemur version 0.7.2 2025-08-22 23:39:57 -04:00
SonicSwordcane 7ecb248407 Update shrooms-vb-core with emulation fixes 2025-08-22 23:38:55 -04:00
SonicSwordcane fdd71f582b Update deps again 2025-08-22 23:37:36 -04:00
SonicSwordcane 83b567cfd4 Fix NumberEdit taking too much space 2025-08-22 23:37:02 -04:00
SonicSwordcane 6687b1f12f Fix SRAM addressable range 2025-08-07 21:09:16 -04:00
SonicSwordcane af2df78809 Move cart to separate file 2025-08-06 23:31:32 -04:00
SonicSwordcane 89dfeeeb2d Randomize SRAM when first created 2025-08-03 18:42:19 -04:00
SonicSwordcane d4844a303a Merge pull request 'Upgrade deps' (#6) from upgrade-deps into main
Reviewed-on: #6
2025-08-03 21:42:34 +00:00
SonicSwordcane 74ce44db8b Remove cruft 2025-08-03 17:38:06 -04:00
SonicSwordcane 5c0f4c2df4 Update remaining dependencies 2025-08-03 17:33:16 -04:00
SonicSwordcane 110a31870f Update egui/wgpu another major version 2025-08-03 17:29:46 -04:00
SonicSwordcane 43288fc71f Update egui/wgpu one major version 2025-08-03 14:11:33 -04:00
SonicSwordcane 052e3e5c03 Update simple deps 2025-08-03 13:48:25 -04:00
SonicSwordcane dc072cc2ba chore: Release lemur version 0.7.1 2025-07-14 19:34:16 -04:00
SonicSwordcane 3ac13d0cf2 Use sync file dialog for screenshots 2025-07-14 19:33:36 -04:00
SonicSwordcane 422fe23cf2 chore: Release lemur version 0.7.0 2025-07-13 00:46:54 -04:00
SonicSwordcane 065f68e9a8 Appease clippy 2025-07-13 00:46:08 -04:00
SonicSwordcane b6b0a8c22b Support screenshots 2025-07-13 00:45:10 -04:00
SonicSwordcane f1658619ad chore: Release lemur version 0.6.0 2025-05-31 00:55:42 -04:00
SonicSwordcane cf1e8a7c9e Merge pull request 'Terminal' (#5) from terminal into main
Reviewed-on: #5
2025-05-31 04:47:17 +00:00
SonicSwordcane 05081a7662 Only show 1000 lines of history in terminal 2025-05-31 00:37:07 -04:00
SonicSwordcane 68a91c4af1 Allow terminal to be open even if game is not 2025-05-31 00:09:33 -04:00
SonicSwordcane a2a5884a2a Implement fake stdout 2025-05-30 23:52:16 -04:00
SonicSwordcane 2d18aeaba2 Add UI for a terminal 2025-05-30 22:25:08 -04:00
SonicSwordcane 8be866656a chore: Release lemur version 0.5.1 2025-03-29 10:24:43 -04:00
SonicSwordcane b58746fee4 Use target format from render state 2025-03-29 10:23:03 -04:00
SonicSwordcane 57f27559b1 chore: Release lemur version 0.5.0 2025-03-24 23:23:23 -04:00
SonicSwordcane caf3a9426e Add a fast-forward command 2025-03-24 23:22:47 -04:00
SonicSwordcane 4a3385f04c chore: Release lemur version 0.4.8 2025-03-24 21:03:51 -04:00
SonicSwordcane d3f51df577 Remove env var which broke windows builds 2025-03-24 21:02:58 -04:00
SonicSwordcane 4646c20823 chore: Release lemur version 0.4.7 2025-03-20 22:50:56 -04:00
SonicSwordcane f649cf72eb Make scrollbars take up space 2025-03-20 22:49:47 -04:00
SonicSwordcane 427c5ec4a9 Fix file picker on Linux 2025-03-20 22:47:43 -04:00
SonicSwordcane 0043cf8a95 chore: Release lemur version 0.4.6 2025-03-05 00:30:01 -05:00
SonicSwordcane 9a34856c2b Update dependencies and rust version 2025-03-04 20:46:16 -05:00
SonicSwordcane fddc7bdd0e When opening an open window, focus on it 2025-03-04 20:23:45 -05:00
SonicSwordcane d475e806c7 Update core to fire VIP interrupts on INTENB write 2025-03-04 20:19:57 -05:00
SonicSwordcane 316d297c91 Update setup instructions in README 2025-03-04 09:46:26 -05:00
SonicSwordcane 273b3f3409 chore: Release lemur version 0.4.5 2025-03-03 21:29:50 -05:00
SonicSwordcane 3f216f85ae Show extents in the world view 2025-03-03 21:29:07 -05:00
SonicSwordcane 6df198b510 chore: Release lemur version 0.4.4 2025-03-03 00:06:47 -05:00
SonicSwordcane bbffde50ec Add bindable keyboard shortcuts 2025-03-03 00:06:14 -05:00
SonicSwordcane fcfb75fead chore: Release lemur version 0.4.3 2025-03-01 20:59:41 -05:00
SonicSwordcane 7654666a17 Update shrooms-vb-core to fix moar audio 2025-03-01 20:59:01 -05:00
SonicSwordcane 9fc9e4c4d9 chore: Release lemur version 0.4.2 2025-02-25 10:21:36 -05:00
SonicSwordcane 5a5c5d4d9b Update core to fix affine parallax 2025-02-25 10:20:44 -05:00
SonicSwordcane fb6f09fce5 Adjust shades 2025-02-25 10:18:36 -05:00
SonicSwordcane f2d3f5ec07 Implement frame advance 2025-02-25 00:04:07 -05:00
SonicSwordcane e96ce2af07 Fix affine source parallax sign 2025-02-24 22:43:31 -05:00
SonicSwordcane 25c7f29735 fix: respect background palette in world view 2025-02-24 22:02:38 -05:00
SonicSwordcane 54edfbc111 chore: Release lemur version 0.4.1 2025-02-24 15:00:52 -05:00
SonicSwordcane d05c79532f Fix wrong framebuffer getting displayed 2025-02-24 15:00:17 -05:00
SonicSwordcane 1b5fb1a6ed Update core to fix VIP/VSU bugs 2025-02-24 14:58:11 -05:00
SonicSwordcane 59e6ba515d chore: Release lemur version 0.4.0 2025-02-23 23:01:49 -05:00
SonicSwordcane 5c5b6a0b9e Merge pull request 'VIP inspection tooling' (#4) from vram into main
Reviewed-on: #4
2025-02-24 04:01:17 +00:00
SonicSwordcane ad994f8851 Fix typo in macro 2025-02-23 22:59:14 -05:00
SonicSwordcane 49343b6903 Separate VIP tools from CPU 2025-02-23 22:48:30 -05:00
SonicSwordcane 6ab9d2ee53 Rename vram to vip internally 2025-02-23 22:44:11 -05:00
SonicSwordcane 0ae2a2f54f More visual fixes 2025-02-23 22:30:23 -05:00
SonicSwordcane f7408ac9b7 Fix layouts and colors 2025-02-23 21:18:21 -05:00
SonicSwordcane d6957ecd09 Add other miscellanii to register view 2025-02-23 01:02:08 -05:00
SonicSwordcane 4a6288147d Add colors to register view 2025-02-23 00:15:05 -05:00
SonicSwordcane 892d48e321 Add dpctrl+xpctrl to register view 2025-02-19 19:52:31 -05:00
SonicSwordcane a737cd39b7 Add register view with interrupts 2025-02-18 23:59:25 -05:00
SonicSwordcane 2966a7c407 View framebuffers 2025-02-17 23:21:16 -05:00
SonicSwordcane 6142179e31 Avoid unnecessary allocation 2025-02-17 22:32:42 -05:00
SonicSwordcane 2c71c20f20 Edit worlds 2025-02-17 22:19:56 -05:00
SonicSwordcane 802da8f93e View affine worlds 2025-02-17 21:19:34 -05:00
SonicSwordcane 3cdc0583a6 View hbias worlds 2025-02-17 13:52:04 -05:00
SonicSwordcane 2a4599756c View normal worlds 2025-02-17 00:24:18 -05:00
SonicSwordcane cfc08032e6 Implement world viewer with support for OBJ worlds 2025-02-16 15:55:21 -05:00
SonicSwordcane f7cf960b62 Support hypothetical big-endian users 2025-02-15 18:33:18 -05:00
SonicSwordcane b888d1140a Make properties editable 2025-02-15 18:33:18 -05:00
SonicSwordcane 7356287030 Finish object view 2025-02-15 18:33:18 -05:00
SonicSwordcane b5e1711a56 Support signed numbers in number picker 2025-02-15 18:33:18 -05:00
SonicSwordcane 92ccc482ae Fix keyboard navigation for numberedit 2025-02-15 18:33:18 -05:00
SonicSwordcane c4f17bfc13 Start drawing objects 2025-02-15 18:33:18 -05:00
SonicSwordcane dfcfd17bfc Optimize image updates 2025-02-15 18:33:18 -05:00
SonicSwordcane d16c5363da Move about into a button 2025-02-15 18:33:18 -05:00
SonicSwordcane a5676c20d1 Add a number editor widget 2025-02-15 18:33:18 -05:00
SonicSwordcane ba15dc77ae When the UI consumes an input, don't send it to the game 2025-02-15 18:33:18 -05:00
SonicSwordcane ebe444870f Sync memory between emulator thread and UI 2025-02-15 18:33:18 -05:00
SonicSwordcane a461faf89d Render images on background thread to keep UI responsive 2025-02-15 18:33:18 -05:00
SonicSwordcane a82389224f Avoid unnecessary computation/cloning in renderer 2025-02-15 18:33:18 -05:00
SonicSwordcane 600148c781 Rerender VRAM graphics every frame (slow in debug) 2025-02-15 18:33:18 -05:00
SonicSwordcane 4601f1b52f Flesh out bgmap viewer 2025-02-15 18:33:18 -05:00
SonicSwordcane 57eebb5874 New implementation for reading/showing vram 2025-02-15 18:33:17 -05:00
SonicSwordcane 75779f2b24 Dummy second window 2025-02-15 18:29:35 -05:00
SonicSwordcane 6c9265cb78 First pass of character data viewer 2025-02-15 18:29:33 -05:00
SonicSwordcane 5f17469dc2 Fix warning on windows 2025-02-15 18:22:21 -05:00
SonicSwordcane db36332307 chore: Release lemur version 0.3.2 2025-02-15 16:56:20 -05:00
SonicSwordcane 8df05d923d Share one wgpu instance across all windows 2025-02-15 16:55:21 -05:00
SonicSwordcane 0086b9c9ce chore: Release lemur version 0.3.1 2025-02-14 16:57:10 -05:00
SonicSwordcane a87fce102d Avoid egui feature unsupported on wayland 2025-02-14 16:56:36 -05:00
SonicSwordcane 1cae0c8e54 chore: Release lemur version 0.3.0 2025-01-18 19:14:25 -05:00
SonicSwordcane 707dd3a200 Merge pull request 'Implement GDB/LLDB compatible server' (#3) from debugger into main
Reviewed-on: #3
2025-01-19 00:13:22 +00:00
SonicSwordcane 16a2aaee92 Fix status returned on interrupt 2025-01-18 19:10:55 -05:00
SonicSwordcane d59da1009a Fix watchpoint reporting 2025-01-18 18:51:25 -05:00
SonicSwordcane 9d37d22a22 Fix LTO for windows/linux, disable for OSX 2025-01-18 17:01:17 -05:00
SonicSwordcane e1a8006af7 Statically compile that callback too 2025-01-18 13:57:19 -05:00
SonicSwordcane 9f7895a457 Statically compile callbacks 2025-01-18 01:21:45 -05:00
SonicSwordcane 5ce39fb70e Implement writing memory 2025-01-18 01:03:22 -05:00
SonicSwordcane 0fff4d427f Implement writing registers 2025-01-18 00:20:57 -05:00
SonicSwordcane 17d3811124 No debugging unless the game is running 2025-01-17 22:00:46 -05:00
SonicSwordcane b79f35ac61 chore: Release lemur version 0.2.9 2025-01-17 10:49:48 -05:00
SonicSwordcane 89128b8207 Update shrooms-vb-core to fix drawing logic 2025-01-17 10:49:09 -05:00
SonicSwordcane b0404a5da2 chore: Release lemur version 0.2.8 2025-01-15 21:08:42 -05:00
SonicSwordcane 16cf54e5cf Update shrooms-vb-core to fix sign-extended hardware reads 2025-01-15 20:59:43 -05:00
SonicSwordcane f031bb17b2 Use logging 2025-01-14 23:33:30 -05:00
SonicSwordcane 102aff1580 Turn LTO on when cutting releases 2025-01-14 22:25:27 -05:00
SonicSwordcane f1c5571aa6 Tell LLDB to use big watch regions 2025-01-14 21:38:44 -05:00
SonicSwordcane af9a4ae8ee Support watchpoints 2025-01-13 00:30:47 -05:00
SonicSwordcane fd7298d24e Add a checkbox to quit on disconnect 2025-01-09 23:00:46 -05:00
SonicSwordcane d99344eb15 Merge branch 'main' into debugger 2025-01-09 22:50:27 -05:00
SonicSwordcane 423743ea5c chore: Release lemur version 0.2.7 2025-01-07 13:07:11 -05:00
SonicSwordcane 005ed509d5 Stop using Vulkan on Windows 2025-01-07 13:06:37 -05:00
SonicSwordcane 0585bc2740 chore: Release lemur version 0.2.6 2025-01-05 22:11:56 -05:00
SonicSwordcane 62de97384d Push fixes for red alarm 2025-01-05 22:08:37 -05:00
SonicSwordcane fda738fb93 Fix some debug protocol issues 2025-01-05 15:03:59 -05:00
SonicSwordcane 2cf99dbc21 Fix wrapping memory reads for 64-bit addresses 2025-01-05 13:54:53 -05:00
SonicSwordcane 84f2cf7ece Implement stepping 2025-01-05 13:44:59 -05:00
SonicSwordcane 6fd8d8f5cf Improve flow of command-line debugging 2025-01-05 11:24:59 -05:00
SonicSwordcane d6eb8ec7ef Implement execute breakpoints 2025-01-05 00:47:58 -05:00
SonicSwordcane 856ee00999 Fix bugs around debugging lifecycle 2025-01-04 12:17:56 -05:00
SonicSwordcane 11df670ff4 Let emulator tell debugger to stop 2025-01-04 01:04:21 -05:00
SonicSwordcane a5b5f8e80f Use explicit states for sims 2025-01-02 23:06:33 -05:00
SonicSwordcane be8cdd958a Implement reading from real memory 2025-01-02 01:10:19 -05:00
SonicSwordcane 82c3104ab9 Read actual register values 2025-01-01 21:48:33 -05:00
SonicSwordcane 8707a5196b Return register info 2025-01-01 21:04:45 -05:00
SonicSwordcane d016538408 Stub out reading registers and memory 2025-01-01 18:15:41 -05:00
SonicSwordcane 9519897711 Separate GDB request and response into structs 2025-01-01 17:02:14 -05:00
SonicSwordcane 24487b21b7 Support pausing and resuming 2025-01-01 14:56:54 -05:00
SonicSwordcane 2c5084d317 CLI argument to start the server at startup 2025-01-01 13:13:01 -05:00
SonicSwordcane b94ae4d586 Add support for moar commands 2024-12-31 00:08:45 -05:00
SonicSwordcane 014b9ebbfa Handle the kill packet 2024-12-30 23:44:06 -05:00
SonicSwordcane 0a5e223ba7 Handle one message 2024-12-30 23:38:28 -05:00
SonicSwordcane 47a05968fb Create tcp server 2024-12-30 21:55:30 -05:00
SonicSwordcane 1a54c2e6fc Replace read calls with cat so it stops erroring 2024-12-29 13:04:50 -05:00
SonicSwordcane d109c71c15 chore: Release lemur version 0.2.5 2024-12-29 11:51:18 -05:00
SonicSwordcane 1628a45f7d Swallow "errors" from heredocs 2024-12-29 11:50:47 -05:00
SonicSwordcane cebdcfe220 chore: Release lemur version 0.2.4 2024-12-29 11:10:14 -05:00
SonicSwordcane cb05a1e5e6 Make the release script more resilient to new envs 2024-12-29 11:09:46 -05:00
SonicSwordcane 8113b003f7 chore: Release lemur version 0.2.3 2024-12-29 11:00:53 -05:00
SonicSwordcane 2e8b39a74d Update shrooms-vb-core to fix trap handling 2024-12-29 10:55:33 -05:00
SonicSwordcane 1c730b3d77 slight restructuring 2024-12-27 23:37:42 -05:00
SonicSwordcane 249a44e278 chore: Release lemur version 0.2.2 2024-12-26 14:30:06 -05:00
SonicSwordcane 826bc23a41 Update shrooms-vb-core to fix audio timing bugs 2024-12-26 14:29:35 -05:00
SonicSwordcane a13c1e622f Update egui and wgpu 2024-12-25 23:29:27 -05:00
SonicSwordcane becf3517e6 chore: Release lemur version 0.2.1 2024-12-25 14:52:07 -05:00
SonicSwordcane 12a299f622 Update shrooms-vb-core to fix other visual bug 2024-12-25 14:50:24 -05:00
SonicSwordcane 6dbb4b34c5 chore: Release lemur version 0.2.0 2024-12-23 11:46:22 -05:00
SonicSwordcane ba5d1ce8ac Update shrooms-vb-core to fix visual bug 2024-12-23 11:45:42 -05:00
SonicSwordcane 8e7ca9acb6 chore: Release lemur version 0.1.2 2024-12-21 14:18:59 -05:00
SonicSwordcane f4438e32e0 Save crash logs to disk 2024-12-21 14:18:10 -05:00
SonicSwordcane 953e10b42b Add installation instructions to the release 2024-12-19 22:51:13 -05:00
SonicSwordcane 5c97c1b0f2 chore: Release lemur version 0.1.1 2024-12-19 22:36:33 -05:00
SonicSwordcane fe7ffd454f Add license/repository link 2024-12-19 22:33:58 -05:00
SonicSwordcane 06fddc43c3 Create script for releases 2024-12-19 22:31:55 -05:00
SonicSwordcane 3c4b89a8da Remove broken MSI, clean up windows build 2024-12-18 22:49:13 -05:00
SonicSwordcane 78289193c7 Add icons to windows builds 2024-12-18 20:34:39 -05:00
SonicSwordcane bc84d88b5f Oh right, Apple Silicon 2024-12-16 23:24:17 -05:00
SonicSwordcane ba7a2c47b0 Script artifact generation 2024-12-16 00:38:16 -05:00
SonicSwordcane 1b8207db17 Set up cross compilation 2024-12-15 22:54:21 -05:00
SonicSwordcane 99390d52e1 Forgot to add version 2024-12-15 15:52:15 -05:00
SonicSwordcane c694d234e7 About popup 2024-12-15 00:00:22 -05:00
SonicSwordcane e31269368d Save/load more config 2024-12-14 23:14:13 -05:00
SonicSwordcane 6e2d70abd7 Save input mappings to disk 2024-12-11 23:44:14 -05:00
SonicSwordcane ae04f9f73b Surface error notifications 2024-12-09 23:18:42 -05:00
SonicSwordcane 25b88c622c Set runtime icon 2024-12-08 16:00:55 -05:00
SonicSwordcane a4ac7c9d6d Fix typo in sram filenames 2024-12-08 14:53:11 -05:00
SonicSwordcane 7f82daa73d Rename to lemur 2024-12-08 14:49:54 -05:00
SonicSwordcane 0504e351ba Save and load SRAM at all the right times 2024-12-08 14:39:40 -05:00
SonicSwordcane e9ae8bed1b Implement SRAM (only saves on pause) 2024-12-07 00:00:40 -05:00
SonicSwordcane 1bac3dedab Implement color picker 2024-12-03 20:34:13 -05:00
SonicSwordcane 761b434108 Implement side-by-side view 2024-12-01 15:52:11 -05:00
SonicSwordcane 2e5d5e140f Support choosing colors 2024-12-01 00:45:13 -05:00
SonicSwordcane 4e42179ef3 Support choosing display mode 2024-11-30 23:14:01 -05:00
SonicSwordcane 2cef67b129 Turning off vsync makes input way smoother 2024-11-30 21:56:33 -05:00
SonicSwordcane 5627e41835 Merge pull request 'Implement multiplayer' (#2) from multiplayer into main
Reviewed-on: PVB/shrooms-vb-native#2
2024-11-30 00:31:04 +00:00
SonicSwordcane 3eb6b9cd3e Update core for good luck 2024-11-29 19:16:08 -05:00
SonicSwordcane 0c35a1e234 Make it easy to clear/reset mappings 2024-11-29 19:09:00 -05:00
SonicSwordcane ce7ba71ea0 Support rebinding gamepad inputs 2024-11-29 18:54:26 -05:00
SonicSwordcane 24474fabd0 Support choosing which player uses a controller 2024-11-29 14:36:46 -05:00
SonicSwordcane 9ff62af310 Graphical fixes 2024-11-28 12:39:08 -05:00
SonicSwordcane 83377ff5fa Stop using nested viewports 2024-11-28 10:50:06 -05:00
SonicSwordcane 08680b27ae Out with dear imgui, in with egui 2024-11-27 22:20:17 -05:00
SonicSwordcane 4b34d138ac Support muting sims 2024-11-23 20:17:32 -05:00
SonicSwordcane 0f536fdd88 Use CPAL fork which supports device switching 2024-11-23 17:36:24 -05:00
SonicSwordcane 167acbde15 VIP performance improvements 2024-11-20 00:02:31 -05:00
SonicSwordcane f60b10ce1d Implement gamepad input 2024-11-18 22:49:56 -05:00
SonicSwordcane c84531e70e Set process priority to high on windows 2024-11-17 20:57:43 -05:00
SonicSwordcane 63cb7a4835 Update core 2024-11-17 16:27:40 -05:00
SonicSwordcane 09e39b37f5 Let sims link/unlink/reset independently 2024-11-15 21:50:21 -05:00
SonicSwordcane 8f62ec1c52 Update wgpu for faster release builds 2024-11-15 00:23:49 -05:00
SonicSwordcane eef8f834d6 Actually implement multiplayer 2024-11-14 21:54:13 -05:00
SonicSwordcane c23fc6e9df Small performance improvements 2024-11-14 21:03:23 -05:00
SonicSwordcane d306f19297 Write textures in separate thread; fixes perf 2024-11-11 19:27:23 -05:00
SonicSwordcane f22a74b036 Sleep instead of just spin-looping 2024-11-11 18:46:25 -05:00
SonicSwordcane e2c38cd03a Turn off LTO so we can use clang 2024-11-11 18:14:11 -05:00
SonicSwordcane 544990c58f Support running two sims at once 2024-11-11 00:52:02 -05:00
SonicSwordcane 9fcf6b3dc5 Merge pull request 'rewrite it in rust' (#1) from riir into main
Reviewed-on: PVB/shrooms-vb-native#1
2024-11-10 23:34:42 +00:00
SonicSwordcane 99d6970323 Block the emulator thread when idle 2024-11-10 18:19:25 -05:00
SonicSwordcane e8b706df20 Use high-priority thread for emulation 2024-11-10 18:19:25 -05:00
SonicSwordcane 6dc3697baf Fix crash on windows when window is minimized 2024-11-10 15:03:07 -05:00
SonicSwordcane 5cb36d0bcc Functional input binding 2024-11-10 14:05:10 -05:00
SonicSwordcane a69247dd33 Support multiple windows, start on input UI 2024-11-09 18:14:18 -05:00
SonicSwordcane 62b34ea760 Remove c impl, update readme 2024-11-06 23:55:25 -05:00
SonicSwordcane a6200732ee Disable emulation options when game is not running 2024-11-06 23:36:33 -05:00
SonicSwordcane 498e6fbdcc Apply styles 2024-11-06 23:26:31 -05:00
SonicSwordcane 5c5d56cb12 Support full controller input 2024-11-05 00:07:48 -05:00
SonicSwordcane 75fa3be25c Support resizing the window 2024-11-04 23:39:00 -05:00
SonicSwordcane 593475960d Make menu items functional 2024-11-04 22:18:57 -05:00
SonicSwordcane 756835f90e Turn vulkan back on because my linux partition needs it 2024-11-04 21:53:59 -05:00
SonicSwordcane 7c9e9c7fa4 Turn off LTO for now, makes builds quite slow 2024-11-04 10:00:12 -05:00
SonicSwordcane 70373647fb Add audio, fix timing 2024-11-04 09:59:58 -05:00
SonicSwordcane 8bba7b9e1b Turn off vulkan because it's slow on windows 2024-11-03 14:02:25 -05:00
SonicSwordcane a0e39796bf Actually do anaglyph 2024-11-03 13:49:10 -05:00
SonicSwordcane b025f72604 Use one texture with two channels for video 2024-11-03 13:26:01 -05:00
SonicSwordcane 8fdff927eb Fix non-windows compilation 2024-11-03 12:44:38 -05:00
SonicSwordcane 40c4561748 Beginnings of rust emulation 2024-11-03 11:32:53 -05:00
SonicSwordcane 59e14b43e8 Set up a rust UI 2024-11-02 16:18:41 -04:00
SonicSwordcane 3c7915fa53 Update core 2024-10-31 21:40:10 -04:00
82 changed files with 19839 additions and 40792 deletions
+1
View File
@@ -4,6 +4,7 @@
*.html text eol=lf diff=html
*.java text eol=lf diff=java
*.js text eol=lf diff=js
*.sh text eol=lf
*.txt text eol=lf
*.class binary
+3 -2
View File
@@ -1,4 +1,5 @@
/shrooms-vb
/shrooms-vb.exe
/lemur
/lemur.exe
.vscode
output
/target
Generated
+5855
View File
File diff suppressed because it is too large Load Diff
+67
View File
@@ -0,0 +1,67 @@
[package]
name = "lemur"
description = "An emulator for the Virtual Boy."
repository = "https://git.virtual-boy.com/PVB/lemur"
publish = false
license = "MIT"
version = "0.15.0"
edition = "2024"
[dependencies]
anyhow = "1"
atoi = "3"
audioadapter-buffers = "4"
atomic = "0.6"
bitflags = { version = "2", features = ["serde"] }
bytemuck = { version = "1", features = ["derive"] }
clap = { version = "4", features = ["derive"] }
cpal = "0.18"
directories = "6"
egui = { version = "0.34", features = ["persistence", "serde"] }
egui_extras = { version = "0.34", features = ["image"] }
egui-notify = "0.22"
egui-winit = "0.34"
egui-wgpu = { version = "0.34", features = ["winit"] }
fxprof-processed-profile = "0.8"
fixed = { version = "1.28", features = ["num-traits"] }
gilrs = { version = "0.11", features = ["serde-serialize"] }
gimli = "0.34"
hex = "0.4"
image = { version = "0.25", default-features = false, features = ["png"] }
itertools = "0.15"
normpath = "1"
notify = "8"
num-derive = "0.4"
num-traits = "0.2"
object = "0.39"
oneshot = { version = "0.2", features = ["async", "std"] }
pollster = "1"
rand = "0.10"
rfd = "0.17"
rtrb = "0.3"
rubato = "4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thread-priority = "3"
tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync", "time"] }
tracing = { version = "0.1", features = ["release_max_level_debug"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
wgpu = { version = "29", features = ["serde"] }
wholesym = "0.8"
winit = { version = "0.30", features = ["serde"] }
[target.'cfg(windows)'.dependencies]
windows = { version = "0.62", features = ["Win32_System_Threading"] }
[build-dependencies]
cc = "1"
winresource = "0.1"
[profile.release]
lto = true
[package.metadata.bundle]
name = "Lemur"
identifier = "com.virtual-boy.Lemur"
icon = ["assets/lemur-256x256.png"]
category = "games"
+16 -8
View File
@@ -1,19 +1,27 @@
# Shrooms VB (native)
# Lemur
An SDL-based implementation of shrooms-vb.
A Virtual Boy emulator built around the shrooms-vb core. Written in Rust, using winit, wgpu, and egui. Should run on any major OS.
## Setup
Install the following dependencies:
- `gcc` (or MinGW on Windows) (or whatever, just set `CC`)
- `pkg-config`
- sdl2
- `cargo` (via [rustup](https://rustup.rs/), the version from your package manager is too old)
- a C compiler (any will do, [the build script](https://docs.rs/cc/latest/cc/#compile-time-requirements) will find it automatically)
- (on linux) `libasound2-dev` and `libudev-dev`
Run
```sh
make build
cargo build --release
```
## Dependencies
The executable will be in `target/release/lemur[.exe]`
This program uses a vendored version of [nuklear](https://github.com/Immediate-Mode-UI/Nuklear/tree/6566d9075d5fed48af014c93f87c4aed8c4bd21c) for GUIs.
## Release
Bump the version number in `Cargo.toml`, then run this script:
```sh
./scripts/release.sh
```
It uses docker to cross compile for Windows, MacOS, and Linux. All binaries are left in the `output` directory.
-17
View File
@@ -1,17 +0,0 @@
#ifndef SHROOMS_VB_NATIVE_ASSETS_
#define SHROOMS_VB_NATIVE_ASSETS_
#include <stdint.h>
extern const uint8_t _binary_assets_lefteye_bin_start;
const uint8_t *LEFT_EYE_DEFAULT = &_binary_assets_lefteye_bin_start;
extern const uint8_t _binary_assets_righteye_bin_start;
const uint8_t *RIGHT_EYE_DEFAULT = &_binary_assets_righteye_bin_start;
extern const uint8_t _binary_assets_selawk_bin_start;
extern const uint8_t _binary_assets_selawk_bin_end;
const uint8_t *SELAWIK = &_binary_assets_selawk_bin_start;
#define SELAWIK_LEN (&_binary_assets_selawk_bin_end - &_binary_assets_selawk_bin_start)
#endif
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.
-69
View File
@@ -1,69 +0,0 @@
#include <audio.h>
#include <stdio.h>
void audioCallback(void *userdata, uint8_t *stream, int len) {
AudioContext *aud;
SDL_assert(len == 834 * 4);
aud = userdata;
if (!aud->filled) {
/* too little data, play silence */
SDL_memset4(stream, 0, 834);
return;
}
SDL_memcpy4(stream, aud->buffers[aud->current], 834);
++aud->current;
aud->current %= 2;
aud->filled -= 1;
}
int audioInit(AudioContext *aud) {
SDL_AudioSpec spec;
spec.freq = 41700;
spec.format = AUDIO_S16;
spec.channels = 2;
spec.samples = 834;
spec.callback = &audioCallback;
spec.userdata = aud;
aud->id = SDL_OpenAudioDevice(NULL, 0, &spec, NULL, 0);
aud->paused = true;
if (!aud->id) {
fprintf(stderr, "could not open audio device: %s\n", SDL_GetError());
return -1;
}
aud->current = 0;
aud->filled = 0;
return 0;
}
void audioDestroy(AudioContext *aud) {
SDL_CloseAudioDevice(aud->id);
}
int audioUpdate(AudioContext *aud, void *data, uint32_t bytes) {
int filled;
if (!aud->id) return -1;
SDL_assert(bytes == 834 * 4);
SDL_LockAudioDevice(aud->id);
if (aud->filled < 2) {
int next = (aud->current + aud->filled) % 2;
SDL_memcpy4(aud->buffers[next], data, bytes / 4);
aud->filled += 1;
}
filled = aud->filled;
SDL_UnlockAudioDevice(aud->id);
if (aud->paused) {
SDL_PauseAudioDevice(aud->id, false);
aud->paused = false;
}
while (filled > 1) {
SDL_Delay(0);
filled = aud->filled;
}
return 0;
}
-19
View File
@@ -1,19 +0,0 @@
#ifndef SHROOMS_VB_NATIVE_AUDIO_
#define SHROOMS_VB_NATIVE_AUDIO_
#include <SDL2/SDL.h>
#include <stdbool.h>
typedef struct {
SDL_AudioDeviceID id;
bool paused;
uint32_t buffers[2][834];
int current;
int filled;
} AudioContext;
int audioInit(AudioContext *aud);
void audioDestroy(AudioContext *aud);
int audioUpdate(AudioContext *aud, void *data, uint32_t bytes);
#endif
+53
View File
@@ -0,0 +1,53 @@
# This Dockerfile produces a base image for builds.
# It includes all dependencies necessary to cross-compile for Windows/MacOS/Linux.
FROM debian:trixie AS osxcross
ADD --chmod=644 "https://apt.llvm.org/llvm-snapshot.gpg.key" /etc/apt/trusted.gpg.d/apt.llvm.org.asc
WORKDIR /build/osxcross
ADD "https://github.com/tpoechtrager/osxcross.git#47936a512273bb3b414b5a2e83043c92eabc7ae7" .
ADD "https://github.com/joseluisq/macosx-sdks/releases/download/14.5/MacOSX14.5.sdk.tar.xz" ./tarballs/MacOSX14.5.sdk.tar.xz
RUN apt-get update && \
apt-get install -y ca-certificates
COPY llvm.sources /etc/apt/sources.list.d/llvm.sources
RUN apt-get update && \
apt-get install -y bash bzip2 clang-22 git lld-22 llvm-22 make patch xz-utils && \
ln -s $(which clang-22) /usr/bin/clang && \
ln -s $(which clang++-22) /usr/bin/clang++ && \
ln -s $(which ld64.lld-22) /usr/bin/ld64.lld && \
SDK_VERSION=14.5 UNATTENDED=1 ENABLE_COMPILER_RT_INSTALL=1 TARGET_DIR=/osxcross ./build.sh
FROM rust:1.97-trixie
ADD --chmod=644 "https://apt.llvm.org/llvm-snapshot.gpg.key" /etc/apt/trusted.gpg.d/apt.llvm.org.asc
COPY llvm.sources /etc/apt/sources.list.d/llvm.sources
RUN rustup target add x86_64-pc-windows-msvc && \
rustup target add x86_64-apple-darwin && \
rustup target add aarch64-apple-darwin && \
apt-get update && \
apt-get install -y clang-22 lld-22 libc6-dev libasound2-dev libudev-dev genisoimage mingw-w64 && \
cargo install cargo-bundle xwin && \
xwin --accept-license splat --output xwin && \
rm -rf .xwin-cache && \
ln -s $(which clang-22) /usr/bin/clang && \
ln -s $(which clang++-22) /usr/bin/clang++
COPY --from=osxcross /osxcross /osxcross
ENV PATH="/osxcross/bin:$PATH" \
LD_LIBRARY_PATH="/osxcross/lib" \
CC="clang-22" CXX="clang++-22" AR="llvm-ar-22" \
CC_x86_64-apple-darwin="o64-clang" \
CXX_x86_64-apple-darwin="o64-clang++" \
CC_aarch64-apple-darwin="oa64-clang" \
CXX_aarch64-apple-darwin="o6a4-clang++" \
SHROOMS_CFLAGS_x86_64-unknown-linux-gnu="-flto" \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" \
SHROOMS_CFLAGS_x86_64-pc-windows-msvc="-flto" \
CFLAGS_x86_64-pc-windows-msvc="-I/xwin/crt/include -I/xwin/sdk/include/ucrt -I/xwin/sdk/include/um -I/xwin/sdk/include/shared" \
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS="-Lnative=/xwin/crt/lib/x86_64 -Lnative=/xwin/sdk/lib/um/x86_64 -Lnative=/xwin/sdk/lib/ucrt/x86_64 -Clinker-plugin-lto -Clink-arg=-fuse-ld=lld" \
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link-22" \
CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER="o64-clang" \
CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-22" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="oa64-clang" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-22" \
RC_PATH="llvm-rc-22" \
MACOSX_DEPLOYMENT_TARGET="14.5"
ENTRYPOINT ["bash"]
+41
View File
@@ -0,0 +1,41 @@
use std::{error::Error, path::Path};
fn main() -> Result<(), Box<dyn Error>> {
if std::env::var("CARGO_CFG_TARGET_OS")? == "windows" {
let mut res = winresource::WindowsResource::new();
res.set_icon("assets/lemur.ico");
res.compile()?;
}
println!("cargo::rerun-if-changed=shrooms-vb-core");
let mut builder = cc::Build::new();
let _ = builder.try_flags_from_environment("SHROOMS_CFLAGS");
let opt_level = if builder.get_compiler().is_like_msvc() {
2
} else {
3
};
builder
.include(Path::new("shrooms-vb-core/core"))
.include(Path::new("shrooms-vb-core/util"))
.opt_level(opt_level)
.flag_if_supported("-fno-strict-aliasing")
.define("_CRT_SECURE_NO_WARNINGS", None)
.define("VB_LITTLE_ENDIAN", None)
.define("VB_SIGNED_PROPAGATE", None)
.define("VB_DIV_GENERIC", None)
.define("VB_DIRECT_EXCEPTION", "on_exception")
.define("VB_DIRECT_EXECUTE", "on_execute")
.define("VB_DIRECT_FETCH", "on_fetch")
.define("VB_DIRECT_FRAME", "on_frame")
.define("VB_DIRECT_READ", "on_read")
.define("VB_DIRECT_WRITE", "on_write")
.define("VBU_REALLOC", "vbu_realloc_shim")
.file(Path::new("shrooms-vb-core/core/vb.c"))
.file(Path::new("shrooms-vb-core/util/isx.c"))
.file(Path::new("shrooms-vb-core/util/vbu.c"))
.compile("vb");
Ok(())
}
-16
View File
@@ -1,16 +0,0 @@
#include <cli.h>
#include <stdio.h>
int parseCLIArgs(int argc, char **argv, CLIArgs *args) {
int arg;
args->filename = NULL;
for (arg = 1; arg < argc; ++arg) {
if (args->filename) {
fprintf(stderr, "usage: %s /path/to/rom.vb\n", argv[0]);
return 1;
}
args->filename = argv[arg];
}
return 0;
}
-10
View File
@@ -1,10 +0,0 @@
#ifndef SHROOMS_VB_NATIVE_CLI_
#define SHROOMS_VB_NATIVE_CLI_
typedef struct {
char *filename;
} CLIArgs;
int parseCLIArgs(int argc, char **argv, CLIArgs *args);
#endif
-65
View File
@@ -1,65 +0,0 @@
#include <controller.h>
#define VB_PWR 0x0001
#define VB_SGN 0x0002
#define VB_A 0x0004
#define VB_B 0x0008
#define VB_RT 0x0010
#define VB_LT 0x0020
#define VB_RU 0x0040
#define VB_RR 0x0080
#define VB_LR 0x0100
#define VB_LL 0x0200
#define VB_LD 0x0400
#define VB_LU 0x0800
#define VB_STA 0x1000
#define VB_SEL 0x2000
#define VB_RL 0x4000
#define VB_RD 0x8000
static uint16_t symToMask(SDL_KeyCode sym) {
switch (sym) {
default: return 0;
case SDLK_a:
return VB_SEL;
case SDLK_s:
return VB_STA;
case SDLK_d:
return VB_B;
case SDLK_f:
return VB_A;
case SDLK_e:
return VB_LT;
case SDLK_r:
return VB_RT;
case SDLK_i:
return VB_RU;
case SDLK_j:
return VB_RL;
case SDLK_k:
return VB_RD;
case SDLK_l:
return VB_RR;
case SDLK_UP:
return VB_LU;
case SDLK_LEFT:
return VB_LL;
case SDLK_DOWN:
return VB_LD;
case SDLK_RIGHT:
return VB_LR;
}
}
void ctrlInit(ControllerState *ctrl) {
ctrl->keys = VB_SGN;
}
void ctrlKeyDown(ControllerState *ctrl, SDL_Keycode sym) {
ctrl->keys |= symToMask(sym);
}
void ctrlKeyUp(ControllerState *ctrl, SDL_Keycode sym) {
ctrl->keys &= ~symToMask(sym);
}
uint16_t ctrlKeys(ControllerState *ctrl) {
return ctrl->keys;
}
-16
View File
@@ -1,16 +0,0 @@
#ifndef SHROOMS_VB_NATIVE_CONTROLLER_
#define SHROOMS_VB_NATIVE_CONTROLLER_
#include <SDL2/SDL.h>
#include <stdint.h>
typedef struct {
uint16_t keys;
} ControllerState;
void ctrlInit(ControllerState *ctrl);
void ctrlKeyDown(ControllerState *ctrl, SDL_Keycode sym);
void ctrlKeyUp(ControllerState *ctrl, SDL_Keycode sym);
uint16_t ctrlKeys(ControllerState *ctrl);
#endif
-75
View File
@@ -1,75 +0,0 @@
#include "emulation.h"
#include <stdlib.h>
static int onFrame(VB *sim) {
SimContext *ctx = vbGetUserData(sim);
ctx->hasFrame = true;
return 1;
}
int emuInit(EmulationContext *emu) {
emu->sim = malloc(vbSizeOf());
vbInit(emu->sim);
emu->ctx = malloc(sizeof(SimContext));
emu->ctx->hasFrame = false;
emu->ctx->currentSample = 0;
vbSetSamples(emu->sim, emu->ctx->samples[emu->ctx->currentSample], VB_S16, 834);
vbSetUserData(emu->sim, emu->ctx);
vbSetFrameCallback(emu->sim, &onFrame);
return 0;
}
void emuDestroy(EmulationContext *emu) {
uint8_t *rom = vbGetCartROM(emu->sim, NULL);
SimContext *ctx = vbGetUserData(emu->sim);
if (rom) free(rom);
if (ctx) free(ctx);
free(emu->sim);
}
void emuLoadGame(EmulationContext *emu, uint8_t *rom, uint32_t romSize) {
uint8_t *oldRom = vbGetCartROM(emu->sim, NULL);
if (oldRom) free(oldRom);
vbSetCartROM(emu->sim, rom, romSize);
vbReset(emu->sim);
}
void emuReset(EmulationContext *emu) {
vbReset(emu->sim);
}
bool emuIsGameLoaded(EmulationContext *emu) {
return vbGetCartROM(emu->sim, NULL) != NULL;
}
#define MAX_STEP_CLOCKS 20000000
void emuTick(EmulationContext *emu) {
uint32_t clocks = MAX_STEP_CLOCKS;
vbEmulate(emu->sim, &clocks);
}
bool emuReadPixels(EmulationContext *emu, uint8_t *left, uint8_t *right) {
if (!emu->ctx->hasFrame) {
return false;
}
emu->ctx->hasFrame = false;
vbGetPixels(emu->sim, left, 1, 384, right, 1, 384);
return true;
}
void emuReadSamples(EmulationContext *emu, void **data, uint32_t *bytes) {
uint32_t samplePairs;
*data = vbGetSamples(emu->sim, NULL, NULL, &samplePairs);
*bytes = samplePairs * 4;
emu->ctx->currentSample += 1;
emu->ctx->currentSample %= 2;
vbSetSamples(emu->sim, emu->ctx->samples[emu->ctx->currentSample], VB_S16, 834);
}
void emuSetKeys(EmulationContext *emu, uint16_t keys) {
vbSetKeys(emu->sim, keys);
}
-30
View File
@@ -1,30 +0,0 @@
#ifndef SHROOMS_VB_NATIVE_EMULATION_
#define SHROOMS_VB_NATIVE_EMULATION_
#include "shrooms-vb-core/core/vb.h"
#include <stdbool.h>
#include <stdint.h>
typedef struct SimContext {
bool hasFrame;
uint32_t samples[2][834];
uint32_t currentSample;
} SimContext;
typedef struct EmulationContext {
VB *sim;
SimContext *ctx;
} EmulationContext;
int emuInit(EmulationContext *emu);
void emuDestroy(EmulationContext *emu);
void emuLoadGame(EmulationContext *emu, uint8_t *rom, uint32_t romSize);
void emuReset(EmulationContext *emu);
bool emuIsGameLoaded(EmulationContext *emu);
void emuTick(EmulationContext *emu);
bool emuReadPixels(EmulationContext *emu, uint8_t *left, uint8_t *right);
void emuReadSamples(EmulationContext *emu, void **data, uint32_t *bytes);
void emuSetKeys(EmulationContext *emu, uint16_t keys);
#endif
-30906
View File
File diff suppressed because it is too large Load Diff
-393
View File
@@ -1,393 +0,0 @@
/*
* Nuklear - 4.9.4 - public domain
*/
/*
* ==============================================================
*
* API
*
* ===============================================================
*/
#ifndef NK_SDL_RENDERER_H_
#define NK_SDL_RENDERER_H_
#ifndef NK_SDL_RENDERER_SDL_H
#define NK_SDL_RENDERER_SDL_H <SDL.h>
#endif
#include NK_SDL_RENDERER_SDL_H
NK_API struct nk_context* nk_sdl_init(SDL_Window *win, SDL_Renderer *renderer);
NK_API void nk_sdl_font_stash_begin(struct nk_font_atlas **atlas);
NK_API void nk_sdl_font_stash_end(void);
NK_API int nk_sdl_handle_event(SDL_Event *evt);
NK_API void nk_sdl_render(enum nk_anti_aliasing);
NK_API void nk_sdl_shutdown(void);
NK_API void nk_sdl_handle_grab(void);
#if SDL_COMPILEDVERSION < SDL_VERSIONNUM(2, 0, 22)
/* Metal API does not support cliprects with negative coordinates or large
* dimensions. The issue is fixed in SDL2 with version 2.0.22 but until
* that version is released, the NK_SDL_CLAMP_CLIP_RECT flag can be used to
* ensure the cliprect is itself clipped to the viewport.
* See discussion at https://discourse.libsdl.org/t/rendergeometryraw-producing-different-results-in-metal-vs-opengl/34953
*/
#define NK_SDL_CLAMP_CLIP_RECT
#endif
#endif /* NK_SDL_RENDERER_H_ */
/*
* ==============================================================
*
* IMPLEMENTATION
*
* ===============================================================
*/
#ifdef NK_SDL_RENDERER_IMPLEMENTATION
#include <string.h>
#include <stdlib.h>
struct nk_sdl_device {
struct nk_buffer cmds;
struct nk_draw_null_texture tex_null;
SDL_Texture *font_tex;
};
struct nk_sdl_vertex {
float position[2];
float uv[2];
nk_byte col[4];
};
static struct nk_sdl {
SDL_Window *win;
SDL_Renderer *renderer;
struct nk_sdl_device ogl;
struct nk_context ctx;
struct nk_font_atlas atlas;
Uint64 time_of_last_frame;
} sdl;
NK_INTERN void
nk_sdl_device_upload_atlas(const void *image, int width, int height)
{
struct nk_sdl_device *dev = &sdl.ogl;
SDL_Texture *g_SDLFontTexture = SDL_CreateTexture(sdl.renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STATIC, width, height);
if (g_SDLFontTexture == NULL) {
SDL_Log("error creating texture");
return;
}
SDL_UpdateTexture(g_SDLFontTexture, NULL, image, 4 * width);
SDL_SetTextureBlendMode(g_SDLFontTexture, SDL_BLENDMODE_BLEND);
dev->font_tex = g_SDLFontTexture;
}
NK_API void
nk_sdl_render(enum nk_anti_aliasing AA)
{
/* setup global state */
struct nk_sdl_device *dev = &sdl.ogl;
{
SDL_Rect saved_clip;
#ifdef NK_SDL_CLAMP_CLIP_RECT
SDL_Rect viewport;
#endif
SDL_bool clipping_enabled;
int vs = sizeof(struct nk_sdl_vertex);
size_t vp = offsetof(struct nk_sdl_vertex, position);
size_t vt = offsetof(struct nk_sdl_vertex, uv);
size_t vc = offsetof(struct nk_sdl_vertex, col);
/* convert from command queue into draw list and draw to screen */
const struct nk_draw_command *cmd;
const nk_draw_index *offset = NULL;
struct nk_buffer vbuf, ebuf;
/* fill converting configuration */
struct nk_convert_config config;
static const struct nk_draw_vertex_layout_element vertex_layout[] = {
{NK_VERTEX_POSITION, NK_FORMAT_FLOAT, NK_OFFSETOF(struct nk_sdl_vertex, position)},
{NK_VERTEX_TEXCOORD, NK_FORMAT_FLOAT, NK_OFFSETOF(struct nk_sdl_vertex, uv)},
{NK_VERTEX_COLOR, NK_FORMAT_R8G8B8A8, NK_OFFSETOF(struct nk_sdl_vertex, col)},
{NK_VERTEX_LAYOUT_END}
};
Uint64 now = SDL_GetTicks64();
sdl.ctx.delta_time_seconds = (float)(now - sdl.time_of_last_frame) / 1000;
sdl.time_of_last_frame = now;
NK_MEMSET(&config, 0, sizeof(config));
config.vertex_layout = vertex_layout;
config.vertex_size = sizeof(struct nk_sdl_vertex);
config.vertex_alignment = NK_ALIGNOF(struct nk_sdl_vertex);
config.tex_null = dev->tex_null;
config.circle_segment_count = 22;
config.curve_segment_count = 22;
config.arc_segment_count = 22;
config.global_alpha = 1.0f;
config.shape_AA = AA;
config.line_AA = AA;
/* convert shapes into vertexes */
nk_buffer_init_default(&vbuf);
nk_buffer_init_default(&ebuf);
nk_convert(&sdl.ctx, &dev->cmds, &vbuf, &ebuf, &config);
/* iterate over and execute each draw command */
offset = (const nk_draw_index*)nk_buffer_memory_const(&ebuf);
clipping_enabled = SDL_RenderIsClipEnabled(sdl.renderer);
SDL_RenderGetClipRect(sdl.renderer, &saved_clip);
#ifdef NK_SDL_CLAMP_CLIP_RECT
SDL_RenderGetViewport(sdl.renderer, &viewport);
#endif
nk_draw_foreach(cmd, &sdl.ctx, &dev->cmds)
{
if (!cmd->elem_count) continue;
{
SDL_Rect r;
r.x = cmd->clip_rect.x;
r.y = cmd->clip_rect.y;
r.w = cmd->clip_rect.w;
r.h = cmd->clip_rect.h;
#ifdef NK_SDL_CLAMP_CLIP_RECT
if (r.x < 0) {
r.w += r.x;
r.x = 0;
}
if (r.y < 0) {
r.h += r.y;
r.y = 0;
}
if (r.h > viewport.h) {
r.h = viewport.h;
}
if (r.w > viewport.w) {
r.w = viewport.w;
}
#endif
SDL_RenderSetClipRect(sdl.renderer, &r);
}
{
const void *vertices = nk_buffer_memory_const(&vbuf);
SDL_RenderGeometryRaw(sdl.renderer,
(SDL_Texture *)cmd->texture.ptr,
(const float*)((const nk_byte*)vertices + vp), vs,
(const SDL_Color*)((const nk_byte*)vertices + vc), vs,
(const float*)((const nk_byte*)vertices + vt), vs,
(vbuf.needed / vs),
(void *) offset, cmd->elem_count, 2);
offset += cmd->elem_count;
}
}
SDL_RenderSetClipRect(sdl.renderer, &saved_clip);
if (!clipping_enabled) {
SDL_RenderSetClipRect(sdl.renderer, NULL);
}
nk_clear(&sdl.ctx);
nk_buffer_clear(&dev->cmds);
nk_buffer_free(&vbuf);
nk_buffer_free(&ebuf);
}
}
static void
nk_sdl_clipboard_paste(nk_handle usr, struct nk_text_edit *edit)
{
const char *text = SDL_GetClipboardText();
if (text) nk_textedit_paste(edit, text, nk_strlen(text));
(void)usr;
}
static void
nk_sdl_clipboard_copy(nk_handle usr, const char *text, int len)
{
char *str = 0;
(void)usr;
if (!len) return;
str = (char*)malloc((size_t)len+1);
if (!str) return;
memcpy(str, text, (size_t)len);
str[len] = '\0';
SDL_SetClipboardText(str);
free(str);
}
NK_API struct nk_context*
nk_sdl_init(SDL_Window *win, SDL_Renderer *renderer)
{
#ifndef NK_SDL_CLAMP_CLIP_RECT
SDL_RendererInfo info;
SDL_version runtimeVer;
/* warn for cases where NK_SDL_CLAMP_CLIP_RECT should have been set but isn't */
SDL_GetRendererInfo(renderer, &info);
SDL_GetVersion(&runtimeVer);
if (strncmp("metal", info.name, 5) == 0 &&
SDL_VERSIONNUM(runtimeVer.major, runtimeVer.minor, runtimeVer.patch) < SDL_VERSIONNUM(2, 0, 22))
{
SDL_LogWarn(
SDL_LOG_CATEGORY_APPLICATION,
"renderer is using Metal API but runtime SDL version %d.%d.%d is older than compiled version %d.%d.%d, "
"which may cause issues with rendering",
runtimeVer.major, runtimeVer.minor, runtimeVer.patch,
SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL
);
}
#endif
sdl.win = win;
sdl.renderer = renderer;
sdl.time_of_last_frame = SDL_GetTicks64();
nk_init_default(&sdl.ctx, 0);
sdl.ctx.clip.copy = nk_sdl_clipboard_copy;
sdl.ctx.clip.paste = nk_sdl_clipboard_paste;
sdl.ctx.clip.userdata = nk_handle_ptr(0);
nk_buffer_init_default(&sdl.ogl.cmds);
return &sdl.ctx;
}
NK_API void
nk_sdl_font_stash_begin(struct nk_font_atlas **atlas)
{
nk_font_atlas_init_default(&sdl.atlas);
nk_font_atlas_begin(&sdl.atlas);
*atlas = &sdl.atlas;
}
NK_API void
nk_sdl_font_stash_end(void)
{
const void *image; int w, h;
image = nk_font_atlas_bake(&sdl.atlas, &w, &h, NK_FONT_ATLAS_RGBA32);
nk_sdl_device_upload_atlas(image, w, h);
nk_font_atlas_end(&sdl.atlas, nk_handle_ptr(sdl.ogl.font_tex), &sdl.ogl.tex_null);
if (sdl.atlas.default_font)
nk_style_set_font(&sdl.ctx, &sdl.atlas.default_font->handle);
}
NK_API void
nk_sdl_handle_grab(void)
{
struct nk_context *ctx = &sdl.ctx;
if (ctx->input.mouse.grab) {
SDL_SetRelativeMouseMode(SDL_TRUE);
} else if (ctx->input.mouse.ungrab) {
/* better support for older SDL by setting mode first; causes an extra mouse motion event */
SDL_SetRelativeMouseMode(SDL_FALSE);
SDL_WarpMouseInWindow(sdl.win, (int)ctx->input.mouse.prev.x, (int)ctx->input.mouse.prev.y);
} else if (ctx->input.mouse.grabbed) {
ctx->input.mouse.pos.x = ctx->input.mouse.prev.x;
ctx->input.mouse.pos.y = ctx->input.mouse.prev.y;
}
}
NK_API int
nk_sdl_handle_event(SDL_Event *evt)
{
struct nk_context *ctx = &sdl.ctx;
switch(evt->type)
{
case SDL_KEYUP: /* KEYUP & KEYDOWN share same routine */
case SDL_KEYDOWN:
{
int down = evt->type == SDL_KEYDOWN;
const Uint8* state = SDL_GetKeyboardState(0);
switch(evt->key.keysym.sym)
{
case SDLK_RSHIFT: /* RSHIFT & LSHIFT share same routine */
case SDLK_LSHIFT: nk_input_key(ctx, NK_KEY_SHIFT, down); break;
case SDLK_DELETE: nk_input_key(ctx, NK_KEY_DEL, down); break;
case SDLK_RETURN: nk_input_key(ctx, NK_KEY_ENTER, down); break;
case SDLK_TAB: nk_input_key(ctx, NK_KEY_TAB, down); break;
case SDLK_BACKSPACE: nk_input_key(ctx, NK_KEY_BACKSPACE, down); break;
case SDLK_HOME: nk_input_key(ctx, NK_KEY_TEXT_START, down);
nk_input_key(ctx, NK_KEY_SCROLL_START, down); break;
case SDLK_END: nk_input_key(ctx, NK_KEY_TEXT_END, down);
nk_input_key(ctx, NK_KEY_SCROLL_END, down); break;
case SDLK_PAGEDOWN: nk_input_key(ctx, NK_KEY_SCROLL_DOWN, down); break;
case SDLK_PAGEUP: nk_input_key(ctx, NK_KEY_SCROLL_UP, down); break;
case SDLK_z: nk_input_key(ctx, NK_KEY_TEXT_UNDO, down && state[SDL_SCANCODE_LCTRL]); break;
case SDLK_r: nk_input_key(ctx, NK_KEY_TEXT_REDO, down && state[SDL_SCANCODE_LCTRL]); break;
case SDLK_c: nk_input_key(ctx, NK_KEY_COPY, down && state[SDL_SCANCODE_LCTRL]); break;
case SDLK_v: nk_input_key(ctx, NK_KEY_PASTE, down && state[SDL_SCANCODE_LCTRL]); break;
case SDLK_x: nk_input_key(ctx, NK_KEY_CUT, down && state[SDL_SCANCODE_LCTRL]); break;
case SDLK_b: nk_input_key(ctx, NK_KEY_TEXT_LINE_START, down && state[SDL_SCANCODE_LCTRL]); break;
case SDLK_e: nk_input_key(ctx, NK_KEY_TEXT_LINE_END, down && state[SDL_SCANCODE_LCTRL]); break;
case SDLK_UP: nk_input_key(ctx, NK_KEY_UP, down); break;
case SDLK_DOWN: nk_input_key(ctx, NK_KEY_DOWN, down); break;
case SDLK_LEFT:
if (state[SDL_SCANCODE_LCTRL])
nk_input_key(ctx, NK_KEY_TEXT_WORD_LEFT, down);
else nk_input_key(ctx, NK_KEY_LEFT, down);
break;
case SDLK_RIGHT:
if (state[SDL_SCANCODE_LCTRL])
nk_input_key(ctx, NK_KEY_TEXT_WORD_RIGHT, down);
else nk_input_key(ctx, NK_KEY_RIGHT, down);
break;
}
}
return 1;
case SDL_MOUSEBUTTONUP: /* MOUSEBUTTONUP & MOUSEBUTTONDOWN share same routine */
case SDL_MOUSEBUTTONDOWN:
{
int down = evt->type == SDL_MOUSEBUTTONDOWN;
const int x = evt->button.x, y = evt->button.y;
switch(evt->button.button)
{
case SDL_BUTTON_LEFT:
if (evt->button.clicks > 1)
nk_input_button(ctx, NK_BUTTON_DOUBLE, x, y, down);
nk_input_button(ctx, NK_BUTTON_LEFT, x, y, down); break;
case SDL_BUTTON_MIDDLE: nk_input_button(ctx, NK_BUTTON_MIDDLE, x, y, down); break;
case SDL_BUTTON_RIGHT: nk_input_button(ctx, NK_BUTTON_RIGHT, x, y, down); break;
}
}
return 1;
case SDL_MOUSEMOTION:
if (ctx->input.mouse.grabbed) {
int x = (int)ctx->input.mouse.prev.x, y = (int)ctx->input.mouse.prev.y;
nk_input_motion(ctx, x + evt->motion.xrel, y + evt->motion.yrel);
}
else nk_input_motion(ctx, evt->motion.x, evt->motion.y);
return 1;
case SDL_TEXTINPUT:
{
nk_glyph glyph;
memcpy(glyph, evt->text.text, NK_UTF_SIZE);
nk_input_glyph(ctx, glyph);
}
return 1;
case SDL_MOUSEWHEEL:
nk_input_scroll(ctx,nk_vec2((float)evt->wheel.x,(float)evt->wheel.y));
return 1;
}
return 0;
}
NK_API
void nk_sdl_shutdown(void)
{
struct nk_sdl_device *dev = &sdl.ogl;
nk_font_atlas_clear(&sdl.atlas);
nk_free(&sdl.ctx);
SDL_DestroyTexture(dev->font_tex);
/* glDeleteTextures(1, &dev->font_tex); */
nk_buffer_free(&dev->cmds);
memset(&sdl, 0, sizeof(sdl));
}
#endif /* NK_SDL_RENDERER_IMPLEMENTATION */
-8171
View File
File diff suppressed because it is too large Load Diff
-314
View File
@@ -1,314 +0,0 @@
/* SPDX-License-Identifier: Zlib
Copyright (c) 2014 - 2024 Guillaume Vareille http://ysengrin.com
____________________________________________________________________
| |
| 100% compatible C C++ -> You can rename tinfiledialogs.c as .cpp |
|____________________________________________________________________|
********* TINY FILE DIALOGS OFFICIAL WEBSITE IS ON SOURCEFORGE *********
_________
/ \ tinyfiledialogs.h v3.18.2 [Jun 8, 2024]
|tiny file| Unique header file created [November 9, 2014]
| dialogs |
\____ ___/ http://tinyfiledialogs.sourceforge.net
\| git clone http://git.code.sf.net/p/tinyfiledialogs/code tinyfd
____________________________________________
| |
| email: tinyfiledialogs at ysengrin.com |
|____________________________________________|
________________________________________________________________________________
| ____________________________________________________________________________ |
| | | |
| | - in tinyfiledialogs, char is UTF-8 by default (since v3.6) | |
| | | |
| | on windows: | |
| | - for UTF-16, use the wchar_t functions at the bottom of the header file | |
| | | |
| | - _wfopen() requires wchar_t | |
| | - fopen() uses char but expects ASCII or MBCS (not UTF-8) | |
| | - if you want char to be MBCS: set tinyfd_winUtf8 to 0 | |
| | | |
| | - alternatively, tinyfiledialogs provides | |
| | functions to convert between UTF-8, UTF-16 and MBCS | |
| |____________________________________________________________________________| |
|________________________________________________________________________________|
If you like tinyfiledialogs, please upvote my stackoverflow answer
https://stackoverflow.com/a/47651444
- License -
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
__________________________________________
| ______________________________________ |
| | | |
| | DO NOT USE USER INPUT IN THE DIALOGS | |
| |______________________________________| |
|__________________________________________|
*/
#ifndef TINYFILEDIALOGS_H
#define TINYFILEDIALOGS_H
#ifdef __cplusplus
extern "C" {
#endif
/******************************************************************************************************/
/**************************************** UTF-8 on Windows ********************************************/
/******************************************************************************************************/
#ifdef _WIN32
/* On windows, if you want to use UTF-8 ( instead of the UTF-16/wchar_t functions at the end of this file )
Make sure your code is really prepared for UTF-8 (on windows, functions like fopen() expect MBCS and not UTF-8) */
extern int tinyfd_winUtf8; /* on windows char strings can be 1:UTF-8(default) or 0:MBCS */
/* for MBCS change this to 0, in tinyfiledialogs.c or in your code */
/* Here are some functions to help you convert between UTF-16 UTF-8 MBSC */
char * tinyfd_utf8toMbcs(char const * aUtf8string);
char * tinyfd_utf16toMbcs(wchar_t const * aUtf16string);
wchar_t * tinyfd_mbcsTo16(char const * aMbcsString);
char * tinyfd_mbcsTo8(char const * aMbcsString);
wchar_t * tinyfd_utf8to16(char const * aUtf8string);
char * tinyfd_utf16to8(wchar_t const * aUtf16string);
#endif
/******************************************************************************************************/
/******************************************************************************************************/
/******************************************************************************************************/
/************* 3 funtions for C# (you don't need this in C or C++) : */
char const * tinyfd_getGlobalChar(char const * aCharVariableName); /* returns NULL on error */
int tinyfd_getGlobalInt(char const * aIntVariableName); /* returns -1 on error */
int tinyfd_setGlobalInt(char const * aIntVariableName, int aValue); /* returns -1 on error */
/* aCharVariableName: "tinyfd_version" "tinyfd_needs" "tinyfd_response"
aIntVariableName : "tinyfd_verbose" "tinyfd_silent" "tinyfd_allowCursesDialogs"
"tinyfd_forceConsole" "tinyfd_assumeGraphicDisplay" "tinyfd_winUtf8"
**************/
extern char tinyfd_version[8]; /* contains tinyfd current version number */
extern char tinyfd_needs[]; /* info about requirements */
extern int tinyfd_verbose; /* 0 (default) or 1 : on unix, prints the command line calls */
extern int tinyfd_silent; /* 1 (default) or 0 : on unix, hide errors and warnings from called dialogs */
/** Curses dialogs are difficult to use and counter-intuitive.
On windows they are only ascii and still uses the unix backslash ! **/
extern int tinyfd_allowCursesDialogs; /* 0 (default) or 1 */
extern int tinyfd_forceConsole; /* 0 (default) or 1 */
/* for unix & windows: 0 (graphic mode) or 1 (console mode).
0: try to use a graphic solution, if it fails then it uses console mode.
1: forces all dialogs into console mode even when an X server is present.
if enabled, it can use the package Dialog or dialog.exe.
on windows it only make sense for console applications */
extern int tinyfd_assumeGraphicDisplay; /* 0 (default) or 1 */
/* some systems don't set the environment variable DISPLAY even when a graphic display is present.
set this to 1 to tell tinyfiledialogs to assume the existence of a graphic display */
extern char tinyfd_response[1024];
/* if you pass "tinyfd_query" as aTitle,
the functions will not display the dialogs
but will return 0 for console mode, 1 for graphic mode.
tinyfd_response is then filled with the retain solution.
possible values for tinyfd_response are (all lowercase)
for graphic mode:
windows_wchar windows applescript kdialog zenity zenity3 yad matedialog
shellementary qarma python2-tkinter python3-tkinter python-dbus
perl-dbus gxmessage gmessage xmessage xdialog gdialog dunst
for console mode:
dialog whiptail basicinput no_solution */
void tinyfd_beep(void);
int tinyfd_notifyPopup(
char const * aTitle, /* NULL or "" */
char const * aMessage, /* NULL or "" may contain \n \t */
char const * aIconType); /* "info" "warning" "error" */
/* return has only meaning for tinyfd_query */
int tinyfd_messageBox(
char const * aTitle , /* NULL or "" */
char const * aMessage , /* NULL or "" may contain \n \t */
char const * aDialogType , /* "ok" "okcancel" "yesno" "yesnocancel" */
char const * aIconType , /* "info" "warning" "error" "question" */
int aDefaultButton ) ;
/* 0 for cancel/no , 1 for ok/yes , 2 for no in yesnocancel */
char * tinyfd_inputBox(
char const * aTitle , /* NULL or "" */
char const * aMessage , /* NULL or "" (\n and \t have no effect) */
char const * aDefaultInput ) ; /* NULL = passwordBox, "" = inputbox */
/* returns NULL on cancel */
char * tinyfd_saveFileDialog(
char const * aTitle , /* NULL or "" */
char const * aDefaultPathAndOrFile , /* NULL or "" , ends with / to set only a directory */
int aNumOfFilterPatterns , /* 0 (1 in the following example) */
char const * const * aFilterPatterns , /* NULL or char const * lFilterPatterns[1]={"*.txt"} */
char const * aSingleFilterDescription ) ; /* NULL or "text files" */
/* returns NULL on cancel */
char * tinyfd_openFileDialog(
char const * aTitle, /* NULL or "" */
char const * aDefaultPathAndOrFile, /* NULL or "" , ends with / to set only a directory */
int aNumOfFilterPatterns , /* 0 (2 in the following example) */
char const * const * aFilterPatterns, /* NULL or char const * lFilterPatterns[2]={"*.png","*.jpg"}; */
char const * aSingleFilterDescription, /* NULL or "image files" */
int aAllowMultipleSelects ) ; /* 0 or 1 */
/* in case of multiple files, the separator is | */
/* returns NULL on cancel */
char * tinyfd_selectFolderDialog(
char const * aTitle, /* NULL or "" */
char const * aDefaultPath); /* NULL or "" */
/* returns NULL on cancel */
char * tinyfd_colorChooser(
char const * aTitle, /* NULL or "" */
char const * aDefaultHexRGB, /* NULL or "" or "#FF0000" */
unsigned char const aDefaultRGB[3] , /* unsigned char lDefaultRGB[3] = { 0 , 128 , 255 }; */
unsigned char aoResultRGB[3] ) ; /* unsigned char lResultRGB[3]; */
/* aDefaultRGB is used only if aDefaultHexRGB is absent */
/* aDefaultRGB and aoResultRGB can be the same array */
/* returns NULL on cancel */
/* returns the hexcolor as a string "#FF0000" */
/* aoResultRGB also contains the result */
/************ WINDOWS ONLY SECTION ************************/
#ifdef _WIN32
/* windows only - utf-16 version */
int tinyfd_notifyPopupW(
wchar_t const * aTitle, /* NULL or L"" */
wchar_t const * aMessage, /* NULL or L"" may contain \n \t */
wchar_t const * aIconType); /* L"info" L"warning" L"error" */
/* windows only - utf-16 version */
int tinyfd_messageBoxW(
wchar_t const * aTitle, /* NULL or L"" */
wchar_t const * aMessage, /* NULL or L"" may contain \n \t */
wchar_t const * aDialogType, /* L"ok" L"okcancel" L"yesno" */
wchar_t const * aIconType, /* L"info" L"warning" L"error" L"question" */
int aDefaultButton ); /* 0 for cancel/no , 1 for ok/yes */
/* returns 0 for cancel/no , 1 for ok/yes */
/* windows only - utf-16 version */
wchar_t * tinyfd_inputBoxW(
wchar_t const * aTitle, /* NULL or L"" */
wchar_t const * aMessage, /* NULL or L"" (\n nor \t not respected) */
wchar_t const * aDefaultInput); /* NULL passwordBox, L"" inputbox */
/* windows only - utf-16 version */
wchar_t * tinyfd_saveFileDialogW(
wchar_t const * aTitle, /* NULL or L"" */
wchar_t const * aDefaultPathAndOrFile, /* NULL or L"" , ends with / to set only a directory */
int aNumOfFilterPatterns, /* 0 (1 in the following example) */
wchar_t const * const * aFilterPatterns, /* NULL or wchar_t const * lFilterPatterns[1]={L"*.txt"} */
wchar_t const * aSingleFilterDescription); /* NULL or L"text files" */
/* returns NULL on cancel */
/* windows only - utf-16 version */
wchar_t * tinyfd_openFileDialogW(
wchar_t const * aTitle, /* NULL or L"" */
wchar_t const * aDefaultPathAndOrFile, /* NULL or L"" , ends with / to set only a directory */
int aNumOfFilterPatterns , /* 0 (2 in the following example) */
wchar_t const * const * aFilterPatterns, /* NULL or wchar_t const * lFilterPatterns[2]={L"*.png","*.jpg"} */
wchar_t const * aSingleFilterDescription, /* NULL or L"image files" */
int aAllowMultipleSelects ) ; /* 0 or 1 */
/* in case of multiple files, the separator is | */
/* returns NULL on cancel */
/* windows only - utf-16 version */
wchar_t * tinyfd_selectFolderDialogW(
wchar_t const * aTitle, /* NULL or L"" */
wchar_t const * aDefaultPath); /* NULL or L"" */
/* returns NULL on cancel */
/* windows only - utf-16 version */
wchar_t * tinyfd_colorChooserW(
wchar_t const * aTitle, /* NULL or L"" */
wchar_t const * aDefaultHexRGB, /* NULL or L"#FF0000" */
unsigned char const aDefaultRGB[3], /* unsigned char lDefaultRGB[3] = { 0 , 128 , 255 }; */
unsigned char aoResultRGB[3]); /* unsigned char lResultRGB[3]; */
/* returns the hexcolor as a string L"#FF0000" */
/* aoResultRGB also contains the result */
/* aDefaultRGB is used only if aDefaultHexRGB is NULL */
/* aDefaultRGB and aoResultRGB can be the same array */
/* returns NULL on cancel */
#endif /*_WIN32 */
#ifdef __cplusplus
} /*extern "C"*/
#endif
#endif /* TINYFILEDIALOGS_H */
/*
________________________________________________________________________________
| ____________________________________________________________________________ |
| | | |
| | on windows: | |
| | - for UTF-16, use the wchar_t functions at the bottom of the header file | |
| | - _wfopen() requires wchar_t | |
| | | |
| | - in tinyfiledialogs, char is UTF-8 by default (since v3.6) | |
| | - but fopen() expects MBCS (not UTF-8) | |
| | - if you want char to be MBCS: set tinyfd_winUtf8 to 0 | |
| | | |
| | - alternatively, tinyfiledialogs provides | |
| | functions to convert between UTF-8, UTF-16 and MBCS | |
| |____________________________________________________________________________| |
|________________________________________________________________________________|
- This is not for ios nor android (it works in termux though).
- The files can be renamed with extension ".cpp" as the code is 100% compatible C C++
(just comment out << extern "C" >> in the header file)
- Windows is fully supported from XP to 10 (maybe even older versions)
- C# & LUA via dll, see files in the folder EXTRAS
- OSX supported from 10.4 to latest (maybe even older versions)
- Do not use " and ' as the dialogs will be displayed with a warning
instead of the title, message, etc...
- There's one file filter only, it may contain several patterns.
- If no filter description is provided,
the list of patterns will become the description.
- On windows link against Comdlg32.lib and Ole32.lib
(on windows the no linking claim is a lie)
- On unix: it tries command line calls, so no such need (NO LINKING).
- On unix you need one of the following:
applescript, kdialog, zenity, matedialog, shellementary, qarma, yad,
python (2 or 3)/tkinter/python-dbus (optional), Xdialog
or curses dialogs (opens terminal if running without console).
- One of those is already included on most (if not all) desktops.
- In the absence of those it will use gdialog, gxmessage or whiptail
with a textinputbox. If nothing is found, it switches to basic console input,
it opens a console if needed (requires xterm + bash).
- for curses dialogs you must set tinyfd_allowCursesDialogs=1
- You can query the type of dialog that will be used (pass "tinyfd_query" as aTitle)
- String memory is preallocated statically for all the returned values.
- File and path names are tested before return, they should be valid.
- tinyfd_forceConsole=1; at run time, forces dialogs into console mode.
- On windows, console mode only make sense for console applications.
- On windows, console mode is not implemented for wchar_T UTF-16.
- Mutiple selects are not possible in console mode.
- The package dialog must be installed to run in curses dialogs in console mode.
It is already installed on most unix systems.
- On osx, the package dialog can be installed via
http://macappstore.org/dialog or http://macports.org
- On windows, for curses dialogs console mode,
dialog.exe should be copied somewhere on your executable path.
It can be found at the bottom of the following page:
http://andrear.altervista.org/home/cdialog.php
*/
+4
View File
@@ -0,0 +1,4 @@
Types: deb deb-src
URIs: http://apt.llvm.org/trixie/
Suites: llvm-toolchain-trixie-22
Components: main
-41
View File
@@ -1,41 +0,0 @@
#include "cli.h"
#include <SDL2/SDL.h>
#include <stdbool.h>
#include <stdio.h>
#include "ui.h"
int main(int argc, char **argv) {
CLIArgs args;
UIContext *ui;
int status;
bool running = false;
if (parseCLIArgs(argc, argv, &args)) {
return 1;
}
SDL_SetHint(SDL_HINT_VIDEO_HIGHDPI_DISABLED, "0");
SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "system");
if (SDL_Init(SDL_INIT_EVERYTHING)) {
fprintf(stderr, "Error initializing SDL: %s\n", SDL_GetError());
return 1;
}
ui = uiInit();
if (!ui) {
SDL_Quit();
return 1;
}
if (args.filename) {
if (uiLoadGame(ui, args.filename)) {
return 1;
}
running = true;
}
status = uiRun(ui, running);
uiDestroy(ui);
SDL_Quit();
return status;
}
-54
View File
@@ -1,54 +0,0 @@
CC?=gcc
LD?=ld
msys_version := $(if $(findstring Msys, $(shell uname -o)),$(word 1, $(subst ., ,$(shell uname -r))),0)
ifeq ($(msys_version), 0)
PKGFLAGS=$(shell pkg-config sdl2 --cflags --libs)
BINLINKFLAGS=-z noexecstack
else
PKGFLAGS=$(shell pkg-config sdl2 --cflags --libs) -mwindows -mconsole -lcomdlg32 -lole32
BINLINKFLAGS=
endif
.PHONY: clean build
clean:
@rm -rf shrooms-vb output
CFILES := $(foreach dir,./,$(notdir $(wildcard $(dir)/*.c)))
BINFILES := $(foreach dir,assets/,$(notdir $(wildcard $(dir)/*.bin)))
COBJS := $(CFILES:%.c=output/%.o)
EXTOBJS = output/vb.o output/tinyfiledialogs.o
BINOBJS := $(BINFILES:%.bin=output/%.o)
OFILES := $(COBJS) $(EXTOBJS) $(BINOBJS)
output/%.o: %.c
@mkdir -p output
@$(CC) -c -o $@ $< -I . \
-I shrooms-vb-core/core $(PKGFLAGS) \
-O3 -flto=auto -Wno-long-long \
-Werror -std=c90 -Wall -Wextra -Wpedantic
output/vb.o: shrooms-vb-core/core/vb.c
@mkdir -p output
@$(CC) -c -o $@ $< -I . \
-I shrooms-vb-core/core \
-O3 -flto=auto -fno-strict-aliasing \
-Werror -std=c90 -Wall -Wextra -Wpedantic
output/tinyfiledialogs.o: external/tinyfiledialogs.c
@mkdir -p output
@$(CC) -c -o $@ $< -I . \
-I external \
-O3 -flto=auto -fno-strict-aliasing -Wno-cast-function-type \
-Werror -std=c90 -Wall -Wextra -Wpedantic
output/%.o: assets/%.bin
@mkdir -p output
@$(LD) -r -b binary $(BINLINKFLAGS) -o $@ $<
shrooms-vb: $(OFILES)
@$(CC) -o $@ $(OFILES) $(PKGFLAGS) -lm -flto=auto
build: shrooms-vb
-3
View File
@@ -1,3 +0,0 @@
#define NK_IMPLEMENTATION
#define NK_SDL_RENDERER_IMPLEMENTATION
#include "nuklear.h"
-17
View File
@@ -1,17 +0,0 @@
#ifndef SHROOMS_VB_NATIVE_NUKLEAR_
#define SHROOMS_VB_NATIVE_NUKLEAR_
#define NK_INCLUDE_FIXED_TYPES
#define NK_INCLUDE_STANDARD_BOOL
#define NK_INCLUDE_STANDARD_IO
#define NK_INCLUDE_STANDARD_VARARGS
#define NK_INCLUDE_DEFAULT_ALLOCATOR
#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
#define NK_INCLUDE_FONT_BAKING
#define NK_INCLUDE_DEFAULT_FONT
#include "external/nuklear.h"
#define NK_SDL_RENDERER_SDL_H <SDL2/SDL.h>
#include "external/nuklear_sdl_renderer.h"
#endif
+4
View File
@@ -0,0 +1,4 @@
set -e
docker build --pull -f build.Dockerfile -t lemur-build .
MSYS_NO_PATHCONV=1 docker run -it --rm -v .:/app -w /app lemur-build /app/scripts/do-bundle.sh
+35
View File
@@ -0,0 +1,35 @@
# Set everything up
rm -rf output
mkdir -p output
cargo clean
# Build for linux
cargo build --release
cp target/release/lemur output/lemur-linux
# Bundle for Linux
cargo bundle --release --format deb
cp target/release/bundle/deb/*.deb output
# Build for Windows
cargo build --release --target x86_64-pc-windows-msvc
cp target/x86_64-pc-windows-msvc/release/lemur.exe output
# Build for MacOS Intel
cargo build --release --target x86_64-apple-darwin
cp target/x86_64-apple-darwin/release/lemur output/lemur-osx-intel
# Bundle for MacOS Intel
cargo bundle --release --target x86_64-apple-darwin --format osx
genisoimage -V lemur -D -R -apple -no-pad -o output/Lemur-Intel.dmg target/x86_64-apple-darwin/release/bundle/osx
# Build for MacOS Apple Silicon
cargo build --release --target aarch64-apple-darwin
cp target/aarch64-apple-darwin/release/lemur output/lemur-osx-apple-silicon
# Bundle for MacOS Apple Silicon
cargo bundle --release --target aarch64-apple-darwin --format osx
genisoimage -V lemur -D -R -apple -no-pad -o output/Lemur-Apple-Silicon.dmg target/aarch64-apple-darwin/release/bundle/osx
# Clean up after ourselves
cargo clean
+71
View File
@@ -0,0 +1,71 @@
set -e
version=$(cat Cargo.toml | sed -n -e '/version/ {s/.* = *//p;q}' | tr -d '"')
read -p "You wanted to release $version, right? [Y/n] " -n 1 -r
echo
case "$REPLY" in
n|N ) exit 1;;
esac
if [ -z "${RELEASE_TOKEN}" ]; then
echo "Please set the RELEASE_TOKEN env var."
exit 1
fi
if ! command -v curl 2>&1 >/dev/null; then
echo "Please install curl."
exit 1
fi
if ! command -v jq 2>&1 >/dev/null; then
echo "Please install jq."
exit 1
fi
if ! command -v docker 2>&1 >/dev/null; then
echo "Please install docker."
exit 1
fi
./scripts/bundle.sh
body=$(cat <<EOF
## How to install
The emulator can be found in the "Downloads" section of this release.
### Windows users
Download \`lemur.exe\`.
### MacOS users
If your Mac uses an Intel processor, download and install \`Lemur-Intel.dmg\`.
If it uses Apple Silicon, download and install \`Lemur-Apple-Silicon.dmg\`.
If you're not sure which to choose, use [this guide](https://support.apple.com/en-us/116943) to find out.
### Linux users
You can either download and run \`lemur-linux\`, or download and install the attached .deb file.
EOF
)
payload=$(cat <<EOF
{
"body": $(echo "$body" | jq -Rsa .),
"draft": false,
"name": "v${version}",
"prerelease": false,
"tag_name": "v${version}"
}
EOF
)
echo "Creating release..."
response=$(curl -s --json "$payload" "https://git.virtual-boy.com/api/v1/repos/PVB/lemur/releases?token=$RELEASE_TOKEN")
echo "$response"
upload_url=$(echo "$response" | jq -r '.upload_url')
for file in output/*; do
echo "Uploading $(basename "$file")..."
upload_res=$(curl -s -F "attachment=@$file" "$upload_url?name=$(basename "$file")&token=$RELEASE_TOKEN")
echo "$upload_res"
done
+706
View File
@@ -0,0 +1,706 @@
use std::{
collections::hash_map::Entry,
num::NonZero,
sync::Arc,
thread,
time::{Duration, Instant},
};
use egui::{
Context, FontData, FontDefinitions, FontFamily, IconData, PlatformOutput, RawInput,
TextWrapMode, ViewportBuilder, ViewportCommand, ViewportEvent, ViewportId, ViewportIdMap,
ViewportIdSet, ViewportInfo, style::ScrollStyle,
};
use egui_wgpu::winit::Painter;
use egui_winit::EventResponse;
use gilrs::{EventType, Gilrs};
use tracing::{debug, error, warn};
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy, OwnedDisplayHandle},
window::{Window, WindowId},
};
use crate::{
config::{AppConfig, CliArgs},
controller::ControllerManager,
emulator::{EmulatorClient, EmulatorCommand, SimId},
images::ImageTextureLoader,
input::{MappingProvider, ShortcutProvider},
memory::MemoryClient,
persistence::Persistence,
window::{AppWindow, ChildWindow, GameScreen, GameWindow, InitArgs},
};
const EGUI_FILENAME: &str = "egui";
fn load_icon() -> anyhow::Result<IconData> {
let bytes = include_bytes!("../assets/lemur-256x256.png");
let img = image::load_from_memory_with_format(bytes, image::ImageFormat::Png)?;
let rgba = img.into_rgba8();
Ok(IconData {
width: rgba.width(),
height: rgba.height(),
rgba: rgba.into_vec(),
})
}
struct SharedViewportState {
viewport_info: ViewportIdMap<ViewportInfo>,
wgpu: WgpuState,
painter: Option<Painter>,
resized_viewport: Option<ViewportId>,
}
impl SharedViewportState {
fn init_painter(&mut self, ctx: &Context, window: &Window) -> &mut Painter {
if self.painter.is_none() {
let wgpu_config = egui_wgpu::WgpuConfiguration {
present_mode: wgpu::PresentMode::AutoVsync,
wgpu_setup: egui_wgpu::WgpuSetup::Existing(self.wgpu.init(window)),
..egui_wgpu::WgpuConfiguration::default()
};
let options = egui_wgpu::RendererOptions::default();
let painter =
pollster::block_on(Painter::new(ctx.clone(), wgpu_config, false, options));
self.painter = Some(painter);
}
self.painter.as_mut().unwrap()
}
fn get_painter(&mut self) -> &mut Painter {
self.painter.as_mut().unwrap()
}
}
pub struct Application {
client: EmulatorClient,
persistence: Persistence,
ctx: Context,
shared: SharedViewportState,
icon: Option<Arc<IconData>>,
app: GameWindow,
controllers: ControllerManager,
viewports: ViewportIdMap<ViewportManager>,
focused: Option<ViewportId>,
redraw_times: ViewportIdMap<Instant>,
initial_windows: Vec<ChildWindow>,
}
impl Application {
pub fn new(
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
handle: OwnedDisplayHandle,
persistence: Persistence,
args: CliArgs,
config: AppConfig,
) -> Self {
let wgpu = WgpuState::new(handle, config);
let icon = load_icon().ok().map(Arc::new);
let mappings = MappingProvider::new(persistence.clone(), args.player2_controller);
let shortcuts = ShortcutProvider::new(persistence.clone());
let controllers = ControllerManager::new(client.clone(), &mappings);
let memory = Arc::new(MemoryClient::new(client.clone()));
let images = Arc::new(ImageTextureLoader::new());
{
let mappings = mappings.clone();
let proxy = proxy.clone();
thread::spawn(|| process_gamepad_input(mappings, proxy));
}
let app = GameWindow::new(
ViewportId::ROOT,
client.clone(),
proxy.clone(),
persistence.clone(),
shortcuts.clone(),
&memory,
&images,
mappings.clone(),
SimId::Player1,
);
let ctx = Context::default();
let data = persistence.load_config(EGUI_FILENAME).unwrap_or_default();
ctx.data_mut(|d| *d = data);
let mut fonts = FontDefinitions::default();
fonts.font_data.insert(
"Selawik".into(),
Arc::new(FontData::from_static(include_bytes!(
"../assets/selawik.ttf"
))),
);
fonts
.families
.get_mut(&FontFamily::Proportional)
.unwrap()
.insert(0, "Selawik".into());
ctx.set_fonts(fonts);
ctx.global_style_mut(|s| {
s.wrap_mode = Some(TextWrapMode::Extend);
s.visuals.menu_corner_radius = Default::default();
s.spacing.scroll = ScrollStyle::thin();
});
egui_extras::install_image_loaders(&ctx);
ctx.add_texture_loader(images.clone());
ctx.set_embed_viewports(false);
{
let proxy = proxy.clone();
ctx.set_request_repaint_callback(move |info| {
let _ = proxy.send_event(UserEvent::RequestRedraw(
info.viewport_id,
Instant::now() + info.delay,
));
});
}
let mut initial_windows = vec![];
if let Some(port) = args.debug_port {
initial_windows.push(ChildWindow::Debugger { port: Some(port) });
}
if args.profile {
initial_windows.push(ChildWindow::Profiler { launch: true });
}
if args.character_data {
initial_windows.push(ChildWindow::CharacterData);
}
if args.bgmap_data {
initial_windows.push(ChildWindow::BgMap);
}
if args.object_data {
initial_windows.push(ChildWindow::Objects);
}
if args.worlds {
initial_windows.push(ChildWindow::Worlds);
}
if args.frame_buffers {
initial_windows.push(ChildWindow::FrameBuffers);
}
if args.registers {
initial_windows.push(ChildWindow::Registers);
}
if args.terminal {
initial_windows.push(ChildWindow::Terminal);
}
if args.player2 {
client.send_command(EmulatorCommand::StartSecondSim(args.rom.clone()));
initial_windows.push(ChildWindow::Player2);
}
Self {
client,
persistence,
ctx,
shared: SharedViewportState {
viewport_info: ViewportIdMap::default(),
wgpu,
painter: None,
resized_viewport: None,
},
icon,
app,
controllers,
viewports: ViewportIdMap::default(),
focused: None,
redraw_times: ViewportIdMap::default(),
initial_windows,
}
}
fn check_repaint(&mut self, event_loop: &ActiveEventLoop) {
let now = Instant::now();
self.redraw_times.retain(|viewport_id, time| {
if *time > now {
return true;
}
if let Some(viewport) = self.viewports.get(viewport_id) {
viewport.window.request_redraw();
}
false
});
if let Some(next_repaint_time) = self.redraw_times.values().min().copied() {
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
}
}
fn repaint_all(&mut self, event_loop: &ActiveEventLoop) {
enum ViewportUpdate {
Keep {
recreate: bool,
builder: ViewportBuilder,
parent: ViewportId,
commands: Vec<ViewportCommand>,
},
Remove,
}
let mut updates = ViewportIdMap::<ViewportUpdate>::default();
for viewport in self.viewports.values() {
updates.insert(
viewport.id,
ViewportUpdate::Keep {
recreate: false,
builder: viewport.builder.clone(),
parent: self
.shared
.viewport_info
.get(&viewport.id)
.and_then(|vp| vp.parent)
.unwrap_or(ViewportId::ROOT),
commands: vec![],
},
);
}
for viewport in self.viewports.values_mut() {
let mut input = viewport.take_egui_input();
if !self.shared.viewport_info.contains_key(&viewport.id) {
continue;
}
input.viewports = self.shared.viewport_info.clone();
let output = self.ctx.run_ui(input, |ui| {
if viewport.id == ViewportId::ROOT {
self.app.show(ui);
} else if let Some(cb) = ui.viewport(|v| v.viewport_ui_cb.clone()) {
cb(ui);
}
});
viewport.handle_platform_output(output.platform_output);
for (id, update) in updates.iter_mut() {
if !output.viewport_output.contains_key(id) {
self.shared.viewport_info.remove(id);
*update = ViewportUpdate::Remove;
}
}
for (id, mut vp) in output.viewport_output {
match updates.entry(id) {
Entry::Vacant(e) => {
e.insert(ViewportUpdate::Keep {
recreate: true,
builder: vp.builder,
parent: vp.parent,
commands: vp.commands,
});
}
Entry::Occupied(e) => {
let ViewportUpdate::Keep {
recreate,
builder,
commands,
..
} = e.into_mut()
else {
continue;
};
let (_, should_recreate) = builder.patch(vp.builder);
*recreate |= should_recreate;
commands.append(&mut vp.commands);
}
}
}
let clipped_primitives = self.ctx.tessellate(output.shapes, output.pixels_per_point);
viewport.window.pre_present_notify();
self.shared.get_painter().paint_and_update_textures(
viewport.id,
output.pixels_per_point,
[0.0, 0.0, 0.0, 0.0],
&clipped_primitives,
&output.textures_delta,
vec![],
);
}
let mut active_viewports = ViewportIdSet::default();
for (id, update) in updates {
match update {
ViewportUpdate::Keep {
recreate,
builder,
parent,
commands,
} => {
active_viewports.insert(id);
let manager = self
.viewports
.remove(&id)
.filter(|_| !recreate)
.unwrap_or_else(|| {
let manager = ViewportManager::new(
&self.ctx,
id,
parent,
event_loop,
builder,
&mut self.shared,
);
self.app.handle_init(
id,
InitArgs {
window: &manager.window,
render_state: &self
.shared
.get_painter()
.render_state()
.unwrap(),
},
);
manager
});
manager.process_viewport_commands(commands, &mut self.shared);
self.viewports.insert(id, manager);
}
ViewportUpdate::Remove => {
self.viewports.remove(&id);
}
}
}
if !active_viewports.contains(&ViewportId::ROOT) {
event_loop.exit();
}
if let Some(painter) = self.shared.painter.as_mut() {
painter.gc_viewports(&active_viewports);
}
}
}
impl ApplicationHandler<UserEvent> for Application {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let mut viewport_builder = self.app.initial_viewport();
if let Some(icon) = &self.icon {
viewport_builder = viewport_builder.with_icon(icon.clone());
}
let manager = ViewportManager::new(
&self.ctx,
ViewportId::ROOT,
ViewportId::ROOT,
event_loop,
viewport_builder,
&mut self.shared,
);
let render_state = self.shared.get_painter().render_state().unwrap();
GameScreen::init_pipeline(&render_state);
self.app.handle_init(
ViewportId::ROOT,
InitArgs {
window: &manager.window,
render_state: &render_state,
},
);
self.viewports.insert(ViewportId::ROOT, manager);
for window in std::mem::take(&mut self.initial_windows) {
self.app.open(window);
}
}
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
if let Some(viewport) = self.viewports.get(&ViewportId::ROOT) {
viewport.window.request_redraw();
}
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
let Some(viewport) = self
.viewports
.values_mut()
.find(|v| v.has_window_id(window_id))
else {
return;
};
let viewport_id = viewport.id;
let (response, close_requested) = viewport.on_window_event(&event, &mut self.shared);
if close_requested && viewport_id == ViewportId::ROOT {
event_loop.exit();
}
if response.repaint {
viewport.window.request_redraw();
}
if !response.consumed {
match event {
WindowEvent::KeyboardInput { event, .. }
if !self.app.handle_key_event(viewport_id, &event) =>
{
self.controllers.handle_key_event(&event);
}
WindowEvent::DroppedFile(path) => {
self.app.handle_dropped_file(viewport_id, path);
}
WindowEvent::Focused(new_focused) => {
self.focused = new_focused.then_some(viewport_id);
}
WindowEvent::RedrawRequested => {
self.repaint_all(event_loop);
}
_ => {}
}
}
self.check_repaint(event_loop);
}
fn device_event(
&mut self,
_event_loop: &ActiveEventLoop,
_device_id: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) {
if let winit::event::DeviceEvent::MouseMotion { delta } = event {
let Some(viewport) = self
.focused
.as_ref()
.and_then(|id| self.viewports.get_mut(id))
else {
return;
};
viewport.on_mouse_motion(delta);
}
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
match event {
UserEvent::GamepadEvent(event) => {
if !self
.focused
.is_some_and(|id| self.app.handle_gamepad_event(id, &event))
{
self.controllers.handle_gamepad_event(&event);
}
}
UserEvent::LowBattery(sim_id, pwr) => {
self.controllers.handle_low_battery(sim_id, pwr);
}
UserEvent::Quit(sim_id) => match sim_id {
SimId::Player1 => event_loop.exit(),
SimId::Player2 => self.app.close(ChildWindow::Player2),
},
UserEvent::RequestRedraw(viewport, when) => {
let scheduled = self.redraw_times.entry(viewport).or_insert(when);
if *scheduled > when {
*scheduled = when;
}
}
}
self.check_repaint(event_loop);
}
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
if let Err(error) = self
.ctx
.data(|d| self.persistence.save_config(EGUI_FILENAME, d))
{
error!(%error, "could not save egui state.");
}
let (sender, receiver) = oneshot::channel();
if self.client.send_command(EmulatorCommand::Exit(sender))
&& let Err(error) = receiver.recv_timeout(Duration::from_secs(5))
{
error!(%error, "could not gracefully exit.");
}
}
}
struct WgpuState {
instance: wgpu::Instance,
device_descriptor: Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
config: AppConfig,
}
impl WgpuState {
fn new(handle: OwnedDisplayHandle, config: AppConfig) -> Self {
#[allow(unused_variables)]
let egui_wgpu::WgpuSetupCreateNew {
instance_descriptor: wgpu::InstanceDescriptor { backends, .. },
device_descriptor,
..
} = egui_wgpu::WgpuSetupCreateNew::without_display_handle();
#[cfg(windows)]
let backends = wgpu::Backends::from_env()
.unwrap_or((wgpu::Backends::PRIMARY | wgpu::Backends::GL) - wgpu::Backends::VULKAN);
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends,
..wgpu::InstanceDescriptor::new_with_display_handle_from_env(Box::new(handle))
});
Self {
instance,
device_descriptor,
config,
}
}
fn init(&mut self, window: &Window) -> egui_wgpu::WgpuSetupExisting {
let power_preference =
wgpu::PowerPreference::from_env().unwrap_or(self.config.power_preference);
debug!("power preference: {power_preference:?}");
let force_fallback_adapter = std::env::var("WGPU_FORCE_FALLBACK")
.map_or(self.config.force_fallback, |f| {
f.eq_ignore_ascii_case("true") || f.eq_ignore_ascii_case("1")
});
debug!("force fallback: {force_fallback_adapter}");
let surface = self
.instance
.create_surface(window)
.expect("could not create surface");
let adapter =
pollster::block_on(self.instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference,
compatible_surface: Some(&surface),
force_fallback_adapter,
}))
.expect("could not create adapter");
debug!("selected adapter: {:?}", adapter.get_info());
drop(surface);
let (device, queue) =
pollster::block_on(adapter.request_device(&(*self.device_descriptor)(&adapter)))
.expect("could not request device");
egui_wgpu::WgpuSetupExisting {
instance: self.instance.clone(),
adapter,
device,
queue,
}
}
}
struct ViewportManager {
id: ViewportId,
window: Arc<Window>,
state: egui_winit::State,
builder: ViewportBuilder,
}
impl ViewportManager {
fn new(
ctx: &Context,
viewport_id: ViewportId,
parent_id: ViewportId,
event_loop: &ActiveEventLoop,
builder: ViewportBuilder,
shared: &mut SharedViewportState,
) -> Self {
let window = Arc::new(egui_winit::create_window(ctx, event_loop, &builder).unwrap());
let painter = shared.init_painter(ctx, &window);
pollster::block_on(painter.set_window(viewport_id, Some(window.clone()))).unwrap();
let state = egui_winit::State::new(
ctx.clone(),
viewport_id,
event_loop,
Some(window.scale_factor() as f32),
event_loop.system_theme(),
painter.max_texture_side(),
);
let mut info = ViewportInfo {
parent: Some(parent_id),
..ViewportInfo::default()
};
egui_winit::update_viewport_info(&mut info, ctx, &window, true);
shared.viewport_info.insert(viewport_id, info);
Self {
id: viewport_id,
window,
state,
builder,
}
}
fn has_window_id(&self, window_id: WindowId) -> bool {
self.window.id() == window_id
}
fn on_mouse_motion(&mut self, delta: (f64, f64)) {
self.state.on_mouse_motion(delta);
}
fn on_window_event(
&mut self,
event: &WindowEvent,
shared: &mut SharedViewportState,
) -> (EventResponse, bool) {
if let WindowEvent::Resized(size) = event
&& let (Some(width), Some(height)) =
(NonZero::new(size.width), NonZero::new(size.height))
{
if shared.resized_viewport != Some(self.id) {
shared.resized_viewport = Some(self.id);
shared
.get_painter()
.on_window_resize_state_change(self.id, true);
}
shared
.get_painter()
.on_window_resized(self.id, width, height);
}
let response = self.state.on_window_event(&self.window, event);
let info = shared.viewport_info.get_mut(&self.id).unwrap();
if let WindowEvent::CloseRequested = event {
info.events.push(ViewportEvent::Close);
}
egui_winit::update_viewport_info(info, self.state.egui_ctx(), &self.window, false);
(response, info.close_requested())
}
fn take_egui_input(&mut self) -> RawInput {
self.state.take_egui_input(&self.window)
}
fn handle_platform_output(&mut self, platform_output: PlatformOutput) {
self.state
.handle_platform_output(&self.window, platform_output);
}
fn process_viewport_commands(
&self,
commands: Vec<ViewportCommand>,
shared: &mut SharedViewportState,
) {
let info = shared.viewport_info.get_mut(&self.id).unwrap();
egui_winit::process_viewport_commands(
self.state.egui_ctx(),
info,
commands,
&self.window,
&mut vec![],
);
}
}
#[derive(Debug)]
pub enum UserEvent {
GamepadEvent(gilrs::Event),
Quit(SimId),
LowBattery(SimId, bool),
RequestRedraw(ViewportId, Instant),
}
fn process_gamepad_input(mappings: MappingProvider, proxy: EventLoopProxy<UserEvent>) {
let mut gilrs = match Gilrs::new() {
Ok(gilrs) => gilrs,
Err(error) => {
warn!(%error, "could not connect gamepad listener");
return;
}
};
while let Some(event) = gilrs.next_event_blocking(None) {
if event.event == EventType::Connected {
let Some(gamepad) = gilrs.connected_gamepad(event.id) else {
continue;
};
mappings.handle_gamepad_connect(&gamepad);
}
if event.event == EventType::Disconnected {
mappings.handle_gamepad_disconnect(event.id);
}
if proxy.send_event(UserEvent::GamepadEvent(event)).is_err() {
// main thread has closed! we done
return;
}
}
}
+266
View File
@@ -0,0 +1,266 @@
use std::time::Duration;
use anyhow::{Context, Result, bail};
use audioadapter_buffers::direct::InterleavedSlice;
use cpal::{
Device, FromSample, Sample, SampleFormat, SizedSample, StreamConfig,
traits::{DeviceTrait, HostTrait, StreamTrait},
};
use itertools::Itertools;
use rubato::{Adjustable as _, Resampler};
use tracing::warn;
struct CpalAudio {
#[allow(unused)]
stream: cpal::Stream,
sampler: rubato::Async<f32>,
input_buffer: Vec<f32>,
output_buffer: Vec<f32>,
sample_sink: rtrb::Producer<f32>,
}
const VB_FREQUENCY: usize = 41700;
impl CpalAudio {
fn init() -> Result<Self> {
let host = cpal::default_host();
let device = host
.default_output_device()
.context("no output device available")?;
let default_rate = device.default_output_config()?.sample_rate();
let supported_config = device
.supported_output_configs()?
.filter(|c| {
c.contains_rate(default_rate)
&& matches!(
c.sample_format(),
SampleFormat::F32
| SampleFormat::F64
| SampleFormat::I32
| SampleFormat::I16
)
})
.max_by(|a, b| a.cmp_default_heuristics(b))
.context("no suitable output config available")?
.with_sample_rate(default_rate);
let mut config = supported_config.config();
let resample_ratio = config.sample_rate as f64 / VB_FREQUENCY as f64;
let chunk_size = (834.0 * resample_ratio) as usize;
let sampler = rubato::Async::new_poly(
resample_ratio,
64.0,
rubato::PolynomialDegree::Cubic,
chunk_size,
2,
rubato::FixedAsync::Output,
)?;
config.buffer_size = cpal::BufferSize::Fixed(sampler.output_frames_max() as u32);
match supported_config.sample_format() {
SampleFormat::F32 if config.channels == 2 => {
Self::new_f32_stereo(device, config, sampler)
}
SampleFormat::F32 => Self::new::<f32>(device, config, sampler),
SampleFormat::F64 => Self::new::<f64>(device, config, sampler),
SampleFormat::I32 => Self::new::<i32>(device, config, sampler),
SampleFormat::I16 => Self::new::<i16>(device, config, sampler),
_ => bail!("unsupported sample format"),
}
}
fn new_f32_stereo(
device: Device,
config: StreamConfig,
sampler: rubato::Async<f32>,
) -> Result<Self> {
let input_buffer = Vec::with_capacity(sampler.nbr_channels() * sampler.input_frames_max());
let output_buffer = vec![0.0; sampler.nbr_channels() * sampler.output_frames_max()];
let (sample_sink, mut sample_source) =
rtrb::RingBuffer::new(sampler.output_frames_max() * 4);
let stream = device.build_output_stream(
config,
move |data: &mut [f32], _| {
let requested = data.len();
let chunk = match sample_source.read_chunk(data.len()) {
Ok(c) => c,
Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => {
sample_source.read_chunk(n).unwrap()
}
};
let len = chunk.len();
let (first, second) = chunk.as_slices();
data[0..first.len()].copy_from_slice(first);
data[first.len()..len].copy_from_slice(second);
for rest in &mut data[len..requested] {
*rest = 0.0;
}
chunk.commit_all();
},
move |error| warn!(%error, "stream error"),
None,
)?;
stream.play()?;
Ok(Self {
stream,
sampler,
input_buffer,
output_buffer,
sample_sink,
})
}
fn new<S: SizedSample + FromSample<f32>>(
device: Device,
config: StreamConfig,
sampler: rubato::Async<f32>,
) -> Result<Self> {
let input_buffer = Vec::with_capacity(sampler.nbr_channels() * sampler.input_frames_max());
let output_buffer = vec![0.0; sampler.nbr_channels() * sampler.output_frames_max()];
let (sample_sink, mut sample_source) =
rtrb::RingBuffer::new(sampler.output_frames_max() * 4);
let channels = config.channels as usize;
let stream = device.build_output_stream(
config,
move |data: &mut [S], _| {
let requested = data.len() * 2 / channels;
let chunk = match sample_source.read_chunk(requested) {
Ok(c) => c,
Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => {
sample_source.read_chunk(n).unwrap()
}
};
let (first, second) = chunk.as_slices();
let new_samples = first
.iter()
.chain(second)
.map(|s: &f32| (*s).to_sample())
.chunks(2);
let buffer = data.iter_mut().chunks(channels);
for (dst, samples) in buffer.into_iter().zip(&new_samples) {
// channels beyond the first two are zeroed
let src = samples.chain(std::iter::repeat(S::EQUILIBRIUM));
for (d, s) in dst.zip(src) {
*d = s;
}
}
chunk.commit_all();
},
move |error| warn!(%error, "stream error"),
None,
)?;
stream.play()?;
Ok(Self {
stream,
sampler,
input_buffer,
output_buffer,
sample_sink,
})
}
fn update(&mut self, mut samples: &[f32]) {
while self.input_buffer.len() + samples.len() >= self.sampler.input_frames_next() * 2 {
let samples_needed =
(self.sampler.input_frames_next() * 2).saturating_sub(self.input_buffer.len());
let (current_samples, future_samples) = samples.split_at(samples_needed);
self.input_buffer.extend_from_slice(current_samples);
samples = future_samples;
let buffer_in =
InterleavedSlice::new(&self.input_buffer, 2, self.sampler.input_frames_next())
.unwrap();
let mut buffer_out = InterleavedSlice::new_mut(
&mut self.output_buffer,
2,
self.sampler.output_frames_next(),
)
.unwrap();
let (_, output_samples) = self
.sampler
.process_into_buffer(&buffer_in, &mut buffer_out, None)
.unwrap();
let chunk = match self.sample_sink.write_chunk_uninit(output_samples * 2) {
Ok(c) => c,
Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => {
self.sample_sink.write_chunk_uninit(n).unwrap()
}
};
chunk.fill_from_iter(self.output_buffer[..output_samples * 2].iter().copied());
self.input_buffer.clear();
}
self.input_buffer.extend_from_slice(samples);
while self.sample_sink.slots() < self.sampler.output_frames_max() * 2 {
std::thread::sleep(Duration::from_micros(500));
}
}
fn set_speed(&mut self, speed: f64) -> Result<()> {
self.sampler
.set_resample_ratio_relative(1.0 / speed, false)?;
Ok(())
}
}
struct NoAudio {
speed: f64,
}
impl NoAudio {
fn new() -> Self {
Self { speed: 1.0 }
}
fn update(&mut self, samples: &[f32]) {
let samples = (samples.len() / 2) as f64;
let elapsed = Duration::from_secs_f64(samples / (VB_FREQUENCY as f64 * self.speed));
std::thread::sleep(elapsed);
}
fn set_speed(&mut self, speed: f64) -> Result<()> {
self.speed = speed;
Ok(())
}
}
#[allow(clippy::large_enum_variant)]
enum AudioInner {
Cpal(CpalAudio),
None(NoAudio),
}
pub struct Audio(AudioInner);
impl Audio {
pub fn init() -> Self {
match CpalAudio::init() {
Ok(a) => Self(AudioInner::Cpal(a)),
Err(e) => {
warn!("could not play audio: {e}");
Self(AudioInner::None(NoAudio::new()))
}
}
}
pub fn update(&mut self, samples: &[f32]) {
match &mut self.0 {
AudioInner::Cpal(a) => a.update(samples),
AudioInner::None(a) => a.update(samples),
}
}
pub fn set_speed(&mut self, speed: f64) -> Result<()> {
match &mut self.0 {
AudioInner::Cpal(a) => a.set_speed(speed),
AudioInner::None(a) => a.set_speed(speed),
}
}
}
+177
View File
@@ -0,0 +1,177 @@
use anyhow::Result;
use clap::{Parser, ValueEnum};
use egui::{Color32, Pos2, Vec2};
use serde::{Deserialize, Serialize};
use crate::{emulator::SimId, persistence::Persistence, window::DisplayMode};
use std::path::PathBuf;
#[derive(Parser)]
#[command(version, long_version = env!("CARGO_PKG_VERSION"))]
pub struct CliArgs {
/// The path to a virtual boy ROM to run.
pub rom: Option<PathBuf>,
/// Start a GDB/LLDB debug server on this port.
#[arg(short, long)]
pub debug_port: Option<u16>,
/// Enable profiling a game
#[arg(short, long)]
pub profile: bool,
/// Open character data window
#[arg(short, long)]
pub character_data: bool,
/// Open bgmap data window
#[arg(short, long)]
pub bgmap_data: bool,
/// Open object data window
#[arg(short, long)]
pub object_data: bool,
/// Open worlds window
#[arg(long)]
pub worlds: bool,
/// Open frame buffers window
#[arg(short, long)]
pub frame_buffers: bool,
/// Open registers window
#[arg(short, long)]
pub registers: bool,
/// Open terminal
#[arg(short, long)]
pub terminal: bool,
/// Watch ROM files for changes, automatically reload
#[arg(short, long)]
pub watch: bool,
/// Automatically open Player 2 for multiplayer
#[arg(long)]
pub player2: bool,
/// Map the first connected controller to Player 2
#[arg(long)]
pub player2_controller: bool,
/// Force the application to render with CPU, rather than GPU.
#[arg(long)]
pub wgpu_force_fallback: Option<bool>,
/// Set a preference for whether to use a low-power or high-performance GPU adapter
#[arg(long)]
pub wgpu_power_preference: Option<PowerPreferenceWrapper>,
}
#[derive(ValueEnum, Clone, Copy)]
pub enum PowerPreferenceWrapper {
Low,
High,
None,
}
impl From<PowerPreferenceWrapper> for wgpu::PowerPreference {
fn from(value: PowerPreferenceWrapper) -> Self {
match value {
PowerPreferenceWrapper::Low => wgpu::PowerPreference::LowPower,
PowerPreferenceWrapper::High => wgpu::PowerPreference::HighPerformance,
PowerPreferenceWrapper::None => wgpu::PowerPreference::None,
}
}
}
pub const COLOR_PRESETS: [[Color32; 2]; 3] = [
[
Color32::from_rgb(0xff, 0x00, 0x00),
Color32::from_rgb(0x00, 0xc6, 0xf0),
],
[
Color32::from_rgb(0x00, 0xb4, 0x00),
Color32::from_rgb(0xc8, 0x00, 0xff),
],
[
Color32::from_rgb(0xb4, 0x9b, 0x00),
Color32::from_rgb(0x00, 0x00, 0xff),
],
];
const fn default_power_preference() -> wgpu::PowerPreference {
wgpu::PowerPreference::LowPower
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct AppConfig {
#[serde(default = "default_power_preference")]
pub power_preference: wgpu::PowerPreference,
#[serde(default)]
pub force_fallback: bool,
}
impl AppConfig {
pub fn load(persistence: &Persistence) -> Self {
if let Ok(config) = persistence.load_config(APP_CONFIG_FILENAME) {
return config;
}
Self {
power_preference: default_power_preference(),
force_fallback: false,
}
}
pub fn save(&self, persistence: &Persistence) -> Result<()> {
persistence.save_config(APP_CONFIG_FILENAME, &self)
}
pub fn update(&mut self, args: &CliArgs) -> bool {
let mut updated = false;
if let Some(pref) = args.wgpu_power_preference {
self.power_preference = pref.into();
updated = true;
}
if let Some(force_fallback) = args.wgpu_force_fallback {
self.force_fallback = force_fallback;
updated = true;
}
updated
}
}
const APP_CONFIG_FILENAME: &str = "config";
const fn default_audio_enabled() -> bool {
true
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct SimConfig {
pub display_mode: DisplayMode,
pub colors: [Color32; 2],
pub dimensions: Vec2,
#[serde(default = "default_audio_enabled")]
pub audio_enabled: bool,
#[serde(default)]
pub low_battery: bool,
#[serde(default)]
pub watch_rom: bool,
#[serde(default)]
pub position: Option<Pos2>,
}
impl SimConfig {
pub fn load(persistence: &Persistence, sim_id: SimId) -> Self {
if let Ok(config) = persistence.load_config(sim_config_filename(sim_id)) {
return config;
}
Self {
display_mode: DisplayMode::Anaglyph,
colors: COLOR_PRESETS[0],
dimensions: DisplayMode::Anaglyph.proportions() + Vec2::new(0.0, 22.0),
audio_enabled: true,
low_battery: false,
watch_rom: false,
position: None,
}
}
pub fn save(&self, persistence: &Persistence, sim_id: SimId) -> Result<()> {
persistence.save_config(sim_config_filename(sim_id), self)
}
}
fn sim_config_filename(sim_id: SimId) -> &'static str {
match sim_id {
SimId::Player1 => "config_p1",
SimId::Player2 => "config_p2",
}
}
+245
View File
@@ -0,0 +1,245 @@
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
use gilrs::{Event as GamepadEvent, EventType, GamepadId, ev::Code};
use winit::{
event::{ElementState, KeyEvent},
keyboard::PhysicalKey,
};
use crate::{
emulator::{EmulatorClient, EmulatorCommand, SimId, VBKey},
input::{AxisMapping, InputMapping, MappingProvider},
};
pub struct Controller {
pub sim_id: SimId,
state: VBKey,
axis_values: HashMap<(GamepadId, Code), f32>,
mapping: Arc<RwLock<InputMapping>>,
}
impl Controller {
pub fn new(sim_id: SimId, mappings: &MappingProvider) -> Self {
Self {
sim_id,
state: VBKey::SGN,
axis_values: HashMap::new(),
mapping: mappings.for_sim(sim_id).clone(),
}
}
pub fn key_event(&mut self, event: &KeyEvent) -> Option<VBKey> {
let keys = self.map_keys(&event.physical_key)?;
match event.state {
ElementState::Pressed => self.update_state(keys, VBKey::empty()),
ElementState::Released => self.update_state(VBKey::empty(), keys),
}
}
pub fn gamepad_event(&mut self, event: &GamepadEvent) -> Option<VBKey> {
let (pressed, released) = match event.event {
EventType::ButtonPressed(_, code) => {
let mappings = self.map_button(&event.id, &code)?;
(mappings, VBKey::empty())
}
EventType::ButtonReleased(_, code) => {
let mappings = self.map_button(&event.id, &code)?;
(VBKey::empty(), mappings)
}
EventType::AxisChanged(_, value, code) => {
let mapping = self.map_axis(&event.id, &code)?;
self.axis_values.insert((event.id, code), value);
let pair_value = mapping
.pair
.and_then(|p| self.axis_values.get(&(event.id, p)))
.copied()
.unwrap_or_default();
let neg = mapping.neg;
let pos = mapping.pos;
let (mut pressed, mut released) = axis_presses(value, pair_value, neg, pos);
if let Some(other) = mapping.pair.and_then(|p| self.map_axis(&event.id, &p)) {
let (other_pressed, other_released) =
axis_presses(pair_value, value, other.neg, other.pos);
pressed = pressed.union(other_pressed);
released = released.union(other_released);
}
(pressed, released)
}
_ => {
return None;
}
};
self.update_state(pressed, released)
}
pub fn low_battery(&mut self, pwr: bool) -> Option<VBKey> {
let (pressed, released) = if pwr {
(VBKey::PWR, VBKey::empty())
} else {
(VBKey::empty(), VBKey::PWR)
};
self.update_state(pressed, released)
}
fn update_state(&mut self, pressed: VBKey, released: VBKey) -> Option<VBKey> {
let old_state = self.state;
self.state = self.state.union(pressed).difference(released);
if self.state != old_state {
Some(self.state)
} else {
None
}
}
fn map_keys(&self, key: &PhysicalKey) -> Option<VBKey> {
self.mapping.read().unwrap().map_keyboard(key)
}
fn map_button(&self, id: &GamepadId, code: &Code) -> Option<VBKey> {
self.mapping.read().unwrap().map_button(id, code)
}
fn map_axis(&self, id: &GamepadId, code: &Code) -> Option<AxisMapping> {
self.mapping.read().unwrap().map_axis(id, code)
}
}
pub struct ControllerManager {
client: EmulatorClient,
controllers: [Controller; 2],
}
impl ControllerManager {
pub fn new(client: EmulatorClient, mappings: &MappingProvider) -> Self {
Self {
client,
controllers: [
Controller::new(SimId::Player1, mappings),
Controller::new(SimId::Player2, mappings),
],
}
}
pub fn handle_key_event(&mut self, event: &KeyEvent) {
for controller in &mut self.controllers {
if let Some(pressed) = controller.key_event(event) {
self.client
.send_command(EmulatorCommand::SetKeys(controller.sim_id, pressed));
}
}
}
pub fn handle_gamepad_event(&mut self, event: &GamepadEvent) {
for controller in &mut self.controllers {
if let Some(pressed) = controller.gamepad_event(event) {
self.client
.send_command(EmulatorCommand::SetKeys(controller.sim_id, pressed));
}
}
}
pub fn handle_low_battery(&mut self, sim_id: SimId, pwr: bool) {
let controller = &mut self.controllers[sim_id.to_index()];
if let Some(pressed) = controller.low_battery(pwr) {
self.client
.send_command(EmulatorCommand::SetKeys(controller.sim_id, pressed));
}
}
}
fn axis_presses(value: f32, pair_value: f32, neg: VBKey, pos: VBKey) -> (VBKey, VBKey) {
use std::f32::consts::FRAC_PI_3;
let mut pressed = VBKey::empty();
let mut released = VBKey::empty();
let magnitude = value.hypot(pair_value);
let abs_angle = pair_value.atan2(value).abs();
if magnitude < 0.65 {
released = released.union(neg).union(pos);
} else if abs_angle <= FRAC_PI_3 {
// stick tilted towards positive
released = released.union(neg);
if magnitude >= 0.75 {
pressed = pressed.union(pos);
}
} else if abs_angle >= 2.0 * FRAC_PI_3 {
// stick tilted towards negative
released = released.union(pos);
if magnitude >= 0.75 {
pressed = pressed.union(neg);
}
} else {
released = released.union(neg).union(pos);
}
(pressed, released)
}
#[cfg(test)]
mod tests {
use super::{VBKey, axis_presses};
const NEG: VBKey = VBKey::LL;
const POS: VBKey = VBKey::LR;
const NONE: VBKey = VBKey::empty();
const BOTH: VBKey = NEG.union(POS);
#[test]
fn detects_no_input() {
let (pressed, released) = axis_presses(0.0, 0.0, NEG, POS);
assert_eq!(pressed, NONE);
assert_eq!(released, BOTH);
}
#[test]
fn detects_pos_input() {
let (pressed, released) = axis_presses(1.0, 0.0, NEG, POS);
assert_eq!(pressed, POS);
assert_eq!(released, NEG);
}
#[test]
fn detects_neg_input() {
let (pressed, released) = axis_presses(-1.0, 0.0, NEG, POS);
assert_eq!(pressed, NEG);
assert_eq!(released, POS);
}
#[test]
fn respects_dead_zone() {
let (pressed, released) = axis_presses(0.70, 0.0, NEG, POS);
assert_eq!(pressed, NONE);
assert_eq!(released, NEG);
}
#[test]
fn handles_diagonals_ok() {
let (pressed, released) = axis_presses(0.6, 0.6, NEG, POS);
assert_eq!(pressed, POS);
assert_eq!(released, NEG);
}
#[test]
fn ignores_diagonal_noise() {
let (pressed, released) = axis_presses(-1.0, -0.1484245, NEG, POS);
assert_eq!(pressed, NEG);
assert_eq!(released, POS);
}
#[test]
fn ignores_small_values() {
let (pressed, released) = axis_presses(-0.1484245, -1.0, NEG, POS);
assert_eq!(pressed, NONE);
assert_eq!(released, BOTH);
}
}
+977
View File
@@ -0,0 +1,977 @@
use std::{
collections::HashMap,
fmt::Display,
path::{Path, PathBuf},
sync::{
Arc, Weak,
atomic::{AtomicBool, Ordering},
mpsc::{self, RecvTimeoutError, TryRecvError},
},
time::Duration,
};
use anyhow::Result;
use atomic::Atomic;
use bytemuck::NoUninit;
use egui_notify::Toast;
use tracing::{error, warn};
use crate::{
audio::Audio,
graphics::TextureSink,
memory::{MemoryRange, MemoryRegion},
};
use cart::Cart;
pub use game_info::GameInfo;
pub use inline_stack_map::InlineStack;
use inline_stack_map::InlineStackMap;
use shrooms_vb_core::{EXPECTED_FRAME_SIZE, Sim, StopReason};
pub use shrooms_vb_core::{SimEvent, VBKey, VBRegister, VBWatchpointType};
mod address_set;
mod cart;
mod game_info;
mod inline_stack_map;
mod shrooms_vb_core;
mod shrooms_vb_util;
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub enum SimId {
Player1,
Player2,
}
impl SimId {
pub const fn values() -> [Self; 2] {
[Self::Player1, Self::Player2]
}
pub const fn to_index(self) -> usize {
match self {
Self::Player1 => 0,
Self::Player2 => 1,
}
}
pub const fn from_index(index: usize) -> Option<Self> {
match index {
0 => Some(Self::Player1),
1 => Some(Self::Player2),
_ => None,
}
}
}
impl Display for SimId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Player1 => "Player 1",
Self::Player2 => "Player 2",
})
}
}
pub struct EmulatorBuilder {
rom: Option<PathBuf>,
commands: mpsc::Receiver<EmulatorCommand>,
sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
start_paused: bool,
watch_rom: Arc<[AtomicBool; 2]>,
}
impl EmulatorBuilder {
pub fn new() -> (Self, EmulatorClient) {
let (queue, commands) = mpsc::channel();
let builder = Self {
rom: None,
commands,
sim_state: Arc::new([
Atomic::new(SimState::Uninitialized),
Atomic::new(SimState::Uninitialized),
]),
state: Arc::new(Atomic::new(EmulatorState::Paused)),
audio_on: Arc::new([AtomicBool::new(true), AtomicBool::new(true)]),
linked: Arc::new(AtomicBool::new(false)),
start_paused: false,
watch_rom: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]),
};
let client = EmulatorClient {
queue,
sim_state: builder.sim_state.clone(),
state: builder.state.clone(),
watch_rom: builder.watch_rom.clone(),
linked: builder.linked.clone(),
};
(builder, client)
}
pub fn with_rom(self, path: &Path) -> Self {
Self {
rom: Some(path.into()),
..self
}
}
pub fn start_paused(self, paused: bool) -> Self {
Self {
start_paused: paused,
..self
}
}
pub fn with_audio_on(self, p1: bool, p2: bool) -> Self {
self.audio_on[0].store(p1, Ordering::Relaxed);
self.audio_on[1].store(p2, Ordering::Relaxed);
self
}
pub fn with_watch_rom(self, p1: bool, p2: bool) -> Self {
self.watch_rom[0].store(p1, Ordering::Relaxed);
self.watch_rom[1].store(p2, Ordering::Relaxed);
self
}
pub fn build(self) -> Result<Emulator> {
let mut emulator = Emulator::new(
self.commands,
self.sim_state,
self.state,
self.audio_on,
self.watch_rom,
self.linked,
);
if let Some(path) = self.rom {
emulator.load_cart(SimId::Player1, &path)?;
}
if self.start_paused {
emulator.pause_sims()?;
}
Ok(emulator)
}
}
pub struct Emulator {
sims: Vec<Sim>,
carts: [Option<Cart>; 2],
audio: Audio,
commands: mpsc::Receiver<EmulatorCommand>,
sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>,
watch_rom: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
profilers: [Option<ProfileSender>; 2],
renderers: HashMap<SimId, TextureSink>,
messages: HashMap<SimId, mpsc::Sender<Toast>>,
debuggers: HashMap<SimId, DebugInfo>,
stdouts: HashMap<SimId, mpsc::Sender<String>>,
watched_regions: HashMap<MemoryRange, Weak<MemoryRegion>>,
eye_contents: [Vec<u8>; 2],
audio_samples: Vec<f32>,
buffer: Vec<u8>,
}
impl Emulator {
fn new(
commands: mpsc::Receiver<EmulatorCommand>,
sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>,
watch_rom: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
) -> Self {
Self {
sims: vec![],
carts: [None, None],
audio: Audio::init(),
commands,
sim_state,
state,
audio_on,
watch_rom,
linked,
profilers: [None, None],
renderers: HashMap::new(),
messages: HashMap::new(),
debuggers: HashMap::new(),
stdouts: HashMap::new(),
watched_regions: HashMap::new(),
eye_contents: [vec![0u8; 384 * 224 * 2], vec![0u8; 384 * 224 * 2]],
audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE),
buffer: vec![],
}
}
pub fn reload_cart(&mut self, sim_id: SimId) -> Result<()> {
let Some(cart) = &self.carts[sim_id.to_index()] else {
return Ok(());
};
let path = cart.file_path.clone();
self.load_cart(sim_id, &path)
}
pub fn load_cart(&mut self, sim_id: SimId, path: &Path) -> Result<()> {
let watch = self.watch_rom[sim_id.to_index()].load(Ordering::Acquire);
let cart = Cart::load(path, sim_id, watch)?;
self.try_reset_sim(sim_id, Some(cart))?;
Ok(())
}
pub fn start_second_sim(&mut self, rom: Option<PathBuf>) -> Result<()> {
let file_path = if let Some(path) = rom {
Some(path)
} else {
self.carts[0].as_ref().map(|c| c.file_path.clone())
};
let watch = self.watch_rom[SimId::Player2.to_index()].load(Ordering::Acquire);
let cart = match file_path {
Some(rom_path) => Some(Cart::load(&rom_path, SimId::Player2, watch)?),
None => None,
};
self.try_reset_sim(SimId::Player2, cart)?;
self.link_sims();
Ok(())
}
fn reset_sim(&mut self, sim_id: SimId, new_cart: Option<Cart>) -> bool {
if let Err(error) = self.try_reset_sim(sim_id, new_cart) {
self.report_error(sim_id, format!("Error resetting sim: {error}"));
false
} else {
true
}
}
fn try_reset_sim(&mut self, sim_id: SimId, new_cart: Option<Cart>) -> Result<()> {
self.save_sram(sim_id)?;
let index = sim_id.to_index();
while self.sims.len() <= index {
let new_index = self.sims.len();
self.sims.push(Sim::new());
if self
.stdouts
.contains_key(&SimId::from_index(new_index).unwrap())
{
self.sims[new_index].watch_stdout(true);
}
self.sim_state[new_index].store(SimState::NoGame, Ordering::Release);
}
let sim = &mut self.sims[index];
sim.reset();
if let Some(cart) = new_cart {
sim.load_cart(cart.rom.clone(), cart.sram.clone())?;
self.carts[index] = Some(cart);
self.sim_state[index].store(SimState::Ready, Ordering::Release);
} else if let Some(cart) = self.carts[index].as_mut()
&& cart.is_watching()
{
cart.reload()?;
sim.load_cart(cart.rom.clone(), cart.sram.clone())?;
}
let mut profiling = false;
if let Some(profiler) = self.profilers[sim_id.to_index()].as_ref()
&& let Some(cart) = self.carts[index].as_ref()
&& profiler
.send(ProfileEvent::Start {
info: cart.info.clone(),
})
.is_ok()
{
sim.monitor_events(true, cart.info.inline_stack_map().clone());
profiling = true;
}
if !profiling {
sim.monitor_events(false, InlineStackMap::empty());
}
if self.sim_state[index].load(Ordering::Acquire) == SimState::Ready {
self.resume_sims();
}
Ok(())
}
fn link_sims(&mut self) {
let (first, second) = self.sims.split_at_mut(1);
let Some(first) = first.first_mut() else {
return;
};
let Some(second) = second.first_mut() else {
return;
};
first.link(second);
self.linked.store(true, Ordering::Release);
}
fn unlink_sims(&mut self) {
let Some(first) = self.sims.first_mut() else {
return;
};
first.unlink();
self.linked.store(false, Ordering::Release);
}
fn pause_sims(&mut self) -> Result<()> {
if self
.state
.compare_exchange(
EmulatorState::Running,
EmulatorState::Paused,
Ordering::AcqRel,
Ordering::Relaxed,
)
.is_ok()
{
for sim_id in SimId::values() {
self.save_sram(sim_id)?;
}
}
Ok(())
}
fn resume_sims(&mut self) {
let _ = self.state.compare_exchange(
EmulatorState::Paused,
EmulatorState::Running,
Ordering::AcqRel,
Ordering::Relaxed,
);
}
fn frame_advance(&mut self) {
if self
.state
.compare_exchange(
EmulatorState::Paused,
EmulatorState::Stepping,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_err_and(|s| s == EmulatorState::Running)
{
let _ = self.state.compare_exchange(
EmulatorState::Running,
EmulatorState::Stepping,
Ordering::AcqRel,
Ordering::Relaxed,
);
}
}
fn set_speed(&mut self, speed: f64) -> Result<()> {
self.audio.set_speed(speed)
}
fn save_sram(&mut self, sim_id: SimId) -> Result<()> {
let sim = self.sims.get_mut(sim_id.to_index());
let cart = self.carts[sim_id.to_index()].as_mut();
if let (Some(sim), Some(cart)) = (sim, cart) {
sim.read_sram(&mut cart.sram);
cart.save_sram()?;
}
Ok(())
}
pub fn stop_second_sim(&mut self) -> Result<()> {
self.save_sram(SimId::Player2)?;
self.renderers.remove(&SimId::Player2);
self.sims.truncate(1);
self.sim_state[SimId::Player2.to_index()].store(SimState::Uninitialized, Ordering::Release);
self.stop_debugging(SimId::Player2);
self.linked.store(false, Ordering::Release);
Ok(())
}
fn start_profiling(&mut self, sim_id: SimId, sender: ProfileSender) -> Result<()> {
self.profilers[sim_id.to_index()] = Some(sender);
self.try_reset_sim(sim_id, None)
}
fn start_debugging(&mut self, sim_id: SimId, sender: DebugSender) {
if self.sim_state[sim_id.to_index()].load(Ordering::Acquire) != SimState::Ready {
// Can't debug unless a game is connected
return;
}
let debug = DebugInfo {
sender,
stop_reason: Some(DebugStopReason::Paused),
};
self.debuggers.insert(sim_id, debug);
self.state
.store(EmulatorState::Debugging, Ordering::Release);
}
fn stop_debugging(&mut self, sim_id: SimId) {
if let Some(sim) = self.sims.get_mut(sim_id.to_index()) {
sim.clear_debug_state();
}
self.debuggers.remove(&sim_id);
if self.debuggers.is_empty() {
let _ = self.state.compare_exchange(
EmulatorState::Debugging,
EmulatorState::Running,
Ordering::AcqRel,
Ordering::Relaxed,
);
}
}
fn debug_interrupt(&mut self, sim_id: SimId) {
self.debug_stop(sim_id, DebugStopReason::Paused);
}
fn debug_stop(&mut self, sim_id: SimId, reason: DebugStopReason) {
let Some(debugger) = self.debuggers.get_mut(&sim_id) else {
self.stop_debugging(sim_id);
return;
};
if debugger.stop_reason != Some(reason) {
debugger.stop_reason = Some(reason);
if debugger.sender.send(DebugEvent::Stopped(reason)).is_err() {
self.stop_debugging(sim_id);
}
}
}
fn debug_continue(&mut self, sim_id: SimId) -> bool {
let Some(debugger) = self.debuggers.get_mut(&sim_id) else {
self.stop_debugging(sim_id);
return false;
};
debugger.stop_reason = None;
true
}
fn debug_step(&mut self, sim_id: SimId) {
if self.debug_continue(sim_id) {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.step();
}
}
fn watch_memory(&mut self, range: MemoryRange, region: Weak<MemoryRegion>) {
self.watched_regions.insert(range, region);
}
pub fn run(&mut self) {
loop {
let idle = self.tick();
if idle {
// The game is paused, and we have output all the video/audio we have.
// Block the thread until a new command comes in, or a ROM changes.
loop {
match self.commands.recv_timeout(Duration::from_millis(250)) {
Ok(command) => {
self.handle_command(command);
break;
}
Err(RecvTimeoutError::Timeout) => {
let mut changed = false;
for sim_id in SimId::values() {
let Some(cart) = self.carts[sim_id.to_index()].as_ref() else {
continue;
};
if cart.changed() {
changed |= self.reset_sim(sim_id, None);
}
}
if changed {
break;
}
}
Err(RecvTimeoutError::Disconnected) => {
return;
}
}
}
}
loop {
match self.commands.try_recv() {
Ok(command) => self.handle_command(command),
Err(TryRecvError::Empty) => {
break;
}
Err(TryRecvError::Disconnected) => {
return;
}
}
}
for sim_id in SimId::values() {
let Some(cart) = self.carts[sim_id.to_index()].as_ref() else {
continue;
};
if cart.changed() {
self.reset_sim(sim_id, None);
}
}
self.watched_regions.retain(|range, region| {
let Some(region) = region.upgrade() else {
return false;
};
let Some(sim) = self.sims.get_mut(range.sim.to_index()) else {
return false;
};
self.buffer.clear();
sim.read_memory(range.start, range.length, &mut self.buffer);
region.update(&self.buffer);
true
});
}
}
// returns true if the emulator is "idle" (i.e. this didn't output anything)
pub fn tick(&mut self) -> bool {
let p1_state = self.sim_state[SimId::Player1.to_index()].load(Ordering::Acquire);
let p2_state = self.sim_state[SimId::Player2.to_index()].load(Ordering::Acquire);
let state = self.state.load(Ordering::Acquire);
// Emulation
// Don't emulate if the state is "paused", or if any sim is paused in the debugger
let running = match state {
EmulatorState::Paused => false,
EmulatorState::Running | EmulatorState::Stepping => true,
EmulatorState::Debugging => self.debuggers.values().all(|d| d.stop_reason.is_none()),
};
let p1_running = running && p1_state == SimState::Ready;
let p2_running = running && p2_state == SimState::Ready;
let mut idle = !p1_running && !p2_running;
let cycles = self.emulate(p1_running, p2_running);
// if we're profiling, track events
for ((sim, profiler), running) in self
.sims
.iter_mut()
.zip(self.profilers.iter_mut())
.zip([p1_running, p2_running])
{
if !running {
continue;
}
if let Some(p) = profiler {
let (event, inline_stack) = sim.take_profiler_updates();
if p.send(ProfileEvent::Update {
cycles,
event,
inline_stack,
})
.is_err()
{
sim.monitor_events(false, InlineStackMap::empty());
*profiler = None;
}
}
}
if state == EmulatorState::Stepping {
self.state.store(EmulatorState::Paused, Ordering::Release);
}
// stdout
self.stdouts.retain(|sim_id, stdout| {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return true;
};
if let Some(text) = sim.take_stdout()
&& stdout.send(text).is_err()
{
sim.watch_stdout(false);
return false;
}
true
});
// Debug state
if state == EmulatorState::Debugging {
for sim_id in SimId::values() {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
continue;
};
if let Some(reason) = sim.take_stop_reason() {
let stop_reason = match reason {
StopReason::Stepped => DebugStopReason::Trace,
StopReason::Watchpoint(watch, address) => {
DebugStopReason::Watchpoint(watch, address)
}
StopReason::Breakpoint => DebugStopReason::Breakpoint,
};
self.debug_stop(sim_id, stop_reason);
}
}
}
// Video
for sim_id in SimId::values() {
let Some(renderer) = self.renderers.get_mut(&sim_id) else {
continue;
};
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
continue;
};
if sim.read_pixels(&mut self.eye_contents[sim_id.to_index()]) {
idle = false;
if renderer
.queue_render(&self.eye_contents[sim_id.to_index()])
.is_err()
{
self.renderers.remove(&sim_id);
}
}
}
// Audio
// Audio playback speed is how we keep the emulator running in real time.
// Even if we're muted, call `read_samples` to know how many frames of silence to play.
let p1_audio =
p1_running && self.audio_on[SimId::Player1.to_index()].load(Ordering::Acquire);
let p2_audio =
p2_running && self.audio_on[SimId::Player2.to_index()].load(Ordering::Acquire);
let (p1_weight, p2_weight) = match (p1_audio, p2_audio) {
(true, true) => (0.5, 0.5),
(true, false) => (1.0, 0.0),
(false, true) => (0.0, 1.0),
(false, false) => (0.0, 0.0),
};
if let Some(sim) = self.sims.get_mut(SimId::Player1.to_index()) {
sim.read_samples(&mut self.audio_samples, p1_weight);
}
if let Some(sim) = self.sims.get_mut(SimId::Player2.to_index()) {
sim.read_samples(&mut self.audio_samples, p2_weight);
}
if !self.audio_samples.is_empty() {
idle = false;
}
self.audio.update(&self.audio_samples);
self.audio_samples.clear();
idle
}
fn emulate(&mut self, p1_running: bool, p2_running: bool) -> u32 {
const MAX_CYCLES: u32 = 20_000_000;
let mut cycles = MAX_CYCLES;
if p1_running && p2_running {
Sim::emulate_many(&mut self.sims, &mut cycles);
} else if p1_running {
self.sims[SimId::Player1.to_index()].emulate(&mut cycles);
} else if p2_running {
self.sims[SimId::Player2.to_index()].emulate(&mut cycles);
}
MAX_CYCLES - cycles
}
fn handle_command(&mut self, command: EmulatorCommand) {
match command {
EmulatorCommand::ConnectToSim(sim_id, renderer, messages) => {
self.renderers.insert(sim_id, renderer);
self.messages.insert(sim_id, messages);
}
EmulatorCommand::LoadGame(sim_id, path) => {
if let Err(error) = self.load_cart(sim_id, &path) {
self.report_error(sim_id, format!("Error loading rom: {error}"));
}
}
EmulatorCommand::ReloadRom(sim_id) => {
if let Err(error) = self.reload_cart(sim_id) {
self.report_error(sim_id, format!("Error loading rom: {error}"));
}
}
EmulatorCommand::WatchRom(sim_id, watch) => {
self.watch_rom[sim_id.to_index()].store(watch, Ordering::Release);
let Some(cart) = self.carts[sim_id.to_index()].as_mut() else {
return;
};
if watch {
cart.restart_watching();
} else {
cart.stop_watching();
}
}
EmulatorCommand::StartSecondSim(path) => {
if let Err(error) = self.start_second_sim(path) {
self.report_error(
SimId::Player2,
format!("Error starting second sim: {error}"),
);
}
}
EmulatorCommand::StopSecondSim => {
if let Err(error) = self.stop_second_sim() {
self.report_error(
SimId::Player2,
format!("Error stopping second sim: {error}"),
);
}
}
EmulatorCommand::Pause => {
if let Err(error) = self.pause_sims() {
self.report_error(SimId::Player1, format!("Error pausing: {error}"));
}
}
EmulatorCommand::Resume => {
self.resume_sims();
}
EmulatorCommand::FrameAdvance => {
self.frame_advance();
}
EmulatorCommand::SetSpeed(speed) => {
if let Err(error) = self.set_speed(speed) {
self.report_error(SimId::Player1, format!("Error setting speed: {error}"));
}
}
EmulatorCommand::StartProfiling(sim_id, profiler) => {
if let Err(error) = self.start_profiling(sim_id, profiler) {
self.report_error(SimId::Player1, format!("Error enaling profiler: {error}"));
}
}
EmulatorCommand::StartDebugging(sim_id, debugger) => {
self.start_debugging(sim_id, debugger);
}
EmulatorCommand::StopDebugging(sim_id) => {
self.stop_debugging(sim_id);
}
EmulatorCommand::DebugInterrupt(sim_id) => {
self.debug_interrupt(sim_id);
}
EmulatorCommand::DebugContinue(sim_id) => {
self.debug_continue(sim_id);
}
EmulatorCommand::DebugStep(sim_id) => {
self.debug_step(sim_id);
}
EmulatorCommand::ReadRegister(sim_id, register, done) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
let value = sim.read_register(register);
let _ = done.send(value);
}
EmulatorCommand::WriteRegister(sim_id, register, value) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.write_register(register, value);
}
EmulatorCommand::ReadMemory(sim_id, start, length, mut buffer, done) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.read_memory(start, length, &mut buffer);
let _ = done.send(buffer);
}
EmulatorCommand::WriteMemory(sim_id, start, buffer, done) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.write_memory(start, &buffer);
let _ = done.send(buffer);
}
EmulatorCommand::WatchMemory(range, region) => {
self.watch_memory(range, region);
}
EmulatorCommand::AddBreakpoint(sim_id, address) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.add_breakpoint(address);
}
EmulatorCommand::RemoveBreakpoint(sim_id, address) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.remove_breakpoint(address);
}
EmulatorCommand::AddWatchpoint(sim_id, address, length, watch) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.add_watchpoint(address, length, watch);
}
EmulatorCommand::RemoveWatchpoint(sim_id, address, length, watch) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.remove_watchpoint(address, length, watch);
}
EmulatorCommand::WatchStdout(sim_id, stdout_sink) => {
self.stdouts.insert(sim_id, stdout_sink);
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.watch_stdout(true);
}
EmulatorCommand::SetAudioEnabled(sim_id, enabled) => {
self.audio_on[sim_id.to_index()].store(enabled, Ordering::Release);
}
EmulatorCommand::Link => {
self.link_sims();
}
EmulatorCommand::Unlink => {
self.unlink_sims();
}
EmulatorCommand::Reset(sim_id) => {
self.reset_sim(sim_id, None);
}
EmulatorCommand::SetKeys(sim_id, keys) => {
if let Some(sim) = self.sims.get_mut(sim_id.to_index()) {
sim.set_keys(keys);
}
}
EmulatorCommand::Screenshot(sim_id, sender) => {
let contents = self.eye_contents[sim_id.to_index()].clone();
let _ = sender.send(contents);
}
EmulatorCommand::Exit(done) => {
for sim_id in SimId::values() {
if let Err(error) = self.save_sram(sim_id) {
self.report_error(sim_id, format!("Error saving sram on exit: {error}"));
}
}
let _ = done.send(());
}
}
}
fn report_error(&self, sim_id: SimId, message: String) {
let messages = self
.messages
.get(&sim_id)
.or_else(|| self.messages.get(&SimId::Player1));
if let Some(msg) = messages {
let mut toast = Toast::error(&message);
toast.duration(Some(Duration::from_secs(5)));
if msg.send(toast).is_ok() {
return;
}
}
error!("{}", message);
}
}
#[derive(Debug)]
pub enum EmulatorCommand {
ConnectToSim(SimId, TextureSink, mpsc::Sender<Toast>),
LoadGame(SimId, PathBuf),
ReloadRom(SimId),
WatchRom(SimId, bool),
StartSecondSim(Option<PathBuf>),
StopSecondSim,
Pause,
Resume,
FrameAdvance,
SetSpeed(f64),
StartProfiling(SimId, ProfileSender),
StartDebugging(SimId, DebugSender),
StopDebugging(SimId),
DebugInterrupt(SimId),
DebugContinue(SimId),
DebugStep(SimId),
ReadRegister(SimId, VBRegister, oneshot::Sender<u32>),
WriteRegister(SimId, VBRegister, u32),
ReadMemory(SimId, u32, usize, Vec<u8>, oneshot::Sender<Vec<u8>>),
WriteMemory(SimId, u32, Vec<u8>, oneshot::Sender<Vec<u8>>),
WatchMemory(MemoryRange, Weak<MemoryRegion>),
AddBreakpoint(SimId, u32),
RemoveBreakpoint(SimId, u32),
AddWatchpoint(SimId, u32, usize, VBWatchpointType),
RemoveWatchpoint(SimId, u32, usize, VBWatchpointType),
WatchStdout(SimId, mpsc::Sender<String>),
SetAudioEnabled(SimId, bool),
Link,
Unlink,
Reset(SimId),
SetKeys(SimId, VBKey),
Screenshot(SimId, oneshot::Sender<Vec<u8>>),
Exit(oneshot::Sender<()>),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, NoUninit)]
#[repr(usize)]
pub enum SimState {
Uninitialized,
NoGame,
Ready,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, NoUninit)]
#[repr(usize)]
pub enum EmulatorState {
Paused,
Running,
Stepping,
Debugging,
}
type ProfileSender = tokio::sync::mpsc::UnboundedSender<ProfileEvent>;
type DebugSender = tokio::sync::mpsc::UnboundedSender<DebugEvent>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DebugStopReason {
// We are stepping
Trace,
// We hit a breakpoint
Breakpoint,
// We hit a watchpoint
Watchpoint(VBWatchpointType, u32),
// The debugger told us to pause
Paused,
}
struct DebugInfo {
sender: DebugSender,
stop_reason: Option<DebugStopReason>,
}
pub enum DebugEvent {
Stopped(DebugStopReason),
}
pub enum ProfileEvent {
Start {
info: Arc<GameInfo>,
},
Update {
cycles: u32,
event: Option<SimEvent>,
inline_stack: Option<InlineStack>,
},
}
#[derive(Clone)]
pub struct EmulatorClient {
queue: mpsc::Sender<EmulatorCommand>,
sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>,
linked: Arc<AtomicBool>,
watch_rom: Arc<[AtomicBool; 2]>,
}
impl EmulatorClient {
pub fn has_player_2(&self) -> bool {
self.sim_state(SimId::Player2) != SimState::Uninitialized
}
pub fn sim_state(&self, sim_id: SimId) -> SimState {
self.sim_state[sim_id.to_index()].load(Ordering::Acquire)
}
pub fn emulator_state(&self) -> EmulatorState {
self.state.load(Ordering::Acquire)
}
pub fn are_sims_linked(&self) -> bool {
self.linked.load(Ordering::Acquire)
}
pub fn is_rom_watched(&self, sim_id: SimId) -> bool {
self.watch_rom[sim_id.to_index()].load(Ordering::Acquire)
}
pub fn send_command(&self, command: EmulatorCommand) -> bool {
match self.queue.send(command) {
Ok(()) => true,
Err(err) => {
warn!(
"could not send command {:?} as emulator is shut down",
err.0
);
false
}
}
}
}
+270
View File
@@ -0,0 +1,270 @@
use std::{
collections::{BTreeMap, BTreeSet},
ops::Bound,
};
#[derive(Debug, Default)]
pub struct AddressSet {
ranges: BTreeSet<(u32, usize)>,
bounds: BTreeMap<u32, usize>,
}
impl AddressSet {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, address: u32, length: usize) {
if length == 0 || !self.ranges.insert((address, length)) {
return;
}
let end = (address as usize)
.checked_add(length)
.and_then(|e| u32::try_from(e).ok());
if let Some(end) = end {
let val_before = self.bounds.range(..=end).next_back().map_or(0, |(_, &v)| v);
self.bounds.insert(end, val_before);
}
let val_before = self
.bounds
.range(..address)
.next_back()
.map_or(0, |(_, &v)| v);
if let Some(&val_at) = self.bounds.get(&address) {
if val_before == val_at + 1 {
self.bounds.remove(&address);
}
} else {
self.bounds.insert(address, val_before);
}
let start_bound = Bound::Included(address);
let end_bound = match end {
Some(e) => Bound::Excluded(e),
None => Bound::Unbounded,
};
for (_, val) in self.bounds.range_mut((start_bound, end_bound)) {
*val += 1;
}
}
pub fn remove(&mut self, address: u32, length: usize) {
if !self.ranges.remove(&(address, length)) {
return;
}
let end = (address as usize)
.checked_add(length)
.and_then(|e| u32::try_from(e).ok());
if let Some(end) = end {
let val_before = self.bounds.range(..end).next_back().map_or(0, |(_, &v)| v);
if let Some(&val_at) = self.bounds.get(&end) {
if val_at + 1 == val_before {
self.bounds.remove(&end);
}
} else {
self.bounds.insert(end, val_before);
}
}
let val_before = self
.bounds
.range(..address)
.next_back()
.map_or(0, |(_, &v)| v);
if let Some(&val_at) = self.bounds.get(&address) {
if val_before + 1 == val_at {
self.bounds.remove(&address);
}
} else {
self.bounds.insert(address, val_before);
}
let start_bound = Bound::Included(address);
let end_bound = match end {
Some(e) => Bound::Excluded(e),
None => Bound::Unbounded,
};
for (_, val) in self.bounds.range_mut((start_bound, end_bound)) {
*val -= 1;
}
}
pub fn clear(&mut self) {
self.ranges.clear();
self.bounds.clear();
}
pub fn is_empty(&self) -> bool {
self.bounds.is_empty()
}
pub fn contains(&self, address: u32) -> bool {
self.bounds
.range(..=address)
.next_back()
.is_some_and(|(_, &val)| val > 0)
}
pub fn start_of_range_containing(&self, address: u32) -> Option<u32> {
if !self.contains(address) {
return None;
}
self.ranges
.range(..=(address, usize::MAX))
.rev()
.find_map(|&(start, length)| {
let contains = start <= address
&& (start as usize)
.checked_add(length)
.is_none_or(|end| end > address as usize);
contains.then_some(start)
})
}
}
#[cfg(test)]
mod tests {
use super::AddressSet;
#[test]
fn should_not_include_addresses_when_empty() {
let set = AddressSet::new();
assert!(set.is_empty());
assert!(!set.contains(0x13374200));
assert_eq!(set.start_of_range_containing(0x13374200), None);
}
#[test]
fn should_include_addresses_when_full() {
let mut set = AddressSet::new();
set.add(0x00000000, 0x100000000);
assert!(set.contains(0x13374200));
assert_eq!(set.start_of_range_containing(0x13374200), Some(0x00000000));
}
#[test]
fn should_ignore_empty_address_ranges() {
let mut set = AddressSet::new();
set.add(0x13374200, 0);
assert!(set.is_empty());
assert!(!set.contains(0x13374200));
}
#[test]
fn should_add_addresses_idempotently() {
let mut set = AddressSet::new();
set.add(0x13374200, 1);
set.add(0x13374200, 1);
set.remove(0x13374200, 1);
assert!(set.is_empty());
assert!(!set.contains(0x13374200));
}
#[test]
fn should_remove_addresses_idempotently() {
let mut set = AddressSet::new();
set.add(0x13374200, 1);
set.remove(0x13374200, 1);
set.remove(0x13374200, 1);
assert!(set.is_empty());
assert!(!set.contains(0x13374200));
}
#[test]
fn should_report_address_in_range() {
let mut set = AddressSet::new();
set.add(0x13374200, 4);
assert!(!set.contains(0x133741ff));
for address in 0x13374200..0x13374204 {
assert!(set.contains(address));
}
assert!(!set.contains(0x13374204));
}
#[test]
fn should_allow_overlapping_addresses() {
let mut set = AddressSet::new();
set.add(0x13374200, 4);
set.add(0x13374201, 1);
set.add(0x13374202, 2);
assert!(!set.contains(0x133741ff));
for address in 0x13374200..0x13374204 {
assert!(set.contains(address));
}
assert!(!set.contains(0x13374204));
set.remove(0x13374200, 4);
assert!(!set.contains(0x13374200));
for address in 0x13374201..0x13374204 {
assert!(set.contains(address));
}
assert!(!set.contains(0x13374204));
}
#[test]
fn should_allow_removing_overlapped_address_ranges() {
let mut set = AddressSet::new();
set.add(0x13374200, 8);
set.add(0x13374204, 8);
set.remove(0x13374204, 8);
for address in 0x13374200..0x13374208 {
assert!(set.contains(address));
}
for address in 0x13374208..0x1337420c {
assert!(!set.contains(address));
}
}
#[test]
fn should_merge_adjacent_ranges() {
let mut set = AddressSet::new();
set.add(0x13374200, 4);
set.add(0x13374204, 4);
set.add(0x13374208, 4);
set.add(0x1337420c, 4);
assert!(!set.contains(0x133741ff));
for address in 0x13374200..0x13374210 {
assert!(set.contains(address));
}
assert!(!set.contains(0x13374210));
set.remove(0x13374200, 4);
set.remove(0x13374204, 4);
set.remove(0x13374208, 4);
set.remove(0x1337420c, 4);
assert!(set.is_empty());
for address in 0x133741ff..=0x13374210 {
assert!(!set.contains(address));
}
}
#[test]
fn should_find_start_of_range() {
let mut set = AddressSet::new();
set.add(0x13374200, 4);
assert_eq!(set.start_of_range_containing(0x133741ff), None);
for address in 0x13374200..0x13374204 {
assert_eq!(set.start_of_range_containing(address), Some(0x13374200));
}
assert_eq!(set.start_of_range_containing(0x13374204), None);
}
#[test]
fn should_ignore_ranges_not_containing_address() {
let mut set = AddressSet::new();
set.add(0x10000000, 1024);
set.add(0x30000000, 1024);
assert!(!set.contains(0x20000000));
assert_eq!(set.start_of_range_containing(0x20000000), None);
}
}
+185
View File
@@ -0,0 +1,185 @@
use anyhow::Result;
use notify::Watcher;
use rand::RngExt;
use std::{
fs::{self, File},
io::{Read, Seek as _, SeekFrom, Write as _},
path::{Path, PathBuf},
sync::{Arc, atomic::AtomicBool},
};
use crate::emulator::{SimId, game_info::GameInfo, shrooms_vb_util::rom_from_isx};
pub struct Cart {
pub file_path: PathBuf,
pub rom: Vec<u8>,
sram_file: File,
pub sram: Vec<u8>,
pub info: Arc<GameInfo>,
watcher: Option<notify::RecommendedWatcher>,
changed: Arc<AtomicBool>,
}
impl Cart {
pub fn load(file_path: &Path, sim_id: SimId, watch: bool) -> Result<Self> {
let (rom, info) = Self::read_rom(file_path)?;
let mut sram_file = File::options()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(sram_path(file_path, sim_id))?;
let sram = if sram_file.metadata()?.len() == 0 {
// new SRAM file, randomize the contents
let mut sram = vec![0; 16 * 1024];
let mut rng = rand::rng();
for dst in sram.iter_mut().step_by(2) {
*dst = rng.random();
}
sram
} else {
let mut sram = Vec::with_capacity(16 * 1024);
sram_file.read_to_end(&mut sram)?;
sram
};
let changed = Arc::new(AtomicBool::new(false));
let watcher = if watch {
build_watcher(file_path, changed.clone())
} else {
None
};
Ok(Cart {
file_path: file_path.to_path_buf(),
rom,
sram_file,
sram,
info: Arc::new(info),
watcher,
changed,
})
}
pub fn reload(&mut self) -> Result<()> {
let (rom, info) = Self::read_rom(&self.file_path)?;
self.rom = rom;
self.info = Arc::new(info);
if self.is_watching() {
self.restart_watching();
}
Ok(())
}
pub fn save_sram(&mut self) -> Result<()> {
self.sram_file.seek(SeekFrom::Start(0))?;
self.sram_file.write_all(&self.sram)?;
Ok(())
}
pub fn changed(&self) -> bool {
self.changed.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn is_watching(&self) -> bool {
self.watcher.is_some()
}
pub fn restart_watching(&mut self) {
self.changed = Arc::new(AtomicBool::new(false));
if let Some(mut watcher) = self.watcher.take() {
let _ = watcher.unwatch(&self.file_path);
};
self.watcher = build_watcher(&self.file_path, self.changed.clone());
}
pub fn stop_watching(&mut self) {
self.changed = Arc::new(AtomicBool::new(false));
self.watcher = None;
}
fn read_rom(file_path: &Path) -> Result<(Vec<u8>, GameInfo)> {
let rom = fs::read(file_path)?;
let (rom, info) = try_parse_isx(file_path, &rom)
.or_else(|| try_parse_elf(file_path, &rom))
.unwrap_or_else(|| (rom, GameInfo::empty(file_path)));
Ok((rom, info))
}
}
fn build_watcher(file_path: &Path, changed: Arc<AtomicBool>) -> Option<notify::RecommendedWatcher> {
let file_path = file_path.to_path_buf();
let mut watcher =
notify::recommended_watcher(move |event: Result<notify::Event, notify::Error>| {
let Ok(e) = event else {
return;
};
let modified = !matches!(
e.kind,
notify::EventKind::Access(_)
| notify::EventKind::Modify(notify::event::ModifyKind::Metadata(_))
);
if modified {
changed.store(true, std::sync::atomic::Ordering::Relaxed);
}
})
.ok()?;
watcher
.watch(&file_path, notify::RecursiveMode::NonRecursive)
.ok()?;
Some(watcher)
}
fn try_parse_isx(file_path: &Path, data: &[u8]) -> Option<(Vec<u8>, GameInfo)> {
let rom = rom_from_isx(data)?;
let info = GameInfo::from_isx(file_path, data);
Some((rom, info))
}
fn try_parse_elf(file_path: &Path, data: &[u8]) -> Option<(Vec<u8>, GameInfo)> {
use object::read::elf::FileHeader;
let program = match object::FileKind::parse(data).ok()? {
object::FileKind::Elf32 => {
let header = object::elf::FileHeader32::parse(data).ok()?;
parse_elf_program(header, data)?
}
object::FileKind::Elf64 => {
let header = object::elf::FileHeader64::parse(data).ok()?;
parse_elf_program(header, data)?
}
_ => return None,
};
let info = GameInfo::from_elf(file_path, data).unwrap_or_else(|_| GameInfo::empty(file_path));
Some((program, info))
}
fn parse_elf_program<Elf: object::read::elf::FileHeader<Endian = object::Endianness>>(
header: &Elf,
data: &[u8],
) -> Option<Vec<u8>> {
use object::read::elf::ProgramHeader;
let endian = header.endian().ok()?;
let mut bytes = vec![];
let mut pstart = None;
for phdr in header.program_headers(endian, data).ok()? {
let pma = phdr.p_paddr(endian).into();
if pma < 0x07000000 || phdr.p_filesz(endian).into() == 0 {
continue;
}
let start = pstart.unwrap_or(pma);
pstart = Some(start);
bytes.resize((pma - start) as usize, 0);
let data = phdr.data(endian, data).ok()?;
bytes.extend_from_slice(data);
}
Some(bytes)
}
fn sram_path(file_path: &Path, sim_id: SimId) -> PathBuf {
match sim_id {
SimId::Player1 => file_path.with_extension("p1.sram"),
SimId::Player2 => file_path.with_extension("p2.sram"),
}
}
+284
View File
@@ -0,0 +1,284 @@
use std::{borrow::Cow, path::Path, sync::Arc};
use anyhow::{Result, bail};
use fxprof_processed_profile::{LibraryInfo, Symbol, SymbolTable, debugid::DebugId};
use object::{Object, ObjectSection, ObjectSymbol};
use wholesym::samply_symbols::{DebugIdExt, demangle_any};
use crate::emulator::inline_stack_map::{InlineStackMap, InlineStackMapBuilder};
#[derive(Debug)]
pub struct GameInfo {
library_info: LibraryInfo,
inline_stack_map: InlineStackMap,
}
impl GameInfo {
pub fn from_elf(file_path: &Path, input: &[u8]) -> Result<Self> {
let file = object::File::parse(input)?;
let (name, path) = name_and_path(file_path);
let debug_id = file
.build_id()?
.map(|id| DebugId::from_identifier(id, true))
.unwrap_or_default();
let code_id = file.build_id()?.map(hex::encode);
let mut symbols = vec![];
for sym in file.symbols() {
symbols.push(Symbol {
address: sym.address() as u32,
size: Some(sym.size() as u32),
name: demangle_any(sym.name()?),
});
}
let inline_stack_map =
build_inline_stack_map(file).unwrap_or_else(|_| InlineStackMap::empty());
let library_info = LibraryInfo {
name: name.clone(),
debug_name: name,
path: path.clone(),
debug_path: path,
debug_id,
code_id,
arch: None,
symbol_table: Some(Arc::new(SymbolTable::new(symbols))),
};
Ok(Self {
library_info,
inline_stack_map,
})
}
pub fn from_isx(file_path: &Path, input: &[u8]) -> Self {
let (name, path) = name_and_path(file_path);
let symbols = extract_isx_symbols(input);
let library_info = LibraryInfo {
name: name.clone(),
debug_name: name,
path: path.clone(),
debug_path: path,
debug_id: DebugId::default(),
code_id: None,
arch: None,
symbol_table: symbols.map(|syms| Arc::new(SymbolTable::new(syms))),
};
let inline_stack_map = InlineStackMap::empty();
Self {
library_info,
inline_stack_map,
}
}
pub fn empty(file_path: &Path) -> Self {
let (name, path) = name_and_path(file_path);
let library_info = LibraryInfo {
name: name.clone(),
debug_name: name,
path: path.clone(),
debug_path: path,
debug_id: DebugId::default(),
code_id: None,
arch: None,
symbol_table: None,
};
let inline_stack_map = InlineStackMap::empty();
Self {
library_info,
inline_stack_map,
}
}
pub fn name(&self) -> &str {
&self.library_info.name
}
pub fn library_info(&self) -> &LibraryInfo {
&self.library_info
}
pub fn inline_stack_map(&self) -> &InlineStackMap {
&self.inline_stack_map
}
}
fn build_inline_stack_map(file: object::File) -> Result<InlineStackMap> {
let endian = if file.is_little_endian() {
gimli::RunTimeEndian::Little
} else {
gimli::RunTimeEndian::Big
};
fn load_section<'a>(file: &'a object::File, id: gimli::SectionId) -> Result<Cow<'a, [u8]>> {
let input = match file.section_by_name(id.name()) {
Some(section) => section.uncompressed_data()?,
None => Cow::Owned(vec![]),
};
Ok(input)
}
let dorf = gimli::DwarfSections::load(|id| load_section(&file, id))?;
let dorf = dorf.borrow(|sec| gimli::EndianSlice::new(sec, endian));
let mut units = dorf.units();
let mut frames = InlineStackMap::builder();
while let Some(header) = units.next()? {
let unit = dorf.unit(header)?;
let mut entree = unit.entries_tree(None)?;
let root = entree.root()?;
let mut ctx = ParseContext {
dorf: &dorf,
unit: &unit,
frames: &mut frames,
};
parse_inline(&mut ctx, root)?;
}
Ok(frames.build())
}
fn extract_isx_symbols(input: &[u8]) -> Option<Vec<Symbol>> {
let mut syms = vec![];
let (_, mut buf) = input.split_at_checked(32)?;
while !buf.is_empty() {
let typ;
(typ, buf) = buf.split_first()?;
match typ {
0x11 => {
// Code (Virtual Boy)
(_, buf) = buf.split_at_checked(4)?;
let len_bytes;
(len_bytes, buf) = buf.split_first_chunk()?;
let len = u32::from_le_bytes(*len_bytes);
(_, buf) = buf.split_at_checked(len as usize)?;
}
0x13 => {
// Range (Virtual Boy)
let count_bytes;
(count_bytes, buf) = buf.split_first_chunk()?;
let count = u16::from_le_bytes(*count_bytes);
(_, buf) = buf.split_at_checked(count as usize * 9)?;
}
0x14 => {
// Symbol (Virtual Boy)
let count_bytes;
(count_bytes, buf) = buf.split_first_chunk()?;
let count = u16::from_le_bytes(*count_bytes);
for _ in 0..count {
let name_len;
(name_len, buf) = buf.split_first()?;
let name_bytes;
(name_bytes, buf) = buf.split_at_checked(*name_len as usize)?;
(_, buf) = buf.split_at_checked(2)?;
let address_bytes;
(address_bytes, buf) = buf.split_first_chunk()?;
let name_str = String::from_utf8_lossy(name_bytes);
let address = u32::from_le_bytes(*address_bytes);
syms.push(Symbol {
address: address & 0x07ffffff,
size: Some(4),
name: demangle_any(&name_str),
});
}
}
0x20..=0x22 => {
// System (undocumented)
let length_bytes;
(length_bytes, buf) = buf.split_first_chunk()?;
let length = u32::from_le_bytes(*length_bytes);
(_, buf) = buf.split_at_checked(length as usize)?;
}
_ => {
return None;
}
}
}
Some(syms)
}
type Reader<'a> = gimli::EndianSlice<'a, gimli::RunTimeEndian>;
struct ParseContext<'a> {
dorf: &'a gimli::Dwarf<Reader<'a>>,
unit: &'a gimli::Unit<Reader<'a>>,
frames: &'a mut InlineStackMapBuilder,
}
impl ParseContext<'_> {
fn name_attr(&self, attr: gimli::AttributeValue<Reader>) -> Result<Option<String>> {
match attr {
gimli::AttributeValue::DebugInfoRef(offset) => {
let mut units = self.dorf.units();
while let Some(header) = units.next()? {
if let Some(offset) = offset.to_unit_offset(&header) {
let unit = self.dorf.unit(header)?;
return self.name_entry(&unit, offset);
}
}
Ok(None)
}
gimli::AttributeValue::UnitRef(offset) => self.name_entry(self.unit, offset),
other => {
bail!("unrecognized attr {other:?}");
}
}
}
fn name_entry(
&self,
unit: &gimli::Unit<Reader>,
offset: gimli::UnitOffset,
) -> Result<Option<String>> {
let abbreviations = self.dorf.abbreviations(&unit.header)?;
let mut entries = unit.header.entries_raw(&abbreviations, Some(offset))?;
let Some(abbrev) = entries.read_abbreviation()? else {
return Ok(None);
};
let mut name = None;
for spec in abbrev.attributes() {
let attr = entries.read_attribute(*spec)?;
if attr.name() == gimli::DW_AT_linkage_name
|| (attr.name() == gimli::DW_AT_name && name.is_none())
{
name = Some(self.dorf.attr_string(unit, attr.value())?)
}
}
Ok(name.map(|n| demangle_any(&String::from_utf8_lossy(&n))))
}
}
fn parse_inline<'a>(
ctx: &mut ParseContext<'a>,
node: gimli::EntriesTreeNode<'a, '_, Reader<'a>>,
) -> Result<()> {
if node.entry().tag() == gimli::DW_TAG_inlined_subroutine
&& let Some(attr) = node.entry().attr_value(gimli::DW_AT_abstract_origin)
&& let Some(name) = ctx.name_attr(attr)?
{
let name = Arc::new(name);
let mut ranges = ctx.dorf.die_ranges(ctx.unit, node.entry())?;
while let Some(range) = ranges.next()? {
let start = range.begin as u32 & 0x07ffffff;
let end = range.end as u32 & 0x07ffffff;
ctx.frames.add(start, end, name.clone());
}
}
let mut children = node.children();
while let Some(child) = children.next()? {
parse_inline(ctx, child)?;
}
Ok(())
}
fn name_and_path(file_path: &Path) -> (String, String) {
let normalized = normpath::PathExt::normalize(file_path);
let path = normalized
.as_ref()
.map(|n| n.as_path())
.unwrap_or(file_path);
let name = match path.file_stem() {
Some(s) => s.to_string_lossy().into_owned(),
None => "game".to_string(),
};
let path = path.to_string_lossy().into_owned();
(name, path)
}
+87
View File
@@ -0,0 +1,87 @@
use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
};
pub type InlineStack = Arc<Vec<Arc<String>>>;
#[derive(Debug, Clone)]
pub struct InlineStackMap {
entries: Vec<(u32, InlineStack)>,
empty: InlineStack,
}
impl InlineStackMap {
pub fn empty() -> Self {
Self {
entries: vec![],
empty: Arc::new(vec![]),
}
}
pub fn builder() -> InlineStackMapBuilder {
InlineStackMapBuilder {
events: BTreeMap::new(),
}
}
pub fn get(&self, address: u32) -> &InlineStack {
match self.entries.binary_search_by_key(&address, |(a, _)| *a) {
Ok(index) => self.entries.get(index),
Err(after) => after.checked_sub(1).and_then(|i| self.entries.get(i)),
}
.map(|(_, s)| s)
.unwrap_or(&self.empty)
}
}
#[derive(Default)]
struct Event {
end: usize,
start: Vec<Arc<String>>,
}
pub struct InlineStackMapBuilder {
events: BTreeMap<u32, Event>,
}
impl InlineStackMapBuilder {
pub fn add(&mut self, start: u32, end: u32, name: Arc<String>) {
self.events.entry(start).or_default().start.push(name);
self.events.entry(end).or_default().end += 1;
}
pub fn build(self) -> InlineStackMap {
let empty = Arc::new(vec![]);
let mut entries = vec![];
let mut stack_indexes = vec![];
let mut stack = vec![];
let mut string_dedup = HashMap::new();
let mut stack_dedup = BTreeMap::new();
stack_dedup.insert(vec![], empty.clone());
for (address, event) in self.events {
for _ in 0..event.end {
stack.pop();
stack_indexes.pop();
}
for call in event.start {
if let Some(index) = string_dedup.get(&call) {
stack.push(call);
stack_indexes.push(*index);
} else {
let index = string_dedup.len();
string_dedup.insert(call.clone(), index);
stack.push(call);
stack_indexes.push(index);
}
}
if let Some(stack) = stack_dedup.get(&stack_indexes) {
entries.push((address, stack.clone()));
} else {
let stack = Arc::new(stack.clone());
stack_dedup.insert(stack_indexes.clone(), stack.clone());
entries.push((address, stack));
}
}
InlineStackMap { entries, empty }
}
}
+925
View File
@@ -0,0 +1,925 @@
use std::{borrow::Cow, ffi::c_void, ptr, slice, sync::Arc};
use anyhow::{Result, anyhow};
use bitflags::bitflags;
use num_derive::{FromPrimitive, ToPrimitive};
use serde::{Deserialize, Serialize};
use crate::emulator::inline_stack_map::{InlineStack, InlineStackMap};
use super::address_set::AddressSet;
#[repr(C)]
struct VB {
_data: [u8; 0],
}
#[allow(non_camel_case_types)]
type c_int = i32;
#[allow(non_camel_case_types)]
type c_uint = u32;
#[repr(u32)]
#[derive(FromPrimitive, ToPrimitive)]
enum VBDataType {
S8 = 0,
U8 = 1,
S16 = 2,
U16 = 3,
S32 = 4,
F32 = 5,
}
#[repr(i32)]
#[derive(FromPrimitive, ToPrimitive)]
enum VBOption {
PseudoHalt = 0,
}
bitflags! {
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct VBKey: u16 {
const PWR = 0x0001;
const SGN = 0x0002;
const A = 0x0004;
const B = 0x0008;
const RT = 0x0010;
const LT = 0x0020;
const RU = 0x0040;
const RR = 0x0080;
const LR = 0x0100;
const LL = 0x0200;
const LD = 0x0400;
const LU = 0x0800;
const STA = 0x1000;
const SEL = 0x2000;
const RL = 0x4000;
const RD = 0x8000;
}
}
#[derive(Clone, Copy, Debug)]
pub enum VBRegister {
Program(u32),
System(u32),
PC,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VBWatchpointType {
Read,
Write,
Access,
}
type OnException = extern "C" fn(sim: *mut VB, cause: *mut u16) -> c_int;
type OnExecute =
extern "C" fn(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> c_int;
type OnFetch = extern "C" fn(
sim: *mut VB,
fetch: c_int,
address: u32,
value: *mut i32,
cycles: *mut u32,
) -> c_int;
type OnFrame = extern "C" fn(sim: *mut VB) -> c_int;
type OnRead = extern "C" fn(
sim: *mut VB,
address: u32,
type_: VBDataType,
value: *mut i32,
cycles: *mut u32,
) -> c_int;
type OnWrite = extern "C" fn(
sim: *mut VB,
address: u32,
type_: VBDataType,
value: *mut i32,
cycles: *mut u32,
cancel: *mut c_int,
) -> c_int;
#[link(name = "vb")]
unsafe extern "C" {
#[link_name = "vbEmulate"]
fn vb_emulate(sim: *mut VB, cycles: *mut u32) -> c_int;
#[link_name = "vbEmulateEx"]
fn vb_emulate_ex(sims: *mut *mut VB, count: c_uint, cycles: *mut u32) -> c_int;
#[link_name = "vbGetCartRAM"]
fn vb_get_cart_ram(sim: *mut VB, size: *mut u32) -> *mut c_void;
#[link_name = "vbGetCartROM"]
fn vb_get_cart_rom(sim: *mut VB, size: *mut u32) -> *mut c_void;
#[link_name = "vbGetPixels"]
fn vb_get_pixels(
sim: *mut VB,
left: *mut c_void,
left_stride_x: c_int,
left_stride_y: c_int,
right: *mut c_void,
right_stride_x: c_int,
right_stride_y: c_int,
);
#[link_name = "vbGetProgramCounter"]
fn vb_get_program_counter(sim: *mut VB) -> u32;
#[link_name = "vbGetProgramRegister"]
fn vb_get_program_register(sim: *mut VB, index: c_uint) -> i32;
#[link_name = "vbGetSamples"]
fn vb_get_samples(
sim: *mut VB,
typ_: *mut VBDataType,
capacity: *mut c_uint,
position: *mut c_uint,
) -> *mut c_void;
#[link_name = "vbGetSystemRegister"]
fn vb_get_system_register(sim: *mut VB, index: c_uint) -> i32;
#[link_name = "vbGetUserData"]
fn vb_get_user_data(sim: *mut VB) -> *mut c_void;
#[link_name = "vbInit"]
fn vb_init(sim: *mut VB) -> *mut VB;
#[link_name = "vbRead"]
fn vb_read(sim: *mut VB, address: u32, typ_: VBDataType) -> i32;
#[link_name = "vbReset"]
fn vb_reset(sim: *mut VB);
#[link_name = "vbSetCartRAM"]
fn vb_set_cart_ram(sim: *mut VB, sram: *mut c_void, size: u32) -> c_int;
#[link_name = "vbSetCartROM"]
fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int;
#[link_name = "vbSetExceptionCallback"]
fn vb_set_exception_callback(
sim: *mut VB,
callback: Option<OnException>,
) -> Option<OnException>;
#[link_name = "vbSetExecuteCallback"]
fn vb_set_execute_callback(sim: *mut VB, callback: Option<OnExecute>) -> Option<OnExecute>;
#[link_name = "vbSetFetchCallback"]
fn vb_set_fetch_callback(sim: *mut VB, callback: Option<OnFetch>) -> Option<OnFetch>;
#[link_name = "vbSetFrameCallback"]
fn vb_set_frame_callback(sim: *mut VB, callback: Option<OnFrame>) -> Option<OnFrame>;
#[link_name = "vbSetKeys"]
fn vb_set_keys(sim: *mut VB, keys: u16) -> u16;
#[link_name = "vbSetOption"]
fn vb_set_option(sim: *mut VB, key: VBOption, value: c_int);
#[link_name = "vbSetPeer"]
fn vb_set_peer(sim: *mut VB, peer: *mut VB);
#[link_name = "vbSetProgramCounter"]
fn vb_set_program_counter(sim: *mut VB, value: u32) -> u32;
#[link_name = "vbSetProgramRegister"]
fn vb_set_program_register(sim: *mut VB, index: c_uint, value: i32) -> i32;
#[link_name = "vbSetReadCallback"]
fn vb_set_read_callback(sim: *mut VB, callback: Option<OnRead>) -> Option<OnRead>;
#[link_name = "vbSetSamples"]
fn vb_set_samples(
sim: *mut VB,
samples: *mut c_void,
typ_: VBDataType,
capacity: c_uint,
) -> c_int;
#[link_name = "vbSetSystemRegister"]
fn vb_set_system_register(sim: *mut VB, index: c_uint, value: u32) -> u32;
#[link_name = "vbSetUserData"]
fn vb_set_user_data(sim: *mut VB, tag: *mut c_void);
#[link_name = "vbSetWriteCallback"]
fn vb_set_write_callback(sim: *mut VB, callback: Option<OnWrite>) -> Option<OnWrite>;
#[link_name = "vbSizeOf"]
fn vb_size_of() -> usize;
#[link_name = "vbWrite"]
fn vb_write(sim: *mut VB, address: u32, _type: VBDataType, value: i32) -> i32;
}
#[unsafe(no_mangle)]
extern "C" fn on_frame(sim: *mut VB) -> c_int {
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
data.frame_seen = true;
if data.monitor.enabled {
data.monitor.event = Some(SimEvent::Marker(Cow::Borrowed("Frame Drawn")));
}
1
}
#[unsafe(no_mangle)]
extern "C" fn on_execute(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> c_int {
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
if data.monitor.enabled {
// SAFETY: length is the length of code, in elements
let code = unsafe { slice::from_raw_parts(code, length as usize) };
data.monitor.detect_event(sim, address, code);
// Something interesting will happen after this instruction is run.
// We'll react in the on_fetch callback it does.
}
let mut stopped = data.stop_reason.is_some() || data.monitor.event.is_some();
if data.step_from.is_some_and(|s| s != address) {
data.step_from = None;
data.stop_reason = Some(StopReason::Stepped);
stopped = true;
}
if data.breakpoints.binary_search(&address).is_ok() {
data.stop_reason = Some(StopReason::Breakpoint);
stopped = true;
}
if stopped { 1 } else { 0 }
}
#[unsafe(no_mangle)]
extern "C" fn on_fetch(
sim: *mut VB,
_fetch: c_int,
address: u32,
_value: *mut i32,
_cycles: *mut u32,
) -> c_int {
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
data.monitor.event = data.monitor.queued_event.take();
data.monitor.new_inline_stack = data.monitor.detect_new_inline_stack(address);
unsafe { vb_set_exception_callback(sim, Some(on_exception)) };
if data.monitor.event.is_some() || data.monitor.new_inline_stack.is_some() {
1
} else {
0
}
}
#[unsafe(no_mangle)]
extern "C" fn on_exception(sim: *mut VB, cause: *mut u16) -> c_int {
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
let cause = unsafe { *cause };
let pc = if cause == 0xff70 {
0xffffff60
} else {
(cause & 0xfff0) as u32 | 0xffff0000
};
data.monitor.event = data.monitor.queued_event.take();
data.monitor.new_inline_stack = data.monitor.detect_new_inline_stack(pc);
data.monitor.queued_event = Some(SimEvent::Interrupt(cause, pc & 0x07ffffff));
unsafe { vb_set_exception_callback(sim, None) };
if data.monitor.event.is_some() || data.monitor.new_inline_stack.is_some() {
1
} else {
0
}
}
#[unsafe(no_mangle)]
extern "C" fn on_read(
sim: *mut VB,
address: u32,
_type: VBDataType,
_value: *mut i32,
_cycles: *mut u32,
) -> c_int {
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
if let Some(start) = data.read_watchpoints.start_of_range_containing(address) {
let watch = if data.write_watchpoints.contains(address) {
VBWatchpointType::Access
} else {
VBWatchpointType::Read
};
data.stop_reason = Some(StopReason::Watchpoint(watch, start));
}
// Don't stop here, the debugger expects us to break after the memory access.
// We'll stop in on_execute instead.
0
}
#[unsafe(no_mangle)]
extern "C" fn on_write(
sim: *mut VB,
address: u32,
typ_: VBDataType,
value: *mut i32,
_cycles: *mut u32,
_cancel: *mut c_int,
) -> c_int {
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
// If we're monitoring stdout, track this write
if let Some(stdout) = data.stdout.as_mut() {
let normalized_hw_address = address & 0x0700003f;
if normalized_hw_address == 0x02000030 {
stdout.push(unsafe { *value } as u8);
}
}
// If we have profiling enabled, track custom markers
if data.monitor.enabled {
let normalized_hw_address = address & 0x0700003f;
if normalized_hw_address == 0x02000038 && matches!(typ_, VBDataType::S32) {
assert!(data.monitor.queued_event.is_none());
// The game has written the address of a null-terminated string
// (whose length is at most 64 bytes). Read that string.
let str_address = unsafe { *value } as u32;
let mut bytes = [0u8; 64];
let mut len = 0;
for (dst, src_address) in bytes.iter_mut().zip(str_address..str_address + 64) {
let char = unsafe { vb_read(sim, src_address, VBDataType::U8) } as u8;
if char == 0 {
break;
}
*dst = char;
len += 1;
}
let name = String::from_utf8_lossy(&bytes[..len]).into_owned();
data.monitor.queued_event = Some(SimEvent::Marker(Cow::Owned(name)));
}
}
if let Some(start) = data.write_watchpoints.start_of_range_containing(address) {
let watch = if data.read_watchpoints.contains(address) {
VBWatchpointType::Access
} else {
VBWatchpointType::Write
};
data.stop_reason = Some(StopReason::Watchpoint(watch, start));
}
// Don't stop here, the debugger expects us to break after the memory access.
// We'll stop in on_execute instead.
0
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum SimEvent {
Call(u32),
Return,
Halt,
Interrupt(u16, u32),
Reti,
Marker(Cow<'static, str>),
}
struct EventMonitor {
enabled: bool,
event: Option<SimEvent>,
queued_event: Option<SimEvent>,
just_halted: bool,
inline_stack_map: InlineStackMap,
new_inline_stack: Option<InlineStack>,
last_inline_stack: InlineStack,
}
impl EventMonitor {
fn new() -> Self {
let inline_stack_map = InlineStackMap::empty();
let last_inline_stack = inline_stack_map.get(0).clone();
Self {
enabled: false,
event: None,
queued_event: None,
just_halted: false,
inline_stack_map,
new_inline_stack: None,
last_inline_stack,
}
}
fn detect_new_inline_stack(&mut self, address: u32) -> Option<InlineStack> {
let stack = self.inline_stack_map.get(address);
if Arc::ptr_eq(stack, &self.last_inline_stack) {
return None;
}
self.last_inline_stack = stack.clone();
Some(stack.clone())
}
fn detect_event(&mut self, sim: *mut VB, address: u32, code: &[u16]) -> bool {
self.queued_event = self.do_detect_event(sim, address, code);
self.queued_event.is_some()
}
fn do_detect_event(&mut self, sim: *mut VB, address: u32, code: &[u16]) -> Option<SimEvent> {
const HALT_OPCODE: u16 = 0b011010;
const JAL_OPCODE: u16 = 0b101011;
const JMP_OPCODE: u16 = 0b000110;
const RETI_OPCODE: u16 = 0b011001;
const fn format_i_reg_1(code: &[u16]) -> u8 {
(code[0] & 0x1f) as u8
}
const fn format_iv_disp(code: &[u16]) -> i32 {
let value = ((code[0] & 0x3ff) as i32) << 16 | (code[1] as i32);
value << 6 >> 6
}
let opcode = code[0] >> 10;
if opcode == HALT_OPCODE {
if !self.just_halted {
self.just_halted = true;
self.event = Some(SimEvent::Halt);
} else {
self.just_halted = false;
}
// Don't _return_ an event, we want to emit this right away.
// If the CPU is halting, no other callbacks will run for a long time.
return None;
}
if opcode == JAL_OPCODE {
let disp = format_iv_disp(code);
if disp != 4 {
// JAL .+4 is how programs get r31 to a known value for indirect calls
// (which we detect later.)
// Any other JAL is a function call.
return Some(SimEvent::Call(
address.wrapping_add_signed(disp) & 0x07ffffff,
));
}
}
if opcode == JMP_OPCODE {
let jmp_reg = format_i_reg_1(code);
if jmp_reg == 31 {
// JMP[r31] is a return
return Some(SimEvent::Return);
}
let r31 = unsafe { vb_get_program_register(sim, 31) };
if r31 as u32 == address.wrapping_add(2) {
// JMP anywhere else, if r31 points to after the JMP, is an indirect call
let target = unsafe { vb_get_program_register(sim, jmp_reg as u32) };
return Some(SimEvent::Call(target as u32 & 0x07ffffff));
}
}
if opcode == RETI_OPCODE {
return Some(SimEvent::Reti);
}
None
}
}
const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4;
const AUDIO_CAPACITY_FLOATS: usize = AUDIO_CAPACITY_SAMPLES * 2;
pub const EXPECTED_FRAME_SIZE: usize = 834 * 2;
struct VBState {
frame_seen: bool,
stop_reason: Option<StopReason>,
monitor: EventMonitor,
step_from: Option<u32>,
breakpoints: Vec<u32>,
read_watchpoints: AddressSet,
write_watchpoints: AddressSet,
stdout: Option<Vec<u8>>,
}
impl VBState {
fn needs_execute_callback(&self) -> bool {
self.step_from.is_some()
|| self.monitor.enabled
|| !self.breakpoints.is_empty()
|| !self.read_watchpoints.is_empty()
|| !self.write_watchpoints.is_empty()
}
fn needs_write_callback(&self) -> bool {
self.stdout.is_some() || self.monitor.enabled || !self.write_watchpoints.is_empty()
}
}
pub enum StopReason {
Breakpoint,
Watchpoint(VBWatchpointType, u32),
Stepped,
}
#[repr(transparent)]
pub struct Sim {
sim: *mut VB,
}
impl Sim {
pub fn new() -> Self {
// init the VB instance itself
let size = unsafe { vb_size_of() };
// allocate a vec of u64 so that this memory is 8-byte aligned
let memory = vec![0u64; size.div_ceil(4)];
let sim: *mut VB = Box::into_raw(memory.into_boxed_slice()).cast();
unsafe { vb_init(sim) };
// pseudohalt is disabled due to breaking red alarm
unsafe { vb_set_option(sim, VBOption::PseudoHalt, 0) };
unsafe { vb_set_keys(sim, VBKey::SGN.bits()) };
unsafe { vb_reset(sim) };
// set up userdata
let state = VBState {
frame_seen: false,
stop_reason: None,
monitor: EventMonitor::new(),
step_from: None,
breakpoints: vec![],
read_watchpoints: AddressSet::new(),
write_watchpoints: AddressSet::new(),
stdout: None,
};
unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) };
unsafe { vb_set_frame_callback(sim, Some(on_frame)) };
// set up audio buffer
let audio_buffer = vec![0.0f32; AUDIO_CAPACITY_FLOATS];
let samples: *mut c_void = Box::into_raw(audio_buffer.into_boxed_slice()).cast();
unsafe { vb_set_samples(sim, samples, VBDataType::F32, AUDIO_CAPACITY_SAMPLES as u32) };
Sim { sim }
}
pub fn reset(&mut self) {
unsafe { vb_reset(self.sim) };
}
pub fn monitor_events(&mut self, enabled: bool, inline_stack_map: InlineStackMap) {
let state = self.get_state();
state.monitor.enabled = enabled;
state.monitor.event = None;
state.monitor.queued_event = None;
state.monitor.new_inline_stack = None;
state.monitor.last_inline_stack = inline_stack_map.get(0).clone();
state.monitor.inline_stack_map = inline_stack_map;
if enabled {
unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) };
unsafe { vb_set_exception_callback(self.sim, Some(on_exception)) };
unsafe { vb_set_fetch_callback(self.sim, Some(on_fetch)) };
unsafe { vb_set_write_callback(self.sim, Some(on_write)) };
} else {
let needs_execute = state.needs_execute_callback();
let needs_write = state.needs_write_callback();
if !needs_execute {
unsafe { vb_set_execute_callback(self.sim, None) };
}
unsafe { vb_set_exception_callback(self.sim, None) };
unsafe { vb_set_fetch_callback(self.sim, None) };
if !needs_write {
unsafe { vb_set_write_callback(self.sim, None) };
}
}
}
pub fn load_cart(&mut self, mut rom: Vec<u8>, mut sram: Vec<u8>) -> Result<()> {
self.unload_cart();
rom.shrink_to_fit();
sram.shrink_to_fit();
let size = rom.len() as u32;
let rom = Box::into_raw(rom.into_boxed_slice()).cast();
let status = unsafe { vb_set_cart_rom(self.sim, rom, size) };
if status != 0 {
let _: Vec<u8> =
unsafe { Vec::from_raw_parts(rom.cast(), size as usize, size as usize) };
return Err(anyhow!("Invalid ROM size of {} bytes", size));
}
let size = sram.len() as u32;
let sram = Box::into_raw(sram.into_boxed_slice()).cast();
let status = unsafe { vb_set_cart_ram(self.sim, sram, size) };
if status != 0 {
let _: Vec<u8> =
unsafe { Vec::from_raw_parts(sram.cast(), size as usize, size as usize) };
return Err(anyhow!("Invalid SRAM size of {} bytes", size));
}
Ok(())
}
fn unload_cart(&mut self) {
let mut size = 0;
let rom = unsafe { vb_get_cart_rom(self.sim, &mut size) };
unsafe { vb_set_cart_rom(self.sim, ptr::null_mut(), 0) };
if !rom.is_null() {
let _: Vec<u8> =
unsafe { Vec::from_raw_parts(rom.cast(), size as usize, size as usize) };
}
let sram = unsafe { vb_get_cart_ram(self.sim, &mut size) };
unsafe { vb_set_cart_ram(self.sim, ptr::null_mut(), 0) };
if !sram.is_null() {
let _: Vec<u8> =
unsafe { Vec::from_raw_parts(sram.cast(), size as usize, size as usize) };
}
}
pub fn read_sram(&mut self, buffer: &mut [u8]) {
let mut size = 0;
let sram = unsafe { vb_get_cart_ram(self.sim, &mut size) };
if sram.is_null() {
return;
}
let bytes = unsafe { slice::from_raw_parts(sram.cast(), size as usize) };
buffer.copy_from_slice(bytes);
}
pub fn link(&mut self, peer: &mut Sim) {
unsafe { vb_set_peer(self.sim, peer.sim) };
}
pub fn unlink(&mut self) {
unsafe { vb_set_peer(self.sim, ptr::null_mut()) };
}
pub fn emulate(&mut self, cycles: &mut u32) {
unsafe { vb_emulate(self.sim, cycles) };
}
pub fn emulate_many(sims: &mut [Sim], cycles: &mut u32) {
let count = sims.len() as c_uint;
let sims = sims.as_mut_ptr().cast();
unsafe { vb_emulate_ex(sims, count, cycles) };
}
pub fn read_pixels(&mut self, buffers: &mut [u8]) -> bool {
let data = self.get_state();
if !data.frame_seen {
return false;
}
data.frame_seen = false;
// the buffer must be big enough for our data
assert!(buffers.len() >= 384 * 224 * 2);
unsafe {
vb_get_pixels(
self.sim,
buffers.as_mut_ptr().cast(),
2,
384 * 2,
buffers.as_mut_ptr().offset(1).cast(),
2,
384 * 2,
);
};
true
}
pub fn read_samples(&mut self, samples: &mut Vec<f32>, weight: f32) {
let mut position = 0;
let ptr =
unsafe { vb_get_samples(self.sim, ptr::null_mut(), ptr::null_mut(), &mut position) };
// SAFETY: position is an offset in a buffer of (f32, f32). so, position * 2 is an offset in a buffer of f32.
let read_samples: &[f32] =
unsafe { slice::from_raw_parts(ptr.cast(), position as usize * 2) };
samples.resize(read_samples.len(), 0.0);
for (index, sample) in read_samples.iter().enumerate() {
samples[index] += sample * weight;
}
unsafe {
vb_set_samples(
self.sim,
ptr,
VBDataType::F32,
AUDIO_CAPACITY_SAMPLES as u32,
)
};
}
pub fn set_keys(&mut self, keys: VBKey) {
unsafe { vb_set_keys(self.sim, keys.bits()) };
}
pub fn read_register(&mut self, register: VBRegister) -> u32 {
match register {
VBRegister::Program(index) => unsafe {
vb_get_program_register(self.sim, index) as u32
},
VBRegister::System(index) => unsafe { vb_get_system_register(self.sim, index) as u32 },
VBRegister::PC => unsafe { vb_get_program_counter(self.sim) },
}
}
pub fn write_register(&mut self, register: VBRegister, value: u32) {
match register {
VBRegister::Program(index) => unsafe {
vb_set_program_register(self.sim, index, value as i32);
},
VBRegister::System(index) => unsafe {
vb_set_system_register(self.sim, index, value);
},
VBRegister::PC => unsafe {
vb_set_program_counter(self.sim, value);
},
}
}
pub fn read_memory(&mut self, start: u32, length: usize, into: &mut Vec<u8>) {
let mut address = start;
for _ in 0..length {
let byte = unsafe { vb_read(self.sim, address, VBDataType::U8) };
into.push(byte as u8);
address = address.wrapping_add(1);
}
}
pub fn write_memory(&mut self, start: u32, buffer: &[u8]) {
let mut address = start;
for byte in buffer {
unsafe { vb_write(self.sim, address, VBDataType::U8, *byte as i32) };
address = address.wrapping_add(1);
}
}
pub fn add_breakpoint(&mut self, address: u32) {
let data = self.get_state();
if let Err(index) = data.breakpoints.binary_search(&address) {
data.breakpoints.insert(index, address);
}
unsafe {
vb_set_execute_callback(self.sim, Some(on_execute));
}
}
pub fn remove_breakpoint(&mut self, address: u32) {
let data = self.get_state();
if let Ok(index) = data.breakpoints.binary_search(&address) {
data.breakpoints.remove(index);
if !data.needs_execute_callback() {
unsafe { vb_set_execute_callback(self.sim, None) };
}
}
}
pub fn add_watchpoint(&mut self, address: u32, length: usize, watch: VBWatchpointType) {
match watch {
VBWatchpointType::Read => self.add_read_watchpoint(address, length),
VBWatchpointType::Write => self.add_write_watchpoint(address, length),
VBWatchpointType::Access => {
self.add_read_watchpoint(address, length);
self.add_write_watchpoint(address, length);
}
}
}
fn add_read_watchpoint(&mut self, address: u32, length: usize) {
let state = self.get_state();
state.read_watchpoints.add(address, length);
if !state.read_watchpoints.is_empty() {
unsafe { vb_set_read_callback(self.sim, Some(on_read)) };
unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) };
}
}
fn add_write_watchpoint(&mut self, address: u32, length: usize) {
let state = self.get_state();
state.write_watchpoints.add(address, length);
if !state.write_watchpoints.is_empty() {
unsafe { vb_set_write_callback(self.sim, Some(on_write)) };
unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) };
}
}
pub fn remove_watchpoint(&mut self, address: u32, length: usize, watch: VBWatchpointType) {
match watch {
VBWatchpointType::Read => self.remove_read_watchpoint(address, length),
VBWatchpointType::Write => self.remove_write_watchpoint(address, length),
VBWatchpointType::Access => {
self.remove_read_watchpoint(address, length);
self.remove_write_watchpoint(address, length);
}
}
}
fn remove_read_watchpoint(&mut self, address: u32, length: usize) {
let state = self.get_state();
state.read_watchpoints.remove(address, length);
let needs_execute = state.needs_execute_callback();
if state.read_watchpoints.is_empty() {
unsafe { vb_set_read_callback(self.sim, None) };
if !needs_execute {
unsafe { vb_set_execute_callback(self.sim, None) };
}
}
}
fn remove_write_watchpoint(&mut self, address: u32, length: usize) {
let state = self.get_state();
state.write_watchpoints.remove(address, length);
let needs_write = state.needs_write_callback();
let needs_execute = state.needs_execute_callback();
if state.write_watchpoints.is_empty() {
if !needs_write {
unsafe { vb_set_write_callback(self.sim, None) };
}
if !needs_execute {
unsafe { vb_set_execute_callback(self.sim, None) };
}
}
}
pub fn step(&mut self) {
let current_pc = unsafe { vb_get_program_counter(self.sim) };
let data = self.get_state();
data.step_from = Some(current_pc);
unsafe {
vb_set_execute_callback(self.sim, Some(on_execute));
}
}
pub fn clear_debug_state(&mut self) {
let data = self.get_state();
data.step_from = None;
data.breakpoints.clear();
data.read_watchpoints.clear();
data.write_watchpoints.clear();
let needs_write = data.needs_write_callback();
let needs_execute = data.needs_execute_callback();
unsafe { vb_set_read_callback(self.sim, None) };
if !needs_write {
unsafe { vb_set_write_callback(self.sim, None) };
}
if !needs_execute {
unsafe { vb_set_execute_callback(self.sim, None) };
}
}
pub fn watch_stdout(&mut self, watch: bool) {
let data = self.get_state();
if watch {
if data.stdout.is_none() {
data.stdout = Some(vec![]);
unsafe { vb_set_write_callback(self.sim, Some(on_write)) };
}
} else {
data.stdout.take();
if !data.needs_write_callback() {
unsafe { vb_set_write_callback(self.sim, None) };
}
}
}
pub fn take_stdout(&mut self) -> Option<String> {
let data = self.get_state();
let stdout = data.stdout.take()?;
let string = match String::from_utf8(stdout) {
Ok(str) => str,
Err(err) => String::from_utf8_lossy(err.as_bytes()).into_owned(),
};
data.stdout = Some(vec![]);
Some(string)
}
pub fn take_stop_reason(&mut self) -> Option<StopReason> {
let data = self.get_state();
let reason = data.stop_reason.take();
if !data.needs_execute_callback() {
unsafe { vb_set_execute_callback(self.sim, None) };
}
reason
}
pub fn take_profiler_updates(&mut self) -> (Option<SimEvent>, Option<InlineStack>) {
let data = self.get_state();
let event = data.monitor.event.take();
let inline_stack = data.monitor.new_inline_stack.take();
(event, inline_stack)
}
fn get_state(&mut self) -> &mut VBState {
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.
unsafe { &mut *vb_get_user_data(self.sim).cast() }
}
}
impl Drop for Sim {
fn drop(&mut self) {
let ptr =
unsafe { vb_get_samples(self.sim, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()) };
// SAFETY: the audio buffer originally came from a Vec<u32>
let floats: Vec<f32> = unsafe {
Vec::from_raw_parts(ptr.cast(), AUDIO_CAPACITY_FLOATS, AUDIO_CAPACITY_FLOATS)
};
drop(floats);
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.
let ptr: *mut VBState = unsafe { vb_get_user_data(self.sim).cast() };
// SAFETY: we made this pointer ourselves, we can for sure free it
unsafe { drop(Box::from_raw(ptr)) };
// If we're linked to another sim, unlink from them.
unsafe { vb_set_peer(self.sim, ptr::null_mut()) };
let len = unsafe { vb_size_of() }.div_ceil(4);
// SAFETY: the sim's memory originally came from a Vec<u64>
let bytes: Vec<u64> = unsafe { Vec::from_raw_parts(self.sim.cast(), len, len) };
drop(bytes);
}
}
+31
View File
@@ -0,0 +1,31 @@
use std::ffi::c_void;
#[unsafe(no_mangle)]
unsafe fn vbu_realloc_shim(ptr: *mut c_void, new_size: usize) -> *mut c_void {
if !ptr.is_null() {
// not supporting proper realloc because it needs bookkeeping and it's unnecessary
return std::ptr::null_mut();
}
let allocation = vec![0u8; new_size].into_boxed_slice();
Box::into_raw(allocation).cast()
}
#[link(name = "vb")]
unsafe extern "C" {
#[link_name = "vbuFromISX"]
fn vbu_from_isx(bytes: *const c_void, length: usize, rom_length: *mut usize) -> *mut c_void;
}
pub fn rom_from_isx(bytes: &[u8]) -> Option<Vec<u8>> {
if !bytes.starts_with(b"ISX") {
return None;
}
let mut rom_length = 0;
let raw_rom = unsafe { vbu_from_isx(bytes.as_ptr().cast(), bytes.len(), &mut rom_length) };
if raw_rom.is_null() {
return None;
}
// SAFETY: the rom was allocated by vbu_realloc_shim, which created it from a Vec<u8>.
let rom = unsafe { Vec::from_raw_parts(raw_rom.cast(), rom_length, rom_length) };
Some(rom)
}
+103
View File
@@ -0,0 +1,103 @@
#[cfg(target_os = "linux")]
use std::fmt::Write as _;
use std::{
path::PathBuf,
sync::{Arc, Weak},
};
use winit::window::Window;
pub struct FilePicker {
window: Option<Weak<Window>>,
}
impl FilePicker {
pub fn new() -> Self {
Self { window: None }
}
pub fn set_window(&mut self, window: &Arc<Window>) {
self.window = Some(Arc::downgrade(window));
}
pub fn pick_file_dialog(&self) -> FileDialogBuilder {
self.new_dialog(FileDialogAction::PickFile, false)
}
pub fn save_file_dialog(&self) -> FileDialogBuilder {
self.new_dialog(FileDialogAction::SaveFile, cfg!(not(target_os = "windows")))
}
fn new_dialog(&self, action: FileDialogAction, attach_window: bool) -> FileDialogBuilder {
#[cfg(not(target_os = "macos"))]
let mut dialog = rfd::AsyncFileDialog::new();
#[cfg(target_os = "macos")]
let mut dialog = rfd::FileDialog::new();
if attach_window && let Some(window) = self.window.as_ref().and_then(|w| w.upgrade()) {
dialog = dialog.set_parent(&window);
}
FileDialogBuilder { dialog, action }
}
}
pub struct FileDialogBuilder {
#[cfg(not(target_os = "macos"))]
dialog: rfd::AsyncFileDialog,
#[cfg(target_os = "macos")]
dialog: rfd::FileDialog,
action: FileDialogAction,
}
impl FileDialogBuilder {
pub fn add_filter(self, name: impl Into<String>, extensions: &[impl ToString]) -> Self {
#[cfg(target_os = "linux")]
let exts = extensions
.iter()
.map(|ext| {
let mut normalized = String::new();
for char in ext.to_string().chars() {
if char.is_ascii_lowercase() {
let _ = write!(&mut normalized, "[{char}{}]", char.to_ascii_uppercase());
} else {
normalized.push(char);
}
}
normalized
})
.collect::<Vec<_>>();
#[cfg(target_os = "linux")]
let extensions = &exts;
Self {
dialog: self.dialog.add_filter(name, extensions),
..self
}
}
pub fn set_file_name(self, file_name: impl Into<String>) -> Self {
Self {
dialog: self.dialog.set_file_name(file_name),
..self
}
}
pub fn open(self) -> Option<PathBuf> {
#[cfg(not(target_os = "macos"))]
fn resolve(action: impl Future<Output = Option<rfd::FileHandle>>) -> Option<PathBuf> {
pollster::block_on(action).map(Into::into)
}
#[cfg(target_os = "macos")]
fn resolve(action: Option<PathBuf>) -> Option<PathBuf> {
action
}
match self.action {
FileDialogAction::PickFile => resolve(self.dialog.pick_file()),
FileDialogAction::SaveFile => resolve(self.dialog.save_file()),
}
}
}
enum FileDialogAction {
PickFile,
SaveFile,
}
+79
View File
@@ -0,0 +1,79 @@
// Vertex shader
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
};
@vertex
fn vs_main(
@builtin(vertex_index) in_vertex_index: u32,
) -> VertexOutput {
var out: VertexOutput;
var x: f32;
var y: f32;
switch in_vertex_index {
case 0u, 3u: {
x = -1.0;
y = 1.0;
}
case 1u: {
x = -1.0;
y = -1.0;
}
case 2u, 4u: {
x = 1.0;
y = -1.0;
}
default: {
x = 1.0;
y = 1.0;
}
}
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.tex_coords = vec2<f32>((x + 1.0) / 2.0, (1.0 - y) / 2.0);
return out;
}
// Fragment shader
@group(0) @binding(0)
var u_texture: texture_2d<f32>;
@group(0) @binding(1)
var u_sampler: sampler;
struct Colors {
left: vec4<f32>,
right: vec4<f32>,
};
@group(0) @binding(2)
var<uniform> colors: Colors;
@fragment
fn fs_lefteye(in: VertexOutput) -> @location(0) vec4<f32> {
let brt = textureSample(u_texture, u_sampler, in.tex_coords);
return colors.left * brt[0];
}
@fragment
fn fs_righteye(in: VertexOutput) -> @location(0) vec4<f32> {
let brt = textureSample(u_texture, u_sampler, in.tex_coords);
return colors.left * brt[1];
}
@fragment
fn fs_anaglyph(in: VertexOutput) -> @location(0) vec4<f32> {
let brt = textureSample(u_texture, u_sampler, in.tex_coords);
return colors.left * brt[0] + colors.right * brt[1];
}
@fragment
fn fs_sidebyside(in: VertexOutput) -> @location(0) vec4<f32> {
var point = in.tex_coords;
point.x = (point.x * 2.0) % 1.0;
let brt = textureSample(u_texture, u_sampler, point);
if in.tex_coords.x < 0.5 {
return colors.left * brt[0];
} else {
return colors.left * brt[1];
};
}
+560
View File
@@ -0,0 +1,560 @@
use anyhow::{Result, bail};
use registers::REGISTERS;
use request::{Request, RequestKind, RequestSource};
use response::Response;
use std::{
sync::{Arc, Mutex},
thread,
};
use tokio::{
io::{AsyncWriteExt as _, BufReader},
net::{TcpListener, TcpStream},
pin, select,
sync::{mpsc, oneshot},
};
use tracing::{Level, debug, enabled, error, info};
use crate::emulator::{
DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId, VBWatchpointType,
};
mod registers;
mod request;
mod response;
pub struct GdbServer {
sim_id: SimId,
client: EmulatorClient,
status: Arc<Mutex<GdbServerStatus>>,
killer: Option<oneshot::Sender<()>>,
}
impl GdbServer {
pub fn new(sim_id: SimId, client: EmulatorClient) -> Self {
Self {
sim_id,
client,
status: Arc::new(Mutex::new(GdbServerStatus::Stopped)),
killer: None,
}
}
pub fn status(&self) -> GdbServerStatus {
self.status.lock().unwrap().clone()
}
pub fn start(&mut self, port: u16) {
*self.status.lock().unwrap() = GdbServerStatus::Connecting;
let sim_id = self.sim_id;
let client = self.client.clone();
let status = self.status.clone();
let (tx, rx) = oneshot::channel();
self.killer = Some(tx);
thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async move {
select! {
_ = run_server(sim_id, client.clone(), port, &status) => {}
_ = rx => {
client.send_command(EmulatorCommand::StopDebugging(sim_id));
*status.lock().unwrap() = GdbServerStatus::Stopped;
}
}
})
});
}
pub fn stop(&mut self) {
if let Some(killer) = self.killer.take() {
let _ = killer.send(());
}
}
}
impl Drop for GdbServer {
fn drop(&mut self) {
self.stop();
}
}
async fn run_server(
sim_id: SimId,
client: EmulatorClient,
port: u16,
status: &Mutex<GdbServerStatus>,
) {
let (debug_sink, mut debug_source) = mpsc::unbounded_channel();
client.send_command(EmulatorCommand::StartDebugging(sim_id, debug_sink));
info!("Connecting to debugger on port {port}...");
let connect_future = try_connect(port, status);
pin!(connect_future);
let stream = loop {
select! {
stream = &mut connect_future => {
if let Some(stream) = stream {
break stream;
} else {
return;
}
}
event = debug_source.recv() => {
if event.is_none() {
// The sim has stopped (or was never started)
*status.lock().unwrap() = GdbServerStatus::Stopped;
return;
}
}
}
};
info!("Connected!");
let mut connection = GdbConnection::new(sim_id, client);
match connection.run(stream, debug_source).await {
Ok(()) => {
info!("Finished debugging.");
*status.lock().unwrap() = GdbServerStatus::Stopped;
}
Err(error) => {
error!(%error, "Error from debugger.");
*status.lock().unwrap() = GdbServerStatus::Error(error.to_string());
}
}
}
async fn try_connect(port: u16, status: &Mutex<GdbServerStatus>) -> Option<TcpStream> {
*status.lock().unwrap() = GdbServerStatus::Connecting;
let listener = match TcpListener::bind(("127.0.0.1", port)).await {
Ok(l) => l,
Err(err) => {
error!(%err, "Could not open port.");
*status.lock().unwrap() = GdbServerStatus::Error(err.to_string());
return None;
}
};
match listener.accept().await {
Ok((stream, _)) => {
*status.lock().unwrap() = GdbServerStatus::Running;
Some(stream)
}
Err(err) => {
error!(%err, "Could not connect to debugger.");
*status.lock().unwrap() = GdbServerStatus::Error(err.to_string());
None
}
}
}
#[derive(Clone)]
pub enum GdbServerStatus {
Stopped,
Connecting,
Running,
Error(String),
}
impl GdbServerStatus {
pub fn running(&self) -> bool {
matches!(self, Self::Connecting | Self::Running)
}
}
struct GdbConnection {
sim_id: SimId,
client: EmulatorClient,
ack_messages: bool,
stop_reason: Option<DebugStopReason>,
response_buf: Option<Vec<u8>>,
memory_buf: Option<Vec<u8>>,
}
impl GdbConnection {
fn new(sim_id: SimId, client: EmulatorClient) -> Self {
Self {
sim_id,
client,
ack_messages: true,
stop_reason: None,
response_buf: None,
memory_buf: None,
}
}
async fn run(
&mut self,
stream: TcpStream,
mut debug_source: mpsc::UnboundedReceiver<DebugEvent>,
) -> Result<()> {
let (rx, mut tx) = stream.into_split();
let mut request_source = RequestSource::new(BufReader::new(rx));
loop {
let response = select! {
maybe_event = debug_source.recv() => {
let Some(event) = maybe_event else {
// debugger has stopped running
break;
};
self.handle_event(event)
}
maybe_request = request_source.recv() => {
let req = maybe_request?;
self.handle_request(req)?
}
};
if let Some(res) = response {
let buffer = res.finish();
if enabled!(Level::DEBUG) {
match std::str::from_utf8(&buffer) {
Ok(text) => debug!("response: {text}"),
Err(_) => debug!("response: {buffer:02x?}"),
}
}
tx.write_all(&buffer).await?;
self.response_buf = Some(buffer);
tx.flush().await?;
}
}
Ok(())
}
fn handle_event(&mut self, event: DebugEvent) -> Option<Response> {
let res = match event {
DebugEvent::Stopped(reason) => {
if self.stop_reason.is_some_and(|r| r == reason) {
return None;
}
self.stop_reason = Some(reason);
self.response()
.write_str(&debug_stop_reason_string(self.stop_reason))
}
};
Some(res)
}
fn handle_request(&mut self, mut req: Request<'_>) -> Result<Option<Response>> {
debug!("received {:02x?}", req);
if req.kind == RequestKind::Signal {
self.client
.send_command(EmulatorCommand::DebugInterrupt(self.sim_id));
return Ok(None); // we'll send a message when the emulator reports it has stopped
}
let res = if req.match_str("QStartNoAckMode") {
let res = self.response().write_str("OK");
self.ack_messages = false;
res
} else if req.match_str("qSupported:") {
self.response()
.write_str("multiprocess+;swbreak+;vContSupported+;PacketSize=10000;SupportedWatchpointTypes=x86_64,aarch64-bas,aarch64-mask")
} else if req
.match_some_str([
"QThreadSuffixSupported",
"QListThreadsInStopReply",
"QEnableErrorStrings",
])
.is_some()
{
self.response().write_str("OK")
} else if req.match_str("qHostInfo") {
self.response().write_str(&format!(
"triple:{};endian:little;ptrsize:4;",
hex::encode("v810-unknown-vb")
))
} else if req.match_str("qProcessInfo") {
self.response().write_str(&format!(
"pid:1;triple:{};endian:little;ptrsize:4;",
hex::encode("v810-unknown-vb")
))
} else if req.match_str("qRegisterInfo") {
let mut get_reg_info = || {
let register = req.match_hex::<usize>()?;
REGISTERS.get(register)
};
if let Some(reg_info) = get_reg_info() {
self.response().write_str(&reg_info.to_description())
} else {
self.response()
}
} else if req.match_str("vCont?") {
self.response().write_str("vCont;c;s;")
} else if req.match_str("qC") {
// The v810 has no threads, so report that the "current thread" is 1.
self.response().write_str("QCp1.t1")
} else if req.match_str("qfThreadInfo") {
self.response().write_str("mp1.t1")
} else if req.match_str("qsThreadInfo") {
self.response().write_str("l")
} else if req.match_str("k") {
bail!("debug process was killed");
} else if req.match_str("?") {
self.response()
.write_str(&debug_stop_reason_string(self.stop_reason))
} else if req.match_some_str(["c", "vCont;c:"]).is_some() {
self.client
.send_command(EmulatorCommand::DebugContinue(self.sim_id));
self.stop_reason = None;
// Don't send a response until we hit a breakpoint or get interrupted
return Ok(None);
} else if req.match_some_str(["s", "vCont;s:"]).is_some() {
self.client
.send_command(EmulatorCommand::DebugStep(self.sim_id));
self.stop_reason = None;
// Don't send a response until we hit a breakpoint or get interrupted
return Ok(None);
} else if req.match_str("p") {
let mut read_register = || {
let register_index = req.match_hex::<usize>()?;
let register = REGISTERS.get(register_index)?.to_vb_register();
let (tx, rx) = ::oneshot::channel();
self.client
.send_command(EmulatorCommand::ReadRegister(self.sim_id, register, tx));
rx.recv().ok()
};
if let Some(value) = read_register() {
self.response().write_hex(value)
} else {
self.response()
}
} else if req.match_str("P") {
let mut write_register = || {
let register_index = req.match_hex::<usize>()?;
let register = REGISTERS.get(register_index)?.to_vb_register();
if !req.match_str("=") {
return None;
}
let value = {
let mut buffer = [0; 4];
if !req.match_hex_bytes(&mut buffer) {
return None;
}
u32::from_le_bytes(buffer)
};
self.client.send_command(EmulatorCommand::WriteRegister(
self.sim_id,
register,
value,
));
Some(())
};
if let Some(()) = write_register() {
self.response().write_str("OK")
} else {
self.response()
}
} else if let Some(op) = req.match_some_str(["m", "x"]) {
let mut read_memory = || {
let (start, length) = parse_memory_range(&mut req)?;
let mut buf = self.memory_buf.take().unwrap_or_default();
buf.clear();
if length == 0 {
return Some(buf);
}
let (tx, rx) = ::oneshot::channel();
self.client.send_command(EmulatorCommand::ReadMemory(
self.sim_id,
start,
length,
buf,
tx,
));
rx.recv().ok()
};
if let Some(memory) = read_memory() {
let mut res = self.response();
if memory.is_empty() {
res = res.write_str("OK");
} else if op == "m" {
// send the hex-encoded byte stream
for byte in &memory {
res = res.write_hex(*byte);
}
} else {
// send the raw byte stream
for byte in &memory {
res = res.write_byte(*byte);
}
}
self.memory_buf = Some(memory);
res
} else {
self.response()
}
} else if let Some(op) = req.match_some_str(["M", "X"]) {
let mut write_memory = || {
let (start, length) = parse_memory_range(&mut req)?;
if length == 0 {
return Some(());
}
if !req.match_str(":") {
return None;
}
let mut buf = self.memory_buf.take().unwrap_or_default();
buf.resize(length, 0);
let successful_read = if op == "M" {
req.match_hex_bytes(&mut buf)
} else {
req.match_bytes(&mut buf)
};
if !successful_read {
self.memory_buf = Some(buf);
return None;
}
let (tx, rx) = ::oneshot::channel();
self.client
.send_command(EmulatorCommand::WriteMemory(self.sim_id, start, buf, tx));
let buf = rx.recv().ok()?;
self.memory_buf = Some(buf);
Some(())
};
if write_memory().is_some() {
self.response().write_str("OK")
} else {
self.response()
}
} else if req.match_str("Z") {
let mut parse_request = || {
let type_ = req.match_hex::<u8>()?;
if !req.match_str(",") {
return None;
}
let address = req.match_hex()?;
if type_ == 0 || type_ == 1 {
return Some(EmulatorCommand::AddBreakpoint(self.sim_id, address));
}
if !req.match_str(",") {
return None;
}
let length = req.match_hex()?;
let watch = match type_ {
2 => VBWatchpointType::Write,
3 => VBWatchpointType::Read,
4 => VBWatchpointType::Access,
_ => return None,
};
Some(EmulatorCommand::AddWatchpoint(
self.sim_id,
address,
length,
watch,
))
};
if let Some(command) = parse_request() {
self.client.send_command(command);
self.response().write_str("OK")
} else {
self.response()
}
} else if req.match_str("z") {
let mut parse_request = || {
let type_ = req.match_hex::<u8>()?;
if !req.match_str(",") {
return None;
}
let address = req.match_hex()?;
if type_ == 0 || type_ == 1 {
return Some(EmulatorCommand::RemoveBreakpoint(self.sim_id, address));
}
if !req.match_str(",") {
return None;
}
let length = req.match_hex()?;
let watch = match type_ {
2 => VBWatchpointType::Write,
3 => VBWatchpointType::Read,
4 => VBWatchpointType::Access,
_ => return None,
};
Some(EmulatorCommand::RemoveWatchpoint(
self.sim_id,
address,
length,
watch,
))
};
if let Some(command) = parse_request() {
self.client.send_command(command);
self.response().write_str("OK")
} else {
self.response()
}
} else {
// unrecognized command
self.response()
};
Ok(Some(res))
}
fn response(&mut self) -> Response {
Response::new(
self.response_buf.take().unwrap_or_default(),
self.ack_messages,
)
}
}
impl Drop for GdbConnection {
fn drop(&mut self) {
self.client
.send_command(EmulatorCommand::StopDebugging(self.sim_id));
}
}
// parse a memory range into a start and a length.
fn parse_memory_range(req: &mut Request<'_>) -> Option<(u32, usize)> {
let start = req.match_hex::<u64>()?;
if !req.match_str(",") {
return None;
};
let length = req.match_hex::<usize>()?;
let Ok(start) = u32::try_from(start) else {
// The v810 has a 32-bit address space.
// Addresses wrap within that space, but we don't need to implement that for 64-bit addresses.
// Just refuse to return any info for addresses above 0xffffffff.
return Some((0, 0));
};
let length = length.min((u32::MAX - start) as usize + 1);
Some((start, length))
}
fn debug_stop_reason_string(reason: Option<DebugStopReason>) -> String {
let mut result = String::new();
result += if reason.is_some_and(|r| r != DebugStopReason::Paused) {
"T05;"
} else {
"T00;"
};
if let Some(DebugStopReason::Breakpoint) = reason {
result += "swbreak;";
}
if let Some(DebugStopReason::Watchpoint(watch, address)) = reason {
result += match watch {
VBWatchpointType::Write => "watch:",
VBWatchpointType::Read => "rwatch:",
VBWatchpointType::Access => "awatch:",
};
result += &format!("{address:08x};");
}
result += "thread:p1.t1;threads:p1.t1;";
if let Some(reason) = reason {
result += "reason:";
result += match reason {
DebugStopReason::Trace => "trace;",
DebugStopReason::Breakpoint => "breakpoint;",
DebugStopReason::Watchpoint(_, _) => "watchpoint;",
DebugStopReason::Paused => "trap;",
};
}
if let Some(DebugStopReason::Watchpoint(_, address)) = reason {
result += "description:";
result += &hex::encode(address.to_string());
result += ";";
}
result
}
+130
View File
@@ -0,0 +1,130 @@
use crate::emulator::VBRegister;
pub struct RegisterInfo {
dwarf: u32,
name: &'static str,
set: &'static str,
alt_name: Option<&'static str>,
generic: Option<&'static str>,
}
impl RegisterInfo {
pub fn to_description(&self) -> String {
let mut string = format!("name:{}", self.name);
if let Some(alt) = self.alt_name {
string.push_str(&format!(";alt-name:{alt}"));
}
string.push_str(&format!(
";bitsize:32;offset:{};encoding:uint;format:hex;set:{};dwarf:{}",
self.dwarf * 4,
self.set,
self.dwarf
));
if let Some(generic) = self.generic {
string.push_str(&format!(";generic:{generic}"));
}
string
}
pub fn to_vb_register(&self) -> VBRegister {
match self.dwarf {
0..32 => VBRegister::Program(self.dwarf),
32..40 => VBRegister::System(self.dwarf - 32),
40..42 => VBRegister::System(self.dwarf - 16),
42..45 => VBRegister::System(self.dwarf - 13),
45 => VBRegister::PC,
other => panic!("unexpected DWARF register {other}"),
}
}
}
macro_rules! register {
($set:expr, $dwarf:expr, $name:expr) => {
RegisterInfo {
dwarf: $dwarf,
name: $name,
set: $set,
alt_name: None,
generic: None,
}
};
($set:expr, $dwarf:expr, $name:expr, alt: $alt:expr) => {
RegisterInfo {
dwarf: $dwarf,
name: $name,
set: $set,
alt_name: Some($alt),
generic: None,
}
};
($set:expr, $dwarf:expr, $name:expr, generic: $generic:expr) => {
RegisterInfo {
dwarf: $dwarf,
name: $name,
set: $set,
alt_name: None,
generic: Some($generic),
}
};
($set:expr, $dwarf:expr, $name:expr, alt: $alt:expr, generic: $generic:expr) => {
RegisterInfo {
dwarf: $dwarf,
name: $name,
set: $set,
alt_name: Some($alt),
generic: Some($generic),
}
};
}
const GENERAL: &str = "General Purpose Registers";
const SPECIAL: &str = "Special Registers";
pub const REGISTERS: [RegisterInfo; 46] = [
register!(GENERAL, 0, "r0"),
register!(GENERAL, 1, "r1"),
register!(GENERAL, 2, "fp", alt: "r2", generic: "fp"),
register!(GENERAL, 3, "sp", alt: "r3", generic: "sp"),
register!(GENERAL, 4, "gp", alt: "r4"),
register!(GENERAL, 5, "tp", alt: "r5"),
register!(GENERAL, 6, "r6", generic: "arg1"),
register!(GENERAL, 7, "r7", generic: "arg2"),
register!(GENERAL, 8, "r8", generic: "arg3"),
register!(GENERAL, 9, "r9", generic: "arg4"),
register!(GENERAL, 10, "r10"),
register!(GENERAL, 11, "r11"),
register!(GENERAL, 12, "r12"),
register!(GENERAL, 13, "r13"),
register!(GENERAL, 14, "r14"),
register!(GENERAL, 15, "r15"),
register!(GENERAL, 16, "r16"),
register!(GENERAL, 17, "r17"),
register!(GENERAL, 18, "r18"),
register!(GENERAL, 19, "r19"),
register!(GENERAL, 20, "r20"),
register!(GENERAL, 21, "r21"),
register!(GENERAL, 22, "r22"),
register!(GENERAL, 23, "r23"),
register!(GENERAL, 24, "r24"),
register!(GENERAL, 25, "r25"),
register!(GENERAL, 26, "r26"),
register!(GENERAL, 27, "r27"),
register!(GENERAL, 28, "r28"),
register!(GENERAL, 29, "r29"),
register!(GENERAL, 30, "r30"),
register!(GENERAL, 31, "lp", alt: "r31", generic: "ra"),
register!(SPECIAL, 32, "eipc", alt: "sr0"),
register!(SPECIAL, 33, "eipsw", alt: "sr1"),
register!(SPECIAL, 34, "fepc", alt: "sr2"),
register!(SPECIAL, 35, "fepsw", alt: "sr3"),
register!(SPECIAL, 36, "ecr", alt: "sr4"),
register!(SPECIAL, 37, "psw", alt: "sr5", generic: "flags"),
register!(SPECIAL, 38, "pir", alt: "sr6"),
register!(SPECIAL, 39, "tkcw", alt: "sr7"),
register!(SPECIAL, 40, "chcw", alt: "sr24"),
register!(SPECIAL, 41, "adtre", alt: "sr25"),
register!(SPECIAL, 42, "sr29"),
register!(SPECIAL, 43, "sr30"),
register!(SPECIAL, 44, "sr31"),
register!(SPECIAL, 45, "pc", generic: "pc"),
];
+202
View File
@@ -0,0 +1,202 @@
use anyhow::{Result, bail};
use atoi::FromRadix16;
use tokio::io::{AsyncRead, AsyncReadExt as _};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RequestKind {
Signal,
Command,
}
impl RequestKind {
fn name(self) -> &'static str {
match self {
Self::Signal => "Signal",
Self::Command => "Command",
}
}
}
pub struct Request<'a> {
pub kind: RequestKind,
buffer: &'a [u8],
}
impl std::fmt::Debug for Request<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut ds = f.debug_tuple(self.kind.name());
match self.kind {
RequestKind::Signal => ds.field(&self.buffer),
RequestKind::Command => match std::str::from_utf8(self.buffer) {
Ok(str) => ds.field(&str),
Err(_) => ds.field(&self.buffer),
},
};
ds.finish()
}
}
impl Request<'_> {
pub fn match_str(&mut self, prefix: &str) -> bool {
if let Some(new_buffer) = self.buffer.strip_prefix(prefix.as_bytes()) {
self.buffer = new_buffer;
return true;
}
false
}
pub fn match_some_str<'a, I: IntoIterator<Item = &'a str>>(
&mut self,
prefixes: I,
) -> Option<&'a str> {
prefixes.into_iter().find(|&prefix| self.match_str(prefix))
}
pub fn match_hex<I: FromRadix16>(&mut self) -> Option<I> {
match I::from_radix_16(self.buffer) {
(_, 0) => None,
(val, used) => {
self.buffer = self.buffer.split_at(used).1;
Some(val)
}
}
}
pub fn match_hex_bytes(&mut self, buffer: &mut [u8]) -> bool {
if self.buffer.len() < buffer.len() * 2 {
return false;
}
for (i, item) in buffer.iter_mut().enumerate() {
match u8::from_radix_16(&self.buffer[(i * 2)..(i * 2) + 2]) {
(byte, 2) => *item = byte,
_ => return false,
};
}
self.buffer = self.buffer.split_at(buffer.len()).1;
true
}
pub fn match_bytes(&mut self, buffer: &mut [u8]) -> bool {
if self.buffer.len() < buffer.len() {
return false;
}
buffer.copy_from_slice(&self.buffer[0..buffer.len()]);
self.buffer = self.buffer.split_at(buffer.len()).1;
true
}
}
pub struct RequestSource<R> {
reader: R,
buffer: Vec<u8>,
state: RequestReadState,
}
impl<R: AsyncRead + Unpin> RequestSource<R> {
pub fn new(reader: R) -> Self {
Self {
reader,
buffer: vec![],
state: RequestReadState::Header,
}
}
pub async fn recv(&mut self) -> Result<Request<'_>> {
let mut char = self.reader.read_u8().await?;
if matches!(self.state, RequestReadState::Start) {
self.buffer.clear();
self.state = RequestReadState::Header;
}
if matches!(self.state, RequestReadState::Header) {
// Just ignore positive acks
while char == b'+' {
char = self.reader.read_u8().await?;
}
if char == b'-' {
bail!("no support for negative acks");
}
if char == 0x03 {
// This is how the client "cancels an in-flight request"
self.buffer.push(char);
self.state = RequestReadState::Start;
return Ok(Request {
kind: RequestKind::Signal,
buffer: &self.buffer,
});
}
if char != b'$' {
// Messages are supposed to start with a dollar sign
bail!("malformed message");
}
self.state = RequestReadState::Body {
checksum: 0,
escaping: false,
};
char = self.reader.read_u8().await?;
}
while let RequestReadState::Body { checksum, escaping } = &mut self.state {
if char == b'#' && !*escaping {
self.state = RequestReadState::Checksum {
expected: *checksum,
actual: 0,
digits: 0,
};
char = self.reader.read_u8().await?;
break;
}
*checksum = checksum.wrapping_add(char);
if *escaping {
// escaped character
self.buffer.push(char ^ 0x20);
*escaping = false;
} else if char == b'}' {
// next character will be escaped
*escaping = true;
} else {
self.buffer.push(char);
}
char = self.reader.read_u8().await?;
}
while let RequestReadState::Checksum {
expected,
actual,
digits,
} = &mut self.state
{
let digit = match char {
b'0'..=b'9' => char - b'0',
b'a'..=b'f' => char - b'a' + 10,
b'A'..=b'F' => char - b'A' + 10,
_ => bail!("invalid checksum"),
};
*actual = (*actual << 4) + digit;
*digits += 1;
if *digits == 2 {
if *expected != *actual {
bail!("mismatched checksum");
}
self.state = RequestReadState::Start;
return Ok(Request {
kind: RequestKind::Command,
buffer: &self.buffer,
});
}
char = self.reader.read_u8().await?;
}
unreachable!();
}
}
enum RequestReadState {
Start,
Header,
Body {
checksum: u8,
escaping: bool,
},
Checksum {
expected: u8,
actual: u8,
digits: u8,
},
}
+63
View File
@@ -0,0 +1,63 @@
use num_traits::ToBytes;
pub struct Response {
buffer: Vec<u8>,
checksum: u8,
}
impl Response {
pub fn new(mut buffer: Vec<u8>, ack: bool) -> Self {
buffer.clear();
if ack {
buffer.push(b'+');
}
buffer.push(b'$');
Self {
buffer,
checksum: 0,
}
}
pub fn write_str(self, str: &str) -> Self {
let mut me = self;
for byte in str.as_bytes() {
me = me.write_byte(*byte);
}
me
}
pub fn write_byte(mut self, byte: u8) -> Self {
if byte == b'}' || byte == b'#' || byte == b'$' || byte == b'*' {
self.buffer.push(b'}');
self.checksum = self.checksum.wrapping_add(b'}');
let escaped = byte ^ 0x20;
self.buffer.push(escaped);
self.checksum = self.checksum.wrapping_add(escaped);
} else {
self.buffer.push(byte);
self.checksum = self.checksum.wrapping_add(byte);
}
self
}
pub fn write_hex<T: ToBytes>(mut self, value: T) -> Self {
for byte in value.to_le_bytes().as_ref() {
for digit in [(byte >> 4), (byte & 0xf)] {
let char = if digit > 9 {
b'a' + digit - 10
} else {
b'0' + digit
};
self.buffer.push(char);
self.checksum = self.checksum.wrapping_add(char);
}
}
self
}
pub fn finish(mut self) -> Vec<u8> {
let checksum = self.checksum;
self.buffer.push(b'#');
self.write_hex(checksum).buffer
}
}
+142
View File
@@ -0,0 +1,142 @@
use std::{
sync::{
Arc, Mutex, MutexGuard,
atomic::{AtomicU64, Ordering},
mpsc,
},
thread,
};
use anyhow::{Result, bail};
use itertools::Itertools as _;
use wgpu::{
Device, Extent3d, Origin3d, Queue, TexelCopyBufferLayout, TexelCopyTextureInfo, Texture,
TextureDescriptor, TextureFormat, TextureUsages, TextureView, TextureViewDescriptor,
};
#[derive(Debug)]
pub struct TextureSink {
buffers: Arc<BufferPool>,
sink: mpsc::Sender<u64>,
}
impl TextureSink {
pub fn new(device: &Device, queue: Queue) -> (Self, TextureView) {
let texture = Self::create_texture(device);
let view = texture.create_view(&TextureViewDescriptor::default());
let buffers = Arc::new(BufferPool::new());
let (sink, source) = mpsc::channel();
let bufs = buffers.clone();
thread::spawn(move || {
let mut local_buf = vec![0; 384 * 224 * 2];
while let Ok(id) = source.recv() {
{
let Some(bytes) = bufs.read(id) else {
continue;
};
local_buf.copy_from_slice(bytes.as_slice());
}
Self::write_texture(&queue, &texture, local_buf.as_slice());
}
});
let sink = Self { buffers, sink };
(sink, view)
}
pub fn queue_render(&mut self, bytes: &[u8]) -> Result<()> {
let id = {
let (mut buf, id) = self.buffers.write()?;
buf.copy_from_slice(bytes);
id
};
self.sink.send(id)?;
Ok(())
}
fn create_texture(device: &Device) -> Texture {
let desc = TextureDescriptor {
label: Some("eyes"),
size: Extent3d {
width: 384,
height: 224,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TextureFormat::Rg8Unorm,
usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING,
view_formats: &[TextureFormat::Rg8Unorm],
};
device.create_texture(&desc)
}
fn write_texture(queue: &Queue, texture: &Texture, bytes: &[u8]) {
let texture = TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
};
let size = Extent3d {
width: 384,
height: 224,
depth_or_array_layers: 1,
};
let data_layout = TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(384 * 2),
rows_per_image: Some(224),
};
queue.write_texture(texture, bytes, data_layout, size);
}
}
#[derive(Debug)]
struct BufferPool {
buffers: [Buffer; 3],
}
impl BufferPool {
fn new() -> Self {
Self {
buffers: std::array::from_fn(|i| Buffer::new(i as u64)),
}
}
fn read(&self, id: u64) -> Option<MutexGuard<'_, Vec<u8>>> {
let buf = self
.buffers
.iter()
.find(|buf| buf.id.load(Ordering::Acquire) == id)?;
buf.data.lock().ok()
}
fn write(&self) -> Result<(MutexGuard<'_, Vec<u8>>, u64)> {
let (min, max) = self
.buffers
.iter()
.minmax_by_key(|buf| buf.id.load(Ordering::Acquire))
.into_option()
.unwrap();
let Ok(lock) = min.data.lock() else {
bail!("lock was poisoned")
};
let id = max.id.load(Ordering::Acquire) + 1;
min.id.store(id, Ordering::Release);
Ok((lock, id))
}
}
#[derive(Debug)]
struct Buffer {
data: Mutex<Vec<u8>>,
id: AtomicU64,
}
impl Buffer {
fn new(id: u64) -> Self {
Self {
data: Mutex::new(vec![0; 384 * 224 * 2]),
id: AtomicU64::new(id),
}
}
}
+337
View File
@@ -0,0 +1,337 @@
use std::{
collections::{HashMap, hash_map::Entry},
ops::Deref,
sync::{Arc, Mutex, Weak},
thread,
time::Duration,
};
use egui::{
Color32, ColorImage, TextureHandle,
epaint::ImageDelta,
generate_loader_id,
load::{LoadError, SizedTexture, TextureLoader, TexturePoll},
};
use tokio::{sync::mpsc, time::timeout};
use crate::emulator::SimId;
pub struct ImageTextureLoader {
cache: Mutex<HashMap<String, ImageEntry>>,
sender: mpsc::UnboundedSender<Box<dyn ImageRendererImpl>>,
}
impl ImageTextureLoader {
pub fn new() -> Self {
let (sender, receiver) = mpsc::unbounded_channel();
thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async move {
let mut worker = ImageTextureLoaderWorker {
receiver,
renderers: vec![],
};
worker.run().await
})
});
Self {
cache: Mutex::new(HashMap::new()),
sender,
}
}
pub fn add<const N: usize, R: ImageRenderer<N> + 'static>(
&self,
sim_id: SimId,
renderer: R,
params: R::Params,
) -> ImageParams<R::Params> {
let states = renderer.sizes().map(ImageState::new);
let images = renderer
.sizes()
.map(|[width, height]| ImageBuffer::new(width, height));
{
let mut cache = self.cache.lock().unwrap();
for (name, state) in renderer.names().into_iter().zip(&states) {
let url = format!("vip://{sim_id}/{name}");
let entry = ImageEntry {
size: state.size.map(|i| i as f32),
data: Arc::downgrade(&state.sink),
texture: None,
};
match cache.entry(url) {
Entry::Vacant(e) => {
e.insert(entry);
}
Entry::Occupied(mut e) => {
// Only overwrite an old entry if that entry is dead.
// Otherwise, we clear out the reference to a texture actually in use.
if e.get().data.strong_count() == 0 {
e.insert(entry);
}
}
}
}
}
let sink = Arc::new(Mutex::new(params.clone()));
let _ = self.sender.send(Box::new(ImageRendererWrapper {
renderer,
params: Arc::downgrade(&sink),
images,
states,
}));
ImageParams {
value: params,
sink,
}
}
}
impl TextureLoader for ImageTextureLoader {
fn id(&self) -> &str {
generate_loader_id!(ImageTextureLoader2)
}
fn load(
&self,
ctx: &egui::Context,
uri: &str,
texture_options: egui::TextureOptions,
_size_hint: egui::SizeHint,
) -> Result<TexturePoll, LoadError> {
let mut cache = self.cache.lock().unwrap();
let Some(entry) = cache.get_mut(uri) else {
return Err(LoadError::NotSupported);
};
if texture_options != egui::TextureOptions::NEAREST {
return Err(LoadError::Loading(
"Only TextureOptions::NEAREST are supported".into(),
));
}
let Some(data) = entry.data.upgrade() else {
cache.remove(uri);
return Err(LoadError::Loading("Backing loader no longer exists".into()));
};
match (data.lock().unwrap().take(), entry.texture.as_ref()) {
(Some(update), Some(handle)) => {
let delta = ImageDelta::full(update, texture_options);
ctx.tex_manager().write().set(handle.id(), delta);
let texture = SizedTexture::new(handle, entry.size);
Ok(TexturePoll::Ready { texture })
}
(Some(update), None) => {
let handle = ctx.load_texture(uri, update, texture_options);
let texture = SizedTexture::new(&handle, entry.size);
entry.texture.replace(handle);
Ok(TexturePoll::Ready { texture })
}
(None, Some(handle)) => {
let texture = SizedTexture::new(handle, entry.size);
Ok(TexturePoll::Ready { texture })
}
(None, None) => {
let size = entry.size.into();
Ok(TexturePoll::Pending { size: Some(size) })
}
}
}
fn forget(&self, uri: &str) {
let _ = uri;
}
fn forget_all(&self) {}
fn byte_size(&self) -> usize {
self.cache
.lock()
.unwrap()
.values()
.map(|entry| {
let [width, height] = entry.size;
width as usize * height as usize * 4
})
.sum()
}
}
struct ImageTextureLoaderWorker {
receiver: mpsc::UnboundedReceiver<Box<dyn ImageRendererImpl>>,
renderers: Vec<Box<dyn ImageRendererImpl>>,
}
impl ImageTextureLoaderWorker {
async fn run(&mut self) {
loop {
if self.renderers.is_empty() {
// if we have nothing to do, block until we have something to do
if self.receiver.recv_many(&mut self.renderers, 64).await == 0 {
// shutdown
return;
}
while let Ok(renderer) = self.receiver.try_recv() {
self.renderers.push(renderer);
}
}
self.renderers
.retain_mut(|renderer| renderer.try_update().is_ok());
// wait up to 10 ms for more renderers
if timeout(
Duration::from_millis(10),
self.receiver.recv_many(&mut self.renderers, 64),
)
.await
.is_ok()
{
while let Ok(renderer) = self.receiver.try_recv() {
self.renderers.push(renderer);
}
}
}
}
}
pub struct ImageBuffer {
pub size: [usize; 2],
pub pixels: Vec<Color32>,
}
impl ImageBuffer {
pub fn new(width: usize, height: usize) -> Self {
Self {
size: [width, height],
pixels: vec![Color32::BLACK; width * height],
}
}
pub fn clear(&mut self) {
for pixel in self.pixels.iter_mut() {
*pixel = Color32::BLACK;
}
}
pub fn write(&mut self, coords: (usize, usize), pixel: Color32) {
self.pixels[coords.1 * self.size[0] + coords.0] = pixel;
}
pub fn add(&mut self, coords: (usize, usize), pixel: Color32) {
let index = coords.1 * self.size[0] + coords.0;
let old = self.pixels[index];
self.pixels[index] = Color32::from_rgb(
old.r() + pixel.r(),
old.g() + pixel.g(),
old.b() + pixel.b(),
);
}
pub fn changed(&self, image: &ColorImage) -> bool {
image.pixels.iter().zip(&self.pixels).any(|(a, b)| a != b)
}
pub fn read(&self, image: &mut ColorImage) {
image.pixels.copy_from_slice(&self.pixels);
}
}
struct ImageEntry {
size: [f32; 2],
data: Weak<Mutex<Option<Arc<ColorImage>>>>,
texture: Option<TextureHandle>,
}
pub struct ImageParams<T> {
value: T,
sink: Arc<Mutex<T>>,
}
impl<T> Deref for ImageParams<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T: Clone + Eq> ImageParams<T> {
pub fn write(&mut self, value: T) {
if self.value != value {
self.value = value.clone();
*self.sink.lock().unwrap() = value;
}
}
}
pub trait ImageRenderer<const N: usize>: Send {
type Params: Clone + Send;
fn names(&self) -> [&str; N];
fn sizes(&self) -> [[usize; 2]; N];
fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; N]);
}
#[derive(Clone)]
struct ImageState {
size: [usize; 2],
buffers: [Arc<ColorImage>; 2],
last_buffer: usize,
sink: Arc<Mutex<Option<Arc<ColorImage>>>>,
}
impl ImageState {
fn new(size: [usize; 2]) -> Self {
let buffers = [
Arc::new(ColorImage::filled(size, Color32::BLACK)),
Arc::new(ColorImage::filled(size, Color32::BLACK)),
];
let sink = buffers[0].clone();
Self {
size,
buffers,
last_buffer: 0,
sink: Arc::new(Mutex::new(Some(sink))),
}
}
fn try_send_update(&mut self, image: &ImageBuffer) {
let last = &self.buffers[self.last_buffer];
if !image.changed(last) {
return;
}
let next_buffer = (self.last_buffer + 1) % self.buffers.len();
let next = &mut self.buffers[next_buffer];
image.read(Arc::make_mut(next));
self.last_buffer = next_buffer;
self.sink.lock().unwrap().replace(next.clone());
}
}
struct ImageRendererWrapper<const N: usize, R: ImageRenderer<N>> {
renderer: R,
params: Weak<Mutex<R::Params>>,
images: [ImageBuffer; N],
states: [ImageState; N],
}
trait ImageRendererImpl: Send {
fn try_update(&mut self) -> Result<(), ()>;
}
impl<const N: usize, R: ImageRenderer<N> + Send> ImageRendererImpl for ImageRendererWrapper<N, R> {
fn try_update(&mut self) -> Result<(), ()> {
let params = match self.params.upgrade() {
Some(params) => params.lock().unwrap().clone(),
None => {
// the UI isn't using this anymore
return Err(());
}
};
self.renderer.render(&params, &mut self.images);
for (state, image) in self.states.iter_mut().zip(&self.images) {
state.try_send_update(image);
}
Ok(())
}
}
+789
View File
@@ -0,0 +1,789 @@
use std::{
cmp::Ordering,
collections::{HashMap, hash_map::Entry},
fmt::Display,
str::FromStr,
sync::{Arc, Mutex, RwLock},
};
use anyhow::anyhow;
use egui::{Event, Key, KeyboardShortcut, Modifiers};
use gilrs::{Axis, Button, Gamepad, GamepadId, ev::Code};
use serde::{Deserialize, Serialize};
use winit::keyboard::{KeyCode, PhysicalKey};
use crate::{
emulator::{SimId, VBKey},
persistence::Persistence,
};
#[derive(Clone, PartialEq, Eq, Hash)]
struct DeviceId(u16, u16);
impl FromStr for DeviceId {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut ids = s.split("-");
let vendor_id: u16 = ids.next().ok_or(anyhow!("missing vendor id"))?.parse()?;
let product_id: u16 = ids.next().ok_or(anyhow!("missing product id"))?.parse()?;
Ok(Self(vendor_id, product_id))
}
}
impl Display for DeviceId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}-{}", self.0, self.1))
}
}
#[derive(Clone)]
pub struct GamepadInfo {
pub id: GamepadId,
pub name: String,
device_id: DeviceId,
pub bound_to: Option<SimId>,
}
pub trait Mappings {
fn mapping_names(&self) -> HashMap<VBKey, Vec<String>>;
fn clear_mappings(&mut self, key: VBKey);
fn clear_all_mappings(&mut self);
fn use_default_mappings(&mut self);
}
#[derive(Clone, Copy, Serialize, Deserialize)]
pub struct AxisMapping {
pub neg: VBKey,
pub pos: VBKey,
pub pair: Option<Code>,
}
impl Default for AxisMapping {
fn default() -> Self {
Self {
neg: VBKey::empty(),
pos: VBKey::empty(),
pair: None,
}
}
}
#[derive(Serialize, Deserialize)]
pub struct GamepadMapping {
buttons: HashMap<Code, VBKey>,
axes: HashMap<Code, AxisMapping>,
default_buttons: HashMap<Code, VBKey>,
default_axes: HashMap<Code, AxisMapping>,
}
impl GamepadMapping {
fn for_gamepad(gamepad: &Gamepad) -> Self {
let mut default_buttons = HashMap::new();
let mut default_button = |btn: Button, key: VBKey| {
if let Some(code) = gamepad.button_code(btn) {
default_buttons.insert(code, key);
}
};
default_button(Button::South, VBKey::A);
default_button(Button::West, VBKey::B);
default_button(Button::RightTrigger, VBKey::RT);
default_button(Button::LeftTrigger, VBKey::LT);
default_button(Button::Start, VBKey::STA);
default_button(Button::Select, VBKey::SEL);
let mut default_axes = HashMap::new();
let mut default_axis = |axis: Axis, neg: VBKey, pos: VBKey| {
if let Some(code) = gamepad.axis_code(axis) {
let pair = axis.second_axis().and_then(|a| gamepad.axis_code(a));
default_axes.insert(code, AxisMapping { neg, pos, pair });
}
};
default_axis(Axis::LeftStickX, VBKey::LL, VBKey::LR);
default_axis(Axis::LeftStickY, VBKey::LD, VBKey::LU);
default_axis(Axis::RightStickX, VBKey::RL, VBKey::RR);
default_axis(Axis::RightStickY, VBKey::RD, VBKey::RU);
default_axis(Axis::DPadX, VBKey::LL, VBKey::LR);
default_axis(Axis::DPadY, VBKey::LD, VBKey::LU);
Self {
buttons: default_buttons.clone(),
axes: default_axes.clone(),
default_buttons,
default_axes,
}
}
pub fn add_button_mapping(&mut self, key: VBKey, code: Code) {
let entry = self.buttons.entry(code).or_insert(VBKey::empty());
*entry = entry.union(key);
}
pub fn add_axis_neg_mapping(&mut self, key: VBKey, code: Code) {
let entry = self.axes.entry(code).or_default();
entry.neg = entry.neg.union(key);
}
pub fn add_axis_pos_mapping(&mut self, key: VBKey, code: Code) {
let entry = self.axes.entry(code).or_default();
entry.pos = entry.pos.union(key);
}
fn save_mappings(&self) -> PersistedGamepadMapping {
fn flatten<V: Copy>(values: &HashMap<Code, V>) -> Vec<(Code, V)> {
values.iter().map(|(k, v)| (*k, *v)).collect()
}
PersistedGamepadMapping {
buttons: flatten(&self.buttons),
axes: flatten(&self.axes),
default_buttons: flatten(&self.default_buttons),
default_axes: flatten(&self.default_axes),
}
}
fn from_mappings(mappings: &PersistedGamepadMapping) -> Self {
fn unflatten<V: Copy>(values: &[(Code, V)]) -> HashMap<Code, V> {
values.iter().map(|(k, v)| (*k, *v)).collect()
}
Self {
buttons: unflatten(&mappings.buttons),
axes: unflatten(&mappings.axes),
default_buttons: unflatten(&mappings.default_buttons),
default_axes: unflatten(&mappings.default_axes),
}
}
}
impl Mappings for GamepadMapping {
fn mapping_names(&self) -> HashMap<VBKey, Vec<String>> {
let mut results: HashMap<VBKey, Vec<String>> = HashMap::new();
for (axis, mapping) in &self.axes {
for key in mapping.neg.iter() {
results.entry(key).or_default().push(format!("-{axis}"));
}
for key in mapping.pos.iter() {
results.entry(key).or_default().push(format!("+{axis}"));
}
}
for (button, keys) in &self.buttons {
for key in keys.iter() {
results.entry(key).or_default().push(format!("{button}"));
}
}
results
}
fn clear_mappings(&mut self, key: VBKey) {
self.axes.retain(|_, mapping| {
mapping.neg = mapping.neg.difference(key);
mapping.pos = mapping.pos.difference(key);
!(mapping.neg.is_empty() && mapping.pos.is_empty())
});
self.buttons.retain(|_, keys| {
*keys = keys.difference(key);
!keys.is_empty()
});
}
fn clear_all_mappings(&mut self) {
self.axes.clear();
self.buttons.clear();
}
fn use_default_mappings(&mut self) {
self.axes = self.default_axes.clone();
self.buttons = self.default_buttons.clone();
}
}
#[derive(Default)]
pub struct InputMapping {
keys: HashMap<PhysicalKey, VBKey>,
gamepads: HashMap<GamepadId, Arc<RwLock<GamepadMapping>>>,
}
impl InputMapping {
pub fn map_keyboard(&self, key: &PhysicalKey) -> Option<VBKey> {
self.keys.get(key).copied()
}
pub fn map_button(&self, id: &GamepadId, code: &Code) -> Option<VBKey> {
let mappings = self.gamepads.get(id)?.read().unwrap();
mappings.buttons.get(code).copied()
}
pub fn map_axis(&self, id: &GamepadId, code: &Code) -> Option<AxisMapping> {
let mappings = self.gamepads.get(id)?.read().unwrap();
mappings.axes.get(code).copied()
}
pub fn add_keyboard_mapping(&mut self, key: VBKey, keyboard_key: PhysicalKey) {
let entry = self.keys.entry(keyboard_key).or_insert(VBKey::empty());
*entry = entry.union(key);
}
fn save_mappings(&self) -> PersistedKeyboardMapping {
PersistedKeyboardMapping {
keys: self.keys.iter().map(|(k, v)| (*k, *v)).collect(),
}
}
fn restore_mappings(&mut self, persisted: &PersistedKeyboardMapping) {
self.keys = persisted.keys.iter().map(|(k, v)| (*k, *v)).collect();
}
}
impl Mappings for InputMapping {
fn mapping_names(&self) -> HashMap<VBKey, Vec<String>> {
let mut results: HashMap<VBKey, Vec<String>> = HashMap::new();
for (keyboard_key, keys) in &self.keys {
let name = match keyboard_key {
PhysicalKey::Code(code) => format!("{code:?}"),
k => format!("{k:?}"),
};
for key in keys.iter() {
results.entry(key).or_default().push(name.clone());
}
}
results
}
fn clear_mappings(&mut self, key: VBKey) {
self.keys.retain(|_, keys| {
*keys = keys.difference(key);
!keys.is_empty()
});
}
fn clear_all_mappings(&mut self) {
self.keys.clear();
}
fn use_default_mappings(&mut self) {
self.keys.clear();
let mut default_key = |code, key| {
self.keys.insert(PhysicalKey::Code(code), key);
};
default_key(KeyCode::KeyA, VBKey::SEL);
default_key(KeyCode::KeyS, VBKey::STA);
default_key(KeyCode::KeyD, VBKey::B);
default_key(KeyCode::KeyF, VBKey::A);
default_key(KeyCode::KeyE, VBKey::LT);
default_key(KeyCode::KeyR, VBKey::RT);
default_key(KeyCode::KeyI, VBKey::RU);
default_key(KeyCode::KeyJ, VBKey::RL);
default_key(KeyCode::KeyK, VBKey::RD);
default_key(KeyCode::KeyL, VBKey::RR);
default_key(KeyCode::ArrowUp, VBKey::LU);
default_key(KeyCode::ArrowLeft, VBKey::LL);
default_key(KeyCode::ArrowDown, VBKey::LD);
default_key(KeyCode::ArrowRight, VBKey::LR);
}
}
#[derive(Clone)]
pub struct MappingProvider {
persistence: Persistence,
first_gamepad_is_p2: bool,
device_mappings: Arc<RwLock<HashMap<DeviceId, Arc<RwLock<GamepadMapping>>>>>,
sim_mappings: HashMap<SimId, Arc<RwLock<InputMapping>>>,
gamepad_info: Arc<RwLock<HashMap<GamepadId, GamepadInfo>>>,
}
impl MappingProvider {
pub fn new(persistence: Persistence, first_gamepad_is_p2: bool) -> Self {
let mut sim_mappings = HashMap::new();
let mut device_mappings = HashMap::new();
let mut p1_mappings = InputMapping::default();
let mut p2_mappings = InputMapping::default();
if let Ok(persisted) = persistence.load_config::<PersistedInputMappings>("mappings") {
p1_mappings.restore_mappings(&persisted.p1_keyboard);
p2_mappings.restore_mappings(&persisted.p2_keyboard);
for (device_id, mappings) in persisted.gamepads {
let Ok(device_id) = device_id.parse::<DeviceId>() else {
continue;
};
let gamepad = GamepadMapping::from_mappings(&mappings);
device_mappings.insert(device_id, Arc::new(RwLock::new(gamepad)));
}
} else {
p1_mappings.use_default_mappings();
}
sim_mappings.insert(SimId::Player1, Arc::new(RwLock::new(p1_mappings)));
sim_mappings.insert(SimId::Player2, Arc::new(RwLock::new(p2_mappings)));
Self {
persistence,
device_mappings: Arc::new(RwLock::new(device_mappings)),
gamepad_info: Arc::new(RwLock::new(HashMap::new())),
sim_mappings,
first_gamepad_is_p2,
}
}
pub fn for_sim(&self, sim_id: SimId) -> &Arc<RwLock<InputMapping>> {
self.sim_mappings.get(&sim_id).unwrap()
}
pub fn for_gamepad(&self, gamepad_id: GamepadId) -> Option<Arc<RwLock<GamepadMapping>>> {
let lock = self.gamepad_info.read().unwrap();
let device_id = lock.get(&gamepad_id)?.device_id.clone();
drop(lock);
let lock = self.device_mappings.read().unwrap();
lock.get(&device_id).cloned()
}
pub fn handle_gamepad_connect(&self, gamepad: &Gamepad) {
let device_id = DeviceId(
gamepad.vendor_id().unwrap_or_default(),
gamepad.product_id().unwrap_or_default(),
);
let mut lock = self.device_mappings.write().unwrap();
let mappings = match lock.entry(device_id.clone()) {
Entry::Vacant(entry) => {
let mappings = GamepadMapping::for_gamepad(gamepad);
entry.insert(Arc::new(RwLock::new(mappings)))
}
Entry::Occupied(entry) => entry.into_mut(),
}
.clone();
drop(lock);
let mut lock = self.gamepad_info.write().unwrap();
let players = if self.first_gamepad_is_p2 {
vec![SimId::Player2, SimId::Player1]
} else {
vec![SimId::Player1, SimId::Player2]
};
let bound_to = players
.into_iter()
.find(|sim_id| lock.values().all(|info| info.bound_to != Some(*sim_id)));
if let Entry::Vacant(entry) = lock.entry(gamepad.id()) {
let info = GamepadInfo {
id: *entry.key(),
name: gamepad.name().to_string(),
device_id,
bound_to,
};
entry.insert(info);
}
drop(lock);
if let Some(sim_id) = bound_to {
self.for_sim(sim_id)
.write()
.unwrap()
.gamepads
.insert(gamepad.id(), mappings);
}
}
pub fn handle_gamepad_disconnect(&self, gamepad_id: GamepadId) {
let mut lock = self.gamepad_info.write().unwrap();
let Some(info) = lock.remove(&gamepad_id) else {
return;
};
if let Some(sim_id) = info.bound_to {
self.for_sim(sim_id)
.write()
.unwrap()
.gamepads
.remove(&gamepad_id);
}
}
pub fn assign_gamepad(&self, gamepad_id: GamepadId, sim_id: SimId) {
self.unassign_gamepad(gamepad_id);
let mut lock = self.gamepad_info.write().unwrap();
let Some(info) = lock.get_mut(&gamepad_id) else {
return;
};
info.bound_to = Some(sim_id);
let device_id = info.device_id.clone();
drop(lock);
let Some(device_mappings) = self
.device_mappings
.write()
.unwrap()
.get(&device_id)
.cloned()
else {
return;
};
self.for_sim(sim_id)
.write()
.unwrap()
.gamepads
.insert(gamepad_id, device_mappings);
}
pub fn unassign_gamepad(&self, gamepad_id: GamepadId) {
let mut lock = self.gamepad_info.write().unwrap();
let Some(info) = lock.get_mut(&gamepad_id) else {
return;
};
if let Some(sim_id) = info.bound_to {
let mut sim_mapping = self.for_sim(sim_id).write().unwrap();
sim_mapping.gamepads.remove(&gamepad_id);
}
info.bound_to = None;
}
pub fn gamepad_info(&self) -> Vec<GamepadInfo> {
self.gamepad_info
.read()
.unwrap()
.values()
.cloned()
.collect()
}
pub fn save(&self) {
let p1_keyboard = self.for_sim(SimId::Player1).read().unwrap().save_mappings();
let p2_keyboard = self.for_sim(SimId::Player2).read().unwrap().save_mappings();
let mut gamepads = HashMap::new();
for (device_id, gamepad) in self.device_mappings.read().unwrap().iter() {
let mapping = gamepad.read().unwrap().save_mappings();
gamepads.insert(device_id.to_string(), mapping);
}
let persisted = PersistedInputMappings {
p1_keyboard,
p2_keyboard,
gamepads,
};
let _ = self.persistence.save_config("mappings", &persisted);
}
}
#[derive(Serialize, Deserialize)]
struct PersistedInputMappings {
p1_keyboard: PersistedKeyboardMapping,
p2_keyboard: PersistedKeyboardMapping,
gamepads: HashMap<String, PersistedGamepadMapping>,
}
#[derive(Serialize, Deserialize)]
struct PersistedKeyboardMapping {
keys: Vec<(PhysicalKey, VBKey)>,
}
#[derive(Serialize, Deserialize)]
struct PersistedGamepadMapping {
buttons: Vec<(Code, VBKey)>,
axes: Vec<(Code, AxisMapping)>,
default_buttons: Vec<(Code, VBKey)>,
default_axes: Vec<(Code, AxisMapping)>,
}
#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum Command {
OpenRom,
ReloadRom,
Quit,
FrameAdvance,
FastForward(u32),
Reset,
PauseResume,
Screenshot,
// if you update this, update Command::all and add a default
}
impl Command {
pub fn all() -> [Self; 8] {
[
Self::OpenRom,
Self::ReloadRom,
Self::Quit,
Self::PauseResume,
Self::Reset,
Self::FrameAdvance,
Self::FastForward(0),
Self::Screenshot,
]
}
pub fn name(self) -> &'static str {
match self {
Self::OpenRom => "Open ROM",
Self::ReloadRom => "Reload ROM",
Self::Quit => "Exit",
Self::PauseResume => "Pause/Resume",
Self::Reset => "Reset",
Self::FrameAdvance => "Frame Advance",
Self::FastForward(_) => "Fast Forward",
Self::Screenshot => "Screenshot",
}
}
}
impl Display for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
struct Shortcuts {
all: Vec<(Command, KeyboardShortcut)>,
by_command: HashMap<Command, KeyboardShortcut>,
}
impl Default for Shortcuts {
fn default() -> Self {
let mut shortcuts = Shortcuts {
all: vec![],
by_command: HashMap::new(),
};
shortcuts.set(
Command::OpenRom,
KeyboardShortcut::new(Modifiers::COMMAND, Key::O),
);
shortcuts.set(
Command::ReloadRom,
KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, Key::F5),
);
shortcuts.set(
Command::Quit,
KeyboardShortcut::new(Modifiers::COMMAND, Key::Q),
);
shortcuts.set(
Command::PauseResume,
KeyboardShortcut::new(Modifiers::NONE, Key::F5),
);
shortcuts.set(
Command::Reset,
KeyboardShortcut::new(Modifiers::SHIFT, Key::F5),
);
shortcuts.set(
Command::FrameAdvance,
KeyboardShortcut::new(Modifiers::NONE, Key::F6),
);
shortcuts.set(
Command::FastForward(0),
KeyboardShortcut::new(Modifiers::NONE, Key::Space),
);
shortcuts.set(
Command::Screenshot,
KeyboardShortcut::new(Modifiers::NONE, Key::F12),
);
shortcuts
}
}
impl Shortcuts {
fn set(&mut self, command: Command, shortcut: KeyboardShortcut) {
if self.by_command.insert(command, shortcut).is_some() {
for (cmd, sht) in &mut self.all {
if *cmd == command {
*sht = shortcut;
break;
}
}
} else {
self.all.push((command, shortcut));
}
self.all.sort_by(|l, r| order_shortcut(l.1, r.1));
}
fn unset(&mut self, command: Command) {
if self.by_command.remove(&command).is_some() {
self.all.retain(|(c, _)| *c != command);
}
}
fn save(&self, saved: &mut PersistedSettings) {
for command in Command::all() {
let shortcut = self.by_command.get(&command).copied();
saved.shortcuts.push((command, shortcut));
}
}
}
fn order_shortcut(left: KeyboardShortcut, right: KeyboardShortcut) -> Ordering {
left.logical_key.cmp(&right.logical_key).then_with(|| {
specificity(left.modifiers)
.cmp(&specificity(right.modifiers))
.reverse()
})
}
fn specificity(modifiers: egui::Modifiers) -> usize {
let mut mods = 0;
if modifiers.alt {
mods += 1;
}
if modifiers.command || modifiers.ctrl {
mods += 1;
}
if modifiers.shift {
mods += 1;
}
mods
}
#[derive(Serialize, Deserialize, Default)]
struct PersistedSettings {
shortcuts: Vec<(Command, Option<KeyboardShortcut>)>,
#[serde(default)]
ff_settings: FastForwardSettings,
}
#[derive(Default, Clone)]
struct ShortcutState {
ff_toggled: bool,
}
#[derive(Default)]
struct Settings {
shortcuts: Shortcuts,
ff_settings: FastForwardSettings,
state: ShortcutState,
}
impl Settings {
fn save(&self) -> PersistedSettings {
let mut saved = PersistedSettings {
shortcuts: vec![],
ff_settings: self.ff_settings.clone(),
};
self.shortcuts.save(&mut saved);
saved
}
}
#[derive(Clone)]
pub struct ShortcutProvider {
persistence: Persistence,
settings: Arc<Mutex<Settings>>,
}
impl ShortcutProvider {
pub fn new(persistence: Persistence) -> Self {
let mut settings = Settings::default();
if let Ok(saved) = persistence.load_config::<PersistedSettings>("shortcuts") {
for (command, shortcut) in saved.shortcuts {
if let Some(shortcut) = shortcut {
settings.shortcuts.set(command, shortcut);
} else {
settings.shortcuts.unset(command);
}
}
settings.ff_settings = saved.ff_settings;
};
Self {
persistence,
settings: Arc::new(Mutex::new(settings)),
}
}
pub fn shortcut_for(&self, command: Command) -> Option<KeyboardShortcut> {
let lock = self.settings.lock().unwrap();
lock.shortcuts.by_command.get(&command).copied()
}
pub fn ff_settings(&self) -> FastForwardSettings {
let lock = self.settings.lock().unwrap();
lock.ff_settings.clone()
}
pub fn consume_all(&self, input: &mut egui::InputState) -> Vec<Command> {
let mut lock = self.settings.lock().unwrap();
let mut state = lock.state.clone();
let mut consumed = vec![];
for (command, shortcut) in &lock.shortcuts.all {
input.events.retain(|event| {
let Event::Key {
key,
pressed,
repeat,
modifiers,
..
} = event
else {
return true;
};
if *key != shortcut.logical_key || !modifiers.contains(shortcut.modifiers) {
return true;
}
if matches!(command, Command::FastForward(_)) {
if *repeat {
return true;
}
let sped_up = if lock.ff_settings.toggle {
if !*pressed {
return true;
}
state.ff_toggled = !state.ff_toggled;
state.ff_toggled
} else {
*pressed
};
let speed = if sped_up { lock.ff_settings.speed } else { 1 };
consumed.push(Command::FastForward(speed));
false
} else {
if !*pressed {
return true;
}
consumed.push(*command);
false
}
});
}
lock.state = state;
consumed
}
pub fn set(&self, command: Command, shortcut: KeyboardShortcut) {
let updated = {
let mut lock = self.settings.lock().unwrap();
lock.shortcuts.set(command, shortcut);
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
}
pub fn unset(&self, command: Command) {
let updated = {
let mut lock = self.settings.lock().unwrap();
lock.shortcuts.unset(command);
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
}
pub fn update_ff_settings(&self, ff_settings: FastForwardSettings) {
let updated = {
let mut lock = self.settings.lock().unwrap();
lock.ff_settings = ff_settings;
if !lock.ff_settings.toggle {
lock.state.ff_toggled = false;
}
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
}
pub fn reset(&self) {
let updated = {
let mut lock = self.settings.lock().unwrap();
*lock = Settings::default();
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct FastForwardSettings {
pub toggle: bool,
pub speed: u32,
}
impl Default for FastForwardSettings {
fn default() -> Self {
Self {
toggle: false,
speed: 10,
}
}
}
+154
View File
@@ -0,0 +1,154 @@
// hide console in release mode
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::{process, time::SystemTime};
use anyhow::{Result, bail};
use app::Application;
use clap::Parser;
use emulator::EmulatorBuilder;
use thread_priority::{ThreadBuilder, ThreadPriority};
use tracing::error;
use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt};
use winit::event_loop::{ControlFlow, EventLoop};
use crate::{
config::{AppConfig, CliArgs, SimConfig},
emulator::SimId,
persistence::Persistence,
};
mod app;
mod audio;
mod config;
mod controller;
mod emulator;
mod filepicker;
mod gdbserver;
mod graphics;
mod images;
mod input;
mod memory;
mod persistence;
mod profiler;
mod window;
fn init_logger() {
let directives = std::env::var("RUST_LOG").unwrap_or("error,lemur=info".into());
let filter = EnvFilter::builder().parse_lossy(directives);
let layer = tracing_subscriber::fmt::layer().with_filter(filter);
tracing_subscriber::registry().with(layer).init();
}
fn set_panic_handler() {
std::panic::set_hook(Box::new(|info| {
let mut message = String::new();
if let Some(msg) = info.payload().downcast_ref::<&str>() {
message += &format!("{msg}\n");
} else if let Some(msg) = info.payload().downcast_ref::<String>() {
message += &format!("{msg}\n");
}
if let Some(location) = info.location() {
message += &format!(
" in file '{}' at line {}\n",
location.file(),
location.line()
);
}
let backtrace = std::backtrace::Backtrace::force_capture();
message += &format!("stack trace:\n{backtrace:#}\n");
eprint!("{message}");
let Some(project_dirs) = directories::ProjectDirs::from("com", "virtual-boy", "Lemur")
else {
return;
};
let data_dir = project_dirs.data_dir();
if std::fs::create_dir_all(data_dir).is_err() {
return;
}
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis();
let logfile_name = format!("crash-{timestamp}.txt");
let _ = std::fs::write(data_dir.join(logfile_name), message);
}));
}
#[cfg(windows)]
fn set_process_priority_to_high() -> Result<()> {
use windows::Win32::{Foundation, System::Threading};
let process = unsafe { Threading::GetCurrentProcess() };
unsafe { Threading::SetPriorityClass(process, Threading::HIGH_PRIORITY_CLASS)? };
unsafe { Foundation::CloseHandle(process)? };
Ok(())
}
fn main() -> Result<()> {
init_logger();
set_panic_handler();
#[cfg(windows)]
set_process_priority_to_high()?;
let args = CliArgs::parse();
let persistence = Persistence::new();
let (mut builder, client) = EmulatorBuilder::new();
if let Some(path) = &args.rom {
builder = builder.with_rom(path);
}
if args.debug_port.is_some() {
if args.rom.is_none() {
bail!("to start debugging, please select a game.");
}
builder = builder.start_paused(true);
}
if args.profile {
builder = builder.start_paused(true)
}
let mut config = AppConfig::load(&persistence);
if config.update(&args) {
let _ = config.save(&persistence);
}
let p1 = SimConfig::load(&persistence, SimId::Player1);
let p2 = SimConfig::load(&persistence, SimId::Player2);
let watch = args.watch;
builder = builder
.with_audio_on(p1.audio_enabled, p2.audio_enabled)
.with_watch_rom(watch, watch);
ThreadBuilder::default()
.name("Emulator".to_owned())
.priority(ThreadPriority::Max)
.spawn_careless(move || {
let mut emulator = match builder.build() {
Ok(e) => e,
Err(error) => {
error!(%error, "Error initializing emulator");
process::exit(1);
}
};
emulator.run();
})?;
let event_loop = EventLoop::with_user_event().build().unwrap();
event_loop.set_control_flow(ControlFlow::Poll);
let proxy = event_loop.create_proxy();
let handle = event_loop.owned_display_handle();
event_loop.run_app(&mut Application::new(
client,
proxy,
handle,
persistence,
args,
config,
))?;
Ok(())
}
+276
View File
@@ -0,0 +1,276 @@
use std::{
collections::HashMap,
fmt::Debug,
iter::FusedIterator,
sync::{Arc, Mutex, RwLock, RwLockReadGuard, TryLockError, Weak, atomic::AtomicU64},
};
use bytemuck::BoxBytes;
use itertools::Itertools;
use tracing::warn;
use crate::emulator::{EmulatorClient, EmulatorCommand, SimId};
pub struct MemoryClient {
client: EmulatorClient,
regions: Mutex<HashMap<MemoryRange, Weak<MemoryRegion>>>,
}
impl MemoryClient {
pub fn new(client: EmulatorClient) -> Self {
Self {
client,
regions: Mutex::new(HashMap::new()),
}
}
pub fn watch(&self, sim: SimId, start: u32, length: usize) -> MemoryView {
let range = MemoryRange { sim, start, length };
let mut regions = self.regions.lock().unwrap_or_else(|e| e.into_inner());
let region = regions
.get(&range)
.and_then(|r| r.upgrade())
.unwrap_or_else(|| {
let region = Arc::new(MemoryRegion::new(start, length));
regions.insert(range, Arc::downgrade(&region));
self.client
.send_command(EmulatorCommand::WatchMemory(range, Arc::downgrade(&region)));
region
});
MemoryView { region }
}
pub fn write<T: MemoryValue>(&self, sim: SimId, address: u32, data: &T) {
let mut buffer = vec![];
data.to_bytes(&mut buffer);
let (tx, _) = oneshot::channel();
self.client
.send_command(EmulatorCommand::WriteMemory(sim, address, buffer, tx));
}
}
fn aligned_memory(start: u32, length: usize) -> BoxBytes {
if start.is_multiple_of(4) && length.is_multiple_of(4) {
let memory = vec![0u32; length / 4].into_boxed_slice();
return bytemuck::box_bytes_of(memory);
}
if start.is_multiple_of(2) && length.is_multiple_of(2) {
let memory = vec![0u16; length / 2].into_boxed_slice();
return bytemuck::box_bytes_of(memory);
}
let memory = vec![0u8; length].into_boxed_slice();
bytemuck::box_bytes_of(memory)
}
pub struct MemoryView {
region: Arc<MemoryRegion>,
}
impl MemoryView {
pub fn borrow(&self) -> MemoryRef<'_> {
self.region.borrow()
}
}
pub struct MemoryRef<'a> {
inner: RwLockReadGuard<'a, BoxBytes>,
}
pub trait MemoryValue {
fn from_bytes(bytes: &[u8]) -> Self;
fn to_bytes(&self, buffer: &mut Vec<u8>);
}
macro_rules! primitive_memory_value_impl {
($T:ty, $L: expr) => {
impl MemoryValue for $T {
#[inline]
fn from_bytes(bytes: &[u8]) -> Self {
let bytes: [u8; std::mem::size_of::<$T>()] = std::array::from_fn(|i| bytes[i]);
<$T>::from_le_bytes(bytes)
}
#[inline]
fn to_bytes(&self, buffer: &mut Vec<u8>) {
buffer.extend_from_slice(&self.to_le_bytes())
}
}
};
}
primitive_memory_value_impl!(u8, 1);
primitive_memory_value_impl!(i8, 1);
primitive_memory_value_impl!(u16, 2);
primitive_memory_value_impl!(i16, 2);
primitive_memory_value_impl!(u32, 4);
primitive_memory_value_impl!(i32, 4);
impl<const N: usize, T: MemoryValue> MemoryValue for [T; N] {
#[inline]
fn from_bytes(bytes: &[u8]) -> Self {
std::array::from_fn(|i| {
T::from_bytes(&bytes[i * std::mem::size_of::<T>()..(i + 1) * std::mem::size_of::<T>()])
})
}
#[inline]
fn to_bytes(&self, buffer: &mut Vec<u8>) {
for item in self {
item.to_bytes(buffer);
}
}
}
pub struct MemoryIter<'a, T> {
bytes: &'a [u8],
_phantom: std::marker::PhantomData<T>,
}
impl<'a, T> MemoryIter<'a, T> {
fn new(bytes: &'a [u8]) -> Self {
Self {
bytes,
_phantom: std::marker::PhantomData,
}
}
}
impl<T: MemoryValue> Iterator for MemoryIter<'_, T> {
type Item = T;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let (bytes, rest) = self.bytes.split_at_checked(std::mem::size_of::<T>())?;
self.bytes = rest;
Some(T::from_bytes(bytes))
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
let size = self.bytes.len() / std::mem::size_of::<T>();
(size, Some(size))
}
}
impl<T: MemoryValue> DoubleEndedIterator for MemoryIter<'_, T> {
fn next_back(&mut self) -> Option<Self::Item> {
let mid = self.bytes.len().checked_sub(std::mem::size_of::<T>())?;
// SAFETY: the checked_sub above is effectively a bounds check
let (rest, bytes) = unsafe { self.bytes.split_at_unchecked(mid) };
self.bytes = rest;
Some(T::from_bytes(bytes))
}
}
impl<T: MemoryValue> FusedIterator for MemoryIter<'_, T> {}
impl MemoryRef<'_> {
pub fn read<T: MemoryValue>(&self, index: usize) -> T {
let from = index * size_of::<T>();
let to = from + size_of::<T>();
T::from_bytes(&self.inner[from..to])
}
pub fn range<T: MemoryValue>(&self, start: usize, count: usize) -> MemoryIter<'_, T> {
let from = start * size_of::<T>();
let to = from + (count * size_of::<T>());
MemoryIter::new(&self.inner[from..to])
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct MemoryRange {
pub sim: SimId,
pub start: u32,
pub length: usize,
}
const BUFFERS: usize = 4;
pub struct MemoryRegion {
gens: [AtomicU64; BUFFERS],
bufs: [RwLock<BoxBytes>; BUFFERS],
}
impl Debug for MemoryRegion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MemoryRegion")
.field("gens", &self.gens)
.finish_non_exhaustive()
}
}
// SAFETY: BoxBytes is meant to be Send+Sync, will be in a future version
unsafe impl Send for MemoryRegion {}
// SAFETY: BoxBytes is meant to be Send+Sync, will be in a future version
unsafe impl Sync for MemoryRegion {}
impl MemoryRegion {
fn new(start: u32, length: usize) -> Self {
Self {
gens: std::array::from_fn(|i| AtomicU64::new(i as u64)),
bufs: std::array::from_fn(|_| RwLock::new(aligned_memory(start, length))),
}
}
pub fn borrow(&self) -> MemoryRef<'_> {
/*
* When reading memory, a thread will grab the newest buffer (with the highest gen)
* It will only fail to grab the lock if the writer already has it,
* but the writer prioritizes older buffers (with lower gens).
* So this method will only block if the writer produces three full buffers
* in the time it takes the reader to do four atomic reads and grab a lock.
* In the unlikely event this happens... just try again.
*/
loop {
let newest_index = self
.gens
.iter()
.map(|i| i.load(std::sync::atomic::Ordering::Acquire))
.enumerate()
.max_by_key(|(_, g)| *g)
.map(|(i, _)| i)
.unwrap();
let inner = match self.bufs[newest_index].try_read() {
Ok(inner) => inner,
Err(TryLockError::Poisoned(e)) => e.into_inner(),
Err(TryLockError::WouldBlock) => {
continue;
}
};
break MemoryRef { inner };
}
}
pub fn update(&self, data: &[u8]) {
let gens = self
.gens
.each_ref()
.map(|i| i.load(std::sync::atomic::Ordering::Acquire));
let next_gen = gens.iter().max().unwrap() + 1;
let indices = gens
.into_iter()
.enumerate()
.sorted_by_key(|(_, val)| *val)
.map(|(i, _)| i);
for index in indices {
let mut lock = match self.bufs[index].try_write() {
Ok(inner) => inner,
Err(TryLockError::Poisoned(e)) => e.into_inner(),
Err(TryLockError::WouldBlock) => {
continue;
}
};
lock.copy_from_slice(data);
self.gens[index].store(next_gen, std::sync::atomic::Ordering::Release);
return;
}
/*
* We have four buffers, and (at time of writing) only three threads interacting with memory:
* - The UI thread, reading small regions of memory
* - The "image renderer" thread, reading large regions of memory
* - The emulation thread, writing memory every so often
* So it should be impossible for all four buffers to have a read lock at the same time,
* and (because readers always read the newest buffer) at least one of the oldest three
* buffers will be free the entire time we're in this method.
* TL;DR this should never happen.
* But if it does, do nothing. This isn't medical software, better to show stale data than crash.
*/
warn!("all buffers were locked by a reader at the same time")
}
}
+46
View File
@@ -0,0 +1,46 @@
use std::{fs, path::PathBuf};
use anyhow::{Result, bail};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
#[derive(Clone)]
pub struct Persistence {
dirs: Option<Dirs>,
}
impl Persistence {
pub fn new() -> Self {
Self { dirs: init_dirs() }
}
pub fn save_config<T: Serialize>(&self, file: &str, data: &T) -> Result<()> {
if let Some(dirs) = self.dirs.as_ref() {
let bytes = serde_json::to_vec_pretty(data)?;
let filename = dirs.config_dir.join(file).with_extension("json");
fs::write(&filename, bytes)?;
}
Ok(())
}
pub fn load_config<T: for<'a> Deserialize<'a>>(&self, file: &str) -> Result<T> {
let Some(dirs) = self.dirs.as_ref() else {
bail!("config directory not found");
};
let filename = dirs.config_dir.join(file).with_extension("json");
let bytes = fs::read(filename)?;
Ok(serde_json::from_slice(&bytes)?)
}
}
#[derive(Clone)]
struct Dirs {
config_dir: PathBuf,
}
fn init_dirs() -> Option<Dirs> {
let dirs = ProjectDirs::from("com", "virtual-boy", "Lemur")?;
let config_dir = dirs.config_dir().to_path_buf();
fs::create_dir_all(&config_dir).ok()?;
Some(Dirs { config_dir })
}
+264
View File
@@ -0,0 +1,264 @@
use std::{
sync::{Arc, Mutex},
thread,
};
use anyhow::Result;
use tokio::{select, sync::mpsc};
use crate::emulator::{
EmulatorClient, EmulatorCommand, GameInfo, InlineStack, ProfileEvent, SimEvent, SimId,
};
use recording::Recording;
use state::ProgramState;
mod recording;
mod state;
pub struct Profiler {
sim_id: SimId,
client: EmulatorClient,
status: Arc<Mutex<ProfilerStatus>>,
action: Option<mpsc::UnboundedSender<RecordingAction>>,
killer: Option<oneshot::Sender<()>>,
}
impl Profiler {
pub fn new(sim_id: SimId, client: EmulatorClient) -> Self {
Self {
sim_id,
client,
status: Arc::new(Mutex::new(ProfilerStatus::Disabled)),
action: None,
killer: None,
}
}
pub fn status(&self) -> ProfilerStatus {
self.status.lock().unwrap().clone()
}
pub fn enable(&mut self) {
let sim_id = self.sim_id;
let client = self.client.clone();
let status = self.status.clone();
let (action_tx, action_rx) = mpsc::unbounded_channel();
self.action = Some(action_tx);
let (killer_tx, killer_rx) = oneshot::channel();
self.killer = Some(killer_tx);
thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async move {
select! {
_ = run_profile(sim_id, client, status.clone(), action_rx) => {}
_ = killer_rx => {
*status.lock().unwrap() = ProfilerStatus::Disabled;
}
}
})
});
}
pub fn disable(&mut self) {
if let Some(killer) = self.killer.take() {
let _ = killer.send(());
}
}
pub fn start_recording(&mut self) {
if let Some(action) = &self.action {
let _ = action.send(RecordingAction::Start);
}
}
pub fn finish_recording(&mut self) -> oneshot::Receiver<Vec<u8>> {
let (tx, rx) = oneshot::channel();
if let Some(action) = &self.action {
let _ = action.send(RecordingAction::Finish(tx));
}
rx
}
pub fn cancel_recording(&mut self) {
if let Some(action) = &self.action {
let _ = action.send(RecordingAction::Cancel);
}
}
}
impl Drop for Profiler {
fn drop(&mut self) {
self.disable();
}
}
async fn run_profile(
sim_id: SimId,
client: EmulatorClient,
status: Arc<Mutex<ProfilerStatus>>,
mut action_source: mpsc::UnboundedReceiver<RecordingAction>,
) {
let (profile_sync, mut profile_source) = mpsc::unbounded_channel();
client.send_command(EmulatorCommand::StartProfiling(sim_id, profile_sync));
*status.lock().unwrap() = ProfilerStatus::Enabled;
let mut session = ProfilerSession::new();
loop {
select! {
maybe_event = profile_source.recv() => {
let Some(event) = maybe_event else {
break; // emulator thread disconnected
};
if let Err(error) = handle_event(event, &mut session).await {
*status.lock().unwrap() = ProfilerStatus::Error(error.to_string());
return;
}
}
maybe_action = action_source.recv() => {
let Some(action) = maybe_action else {
break; // ui thread disconnected
};
handle_action(action, &mut session, &status);
}
}
}
*status.lock().unwrap() = ProfilerStatus::Disabled;
}
async fn handle_event(event: ProfileEvent, session: &mut ProfilerSession) -> Result<()> {
match event {
ProfileEvent::Start { info } => session.start_profiling(info).await,
ProfileEvent::Update {
cycles,
event,
inline_stack,
} => {
session.track_elapsed_cycles(cycles);
if let Some(event) = event {
session.track_event(event)?;
}
if let Some(stack) = inline_stack {
session.track_inline_stack(stack);
}
}
}
Ok(())
}
fn handle_action(
action: RecordingAction,
session: &mut ProfilerSession,
status: &Mutex<ProfilerStatus>,
) {
match action {
RecordingAction::Start => {
session.start_recording();
*status.lock().unwrap() = ProfilerStatus::Recording;
}
RecordingAction::Finish(rx) => {
if let Some(bytes) = session.finish_recording() {
let _ = rx.send(bytes);
}
*status.lock().unwrap() = ProfilerStatus::Enabled;
}
RecordingAction::Cancel => {
session.cancel_recording();
*status.lock().unwrap() = ProfilerStatus::Enabled;
}
}
}
#[derive(Clone)]
pub enum ProfilerStatus {
Disabled,
Enabled,
Recording,
Error(String),
}
impl ProfilerStatus {
pub fn enabled(&self) -> bool {
matches!(self, Self::Enabled | Self::Recording)
}
}
enum RecordingAction {
Start,
Finish(oneshot::Sender<Vec<u8>>),
Cancel,
}
struct ProfilerSession {
program: Option<ProgramState>,
recording: Option<Recording>,
}
impl ProfilerSession {
fn new() -> Self {
Self {
program: None,
recording: None,
}
}
async fn start_profiling(&mut self, info: Arc<GameInfo>) {
let program = ProgramState::new(info).await;
let recording = if self.recording.is_some() {
Some(Recording::new(&program))
} else {
None
};
self.program = Some(program);
self.recording = recording;
}
fn track_elapsed_cycles(&mut self, cycles: u32) {
if let (Some(state), Some(recording)) = (&self.program, &mut self.recording) {
recording.track_elapsed_cycles(state, cycles);
}
}
fn track_event(&mut self, event: SimEvent) -> Result<()> {
let Some(program) = &mut self.program else {
return Ok(());
};
match event {
SimEvent::Call(address) => program.track_call(address),
SimEvent::Return => program.track_return(),
SimEvent::Halt => program.track_halt(),
SimEvent::Interrupt(code, address) => program.track_interrupt(code, address),
SimEvent::Reti => program.track_reti(),
SimEvent::Marker(name) => {
if let Some(recording) = &mut self.recording {
recording.track_marker(name);
};
Ok(())
}
}
}
fn track_inline_stack(&mut self, inline_stack: InlineStack) {
if let Some(program) = &mut self.program {
program.track_inline_stack(inline_stack);
}
}
fn start_recording(&mut self) {
if let Some(program) = &self.program {
self.recording = Some(Recording::new(program));
}
}
fn finish_recording(&mut self) -> Option<Vec<u8>> {
self.recording.take().map(|r| r.finish())
}
fn cancel_recording(&mut self) {
self.recording.take();
}
}
+152
View File
@@ -0,0 +1,152 @@
use std::{borrow::Cow, collections::HashMap};
use fxprof_processed_profile::{
CategoryHandle, CpuDelta, Frame, FrameFlags, FrameInfo, MarkerTiming, ProcessHandle, Profile,
ReferenceTimestamp, SamplingInterval, StackHandle, StaticSchemaMarker, StringHandle,
ThreadHandle, Timestamp,
};
use crate::profiler::state::{ProgramState, RESET_CODE, StackFrame};
pub struct Recording {
profile: Profile,
process: ProcessHandle,
threads: HashMap<u16, ThreadHandle>,
now: u64,
}
impl Recording {
pub fn new(state: &ProgramState) -> Self {
let reference_timestamp = ReferenceTimestamp::from_millis_since_unix_epoch(0.0);
let interval = SamplingInterval::from_hz(20_000_000.0);
let mut profile = Profile::new(state.name(), reference_timestamp, interval);
let process =
profile.add_process(state.name(), 1, Timestamp::from_nanos_since_reference(0));
let lib = profile.add_lib(state.library_info().clone());
profile.add_lib_mapping(process, lib, 0x00000000, 0xffffffff, 0);
let mut me = Self {
profile,
process,
threads: HashMap::new(),
now: 0,
};
me.track_elapsed_cycles(state, 0);
me
}
pub fn track_elapsed_cycles(&mut self, state: &ProgramState, cycles: u32) {
self.now += cycles as u64;
let timestamp = Timestamp::from_nanos_since_reference(self.now * 50);
let weight = 1;
let active_code = if let Some((code, frames)) = state.current_stack() {
let thread = *self.threads.entry(code).or_insert_with(|| {
let process = self.process;
let tid = code as u32;
let start_time = Timestamp::from_nanos_since_reference(self.now * 50);
let is_main = code == RESET_CODE;
let thread = self.profile.add_thread(process, tid, start_time, is_main);
self.profile
.set_thread_name(thread, &thread_name_for_code(code));
thread
});
let stack = self.handle_for_stack(thread, frames);
let cpu_delta = CpuDelta::from_nanos((self.now - cycles as u64) * 50);
self.profile
.add_sample(thread, timestamp, stack, cpu_delta, weight);
Some(code)
} else {
None
};
for (code, thread) in &self.threads {
if active_code == Some(*code) {
continue;
}
self.profile
.add_sample_same_stack_zero_cpu(*thread, timestamp, weight);
}
}
pub fn track_marker(&mut self, name: Cow<'static, str>) {
let Some(thread) = self.threads.get(&RESET_CODE) else {
return;
};
let timing = MarkerTiming::Instant(Timestamp::from_nanos_since_reference(self.now * 50));
let marker = SimpleMarker(name);
self.profile.add_marker(*thread, timing, marker);
}
pub fn finish(self) -> Vec<u8> {
serde_json::to_vec(&self.profile).expect("could not serialize profile")
}
fn handle_for_stack(
&mut self,
thread: ThreadHandle,
frames: &[StackFrame],
) -> Option<StackHandle> {
let frames = frames
.iter()
.map(|f| {
let frame = match f {
StackFrame::Address(address) => Frame::InstructionPointer(*address as u64),
StackFrame::Label(label) => Frame::Label(self.profile.intern_string(label)),
};
FrameInfo {
frame,
category_pair: CategoryHandle::OTHER.into(),
flags: FrameFlags::empty(),
}
})
.collect::<Vec<_>>();
self.profile.intern_stack_frames(thread, frames.into_iter())
}
}
struct SimpleMarker(Cow<'static, str>);
impl StaticSchemaMarker for SimpleMarker {
const UNIQUE_MARKER_TYPE_NAME: &'static str = "Simple";
const FIELDS: &'static [fxprof_processed_profile::StaticSchemaMarkerField] = &[];
fn name(&self, profile: &mut Profile) -> StringHandle {
profile.intern_string(&self.0)
}
fn category(&self, _profile: &mut Profile) -> CategoryHandle {
CategoryHandle::OTHER
}
fn string_field_value(&self, _field_index: u32) -> StringHandle {
unreachable!()
}
fn number_field_value(&self, _field_index: u32) -> f64 {
unreachable!()
}
}
fn thread_name_for_code(code: u16) -> std::borrow::Cow<'static, str> {
match code {
RESET_CODE => "Main".into(),
0xffd0 => "Duplexed exception".into(),
0xfe40 => "VIP interrupt".into(),
0xfe30 => "Communication interrupt".into(),
0xfe20 => "Game pak interrupt".into(),
0xfe10 => "Timer interrupt".into(),
0xfe00 => "Game pad interrupt".into(),
0xffc0 => "Address trap".into(),
0xffa0..0xffc0 => format!("Trap (vector {})", code - 0xffa0).into(),
0xff90 => "Illegal opcode exception".into(),
0xff80 => "Zero division exception".into(),
0xff60 => "Floating-point reserved operand exception".into(),
0xff70 => "Floating-point invalid operation exception".into(),
0xff68 => "Floating-point zero division exception".into(),
0xff64 => "Floating-point overflow exception".into(),
other => format!("Unrecognized handler (0x{other:04x})").into(),
}
}
+129
View File
@@ -0,0 +1,129 @@
use std::{collections::HashMap, sync::Arc};
use anyhow::{Result, bail};
use fxprof_processed_profile::LibraryInfo;
use crate::emulator::{GameInfo, InlineStack};
pub struct ProgramState {
info: Arc<GameInfo>,
call_stacks: HashMap<u16, Vec<StackFrame>>,
context_stack: Vec<u16>,
}
pub enum StackFrame {
Address(u32),
Label(Arc<String>),
}
pub const RESET_CODE: u16 = 0xfff0;
impl ProgramState {
pub async fn new(info: Arc<GameInfo>) -> Self {
let mut call_stacks = HashMap::new();
call_stacks.insert(RESET_CODE, vec![StackFrame::Address(0xfffffff0)]);
Self {
info,
call_stacks,
context_stack: vec![RESET_CODE],
}
}
pub fn name(&self) -> &str {
self.info.name()
}
pub fn library_info(&self) -> &LibraryInfo {
self.info.library_info()
}
pub fn current_stack(&self) -> Option<(u16, &[StackFrame])> {
let code = self.context_stack.last()?;
let call_stack = self.call_stacks.get(code)?;
Some((*code, call_stack))
}
pub fn track_call(&mut self, address: u32) -> Result<()> {
let Some(code) = self.context_stack.last() else {
bail!("How did we call anything when we're halted?");
};
let Some(stack) = self.call_stacks.get_mut(code) else {
bail!("missing stack {code:04x}");
};
stack.push(StackFrame::Address(address));
Ok(())
}
pub fn track_return(&mut self) -> Result<()> {
let Some(code) = self.context_stack.last() else {
bail!("how did we return when we're halted?");
};
let Some(stack) = self.call_stacks.get_mut(code) else {
bail!("missing stack {code:04x}");
};
// just popping the inline frames first
while stack
.pop_if(|f| matches!(f, StackFrame::Label(_)))
.is_some()
{}
if stack.pop().is_none() {
bail!("returned from {code:04x} but stack was empty");
}
if stack.is_empty() {
bail!("returned to oblivion");
}
Ok(())
}
pub fn track_halt(&mut self) -> Result<()> {
let Some(RESET_CODE) = self.context_stack.pop() else {
bail!("halted when not in an interrupt");
};
Ok(())
}
pub fn track_interrupt(&mut self, code: u16, address: u32) -> Result<()> {
// if the CPU was halted before, wake it up now
if self.context_stack.is_empty() {
self.context_stack.push(RESET_CODE);
}
self.context_stack.push(code);
if self
.call_stacks
.insert(code, vec![StackFrame::Address(address)])
.is_some()
{
bail!("{code:04x} fired twice");
}
Ok(())
}
pub fn track_reti(&mut self) -> Result<()> {
let Some(code) = self.context_stack.pop() else {
bail!("RETI when halted");
};
if code == RESET_CODE {
bail!("RETI when not in interrupt");
}
if self.call_stacks.remove(&code).is_none() {
bail!("{code:04x} popped but never called");
}
Ok(())
}
pub fn track_inline_stack(&mut self, inline_stack: InlineStack) {
let Some(code) = self.context_stack.last() else {
return;
};
let Some(call_stack) = self.call_stacks.get_mut(code) else {
return;
};
while call_stack
.pop_if(|f| matches!(f, StackFrame::Label(_)))
.is_some()
{}
for label in inline_stack.iter() {
call_stack.push(StackFrame::Label(label.clone()));
}
}
}
+59
View File
@@ -0,0 +1,59 @@
use std::{path::PathBuf, sync::Arc};
pub use about::AboutWindow;
use egui::{Ui, ViewportBuilder};
pub use game::{ChildWindow, GameWindow};
pub use game_screen::{DisplayMode, GameScreen};
pub use gdb::GdbServerWindow;
pub use hotkeys::HotkeysWindow;
pub use input::InputWindow;
pub use profile::ProfileWindow;
pub use terminal::TerminalWindow;
pub use vip::{
BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow,
};
use winit::{event::KeyEvent, window::Window};
use crate::emulator::SimId;
mod about;
mod game;
mod game_screen;
mod gdb;
mod hotkeys;
mod input;
mod profile;
mod terminal;
mod utils;
mod vip;
pub trait AppWindow {
fn sim_id(&self) -> Option<SimId> {
None
}
fn image_url(&self, name: &str) -> String {
format!("vip://{}/{name}", self.sim_id().unwrap_or(SimId::Player1))
}
fn initial_viewport(&self) -> ViewportBuilder;
fn show(&mut self, ui: &mut Ui);
fn on_init(&mut self, args: InitArgs) {
let _ = args;
}
fn handle_key_event(&mut self, event: &KeyEvent) -> bool {
let _ = event;
false
}
fn handle_gamepad_event(&mut self, event: &gilrs::Event) -> bool {
let _ = event;
false
}
fn handle_dropped_file(&mut self, path: PathBuf) -> bool {
let _ = path;
false
}
}
pub struct InitArgs<'a> {
pub window: &'a Arc<Window>,
pub render_state: &'a egui_wgpu::RenderState,
}
+27
View File
@@ -0,0 +1,27 @@
use egui::{CentralPanel, Image, Ui, ViewportBuilder};
use super::AppWindow;
pub struct AboutWindow;
impl AppWindow for AboutWindow {
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("About Lemur")
.with_inner_size((300.0, 200.0))
}
fn show(&mut self, ui: &mut Ui) {
CentralPanel::default().show_inside(ui, |ui| {
ui.vertical_centered(|ui| {
ui.label("Lemur Virtual Boy Emulator");
ui.label(format!("Version {}", env!("CARGO_PKG_VERSION")));
ui.hyperlink("https://git.virtual-boy.com/PVB/lemur");
let logo = Image::new(egui::include_image!("../../assets/lemur-256x256.png"))
.max_width(256.0)
.maintain_aspect_ratio(true);
ui.add(logo);
});
});
}
}
+887
View File
@@ -0,0 +1,887 @@
use std::{
ops::{Deref, DerefMut},
path::PathBuf,
sync::{Arc, Mutex, atomic::AtomicBool, mpsc},
time::Duration,
};
use crate::{
app::UserEvent,
config::{COLOR_PRESETS, SimConfig},
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
filepicker::FilePicker,
images::ImageTextureLoader,
input::{Command, MappingProvider, ShortcutProvider},
memory::MemoryClient,
persistence::Persistence,
window::{
AboutWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GdbServerWindow,
HotkeysWindow, InitArgs, InputWindow, ObjectWindow, ProfileWindow, RegisterWindow,
TerminalWindow, WorldWindow, utils::UiData,
},
};
use anyhow::Context as _;
use egui::{
Align2, Button, CentralPanel, Color32, Context, Frame, MenuBar, Panel, Pos2, Ui, Vec2,
ViewportBuilder, ViewportCommand, ViewportId, ViewportIdMap, Window,
};
use egui_notify::{Anchor, Toast, Toasts};
use serde::{Deserialize, Serialize};
use winit::{event::KeyEvent, event_loop::EventLoopProxy};
use super::{
AppWindow,
game_screen::{DisplayMode, GameScreen},
utils::UiExt as _,
};
pub struct GameWindow {
viewport_id: ViewportId,
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
shortcuts: ShortcutProvider,
sim_id: SimId,
config: SimConfig,
toasts: Toasts,
screen: Option<GameScreen>,
messages: mpsc::Receiver<Toast>,
message_sink: mpsc::Sender<Toast>,
color_picker: Option<ColorPickerState>,
file_picker: FilePicker,
memory: Arc<MemoryClient>,
images: Arc<ImageTextureLoader>,
mappings: MappingProvider,
children: ViewportIdMap<ChildWindowWrapper>,
child_states: UiData<ViewportIdMap<ChildState>>,
queued_children: Vec<ChildWindow>,
show_player_name: bool,
}
impl GameWindow {
#[expect(clippy::too_many_arguments)]
pub fn new(
viewport_id: ViewportId,
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
shortcuts: ShortcutProvider,
memory: &Arc<MemoryClient>,
images: &Arc<ImageTextureLoader>,
mappings: MappingProvider,
sim_id: SimId,
) -> Self {
let config = SimConfig::load(&persistence, sim_id);
let _ = proxy.send_event(UserEvent::LowBattery(sim_id, config.low_battery));
client.send_command(EmulatorCommand::WatchRom(sim_id, config.watch_rom));
let toasts = Toasts::new()
.with_anchor(Anchor::BottomLeft)
.with_margin((10.0, 10.0).into())
.reverse(true);
let (message_sink, messages) = mpsc::channel();
Self {
viewport_id,
client,
proxy,
persistence,
shortcuts,
sim_id,
config,
toasts,
screen: None,
messages,
message_sink,
color_picker: None,
file_picker: FilePicker::new(),
memory: memory.clone(),
images: images.clone(),
mappings: mappings.clone(),
children: ViewportIdMap::default(),
child_states: UiData::new(),
queued_children: vec![],
show_player_name: false,
}
}
pub fn open(&mut self, window: ChildWindow) {
// queue opening this child until our next render call,
// so that we get a chance to look up its size
self.queued_children.push(window);
}
pub fn close(&mut self, window: ChildWindow) {
let id = window.viewport_id(self.sim_id);
self.children.remove(&id);
}
fn do_open(&mut self, window: ChildWindow) {
let viewport_id = window.viewport_id(self.sim_id);
if self.children.contains_key(&viewport_id) {
return;
}
let child = match window {
ChildWindow::About => AppWrapper::of_dyn(AboutWindow),
ChildWindow::CharacterData => AppWrapper::of_dyn(CharacterDataWindow::new(
self.sim_id,
&self.memory,
&self.images,
)),
ChildWindow::BgMap => {
AppWrapper::of_dyn(BgMapWindow::new(self.sim_id, &self.memory, &self.images))
}
ChildWindow::Objects => {
AppWrapper::of_dyn(ObjectWindow::new(self.sim_id, &self.memory, &self.images))
}
ChildWindow::Worlds => {
AppWrapper::of_dyn(WorldWindow::new(self.sim_id, &self.memory, &self.images))
}
ChildWindow::FrameBuffers => AppWrapper::of_dyn(FrameBufferWindow::new(
self.sim_id,
&self.memory,
&self.images,
)),
ChildWindow::Registers => {
AppWrapper::of_dyn(RegisterWindow::new(self.sim_id, &self.memory))
}
ChildWindow::Terminal => {
AppWrapper::of_dyn(TerminalWindow::new(self.sim_id, &self.client))
}
ChildWindow::Profiler { launch } => {
let mut profile = ProfileWindow::new(self.sim_id, self.client.clone());
if launch {
profile.launch();
}
AppWrapper::of_dyn(profile)
}
ChildWindow::Debugger { port } => {
let mut debugger =
GdbServerWindow::new(self.sim_id, self.client.clone(), self.proxy.clone());
if let Some(port) = port {
debugger.launch(port);
}
AppWrapper::of_dyn(debugger)
}
ChildWindow::Input => AppWrapper::of_dyn(InputWindow::new(self.mappings.clone())),
ChildWindow::Hotkeys => AppWrapper::of_dyn(HotkeysWindow::new(self.shortcuts.clone())),
ChildWindow::Player2 => {
if self.sim_id == SimId::Player2 {
return;
}
AppWrapper::of_game(GameWindow::new(
viewport_id,
self.client.clone(),
self.proxy.clone(),
self.persistence.clone(),
self.shortcuts.clone(),
&self.memory,
&self.images,
self.mappings.clone(),
SimId::Player2,
))
}
};
let mut viewport = child.initial_viewport();
let title = viewport.title.clone().unwrap();
let sim_id = child.sim_id();
if let Some(sim_id) = sim_id
&& self.show_player_name
{
viewport.title = Some(format!("{title} ({sim_id})"));
}
if let Some(state) = self.child_states.get(&viewport_id) {
viewport.position = Some(state.position);
viewport.inner_size = Some(state.size);
}
self.children.insert(
viewport_id,
ChildWindowWrapper {
app: Arc::new(Mutex::new(child)),
title,
sim_id,
updates: Some(viewport),
close_requested: Arc::new(AtomicBool::new(false)),
},
);
}
pub fn handle_init(&mut self, viewport_id: ViewportId, args: InitArgs) {
self.handle_event(viewport_id, |window| window.on_init(args))
}
pub fn handle_key_event(&mut self, viewport_id: ViewportId, event: &KeyEvent) -> bool {
self.handle_event(viewport_id, |window| window.handle_key_event(event))
}
pub fn handle_gamepad_event(&mut self, viewport_id: ViewportId, event: &gilrs::Event) -> bool {
self.handle_event(viewport_id, |window| window.handle_gamepad_event(event))
}
pub fn handle_dropped_file(&mut self, viewport_id: ViewportId, path: PathBuf) -> bool {
self.handle_event(viewport_id, |window| window.handle_dropped_file(path))
}
fn handle_event<F, R>(&mut self, viewport_id: ViewportId, cb: F) -> R
where
F: FnOnce(&mut dyn AppWindow) -> R,
R: Default,
{
let p2_viewport_id = ChildWindow::Player2.viewport_id(SimId::Player1);
if self.viewport_id == viewport_id {
cb(self)
} else if let Some(child) = self.children.get_mut(&viewport_id) {
cb(child.app.lock().unwrap().deref_mut().deref_mut())
} else if let Some(mut p2) = self
.children
.get_mut(&p2_viewport_id)
.map(|w| w.app.lock().unwrap())
{
if let AppWrapper::Game(g) = p2.deref_mut() {
g.handle_event(viewport_id, cb)
} else {
R::default()
}
} else {
R::default()
}
}
fn show_menu(&mut self, ui: &mut Ui) {
let state = self.client.emulator_state();
let is_ready = self.client.sim_state(self.sim_id) == SimState::Ready;
let can_pause = is_ready && state == EmulatorState::Running;
let can_resume = is_ready && state == EmulatorState::Paused;
let can_frame_advance = is_ready && state != EmulatorState::Debugging;
for command in ui.input_mut(|input| self.shortcuts.consume_all(input)) {
match command {
Command::OpenRom => {
let rom = self
.file_picker
.pick_file_dialog()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom", "elf", "isx"])
.open();
if let Some(path) = rom {
self.client
.send_command(EmulatorCommand::LoadGame(self.sim_id, path));
}
}
Command::ReloadRom => {
self.client
.send_command(EmulatorCommand::ReloadRom(self.sim_id));
}
Command::Quit => {
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
}
Command::PauseResume => {
if state == EmulatorState::Paused && can_resume {
self.client.send_command(EmulatorCommand::Resume);
}
if state == EmulatorState::Running && can_pause {
self.client.send_command(EmulatorCommand::Pause);
}
}
Command::Reset => {
if is_ready {
self.client
.send_command(EmulatorCommand::Reset(self.sim_id));
}
}
Command::FrameAdvance => {
if can_frame_advance {
self.client.send_command(EmulatorCommand::FrameAdvance);
}
}
Command::FastForward(speed) => {
self.client
.send_command(EmulatorCommand::SetSpeed(speed as f64));
}
Command::Screenshot => {
let autopause = state == EmulatorState::Running && can_pause;
if autopause {
self.client.send_command(EmulatorCommand::Pause);
}
pollster::block_on(self.take_screenshot());
if autopause {
self.client.send_command(EmulatorCommand::Resume);
}
}
}
}
ui.menu_button("ROM", |ui| {
if ui
.add(self.button_for(ui.ctx(), "Open ROM", Command::OpenRom))
.clicked()
{
let rom = self
.file_picker
.pick_file_dialog()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom", "elf", "isx"])
.open();
if let Some(path) = rom {
self.client
.send_command(EmulatorCommand::LoadGame(self.sim_id, path));
}
}
if ui
.add(self.button_for(ui.ctx(), "Reload ROM", Command::ReloadRom))
.clicked()
{
self.client
.send_command(EmulatorCommand::ReloadRom(self.sim_id));
}
let watch_rom = self.client.is_rom_watched(self.sim_id);
if ui.selectable_button(watch_rom, "Watch ROM").clicked() {
self.update_config(|c| c.watch_rom = !watch_rom);
self.client
.send_command(EmulatorCommand::WatchRom(self.sim_id, !watch_rom));
}
if ui
.add(self.button_for(ui.ctx(), "Quit", Command::Quit))
.clicked()
{
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
}
});
ui.menu_button("Emulation", |ui| {
if state == EmulatorState::Running {
if ui
.add_enabled(
can_pause,
self.button_for(ui.ctx(), "Pause", Command::PauseResume),
)
.clicked()
{
self.client.send_command(EmulatorCommand::Pause);
}
} else if ui
.add_enabled(
can_resume,
self.button_for(ui.ctx(), "Resume", Command::PauseResume),
)
.clicked()
{
self.client.send_command(EmulatorCommand::Resume);
}
if ui
.add_enabled(is_ready, self.button_for(ui.ctx(), "Reset", Command::Reset))
.clicked()
{
self.client
.send_command(EmulatorCommand::Reset(self.sim_id));
}
ui.separator();
if ui
.add_enabled(
can_frame_advance,
self.button_for(ui.ctx(), "Frame Advance", Command::FrameAdvance),
)
.clicked()
{
self.client.send_command(EmulatorCommand::FrameAdvance);
}
ui.separator();
if ui
.add_enabled(
is_ready,
self.button_for(ui.ctx(), "Screenshot", Command::Screenshot),
)
.clicked()
{
pollster::block_on(self.take_screenshot());
}
ui.separator();
let low_battery = self.config.low_battery;
if ui.selectable_button(low_battery, "Low Battery").clicked() {
self.update_config(|c| c.low_battery = !low_battery);
let _ = self
.proxy
.send_event(UserEvent::LowBattery(self.sim_id, !low_battery));
}
});
ui.menu_button("Options", |ui| self.show_options_menu(ui));
ui.menu_button("Multiplayer", |ui| {
let has_player_2 = self.client.has_player_2();
if self.sim_id == SimId::Player1
&& !has_player_2
&& ui.button("Open Player 2").clicked()
{
self.client
.send_command(EmulatorCommand::StartSecondSim(None));
self.open(ChildWindow::Player2);
}
if has_player_2 {
let linked = self.client.are_sims_linked();
if linked && ui.button("Unlink").clicked() {
self.client.send_command(EmulatorCommand::Unlink);
}
if !linked && ui.button("Link").clicked() {
self.client.send_command(EmulatorCommand::Link);
}
}
});
ui.menu_button("Tools", |ui| {
if ui.button("Terminal").clicked() {
self.open(ChildWindow::Terminal);
}
if ui.button("Profiler").clicked() {
self.open(ChildWindow::Profiler { launch: false });
}
if ui.button("GDB Server").clicked() {
self.open(ChildWindow::Debugger { port: None });
}
ui.separator();
if ui.button("Character Data").clicked() {
self.open(ChildWindow::CharacterData);
}
if ui.button("Background Maps").clicked() {
self.open(ChildWindow::BgMap);
}
if ui.button("Objects").clicked() {
self.open(ChildWindow::Objects);
}
if ui.button("Worlds").clicked() {
self.open(ChildWindow::Worlds);
}
if ui.button("Frame Buffers").clicked() {
self.open(ChildWindow::FrameBuffers);
}
if ui.button("Registers").clicked() {
self.open(ChildWindow::Registers);
}
});
ui.menu_button("Help", |ui| {
if ui.button("About").clicked() {
self.open(ChildWindow::About);
}
});
}
async fn take_screenshot(&mut self) {
match self.try_take_screenshot().await {
Ok(Some(path)) => {
let mut toast = Toast::info(format!("Saved to {path}"));
toast.duration(Some(Duration::from_secs(5)));
self.toasts.add(toast);
}
Ok(None) => {}
Err(error) => {
let mut toast = Toast::error(format!("{error:#}"));
toast.duration(Some(Duration::from_secs(5)));
self.toasts.add(toast);
}
}
}
async fn try_take_screenshot(&self) -> anyhow::Result<Option<String>> {
let (tx, rx) = oneshot::channel();
self.client
.send_command(EmulatorCommand::Screenshot(self.sim_id, tx));
let bytes = rx.await.context("Could not take screenshot")?;
let file = self
.file_picker
.save_file_dialog()
.add_filter("PNG images", &["png"])
.set_file_name("screenshot.png")
.open();
let Some(path) = file else {
return Ok(None);
};
if bytes.len() != 384 * 224 * 2 {
anyhow::bail!("Unexpected screenshot size");
}
let mut screencap = image::GrayImage::new(384 * 2, 224);
for (index, pixel) in bytes.into_iter().enumerate() {
let x = (index / 2) % 384 + ((index % 2) * 384);
let y = (index / 2) / 384;
screencap.put_pixel(x as u32, y as u32, image::Luma([pixel]));
}
screencap.save(&path).context("Could not save screenshot")?;
Ok(Some(path.display().to_string()))
}
fn show_options_menu(&mut self, ui: &mut Ui) {
ui.menu_button("Video", |ui| {
ui.menu_button("Screen Size", |ui| {
let current_dims = self.config.dimensions;
for scale in 1..=4 {
let label = format!("x{scale}");
let scale = scale as f32;
let dims = {
let Vec2 { x, y } = self.config.display_mode.proportions();
Vec2::new(x * scale, y * scale + 22.0)
};
if ui
.selectable_button((current_dims - dims).length() < 1.0, label)
.clicked()
{
ui.send_viewport_cmd(ViewportCommand::InnerSize(dims));
}
}
});
ui.menu_button("Display Mode", |ui| {
let old_proportions = self.config.display_mode.proportions();
let mut changed = false;
let mut display_mode = self.config.display_mode;
changed |= ui
.selectable_option(&mut display_mode, DisplayMode::Anaglyph, "Anaglyph")
.clicked();
changed |= ui
.selectable_option(&mut display_mode, DisplayMode::LeftEye, "Left Eye")
.clicked();
changed |= ui
.selectable_option(&mut display_mode, DisplayMode::RightEye, "Right Eye")
.clicked();
changed |= ui
.selectable_option(&mut display_mode, DisplayMode::SideBySide, "Side by Side")
.clicked();
if !changed {
return;
}
let current_dims = self.config.dimensions;
let new_proportions = display_mode.proportions();
let scale = new_proportions / old_proportions;
if scale != Vec2::new(1.0, 1.0) {
ui.send_viewport_cmd(ViewportCommand::InnerSize(current_dims * scale));
}
self.update_config(|c| {
c.display_mode = display_mode;
c.dimensions = current_dims * scale;
});
});
ui.menu_button("Colors", |ui| {
for preset in COLOR_PRESETS {
if ui.color_pair_button(preset[0], preset[1]).clicked() {
self.update_config(|c| c.colors = preset);
}
}
ui.with_layout(ui.layout().with_cross_align(egui::Align::Center), |ui| {
if ui.button("Custom").clicked() {
let color_str = |color: Color32| {
format!("{:02x}{:02x}{:02x}", color.r(), color.g(), color.b())
};
let is_running = self.client.emulator_state() == EmulatorState::Running;
if is_running {
self.client.send_command(EmulatorCommand::Pause);
}
let color_codes = [
color_str(self.config.colors[0]),
color_str(self.config.colors[1]),
];
self.color_picker = Some(ColorPickerState {
color_codes,
just_opened: true,
unpause_on_close: is_running,
});
}
});
});
});
ui.menu_button("Audio", |ui| {
if ui
.selectable_button(self.config.audio_enabled, "Enabled")
.clicked()
{
self.update_config(|c| c.audio_enabled = !c.audio_enabled);
self.client.send_command(EmulatorCommand::SetAudioEnabled(
self.sim_id,
self.config.audio_enabled,
));
}
});
ui.menu_button("Input", |ui| {
if ui.button("Bind Inputs").clicked() {
self.open(ChildWindow::Input);
}
});
if ui.button("Hotkeys").clicked() {
self.open(ChildWindow::Hotkeys);
}
}
fn show_color_picker(&mut self, ui: &mut Ui) {
let mut colors = self.config.colors;
let Some(state) = self.color_picker.as_mut() else {
return;
};
let (open, updated) = ui
.horizontal(|ui| {
let left_color = ui.color_picker(&mut colors[0], &mut state.color_codes[0]);
if state.just_opened {
left_color.request_focus();
state.just_opened = false;
}
let right_color = ui.color_picker(&mut colors[1], &mut state.color_codes[1]);
let open = left_color.has_focus() || right_color.has_focus();
let updated = left_color.changed() || right_color.changed();
(open, updated)
})
.inner;
if !open {
if state.unpause_on_close {
self.client.send_command(EmulatorCommand::Resume);
}
self.color_picker = None;
}
if updated {
self.update_config(|c| c.colors = colors);
}
}
fn update_config(&mut self, update: impl FnOnce(&mut SimConfig)) {
let mut new_config = self.config.clone();
update(&mut new_config);
if self.config != new_config {
let _ = new_config.save(&self.persistence, self.sim_id);
}
self.config = new_config;
}
fn button_for(&self, ctx: &Context, text: &str, command: Command) -> Button<'_> {
let button = Button::new(text);
match self.shortcuts.shortcut_for(command) {
Some(shortcut) => button.shortcut_text(ctx.format_shortcut(&shortcut)),
None => button,
}
}
}
impl AppWindow for GameWindow {
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
let builder = ViewportBuilder::default()
.with_title("Lemur")
.with_inner_size(self.config.dimensions);
if let Some(position) = self.config.position {
builder.with_position(position)
} else {
builder
}
}
fn show(&mut self, ui: &mut Ui) {
let has_player_2 = self.client.has_player_2();
if !self.show_player_name && has_player_2 {
ui.send_viewport_cmd(ViewportCommand::Title(format!("Lemur ({})", self.sim_id)));
for (viewport_id, child) in &self.children {
if let Some(sim_id) = child.sim_id {
ui.send_viewport_cmd_to(
*viewport_id,
ViewportCommand::Title(format!("{} ({sim_id})", child.title)),
);
}
}
self.show_player_name = true;
} else if self.show_player_name && !has_player_2 {
ui.send_viewport_cmd(ViewportCommand::Title("Lemur".to_string()));
for (viewport_id, child) in &self.children {
if child.sim_id.is_some() {
ui.send_viewport_cmd_to(
*viewport_id,
ViewportCommand::Title(child.title.clone()),
);
}
}
self.show_player_name = false;
}
self.child_states.load(ui);
let dimensions = {
let bounds = ui.content_rect();
bounds.max - bounds.min
};
let position = ui.input(|i| i.viewport().outer_rect.map(|r| r.min));
self.update_config(|c| {
c.dimensions = dimensions;
c.position = position;
});
while let Ok(toast) = self.messages.try_recv() {
self.toasts.add(toast);
}
Panel::top("menubar")
.exact_size(22.0)
.show_inside(ui, |ui| {
MenuBar::new().ui(ui, |ui| {
self.show_menu(ui);
});
});
if self.color_picker.is_some() {
Window::new("Color Picker")
.title_bar(false)
.resizable(false)
.anchor(Align2::CENTER_CENTER, Vec2::ZERO)
.show(ui, |ui| {
self.show_color_picker(ui);
});
}
let frame = Frame::central_panel(ui.style()).fill(Color32::BLACK);
CentralPanel::default().frame(frame).show_inside(ui, |ui| {
if let Some(screen) = self.screen.as_mut() {
screen.update(self.config.display_mode, self.config.colors);
ui.add(screen);
}
});
self.toasts.show(ui);
for window in std::mem::take(&mut self.queued_children) {
self.do_open(window);
}
self.children.retain(|id, child| {
if child
.close_requested
.load(std::sync::atomic::Ordering::Relaxed)
{
return false;
}
let app = child.app.clone();
let viewport_builder = child.updates.take().unwrap_or_default();
let close_requested = child.close_requested.clone();
if let (Some(outer), Some(inner)) = ui.input_for(*id, |inp| {
let viewport = inp.viewport();
(viewport.outer_rect, viewport.inner_rect)
}) {
self.child_states.insert(
*id,
ChildState {
position: outer.min,
size: inner.size(),
},
);
}
ui.show_viewport_deferred(*id, viewport_builder, move |ui, _| {
app.lock().unwrap().show(ui);
if ui.input(|s| s.viewport().close_requested()) {
close_requested.store(true, std::sync::atomic::Ordering::Relaxed);
}
});
true
});
self.child_states.save(ui);
ui.request_repaint_after(Duration::from_millis(10));
}
fn on_init(&mut self, args: InitArgs) {
if self.screen.is_none() {
let (screen, sink) = GameScreen::init(args.render_state);
self.client.send_command(EmulatorCommand::ConnectToSim(
self.sim_id,
sink,
self.message_sink.clone(),
));
self.screen = Some(screen);
}
self.file_picker.set_window(args.window);
}
fn handle_dropped_file(&mut self, path: PathBuf) -> bool {
self.client
.send_command(EmulatorCommand::LoadGame(self.sim_id, path));
true
}
}
impl Drop for GameWindow {
fn drop(&mut self) {
if self.sim_id == SimId::Player2 {
self.client.send_command(EmulatorCommand::StopSecondSim);
}
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
}
}
struct ColorPickerState {
color_codes: [String; 2],
just_opened: bool,
unpause_on_close: bool,
}
struct ChildWindowWrapper {
app: Arc<Mutex<AppWrapper>>,
title: String,
sim_id: Option<SimId>,
updates: Option<ViewportBuilder>,
close_requested: Arc<AtomicBool>,
}
enum AppWrapper {
Game(Box<GameWindow>),
Dyn(Box<dyn AppWindow + Send + 'static>),
}
impl AppWrapper {
fn of_game(game: GameWindow) -> Self {
Self::Game(Box::new(game))
}
fn of_dyn<T: AppWindow + Send + 'static>(inner: T) -> Self {
Self::Dyn(Box::new(inner))
}
}
impl Deref for AppWrapper {
type Target = dyn AppWindow + Send + 'static;
fn deref(&self) -> &Self::Target {
match self {
Self::Dyn(inner) => inner.as_ref(),
Self::Game(inner) => inner.as_ref(),
}
}
}
impl DerefMut for AppWrapper {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
Self::Dyn(inner) => inner.as_mut(),
Self::Game(inner) => inner.as_mut(),
}
}
}
#[derive(Debug)]
pub enum ChildWindow {
About,
CharacterData,
BgMap,
Objects,
Worlds,
FrameBuffers,
Registers,
Terminal,
Profiler { launch: bool },
Debugger { port: Option<u16> },
Input,
Hotkeys,
Player2,
}
impl ChildWindow {
fn viewport_id(&self, sim_id: SimId) -> ViewportId {
ViewportId::from_hash_of(format!("{sim_id:?}{}", self.name()))
}
fn name(&self) -> &'static str {
match self {
Self::About => "About",
Self::CharacterData => "CharacterData",
Self::BgMap => "BgMap",
Self::Objects => "Objects",
Self::Worlds => "Worlds",
Self::FrameBuffers => "FrameBuffers",
Self::Registers => "Registers",
Self::Terminal => "Terminal",
Self::Profiler { .. } => "Profiler",
Self::Debugger { .. } => "Debugger",
Self::Input => "Input",
Self::Hotkeys => "Hotkeys",
Self::Player2 => "Player2",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
struct ChildState {
position: Pos2,
size: Vec2,
}
+277
View File
@@ -0,0 +1,277 @@
use std::{collections::HashMap, sync::Arc};
use egui::{Color32, Rgba, Vec2, Widget};
use serde::{Deserialize, Serialize};
use wgpu::{BindGroup, BindGroupLayout, Buffer, RenderPipeline, util::DeviceExt as _};
use crate::graphics::TextureSink;
pub struct GameScreen {
bind_group: Arc<BindGroup>,
color_buffer: Arc<Buffer>,
display_mode: DisplayMode,
colors: Colors,
}
impl GameScreen {
pub fn init_pipeline(render_state: &egui_wgpu::RenderState) {
let device = &render_state.device;
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("texture bind group layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let shader = device.create_shader_module(wgpu::include_wgsl!("../game.wgsl"));
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("render pipeline layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
let create_render_pipeline = |entry_point: &str| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("render pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some(entry_point),
targets: &[Some(wgpu::ColorTargetState {
format: render_state.target_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: Some(wgpu::Face::Back),
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
};
let mut render_pipelines = HashMap::new();
render_pipelines.insert(DisplayMode::Anaglyph, create_render_pipeline("fs_anaglyph"));
render_pipelines.insert(DisplayMode::LeftEye, create_render_pipeline("fs_lefteye"));
render_pipelines.insert(DisplayMode::RightEye, create_render_pipeline("fs_righteye"));
render_pipelines.insert(
DisplayMode::SideBySide,
create_render_pipeline("fs_sidebyside"),
);
render_state
.renderer
.write()
.callback_resources
.insert(SharedGameScreenResources {
render_pipelines,
bind_group_layout,
});
}
pub fn init(render_state: &egui_wgpu::RenderState) -> (Self, TextureSink) {
let device = &render_state.device;
let queue = &render_state.queue;
let (sink, texture_view) = TextureSink::new(device, queue.clone());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default());
let colors = Colors::new(
Color32::from_rgb(0xff, 0x00, 0x00),
Color32::from_rgb(0x00, 0xc6, 0xf0),
);
let color_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("colors"),
contents: bytemuck::bytes_of(&colors),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let renderer = render_state.renderer.read();
let resources: &SharedGameScreenResources = renderer.callback_resources.get().unwrap();
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bind group"),
layout: &resources.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: color_buffer.as_entire_binding(),
},
],
});
(
Self {
bind_group: Arc::new(bind_group),
color_buffer: Arc::new(color_buffer),
display_mode: DisplayMode::Anaglyph,
colors,
},
sink,
)
}
pub fn update(&mut self, display_mode: DisplayMode, colors: [Color32; 2]) {
self.display_mode = display_mode;
self.colors = Colors::new(colors[0], colors[1]);
}
}
impl Widget for &mut GameScreen {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
let response = ui.allocate_rect(ui.clip_rect(), egui::Sense::hover());
let callback = egui_wgpu::Callback::new_paint_callback(
response.rect,
GameScreenCallback {
bind_group: self.bind_group.clone(),
color_buffer: self.color_buffer.clone(),
display_mode: self.display_mode,
colors: self.colors,
},
);
ui.painter().add(callback);
response
}
}
struct GameScreenCallback {
bind_group: Arc<BindGroup>,
color_buffer: Arc<Buffer>,
display_mode: DisplayMode,
colors: Colors,
}
impl egui_wgpu::CallbackTrait for GameScreenCallback {
fn prepare(
&self,
_device: &wgpu::Device,
queue: &wgpu::Queue,
_screen_descriptor: &egui_wgpu::ScreenDescriptor,
_egui_encoder: &mut wgpu::CommandEncoder,
_callback_resources: &mut egui_wgpu::CallbackResources,
) -> Vec<wgpu::CommandBuffer> {
queue.write_buffer(&self.color_buffer, 0, bytemuck::bytes_of(&self.colors));
vec![]
}
fn paint(
&self,
info: egui::PaintCallbackInfo,
render_pass: &mut wgpu::RenderPass<'static>,
callback_resources: &egui_wgpu::CallbackResources,
) {
let resources: &SharedGameScreenResources = callback_resources.get().unwrap();
let viewport = info.viewport_in_pixels();
let left = viewport.left_px as f32;
let top = viewport.top_px as f32;
let width = viewport.width_px as f32;
let height = viewport.height_px as f32;
let aspect_ratio = {
let proportions = self.display_mode.proportions();
proportions.x / proportions.y
};
let w = width.min(height * aspect_ratio);
let h = height.min(width / aspect_ratio);
let x = left + (width - w) / 2.0;
let y = top + (height - h) / 2.0;
let pipeline = resources
.render_pipelines
.get(&self.display_mode)
.unwrap_or_else(|| panic!("Unrecognized display mode {:?}", self.display_mode));
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, Some(self.bind_group.as_ref()), &[]);
render_pass.set_viewport(x, y, w, h, 0.0, 1.0);
render_pass.draw(0..6, 0..1);
}
}
struct SharedGameScreenResources {
render_pipelines: HashMap<DisplayMode, RenderPipeline>,
bind_group_layout: BindGroupLayout,
}
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
struct Colors {
left: Rgba,
right: Rgba,
}
impl Colors {
fn new(left: Color32, right: Color32) -> Self {
Self {
left: left.into(),
right: right.into(),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
pub enum DisplayMode {
Anaglyph,
LeftEye,
RightEye,
SideBySide,
}
impl DisplayMode {
pub fn proportions(self) -> Vec2 {
match self {
Self::SideBySide => Vec2::new(768.0, 224.0),
_ => Vec2::new(384.0, 224.0),
}
}
}
+107
View File
@@ -0,0 +1,107 @@
use egui::{Button, CentralPanel, TextEdit, ViewportBuilder};
use winit::event_loop::EventLoopProxy;
use crate::{
app::UserEvent,
emulator::{EmulatorClient, SimId, SimState},
gdbserver::{GdbServer, GdbServerStatus},
};
use super::AppWindow;
pub struct GdbServerWindow {
sim_id: SimId,
client: EmulatorClient,
port_str: String,
connected: bool,
quit_on_disconnect: bool,
server: GdbServer,
proxy: EventLoopProxy<UserEvent>,
}
impl GdbServerWindow {
pub fn new(sim_id: SimId, client: EmulatorClient, proxy: EventLoopProxy<UserEvent>) -> Self {
Self {
sim_id,
client: client.clone(),
port_str: (8080 + sim_id.to_index()).to_string(),
connected: false,
quit_on_disconnect: false,
server: GdbServer::new(sim_id, client),
proxy,
}
}
pub fn launch(&mut self, port: u16) {
self.server.stop();
self.port_str = port.to_string();
self.quit_on_disconnect = true;
self.server.start(port);
self.connected = true;
}
}
impl AppWindow for GdbServerWindow {
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("GDB Server")
.with_inner_size((300.0, 200.0))
}
fn show(&mut self, ui: &mut egui::Ui) {
let port_num: Option<u16> = self.port_str.parse().ok();
let status = self.server.status();
CentralPanel::default().show_inside(ui, |ui| {
ui.horizontal(|ui| {
if port_num.is_none() {
let style = ui.style_mut();
let error = style.visuals.error_fg_color;
style.visuals.widgets.active.bg_stroke.color = error;
style.visuals.widgets.hovered.bg_stroke.color = error;
}
ui.label("Port");
let port_editor = TextEdit::singleline(&mut self.port_str).desired_width(100.0);
ui.add_enabled(!status.running(), port_editor);
ui.checkbox(&mut self.quit_on_disconnect, "Quit on disconnect");
});
if !status.running() {
if self.connected && self.quit_on_disconnect {
self.proxy.send_event(UserEvent::Quit(self.sim_id)).unwrap();
} else {
self.connected = false;
}
let start_button = Button::new("Start");
let can_start =
port_num.is_some() && self.client.sim_state(self.sim_id) == SimState::Ready;
if ui.add_enabled(can_start, start_button).clicked() {
let port = port_num.unwrap();
self.server.start(port);
self.connected = true;
}
} else {
let stop_button = Button::new("Stop");
if ui.add(stop_button).clicked() {
self.server.stop();
}
}
match &status {
GdbServerStatus::Stopped => {}
GdbServerStatus::Connecting => {
ui.label("Connecting...");
}
GdbServerStatus::Running => {
ui.label("Running");
}
GdbServerStatus::Error(message) => {
ui.label(message);
}
}
});
}
}
+145
View File
@@ -0,0 +1,145 @@
use egui::{
Button, CentralPanel, Event, KeyboardShortcut, Label, Layout, Slider, Ui, ViewportBuilder,
};
use egui_extras::{Column, TableBuilder};
use crate::input::{Command, ShortcutProvider};
use super::{AppWindow, utils::UiExt};
pub struct HotkeysWindow {
shortcuts: ShortcutProvider,
now_binding: Option<Command>,
}
impl HotkeysWindow {
pub fn new(shortcuts: ShortcutProvider) -> Self {
Self {
shortcuts,
now_binding: None,
}
}
fn show_shortcuts(&mut self, ui: &mut Ui) {
let row_height = ui.spacing().interact_size.y;
ui.section("Shortcuts", |ui| {
let width = ui.available_width() - 16.0;
TableBuilder::new(ui)
.column(Column::exact(width * 0.3))
.column(Column::exact(width * 0.5))
.column(Column::exact(width * 0.2))
.cell_layout(Layout::left_to_right(egui::Align::Center))
.body(|mut body| {
for command in Command::all() {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_sized(ui.available_size(), Label::new(command.name()));
});
row.col(|ui| {
let button = if self.now_binding == Some(command) {
Button::new("Binding...")
} else if let Some(shortcut) = self.shortcuts.shortcut_for(command)
{
Button::new(ui.ctx().format_shortcut(&shortcut))
} else {
Button::new("")
};
if ui.add_sized(ui.available_size(), button).clicked() {
self.now_binding = Some(command);
}
});
row.col(|ui| {
if ui
.add_sized(ui.available_size(), Button::new("Clear"))
.clicked()
{
self.shortcuts.unset(command);
self.now_binding = None;
}
});
});
}
});
});
if let Some(command) = self.now_binding
&& let Some(shortcut) = ui.input_mut(|i| {
i.events.iter().find_map(|event| match event {
Event::Key {
key,
pressed: true,
modifiers,
..
} => Some(KeyboardShortcut::new(*modifiers, *key)),
_ => None,
})
})
{
self.shortcuts.set(command, shortcut);
self.now_binding = None;
}
}
fn show_ff_settings(&mut self, ui: &mut Ui) {
let row_height = ui.spacing().interact_size.y;
ui.section("Fast Forward", |ui| {
let width = ui.available_width() - 8.0;
let mut ff_settings = self.shortcuts.ff_settings();
let mut updated = false;
TableBuilder::new(ui)
.column(Column::exact(width * 0.5))
.column(Column::exact(width * 0.5))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Treat button as toggle");
});
row.col(|ui| {
if ui.checkbox(&mut ff_settings.toggle, "").changed() {
updated = true;
}
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Speed multiplier");
});
row.col(|ui| {
if ui
.add_sized(
ui.available_size(),
Slider::new(&mut ff_settings.speed, 1..=15),
)
.changed()
{
updated = true;
}
});
});
});
if updated {
self.shortcuts.update_ff_settings(ff_settings);
}
});
}
}
impl AppWindow for HotkeysWindow {
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Keyboard Shortcuts")
.with_inner_size((400.0, 400.0))
}
fn show(&mut self, ui: &mut Ui) {
CentralPanel::default().show_inside(ui, |ui| {
ui.horizontal(|ui| {
if ui.button("Use defaults").clicked() {
self.shortcuts.reset();
}
});
ui.separator();
self.show_shortcuts(ui);
self.show_ff_settings(ui);
});
}
}
+262
View File
@@ -0,0 +1,262 @@
use egui::{Button, CentralPanel, Label, Layout, Panel, Ui, ViewportBuilder};
use egui_extras::{Column, TableBuilder};
use gilrs::{EventType, GamepadId};
use std::sync::RwLock;
use crate::{
emulator::{SimId, VBKey},
input::{MappingProvider, Mappings},
};
use super::AppWindow;
pub struct InputWindow {
mappings: MappingProvider,
now_binding: Option<VBKey>,
active_tab: InputTab,
}
const KEY_NAMES: [(VBKey, &str); 14] = [
(VBKey::LU, "Up"),
(VBKey::LD, "Down"),
(VBKey::LL, "Left"),
(VBKey::LR, "Right"),
(VBKey::SEL, "Select"),
(VBKey::STA, "Start"),
(VBKey::B, "B"),
(VBKey::A, "A"),
(VBKey::LT, "L-Trigger"),
(VBKey::RT, "R-Trigger"),
(VBKey::RU, "R-Up"),
(VBKey::RD, "R-Down"),
(VBKey::RL, "R-Left"),
(VBKey::RR, "R-Right"),
];
impl InputWindow {
pub fn new(mappings: MappingProvider) -> Self {
Self {
mappings,
now_binding: None,
active_tab: InputTab::Player1,
}
}
fn show_bindings<T: Mappings>(
&mut self,
ui: &mut Ui,
mappings: &RwLock<T>,
bind_message: &str,
) {
ui.horizontal(|ui| {
if ui.button("Use defaults").clicked() {
mappings.write().unwrap().use_default_mappings();
self.mappings.save();
self.now_binding = None;
}
if ui.button("Clear all").clicked() {
mappings.write().unwrap().clear_all_mappings();
self.mappings.save();
self.now_binding = None;
}
});
ui.separator();
let mut names = {
let mapping = mappings.read().unwrap();
mapping.mapping_names()
};
TableBuilder::new(ui)
.column(Column::remainder())
.column(Column::remainder())
.cell_layout(Layout::left_to_right(egui::Align::Center))
.body(|mut body| {
for keys in KEY_NAMES.chunks_exact(2) {
body.row(20.0, |mut row| {
for (key, name) in keys {
let binding = names.remove(key).map(|mut s| {
s.sort();
s.join(", ")
});
row.col(|ui| {
let size = ui.available_size_before_wrap();
let width = size.x;
let height = size.y;
ui.add_sized((width * 0.2, height), Label::new(*name));
let label_text = if self.now_binding == Some(*key) {
bind_message
} else {
binding.as_deref().unwrap_or("")
};
if ui
.add_sized((width * 0.6, height), Button::new(label_text))
.clicked()
{
self.now_binding = Some(*key);
}
if ui
.add_sized(ui.available_size(), Button::new("Clear"))
.clicked()
{
let mut mapping = mappings.write().unwrap();
mapping.clear_mappings(*key);
drop(mapping);
self.mappings.save();
self.now_binding = None;
}
});
}
});
}
});
}
fn show_key_bindings(&mut self, ui: &mut Ui, sim_id: SimId) {
let mappings = self.mappings.for_sim(sim_id).clone();
self.show_bindings(ui, &mappings, "Press any key");
}
fn show_gamepads(&mut self, ui: &mut Ui) {
let mut gamepads = self.mappings.gamepad_info();
gamepads.sort_by_key(|g| usize::from(g.id));
if gamepads.is_empty() {
ui.label("No gamepads connected.");
return;
}
for (index, gamepad) in gamepads.into_iter().enumerate() {
ui.horizontal(|ui| {
ui.label(format!("Gamepad {index}: {}", gamepad.name));
let mut bound_to = gamepad.bound_to;
let mut rebind = false;
rebind |= ui
.selectable_value(&mut bound_to, Some(SimId::Player1), "Player 1")
.changed();
rebind |= ui
.selectable_value(&mut bound_to, Some(SimId::Player2), "Player 2")
.changed();
rebind |= ui.selectable_value(&mut bound_to, None, "Nobody").changed();
if rebind {
match bound_to {
Some(sim_id) => self.mappings.assign_gamepad(gamepad.id, sim_id),
None => self.mappings.unassign_gamepad(gamepad.id),
}
}
ui.separator();
if ui.button("Rebind").clicked() {
self.active_tab = InputTab::RebindGamepad(gamepad.id);
}
});
}
}
fn show_gamepad_bindings(&mut self, ui: &mut Ui, gamepad_id: GamepadId) {
let Some(mappings) = self.mappings.for_gamepad(gamepad_id) else {
self.active_tab = InputTab::Gamepads;
return;
};
self.show_bindings(ui, &mappings, "Press any input");
}
}
impl AppWindow for InputWindow {
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Bind Inputs")
.with_inner_size((600.0, 400.0))
}
fn show(&mut self, ui: &mut Ui) {
Panel::top("options").show_inside(ui, |ui| {
ui.horizontal(|ui| {
let old_active_tab = self.active_tab;
ui.selectable_value(&mut self.active_tab, InputTab::Player1, "Player 1");
ui.selectable_value(&mut self.active_tab, InputTab::Player2, "Player 2");
ui.selectable_value(&mut self.active_tab, InputTab::Gamepads, "Gamepads");
if matches!(self.active_tab, InputTab::RebindGamepad(_)) {
let tab = self.active_tab;
ui.selectable_value(&mut self.active_tab, tab, "Rebind Gamepad");
}
if old_active_tab != self.active_tab {
self.now_binding = None;
}
});
});
CentralPanel::default().show_inside(ui, |ui| {
match self.active_tab {
InputTab::Player1 => self.show_key_bindings(ui, SimId::Player1),
InputTab::Player2 => self.show_key_bindings(ui, SimId::Player2),
InputTab::Gamepads => self.show_gamepads(ui),
InputTab::RebindGamepad(id) => self.show_gamepad_bindings(ui, id),
};
});
}
fn handle_key_event(&mut self, event: &winit::event::KeyEvent) -> bool {
if !event.state.is_pressed() {
return false;
}
let sim_id = match self.active_tab {
InputTab::Player1 => SimId::Player1,
InputTab::Player2 => SimId::Player2,
_ => {
return false;
}
};
let Some(vb) = self.now_binding.take() else {
return false;
};
let mut mappings = self.mappings.for_sim(sim_id).write().unwrap();
mappings.add_keyboard_mapping(vb, event.physical_key);
drop(mappings);
self.mappings.save();
true
}
fn handle_gamepad_event(&mut self, event: &gilrs::Event) -> bool {
let InputTab::RebindGamepad(gamepad_id) = self.active_tab else {
return false;
};
if gamepad_id != event.id {
return false;
}
let Some(mappings) = self.mappings.for_gamepad(gamepad_id) else {
return false;
};
let Some(vb) = self.now_binding else {
return false;
};
match event.event {
EventType::ButtonPressed(_, code) => {
let mut mapping = mappings.write().unwrap();
mapping.add_button_mapping(vb, code);
self.now_binding.take();
true
}
EventType::AxisChanged(_, value, code) => {
if value < -0.75 {
let mut mapping = mappings.write().unwrap();
mapping.add_axis_neg_mapping(vb, code);
self.now_binding.take();
true
} else if value > 0.75 {
let mut mapping = mappings.write().unwrap();
mapping.add_axis_pos_mapping(vb, code);
self.now_binding.take();
true
} else {
false
}
}
_ => false,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum InputTab {
Player1,
Player2,
Gamepads,
RebindGamepad(GamepadId),
}
+168
View File
@@ -0,0 +1,168 @@
use std::{fs, time::Duration};
use anyhow::Result;
use egui::{Button, CentralPanel, Checkbox, Label, ViewportBuilder};
use egui_notify::{Anchor, Toast, Toasts};
use crate::{
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId},
filepicker::FilePicker,
profiler::{Profiler, ProfilerStatus},
window::{AppWindow, InitArgs},
};
pub struct ProfileWindow {
sim_id: SimId,
client: EmulatorClient,
profiler: Profiler,
toasts: Toasts,
file_picker: FilePicker,
}
impl ProfileWindow {
pub fn new(sim_id: SimId, client: EmulatorClient) -> Self {
Self {
sim_id,
client: client.clone(),
profiler: Profiler::new(sim_id, client),
toasts: Toasts::new()
.with_anchor(Anchor::BottomLeft)
.with_margin((10.0, 10.0).into())
.reverse(true),
file_picker: FilePicker::new(),
}
}
pub fn launch(&mut self) {
self.profiler.enable();
}
fn finish_recording(&mut self) {
let pause = matches!(self.client.emulator_state(), EmulatorState::Running);
if pause {
self.client.send_command(EmulatorCommand::Pause);
}
match self.try_finish_recording() {
Ok(Some(path)) => {
let mut toast = Toast::info(format!("Saved to {path}"));
toast.duration(Some(Duration::from_secs(5)));
self.toasts.add(toast);
}
Ok(None) => {}
Err(error) => {
let mut toast = Toast::error(format!("{error:#}"));
toast.duration(Some(Duration::from_secs(5)));
self.toasts.add(toast);
}
}
if pause {
self.client.send_command(EmulatorCommand::Resume);
}
}
fn try_finish_recording(&mut self) -> Result<Option<String>> {
let bytes_receiver = self.profiler.finish_recording();
let file = self
.file_picker
.save_file_dialog()
.add_filter("Profiler files", &["json"])
.set_file_name("profile.json")
.open();
if let Some(path) = file {
let bytes = pollster::block_on(bytes_receiver)?;
let _ = fs::remove_file(&path);
fs::write(&path, bytes)?;
Ok(Some(path.display().to_string()))
} else {
self.profiler.cancel_recording();
Ok(None)
}
}
}
impl AppWindow for ProfileWindow {
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Profiler")
.with_inner_size((300.0, 200.0))
}
fn show(&mut self, ui: &mut egui::Ui) {
let status = self.profiler.status();
let recording = matches!(status, ProfilerStatus::Recording);
CentralPanel::default().show_inside(ui, |ui| {
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.add(
Label::new(
"Use this tool to record performance profiles of your game, for use in ",
)
.wrap_mode(egui::TextWrapMode::Wrap),
);
ui.hyperlink("https://profiler.firefox.com");
ui.label(".");
});
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.add(
Label::new("For more instructions, see ").wrap_mode(egui::TextWrapMode::Wrap),
);
ui.hyperlink_to(
"the Lemur wiki",
"https://git.virtual-boy.com/PVB/lemur/wiki/Profiling-with-Lemur",
);
ui.label(".");
});
ui.separator();
let mut enabled = status.enabled();
let enabled_checkbox = Checkbox::new(&mut enabled, "Enable profiling");
if ui.add_enabled(!recording, enabled_checkbox).changed() {
if enabled {
self.profiler.enable();
} else {
self.profiler.disable();
}
}
if !enabled {
ui.label("Enabling profiling will restart your current game.");
} else {
ui.horizontal(|ui| {
if !recording {
let record_button = Button::new("Record");
let can_record = matches!(status, ProfilerStatus::Enabled);
if ui.add_enabled(can_record, record_button).clicked() {
self.profiler.start_recording();
}
} else {
if ui.button("Finish recording").clicked() {
self.finish_recording();
}
if ui.button("Cancel recording").clicked() {
self.profiler.cancel_recording();
}
}
});
}
match &status {
ProfilerStatus::Recording => {
ui.label("Recording...");
}
ProfilerStatus::Error(message) => {
ui.label(message);
}
_ => {}
}
});
self.toasts.show(ui);
}
fn on_init(&mut self, args: InitArgs) {
self.file_picker.set_window(args.window);
}
}
+72
View File
@@ -0,0 +1,72 @@
use std::{collections::VecDeque, sync::mpsc};
use egui::{Align, CentralPanel, FontFamily, Label, RichText, ScrollArea, Ui, ViewportBuilder};
use crate::emulator::{EmulatorClient, EmulatorCommand, SimId};
use super::AppWindow;
const SCROLLBACK: usize = 1000;
pub struct TerminalWindow {
sim_id: SimId,
receiver: mpsc::Receiver<String>,
lines: VecDeque<String>,
}
impl TerminalWindow {
pub fn new(sim_id: SimId, client: &EmulatorClient) -> Self {
let (sender, receiver) = mpsc::channel();
client.send_command(EmulatorCommand::WatchStdout(sim_id, sender));
let mut lines = VecDeque::new();
lines.push_back(String::new());
Self {
sim_id,
receiver,
lines,
}
}
}
impl AppWindow for TerminalWindow {
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Terminal")
.with_inner_size((640.0, 480.0))
}
fn show(&mut self, ui: &mut Ui) {
if let Ok(text) = self.receiver.try_recv() {
let mut rest = text.as_str();
while let Some(index) = rest.find('\n') {
let (line, lines) = rest.split_at(index);
let current = self.lines.back_mut().unwrap();
current.push_str(line);
self.lines.push_back(String::new());
if self.lines.len() > SCROLLBACK {
self.lines.pop_front();
}
rest = &lines[1..];
}
self.lines.back_mut().unwrap().push_str(rest);
}
CentralPanel::default().show_inside(ui, |ui| {
ScrollArea::vertical()
.stick_to_bottom(true)
.auto_shrink([false, false])
.animated(false)
.show(ui, |ui| {
for line in &self.lines {
let label = Label::new(RichText::new(line).family(FontFamily::Monospace))
.halign(Align::LEFT)
.wrap();
ui.add(label);
}
});
});
}
}
+463
View File
@@ -0,0 +1,463 @@
use std::{
fmt::{Display, UpperHex},
ops::{AddAssign, Bound, Deref, DerefMut, MulAssign, RangeBounds},
str::FromStr,
};
use atoi::FromRadix16;
use egui::{
Align, Color32, CornerRadius, CursorIcon, Event, Frame, Key, Layout, Margin, Rect, Response,
RichText, Sense, Shape, Stroke, StrokeKind, TextEdit, Ui, UiBuilder, Vec2, Widget, WidgetText,
ecolor::HexColor, util::id_type_map::SerializableAny,
};
use num_traits::{CheckedAdd, CheckedSub, One, Zero};
pub trait UiExt {
fn section(&mut self, title: impl Into<String>, add_contents: impl FnOnce(&mut Ui));
fn selectable_button(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response;
fn selectable_option<T: Eq>(
&mut self,
current_value: &mut T,
selected_value: T,
text: impl Into<WidgetText>,
) -> Response {
let response = self.selectable_button(*current_value == selected_value, text);
if response.clicked() {
*current_value = selected_value;
}
response
}
fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response;
fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response;
}
impl UiExt for Ui {
fn section(&mut self, title: impl Into<String>, add_contents: impl FnOnce(&mut Ui)) {
let title: String = title.into();
let mut frame = Frame::group(self.style());
frame.outer_margin.top += 10;
frame.inner_margin.top += 2;
let res = self.push_id(&title, |ui| {
frame.show(ui, |ui| {
ui.set_max_width(ui.available_width());
add_contents(ui);
})
});
let text = RichText::new(title).background_color(self.style().visuals.panel_fill);
let old_rect = res.response.rect;
let mut text_rect = old_rect;
text_rect.min.x += 6.0;
self.scope_builder(UiBuilder::new().max_rect(text_rect), |ui| ui.label(text));
if old_rect.width() > 0.0 {
self.advance_cursor_after_rect(old_rect);
}
}
fn selectable_button(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response {
self.style_mut().visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
self.style_mut().visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT;
self.style_mut().visuals.widgets.active.bg_fill = Color32::TRANSPARENT;
let mut selected = selected;
self.checkbox(&mut selected, text)
}
fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response {
let button_size = Vec2::new(60.0, 20.0);
let (rect, response) = self.allocate_at_least(button_size, Sense::click());
let center_x = rect.center().x;
let left_rect = rect.with_max_x(center_x);
self.painter().rect_filled(left_rect, 0.0, left);
let right_rect = rect.with_min_x(center_x);
self.painter().rect_filled(right_rect, 0.0, right);
let style = self.style().interact(&response);
self.painter()
.rect_stroke(rect, 0.0, style.fg_stroke, StrokeKind::Inside);
response
}
fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response {
self.allocate_ui_with_layout(
Vec2::new(100.0, 130.0),
Layout::top_down_justified(egui::Align::Center),
|ui| {
let (rect, _) = ui.allocate_at_least(Vec2::new(100.0, 100.0), Sense::hover());
ui.painter().rect_filled(rect, 0.0, *color);
let resp = ui.text_edit_singleline(hex);
if resp.changed()
&& let Ok(new_color) = HexColor::from_str_without_hash(hex)
{
*color = new_color.color();
}
resp
},
)
.inner
}
}
enum Direction {
Up,
Down,
}
pub trait Number:
Copy
+ Zero
+ One
+ AddAssign
+ MulAssign
+ CheckedAdd
+ CheckedSub
+ Eq
+ Ord
+ Display
+ FromStr
+ UpperHex
+ Send
+ Sync
+ 'static
{
}
impl<
T: Copy
+ Zero
+ One
+ AddAssign
+ MulAssign
+ CheckedAdd
+ CheckedSub
+ Eq
+ Ord
+ Display
+ FromStr
+ UpperHex
+ Send
+ Sync
+ 'static,
> Number for T
{
}
pub struct NumberEdit<'a, T: Number> {
value: &'a mut T,
increment: T,
precision: usize,
min: Option<T>,
max: Option<T>,
desired_width: Option<f32>,
arrows: bool,
hex: bool,
}
impl<'a, T: Number> NumberEdit<'a, T> {
pub fn new(value: &'a mut T) -> Self {
Self {
value,
increment: T::one(),
precision: 3,
min: None,
max: None,
desired_width: None,
arrows: true,
hex: false,
}
}
pub fn precision(self, precision: usize) -> Self {
Self { precision, ..self }
}
pub fn range(self, range: impl RangeBounds<T>) -> Self {
let min = match range.start_bound() {
Bound::Unbounded => None,
Bound::Included(t) => Some(*t),
Bound::Excluded(t) => t.checked_add(&self.increment),
};
let max = match range.end_bound() {
Bound::Unbounded => None,
Bound::Included(t) => Some(*t),
Bound::Excluded(t) => t.checked_sub(&self.increment),
};
Self { min, max, ..self }
}
pub fn desired_width(self, desired_width: f32) -> Self {
Self {
desired_width: Some(desired_width),
..self
}
}
pub fn arrows(self, arrows: bool) -> Self {
Self { arrows, ..self }
}
pub fn hex(self, hex: bool) -> Self {
Self { hex, ..self }
}
}
impl<T: Number> Widget for NumberEdit<'_, T> {
fn ui(self, ui: &mut Ui) -> Response {
let id = ui.id();
let to_string = |val: &T| {
if self.hex {
format!("{val:.0$X}", self.precision)
} else {
format!("{val:.0$}", self.precision)
}
};
let from_string = |val: &str| {
if self.hex {
let bytes = val.as_bytes();
let (result, consumed) = atoi::Integer::<T>::from_radix_16(bytes);
(consumed == bytes.len()).then_some(result.0)
} else {
val.parse::<T>().ok()
}
};
let (last_value, mut str, focus) = ui.memory(|m| {
let (lv, s) = m
.data
.get_temp(id)
.unwrap_or((*self.value, to_string(self.value)));
let focus = m.has_focus(id);
(lv, s, focus)
});
let mut stale = false;
if *self.value != last_value {
str = to_string(self.value);
stale = true;
}
let valid = from_string(&str).is_some_and(|v: T| v == *self.value);
let mut up_pressed = false;
let mut down_pressed = false;
if focus {
ui.input_mut(|i| {
i.events.retain(|e| match e {
Event::Key {
key: Key::ArrowUp,
pressed: true,
..
} => {
up_pressed = true;
false
}
Event::Key {
key: Key::ArrowDown,
pressed: true,
..
} => {
down_pressed = true;
false
}
_ => true,
})
});
}
let mut desired_width = self
.desired_width
.unwrap_or_else(|| ui.spacing().text_edit_width);
if self.arrows {
desired_width -= 16.0;
}
let text = TextEdit::singleline(&mut str)
.horizontal_align(Align::Max)
.id(id)
.desired_width(desired_width)
.margin(Margin {
left: 4,
right: if self.arrows { 20 } else { 4 },
top: 2,
bottom: 2,
});
let mut res = if valid {
ui.add(text)
} else {
let message = match (self.min, self.max) {
(Some(min), Some(max)) => format!(
"Please enter a number between {} and {}.",
to_string(&min),
to_string(&max)
),
(Some(min), None) => {
format!("Please enter a number greater than {}.", to_string(&min))
}
(None, Some(max)) => {
format!("Please enter a number less than {}.", to_string(&max))
}
(None, None) => "Please enter a number.".to_string(),
};
ui.scope(|ui| {
let style = ui.style_mut();
style.visuals.selection.stroke.color = style.visuals.error_fg_color;
style.visuals.widgets.hovered.bg_stroke.color =
style.visuals.error_fg_color.gamma_multiply(0.60);
ui.add(text)
})
.inner
.on_hover_text(message)
};
let mut delta = None;
if self.arrows {
let arrow_left = res.rect.max.x - 16.0;
let arrow_right = res.rect.max.x;
let arrow_top = res.rect.min.y;
let arrow_middle = (res.rect.min.y + res.rect.max.y) / 2.0;
let arrow_bottom = res.rect.max.y;
let top_arrow_rect = Rect {
min: (arrow_left, arrow_top).into(),
max: (arrow_right, arrow_middle).into(),
};
if draw_arrow(ui, top_arrow_rect, true).clicked_or_dragged() || up_pressed {
delta = Some(Direction::Up);
}
let bottom_arrow_rect = Rect {
min: (arrow_left, arrow_middle).into(),
max: (arrow_right, arrow_bottom).into(),
};
if draw_arrow(ui, bottom_arrow_rect, false).clicked_or_dragged() || down_pressed {
delta = Some(Direction::Down);
}
}
let in_range =
|val: &T| self.min.is_none_or(|m| &m <= val) && self.max.is_none_or(|m| &m >= val);
if let Some(dir) = delta {
let value = match dir {
Direction::Up => self.value.checked_add(&self.increment),
Direction::Down => self.value.checked_sub(&self.increment),
};
if let Some(new_value) = value.filter(in_range) {
if *self.value != new_value {
res.mark_changed();
}
*self.value = new_value;
}
str = to_string(self.value);
stale = true;
} else if res.changed() {
if let Some(new_value) = from_string(&str).filter(in_range) {
if *self.value != new_value {
res.mark_changed();
}
*self.value = new_value;
}
stale = true;
}
if stale {
ui.memory_mut(|m| m.data.insert_temp(id, (*self.value, str)));
}
res
}
}
fn draw_arrow(ui: &mut Ui, rect: Rect, up: bool) -> Response {
let arrow_res = ui
.allocate_rect(rect, Sense::all())
.on_hover_cursor(CursorIcon::Default);
let visuals = ui.style().visuals.widgets.style(&arrow_res);
let painter = ui.painter_at(arrow_res.rect);
let rounding = if up {
CornerRadius {
ne: 2,
..CornerRadius::ZERO
}
} else {
CornerRadius {
se: 2,
..CornerRadius::ZERO
}
};
painter.rect_filled(arrow_res.rect, rounding, visuals.bg_fill);
let left = rect.left() + 4.0;
let center = (rect.left() + rect.right()) / 2.0;
let right = rect.right() - 4.0;
let top = rect.top() + 3.0;
let bottom = rect.bottom() - 3.0;
let points = if up {
vec![
(left, bottom).into(),
(center, top).into(),
(right, bottom).into(),
]
} else {
vec![
(right, top).into(),
(center, bottom).into(),
(left, top).into(),
]
};
painter.add(Shape::convex_polygon(
points,
visuals.fg_stroke.color,
Stroke::NONE,
));
arrow_res
}
trait ResponseExt {
fn clicked_or_dragged(&self) -> bool;
}
impl ResponseExt for Response {
fn clicked_or_dragged(&self) -> bool {
self.clicked() || self.dragged()
}
}
pub struct UiData<T> {
current: T,
prev: T,
loaded: bool,
}
impl<T> UiData<T>
where
T: Default + PartialEq + SerializableAny,
{
pub fn new() -> Self {
Self {
current: T::default(),
prev: T::default(),
loaded: false,
}
}
pub fn load(&mut self, ui: &Ui) {
if !self.loaded {
self.current = ui
.data_mut(|d| d.get_persisted(ui.id()))
.unwrap_or_default();
self.loaded = true;
}
}
pub fn save(&mut self, ui: &Ui) {
if self.current != self.prev {
ui.data_mut(|d| d.insert_persisted(ui.id(), self.current.clone()));
self.prev = self.current.clone();
}
}
}
impl<T> Deref for UiData<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.current
}
}
impl<T> DerefMut for UiData<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.current
}
}
+14
View File
@@ -0,0 +1,14 @@
mod bgmap;
mod chardata;
mod framebuffer;
mod object;
mod registers;
mod utils;
mod world;
pub use bgmap::*;
pub use chardata::*;
pub use framebuffer::*;
pub use object::*;
pub use registers::*;
pub use world::*;
+331
View File
@@ -0,0 +1,331 @@
use std::sync::Arc;
use egui::{
Align, CentralPanel, Checkbox, Color32, ComboBox, Image, ScrollArea, Slider, TextEdit,
TextureOptions, Ui, ViewportBuilder,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use serde::{Deserialize, Serialize};
use crate::{
emulator::SimId,
images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView},
window::{
AppWindow,
utils::{NumberEdit, UiData, UiExt},
},
};
use super::utils::{self, CellData, CharacterGrid};
#[derive(Clone, PartialEq, Serialize, Deserialize)]
struct State {
cell_index: usize,
generic_palette: bool,
scale: f32,
show_grid: bool,
}
impl Default for State {
fn default() -> Self {
Self {
cell_index: 0,
generic_palette: false,
scale: 1.0,
show_grid: false,
}
}
}
pub struct BgMapWindow {
sim_id: SimId,
memory: Arc<MemoryClient>,
bgmaps: MemoryView,
params: ImageParams<BgMapParams>,
state: UiData<State>,
}
impl BgMapWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &ImageTextureLoader) -> Self {
let state: UiData<State> = UiData::new();
let renderer = BgMapRenderer::new(sim_id, memory);
let params = images.add(
sim_id,
renderer,
BgMapParams {
cell_index: state.cell_index,
generic_palette: state.generic_palette,
},
);
Self {
sim_id,
memory: memory.clone(),
bgmaps: memory.watch(sim_id, 0x00020000, 0x20000),
params,
state,
}
}
fn show_form(&mut self, ui: &mut Ui) {
let row_height = ui.spacing().interact_size.y;
ui.vertical(|ui| {
TableBuilder::new(ui)
.column(Column::auto())
.column(Column::remainder())
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Map");
});
row.col(|ui| {
let mut bgmap_index = self.state.cell_index / 4096;
ui.add(NumberEdit::new(&mut bgmap_index).range(0..16));
if bgmap_index != self.state.cell_index / 4096 {
self.state.cell_index =
(bgmap_index * 4096) + (self.state.cell_index % 4096);
}
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Cell");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut self.state.cell_index).range(0..16 * 4096));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Address");
});
row.col(|ui| {
let address = 0x00020000 + (self.state.cell_index * 2);
let mut address_str = format!("{address:08x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut address_str).horizontal_align(Align::Max),
);
});
});
});
let image = Image::new(self.image_url("bgmap-cell"))
.maintain_aspect_ratio(true)
.texture_options(TextureOptions::NEAREST);
ui.add(image);
ui.section("Cell", |ui| {
let mut data = self.bgmaps.borrow().read::<u16>(self.state.cell_index);
let mut cell = CellData::parse(data);
TableBuilder::new(ui)
.column(Column::remainder())
.column(Column::remainder())
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Character");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut cell.char_index).range(0..2048));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Palette");
});
row.col(|ui| {
ComboBox::from_id_salt("palette")
.selected_text(format!("BG {}", cell.palette_index))
.width(ui.available_width())
.show_ui(ui, |ui| {
for palette in 0..4 {
ui.selectable_value(
&mut cell.palette_index,
palette,
format!("BG {palette}"),
);
}
});
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add(Checkbox::new(&mut cell.hflip, "H-flip"));
});
row.col(|ui| {
ui.add(Checkbox::new(&mut cell.vflip, "V-flip"));
});
});
});
if cell.update(&mut data) {
let address = 0x00020000 + (self.state.cell_index * 2);
self.memory.write(self.sim_id, address as u32, &data);
}
});
ui.section("Display", |ui| {
ui.horizontal(|ui| {
ui.label("Scale");
ui.spacing_mut().slider_width = ui.available_width();
let slider = Slider::new(&mut self.state.scale, 1.0..=10.0)
.step_by(1.0)
.show_value(false);
ui.add(slider);
});
ui.checkbox(&mut self.state.show_grid, "Show grid");
ui.checkbox(&mut self.state.generic_palette, "Generic palette");
});
});
self.params.write(BgMapParams {
cell_index: self.state.cell_index,
generic_palette: self.state.generic_palette,
});
}
fn show_bgmap(&mut self, ui: &mut Ui) {
let grid = CharacterGrid::new(self.image_url("bgmap"))
.with_scale(self.state.scale)
.with_grid(self.state.show_grid)
.with_selected(self.state.cell_index % 4096);
if let Some(selected) = grid.show(ui) {
self.state.cell_index = (self.state.cell_index / 4096 * 4096) + selected;
}
}
}
impl AppWindow for BgMapWindow {
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("BG Map Data")
.with_inner_size((640.0, 480.0))
}
fn show(&mut self, ui: &mut Ui) {
self.state.load(ui);
CentralPanel::default().show_inside(ui, |ui| {
ui.horizontal_top(|ui| {
StripBuilder::new(ui)
.size(Size::relative(0.3).at_most(200.0))
.size(Size::remainder())
.horizontal(|mut strip| {
strip.cell(|ui| {
ScrollArea::vertical().show(ui, |ui| self.show_form(ui));
});
strip.cell(|ui| {
ScrollArea::both().show(ui, |ui| self.show_bgmap(ui));
});
})
});
});
self.state.save(ui);
}
}
#[derive(Default, Clone, PartialEq, Eq)]
struct BgMapParams {
cell_index: usize,
generic_palette: bool,
}
struct BgMapRenderer {
chardata: MemoryView,
bgmaps: MemoryView,
brightness: MemoryView,
palettes: MemoryView,
}
impl BgMapRenderer {
pub fn new(sim_id: SimId, memory: &MemoryClient) -> Self {
Self {
chardata: memory.watch(sim_id, 0x00078000, 0x8000),
bgmaps: memory.watch(sim_id, 0x00020000, 0x20000),
brightness: memory.watch(sim_id, 0x0005f824, 8),
palettes: memory.watch(sim_id, 0x0005f860, 16),
}
}
fn render_bgmap(&self, image: &mut ImageBuffer, bgmap_index: usize, generic_palette: bool) {
let chardata = self.chardata.borrow();
let bgmaps = self.bgmaps.borrow();
let brightness = self.brightness.borrow();
let palettes = self.palettes.borrow();
let brts = brightness.read::<[u8; 8]>(0);
let colors = if generic_palette {
[utils::generic_palette(Color32::RED); 4]
} else {
[0, 2, 4, 6].map(|i| utils::palette_colors(palettes.read(i), &brts, Color32::RED))
};
for (i, cell) in bgmaps.range::<u16>(bgmap_index * 4096, 4096).enumerate() {
let CellData {
char_index,
vflip,
hflip,
palette_index,
} = CellData::parse(cell);
let char = chardata.read::<[u16; 8]>(char_index);
let palette = &colors[palette_index];
for row in 0..8 {
let y = row + (i / 64) * 8;
for (col, pixel) in utils::read_char_row(&char, hflip, vflip, row).enumerate() {
let x = col + (i % 64) * 8;
image.write((x, y), palette[pixel as usize]);
}
}
}
}
fn render_bgmap_cell(&self, image: &mut ImageBuffer, index: usize, generic_palette: bool) {
let chardata = self.chardata.borrow();
let bgmaps = self.bgmaps.borrow();
let brightness = self.brightness.borrow();
let palettes = self.palettes.borrow();
let brts = brightness.read::<[u8; 8]>(0);
let cell = bgmaps.read::<u16>(index);
let CellData {
char_index,
vflip,
hflip,
palette_index,
} = CellData::parse(cell);
let char = chardata.read::<[u16; 8]>(char_index);
let palette = if generic_palette {
utils::generic_palette(Color32::RED)
} else {
utils::palette_colors(palettes.read(palette_index * 2), &brts, Color32::RED)
};
for row in 0..8 {
for (col, pixel) in utils::read_char_row(&char, hflip, vflip, row).enumerate() {
image.write((col, row), palette[pixel as usize]);
}
}
}
}
impl ImageRenderer<2> for BgMapRenderer {
type Params = BgMapParams;
fn names(&self) -> [&str; 2] {
["bgmap-cell", "bgmap"]
}
fn sizes(&self) -> [[usize; 2]; 2] {
[[8, 8], [8 * 64, 8 * 64]]
}
fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 2]) {
self.render_bgmap_cell(&mut images[0], params.cell_index, params.generic_palette);
self.render_bgmap(
&mut images[1],
params.cell_index / 4096,
params.generic_palette,
);
}
}
+362
View File
@@ -0,0 +1,362 @@
use std::fmt::Display;
use egui::{
Align, CentralPanel, Color32, ComboBox, Image, ScrollArea, Slider, TextEdit, TextureOptions,
Ui, Vec2, ViewportBuilder,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use serde::{Deserialize, Serialize};
use crate::{
emulator::SimId,
images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView},
window::{
AppWindow,
utils::{NumberEdit, UiData, UiExt as _},
},
};
use super::utils::{self, CharacterGrid};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Palette {
#[default]
Generic,
Bg0,
Bg1,
Bg2,
Bg3,
Obj0,
Obj1,
Obj2,
Obj3,
}
impl Palette {
pub const fn values() -> [Palette; 9] {
[
Self::Generic,
Self::Bg0,
Self::Bg1,
Self::Bg2,
Self::Bg3,
Self::Obj0,
Self::Obj1,
Self::Obj2,
Self::Obj3,
]
}
pub const fn offset(self) -> Option<usize> {
match self {
Self::Generic => None,
Self::Bg0 => Some(0),
Self::Bg1 => Some(2),
Self::Bg2 => Some(4),
Self::Bg3 => Some(6),
Self::Obj0 => Some(8),
Self::Obj1 => Some(10),
Self::Obj2 => Some(12),
Self::Obj3 => Some(14),
}
}
}
impl Display for Palette {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Generic => f.write_str("Generic"),
Self::Bg0 => f.write_str("BG 0"),
Self::Bg1 => f.write_str("BG 1"),
Self::Bg2 => f.write_str("BG 2"),
Self::Bg3 => f.write_str("BG 3"),
Self::Obj0 => f.write_str("OBJ 0"),
Self::Obj1 => f.write_str("OBJ 1"),
Self::Obj2 => f.write_str("OBJ 2"),
Self::Obj3 => f.write_str("OBJ 3"),
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
struct State {
palette: Palette,
index: usize,
scale: f32,
show_grid: bool,
}
impl Default for State {
fn default() -> Self {
Self {
palette: Palette::default(),
index: 0,
scale: 4.0,
show_grid: true,
}
}
}
pub struct CharacterDataWindow {
sim_id: SimId,
brightness: MemoryView,
palettes: MemoryView,
params: ImageParams<CharDataParams>,
state: UiData<State>,
}
impl CharacterDataWindow {
pub fn new(sim_id: SimId, memory: &MemoryClient, images: &ImageTextureLoader) -> Self {
let state: UiData<State> = UiData::new();
let renderer = CharDataRenderer::new(sim_id, memory);
let params = images.add(
sim_id,
renderer,
CharDataParams {
palette: state.palette,
index: state.index,
},
);
Self {
sim_id,
brightness: memory.watch(sim_id, 0x0005f824, 8),
palettes: memory.watch(sim_id, 0x0005f860, 16),
params,
state,
}
}
fn show_form(&mut self, ui: &mut Ui) {
let row_height = ui.spacing().interact_size.y;
ui.vertical(|ui| {
TableBuilder::new(ui)
.column(Column::auto())
.column(Column::remainder())
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Index");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut self.state.index).range(0..2048));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Address");
});
row.col(|ui| {
let address = match self.state.index {
0x000..0x200 => 0x00060000 + self.state.index * 16,
0x200..0x400 => 0x000e0000 + (self.state.index - 0x200) * 16,
0x400..0x600 => 0x00160000 + (self.state.index - 0x400) * 16,
0x600..0x800 => 0x001e0000 + (self.state.index - 0x600) * 16,
_ => unreachable!("can't happen"),
};
let mut address_str = format!("{address:08x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut address_str).horizontal_align(Align::Max),
);
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Mirror");
});
row.col(|ui| {
let mirror = 0x00078000 + (self.state.index * 16);
let mut mirror_str = format!("{mirror:08x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut mirror_str).horizontal_align(Align::Max),
);
});
});
});
let image = Image::new(self.image_url("chardata-char"))
.maintain_aspect_ratio(true)
.texture_options(TextureOptions::NEAREST);
ui.add(image);
ui.section("Colors", |ui| {
ui.horizontal(|ui| {
ui.label("Palette");
ComboBox::from_id_salt("palette")
.selected_text(self.state.palette.to_string())
.width(ui.available_width())
.show_ui(ui, |ui| {
for palette in Palette::values() {
ui.selectable_value(
&mut self.state.palette,
palette,
palette.to_string(),
);
}
});
});
TableBuilder::new(ui)
.columns(Column::remainder(), 4)
.body(|mut body| {
let palette = self.load_palette_colors();
body.row(30.0, |mut row| {
for color in palette {
row.col(|ui| {
let rect = ui.available_rect_before_wrap();
let scale = rect.height() / rect.width();
let rect = rect.scale_from_center2(Vec2::new(scale, 1.0));
ui.painter().rect_filled(rect, 0.0, color);
});
}
});
});
});
ui.section("Display", |ui| {
ui.horizontal(|ui| {
ui.label("Scale");
ui.spacing_mut().slider_width = ui.available_width();
let slider = Slider::new(&mut self.state.scale, 1.0..=10.0)
.step_by(1.0)
.show_value(false);
ui.add(slider);
});
ui.checkbox(&mut self.state.show_grid, "Show grid");
});
});
self.params.write(CharDataParams {
palette: self.state.palette,
index: self.state.index,
});
}
fn load_palette_colors(&self) -> [Color32; 4] {
let Some(offset) = self.state.palette.offset() else {
return utils::generic_palette(Color32::RED);
};
let palette = self.palettes.borrow().read(offset);
let brightnesses = self.brightness.borrow();
let brts = brightnesses.read(0);
utils::palette_colors(palette, &brts, Color32::RED)
}
fn show_chardata(&mut self, ui: &mut Ui) {
let grid = CharacterGrid::new(self.image_url("chardata"))
.with_scale(self.state.scale)
.with_grid(self.state.show_grid)
.with_selected(self.state.index);
if let Some(selected) = grid.show(ui) {
self.state.index = selected;
}
}
}
impl AppWindow for CharacterDataWindow {
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Character Data")
.with_inner_size((640.0, 480.0))
}
fn show(&mut self, ui: &mut Ui) {
self.state.load(ui);
CentralPanel::default().show_inside(ui, |ui| {
ui.horizontal_top(|ui| {
StripBuilder::new(ui)
.size(Size::relative(0.3).at_most(200.0))
.size(Size::remainder())
.horizontal(|mut strip| {
strip.cell(|ui| {
ScrollArea::vertical().show(ui, |ui| self.show_form(ui));
});
strip.cell(|ui| {
ScrollArea::both().show(ui, |ui| self.show_chardata(ui));
});
});
});
});
self.state.save(ui);
}
}
#[derive(Clone, PartialEq, Eq)]
struct CharDataParams {
palette: Palette,
index: usize,
}
struct CharDataRenderer {
chardata: MemoryView,
brightness: MemoryView,
palettes: MemoryView,
}
impl ImageRenderer<2> for CharDataRenderer {
type Params = CharDataParams;
fn names(&self) -> [&str; 2] {
["chardata-char", "chardata"]
}
fn sizes(&self) -> [[usize; 2]; 2] {
[[8, 8], [16 * 8, 128 * 8]]
}
fn render(&mut self, params: &Self::Params, image: &mut [ImageBuffer; 2]) {
self.render_character(&mut image[0], params.palette, params.index);
self.render_character_data(&mut image[1], params.palette);
}
}
impl CharDataRenderer {
pub fn new(sim_id: SimId, memory: &MemoryClient) -> Self {
Self {
chardata: memory.watch(sim_id, 0x00078000, 0x8000),
brightness: memory.watch(sim_id, 0x0005f824, 8),
palettes: memory.watch(sim_id, 0x0005f860, 16),
}
}
fn render_character(&self, image: &mut ImageBuffer, palette: Palette, index: usize) {
if index >= 2048 {
return;
}
let palette = self.load_palette(palette);
let chardata = self.chardata.borrow();
let character = chardata.range::<u16>(index * 8, 8);
for (row, pixels) in character.enumerate() {
for col in 0..8 {
let char = (pixels >> (col * 2)) & 0x03;
image.write((col, row), palette[char as usize]);
}
}
}
fn render_character_data(&self, image: &mut ImageBuffer, palette: Palette) {
let palette = self.load_palette(palette);
let chardata = self.chardata.borrow();
for (row, pixels) in chardata.range::<u16>(0, 8 * 2048).enumerate() {
let char_index = row / 8;
let row_index = row % 8;
let x = (char_index % 16) * 8;
let y = (char_index / 16) * 8 + row_index;
for col in 0..8 {
let char = (pixels >> (col * 2)) & 0x03;
image.write((x + col, y), palette[char as usize]);
}
}
}
fn load_palette(&self, palette: Palette) -> [Color32; 4] {
let Some(offset) = palette.offset() else {
return utils::GENERIC_PALETTE.map(|p| utils::shade(p, Color32::RED));
};
let palette = self.palettes.borrow().read(offset);
let brightnesses = self.brightness.borrow();
let brts = brightnesses.read(0);
utils::palette_colors(palette, &brts, Color32::RED)
}
}
+277
View File
@@ -0,0 +1,277 @@
use egui::{
Align, CentralPanel, Color32, Image, ScrollArea, Slider, TextEdit, TextureOptions, Ui,
ViewportBuilder,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use serde::{Deserialize, Serialize};
use crate::{
emulator::SimId,
images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView},
window::{
AppWindow,
utils::{NumberEdit, UiData, UiExt as _},
},
};
use super::utils;
#[derive(Clone, PartialEq, Serialize, Deserialize)]
struct State {
index: usize,
left: bool,
right: bool,
generic_palette: bool,
scale: f32,
}
impl Default for State {
fn default() -> Self {
Self {
index: 0,
left: true,
right: true,
generic_palette: false,
scale: 2.0,
}
}
}
pub struct FrameBufferWindow {
sim_id: SimId,
params: ImageParams<FrameBufferParams>,
state: UiData<State>,
}
impl FrameBufferWindow {
pub fn new(sim_id: SimId, memory: &MemoryClient, images: &ImageTextureLoader) -> Self {
let state: UiData<State> = UiData::new();
let renderer = FrameBufferRenderer::new(sim_id, memory);
let params = images.add(
sim_id,
renderer,
FrameBufferParams {
index: state.index,
left: state.left,
right: state.right,
generic_palette: state.generic_palette,
left_color: Color32::from_rgb(0xff, 0x00, 0x00),
right_color: Color32::from_rgb(0x00, 0xc6, 0xf0),
},
);
Self {
sim_id,
params,
state,
}
}
fn show_form(&mut self, ui: &mut Ui) {
let row_height = ui.spacing().interact_size.y;
ui.vertical(|ui| {
TableBuilder::new(ui)
.column(Column::auto())
.column(Column::remainder())
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Index");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut self.state.index).range(0..2));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Left");
});
row.col(|ui| {
let address = self.state.index * 0x00008000;
let mut address_str = format!("{address:08x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut address_str).horizontal_align(Align::Max),
);
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Right");
});
row.col(|ui| {
let address = self.state.index * 0x00008000 + 0x00010000;
let mut address_str = format!("{address:08x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut address_str).horizontal_align(Align::Max),
);
});
});
});
ui.section("Display", |ui| {
ui.horizontal(|ui| {
ui.label("Scale");
ui.spacing_mut().slider_width = ui.available_width();
let slider = Slider::new(&mut self.state.scale, 1.0..=10.0)
.step_by(1.0)
.show_value(false);
ui.add(slider);
});
TableBuilder::new(ui)
.columns(Column::remainder(), 2)
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.checkbox(&mut self.state.left, "Left");
});
row.col(|ui| {
ui.checkbox(&mut self.state.right, "Right");
});
});
});
ui.checkbox(&mut self.state.generic_palette, "Generic colors");
});
});
self.params.write(FrameBufferParams {
index: self.state.index,
left: self.state.left,
right: self.state.right,
generic_palette: self.state.generic_palette,
..*self.params
});
}
fn show_buffers(&mut self, ui: &mut Ui) {
let image = Image::new(self.image_url("buffer"))
.fit_to_original_size(self.state.scale)
.texture_options(TextureOptions::NEAREST);
ui.add(image);
}
}
impl AppWindow for FrameBufferWindow {
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Frame Buffers")
.with_inner_size((640.0, 480.0))
}
fn show(&mut self, ui: &mut Ui) {
self.state.load(ui);
CentralPanel::default().show_inside(ui, |ui| {
ui.horizontal_top(|ui| {
StripBuilder::new(ui)
.size(Size::relative(0.3).at_most(200.0))
.size(Size::remainder())
.horizontal(|mut strip| {
strip.cell(|ui| {
ScrollArea::vertical().show(ui, |ui| self.show_form(ui));
});
strip.cell(|ui| {
ScrollArea::both().show(ui, |ui| self.show_buffers(ui));
});
});
});
});
self.state.save(ui);
}
}
#[derive(Clone, PartialEq, Eq)]
struct FrameBufferParams {
index: usize,
left: bool,
right: bool,
generic_palette: bool,
left_color: Color32,
right_color: Color32,
}
struct FrameBufferRenderer {
buffers: [MemoryView; 4],
brightness: MemoryView,
}
impl FrameBufferRenderer {
fn new(sim_id: SimId, memory: &MemoryClient) -> Self {
Self {
buffers: [
memory.watch(sim_id, 0x00000000, 0x6000),
memory.watch(sim_id, 0x00008000, 0x6000),
memory.watch(sim_id, 0x00010000, 0x6000),
memory.watch(sim_id, 0x00018000, 0x6000),
],
brightness: memory.watch(sim_id, 0x0005f824, 8),
}
}
}
impl ImageRenderer<1> for FrameBufferRenderer {
type Params = FrameBufferParams;
fn names(&self) -> [&str; 1] {
["buffer"]
}
fn sizes(&self) -> [[usize; 2]; 1] {
[[384, 224]]
}
fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 1]) {
let image = &mut images[0];
let left_buffer = self.buffers[params.index].borrow();
let right_buffer = self.buffers[2 + params.index].borrow();
let colors = if params.generic_palette {
[
utils::generic_palette(params.left_color),
utils::generic_palette(params.right_color),
]
} else {
let brts = self.brightness.borrow().read::<[u8; 8]>(0);
let shades = utils::parse_shades(&brts);
[
shades.map(|s| utils::shade(s, params.left_color)),
shades.map(|s| utils::shade(s, params.right_color)),
]
};
let left_cols = left_buffer.range::<u8>(0, 0x6000);
let right_cols = right_buffer.range::<u8>(0, 0x6000);
for (index, (left, right)) in left_cols.zip(right_cols).enumerate() {
let top = (index % 64) * 4;
if top >= 224 {
continue;
}
let pixels = [0, 2, 4, 6].map(|i| {
let left = if params.left {
colors[0][(left >> i) as usize & 0x3]
} else {
Color32::BLACK
};
let right = if params.right {
colors[1][(right >> i) as usize & 0x3]
} else {
Color32::BLACK
};
Color32::from_rgb(
left.r() + right.r(),
left.g() + right.g(),
left.b() + right.b(),
)
});
let x = index / 64;
for (i, pixel) in pixels.into_iter().enumerate() {
let y = top + i;
image.write((x, y), pixel);
}
}
}
}
+353
View File
@@ -0,0 +1,353 @@
use std::sync::Arc;
use egui::{
Align, CentralPanel, Checkbox, Color32, ComboBox, Image, ScrollArea, Slider, TextEdit,
TextureOptions, Ui, ViewportBuilder,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use serde::{Deserialize, Serialize};
use crate::{
emulator::SimId,
images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView},
window::{
AppWindow,
utils::{NumberEdit, UiData, UiExt as _},
},
};
use super::utils::{self, Object};
#[derive(Clone, PartialEq, Serialize, Deserialize)]
struct State {
index: usize,
generic_palette: bool,
scale: f32,
}
impl Default for State {
fn default() -> Self {
Self {
index: 0,
generic_palette: false,
scale: 1.0,
}
}
}
pub struct ObjectWindow {
sim_id: SimId,
memory: Arc<MemoryClient>,
objects: MemoryView,
params: ImageParams<ObjectParams>,
state: UiData<State>,
}
impl ObjectWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &ImageTextureLoader) -> Self {
let state: UiData<State> = UiData::new();
let renderer = ObjectRenderer::new(sim_id, memory);
let params = images.add(
sim_id,
renderer,
ObjectParams {
index: state.index,
generic_palette: state.generic_palette,
left_color: Color32::from_rgb(0xff, 0x00, 0x00),
right_color: Color32::from_rgb(0x00, 0xc6, 0xf0),
},
);
Self {
sim_id,
memory: memory.clone(),
objects: memory.watch(sim_id, 0x0003e000, 0x2000),
params,
state,
}
}
fn show_form(&mut self, ui: &mut Ui) {
let row_height = ui.spacing().interact_size.y;
ui.vertical(|ui| {
TableBuilder::new(ui)
.column(Column::auto())
.column(Column::remainder())
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Index");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut self.state.index).range(0..1024));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Address");
});
row.col(|ui| {
let address = 0x3e000 + self.state.index * 8;
let mut address_str = format!("{address:08x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut address_str).horizontal_align(Align::Max),
);
});
});
});
let image = Image::new(self.image_url("object-zoom"))
.maintain_aspect_ratio(true)
.texture_options(TextureOptions::NEAREST);
ui.add(image);
ui.section("Properties", |ui| {
let mut object = self.objects.borrow().read::<[u16; 4]>(self.state.index);
let mut obj = Object::parse(object);
TableBuilder::new(ui)
.column(Column::remainder())
.column(Column::remainder())
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Character");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut obj.data.char_index).range(0..2048));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Palette");
});
row.col(|ui| {
ComboBox::from_id_salt("palette")
.selected_text(format!("OBJ {}", obj.data.palette_index))
.width(ui.available_width())
.show_ui(ui, |ui| {
for palette in 0..4 {
ui.selectable_value(
&mut obj.data.palette_index,
palette,
format!("OBJ {palette}"),
);
}
});
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("X");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut obj.x).range(-512..512));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Y");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut obj.y).range(-8..=224));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Parallax");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut obj.parallax).range(-512..512));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add(Checkbox::new(&mut obj.data.hflip, "H-flip"));
});
row.col(|ui| {
ui.add(Checkbox::new(&mut obj.data.vflip, "V-flip"));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add(Checkbox::new(&mut obj.lon, "Left"));
});
row.col(|ui| {
ui.add(Checkbox::new(&mut obj.ron, "Right"));
});
});
});
if obj.update(&mut object) {
let address = 0x3e000 + self.state.index * 8;
self.memory.write(self.sim_id, address as u32, &object);
}
});
ui.section("Display", |ui| {
ui.horizontal(|ui| {
ui.label("Scale");
ui.spacing_mut().slider_width = ui.available_width();
let slider = Slider::new(&mut self.state.scale, 1.0..=10.0)
.step_by(1.0)
.show_value(false);
ui.add(slider);
});
ui.checkbox(&mut self.state.generic_palette, "Generic palette");
});
});
self.params.write(ObjectParams {
index: self.state.index,
generic_palette: self.state.generic_palette,
..*self.params
});
}
fn show_object(&mut self, ui: &mut Ui) {
let image = Image::new(self.image_url("object-full"))
.fit_to_original_size(self.state.scale)
.texture_options(TextureOptions::NEAREST);
ui.add(image);
}
}
impl AppWindow for ObjectWindow {
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Object Data")
.with_inner_size((640.0, 500.0))
}
fn show(&mut self, ui: &mut Ui) {
self.state.load(ui);
CentralPanel::default().show_inside(ui, |ui| {
ui.horizontal_top(|ui| {
StripBuilder::new(ui)
.size(Size::relative(0.3).at_most(200.0))
.size(Size::remainder())
.horizontal(|mut strip| {
strip.cell(|ui| {
ScrollArea::vertical().show(ui, |ui| self.show_form(ui));
});
strip.cell(|ui| {
ScrollArea::both().show(ui, |ui| self.show_object(ui));
});
});
});
});
self.state.save(ui);
}
}
#[derive(Clone, PartialEq, Eq)]
struct ObjectParams {
index: usize,
generic_palette: bool,
left_color: Color32,
right_color: Color32,
}
enum Eye {
Left,
Right,
}
struct ObjectRenderer {
chardata: MemoryView,
objects: MemoryView,
brightness: MemoryView,
palettes: MemoryView,
}
impl ObjectRenderer {
pub fn new(sim_id: SimId, memory: &MemoryClient) -> Self {
Self {
chardata: memory.watch(sim_id, 0x00078000, 0x8000),
objects: memory.watch(sim_id, 0x0003e000, 0x2000),
brightness: memory.watch(sim_id, 0x0005f824, 8),
palettes: memory.watch(sim_id, 0x0005f860, 16),
}
}
fn render_object(
&self,
image: &mut ImageBuffer,
params: &ObjectParams,
use_pos: bool,
eye: Eye,
) {
let chardata = self.chardata.borrow();
let objects = self.objects.borrow();
let brightness = self.brightness.borrow();
let palettes = self.palettes.borrow();
let object: [u16; 4] = objects.read(params.index);
let obj = Object::parse(object);
if match eye {
Eye::Left => !obj.lon,
Eye::Right => !obj.ron,
} {
return;
}
let brts = brightness.read::<[u8; 8]>(0);
let (x, y) = if use_pos {
let x = match eye {
Eye::Left => obj.x - obj.parallax,
Eye::Right => obj.x + obj.parallax,
};
(x, obj.y)
} else {
(0, 0)
};
let color = match eye {
Eye::Left => params.left_color,
Eye::Right => params.right_color,
};
let char = chardata.read::<[u16; 8]>(obj.data.char_index);
let palette = if params.generic_palette {
utils::generic_palette(color)
} else {
utils::palette_colors(palettes.read(8 + obj.data.palette_index * 2), &brts, color)
};
for row in 0..8 {
let real_y = y + row as i16;
if !(0..224).contains(&real_y) {
continue;
}
for (col, pixel) in
utils::read_char_row(&char, obj.data.hflip, obj.data.vflip, row).enumerate()
{
let real_x = x + col as i16;
if !(0..384).contains(&real_x) {
continue;
}
image.add((real_x as usize, real_y as usize), palette[pixel as usize]);
}
}
}
}
impl ImageRenderer<2> for ObjectRenderer {
type Params = ObjectParams;
fn names(&self) -> [&str; 2] {
["object-zoom", "object-full"]
}
fn sizes(&self) -> [[usize; 2]; 2] {
[[8, 8], [384, 224]]
}
fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 2]) {
images[0].clear();
self.render_object(&mut images[0], params, false, Eye::Left);
self.render_object(&mut images[0], params, false, Eye::Right);
images[1].clear();
self.render_object(&mut images[1], params, true, Eye::Left);
self.render_object(&mut images[1], params, true, Eye::Right);
}
}
+815
View File
@@ -0,0 +1,815 @@
use std::sync::Arc;
use egui::{
Align, Button, CentralPanel, Checkbox, Color32, Direction, Label, Layout, ScrollArea, TextEdit,
Ui, ViewportBuilder,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use crate::{
emulator::SimId,
memory::{MemoryClient, MemoryRef, MemoryValue, MemoryView},
window::{
AppWindow,
utils::{NumberEdit, UiExt},
},
};
use super::utils;
pub struct RegisterWindow {
sim_id: SimId,
memory: Arc<MemoryClient>,
registers: MemoryView,
}
impl RegisterWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>) -> Self {
Self {
sim_id,
memory: memory.clone(),
registers: memory.watch(sim_id, 0x0005f800, 0x72),
}
}
fn show_interrupts(&mut self, ui: &mut Ui) {
let row_height = self.row_height(ui);
let [mut raw_intpnd, mut raw_intenb] = self.read_address(0x0005f800);
let mut intenb = InterruptReg::parse(raw_intenb);
let mut intpnd = InterruptReg::parse(raw_intpnd);
ui.section("Interrupt", |ui| {
let width = ui.available_width();
let xspace = ui.spacing().item_spacing.x;
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
TableBuilder::new(ui)
.id_salt("raw_values")
.columns(Column::exact(width * 0.5 - xspace), 2)
.cell_layout(Layout::left_to_right(Align::Max))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("INTENB");
});
row.col(|ui| {
let mut text = format!("{raw_intenb:04x}");
ui.add_sized(
ui.available_size(),
TextEdit::singleline(&mut text).horizontal_align(Align::Max),
);
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("INTPND");
});
row.col(|ui| {
let mut text = format!("{raw_intpnd:04x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut text).horizontal_align(Align::Max),
);
});
});
});
ui.add_space(8.0);
TableBuilder::new(ui)
.id_salt("flags")
.column(Column::exact(width * 0.5 - xspace))
.columns(Column::exact(width * 0.25 - xspace), 2)
.cell_layout(Layout::left_to_right(Align::Center).with_main_align(Align::RIGHT))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|_ui| {});
row.col(|ui| {
ui.add_sized(ui.available_size(), Label::new("ENB"));
});
row.col(|ui| {
ui.add_sized(ui.available_size(), Label::new("PND"));
});
});
let mut add_row = |label: &str, enb: &mut bool, pnd: &mut bool| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label(label);
});
row.col(|ui| {
let space =
(ui.available_width() - ui.spacing().icon_width) / 2.0;
ui.add_space(space);
ui.checkbox(enb, "");
});
row.col(|ui| {
let space =
(ui.available_width() - ui.spacing().icon_width) / 2.0;
ui.add_space(space);
ui.checkbox(pnd, "");
});
});
};
add_row("TIMEERR", &mut intenb.timeerr, &mut intpnd.timeerr);
add_row("XPEND", &mut intenb.xpend, &mut intpnd.xpend);
add_row("SBHIT", &mut intenb.sbhit, &mut intpnd.sbhit);
add_row("FRAMESTART", &mut intenb.framestart, &mut intpnd.framestart);
add_row("GAMESTART", &mut intenb.gamestart, &mut intpnd.gamestart);
add_row("RFBEND", &mut intenb.rfbend, &mut intpnd.rfbend);
add_row("LFBEND", &mut intenb.lfbend, &mut intpnd.lfbend);
add_row("SCANERR", &mut intenb.scanerr, &mut intpnd.scanerr);
});
});
ui.allocate_space(ui.available_size());
});
if intpnd.update(&mut raw_intpnd) {
self.memory.write(self.sim_id, 0x0005f800, &raw_intpnd);
}
if intenb.update(&mut raw_intenb) {
self.memory.write(self.sim_id, 0x0005f802, &raw_intenb);
}
}
fn show_display_status(&mut self, ui: &mut Ui) {
let row_height = self.row_height(ui);
let mut raw_dpstts = self.read_address(0x0005f820);
let mut dpstts = DisplayReg::parse(raw_dpstts);
ui.section("Display", |ui| {
let width = ui.available_width();
TableBuilder::new(ui)
.column(Column::exact(width * 0.5))
.column(Column::exact(width * 0.5))
.cell_layout(Layout::left_to_right(Align::Max))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("DPSTTS");
});
row.col(|ui| {
let mut value_str = format!("{raw_dpstts:04x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut value_str).horizontal_align(Align::Max),
);
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_enabled(true, Checkbox::new(&mut dpstts.lock, "LOCK"));
});
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut dpstts.r1bsy, "R1BSY"));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_enabled(true, Checkbox::new(&mut dpstts.synce, "SYNCE"));
});
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut dpstts.l1bsy, "L1BSY"));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_enabled(true, Checkbox::new(&mut dpstts.re, "RE"));
});
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut dpstts.r0bsy, "R0BSY"));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut dpstts.fclk, "FCLK"));
});
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut dpstts.l0bsy, "L0BSY"));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut dpstts.scanrdy, "SCANRDY"));
});
row.col(|ui| {
ui.add_enabled(true, Checkbox::new(&mut dpstts.disp, "DISP"));
});
});
body.row(row_height, |mut row| {
row.col(|_ui| {});
row.col(|ui| {
if ui
.add(Button::new("DPRST").min_size(ui.available_size()))
.clicked()
{
dpstts.dprst = true;
}
});
});
});
ui.allocate_space(ui.available_size());
});
if dpstts.update(&mut raw_dpstts) {
self.memory.write(self.sim_id, 0x0005f822, &raw_dpstts);
}
}
fn show_drawing_status(&mut self, ui: &mut Ui) {
let row_height = self.row_height(ui);
let [mut raw_xpstts, raw_xpctrl] = self.read_address(0x0005f840);
let mut xpstts = DrawingReg::parse(raw_xpstts);
ui.section("Drawing", |ui| {
let width = ui.available_width();
TableBuilder::new(ui)
.column(Column::exact(width * 0.5))
.column(Column::exact(width * 0.5))
.cell_layout(Layout::left_to_right(Align::Max))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("XPCTRL");
});
row.col(|ui| {
let mut value_str = format!("{raw_xpctrl:04x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut value_str).horizontal_align(Align::Max),
);
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("XPSTTS");
});
row.col(|ui| {
let mut value_str = format!("{raw_xpstts:04x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut value_str).horizontal_align(Align::Max),
);
});
});
/*
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("SBCMP");
});
row.col(|ui| {
let old_value = xpctrl.sbcmp;
ui.add_enabled(true, NumberEdit::new(&mut xpctrl.sbcmp).range(0..32));
cmp_changed = xpctrl.sbcmp != old_value;
});
});
*/
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("SBCOUNT");
});
row.col(|ui| {
ui.add_enabled(
false,
NumberEdit::new(&mut xpstts.sbcount)
.arrows(false)
.range(0..32),
);
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut xpstts.sbout, "SBOUT"));
});
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut xpstts.f1bsy, "F1BSY"));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut xpstts.f0bsy, "F0BSY"));
});
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut xpstts.overtime, "OVERTIME"));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_enabled(true, Checkbox::new(&mut xpstts.xpen, "XPEN"));
});
row.col(|ui| {
if ui
.add(Button::new("XPRST").min_size(ui.available_size()))
.clicked()
{
xpstts.xprst = true;
}
});
});
});
ui.allocate_space(ui.available_size());
});
if xpstts.update(&mut raw_xpstts) {
xpstts.update(&mut raw_xpstts);
self.memory.write(self.sim_id, 0x0005f842, &raw_xpstts);
}
}
fn show_colors(&mut self, ui: &mut Ui) {
let row_height = self.row_height(ui);
let registers = self.registers.borrow();
ui.section("Colors", |ui| {
let width = ui.available_width();
let xspace = ui.spacing().item_spacing.x;
TableBuilder::new(ui)
.column(Column::exact(width * 0.2 - xspace))
.columns(Column::exact(width * 0.20 - xspace), 4)
.cell_layout(Layout::left_to_right(Align::Max))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|_ui| {});
row.col(|_ui| {});
row.col(|ui| {
ui.with_layout(
Layout::centered_and_justified(Direction::LeftToRight),
|ui| ui.label("1"),
);
});
row.col(|ui| {
ui.with_layout(
Layout::centered_and_justified(Direction::LeftToRight),
|ui| ui.label("2"),
);
});
row.col(|ui| {
ui.with_layout(
Layout::centered_and_justified(Direction::LeftToRight),
|ui| ui.label("3"),
);
});
});
let mut brts: [u16; 3] = [
read_address(&registers, 0x0005f824),
read_address(&registers, 0x0005f826),
read_address(&registers, 0x0005f828),
];
body.row(row_height, |mut row| {
let mut stale = false;
row.col(|ui| {
ui.label("BRT");
});
row.col(|_ui| {});
for brt in brts.iter_mut() {
row.col(|ui| {
if ui
.add(NumberEdit::new(brt).range(0..256).arrows(false).hex(true))
.changed()
{
stale = true;
}
});
}
if stale {
self.memory.write(self.sim_id, 0x0005f824, &brts);
}
});
body.row(row_height, |mut row| {
row.col(|_ui| {});
for shade in utils::parse_brts(&brts, Color32::RED) {
row.col(|ui| {
ui.painter().rect_filled(
ui.available_rect_before_wrap(),
0.0,
shade,
);
});
}
});
let mut palettes = read_address::<[u16; 8]>(&registers, 0x0005f860);
let mut add_row = |name: &str, address: u32, value: &mut u16| {
let mut c1 = (*value >> 2) & 0x03;
let mut c2 = (*value >> 4) & 0x03;
let mut c3 = (*value >> 6) & 0x03;
let mut stale = false;
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label(name);
});
row.col(|ui| {
if ui
.add(
NumberEdit::new(value)
.range(0..256)
.desired_width(width * 0.2)
.arrows(false)
.hex(true),
)
.changed()
{
stale = true;
};
});
row.col(|ui| {
if ui
.add(
NumberEdit::new(&mut c1)
.range(0..4)
.desired_width(ui.available_width() - xspace),
)
.changed()
{
*value = (*value & 0xfff3) | (c1 << 2);
stale = true;
}
});
row.col(|ui| {
if ui
.add(
NumberEdit::new(&mut c2)
.range(0..4)
.desired_width(ui.available_width() - xspace),
)
.changed()
{
*value = (*value & 0xffcf) | (c2 << 4);
stale = true;
}
});
row.col(|ui| {
if ui
.add(
NumberEdit::new(&mut c3)
.range(0..4)
.desired_width(ui.available_width() - xspace),
)
.changed()
{
*value = (*value & 0xff3f) | (c3 << 6);
stale = true;
}
});
});
if stale {
self.memory.write(self.sim_id, address, value);
}
};
add_row("GPLT0", 0x0005f860, &mut palettes[0]);
add_row("GPLT1", 0x0005f862, &mut palettes[1]);
add_row("GPLT2", 0x0005f864, &mut palettes[2]);
add_row("GPLT3", 0x0005f866, &mut palettes[3]);
add_row("JPLT0", 0x0005f868, &mut palettes[4]);
add_row("JPLT1", 0x0005f86a, &mut palettes[5]);
add_row("JPLT2", 0x0005f86c, &mut palettes[6]);
add_row("JPLT3", 0x0005f86e, &mut palettes[7]);
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("BKCOL");
});
row.col(|ui| {
let mut bkcol: u16 = read_address(&registers, 0x0005f870);
if ui
.add(
NumberEdit::new(&mut bkcol)
.range(0..4)
.desired_width(ui.available_width() - xspace),
)
.changed()
{
self.memory.write(self.sim_id, 0x0005f870, &bkcol);
}
});
row.col(|_ui| {});
row.col(|ui| {
ui.label("REST");
});
row.col(|ui| {
let mut rest: u16 = read_address(&registers, 0x0005f82a);
if ui
.add(
NumberEdit::new(&mut rest)
.range(0..256)
.arrows(false)
.hex(true),
)
.changed()
{
self.memory.write(self.sim_id, 0x0005f82a, &rest);
}
});
});
});
ui.allocate_space(ui.available_size_before_wrap());
});
}
fn show_objects(&mut self, ui: &mut Ui) {
let row_height = self.row_height(ui);
ui.section("Objects", |ui| {
let width = ui.available_width();
let xspace = ui.spacing().item_spacing.x;
TableBuilder::new(ui)
.column(Column::exact(width * 0.3 - xspace))
.column(Column::exact(width * 0.3 - xspace))
.column(Column::exact(width * 0.4 - xspace))
.cell_layout(Layout::left_to_right(Align::Center).with_main_align(Align::RIGHT))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|_ui| {});
row.col(|ui| {
ui.add_sized(ui.available_size(), Label::new("Start"));
});
row.col(|ui| {
ui.add_sized(ui.available_size(), Label::new("End"));
});
});
let mut spts = self.read_address::<[u16; 4]>(0x0005f848);
let prevs = std::iter::once(0u16).chain(spts.map(|i| (i + 1) & 0x03ff));
let mut changed = false;
for (index, (spt, prev)) in spts.iter_mut().zip(prevs).enumerate() {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label(format!("SPT{index}"));
});
row.col(|ui| {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
ui.label(prev.to_string());
});
});
row.col(|ui| {
if ui.add(NumberEdit::new(spt).range(0..1024)).changed() {
changed = true;
}
});
});
}
if changed {
self.memory.write(self.sim_id, 0x0005f848, &spts);
}
});
ui.allocate_space(ui.available_size_before_wrap());
});
}
fn show_misc(&mut self, ui: &mut Ui) {
let row_height = self.row_height(ui);
let registers = self.registers.borrow();
ui.section("Misc.", |ui| {
let width = ui.available_width();
let xspace = ui.spacing().item_spacing.x;
TableBuilder::new(ui)
.column(Column::exact(width * 0.5 - xspace))
.column(Column::exact(width * 0.5 - xspace))
.cell_layout(Layout::left_to_right(Align::Center).with_main_align(Align::RIGHT))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("FRMCYC");
});
row.col(|ui| {
let mut frmcyc = read_address::<u16>(&registers, 0x0005f82e);
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if ui.add(NumberEdit::new(&mut frmcyc).range(0..32)).changed() {
self.memory.write(self.sim_id, 0x0005f82e, &frmcyc);
}
});
});
});
let mut cta = read_address::<u16>(&registers, 0x0005f830);
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("CTA");
});
row.col(|ui| {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
ui.add_enabled(
false,
NumberEdit::new(&mut cta).arrows(false).hex(true),
);
});
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("CTA_L");
});
row.col(|ui| {
let mut cta_l = cta & 0xff;
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
ui.add_enabled(
false,
NumberEdit::new(&mut cta_l).arrows(false).hex(true),
);
});
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("CTA_R");
});
row.col(|ui| {
let mut cta_r = (cta >> 8) & 0xff;
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
ui.add_enabled(
false,
NumberEdit::new(&mut cta_r).arrows(false).hex(true),
);
});
});
});
});
ui.allocate_space(ui.available_size_before_wrap());
});
}
fn row_height(&self, ui: &mut Ui) -> f32 {
ui.spacing().interact_size.y + ui.style().visuals.selection.stroke.width
}
fn read_address<T: MemoryValue>(&self, address: usize) -> T {
read_address(&self.registers.borrow(), address)
}
}
fn read_address<T: MemoryValue>(registers: &MemoryRef, address: usize) -> T {
let index = (address - 0x0005f800) / size_of::<T>();
registers.read(index)
}
impl AppWindow for RegisterWindow {
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Registers")
.with_inner_size((800.0, 480.0))
}
fn show(&mut self, ui: &mut Ui) {
CentralPanel::default().show_inside(ui, |ui| {
ScrollArea::vertical().show(ui, |ui| {
ui.horizontal_top(|ui| {
let width = ui.available_width();
let xspace = ui.spacing().item_spacing.x;
StripBuilder::new(ui)
.size(Size::exact(width * 0.25 - xspace))
.size(Size::exact(width * 0.225 - xspace))
.size(Size::exact(width * 0.325 - xspace))
.size(Size::exact(width * 0.2 - xspace))
.horizontal(|mut strip| {
strip.cell(|ui| {
self.show_interrupts(ui);
});
strip.strip(|nested| {
nested.sizes(Size::remainder(), 2).vertical(|mut strip| {
strip.cell(|ui| {
self.show_display_status(ui);
});
strip.cell(|ui| {
self.show_drawing_status(ui);
});
});
});
strip.cell(|ui| {
self.show_colors(ui);
});
strip.strip(|nested| {
nested.sizes(Size::remainder(), 2).vertical(|mut strip| {
strip.cell(|ui| {
self.show_objects(ui);
});
strip.cell(|ui| {
self.show_misc(ui);
});
});
});
});
});
});
});
}
}
struct InterruptReg {
timeerr: bool,
xpend: bool,
sbhit: bool,
framestart: bool,
gamestart: bool,
rfbend: bool,
lfbend: bool,
scanerr: bool,
}
impl InterruptReg {
fn parse(value: u16) -> Self {
Self {
timeerr: value & 0x8000 != 0,
xpend: value & 0x4000 != 0,
sbhit: value & 0x2000 != 0,
framestart: value & 0x0010 != 0,
gamestart: value & 0x0008 != 0,
rfbend: value & 0x0004 != 0,
lfbend: value & 0x0002 != 0,
scanerr: value & 0x0001 != 0,
}
}
fn update(&self, value: &mut u16) -> bool {
let new_value = (*value & 0x1fe0)
| if self.timeerr { 0x8000 } else { 0x0000 }
| if self.xpend { 0x4000 } else { 0x0000 }
| if self.sbhit { 0x2000 } else { 0x0000 }
| if self.framestart { 0x0010 } else { 0x0000 }
| if self.gamestart { 0x0008 } else { 0x0000 }
| if self.rfbend { 0x0004 } else { 0x0000 }
| if self.lfbend { 0x0002 } else { 0x0000 }
| if self.scanerr { 0x0001 } else { 0x0000 };
let changed = *value != new_value;
*value = new_value;
changed
}
}
struct DisplayReg {
lock: bool,
synce: bool,
re: bool,
fclk: bool,
scanrdy: bool,
r1bsy: bool,
l1bsy: bool,
r0bsy: bool,
l0bsy: bool,
disp: bool,
dprst: bool,
}
impl DisplayReg {
fn parse(value: u16) -> Self {
Self {
lock: value & 0x0400 != 0,
synce: value & 0x0200 != 0,
re: value & 0x0100 != 0,
fclk: value & 0x0080 != 0,
scanrdy: value & 0x0040 != 0,
r1bsy: value & 0x0020 != 0,
l1bsy: value & 0x0010 != 0,
r0bsy: value & 0x0008 != 0,
l0bsy: value & 0x0004 != 0,
disp: value & 0x0002 != 0,
dprst: value & 0x0001 != 0,
}
}
fn update(&self, value: &mut u16) -> bool {
let new_value = (*value & 0xf800)
| if self.lock { 0x0400 } else { 0x0000 }
| if self.synce { 0x0200 } else { 0x0000 }
| if self.re { 0x0100 } else { 0x0000 }
| if self.fclk { 0x0080 } else { 0x0000 }
| if self.scanrdy { 0x0040 } else { 0x0000 }
| if self.r1bsy { 0x0020 } else { 0x0000 }
| if self.l1bsy { 0x0010 } else { 0x0000 }
| if self.r0bsy { 0x0008 } else { 0x0000 }
| if self.l0bsy { 0x0004 } else { 0x0000 }
| if self.disp { 0x0002 } else { 0x0000 }
| if self.dprst { 0x0001 } else { 0x0000 };
let changed = *value != new_value;
*value = new_value;
changed
}
}
struct DrawingReg {
sbout: bool,
sbcount: u8,
overtime: bool,
f1bsy: bool,
f0bsy: bool,
xpen: bool,
xprst: bool,
}
impl DrawingReg {
fn parse(value: u16) -> Self {
Self {
sbout: value & 0x8000 != 0,
sbcount: (value >> 8) as u8 & 0x1f,
overtime: value & 0x0010 != 0,
f1bsy: value & 0x0008 != 0,
f0bsy: value & 0x0004 != 0,
xpen: value & 0x0002 != 0,
xprst: value & 0x0001 != 0,
}
}
fn update(&self, value: &mut u16) -> bool {
let new_value = (*value & 0x60e0)
| if self.sbout { 0x8000 } else { 0x0000 }
| (((self.sbcount & 0x1f) as u16) << 8)
| if self.overtime { 0x0010 } else { 0x0000 }
| if self.f1bsy { 0x0008 } else { 0x0000 }
| if self.f0bsy { 0x0004 } else { 0x0000 }
| if self.xpen { 0x0002 } else { 0x0000 }
| if self.xprst { 0x0001 } else { 0x0000 };
let changed = *value != new_value;
*value = new_value;
changed
}
}
+251
View File
@@ -0,0 +1,251 @@
use egui::{Color32, Image, ImageSource, Response, Sense, TextureOptions, Ui, Widget};
pub const GENERIC_PALETTE: [u8; 4] = [0, 32, 64, 128];
pub fn shade(brt: u8, color: Color32) -> Color32 {
let corrected = if brt > 132 {
255.0
} else {
(brt as f32 * 255.0 / 133.0).round()
};
color.gamma_multiply(corrected / 255.0)
}
pub fn generic_palette(color: Color32) -> [Color32; 4] {
GENERIC_PALETTE.map(|brt| shade(brt, color))
}
pub const fn parse_palette(palette: u8) -> [u8; 4] {
[
0,
(palette >> 2) & 0x03,
(palette >> 4) & 0x03,
(palette >> 6) & 0x03,
]
}
pub const fn parse_shades(brts: &[u8; 8]) -> [u8; 4] {
[
0,
brts[0],
brts[2],
brts[0].saturating_add(brts[2]).saturating_add(brts[4]),
]
}
pub fn parse_brts(brts: &[u16; 3], color: Color32) -> [Color32; 4] {
[
Color32::BLACK,
shade(brts[0] as u8, color),
shade(brts[1] as u8, color),
shade((brts[0] + brts[1] + brts[2]) as u8, color),
]
}
pub fn palette_colors(palette: u8, brts: &[u8; 8], color: Color32) -> [Color32; 4] {
let colors = parse_shades(brts).map(|s| shade(s, color));
parse_palette(palette).map(|p| colors[p as usize])
}
pub struct Object {
pub x: i16,
pub lon: bool,
pub ron: bool,
pub parallax: i16,
pub y: i16,
pub data: CellData,
}
impl Object {
pub fn parse(object: [u16; 4]) -> Self {
let x = ((object[0] & 0x03ff) << 6 >> 6) as i16;
let parallax = ((object[1] & 0x03ff) << 6 >> 6) as i16;
let lon = object[1] & 0x8000 != 0;
let ron = object[1] & 0x4000 != 0;
let y = (object[2] & 0x00ff) as i16;
// Y is stored as the bottom 8 bits of an i16,
// so only sign extend if it's out of range.
let y = if y > 224 { y << 8 >> 8 } else { y };
let data = CellData::parse(object[3]);
Self {
x,
lon,
ron,
parallax,
y,
data,
}
}
pub fn update(&self, source: &mut [u16; 4]) -> bool {
let mut changed = false;
let new_x = (self.x as u16 & 0x03ff) | (source[0] & 0xfc00);
changed |= source[0] != new_x;
source[0] = new_x;
let new_p = if self.lon { 0x8000 } else { 0x0000 }
| if self.ron { 0x4000 } else { 0x0000 }
| (self.parallax as u16 & 0x3ff)
| (source[1] & 0x3c00);
changed |= source[1] != new_p;
source[1] = new_p;
let new_y = (self.y as u16 & 0x00ff) | (source[2] & 0xff00);
changed |= source[2] != new_y;
source[2] = new_y;
if self.data.update(&mut source[3]) {
changed = true;
}
changed
}
}
pub struct CellData {
pub palette_index: usize,
pub hflip: bool,
pub vflip: bool,
pub char_index: usize,
}
impl CellData {
pub fn parse(cell: u16) -> Self {
let char_index = (cell & 0x7ff) as usize;
let vflip = cell & 0x1000 != 0;
let hflip = cell & 0x2000 != 0;
let palette_index = (cell >> 14) as usize;
Self {
char_index,
vflip,
hflip,
palette_index,
}
}
pub fn update(&self, source: &mut u16) -> bool {
let new_value = ((self.palette_index as u16) << 14)
| if self.hflip { 0x2000 } else { 0x0000 }
| if self.vflip { 0x1000 } else { 0x0000 }
| (self.char_index as u16 & 0x07ff)
| (*source & 0x0800);
let changed = *source != new_value;
*source = new_value;
changed
}
}
pub fn read_char_row(
char: &[u16; 8],
hflip: bool,
vflip: bool,
row: usize,
) -> impl Iterator<Item = u8> {
let pixels = if vflip { char[7 - row] } else { char[row] };
(0..16).step_by(2).map(move |i| {
let pixel = if hflip { 14 - i } else { i };
((pixels >> pixel) & 0x3) as u8
})
}
pub fn read_char_pixel(char: &[u16; 8], hflip: bool, vflip: bool, row: usize, col: usize) -> u8 {
let pixels = if vflip { char[7 - row] } else { char[row] };
let pixel = if hflip { 7 - col } else { col } << 1;
((pixels >> pixel) & 0x3) as u8
}
pub struct CharacterGrid<'a> {
source: ImageSource<'a>,
scale: f32,
show_grid: bool,
selected: Option<usize>,
}
impl<'a> CharacterGrid<'a> {
pub fn new(source: impl Into<ImageSource<'a>>) -> Self {
Self {
source: source.into(),
scale: 1.0,
show_grid: false,
selected: None,
}
}
pub fn with_scale(self, scale: f32) -> Self {
Self { scale, ..self }
}
pub fn with_grid(self, show_grid: bool) -> Self {
Self { show_grid, ..self }
}
pub fn with_selected(self, selected: usize) -> Self {
Self {
selected: Some(selected),
..self
}
}
pub fn show(self, ui: &mut Ui) -> Option<usize> {
let start_pos = ui.cursor().min;
let cell_size = 8.0 * self.scale;
let res = self.ui(ui);
let grid_width_cells = ((res.rect.max.x - res.rect.min.x) / cell_size).round() as usize;
if res.clicked() {
let click_pos = res.interact_pointer_pos()?;
let grid_pos = (click_pos - start_pos) / cell_size;
Some((grid_pos.y as usize * grid_width_cells) + grid_pos.x as usize)
} else {
None
}
}
}
impl Widget for CharacterGrid<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let image = Image::new(self.source)
.fit_to_original_size(self.scale)
.texture_options(TextureOptions::NEAREST)
.sense(Sense::click());
let res = ui.add(image);
let cell_size = 8.0 * self.scale;
let grid_width_cells = ((res.rect.max.x - res.rect.min.x) / cell_size).round() as usize;
let grid_height_cells = ((res.rect.max.y - res.rect.min.y) / cell_size).round() as usize;
let painter = ui.painter_at(res.rect);
if self.show_grid {
let stroke = ui.style().visuals.widgets.noninteractive.fg_stroke;
for x in (1..grid_width_cells).map(|i| (i as f32) * cell_size) {
let p1 = (res.rect.min.x + x, res.rect.min.y).into();
let p2 = (res.rect.min.x + x, res.rect.max.y).into();
painter.line_segment([p1, p2], stroke);
}
for y in (1..grid_height_cells).map(|i| (i as f32) * cell_size) {
let p1 = (res.rect.min.x, res.rect.min.y + y).into();
let p2 = (res.rect.max.x, res.rect.min.y + y).into();
painter.line_segment([p1, p2], stroke);
}
}
if let Some(selected) = self.selected {
let x1 = (selected % grid_width_cells) as f32 * cell_size;
let x2 = x1 + cell_size;
let y1 = (selected / grid_width_cells) as f32 * cell_size;
let y2 = y1 + cell_size;
painter.line(
vec![
(res.rect.min + (x1, y1).into()),
(res.rect.min + (x2, y1).into()),
(res.rect.min + (x2, y2).into()),
(res.rect.min + (x1, y2).into()),
(res.rect.min + (x1, y1).into()),
],
ui.style().visuals.widgets.active.fg_stroke,
);
}
res
}
}
File diff suppressed because it is too large Load Diff
-218
View File
@@ -1,218 +0,0 @@
#include "audio.h"
#include "controller.h"
#include "emulation.h"
#include "external/tinyfiledialogs.h"
#include <stdio.h>
#include <stdlib.h>
#include "ui.h"
#include "window.h"
struct UIContext {
EmulationContext emu;
WindowContext win;
AudioContext aud;
ControllerState ctrl;
};
UIContext *uiInit() {
UIContext *ui = malloc(sizeof(UIContext));
if (emuInit(&ui->emu)) {
goto destroy_ui;
}
if (windowInit(&ui->win, "Shrooms VB")) {
goto destroy_emu;
}
if (audioInit(&ui->aud)) {
goto destroy_window;
}
ctrlInit(&ui->ctrl);
return ui;
destroy_window:
windowDestroy(&ui->win);
destroy_emu:
emuDestroy(&ui->emu);
destroy_ui:
free(ui);
return NULL;
}
void uiDestroy(UIContext *ui) {
audioDestroy(&ui->aud);
windowDestroy(&ui->win);
emuDestroy(&ui->emu);
free(ui);
}
int uiLoadGame(UIContext *ui, const char *path) {
FILE *file = fopen(path, "rb");
uint8_t *rom = NULL;
long fileSize;
uint32_t romSize;
int result;
if (!file) {
perror("could not open file");
return -1;
}
if (fseek(file, 0, SEEK_END)) {
perror("could not seek file end");
goto close_file;
}
fileSize = ftell(file);
if (fileSize == -1) {
perror("could not read file size");
goto close_file;
}
if (fseek(file, 0, SEEK_SET)) {
perror("could not seek file start");
goto close_file;
}
romSize = (uint32_t) fileSize;
rom = malloc(romSize);
if (!rom) {
perror("could not allocate ROM");
goto close_file;
}
fread(rom, 1, romSize, file);
if (ferror(file)) {
perror("could not read file");
goto free_rom;
}
result = fclose(file);
file = NULL;
if (result) {
perror("could not close file");
goto free_rom;
}
emuLoadGame(&ui->emu, rom, romSize);
return 0;
free_rom:
free(rom);
close_file:
if (file) fclose(file);
return -1;
}
static const char *MENU_ITEMS[4] = {
"File",
"Emulation",
"Video",
NULL
};
static const char *ROM_EXTENSIONS[2] = {
"*.vb",
NULL
};
typedef enum status_t {
status_paused,
status_running
} status_t;
int uiRun(UIContext *ui, bool running) {
static uint8_t leftEye[384*224] = {0};
static uint8_t rightEye[384*224] = {0};
status_t status = running ? status_running : status_paused;
windowUpdate(&ui->win, leftEye, rightEye);
while (1) {
struct nk_context *ctx;
SDL_Event event;
void *samples;
uint32_t bytes;
ctx = ui->win.nk;
if (status == status_running) {
emuTick(&ui->emu);
}
if (emuReadPixels(&ui->emu, leftEye, rightEye)) {
windowUpdate(&ui->win, leftEye, rightEye);
}
emuReadSamples(&ui->emu, &samples, &bytes);
if (bytes) {
audioUpdate(&ui->aud, samples, bytes);
}
windowDisplayBegin(&ui->win);
/* GUI */
if (windowGuiBegin(&ui->win, "Shrooms VB")) {
windowMenubarBegin(&ui->win, MENU_ITEMS);
if (windowMenuBegin(&ui->win, "File", 100)) {
if (windowMenuItemLabel(&ui->win, "Open ROM")) {
char *file = tinyfd_openFileDialog("Pick a ROM", NULL, 1, ROM_EXTENSIONS, "Virtual Boy ROM files", false);
if (file) {
uiLoadGame(ui, file);
status = status_running;
}
}
if (windowMenuItemLabel(&ui->win, "Quit")) {
SDL_Event QuitEvent;
QuitEvent.type = SDL_QUIT;
QuitEvent.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&QuitEvent);
}
windowMenuEnd(&ui->win);
}
if (windowMenuBegin(&ui->win, "Emulation", 100)) {
const char *label = status == status_paused ? "Resume" : "Pause";
if (windowMenuItemLabel(&ui->win, label)) {
if (status == status_paused)
status = status_running;
else
status = status_paused;
}
if (windowMenuItemLabel(&ui->win, "Reset")) {
emuReset(&ui->emu);
status = emuIsGameLoaded(&ui->emu) ? status_running : status_paused;
}
windowMenuEnd(&ui->win);
}
if (windowMenuBegin(&ui->win, "Video", 100)) {
float multiplier = windowGetScreenSizeMultiplier(&ui->win);
if (windowMenuItemLabelChecked(&ui->win, "x1", multiplier == 1.0f)) {
windowSetScreenSizeMultiplier(&ui->win, 1.0f);
}
if (windowMenuItemLabelChecked(&ui->win, "x2", multiplier == 2.0f)) {
windowSetScreenSizeMultiplier(&ui->win, 2.0f);
}
if (windowMenuItemLabelChecked(&ui->win, "x3", multiplier == 3.0f)) {
windowSetScreenSizeMultiplier(&ui->win, 3.0f);
}
if (windowMenuItemLabelChecked(&ui->win, "x4", multiplier == 4.0f)) {
windowSetScreenSizeMultiplier(&ui->win, 4.0f);
}
windowMenuEnd(&ui->win);
}
windowMenubarEnd(&ui->win);
}
windowGuiEnd(&ui->win);
windowDisplayEnd(&ui->win);
nk_input_begin(ctx);
while (SDL_PollEvent(&event)) {
nk_sdl_handle_event(&event);
if (event.type == SDL_QUIT) {
return 0;
}
if (event.type == SDL_KEYDOWN) {
ctrlKeyDown(&ui->ctrl, event.key.keysym.sym);
}
if (event.type == SDL_KEYUP) {
ctrlKeyUp(&ui->ctrl, event.key.keysym.sym);
}
}
nk_input_end(ctx);
emuSetKeys(&ui->emu, ctrlKeys(&ui->ctrl));
}
}
-13
View File
@@ -1,13 +0,0 @@
#ifndef SHROOMS_VB_NATIVE_UI_
#define SHROOMS_VB_NATIVE_UI_
#include <stdbool.h>
typedef struct UIContext UIContext;
UIContext *uiInit();
void uiDestroy(UIContext *ui);
int uiLoadGame(UIContext *ui, const char *path);
int uiRun(UIContext *ui, bool running);
#endif
-291
View File
@@ -1,291 +0,0 @@
#include "assets.h"
#include "nuklear.h"
#include <string.h>
#include "window.h"
#define MENU_HEIGHT 20
#define SCREEN_WIDTH 384
#define SCREEN_HEIGHT 224
static void setColorTable(struct nk_color *table) {
table[NK_COLOR_TEXT] = nk_rgb(40, 40, 40);
table[NK_COLOR_WINDOW] = nk_rgb(255, 255, 255);
table[NK_COLOR_HEADER] = nk_rgb(40, 40, 40);
table[NK_COLOR_BORDER] = nk_rgb(175, 175, 175);
table[NK_COLOR_BUTTON] = nk_rgb(255, 255, 255);
table[NK_COLOR_BUTTON_HOVER] = nk_rgb(215, 215, 215);
table[NK_COLOR_BUTTON_ACTIVE] = nk_rgb(175, 175, 175);
table[NK_COLOR_TOGGLE] = nk_rgb(100, 100, 100);
table[NK_COLOR_TOGGLE_HOVER] = nk_rgb(120, 120, 120);
table[NK_COLOR_TOGGLE_CURSOR] = nk_rgb(45, 45, 45);
table[NK_COLOR_SELECT] = nk_rgb(45, 45, 45);
table[NK_COLOR_SELECT_ACTIVE] = nk_rgb(35, 35, 35);
table[NK_COLOR_SLIDER] = nk_rgb(38, 38, 38);
table[NK_COLOR_SLIDER_CURSOR] = nk_rgb(100, 100, 100);
table[NK_COLOR_SLIDER_CURSOR_HOVER] = nk_rgb(120, 120, 120);
table[NK_COLOR_SLIDER_CURSOR_ACTIVE] = nk_rgb(150, 150, 150);
table[NK_COLOR_PROPERTY] = nk_rgb(38, 38, 38);
table[NK_COLOR_EDIT] = nk_rgb(38, 38, 38);
table[NK_COLOR_EDIT_CURSOR] = nk_rgb(175, 175, 175);
table[NK_COLOR_COMBO] = nk_rgb(45, 45, 45);
table[NK_COLOR_CHART] = nk_rgb(120, 120, 120);
table[NK_COLOR_CHART_COLOR] = nk_rgb(45, 45, 45);
table[NK_COLOR_CHART_COLOR_HIGHLIGHT] = nk_rgb(255, 0, 0);
table[NK_COLOR_SCROLLBAR] = nk_rgb(40, 40, 40);
table[NK_COLOR_SCROLLBAR_CURSOR] = nk_rgb(100, 100, 100);
table[NK_COLOR_SCROLLBAR_CURSOR_HOVER] = nk_rgb(120, 120, 120);
table[NK_COLOR_SCROLLBAR_CURSOR_ACTIVE] = nk_rgb(150, 150, 150);
table[NK_COLOR_TAB_HEADER] = nk_rgb(40, 40, 40);
table[NK_COLOR_KNOB] = nk_rgb(38, 38, 38);
table[NK_COLOR_KNOB_CURSOR] = nk_rgb(100, 100, 100);
table[NK_COLOR_KNOB_CURSOR_HOVER] = nk_rgb(120, 120, 120);
table[NK_COLOR_KNOB_CURSOR_ACTIVE] = nk_rgb(150, 150, 150);
}
static void applyStyles(struct nk_context *ctx, float scaleX, float scaleY) {
struct nk_color table[NK_COLOR_COUNT];
setColorTable(table);
nk_style_from_table(ctx, table);
ctx->style.window.padding = nk_vec2(0, 0);
ctx->style.window.spacing = nk_vec2(0, 0);
ctx->style.window.menu_padding = nk_vec2(0, 0);
ctx->style.window.menu_border = 0;
ctx->style.menu_button.hover = nk_style_item_color(table[NK_COLOR_BUTTON_HOVER]);
ctx->style.menu_button.active = nk_style_item_color(table[NK_COLOR_BUTTON_ACTIVE]);
ctx->style.menu_button.padding = nk_vec2(2 * scaleX, 2 * scaleY);
ctx->style.contextual_button.padding = nk_vec2(20 * scaleX, 4 * scaleY);
}
/* scale the window for High-DPI displays */
static void scaleWindow(WindowContext *win) {
int renderW, renderH;
int oldWindowX, oldWindowY;
int newWindowX, newWindowY;
int oldWindowW, oldWindowH;
int newWindowW, newWindowH;
float scaleX, scaleY;
float hdpi, vdpi;
SDL_GetRendererOutputSize(win->renderer, &renderW, &renderH);
SDL_GetWindowPosition(win->window, &oldWindowX, &oldWindowY);
SDL_GetWindowSize(win->window, &oldWindowW, &oldWindowH);
SDL_GetDisplayDPI(SDL_GetWindowDisplayIndex(win->window), NULL, &hdpi, &vdpi);
scaleX = (float)(renderW) / (float)(oldWindowW);
scaleY = (float)(renderH) / (float)(oldWindowH);
win->screenScaleX = (hdpi / 96) * scaleX;
win->screenScaleY = (vdpi / 96) * scaleY;
newWindowW = SCREEN_WIDTH * win->screenSizeMultiplier * (hdpi / 96) / scaleX;
newWindowH = (SCREEN_HEIGHT * win->screenSizeMultiplier + MENU_HEIGHT) * (vdpi / 96) / scaleY;
newWindowX = oldWindowX - (newWindowW - oldWindowW) / 2;
newWindowY = oldWindowY - (newWindowH - oldWindowH) / 2;
if (newWindowX < 0) newWindowX = 0;
if (newWindowY < 0) newWindowY = 0;
SDL_SetWindowSize(win->window, newWindowW, newWindowH);
SDL_SetWindowPosition(win->window, newWindowX, newWindowY);
}
int windowInit(WindowContext *win, const char *title) {
win->screenSizeMultiplier = 1.0f;
win->window = SDL_CreateWindow(title,
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
SCREEN_WIDTH, SCREEN_HEIGHT + MENU_HEIGHT, SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI);
if (!win->window) {
fprintf(stderr, "Error creating window: %s\n", SDL_GetError());
return -1;
}
win->renderer = SDL_CreateRenderer(win->window, -1, 0);
if (!win->renderer) {
fprintf(stderr, "Error creating renderer: %s\n", SDL_GetError());
goto cleanup_window;
}
win->leftEye = SDL_CreateTexture(win->renderer, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STREAMING, 384, 224);
if (!win->leftEye) {
fprintf(stderr, "Error creating left eye texture: %s\n", SDL_GetError());
goto cleanup_renderer;
}
SDL_SetTextureColorMod(win->leftEye, 0xff, 0, 0);
win->rightEye = SDL_CreateTexture(win->renderer, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STREAMING, 384, 224);
if (!win->rightEye) {
fprintf(stderr, "Error creating right eye texture: %s\n", SDL_GetError());
goto cleanup_left_eye;
}
SDL_SetTextureColorMod(win->rightEye, 0, 0xc6, 0xf0);
SDL_SetTextureBlendMode(win->rightEye, SDL_BLENDMODE_ADD);
scaleWindow(win);
win->nk = nk_sdl_init(win->window, win->renderer);
applyStyles(win->nk, win->screenScaleX, win->screenScaleY);
/* tell nuklear the mouse moved somewhere so it doesn't think we're hovering in the top left */
nk_input_motion(win->nk, 1024, 1024);
{
struct nk_font_atlas *atlas;
struct nk_font_config config = nk_font_config(0);
config.pixel_snap = 1;
config.oversample_h = 8;
config.oversample_v = 8;
nk_sdl_font_stash_begin(&atlas);
win->font = nk_font_atlas_add_from_memory(atlas, (void*) SELAWIK, SELAWIK_LEN, 13 * win->screenScaleY, &config);
nk_sdl_font_stash_end();
nk_style_set_font(win->nk, &win->font->handle);
}
return 0;
cleanup_left_eye:
SDL_DestroyTexture(win->leftEye);
cleanup_renderer:
SDL_DestroyRenderer(win->renderer);
cleanup_window:
SDL_DestroyWindow(win->window);
return -1;
}
void windowDestroy(WindowContext *win) {
SDL_DestroyTexture(win->rightEye);
SDL_DestroyTexture(win->leftEye);
SDL_DestroyRenderer(win->renderer);
SDL_DestroyWindow(win->window);
}
float windowGetScreenSizeMultiplier(WindowContext *win) {
return win->screenSizeMultiplier;
}
void windowSetScreenSizeMultiplier(WindowContext *win, float multiplier) {
win->screenSizeMultiplier = multiplier;
scaleWindow(win);
applyStyles(win->nk, win->screenScaleX, win->screenScaleY);
}
static void copyScreenTexture(uint8_t *dst, const uint8_t *src, int pitch) {
int x, y, i;
uint8_t color;
int delta = pitch / 384;
for (y = 0; y < 224; ++y) {
for (x = 0; x < 384; x += 1) {
color = src[(y * 384) + x];
for (i = 0; i < delta; ++i) {
dst[(y * pitch) + (x * delta) + i] = color;
}
}
}
}
static void updateEye(SDL_Texture *eye, const uint8_t *bytes) {
void *target;
int pitch;
if (SDL_LockTexture(eye, NULL, &target, &pitch)) {
fprintf(stderr, "Error locking buffer for eye: %s\n", SDL_GetError());
return;
}
copyScreenTexture(target, bytes, pitch);
SDL_UnlockTexture(eye);
}
void windowUpdate(WindowContext *win, const uint8_t *left, const uint8_t *right) {
updateEye(win->leftEye, left);
updateEye(win->rightEye, right);
}
int windowScaleX(WindowContext *win, int x) {
return x * win->screenScaleX;
}
int windowScaleY(WindowContext *win, int y) {
return y * win->screenScaleY;
}
int windowGetMenuHeight(WindowContext *win) {
return (MENU_HEIGHT + 2) * win->screenScaleY;
}
int windowGetScreenHeight(WindowContext *win) {
return SCREEN_HEIGHT * win->screenSizeMultiplier * win->screenScaleY;
}
void windowDisplayBegin(WindowContext *win) {
SDL_Rect dst;
dst.x = 0;
dst.y = MENU_HEIGHT * win->screenScaleY;
dst.w = SCREEN_WIDTH * win->screenSizeMultiplier * win->screenScaleX;
dst.h = SCREEN_HEIGHT * win->screenSizeMultiplier * win->screenScaleY;
SDL_RenderClear(win->renderer);
SDL_RenderCopy(win->renderer, win->leftEye, NULL, &dst);
SDL_RenderCopy(win->renderer, win->rightEye, NULL, &dst);
}
void windowDisplayEnd(WindowContext *win) {
nk_sdl_render(NK_ANTI_ALIASING_ON);
SDL_RenderPresent(win->renderer);
}
bool windowGuiBegin(WindowContext *win, const char *title) {
return nk_begin(win->nk, title,
nk_rect(0, 0, SCREEN_WIDTH * win->screenSizeMultiplier * win->screenScaleX, MENU_HEIGHT * win->screenScaleY),
NK_WINDOW_NO_SCROLLBAR | NK_WINDOW_BACKGROUND);
}
void windowGuiEnd(WindowContext *win) {
nk_end(win->nk);
}
void windowMenubarBegin(WindowContext *win, const char **items) {
const char **item;
nk_menubar_begin(win->nk);
nk_layout_row_template_begin(win->nk, MENU_HEIGHT * win->screenScaleY);
for (item = items; *item != NULL; item++) {
struct nk_user_font *handle;
int len;
float width;
handle = &win->font->handle;
len = nk_strlen(*item);
width = handle->width(handle->userdata, handle->height, *item, len) + (16 * win->screenScaleX);
nk_layout_row_template_push_static(win->nk, width);
}
nk_layout_row_template_end(win->nk);
}
void windowMenubarEnd(WindowContext *win) {
nk_menubar_end(win->nk);
}
bool windowMenuBegin(WindowContext *win, const char *label, int width) {
if (!nk_menu_begin_label(win->nk, label, NK_TEXT_ALIGN_CENTERED, nk_vec2(windowScaleX(win, width), windowGetScreenHeight(win)))) {
return false;
}
nk_layout_row_dynamic(win->nk, windowGetMenuHeight(win), 1);
return true;
}
void windowMenuEnd(WindowContext *win) {
nk_menu_end(win->nk);
}
bool windowMenuItemLabel(WindowContext *win, const char *label) {
return nk_menu_item_label(win->nk, label, NK_TEXT_ALIGN_LEFT);
}
bool windowMenuItemLabelChecked(WindowContext *win, const char *label, bool checked) {
char buffer[80];
bool result;
if (!checked) {
return windowMenuItemLabel(win, label);
}
strcpy(buffer, " * ");
strncpy(buffer + 5, label, 74);
buffer[79] = '\0';
nk_style_push_vec2(win->nk, &win->nk->style.contextual_button.padding, nk_vec2(4 * win->screenScaleX, 4 * win->screenScaleY));
result = nk_menu_item_label(win->nk, buffer, NK_TEXT_ALIGN_LEFT);
nk_style_pop_vec2(win->nk);
return result;
}
-41
View File
@@ -1,41 +0,0 @@
#ifndef SHROOMS_VB_NATIVE_WINDOW_
#define SHROOMS_VB_NATIVE_WINDOW_
#include <SDL2/SDL.h>
#include "nuklear.h"
typedef struct WindowContext {
SDL_Window *window;
float screenSizeMultiplier;
float screenScaleX, screenScaleY;
SDL_Renderer *renderer;
SDL_Texture *leftEye;
SDL_Texture *rightEye;
struct nk_context *nk;
struct nk_font *font;
} WindowContext;
int windowInit(WindowContext *win, const char *title);
void windowDestroy(WindowContext *win);
float windowGetScreenSizeMultiplier(WindowContext *win);
void windowSetScreenSizeMultiplier(WindowContext *win, float multiplier);
void windowUpdate(WindowContext *win, const uint8_t *left, const uint8_t *right);
int windowScaleX(WindowContext *win, int x);
int windowScaleY(WindowContext *win, int y);
int windowGetMenuHeight(WindowContext *win);
int windowGetScreenHeight(WindowContext *win);
void windowDisplayBegin(WindowContext *win);
void windowDisplayEnd(WindowContext *win);
bool windowGuiBegin(WindowContext *win, const char *title);
void windowGuiEnd(WindowContext *win);
void windowMenubarBegin(WindowContext *win, const char **items);
void windowMenubarEnd(WindowContext *win);
bool windowMenuBegin(WindowContext *win, const char *label, int width);
void windowMenuEnd(WindowContext *win);
bool windowMenuItemLabel(WindowContext *win, const char *label);
bool windowMenuItemLabelChecked(WindowContext *win, const char *label, bool checked);
#endif