Skip to content

Commit 0b54313

Browse files
committed
feat: 增加 chu2to3 共享内存生产端
1 parent 8a84f58 commit 0b54313

9 files changed

Lines changed: 230 additions & 7 deletions

File tree

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
target/
22
Cargo.lock
3-
.idea/
4-
.vs/
3+
54
*.user
5+
.codegraph
6+
.sisyphus

.idea/.gitignore

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/AppleChu.iml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/modules.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/vcs.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/chuniio/chu2to3.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
//! chu2to3 shared-memory producer (x86 side).
2+
//!
3+
//! Faithful port of segatools' chu2to3 engine, x86 half. When a single 32-bit
4+
//! chuniio DLL is loaded via the `[ChuniIo] path` setting, the 64-bit
5+
//! `chusanhook_x64` injected into `amdaemon.exe` runs the chu2to3 engine in its
6+
//! x64 half, which only does `OpenFileMapping("Local\\Chu2to3Shmem")` and reads
7+
//! JVS state from there. This module creates that shared memory and continuously
8+
//! writes opbtn/beams/coin/version into it, exactly like segatools'
9+
//! `jvs_poll_thread_proc`, so the amdaemon side comes alive.
10+
//!
11+
//! Layout reference (segatools games/chuniio/chu2to3.c):
12+
//! ```c
13+
//! #pragma pack(1)
14+
//! typedef struct shared_data_s {
15+
//! uint16_t coin_counter;
16+
//! uint8_t opbtn;
17+
//! uint8_t beams;
18+
//! uint16_t version;
19+
//! } shared_data_t;
20+
//! TCHAR g_shmem_name[] = TEXT("Local\\Chu2to3Shmem");
21+
//! #define BUF_SIZE 1024
22+
//! ```
23+
24+
use std::sync::atomic::{AtomicBool, Ordering};
25+
use std::thread;
26+
use std::time::Duration;
27+
28+
use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE};
29+
use windows_sys::Win32::System::Memory::{
30+
CreateFileMappingW, MapViewOfFile, FILE_MAP_ALL_ACCESS, PAGE_READWRITE,
31+
};
32+
33+
use crate::util::api::Api;
34+
35+
use super::external::JvsRawFns;
36+
37+
const SHMEM_NAME: &[u16] = &[
38+
b'L' as u16, b'o' as u16, b'c' as u16, b'a' as u16, b'l' as u16, b'\\' as u16, b'C' as u16,
39+
b'h' as u16, b'u' as u16, b'2' as u16, b't' as u16, b'o' as u16, b'3' as u16, b'S' as u16,
40+
b'h' as u16, b'm' as u16, b'e' as u16, b'm' as u16, 0,
41+
];
42+
const BUF_SIZE: u32 = 1024;
43+
const SHARED_DATA_LEN: usize = 6;
44+
45+
static STARTED: AtomicBool = AtomicBool::new(false);
46+
47+
struct ShmemView(*mut u8);
48+
49+
unsafe impl Send for ShmemView {}
50+
51+
impl ShmemView {
52+
fn write(&self, coin: u16, opbtn: u8, beams: u8, version: u16) {
53+
let mut buf = [0u8; SHARED_DATA_LEN];
54+
buf[0..2].copy_from_slice(&coin.to_le_bytes());
55+
buf[2] = opbtn;
56+
buf[3] = beams;
57+
buf[4..6].copy_from_slice(&version.to_le_bytes());
58+
unsafe {
59+
std::ptr::copy_nonoverlapping(buf.as_ptr(), self.0, SHARED_DATA_LEN);
60+
}
61+
}
62+
}
63+
64+
/// Start the chu2to3 x86 producer for a single-DLL (`path`) external chuniio.
65+
///
66+
/// Creates `Local\Chu2to3Shmem`, writes the API version immediately (so the
67+
/// x64 side's version handshake doesn't time out), runs `jvs_init`, then spawns
68+
/// a 1 ms polling thread mirroring segatools' `jvs_poll_thread_proc`.
69+
pub fn start(api: &Api, fns: JvsRawFns) {
70+
if STARTED.swap(true, Ordering::SeqCst) {
71+
return;
72+
}
73+
74+
let mapping = unsafe {
75+
CreateFileMappingW(
76+
INVALID_HANDLE_VALUE,
77+
std::ptr::null(),
78+
PAGE_READWRITE,
79+
0,
80+
BUF_SIZE,
81+
SHMEM_NAME.as_ptr(),
82+
)
83+
};
84+
if mapping == 0 {
85+
api.log_warn("chu2to3: CreateFileMapping failed; amdaemon JVS bridge inactive");
86+
STARTED.store(false, Ordering::SeqCst);
87+
return;
88+
}
89+
90+
let mapped = unsafe { MapViewOfFile(mapping, FILE_MAP_ALL_ACCESS, 0, 0, 0) };
91+
if mapped.Value.is_null() {
92+
api.log_warn("chu2to3: MapViewOfFile failed; amdaemon JVS bridge inactive");
93+
unsafe { CloseHandle(mapping) };
94+
STARTED.store(false, Ordering::SeqCst);
95+
return;
96+
}
97+
let view = ShmemView(mapped.Value as *mut u8);
98+
99+
// Write the API version up front. The x64 side polls until version != 0 to
100+
// confirm the handshake, so writing it now avoids its 3x5s timeout.
101+
view.write(0, 0, 0, fns.api_version);
102+
103+
unsafe { fns.jvs_init() };
104+
105+
api.log_info(&format!(
106+
"chu2to3: shared memory bridge started (API {:#06x})",
107+
fns.api_version
108+
));
109+
110+
thread::spawn(move || {
111+
let _keep_mapping = mapping;
112+
loop {
113+
let coin = unsafe { fns.jvs_read_coin() };
114+
let mut opbtn = 0u8;
115+
let mut beams = 0u8;
116+
unsafe { fns.jvs_poll(&mut opbtn, &mut beams) };
117+
view.write(coin, opbtn, beams, fns.api_version);
118+
thread::sleep(Duration::from_millis(1));
119+
}
120+
});
121+
}

