commit 6f829c5252a4868799392c2e3d823b6ff5ba6757 Author: modeco80 Date: Fri Aug 2 03:20:29 2024 -0400 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0b53eb8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = tab +indent_size = 4 + +# specifically for YAML +[{yml, yaml}] +indent_style = space diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10a3b1f --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +vgcore* +perf* + +trash/ +cores/ +roms/ +libretro.so +core + +# CLion +.idea/ +cmake-build-debug/ + +# Rust +target/ + +# misc test data +testdata/ diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..18d655e --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +hard_tabs = true \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7dbfba7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,684 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "cc" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "libretro-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207b060b02cecbcee6df3d0f5ed38691d5c4df1379dd1acd5c49c9b25d20b439" +dependencies = [ + "libc", +] + +[[package]] +name = "libvnc-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8399305e393bfa13b9974237b72a5190c41b0c65c4355df1ce7ac381bf75f775" +dependencies = [ + "bindgen", + "cc", + "cmake", + "pkg-config", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "prettyplease" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "retro_frontend" +version = "0.1.0" +dependencies = [ + "cc", + "libc", + "libloading", + "libretro-sys", + "once_cell", + "rgb565", + "thiserror", + "tracing", +] + +[[package]] +name = "retrovnc" +version = "0.1.0" +dependencies = [ + "clap", + "libvnc-sys", + "retro_frontend", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "rgb565" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43e85498d0bb728f77a88b4313eaf4ed21673f3f8a05c36e835cf6c9c0d066" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8ca25ba --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +resolver = "2" +members = [ + "crates/*" +] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..104aeda --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2024 Lily Tsuru (modeco80) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a105c1a --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# retrovnc + +a headless Libretro frontend that exports a VNC server. + +# Dependencies + +- A C++ toolchain +- A Rust toolchain. +- Maybe libvncserver (i'm not sure, it seems like the package can build it). + +# Building + +`$ cargo b --release` + +# Usage + +`$ retrovnc --core --rom ` + +For disc-based titles it is probably a good idea to pass the cuesheet file. I will implement stuff later to make this less annoying. + + diff --git a/crates/retro_frontend/Cargo.toml b/crates/retro_frontend/Cargo.toml new file mode 100644 index 0000000..3093777 --- /dev/null +++ b/crates/retro_frontend/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "retro_frontend" +version = "0.1.0" +edition = "2021" +# Could be useful, and does build seperately, so... +# publish = false + +[dependencies] +libc = "0.2.155" +libloading = "0.8.3" +libretro-sys = "0.1.1" +once_cell = "1.19.0" +rgb565 = "0.1.3" +thiserror = "1.0.61" +tracing = "0.1.40" + +[build-dependencies] +cc = "1.0.99" diff --git a/crates/retro_frontend/build.rs b/crates/retro_frontend/build.rs new file mode 100644 index 0000000..854edfe --- /dev/null +++ b/crates/retro_frontend/build.rs @@ -0,0 +1,12 @@ +use cc; + +fn main() { + let mut build = cc::Build::new(); + + build + .emit_rerun_if_env_changed(true) + .cpp(true) + .std("c++20") + .file("src/libretro_log_helper.cpp") + .compile("retro_log_helper"); +} diff --git a/crates/retro_frontend/src/core.rs b/crates/retro_frontend/src/core.rs new file mode 100644 index 0000000..54ec90f --- /dev/null +++ b/crates/retro_frontend/src/core.rs @@ -0,0 +1,34 @@ +use std::path::Path; + +use crate::frontend; +use crate::result::Result; + +/// A "RAII" wrapper over a core, useful for making cleanup a bit less ardous. +pub struct Core(); + +impl Core { + /// Same as [frontend::load_core], but returns a struct which will keep the core + /// alive until it is dropped. + pub fn load>(path: P) -> Result { + frontend::load_core(path.as_ref())?; + Ok(Self {}) + } + + /// Same as [frontend::load_game]. + pub fn load_game>(&mut self, rom_path: P) -> Result<()> { + frontend::load_game(rom_path)?; + Ok(()) + } + + /// Same as [frontend::unload_game]. + pub fn unload_game(&mut self) -> Result<()> { + frontend::unload_game()?; + Ok(()) + } +} + +impl Drop for Core { + fn drop(&mut self) { + let _ = frontend::unload_core(); + } +} diff --git a/crates/retro_frontend/src/frontend.rs b/crates/retro_frontend/src/frontend.rs new file mode 100644 index 0000000..e784a99 --- /dev/null +++ b/crates/retro_frontend/src/frontend.rs @@ -0,0 +1,98 @@ +//! The primary frontend API. +//! This is a singleton API, not by choice, but due to Libretro's design. +//! +//! # Safety +//! Don't even think about using this across multiple threads. If you want to run multiple frontends, +//! it's easier to just host this crate in a runner process and fork off those runners. +use crate::frontend_impl::FRONTEND_IMPL; +use crate::joypad::Joypad; +use crate::libretro_sys_new::*; +use crate::result::Result; +use std::cell::RefCell; +use std::rc::Rc; + +/// Sets the callback used to update video. +pub fn set_video_update_callback(cb: impl FnMut(&[u32]) + 'static) { + unsafe { + FRONTEND_IMPL.set_video_update_callback(cb); + } +} + +/// Sets the callback for video resize. +pub fn set_video_resize_callback(cb: impl FnMut(u32, u32) + 'static) { + unsafe { + FRONTEND_IMPL.set_video_resize_callback(cb); + } +} + +/// Sets the callback for audio samples. +pub fn set_audio_sample_callback(cb: impl FnMut(&[i16], usize) + 'static) { + unsafe { + FRONTEND_IMPL.set_audio_sample_callback(cb); + } +} + +/// Sets the callback for input polling. +pub fn set_input_poll_callback(cb: impl FnMut() + 'static) { + unsafe { + FRONTEND_IMPL.set_input_poll_callback(cb); + } +} + +/// Sets the given port's input device. This takes a implementation of the [crate::joypad::Joypad] +/// trait, which will provide the information needed for libretro to work and all that. +pub fn set_input_port_device(port: u32, device: Rc>) { + unsafe { + FRONTEND_IMPL.set_input_port_device(port, device); + } +} + +/// Loads a core from the given path into the global frontend state. +/// +/// ```rust +/// use retro_frontend::frontend; +/// frontend::load_core("./cores/gbasp.so"); +/// ``` +pub fn load_core>(path: P) -> Result<()> { + unsafe { FRONTEND_IMPL.load_core(path) } +} + +/// Unloads the core currently running in the global frontend state. +/// +/// ```rust +/// use retro_frontend::frontend; +/// frontend::unload_core(); +/// ``` +pub fn unload_core() -> Result<()> { + unsafe { FRONTEND_IMPL.unload_core() } +} + +/// Loads a ROM into the given core. This function requires that [load_core] has been called and has succeeded first. +/// +/// ```rust +/// use retro_frontend::frontend; +/// frontend::load_game("./roms/sma2.gba"); +/// ``` +pub fn load_game>(path: P) -> Result<()> { + unsafe { FRONTEND_IMPL.load_game(path) } +} + +/// Unloads a ROM from the given core. +pub fn unload_game() -> Result<()> { + unsafe { FRONTEND_IMPL.unload_game() } +} + +/// Gets the core's current AV information. +pub fn get_av_info() -> Result { + unsafe { FRONTEND_IMPL.get_av_info() } +} + +/// Gets the current framebuffer width and height as a tuple. +pub fn get_size() -> (u32, u32) { + unsafe { FRONTEND_IMPL.get_size() } +} + +/// Runs the currently loaded core for one video frame. +pub fn run_frame() { + unsafe { FRONTEND_IMPL.run() } +} diff --git a/crates/retro_frontend/src/frontend_impl.rs b/crates/retro_frontend/src/frontend_impl.rs new file mode 100644 index 0000000..a6e1c95 --- /dev/null +++ b/crates/retro_frontend/src/frontend_impl.rs @@ -0,0 +1,344 @@ +use crate::joypad::Joypad; +use crate::libretro_callbacks; +use crate::result::{Error, Result}; +use ffi::CString; +use libloading::Library; +use libretro_sys::*; +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; +use std::{fs, mem::MaybeUninit}; + +use std::cell::RefCell; +use std::rc::Rc; + +use std::ffi; + +use tracing::{error, info}; + +// FIXME(lily): Rust 2024 will make a good chunk of this code illegal. +// It might be wise to just bind some "simpler" C++ code and make it safe with lifetimes here, +// or something. It's a bit of a pickle. + +/// The frontend implementation. +/// +/// # Safety +/// Note that Libretro itself is not thread safe, so we do not try and pretend +/// that we are thread safe either. +pub(crate) static mut FRONTEND_IMPL: Lazy = + Lazy::new(|| FrontendStateImpl::new()); + +pub(crate) type VideoUpdateCallback = dyn FnMut(&[u32]); +pub(crate) type VideoResizeCallback = dyn FnMut(u32, u32); + +// TODO(lily): This should probably return the amount of consumed frames, +// as in some cases that *might* differ? +pub(crate) type AudioSampleCallback = dyn FnMut(&[i16], usize); + +pub(crate) type InputPollCallback = dyn FnMut(); + +pub(crate) struct FrontendStateImpl { + /// The current core's libretro functions. + pub(crate) core_api: Option, + + /// The current core library. + pub(crate) core_library: Option>, + + pub(crate) game_loaded: bool, + + pub(crate) av_info: Option, + + /// Core requested pixel format. + pub(crate) pixel_format: PixelFormat, + + // Converted pixel buffer. We store it here so we don't keep allocating over and over. + pub(crate) converted_pixel_buffer: Vec, + + pub(crate) fb_width: u32, + pub(crate) fb_height: u32, + pub(crate) fb_pitch: u32, + + pub(crate) system_directory: CString, + pub(crate) save_directory: CString, + + pub(crate) joypads: HashMap>>, + + // Callbacks that consumers can set + pub(crate) video_update_callback: Option>, + pub(crate) video_resize_callback: Option>, + pub(crate) audio_sample_callback: Option>, + pub(crate) input_poll_callback: Option>, +} + +impl FrontendStateImpl { + fn new() -> Self { + Self { + core_api: None, + core_library: None, + + game_loaded: false, + + av_info: None, + + pixel_format: PixelFormat::RGB565, + converted_pixel_buffer: Vec::new(), + + fb_width: 0, + fb_height: 0, + fb_pitch: 0, + + // TODO: We should let callers set these!! + system_directory: CString::new("system").unwrap(), + save_directory: CString::new("save").unwrap(), + + joypads: HashMap::new(), + + video_update_callback: None, + video_resize_callback: None, + audio_sample_callback: None, + input_poll_callback: None, + } + } + + pub(crate) fn core_loaded(&self) -> bool { + // Ideally this logic could be simplified but just to make sure.. + self.core_library.is_some() && self.core_api.is_some() + } + + pub(crate) fn set_video_update_callback(&mut self, cb: impl FnMut(&[u32]) + 'static) { + self.video_update_callback = Some(Box::new(cb)); + } + + pub(crate) fn set_video_resize_callback(&mut self, cb: impl FnMut(u32, u32) + 'static) { + self.video_resize_callback = Some(Box::new(cb)); + } + + pub(crate) fn set_audio_sample_callback(&mut self, cb: impl FnMut(&[i16], usize) + 'static) { + self.audio_sample_callback = Some(Box::new(cb)); + } + + pub(crate) fn set_input_poll_callback(&mut self, cb: impl FnMut() + 'static) { + self.input_poll_callback = Some(Box::new(cb)); + } + + pub(crate) fn set_input_port_device(&mut self, port: u32, device: Rc>) { + if self.core_loaded() { + let core_api = self.core_api.as_mut().unwrap(); + + unsafe { + (core_api.retro_set_controller_port_device)(port, device.borrow().device_type()); + } + + self.joypads.insert(port, device); + } + } + + // clear_input_port_device? + + pub(crate) fn load_core>(&mut self, path: P) -> Result<()> { + if self.core_loaded() { + return Err(Error::CoreAlreadyLoaded); + } + + unsafe { + let lib = Box::new(Library::new(path.as_ref())?); + + // bleh; CoreAPI doesn't implement Default so I can't do this in a "good" way + let mut api_uninitialized: MaybeUninit = MaybeUninit::zeroed(); + let api_ptr = api_uninitialized.as_mut_ptr(); + + // helper for DRY reasons + macro_rules! load_symbol { + ($name:ident) => { + (*api_ptr).$name = *(lib.get(stringify!($name).as_bytes())?); + }; + } + + load_symbol!(retro_set_environment); + load_symbol!(retro_set_video_refresh); + load_symbol!(retro_set_audio_sample); + load_symbol!(retro_set_audio_sample_batch); + load_symbol!(retro_set_input_poll); + load_symbol!(retro_set_input_state); + load_symbol!(retro_init); + load_symbol!(retro_deinit); + load_symbol!(retro_api_version); + load_symbol!(retro_get_system_info); + load_symbol!(retro_get_system_av_info); + load_symbol!(retro_set_controller_port_device); + load_symbol!(retro_reset); + load_symbol!(retro_run); + load_symbol!(retro_serialize_size); + load_symbol!(retro_serialize); + load_symbol!(retro_unserialize); + load_symbol!(retro_cheat_reset); + load_symbol!(retro_cheat_set); + load_symbol!(retro_load_game); + load_symbol!(retro_load_game_special); + load_symbol!(retro_unload_game); + load_symbol!(retro_get_region); + load_symbol!(retro_get_memory_data); + load_symbol!(retro_get_memory_size); + + // If we get here, then we have initalized all the core API without failing. + // We can now get an initalized CoreAPI. + let core_api = api_uninitialized.assume_init(); + + // Let's sanity check the libretro API version against bindings to make sure we can actually use this core. + // If we can't then fail the load. + let api_version = (core_api.retro_api_version)(); + if api_version != libretro_sys::API_VERSION { + error!( + "Core {} has invalid API version {api_version}; refusing to continue loading", + path.as_ref().display() + ); + return Err(Error::InvalidLibRetroAPI { + expected: libretro_sys::API_VERSION, + got: api_version, + }); + } + + // Set required libretro callbacks before calling libretro_init. + // Some cores expect some callbacks to be set before libretro_init is called, + // some cores don't. For maximum compatibility, pamper the cores which do. + (core_api.retro_set_environment)(libretro_callbacks::environment_callback); + + // Initalize the libretro core. We do this first because + // there are a Few cores which initalize resources that later + // are poked by the later callback setting that could break if we don't. + (core_api.retro_init)(); + + // Set more libretro callbacks now that we have initalized the core. + (core_api.retro_set_video_refresh)(libretro_callbacks::video_refresh_callback); + (core_api.retro_set_input_poll)(libretro_callbacks::input_poll_callback); + (core_api.retro_set_input_state)(libretro_callbacks::input_state_callback); + (core_api.retro_set_audio_sample_batch)( + libretro_callbacks::audio_sample_batch_callback, + ); + + info!("Core {} loaded", path.as_ref().display()); + + // Get AV info + // Like core API, we have to MaybeUninit again. + let mut av_info: MaybeUninit = MaybeUninit::uninit(); + (core_api.retro_get_system_av_info)(av_info.as_mut_ptr()); + + self.av_info = Some(av_info.assume_init()); + + self.core_library = Some(lib); + self.core_api = Some(core_api); + } + + Ok(()) + } + + pub(crate) fn unload_core(&mut self) -> Result<()> { + if !self.core_loaded() { + return Err(Error::CoreNotLoaded); + } + + if self.game_loaded { + self.unload_game()?; + } + + // First deinitalize the libretro core before unloading the library. + if let Some(core_api) = &self.core_api { + unsafe { + (core_api.retro_deinit)(); + } + } + + // Unload the library. We don't worry about error handling right now, but + // we could. + let lib = self.core_library.take().unwrap(); + lib.close()?; + + self.core_api = None; + self.core_library = None; + + // FIXME: Do other various cleanup (when we need to do said cleanup) + self.av_info = None; + + self.fb_width = 0; + self.fb_height = 0; + self.fb_pitch = 0; + + // disconnect all currently connected joypads + self.joypads.clear(); + + Ok(()) + } + + pub(crate) fn load_game>(&mut self, path: P) -> Result<()> { + if !self.core_loaded() { + return Err(Error::CoreNotLoaded); + } + + // For now I'm only implementing the gameinfo garbage that + // makes you read the whole file in. Later on I'll look into VFS + // support; but for now, it seems more cores will probably + // play ball with this.. which sucks :( + + // I'm aware this is nasty but bleh + let slice = path.as_ref().as_os_str().as_bytes(); + let path_string = CString::new(slice).expect("shouldn't fail"); + let contents = fs::read(path)?; + + let gameinfo = GameInfo { + path: path_string.as_ptr(), + data: contents.as_ptr() as *const ffi::c_void, + size: contents.len(), + meta: std::ptr::null(), + }; + + let core_api = self.core_api.as_ref().unwrap(); + + unsafe { + if !(core_api.retro_load_game)(&gameinfo) { + return Err(Error::RomLoadFailed); + } + + self.game_loaded = true; + Ok(()) + } + } + + pub(crate) fn unload_game(&mut self) -> Result<()> { + if !self.core_loaded() { + return Err(Error::CoreNotLoaded); + } + + let core_api = self.core_api.as_ref().unwrap(); + + if self.game_loaded { + unsafe { + (core_api.retro_unload_game)(); + } + + self.game_loaded = false; + } + + Ok(()) + } + + pub(crate) fn get_av_info(&mut self) -> Result { + if !self.core_loaded() { + return Err(Error::CoreNotLoaded); + } + + Ok(self.av_info.as_ref().unwrap().clone()) + } + + pub(crate) fn get_size(&mut self) -> (u32, u32) { + (self.fb_width, self.fb_height) + } + + pub(crate) fn run(&mut self) { + let core_api = self.core_api.as_ref().unwrap(); + + unsafe { + (core_api.retro_run)(); + } + } +} diff --git a/crates/retro_frontend/src/joypad.rs b/crates/retro_frontend/src/joypad.rs new file mode 100644 index 0000000..d80d340 --- /dev/null +++ b/crates/retro_frontend/src/joypad.rs @@ -0,0 +1,67 @@ +//! libretro pad abstraction + +use crate::libretro_sys_new; + +pub trait Joypad { + // TODO: is_pressed(id: u32)? + + fn device_type(&self) -> u32; + + fn get_button(&self, id: u32) -> i16; + + fn reset(&mut self); + + fn press_button(&mut self, id: u32, pressure: Option); +} + +// TODO: Split this into a new module, and make this a dir based one +// (mod.rs/retropad.rs/analogpad.rs) once we have AnalogPad support. + +/// Implementation of the [Joypad] trait for the Libretro +/// RetroPad; which is essentially a standard PS1 controller, +/// with a couple more buttons inherited from the Dual Analog/DualShock. +pub struct RetroPad { + buttons: [i16; 16], +} + +impl RetroPad { + pub fn new() -> Self { + Self { buttons: [0; 16] } + } +} + +impl Joypad for RetroPad { + fn device_type(&self) -> u32 { + libretro_sys_new::DEVICE_JOYPAD + } + + fn get_button(&self, id: u32) -> i16 { + if id > 16 { + return 0; + } + + self.buttons[id as usize] + } + + fn reset(&mut self) { + for button in &mut self.buttons { + *button = 0i16; + } + } + + fn press_button(&mut self, id: u32, pressure: Option) { + if id > 16 { + return; + } + + match pressure { + Some(pressure_value) => { + self.buttons[id as usize] = pressure_value; + } + None => { + // ? or 0x7fff ? Unsure + self.buttons[id as usize] = 1; + } + } + } +} diff --git a/crates/retro_frontend/src/lib.rs b/crates/retro_frontend/src/lib.rs new file mode 100644 index 0000000..01381c9 --- /dev/null +++ b/crates/retro_frontend/src/lib.rs @@ -0,0 +1,17 @@ +//! A libretro frontend as a reusable library crate. + +mod frontend_impl; +mod libretro_callbacks; +mod libretro_log; + +pub mod libretro_sys_new; + +pub mod core; + +pub mod joypad; + +//#[macro_use] +pub mod util; + +pub mod frontend; +pub mod result; diff --git a/crates/retro_frontend/src/libretro_callbacks.rs b/crates/retro_frontend/src/libretro_callbacks.rs new file mode 100644 index 0000000..80aef0e --- /dev/null +++ b/crates/retro_frontend/src/libretro_callbacks.rs @@ -0,0 +1,272 @@ +use crate::libretro_sys_new::*; +use crate::{frontend_impl::*, libretro_log, util}; + +use rgb565::Rgb565; + +use std::ffi; + +use tracing::{debug, error, info}; + +pub(crate) unsafe extern "C" fn environment_callback( + environment_command: u32, + data: *mut ffi::c_void, +) -> bool { + match environment_command { + ENVIRONMENT_GET_LOG_INTERFACE => { + *(data as *mut LogCallback) = libretro_log::LOG_INTERFACE.clone(); + return true; + } + + ENVIRONMENT_SET_PERFORMANCE_LEVEL => { + let level = *(data as *const ffi::c_uint); + debug!("Core is performance level {level}"); + return true; + } + + ENVIRONMENT_SET_CONTROLLER_INFO => { + let ptr = data as *const ControllerInfo; + + let slice = util::terminated_array(ptr, |item| { + return item.num_types == 0 && item.types.is_null(); + }); + + for desc in slice { + debug!("{:?}", desc); + + for i in 0..desc.num_types as usize { + let p = desc.types.add(i).as_ref().unwrap(); + debug!( + "type {i} = {:?} (name is {})", + p, + std::ffi::CStr::from_ptr(p.desc).to_str().unwrap() + ); + } + } + + return true; + } + + ENVIRONMENT_SET_INPUT_DESCRIPTORS => { + let ptr = data as *const InputDescriptor; + + let slice = util::terminated_array(ptr, |item| { + return item.description.is_null(); + }); + + debug!("{} input descriptor entries", slice.len()); + + for desc in slice { + debug!("Descriptor {:?}", desc); + } + + return true; + } + + ENVIRONMENT_GET_CAN_DUPE => { + *(data as *mut bool) = true; + return true; + } + + ENVIRONMENT_GET_SYSTEM_DIRECTORY => { + *(data as *mut *const ffi::c_char) = FRONTEND_IMPL.system_directory.as_ptr(); + return true; + } + + ENVIRONMENT_GET_SAVE_DIRECTORY => { + *(data as *mut *const ffi::c_char) = FRONTEND_IMPL.save_directory.as_ptr(); + return true; + } + + ENVIRONMENT_SET_PIXEL_FORMAT => { + let _pixel_format = *(data as *const ffi::c_uint); + let pixel_format = PixelFormat::from_uint(_pixel_format).unwrap(); + FRONTEND_IMPL.pixel_format = pixel_format; + return true; + } + + ENVIRONMENT_SET_GEOMETRY => { + if data.is_null() { + return false; + } + + let geometry = (data as *const GameGeometry).as_ref().unwrap(); + + FRONTEND_IMPL.fb_width = geometry.base_width; + FRONTEND_IMPL.fb_height = geometry.base_height; + + if let Some(resize_callback) = &mut FRONTEND_IMPL.video_resize_callback { + resize_callback(geometry.base_width, geometry.base_height); + } + return true; + } + + ENVIRONMENT_GET_VARIABLE => { + // Make sure the core actually is giving us a pointer to a *Variable + // so we can (if we have it!) fill it in. + if data.is_null() { + return false; + } + + let var = (data as *mut Variable).as_mut().unwrap(); + + match ffi::CStr::from_ptr(var.key).to_str() { + Ok(_key) => { + debug!("Core wants to get variable \"{_key}\"",); + return false; + } + Err(err) => { + error!( + "Core gave an invalid key for ENVIRONMENT_GET_VARIABLE: {:?}", + err + ); + return false; + } + } + } + + ENVIRONMENT_GET_VARIABLE_UPDATE => { + // We currently pressent no changed variables to the core. + // TODO: this will change + *(data as *mut bool) = false; + return true; + } + + // TODO: Fully implement, we'll need to implement above more fully. + // Ideas: + // - FrontendStateImpl can have a HashMap which will then + // be where we can store stuff. Also the consumer application could in theory + // use that to save/restore (by injecting keys from another source) + ENVIRONMENT_SET_VARIABLES => { + let ptr = data as *const Variable; + + let _slice = util::terminated_array(ptr, |item| item.key.is_null()); + + /* + + for var in slice { + let key = std::ffi::CStr::from_ptr(var.key).to_str().unwrap(); + let value = std::ffi::CStr::from_ptr(var.value).to_str().unwrap(); + }*/ + + return true; + } + + _ => { + debug!("Environment callback called with currently unhandled command: {environment_command}"); + return false; + } + } +} + +pub(crate) unsafe extern "C" fn video_refresh_callback( + pixels: *const ffi::c_void, + width: ffi::c_uint, + height: ffi::c_uint, + pitch: usize, +) { + // I guess this must be how duplicated frames are signaled. + // one word: Bleh + if pixels.is_null() { + return; + } + + //info!("Video refresh called, {width}, {height}, {pitch}"); + + // bleh + FRONTEND_IMPL.fb_width = width; + FRONTEND_IMPL.fb_height = height; + FRONTEND_IMPL.fb_pitch = + pitch as u32 / util::bytes_per_pixel_from_libretro(FRONTEND_IMPL.pixel_format); + + let pitch = FRONTEND_IMPL.fb_pitch as usize; + + match FRONTEND_IMPL.pixel_format { + PixelFormat::RGB565 => { + let pixel_data_slice = std::slice::from_raw_parts( + pixels as *const u16, + (pitch * height as usize) as usize, + ); + + // Resize the pixel buffer if we need to + if (pitch * height as usize) as usize != FRONTEND_IMPL.converted_pixel_buffer.len() { + info!("Resizing RGB565 -> RGBA buffer"); + FRONTEND_IMPL + .converted_pixel_buffer + .resize((pitch * height as usize) as usize, 0); + + // some cores are stupid + if let Some(resize_callback) = &mut FRONTEND_IMPL.video_resize_callback { + resize_callback(pitch as u32, height); + } + } + + // TODO: Make this convert from weird pitches to native resolution where possible. + for x in 0..pitch as usize { + for y in 0..height as usize { + let rgb = Rgb565::from_rgb565(pixel_data_slice[y * pitch as usize + x]); + let comp = rgb.to_rgb888_components(); + + // Finally save the pixel data in the result array as an XRGB8888 value + FRONTEND_IMPL.converted_pixel_buffer[y * pitch as usize + x] = + ((comp[0] as u32) << 16) | ((comp[1] as u32) << 8) | (comp[2] as u32); + } + } + + if let Some(update_callback) = &mut FRONTEND_IMPL.video_update_callback { + update_callback(&FRONTEND_IMPL.converted_pixel_buffer[..]); + } + } + _ => { + let pixel_data_slice = std::slice::from_raw_parts( + pixels as *const u32, + (pitch * height as usize) as usize, + ); + + if let Some(update_callback) = &mut FRONTEND_IMPL.video_update_callback { + update_callback(&pixel_data_slice); + } + } + } +} + +pub(crate) unsafe extern "C" fn input_poll_callback() { + if let Some(poll) = &mut FRONTEND_IMPL.input_poll_callback { + poll(); + } +} + +pub(crate) unsafe extern "C" fn input_state_callback( + port: ffi::c_uint, + device: ffi::c_uint, + _index: ffi::c_uint, // not used? + button_id: ffi::c_uint, +) -> ffi::c_short { + if FRONTEND_IMPL.joypads.contains_key(&port) { + let joypad = FRONTEND_IMPL + .joypads + .get(&port) + .expect("How do we get here when contains_key() returns true but the key doen't exist") + .borrow(); + + if device == joypad.device_type() { + return joypad.get_button(button_id); + } + } + + 0 +} + +pub(crate) unsafe extern "C" fn audio_sample_batch_callback( + // Is actually a [[l, r]] interleaved pair. + samples: *const i16, + frames: usize, +) -> usize { + if let Some(callback) = &mut FRONTEND_IMPL.audio_sample_callback { + let slice = std::slice::from_raw_parts(samples, frames * 2); + + // I might not need to give the callback the amount of frames since it can figure it out as + // slice.len() / 2, but /shrug + callback(slice, frames); + } + frames +} diff --git a/crates/retro_frontend/src/libretro_log.rs b/crates/retro_frontend/src/libretro_log.rs new file mode 100644 index 0000000..60330bb --- /dev/null +++ b/crates/retro_frontend/src/libretro_log.rs @@ -0,0 +1,51 @@ +use crate::libretro_sys_new::*; +use std::ffi; +use tracing::*; + +#[allow(dead_code)] // This *is* used; just not in Rust code +#[no_mangle] +/// This recieves log messages from our C++ helper code, and pulls them out into Tracing messages. +pub extern "C" fn libretro_log_recieve(level: LogLevel, buf: *const ffi::c_char) { + let mut msg: Option<&str> = None; + + // Safety: This pointer is never null, and always comes from the stack; + // we really only should get UTF-8 errors here in the case a core spits out something invalid. + unsafe { + match ffi::CStr::from_ptr(buf).to_str() { + Ok(message) => msg = Some(message), + Err(err) => { + error!( + "Core for some reason gave a broken string to log interface: {:?}", + err + ); + } + } + } + + if let Some(message) = &msg { + match level { + LogLevel::Debug => { + debug!("Core log: {}", message) + } + LogLevel::Info => { + info!("Core log: {}", message) + } + LogLevel::Warn => { + warn!("Core log: {}", message) + } + LogLevel::Error => { + error!("Core log: {}", message) + } + } + } +} + +extern "C" { + // We intententionally do not declare the ... varadic arm here, + // because libretro_sys doesn't want it, and additionally, + // that requires nightly Rust to even do, which defeats the purpose + // of moving it into a helper. + fn libretro_log(level: LogLevel, fmt: *const ffi::c_char); +} + +pub static LOG_INTERFACE: LogCallback = LogCallback { log: libretro_log }; diff --git a/crates/retro_frontend/src/libretro_log_helper.cpp b/crates/retro_frontend/src/libretro_log_helper.cpp new file mode 100644 index 0000000..54e6ef7 --- /dev/null +++ b/crates/retro_frontend/src/libretro_log_helper.cpp @@ -0,0 +1,35 @@ +#include +#include +#include + +using LibRetroLogLevel = std::uint32_t; + +extern "C" { + + /// This function is defined in Rust and recieves our formatted log messages. + void libretro_log_recieve(LibRetroLogLevel level, const char* buf); + + /// This helper function is given to Rust code to implement the libretro logging + /// (because it's a C-varadic function; that requires nightly/unstable Rust) + /// + /// By implementing it in C++, we can dodge all that and keep using stable rustc. + void libretro_log(LibRetroLogLevel level, const char* format, ...) { + char buf[512]{}; + va_list val; + + va_start(val, format); + auto n = std::vsnprintf(&buf[0], sizeof(buf)-1, format, val); + va_end(val); + + // Failed to format for some reason, just give up. + if(n == -1) + return; + + // Remove the last newline and replace it with a null terminator. + if(buf[n-1] == '\n') + buf[n-1] = '\0'; + + // Call the Rust-side reciever. + return libretro_log_recieve(level, &buf[0]); + } +} diff --git a/crates/retro_frontend/src/libretro_sys_new.rs b/crates/retro_frontend/src/libretro_sys_new.rs new file mode 100644 index 0000000..71137d4 --- /dev/null +++ b/crates/retro_frontend/src/libretro_sys_new.rs @@ -0,0 +1,38 @@ +//! Selective additional (2019+) updates on top of the existing libretro_sys crate. + +pub use libretro_sys::*; +use std::ffi; + +/// Defines overrides which modify frontend handling of specific content file types. +/// An array of [SystemContentInfoOverride] is passed to [RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE] +#[repr(C)] +pub struct SystemContentInfoOverride { + pub extensions: *const ffi::c_char, + pub need_fullpath: bool, + pub persistent_data: bool, +} + +#[repr(C)] +pub struct GameInfoExt { + pub full_path: *const ffi::c_char, + pub archive_path: *const ffi::c_char, + pub archive_file: *const ffi::c_char, + pub dir: *const ffi::c_char, + pub name: *const ffi::c_char, + pub ext: *const ffi::c_char, + pub meta: *const ffi::c_char, + + pub data: *const ffi::c_void, + pub size: usize, + + /// True if loaded content file is inside a compressed archive + pub file_in_archive: bool, + + pub persistent_data: bool, +} + +/// *const [SystemContentInfoOverride] (array, NULL extensions terminates it) +pub const RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE: ffi::c_uint = 65; + +/// *const *const [GameInfoExt] +pub const RETRO_ENVIRONMENT_GET_GAME_INFO_EXT: ffi::c_uint = 66; diff --git a/crates/retro_frontend/src/result.rs b/crates/retro_frontend/src/result.rs new file mode 100644 index 0000000..6961628 --- /dev/null +++ b/crates/retro_frontend/src/result.rs @@ -0,0 +1,24 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("error while loading core library")] + LibError(#[from] libloading::Error), + + #[error(transparent)] + IoError(#[from] std::io::Error), + + #[error("expected core API version {expected}, but core returned {got}")] + InvalidLibRetroAPI { expected: u32, got: u32 }, + + #[error("no core is currently loaded into the frontend")] + CoreNotLoaded, + + #[error("a core is already loaded into the frontend")] + CoreAlreadyLoaded, + + #[error("ROM load failed")] + RomLoadFailed, +} + +pub type Result = std::result::Result; diff --git a/crates/retro_frontend/src/util.rs b/crates/retro_frontend/src/util.rs new file mode 100644 index 0000000..7e51009 --- /dev/null +++ b/crates/retro_frontend/src/util.rs @@ -0,0 +1,53 @@ +use crate::libretro_sys_new::*; + +pub fn bytes_per_pixel_from_libretro(pf: PixelFormat) -> u32 { + match pf { + PixelFormat::ARGB1555 | PixelFormat::RGB565 => 2, + PixelFormat::ARGB8888 => 4, + } +} + + +/// Boilerplate code for dealing with NULL/otherwise terminated arrays, +/// which converts them into a Rust slice. +/// +/// We rely on a user-provided callback currently to determine when iteration is complete. +/// This *could* be replaced with a object-safe trait (and a constraint to allow us to use said trait) to codify +/// the expected "end conditions" of a terminated array of a given type, but for now, the callback works. +pub fn terminated_array<'a, T>(ptr: *const T, end_fn: impl Fn(&T) -> bool) -> &'a [T] { + // Make sure the array pointer itself isn't null. Strictly speaking, this check should be done + // *before* this is called by the user, but to avoid anything going haywire + // we additionally check here. + assert!(!ptr.is_null(), "pointer to array given to terminated_array! cannot be null"); + + unsafe { + let mut iter = ptr.clone(); + let mut len: usize = 0; + + loop { + let item = iter.as_ref().unwrap(); + + if end_fn(item) { + break; + } + + len += 1; + iter = iter.add(1); + } + + std::slice::from_raw_parts(ptr, len) + } +} + +/* +#[doc(hidden)] +#[macro_export] +macro_rules! __terminated_array { + ($pex:ident, $lex:expr $(,)?) => { + $crate::util::__terminated_array_impl($pex, $lex) + }; +} + +#[doc(inline)] +pub use __terminated_array as terminated_array; +*/ diff --git a/crates/retrovnc/Cargo.toml b/crates/retrovnc/Cargo.toml new file mode 100644 index 0000000..02a7f9b --- /dev/null +++ b/crates/retrovnc/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "retrovnc" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +clap = { version = "4.5.6", features = ["cargo"] } +libvnc-sys = "0.1.4" +retro_frontend = { path = "../retro_frontend" } +tracing = "0.1.40" +tracing-subscriber = "0.3.18" diff --git a/crates/retrovnc/src/main.rs b/crates/retrovnc/src/main.rs new file mode 100644 index 0000000..5194e20 --- /dev/null +++ b/crates/retrovnc/src/main.rs @@ -0,0 +1,155 @@ +use std::{ + cell::RefCell, + rc::Rc, +}; + +use retro_frontend::{ + core::Core, + frontend, + joypad::{Joypad, RetroPad} +}; +use tracing::Level; +use tracing_subscriber::FmtSubscriber; + +use clap::{arg, command}; + +mod rfb; +use rfb::*; + +struct App { + rfb_server: Box, + pad: Rc>, +} + +impl App { + fn new() -> Self { + Self { + rfb_server: RfbServer::new(RfbServerConfig { + width: 640, + height: 480, + }), + // nasty, but idk a better way + pad: Rc::new(RefCell::new(RetroPad::new())), + } + } + + fn new_and_init() -> Rc> { + let app = App::new(); + let rc = Rc::new(RefCell::new(app)); + + // Initalize all the frontend callbacks and stuff. + App::init(&rc); + + rc + } + + /// Initalizes the frontend library with callbacks back to us, + /// and performs an initial window resize. + fn init(rc: &Rc>) { + let app_clone = rc.clone(); + frontend::set_video_update_callback(move |slice| { + app_clone.borrow_mut().frame_update(slice); + }); + + let app_resize_clone = rc.clone(); + frontend::set_video_resize_callback(move |width, height| { + app_resize_clone.borrow_mut().resize(width, height); + }); + + frontend::set_audio_sample_callback(|_slice, _frames| { + //println!("Got audio sample batch with {_frames} frames"); + }); + + let app_input_poll_clone = rc.clone(); + frontend::set_input_poll_callback(move || { + app_input_poll_clone.borrow_mut().input_poll(); + }); + + // Currently retrodemo just hardcodes the assumption of a single RetroPad. + frontend::set_input_port_device(0, rc.borrow().pad.clone()); + + let av_info = frontend::get_av_info().expect("No AV info"); + + // Start VNC server. + { + let server = &mut rc.borrow_mut().rfb_server; + tracing::info!("Starting VNC server"); + server.start(); + server.resize(av_info.geometry.base_width as u16, av_info.geometry.base_height as u16); + } + } + + /// Called by the frontend library when a video resize needs to occur. + fn resize(&mut self, width: u32, height: u32) { + //let width = width * 2; + //let height = height * 2; + + tracing::info!("Resized to {width}x{height}"); + self.rfb_server.resize(width as u16, height as u16); + } + + /// Called by the frontend library on video frame updates + /// The framebuffer is *always* a RGBX8888 slice regardless of whatever video mode + /// the core has setup internally; this is by design to make code less annoying + fn frame_update(&mut self, slice: &[u32]) { + let framebuffer_size = frontend::get_size(); + self.rfb_server + .update_buffer(&slice, framebuffer_size.0 as u16, framebuffer_size.1 as u16); + } + + /// Called by the frontend library during retro_run() to poll input + fn input_poll(&mut self) { + let mut pad = self.pad.borrow_mut(); + + pad.reset(); + + // Press all buttons the VNC server marked as pressed + let buttons = self.rfb_server.get_buttons(); + for i in 0..buttons.len() { + if buttons[i] { + pad.press_button(i as u32, None); + } + } + } + +} + +fn main() { + // Setup a tracing subscriber + let subscriber = FmtSubscriber::builder() + .with_max_level(Level::INFO) + .finish(); + + tracing::subscriber::set_global_default(subscriber).unwrap(); + + let matches = command!() + .arg(arg!(--core ).required(true)) + // Not that it matters, but this is only really required for cores that require + // content to be loaded; that's most cores, but libretro does support the difference. + .arg(arg!(--rom ).required(false)) + .get_matches(); + + let core_path: &String = matches.get_one("core").unwrap(); + + // Load the user's provided core + let mut core = Core::load(core_path).expect("Provided core failed to load"); + + // Initalize the app + let _app = App::new_and_init(); + + if let Some(rom_path) = matches.get_one::("rom") { + core.load_game(rom_path) + .expect("Provided ROM failed to load"); + } + + let av_info = frontend::get_av_info().expect("Should have AV info by this point."); + let step_ms = ((1.0 / av_info.timing.fps) * 1000.) as u64; + + // Do the main loop + loop { + frontend::run_frame(); + std::thread::sleep(std::time::Duration::from_millis( + step_ms, + )); + } +} diff --git a/crates/retrovnc/src/rfb.rs b/crates/retrovnc/src/rfb.rs new file mode 100644 index 0000000..f458353 --- /dev/null +++ b/crates/retrovnc/src/rfb.rs @@ -0,0 +1,239 @@ +//! Retrovnc-specific bindings to LibVNCServer. +use libvnc_sys::rfb::{self, bindings::_rfbScreenInfo}; +use retro_frontend::libretro_sys_new; +use std::ptr::{addr_of, NonNull}; + +pub struct RfbServerConfig { + pub width: u16, + pub height: u16, + // TODO: Listen address + // TODO: listen port +} + +pub struct RfbServer { + ptr: NonNull<_rfbScreenInfo>, + framebuffer: Vec, + + // This follows the same format as libretro joypad buttons. + buttons: [bool; 32], +} + +impl RfbServer { + pub fn new(config: RfbServerConfig) -> Box { + unsafe { + // Feed a fake argv in (TODO: Make this better.) + let argc = 3; + let argv: [*const std::ffi::c_char; 3] = [ + b"RfbServer".as_ptr() as *const i8, + b"-listen".as_ptr() as *const i8, + b"127.0.0.1".as_ptr() as *const i8, + ]; + + let screen = rfb::bindings::rfbGetScreen( + addr_of!(argc) as *mut i32, + addr_of!(argv) as *mut *mut i8, + config.width as i32, + config.height as i32, + 8, + 3, + 4, + ); + + // result + if screen.is_null() { + panic!("rfbGetScreen() failed"); + } + + let mut ret = Box::new(Self { + // Safety: See the above check + ptr: NonNull::new_unchecked(screen), + framebuffer: Vec::new(), + buttons: [false; 32], + }); + + // Disable the normal libvnc cursor + (*screen).cursor = std::ptr::null_mut(); + + (*screen).screenData = ((&mut *ret) as *mut RfbServer) as *mut std::ffi::c_void; + (*screen).alwaysShared = 1; + + (*screen).newClientHook = Some(Self::connection_callback); + (*screen).kbdAddEvent = Some(Self::on_key_callback); + + // testing + (*screen).port = 6930; + (*screen).ipv6port = 0; + + ret.resize(config.width, config.height); + + ret + } + } + + pub fn resize(&mut self, w: u16, h: u16) { + let len = (w as usize) * (h as usize); + + //self.framebuffer = Vec::with_capacity(len); + self.framebuffer.resize(len, 0); + + unsafe { + rfb::bindings::rfbNewFramebuffer( + self.ptr.as_ptr(), + self.framebuffer.as_mut_ptr() as *mut i8, + w as i32, + h as i32, + 8, + 3, + 4, + ); + } + } + + pub fn update_buffer(&mut self, slice: &[u32], width: u16, height: u16) { + // lame slow loop (dont use this sucks) + /* + for x in 0..width { + for y in 0..height { + self.framebuffer[(y * width + x) as usize] = slice[(y * width + x) as usize]; + } + }*/ + + self.framebuffer.copy_from_slice(&slice); + + unsafe { + rfb::bindings::rfbMarkRectAsModified( + self.ptr.as_ptr(), + 0, + 0, + width as i32, + height as i32, + ); + } + } + + pub fn start(&mut self) { + unsafe { + rfb::bindings::rfbInitServerWithPthreadsAndZRLE(self.ptr.as_ptr()); + + // Use the threaded event loop (it's a lot faster) + rfb::bindings::rfbRunEventLoop(self.ptr.as_ptr(), -1, 1); + } + } + + + pub fn get_buttons(&self) -> [bool; 32] { + self.buttons + } + + pub fn on_key( + &mut self, + down: rfb::bindings::rfbBool, + keysym: rfb::bindings::rfbKeySym, + _client: rfb::bindings::rfbClientPtr, + ) { + use rfb::bindings::*; + + //println!( + // "on_key: keysym {} (0x{:02x}), pressed: {}", + // keysym, + // keysym, + // down == 1 + //); + + // This is because bindgen or something. + #[allow(non_upper_case_globals)] + match keysym { + // START | SELECT + XK_backslash => { + self.buttons[libretro_sys_new::DEVICE_ID_JOYPAD_SELECT as usize] = down == 1; + } + + XK_Return => { + self.buttons[libretro_sys_new::DEVICE_ID_JOYPAD_START as usize] = down == 1; + } + + XK_Up => { + self.buttons[libretro_sys_new::DEVICE_ID_JOYPAD_UP as usize] = down == 1; + } + XK_Down => { + self.buttons[libretro_sys_new::DEVICE_ID_JOYPAD_DOWN as usize] = down == 1; + } + XK_Left => { + self.buttons[libretro_sys_new::DEVICE_ID_JOYPAD_LEFT as usize] = down == 1; + } + XK_Right => { + self.buttons[libretro_sys_new::DEVICE_ID_JOYPAD_RIGHT as usize] = down == 1; + } + + // Face buttons + XK_s | XK_S => { + // CROSS + self.buttons[libretro_sys_new::DEVICE_ID_JOYPAD_B as usize] = down == 1; + } + + XK_a | XK_A => { + // CIRCLE + self.buttons[libretro_sys_new::DEVICE_ID_JOYPAD_A as usize] = down == 1; + } + + XK_q | XK_Q => { + // TRIANGLE + self.buttons[libretro_sys_new::DEVICE_ID_JOYPAD_X as usize] = down == 1; + } + + XK_w | XK_W => { + //SQUARE + self.buttons[libretro_sys_new::DEVICE_ID_JOYPAD_Y as usize] = down == 1; + } + + // L buttons + XK_Control_L => { + // L1 + self.buttons[libretro_sys_new::DEVICE_ID_JOYPAD_L as usize] = down == 1; + } + + XK_Shift_L => { + // L2 + self.buttons[libretro_sys_new::DEVICE_ID_JOYPAD_L2 as usize] = down == 1; + } + + XK_Alt_L => { + // R1 + self.buttons[libretro_sys_new::DEVICE_ID_JOYPAD_R as usize] = down == 1; + } + + XK_z | XK_Z => { + // R2 + self.buttons[libretro_sys_new::DEVICE_ID_JOYPAD_R2 as usize] = down == 1; + } + + _ => {} + } + } + + unsafe extern "C" fn connection_callback( + ptr: rfb::bindings::rfbClientPtr, + ) -> rfb::bindings::rfbNewClientAction { + let server = (*(*ptr).screen).screenData as *mut RfbServer; + if server.is_null() { + return rfb::bindings::rfbNewClientAction_RFB_CLIENT_REFUSE; + } + + return rfb::bindings::rfbNewClientAction_RFB_CLIENT_ACCEPT; + } + + unsafe extern "C" fn on_key_callback( + down: rfb::bindings::rfbBool, + keysym: rfb::bindings::rfbKeySym, + client: rfb::bindings::rfbClientPtr, + ) { + unsafe { + let server = (*(*client).screen).screenData as *mut RfbServer; + if server.is_null() { + return; + } + + (*server).on_key(down, keysym, client); + } + } +}