diff --git a/Config/Supermodel.ini b/Config/Supermodel.ini index 7986237..56355b9 100644 --- a/Config/Supermodel.ini +++ b/Config/Supermodel.ini @@ -40,6 +40,7 @@ WideBackground = false ; Refresh rate (milliHertz accuracy). Actual Model 3 refresh rate is 57.524 Hz ; but this can cause judder so we use 60 Hz by default. +Throttle = 1 RefreshRate = 60.000 ; Legacy SCSP DSP implementation for games that do not play well with the newer diff --git a/README.md b/README.md index 8df50c5..cfa7f4c 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,15 @@ From the main game list, open the side menu (toolbar "menu" icon) and look under - **Wide background:** Stretches the 2D background layers when using widescreen (Supermodel `-wide-bg`). - **Enhanced Real3D (experimental):** Enables more desktop-like Real3D rendering (multi-pass transparency compositing + scroll fog). This can improve visuals in some games/areas, but may reduce performance on lower-end devices. +## Frame rate (High refresh devices) + +If games run too fast on 90/120Hz devices, enable frame limiting in `.../Android/data//files/super3/Config/Supermodel.ini`: + +- `Throttle = 1` +- `RefreshRate = 60.000` (or `57.524` for true Model 3 speed) + +Note: the app overwrites `Supermodel.ini` with the APK defaults after app updates. + ## Files & Storage - Save states and thumbnails are stored under the app's user data directory in `Saves/` (for example: `.../Android/data//files/super3/Saves/`). diff --git a/android/app/src/main/assets/Config/Supermodel.ini b/android/app/src/main/assets/Config/Supermodel.ini index 19c0017..031f872 100644 --- a/android/app/src/main/assets/Config/Supermodel.ini +++ b/android/app/src/main/assets/Config/Supermodel.ini @@ -28,6 +28,11 @@ QuadRendering = 0 XResolution = 496 YResolution = 384 VSync = 1 +; Frame limiting (recommended on 90/120Hz devices to prevent speedups) +Throttle = 1 +; Refresh rate (milliHertz accuracy). Actual Model 3 refresh rate is 57.524 Hz +; but this can cause judder so we use 60 Hz by default. +RefreshRate = 60.000 WideScreen = 0 WideBackground = 0 ; Enhanced Real3D compositing (FBO transparency + scroll fog). More accurate but may be slower. diff --git a/android/app/src/main/cpp/native-lib.cpp b/android/app/src/main/cpp/native-lib.cpp index 8706f2e..589fc7a 100644 --- a/android/app/src/main/cpp/native-lib.cpp +++ b/android/app/src/main/cpp/native-lib.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include @@ -259,6 +260,13 @@ struct Super3Host { // Supermodel.ini commonly uses 200 as "100%". config.Set("SoundVolume", "100"); config.Set("MusicVolume", "150"); + + // Match desktop defaults: cap emulation to the requested refresh rate. + // Without this, vsync on high-refresh devices (e.g., 120 Hz) can drive the + // emulation loop too fast. + config.Set("Throttle", true); + config.Set("RefreshRate", "60.0"); + config.Set("LegacySoundDSP", false); config.Set("New3DEngine", true); config.Set("New3DAccurate", false); @@ -736,6 +744,45 @@ static std::optional FindFirstExisting(const std::vector(60.0f)); + } catch (...) { + refreshRateHz = 60.0f; + } + + if (!std::isfinite(refreshRateHz) || refreshRateHz <= 0.0f) { + return 0; + } + + return uint64_t(1000.0f * refreshRateHz); +} + +static void SuperSleepUntil(uint64_t target, uint64_t perfCounterFrequency) +{ + const uint64_t time = SDL_GetPerformanceCounter(); + if (time > target) { + return; + } + + // Sleep the whole number of milliseconds minus one, then spin for the tail. + int32_t numWholeMillisToSleep = int32_t((target - time) * 1000 / perfCounterFrequency); + numWholeMillisToSleep -= 1; + if (numWholeMillisToSleep > 0) { + SDL_Delay((uint32_t)numWholeMillisToSleep); + } + + volatile uint64_t now; + int32_t remain; + do { + now = SDL_GetPerformanceCounter(); + remain = int32_t(target - now); + } while (remain > 0); +} + // SDL entry point; currently a stub until the emulator core is hooked up. extern "C" int SDL_main(int argc, char* argv[]) { (void)argc; @@ -896,6 +943,10 @@ extern "C" int SDL_main(int argc, char* argv[]) { bool loggedControls = false; bool audioOpened = false; bool new3dAttached = false; + + const uint64_t perfCounterFrequency = SDL_GetPerformanceFrequency(); + uint64_t perfCountPerFrame = 0; + uint64_t nextTime = 0; while (running) { SDL_Event ev; while (SDL_PollEvent(&ev)) { @@ -1027,6 +1078,20 @@ extern "C" int SDL_main(int argc, char* argv[]) { SDL_GL_SwapWindow(window); + // Frame limiting: cap emulation on high refresh rate displays (e.g., 120 Hz). + // Mirrors the desktop SDL OSD behavior (Throttle + RefreshRate). + const bool throttle = host.config["Throttle"].ValueAsDefault(true); + if (throttle || host.threadsPausedByMenu) { + const uint64_t refreshRateMilliHz = GetDesiredRefreshRateMilliHz(host.config); + if (refreshRateMilliHz > 0 && perfCounterFrequency > 0) { + perfCountPerFrame = (perfCounterFrequency * 1000) / refreshRateMilliHz; + if (perfCountPerFrame == 0) perfCountPerFrame = 1; + + SuperSleepUntil(nextTime, perfCounterFrequency); + nextTime = SDL_GetPerformanceCounter() + perfCountPerFrame; + } + } + uint32_t t = SDL_GetTicks(); if (t - lastStatusLog > 2000) { lastStatusLog = t; diff --git a/android/app/src/main/java/com/izzy2lost/super3/AssetInstaller.kt b/android/app/src/main/java/com/izzy2lost/super3/AssetInstaller.kt index ac73646..8fa9fef 100644 --- a/android/app/src/main/java/com/izzy2lost/super3/AssetInstaller.kt +++ b/android/app/src/main/java/com/izzy2lost/super3/AssetInstaller.kt @@ -1,19 +1,43 @@ package com.izzy2lost.super3 import android.content.Context +import android.os.Build import java.io.File object AssetInstaller { + private const val PREFS_NAME = "super3_prefs" + private const val KEY_ASSET_VERSION_CODE = "asset_installed_version_code" + fun ensureInstalled(context: Context, internalUserRoot: File) { internalUserRoot.mkdirs() // Copy the standard Supermodel folder layout from APK assets into the internal user root. - // We never overwrite existing files so user edits persist. + // We generally avoid overwriting existing files so user data persists. val topLevel = listOf("Assets", "Config", "GraphicsAnalysis", "NVRAM", "Saves") for (dir in topLevel) { copyAssetTree(context, dir, File(internalUserRoot, dir)) } + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val lastInstalledVersionCode = (prefs.all[KEY_ASSET_VERSION_CODE] as? Number)?.toLong() ?: -1L + val currentVersionCode = + runCatching { + val info = context.packageManager.getPackageInfo(context.packageName, 0) + if (Build.VERSION.SDK_INT >= 28) info.longVersionCode else info.versionCode.toLong() + }.getOrDefault(-1L) + + // If the app was updated, overwrite Supermodel.ini with the new defaults from the APK. + // (Other files are still preserved by default.) + if (lastInstalledVersionCode != currentVersionCode) { + copyAssetFile( + context, + "Config/Supermodel.ini", + File(File(internalUserRoot, "Config"), "Supermodel.ini"), + overwrite = true, + ) + prefs.edit().putLong(KEY_ASSET_VERSION_CODE, currentVersionCode).apply() + } + migrateSupermodelIni(File(File(internalUserRoot, "Config"), "Supermodel.ini")) } @@ -40,6 +64,16 @@ object AssetInstaller { } } + private fun copyAssetFile(context: Context, assetPath: String, dest: File, overwrite: Boolean) { + if (!overwrite && dest.exists()) return + dest.parentFile?.mkdirs() + context.assets.open(assetPath).use { input -> + dest.outputStream().use { output -> + input.copyTo(output) + } + } + } + private fun migrateSupermodelIni(ini: File) { if (!ini.exists()) return