src/chuniio/external.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,36 @@ pub struct ExternalChuniIo {
3939
unsafe impl Send for ExternalChuniIo {}
4040
unsafe impl Sync for ExternalChuniIo {}
4141

42+
/// Raw JVS function pointers extracted from an external IO DLL, used by the
43+
/// chu2to3 shared-memory producer thread so it can poll the DLL directly
44+
/// without contending for the global EXTERNAL mutex.
45+
#[derive(Clone, Copy)]
46+
pub struct JvsRawFns {
47+
pub api_version: u16,
48+
jvs_init: JvsInitFn,
49+
jvs_poll: JvsPollFn,
50+
jvs_read_coin: JvsReadCoinFn,
51+
}
52+
53+
unsafe impl Send for JvsRawFns {}
54+
unsafe impl Sync for JvsRawFns {}
55+
56+
impl JvsRawFns {
57+
pub unsafe fn jvs_init(&self) -> i32 {
58+
(self.jvs_init)()
59+
}
60+
61+
pub unsafe fn jvs_poll(&self, opbtn: &mut u8, beams: &mut u8) {
62+
(self.jvs_poll)(opbtn, beams);
63+
}
64+
65+
pub unsafe fn jvs_read_coin(&self) -> u16 {
66+
let mut total = 0u16;
67+
(self.jvs_read_coin)(&mut total);
68+
total
69+
}
70+
}
71+
4272
unsafe fn resolve(module: HMODULE, name: &[u8]) -> Option<*const c_void> {
4373
GetProcAddress(module, name.as_ptr()).map(|proc| proc as *const c_void)
4474
}
@@ -180,6 +210,18 @@ impl ExternalChuniIo {
180210
(self.jvs_poll)(opbtn, beams);
181211
}
182212

213+
/// Snapshot the raw JVS function pointers for use by the chu2to3 producer
214+
/// thread. The thread polls the DLL directly, matching segatools' x86
215+
/// jvs_poll_thread_proc behaviour.
216+
pub fn jvs_raw_fns(&self) -> JvsRawFns {
217+
JvsRawFns {
218+
api_version: self.api_version,
219+
jvs_init: self.jvs_init,
220+
jvs_poll: self.jvs_poll,
221+
jvs_read_coin: self.jvs_read_coin,
222+
}
223+
}
224+
183225
pub unsafe fn jvs_read_coin(&self) -> u16 {
184226
self.ensure_jvs_init();
185227
let mut total = 0u16;

src/chuniio/mod.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod chu2to3;
12
pub mod config;
23
pub mod external;
34
mod led_output;
@@ -38,11 +39,20 @@ static BACKEND: Lazy<Mutex<Option<ChuniIoBackend>>> = Lazy::new(|| Mutex::new(No
3839

3940
pub fn init(api: &Api, config: &Config) {
4041
led_output::init(LedOutputConfig::load(config));
41-
let path = chuniio_path(config);
42+
let (path, single_dll) = chuniio_path(config);
4243
if !path.is_empty() {
4344
match unsafe { ExternalChuniIo::load(&path) } {
4445
Ok(external) => {
4546
let version = external.api_version;
47+
// Single-DLL `path` mode mirrors segatools' chu2to3 engine: the
48+
// 32-bit chusanApp side must publish JVS state into the
49+
// `Local\\Chu2to3Shmem` shared memory so the chusanhook_x64
50+
// injected into amdaemon can read it. Without this, amdaemon's
51+
// OpenFileMapping fails and no JVS input reaches the game.
52+
if single_dll {
53+
let fns = external.jvs_raw_fns();
54+
chu2to3::start(api, fns);
55+
}
4656
if let Ok(mut guard) = EXTERNAL.lock() {
4757
*guard = Some(external);
4858
}
@@ -250,16 +260,24 @@ pub fn led_set_colors(board: u8, rgb: &mut [u8]) {
250260
}
251261
}
252262

253-
fn chuniio_path(config: &Config) -> String {
263+
/// Resolve the external chuniio DLL path.
264+
///
265+
/// Returns `(path, single_dll)`. `single_dll` is true when the single-DLL
266+
/// `path` setting is used, which triggers the chu2to3 shared-memory bridge so
267+
/// the amdaemon-side chusanhook_x64 can consume JVS state. The split
268+
/// `path32`/`path64` form is the dual-DLL mode (each process loads its own
269+
/// matching DLL) and does not use chu2to3.
270+
fn chuniio_path(config: &Config) -> (String, bool) {
254271
let path = config.get_string_alias(&[("ChuniIo", "path"), ("chuniio", "path")], "");
255272
if !path.is_empty() {
256-
return path;
273+
return (path, true);
257274
}
258-
if cfg!(target_pointer_width = "64") {
275+
let split = if cfg!(target_pointer_width = "64") {
259276
config.get_string_alias(&[("ChuniIo", "path64"), ("chuniio", "path64")], "")
260277
} else {
261278
config.get_string_alias(&[("ChuniIo", "path32"), ("chuniio", "path32")], "")
262-
}
279+
};
280+
(split, false)
263281
}
264282

265283
#[cfg(windows)]

0 commit comments

Comments
 (0)