Allocate guest memory using platform specific calls

Creating Mem using the host allocator allows use to explicitly drop the
previous allocation and freely allocate a new instance without running
into crashes on Android. As such this commit removes the previous
refurbishing mechanism, in favor of the simpler drop -> reallocate.

Change-Id: Ic9fd3b790640bb0525c2ae425c26cc9fb922a843
This commit is contained in:
Alexander Cieslewicz
2025-09-11 13:53:35 +03:00
committed by acieslewicz
parent 261f0b7bf9
commit e8e8711290
8 changed files with 142 additions and 85 deletions

21
Cargo.lock generated
View File

@@ -385,9 +385,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.137"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "log"
@@ -860,6 +860,7 @@ dependencies = [
"encoding_rs",
"gl_generator",
"hound",
"libc",
"mach_object",
"md5",
"plist",
@@ -874,6 +875,7 @@ dependencies = [
"touchHLE_pvrt_decompress_wrapper",
"touchHLE_stb_image_wrapper",
"touchHLE_version",
"windows-sys",
"yore",
"zip",
]
@@ -985,6 +987,21 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-link"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-sys"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa"
dependencies = [
"windows-link",
]
[[package]]
name = "xml-rs"
version = "0.8.4"

View File

@@ -68,6 +68,8 @@ touchHLE_openal_soft_wrapper = { path = "src/audio/openal_soft_wrapper" }
touchHLE_pvrt_decompress_wrapper = { path = "src/image/pvrt_decompress_wrapper" }
touchHLE_stb_image_wrapper = { path = "src/image/stb_image_wrapper" }
touchHLE_version = { path = "src/version" }
windows-sys = { version = "0.61.0", features = ["Win32_System_Memory"] }
libc = "0.2.175"
[build-dependencies]
cargo-license = "0.5.1"

View File

@@ -49,10 +49,7 @@ struct AppInfo {
icon_ui_image: Option<id>,
}
pub fn app_picker(
options: Options,
option_args: &mut Vec<String>,
) -> Result<(PathBuf, Environment), String> {
pub fn app_picker(options: Options, option_args: &mut Vec<String>) -> Result<PathBuf, String> {
let apps_dir = paths::user_data_base_path().join(paths::APPS_DIR);
let apps: Result<Vec<AppInfo>, String> = if !apps_dir.is_dir() {
@@ -274,7 +271,7 @@ fn show_app_picker_gui(
options: Options,
option_args: &mut Vec<String>,
mut apps: Result<Vec<AppInfo>, String>,
) -> Result<(PathBuf, Environment), String> {
) -> Result<PathBuf, String> {
let icon = {
let bytes: &[u8] = match super::branding() {
"" => include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/res/icon.png")),
@@ -732,8 +729,7 @@ fn show_app_picker_gui(
option_args.push("--allow-network-access".to_string());
}
// Return the environment so some parts of it can be salvaged.
Ok((app_path, environment))
Ok(app_path)
}
const ICON_SIZE: CGSize = CGSize {

View File

@@ -234,33 +234,14 @@ fn generate_binary_load_order(graph: &[BinaryDependencyNode]) -> Result<Vec<usiz
impl Environment {
/// Loads the binary and sets up the emulator.
///
/// `env_for_salvage` can be used to provide an existing environment (in
/// practice, the app picker's, created with [Environment::new_without_app])
/// that is to be destroyed. Certain components may be salvaged from the
/// old environment, but their states will be reset, so the result should be
/// "like new". This option exists because touchHLE on Android would crash
/// when allocating a second [mem::Mem] instance.
pub fn new(
bundle: bundle::Bundle,
fs: fs::Fs,
mut options: options::Options,
app_args: Vec<String>,
env_for_salvage: Option<Environment>,
) -> Result<Environment, String> {
let startup_time = Instant::now();
// Extract things to salvage from the old environment, and then drop it.
// This needs to be done before creating a new window, because SDL2 only
// allows one window at once.
let mem_for_salvage = if let Some(env_for_salvage) = env_for_salvage {
let Environment { mem, .. } = env_for_salvage;
// Everything other than the memory is now dropped.
Some(mem)
} else {
None
};
// Certain apps need to launch in a non-portrait orientation, and this
// should be handled before creating the window because handling of
// window rotation after-the-fact is somewhat glitchy.
@@ -336,11 +317,7 @@ impl Environment {
))
};
let mut mem = if let Some(mem) = mem_for_salvage {
mem::Mem::refurbish(mem)
} else {
mem::Mem::new()
};
let mut mem = mem::Mem::new();
let is_spore = bundle.bundle_identifier().starts_with("com.ea.spore");
// We always reset this flag depending on which game is launched.

View File

@@ -182,8 +182,8 @@ pub fn main<T: Iterator<Item = String>>(mut args: T) -> Result<(), String> {
return Ok(());
}
let (bundle_path, env_for_salvage) = if let Some(bundle_path) = bundle_path {
(bundle_path, None)
let bundle_path = if let Some(bundle_path) = bundle_path {
bundle_path
} else {
let mut options = options::Options::default();
// Apply command-line options only (no app-specific options apply)
@@ -199,8 +199,7 @@ pub fn main<T: Iterator<Item = String>>(mut args: T) -> Result<(), String> {
echo!(
"No app specified, opening app picker. Use the --help flag to see command-line usage."
);
let (bundle_path, env_for_salvage) = app_picker::app_picker(options, &mut option_args)?;
(bundle_path, Some(env_for_salvage))
app_picker::app_picker(options, &mut option_args)?
};
// When PowerShell does tab-completion on a directory, for some reason it
@@ -311,13 +310,7 @@ pub fn main<T: Iterator<Item = String>>(mut args: T) -> Result<(), String> {
}
let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
Environment::new(
bundle,
fs,
options.clone(),
app_args.unwrap_or_default(),
env_for_salvage,
)
Environment::new(bundle, fs, options.clone(), app_args.unwrap_or_default())
}));
let mut env = match res {
Ok(ret) => match ret {

View File

@@ -17,6 +17,7 @@
use crate::libc::wchar::wchar_t;
mod allocator;
mod host;
/// Equivalent of `usize` for guest memory.
pub type GuestUSize = u32;
@@ -252,9 +253,8 @@ pub struct Mem {
impl Drop for Mem {
fn drop(&mut self) {
let layout = std::alloc::Layout::new::<Bytes>();
unsafe {
std::alloc::dealloc(self.bytes as *mut _, layout);
crate::mem::host::free_memory(self.bytes.cast(), std::mem::size_of::<Bytes>()).unwrap();
}
}
}
@@ -275,13 +275,17 @@ impl Mem {
/// Create a fresh instance of guest memory.
pub fn new() -> Mem {
// This will hopefully get the host OS to lazily allocate the memory.
let layout = std::alloc::Layout::new::<Bytes>();
// TODO: align memory to guest page size
// Right now, if aligned, will cause OOM on low end Android devices.
// See relevant [Github's issue](https://github.com/touchHLE/touchHLE/issues/498)
let bytes = unsafe { std::alloc::alloc_zeroed(layout) as *mut Bytes };
assert!(!bytes.is_null());
let size = std::mem::size_of::<Bytes>();
let ptr = unsafe { crate::mem::host::allocate_memory(size).unwrap() };
assert_eq!(
ptr as usize & PAGE_SIZE_ALIGN_MASK as usize,
0,
"Failed to align host memory with guest memory"
);
let bytes = ptr as *mut Bytes;
let allocator = allocator::Allocator::new();
@@ -293,26 +297,6 @@ impl Mem {
}
}
/// Take an existing instance of [Mem], but free and zero all the
/// allocations so it's "like new".
///
/// Note that, since there is no protection against writing outside an
/// allocation, there might be stray bytes preserved in the result.
pub fn refurbish(mut mem: Mem) -> Mem {
let Mem {
bytes: _,
null_segment_size: _,
ref mut allocator,
..
} = mem;
let used_chunks = allocator.reset_and_drain_used_chunks();
for allocator::Chunk { base, size } in used_chunks {
mem.bytes_mut()[base as usize..][..size.get() as usize].fill(0);
}
mem.null_segment_size = 0;
mem
}
/// Sets up the null segment of the given size. There's no reason to call
/// this outside of binary loading, and it won't be respected even if you
/// do. The size must not have been set already, and must be page aligned.

View File

@@ -128,12 +128,6 @@ mod collections {
Some(self.remove_with_base(chunk.base).unwrap())
}
#[inline(always)]
pub fn drain(self) -> impl Iterator<Item = Chunk> {
self.chunks
.into_iter()
.map(|(base, size)| Chunk { base, size })
}
#[inline(always)]
pub fn get_size_with_base(&self, base: VAddr) -> Option<NonZeroU32> {
self.chunks.get(&base).copied()
}
@@ -383,10 +377,4 @@ impl Allocator {
freed.size.get()
}
pub(super) fn reset_and_drain_used_chunks(&mut self) -> impl Iterator<Item = Chunk> {
let chunks = std::mem::take(&mut self.used_chunks);
*self = Allocator::new();
chunks.drain()
}
}

100
src/mem/host.rs Normal file
View File

@@ -0,0 +1,100 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
//! Cross-platform memory management wrappers using the host's system calls.
/// Cross-platform memory allocation using the host's system calls.
/// Returns an address aligned to the guest's 4KB page boundaries.
///
/// - The function returns a raw pointer to allocated memory.
/// The caller is responsible for managing that memory.
/// - The returned pointer must be freed using the corresponding
/// [`free_memory`] call.
#[cfg(windows)]
pub(super) unsafe fn allocate_memory(size: usize) -> std::io::Result<*mut core::ffi::c_void> {
use windows_sys::Win32::System::Memory::{
VirtualAlloc, MEM_COMMIT, MEM_RESERVE, PAGE_READWRITE,
};
let ptr = unsafe {
VirtualAlloc(
std::ptr::null(),
size,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE,
)
};
if ptr.is_null() {
return Err(std::io::Error::last_os_error());
}
Ok(ptr)
}
#[cfg(unix)]
pub(super) unsafe fn allocate_memory(size: usize) -> std::io::Result<*mut core::ffi::c_void> {
use libc::{mmap, sysconf, MAP_ANONYMOUS, MAP_PRIVATE, PROT_READ, PROT_WRITE, _SC_PAGESIZE};
const PAGE_SIZE: usize = crate::mem::PAGE_SIZE as usize;
let host_page_size = unsafe { sysconf(_SC_PAGESIZE) as usize };
assert!(
host_page_size >= PAGE_SIZE,
"Hosts with smaller than 4KiB pages are not supported."
);
let ptr = unsafe {
mmap(
std::ptr::null_mut(),
size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1,
0,
)
};
if ptr == libc::MAP_FAILED {
return Err(std::io::Error::last_os_error());
}
Ok(ptr)
}
/// Cross-platform memory free using the host's system calls.
///
/// # Safety
/// - The address and size should match parameters and result of the
/// [`allocate_memory`] call.
#[cfg(windows)]
pub(super) unsafe fn free_memory(
address: *mut core::ffi::c_void,
_size: usize,
) -> std::io::Result<()> {
use windows_sys::Win32::System::Memory::{VirtualFree, MEM_RELEASE};
let res = unsafe { VirtualFree(address, 0, MEM_RELEASE) };
if res == 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
#[cfg(unix)]
pub(super) unsafe fn free_memory(
address: *mut core::ffi::c_void,
size: usize,
) -> std::io::Result<()> {
use libc::munmap;
let res = unsafe { munmap(address, size) };
if res == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}