diff --git a/Config/Supermodel.ini b/Config/Supermodel.ini index 50d0b44..2657c44 100644 --- a/Config/Supermodel.ini +++ b/Config/Supermodel.ini @@ -406,18 +406,22 @@ PowerPCFrequency = 40 [ vf3tb ] PowerPCFrequency = 40 ;Virtual On 2: Oratorio Tangram (Revision B) - [ von2 ] -PowerPCFrequency = 30 -;Virtual On 2: Oratorio Tangram (Ver 5.4g) - [ von254g ] -PowerPCFrequency = 30 -;Virtual On 2: Oratorio Tangram (Revision A) - [ von2a ] -PowerPCFrequency = 30 -;Virtual On 2: Oratorio Tangram (?) - [ von2o ] -PowerPCFrequency = 35 -;Virtua Striker 2 (Step 2.0) - no special settings +[ von2 ] + PowerPCFrequency = 30 + AndroidSimpleShader = 1 + ;Virtual On 2: Oratorio Tangram (Ver 5.4g) +[ von254g ] + PowerPCFrequency = 30 + AndroidSimpleShader = 1 + ;Virtual On 2: Oratorio Tangram (Revision A) +[ von2a ] + PowerPCFrequency = 30 + AndroidSimpleShader = 1 + ;Virtual On 2: Oratorio Tangram (?) +[ von2o ] + PowerPCFrequency = 35 + AndroidSimpleShader = 1 + ;Virtua Striker 2 (Step 2.0) - no special settings [ vs2 ] ;Virtua Striker 2 (Step 1.5) - no special settings [ vs215 ] diff --git a/Src/Graphics/New3D/New3D.cpp b/Src/Graphics/New3D/New3D.cpp index 153b69a..3f11b3e 100644 --- a/Src/Graphics/New3D/New3D.cpp +++ b/Src/Graphics/New3D/New3D.cpp @@ -9,6 +9,7 @@ #include #include "R3DFloat.h" #include "Util/BitCast.h" +#include "OSD/Logger.h" #ifdef __ANDROID__ #define MAX_RAM_VERTS 150000 @@ -398,6 +399,16 @@ void CNew3D::RenderFrame(void) RenderViewport(0x800000); m_vbo.Bind(true); +#ifdef __ANDROID__ + const bool clampRamVerts = m_config["AndroidClampRamVerts"].ValueAsDefault(true); + if (clampRamVerts && m_polyBufferRam.size() > MAX_RAM_VERTS) { + if (!m_warnedRamOverflow) { + ErrorLog("New3D: RAM vertex buffer overflow (size=%zu, max=%d). Clamping.", m_polyBufferRam.size(), MAX_RAM_VERTS); + m_warnedRamOverflow = true; + } + m_polyBufferRam.resize(MAX_RAM_VERTS); + } +#endif m_vbo.BufferSubData(MAX_ROM_VERTS*sizeof(FVertex), m_polyBufferRam.size()*sizeof(FVertex), m_polyBufferRam.data()); if (!m_polyBufferRom.empty()) { @@ -493,6 +504,18 @@ void CNew3D::RenderFrame(void) DrawScrollFog(); // fog layer if applicable must be drawn here m_vbo.Bind(true); +#ifdef __ANDROID__ + { + const bool clampRamVerts = m_config["AndroidClampRamVerts"].ValueAsDefault(true); + if (clampRamVerts && m_polyBufferRam.size() > MAX_RAM_VERTS) { + if (!m_warnedRamOverflow) { + ErrorLog("New3D: RAM vertex buffer overflow (size=%zu, max=%d). Clamping.", m_polyBufferRam.size(), MAX_RAM_VERTS); + m_warnedRamOverflow = true; + } + m_polyBufferRam.resize(MAX_RAM_VERTS); + } + } +#endif m_vbo.BufferSubData(MAX_ROM_VERTS*sizeof(FVertex), m_polyBufferRam.size()*sizeof(FVertex), m_polyBufferRam.data()); // upload all the dynamic data to GPU in one go if (!m_polyBufferRom.empty()) { diff --git a/Src/Graphics/New3D/New3D.h b/Src/Graphics/New3D/New3D.h index de52404..81c82f6 100644 --- a/Src/Graphics/New3D/New3D.h +++ b/Src/Graphics/New3D/New3D.h @@ -244,6 +244,7 @@ private: bool m_sunClamp; bool m_shadeIsSigned; bool m_new3dAccurate; + bool m_warnedRamOverflow = false; // Stepping int m_step; diff --git a/Src/Graphics/New3D/R3DShader.cpp b/Src/Graphics/New3D/R3DShader.cpp index bf76a8d..aec86aa 100644 --- a/Src/Graphics/New3D/R3DShader.cpp +++ b/Src/Graphics/New3D/R3DShader.cpp @@ -59,6 +59,13 @@ bool R3DShader::LoadShader(const char* vertexShader, const char* fragmentShader) const char* gShader = ""; const char* fShader = fragmentShaderR3D; +#ifdef __ANDROID__ + if (m_config["AndroidSimpleShader"].ValueAsDefault(false)) { + vShader = vertexShaderR3DSimple; + fShader = fragmentShaderR3DSimple; + } +#endif + #ifndef __ANDROID__ if (quads) { vShader = vertexShaderR3DQuads; diff --git a/Src/Graphics/New3D/R3DShaderTriangles.h b/Src/Graphics/New3D/R3DShaderTriangles.h index c11d7e1..1dd1c4b 100644 --- a/Src/Graphics/New3D/R3DShaderTriangles.h +++ b/Src/Graphics/New3D/R3DShaderTriangles.h @@ -2,7 +2,85 @@ #define _R3DSHADERTRIANGLES_H_ #ifdef __ANDROID__ -// GLES 3.0 shaders (full path adapted from desktop triangle shader). +// GLES 3.0 shaders. +// - Simple path: textured vertex-colour with optional alpha discard (older Android fallback). +// - Full path: adapted from desktop triangle shader. +static const char *vertexShaderR3DSimple = R"glsl(#version 300 es +precision highp float; +precision highp int; + +// uniforms +uniform float modelScale; +uniform mat4 modelMat; +uniform mat4 projMat; +uniform int translatorMap; + +// attributes +layout(location=0) in vec4 inVertex; +layout(location=1) in vec3 inNormal; +layout(location=2) in vec2 inTexCoord; +layout(location=3) in vec4 inColour; // normalized UBYTE -> float +layout(location=4) in vec3 inFaceNormal; +layout(location=5) in float inFixedShade; + +out vec2 vTexCoord; +out vec4 vColor; +out float vAlpha; +out vec3 vDummy; // keeps attributes alive + +void main() +{ + vec4 c = inColour; + if (translatorMap != 0) c.rgb *= 16.0; + vColor = c; + vTexCoord = inTexCoord; + vAlpha = c.a; + vDummy = inNormal + inFaceNormal + vec3(inFixedShade); + gl_Position = projMat * modelMat * inVertex; +} +)glsl"; + +static const char *fragmentShaderR3DSimple = R"glsl(#version 300 es +precision mediump float; +precision mediump int; + +in vec2 vTexCoord; +in vec4 vColor; +in float vAlpha; +in vec3 vDummy; + +uniform sampler2D tex1; +uniform int textureEnabled; +uniform int textureAlpha; +uniform int alphaTest; +uniform int discardAlpha; + +out vec4 oColor; + +void main() +{ + vec4 col = vColor; + vec4 tex = vec4(1.0); + + if (textureEnabled != 0) { + tex = texture(tex1, vTexCoord); + if (textureAlpha != 0) { + col *= tex; + } else { + col.rgb *= tex.rgb; + } + } + + // keep vDummy "used" + col.rgb += vDummy * 0.0; + + if (alphaTest != 0 && tex.a < (32.0/255.0)) discard; + if (discardAlpha != 0 && col.a < 0.99) discard; + + oColor = col; +} +)glsl"; + static const char *vertexShaderR3D = R"glsl(#version 300 es precision highp float; precision highp int; diff --git a/Src/Model3/Model3.cpp b/Src/Model3/Model3.cpp index 6c91037..79a0edf 100644 --- a/Src/Model3/Model3.cpp +++ b/Src/Model3/Model3.cpp @@ -1774,8 +1774,11 @@ void CModel3::Write32(UINT32 addr, UINT32 data) { UINT32 flipped = FLIPENDIAN32(data); TileGen.WriteRegister(addr&0xFF, flipped); - if (addr == 0xF118000C) - GPU.TilegenDrawFrame(flipped); + if (addr == 0xF118000C) { + const bool legacyReal3D = m_config["LegacyReal3D"].ValueAsDefault(false); + if (!legacyReal3D) + GPU.TilegenDrawFrame(flipped); + } break; } @@ -2055,9 +2058,10 @@ void CModel3::RunMainBoardFrame(void) unsigned vBlankCycles = lineCycles * 40; unsigned dispCycles = lineCycles * 384; unsigned statusCycles = (unsigned)((float)frameCycles * (0.005f)); - const bool legacyTiming = - m_config["LegacyReal3DTiming"].ValueAsDefault( - m_config["LegacyStatusBit"].ValueAsDefault(false)); + const bool legacyTiming = + m_config["LegacyReal3D"].ValueAsDefault(false) || + m_config["LegacyReal3DTiming"].ValueAsDefault( + m_config["LegacyStatusBit"].ValueAsDefault(false)); // Scale PPC timer ratio according to speed at which the PowerPC is being emulated so that the observed running frequency of the PPC timer // registers is more or less correct. This is needed to get the Virtua Striker 2 series of games running at the right speed (they are diff --git a/Src/Model3/Real3D.cpp b/Src/Model3/Real3D.cpp index 86f9152..7438c86 100644 --- a/Src/Model3/Real3D.cpp +++ b/Src/Model3/Real3D.cpp @@ -166,16 +166,20 @@ static void UpdateRenderConfig(IRender3D *Render3D, uint64_t internalRenderConfi void CReal3D::BeginVBlank(int statusCycles) { + const bool legacyReal3D = m_config["LegacyReal3D"].ValueAsDefault(false); const bool legacyTiming = m_config["LegacyReal3DTiming"].ValueAsDefault( m_config["LegacyStatusBit"].ValueAsDefault(false)); - m_useLegacyStatusBit = legacyTiming; + m_useLegacyStatusBit = legacyReal3D || legacyTiming; if (m_useLegacyStatusBit) { statusChange = ppc_total_cycles() + statusCycles; m_evenFrame = !m_evenFrame; } + if (legacyReal3D) + return; + m_pingPongCopy = m_pingPong; if (commandPortWritten) @@ -192,6 +196,8 @@ void CReal3D::EndVBlank(void) void CReal3D::FlipPingPongBit(void) { + if (m_config["LegacyReal3D"].ValueAsDefault(false)) + return; m_pingPong = !m_pingPong; m_tilegenDrawFrame = false; commandPortWritten = false; @@ -199,6 +205,9 @@ void CReal3D::FlipPingPongBit(void) uint32_t CReal3D::SyncSnapshots(void) { + const bool legacyReal3D = m_config["LegacyReal3D"].ValueAsDefault(false); + if (legacyReal3D) + commandPortWritten = false; if (!m_gpuMultiThreaded) return 0; @@ -688,6 +697,8 @@ void CReal3D::FlushTextures() bool CReal3D::PollPingPong() { + if (m_config["LegacyReal3D"].ValueAsDefault(false)) + return false; return m_pingPong != m_pingPongCopy; } @@ -699,6 +710,8 @@ void CReal3D::DrawFrame() void CReal3D::TilegenDrawFrame(uint32_t flags) { + if (m_config["LegacyReal3D"].ValueAsDefault(false)) + return; (void)flags; m_tilegenDrawFrame = true; @@ -714,6 +727,12 @@ void CReal3D::Flush(void) commandPortWritten = true; DebugLog("Real3D 88000000 written @ PC=%08X\n", ppc_get_pc()); + if (m_config["LegacyReal3D"].ValueAsDefault(false)) + { + FlushTextures(); + return; + } + if (!PollPingPong()) { FlushTextures(); @@ -838,7 +857,7 @@ uint32_t CReal3D::ReadRegister(unsigned reg) DebugLog("Real3D: Read reg %X\n", reg); if (reg == 0) { - if (m_useLegacyStatusBit) + if (m_useLegacyStatusBit || m_config["LegacyReal3D"].ValueAsDefault(false)) { uint32_t ping_pong; if (m_evenFrame) { diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 2074a74..4a598db 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -29,8 +29,8 @@ android { minSdk = 26 targetSdk = 36 - versionCode = 19 - versionName = "1.1.8" + versionCode = 20 + versionName = "1.1.9" ndk { abiFilters += listOf("arm64-v8a") diff --git a/android/app/src/main/assets/Config/Supermodel.ini b/android/app/src/main/assets/Config/Supermodel.ini index 602d55a..d6e31ec 100644 --- a/android/app/src/main/assets/Config/Supermodel.ini +++ b/android/app/src/main/assets/Config/Supermodel.ini @@ -205,4 +205,17 @@ InputAnalogGunRight = JOY1_XAXIS_POS InputAnalogGunUp = JOY1_YAXIS_NEG InputAnalogGunDown = JOY1_YAXIS_POS +; Virtual On 2: force simple shader on Android to avoid GPU hangs. +[ von2 ] +AndroidSimpleShader = 1 + +[ von254g ] +AndroidSimpleShader = 1 + +[ von2a ] +AndroidSimpleShader = 1 + +[ von2o ] +AndroidSimpleShader = 1 + diff --git a/android/app/src/main/cpp/native-lib.cpp b/android/app/src/main/cpp/native-lib.cpp index e958f8d..6ee5b1e 100644 --- a/android/app/src/main/cpp/native-lib.cpp +++ b/android/app/src/main/cpp/native-lib.cpp @@ -197,6 +197,13 @@ static void AndroidTileBlit(const uint32_t* pixelsARGB, int width, int height, b g_presenter->Render(alphaBlend); } +static bool IsVonGame(const std::string& name) +{ + auto lower = name; + std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return (char)std::tolower(c); }); + return lower == "von2" || lower == "von254g" || lower == "von2a" || lower == "von2o"; +} + static void GunToViewCoords(float& x, float& y) { x = (x - 150.0f) / (651.0f - 150.0f); @@ -316,6 +323,10 @@ struct Super3Host { config.Set("LegacySoundDSP", false); config.Set("New3DEngine", true); config.Set("New3DAccurate", false); + config.Set("AndroidForceNew3D", true); + config.Set("AndroidTileBlit", true); + config.Set("AndroidSimpleShader", false); + config.Set("AndroidClampRamVerts", true); config.Set("QuadRendering", false); config.Set("FlipStereo", false); // The core expects this node to exist (throws std::range_error otherwise). @@ -357,6 +368,20 @@ struct Super3Host { inputSystem.ApplyConfig(config); } + void ApplyAndroidTileBlit() + { + const bool enabled = config["AndroidTileBlit"].ValueAsDefault(true); + CRender2D::SetAndroidTileBlit(enabled ? &AndroidTileBlit : nullptr); + } + + void ApplyGameHardOverrides(const std::string& gameName) + { + if (IsVonGame(gameName)) { + // Force simple shader path for Virtual On 2 variants to avoid GPU hangs. + config.Set("AndroidSimpleShader", true); + } + } + void ApplyAndroidHardOverrides() { auto ensureKeyboardFallback = [&](const char* cfgKey, const char* defaultKeyToken) { @@ -384,7 +409,9 @@ struct Super3Host { // Keep unsupported rendering knobs clamped on Android. config.Set("QuadRendering", false); - config.Set("New3DEngine", true); + const bool forceNew3D = config["AndroidForceNew3D"].ValueAsDefault(true); + if (forceNew3D) + config.Set("New3DEngine", true); config.Set("New3DAccurate", false); // Allow user-specified framebuffer sizes. Clamp to sane bounds so we don't @@ -485,6 +512,7 @@ struct Super3Host { } ApplyAndroidHardOverrides(); + ApplyAndroidTileBlit(); inputSystem.ApplyConfig(config); } @@ -492,6 +520,7 @@ struct Super3Host { ApplyDefaults(); ApplyIniOverrides(std::string()); ApplyAndroidHardOverrides(); + ApplyAndroidTileBlit(); if (!std::filesystem::exists(gamesXml)) { ErrorLog("Games XML not found at %s", gamesXml.c_str()); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Games XML not found at %s", gamesXml.c_str()); @@ -542,6 +571,8 @@ struct Super3Host { // Apply Supermodel.ini overrides (Global + [ game ]) after the loader has determined game->name. ApplyIniOverrides(game.name); ApplyAndroidHardOverrides(); + ApplyGameHardOverrides(game.name); + ApplyAndroidTileBlit(); // Initialize inputs before attaching to model if (!inputs.Initialize()) { @@ -977,7 +1008,6 @@ extern "C" int SDL_main(int argc, char* argv[]) { Super3Host host; g_presenter = &presenter; - CRender2D::SetAndroidTileBlit(&AndroidTileBlit); g_host = &host; // Initialize renderer backends up-front. The core will attach VRAM/palette/register // pointers later (after it has initialized the tile generator). 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 39771fe..42a9c35 100644 --- a/android/app/src/main/java/com/izzy2lost/super3/AssetInstaller.kt +++ b/android/app/src/main/java/com/izzy2lost/super3/AssetInstaller.kt @@ -120,7 +120,39 @@ object AssetInstaller { changed = true } + fun ensureSectionKey(sectionName: String, key: String, value: String) { + val sectionIdx = + out.indexOfFirst { line -> + val t = line.trim() + if (!t.startsWith("[") || !t.endsWith("]")) return@indexOfFirst false + val name = t.removePrefix("[").removeSuffix("]").trim() + name.equals(sectionName, ignoreCase = true) + } + if (sectionIdx < 0) { + if (out.isNotEmpty() && out.last().isNotBlank()) out.add("") + out.add("[ $sectionName ]") + out.add("$key = $value") + changed = true + return + } + + val endIdx = (sectionIdx + 1 + out.drop(sectionIdx + 1).indexOfFirst { it.trim().startsWith("[") }) + .let { if (it <= sectionIdx) out.size else it } + + val hasKey = out.subList(sectionIdx + 1, endIdx).any { + it.trim().startsWith("$key", ignoreCase = true) + } + if (hasKey) return + + out.add(endIdx, "$key = $value") + changed = true + } + ensureKey("[ Global ]", "LegacyReal3DTiming", "1") + ensureSectionKey("von2", "AndroidSimpleShader", "1") + ensureSectionKey("von254g", "AndroidSimpleShader", "1") + ensureSectionKey("von2a", "AndroidSimpleShader", "1") + ensureSectionKey("von2o", "AndroidSimpleShader", "1") if (!changed) return runCatching { ini.writeText(out.joinToString(System.lineSeparator())) } diff --git a/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt b/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt index dfce2e5..2394cae 100644 --- a/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt +++ b/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt @@ -366,9 +366,6 @@ class MainActivity : AppCompatActivity() { AssetInstaller.ensureInstalled(this, internalUserRoot()) - applyVideoSettingsToIni(internalUserRoot(), loadVideoSettings()) - applyTimingDefaultToIni(internalUserRoot(), loadTimingPref()) - applyViewMode(viewMode) refreshUi() pendingRomLaunch?.let { info -> @@ -1162,8 +1159,6 @@ class MainActivity : AppCompatActivity() { } AssetInstaller.ensureInstalled(this, internalRoot) - applyVideoSettingsToIni(internalRoot, loadVideoSettings()) - applyTimingDefaultToIni(internalRoot, loadTimingPref()) val cacheDir = File(internalRoot, "romcache") val required = gameDef?.let { resolveRequiredRomZips(it) } ?: listOf(info.baseName) @@ -1294,8 +1289,6 @@ class MainActivity : AppCompatActivity() { UserDataSync.syncFromTreeIntoInternal(this, userUri, internalRoot, UserDataSync.DIRS_GAME_SYNC) } - applyVideoSettingsToIni(internalRoot, loadVideoSettings()) - val cacheDir = File(internalRoot, "romcache") val required = resolveRequiredRomZips(game) val missing = required.filter { !zipDocs.containsKey(it) } diff --git a/android/app/src/main/java/com/izzy2lost/super3/UserDataSync.kt b/android/app/src/main/java/com/izzy2lost/super3/UserDataSync.kt index a07bef8..cd7e6b5 100644 --- a/android/app/src/main/java/com/izzy2lost/super3/UserDataSync.kt +++ b/android/app/src/main/java/com/izzy2lost/super3/UserDataSync.kt @@ -59,6 +59,7 @@ object UserDataSync { } private fun copyDocFileToFile(resolver: ContentResolver, from: DocumentFile, to: File) { + if (!shouldCopyDocToFile(from, to)) return val uri = from.uri resolver.openInputStream(uri)?.use { input -> to.parentFile?.mkdirs() @@ -82,6 +83,7 @@ object UserDataSync { private fun copyFileToDocFile(resolver: ContentResolver, from: File, toDir: DocumentFile) { val existing = toDir.findFile(from.name) + if (!shouldCopyFileToDoc(from, existing)) return val outDoc = existing ?: toDir.createFile("application/octet-stream", from.name) ?: return resolver.openOutputStream(outDoc.uri)?.use { output -> from.inputStream().use { input -> @@ -89,4 +91,30 @@ object UserDataSync { } } } + + private fun shouldCopyDocToFile(from: DocumentFile, to: File): Boolean { + if (!to.exists()) return true + val srcMod = from.lastModified() + val dstMod = to.lastModified() + if (srcMod > 0L && dstMod > 0L) return srcMod > dstMod + if (srcMod > 0L && dstMod == 0L) return true + if (srcMod == 0L && dstMod > 0L) return false + val srcLen = from.length() + val dstLen = to.length() + if (srcLen > 0L && dstLen > 0L) return srcLen != dstLen + return false + } + + private fun shouldCopyFileToDoc(from: File, to: DocumentFile?): Boolean { + if (to == null || !to.exists()) return true + val srcMod = from.lastModified() + val dstMod = to.lastModified() + if (srcMod > 0L && dstMod > 0L) return srcMod > dstMod + if (srcMod > 0L && dstMod == 0L) return true + if (srcMod == 0L && dstMod > 0L) return false + val srcLen = from.length() + val dstLen = to.length() + if (srcLen > 0L && dstLen > 0L) return srcLen != dstLen + return false + } }