Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@
!/ps2/build/.gitkeep
!/port/build/.gitkeep

# Android/Gradle local caches, downloaded build tools, and generated packages.
/android/.gradle/
/android/.gradle-home/
/android/.cxx/
/android/app/.cxx/
/android/build/
/android/app/build/
/android/app/libs/*.aar
/android/host-tools/glslang.exe
/android/app/src/main/jniLibs/*/libSDL3.so
/android/*.log

# Reference assembly, split from the disc by splat and never committed: one
# tree per release, ps2/asm/ntsc and ps2/asm/pal.
/ps2/asm/
Expand Down
4 changes: 2 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[submodule "tools/kazarov"]
path = tools/kazarov
url = https://github.com/TheMoonPeople/Kazarov.git
branch = main
url = https://github.com/anxiousintrovert/Kazarov-Android.git
branch = codex/android-support-upstream
6 changes: 4 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ project(dcdecomp NONE)
# PLATFORM picks what is built: PS2 builds and verifies the retail images
# (ps2/CMakeLists.txt), PC builds the x64 port (port/CMakeLists.txt).
set(PLATFORM PS2 CACHE STRING "Platform to build: PS2 or PC")
set_property(CACHE PLATFORM PROPERTY STRINGS PS2 PC)
set_property(CACHE PLATFORM PROPERTY STRINGS PS2 PC ANDROID)
if(PLATFORM STREQUAL "PS2")
add_subdirectory(ps2)
elseif(PLATFORM STREQUAL "PC")
# Here rather than in port so ctest finds the tests from the top of the build tree.
enable_testing()
add_subdirectory(port)
elseif(PLATFORM STREQUAL "ANDROID")
add_subdirectory(port)
else()
message(FATAL_ERROR "PLATFORM must be PS2 or PC, not ${PLATFORM}")
message(FATAL_ERROR "PLATFORM must be PS2, PC or ANDROID, not ${PLATFORM}")
endif()
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,44 @@ Game text can come from JSON language files (one per language, laid out as Minec
TrueType font so it stays sharp at any window size. The first start writes the language files from your own
game data; the Dark Cloud community font is bundled. See `docs/LOCALIZATION.md`.

## Android build

This fork builds Chronicle as a native Android app for `arm64-v8a` and `x86_64`, named Chronicle. It
includes a multi-touch PS2 control overlay with separate movement and camera sticks; unused L3/R3 buttons
are omitted, touches outside the controls do not send game input, and the overlay hides when a hardware
controller, keyboard, or mouse is connected. Android settings, controller bindings, Foldable display
changes, 16 KB page-size compatibility, and renderer/performance work are included. The renderer requires
Vulkan 1.3 and a compatible GPU. Android support remains experimental and device compatibility varies.

The app does not include Dark Cloud game data. On first launch, select your own PAL disc image; the app
imports it into private storage. Keep disc images and extracted `DATA.DAT`/`DATA.HD2` files out of this
repository and APK.

### Build on Windows

Install JDK 17 and Android Studio's SDK platform 35, build tools 35.0.0, NDK 27.0.12077973, and CMake
3.31.6. Clone this fork with its Kazarov submodule:

```powershell
git clone --recurse-submodules https://github.com/anxiousintrovert/Chronicle-Android.git
cd Chronicle-Android\android
.\fetch-deps.ps1
.\gradlew.bat assembleDebug
```

The Gradle wrapper pins Gradle 8.13. `fetch-deps.ps1` downloads SDL 3.4.16 and glslang 16.6.0, checking
their archive SHA-256 values before extraction. The debug APK is written to
`android/app/build/outputs/apk/debug/app-debug.apk`.

For local device testing, `android/publish-test-apk.ps1` builds and zips a timestamp-labeled package,
archives previous Chronicle test packages in `DARKCLOUDBUILDS`, and leaves the current ZIP in the chosen
destination root. Its default destination is the maintainer's mapped `Z:\` Games share; pass
`-DestinationRoot` to use another folder.

See [`android/README.md`](android/README.md) for prerequisites, setup, and build details. Prebuilt test
packages, when available, are published on the [GitHub Releases page](https://github.com/anxiousintrovert/Chronicle-Android/releases).
They contain the app only; users must supply their own compatible PAL disc image.

## Repository layout

- `ps2/src`, `ps2/include`: matching game sources and headers.
Expand Down
28 changes: 28 additions & 0 deletions android/FOLD-PERFORMANCE-2026-10-13.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Z Fold 7 performance profile — 2026-10-13

Device: Samsung SM-F966W, inner landscape display, thermal status 0. Android build with 50% internal render scale, 60 Hz game ticks, interpolation on, immediate present mode, and unlimited display FPS. The app was profiled in dungeon floor 1. These are debug builds with Android-only timing probes.

## Measurements

| Renderer | Scene/window | Ticks per 5 s | Fence slot wait | CPU shadow pass | GPU canonical | GPU display |
| --- | --- | ---: | ---: | ---: | ---: | ---: |
| 2 slots | Steady corridor, 22:15:41 | 240 (~48/s) | 1,364 ms total, 2.85 ms/wait | ~1,835 ms/5 s | 6.85 ms/submit | 1.28 ms/submit |
| 3 slots | Dungeon, 22:19:27 | 259 (~52/s) | 0.7 ms total, ~0.001 ms/wait | 2,068 ms/5 s | 6.77 ms/submit | 1.44 ms/submit |

Other three-slot windows reached 300 canonical submissions per five seconds (~60 ticks/s). GPU busy samples in a two-slot dungeon window were about 39–47%; in a three-slot window, 48–55%. These scenes are not identical, so tick-rate improvement is indicative rather than a controlled benchmark. The fence-wait reduction is direct and large. `fence_depth` stayed at zero in these scenes. Pool reset averaged below 0.1 ms in steady play. Acquire and queue submit were small, while three-slot off-CPU samples moved toward Android BufferQueue / `vkQueuePresentKHR` when display FPS was unlimited.

Before the overlay moved to an Android View, `TouchOverlayDraw` was a major native CPU sample in a menu scene. It no longer appeared in the post-change Fold profile. In the two-slot dungeon CPU profile, `DunMainDraw`, `Draw_MainUnitShadow`, `MGDrawShadowFast`, and `CVisualShadow::DrawVu1` dominated active game CPU work. The shadow path constructs prism faces and dynamic vertex/index vectors on the game thread each tick. Timed `DrawShadowPass` confirms several milliseconds of work per tick in active dungeon scenes.

## Decision

Keep three frames in flight on Android for now. It removes the recurring fence wait without changing game content and improved tick throughput in the sampled dungeon windows. It does not guarantee 60 ticks in shadow-heavy scenes. The next optimization target is CPU shadow-volume construction and its dynamic allocations, followed by reducing redundant display submissions when the app is already behind its 60 Hz tick target. A frame cap aligned with the display refresh should also prevent menus from drawing hundreds of frames per second. Do not treat the fence's `vkWaitForFences` stack as proof that the GPU is saturated.

The three-slot change is limited to Android; desktop stays at two slots. Before an upstream PR, run longer gameplay and fold/unfold checks, compare controlled same-scene A/B builds, and strip or gate the diagnostic timing code.

Evidence files: `fold-vulkan-timings-20261013.log`, `../dc-overlay-new-cpu-20261013.data`, `../dc-overlay-new-offcpu-20261013.data`, `../dc-3slot-cpu-20261013.data`, `../dc-3slot-offcpu-20261013.data`.

## Shadow-volume allocation pass

The next Android build reuses the temporary face/index arrays, constructs prism faces and near caps in fixed-size arrays, and reserves the fast volume's maximum vertex count. It does not alter the projection, winding, blend, depth, or shadow-band calculations. On the Fold, sampled active-dungeon shadow calls fell from about 0.68–0.95 ms each in the earlier three-slot log to about 0.27–0.36 ms each after the change. The scenes and number of casters differed, so this is evidence of a substantial per-call CPU reduction, not a controlled frame-rate claim. In the optimized capture the canonical GPU pass was roughly 2–3 ms and the display pass roughly 5–6 ms; fence waits remained negligible. Floor 1 was reloaded with the optimized build, and the app remained running.

Ground/wall shadow flicker was reported before this allocation pass. Pre-change and post-change Fold videos were captured, but they show different locations and camera angles, so they cannot establish a change in flicker frequency. The allocation pass preserves shadow geometry ordering. Investigate the shadow target/depth reuse and frame interpolation with a stationary same-scene A/B before claiming the flicker is fixed.
67 changes: 67 additions & 0 deletions android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Chronicle for Android

This project packages Chronicle's native game port as an Android app for `arm64-v8a` and `x86_64`. The
app is named Chronicle and uses SDL3 with the Kazarov Vulkan renderer. The renderer requires Vulkan 1.3
and the device features documented in [`../tools/kazarov/port/gfx/README.md`](../tools/kazarov/port/gfx/README.md).

The app includes a multi-touch PS2-style controller overlay with separate movement and camera sticks.
Analog input is clamped radially to match the circular stick pads. It omits unused L3/R3 buttons, leaves
areas outside controls input-inert, hides when a hardware gamepad, keyboard, or mouse is connected, and
recalculates its layout across Foldable display changes. Android
settings, renderer integration, 16 KB page-size compatibility, and performance work are in progress. The
Vulkan renderer requires Vulkan 1.3; support and performance vary by device.

## Game data

The APK contains no Dark Cloud disc image or extracted game data. On first launch, use the system file picker
to choose your own PAL disc image. Chronicle imports the required files into private app storage. Do not
commit or distribute disc images, `DATA.DAT`, `DATA.HD2`, saves, or extracted game assets with this source.

## Prerequisites

- JDK 17.
- Android SDK platform 35 and build tools 35.0.0.
- Android NDK 27.0.12077973.
- CMake 3.31.6.
- Windows PowerShell for the dependency setup script below.

Gradle 8.13 is pinned by the checked-in wrapper configuration. `fetch-deps.ps1` downloads SDL 3.4.16 and
the Windows glslang 16.6.0 shader compiler. It verifies the downloaded archive hashes before extracting
the dependencies into ignored local build folders.

## Build a debug APK

Clone the repository and initialize its renderer submodule:

```powershell
git clone --recurse-submodules https://github.com/anxiousintrovert/Chronicle-Android.git
cd Chronicle-Android\android
```

Then fetch the build dependencies and assemble the APK:

```powershell
.\fetch-deps.ps1
.\gradlew.bat assembleDebug
```

The output is `app\build\outputs\apk\debug\app-debug.apk`. The APK contains both `arm64-v8a` and
`x86_64` native libraries. No game image is needed to compile; the player selects their own PAL disc when
setting up the app.

## Releases

Prebuilt app-only packages are published on the [GitHub Releases page](https://github.com/anxiousintrovert/Chronicle-Android/releases).
The Android release artifact is a ZIP containing the APK for download compatibility. Releases do not
contain a disc image or extracted game data.

## Maintainer test packages

`publish-test-apk.ps1` builds the debug APK, makes a timestamped ZIP for easier Nextcloud downloads, and
moves previous Chronicle/Dark Cloud test APKs and ZIPs into `DARKCLOUDBUILDS`. It leaves the current ZIP at
the destination root. The script defaults to the maintainer's mapped `Z:\` Games share; provide
`-DestinationRoot` to use a different writable folder:

```powershell
.\publish-test-apk.ps1 -DestinationRoot 'D:\Chronicle-builds'
```
49 changes: 49 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
plugins {
id 'com.android.application'
}

android {
namespace 'org.themoonpeople.chronicle'
compileSdk 35

defaultConfig {
applicationId 'org.themoonpeople.chronicle'
minSdk 29
targetSdk 35
versionCode 11
versionName '0.11'

ndk {
abiFilters 'arm64-v8a', 'x86_64'
}

externalNativeBuild {
cmake {
arguments '-DPLATFORM=ANDROID', '-DANDROID_STL=c++_shared', '-DDC_BUILD_TESTS=OFF',
"-DGLSLANG_VALIDATOR=${projectDir.parentFile}/host-tools/glslang.exe"
cppFlags '-std=c++26'
}
}
}

buildFeatures {
prefab true
}

externalNativeBuild {
cmake {
path '../../CMakeLists.txt'
version '3.31.6'
}
}

sourceSets {
main {
assets.srcDirs = ['src/main/assets']
}
}
}

dependencies {
implementation files('libs/SDL3-3.4.16.aar')
}
26 changes: 26 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x00403000" android:required="true" />
<uses-feature android:name="android.hardware.vulkan.level" android:version="1" android:required="true" />
<application
android:appCategory="game"
android:allowBackup="true"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<property
android:name="android.window.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE"
android:value="false" />
<activity
android:name=".ChronicleActivity"
android:screenOrientation="landscape"
android:configChanges="orientation|keyboard|keyboardHidden|screenSize|smallestScreenSize|uiMode|density|layoutDirection|locale|navigation|screenLayout|touchscreen"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Loading