Skip to content

Repository files navigation

BlackBox

Real-time black hole and accretion disk visualisation for Android.

A GPU-driven particle engine that renders a gravitationally lensed accretion disk — event horizon shadow, photon ring, and the far side of the disk arcing over the top — at up to 1.25 million particles, on hardware that has no business doing so.

Built in Kotlin and C++ with a Vulkan renderer, an OpenGL ES fallback, and a closed-loop quality governor that keeps a frame budget on everything from a Snapdragon 6-class phone upward.

app.blackbox.auriel

What it does

  • Up to 1,250,000 particles with state resident in GPU memory and integration in compute shaders
  • Gravitational lensing per particle — the shadow, the photon ring, and the lensed arcs above and below the disk, from a thin-lens solve costing one sqrt per particle
  • Tree-PM gravity solverO(n) forces via a particle mesh, with Barnes-Hut run on the mesh cells rather than on the particles
  • Relativistic effects — Doppler beaming, gravitational redshift, Kerr ISCO, Lense-Thirring frame dragging
  • Seven Image Modes — complete display pipeline configurations, from calibrated sRGB through HDR10 and HLG
  • Vulkan 1.1+ primary, OpenGL ES 3.1 fallback, chosen at runtime
  • Adaptive quality that scales both ways — it sheds work on weak hardware and promotes itself above the selected tier on strong hardware

Install

Grab the APK from Releases and tap it. Android will ask you to allow installs from your browser or file manager the first time — that is the standard prompt for anything not distributed through the Play Store.

Requires Android 8.0 (API 26) or newer on a 64-bit ARM device, which is every Android phone released since roughly 2019.


Measured performance

Both figures below are the engine's own telemetry, sustained, after the governor has converged.

Device SoC GPU Result
OnePlus Nord 4 Snapdragon 7+ Gen 3 Adreno 732 700,000 particles @ 66–80 fps, bloom + lensing + counter-image, GPU 14.4 ms
Galaxy M31 Exynos 9611 Mali-G72 MP3 142,000 particles @ 30 fps, lensing on, GPU 24 ms

The Nord 4 figure is the governor promoting itself two tiers above the Medium preset it started on. The M31 figure is it settling two tiers below. Neither was configured by hand.


Using it

Gesture Action
Drag Orbit — azimuth and inclination
Pinch Zoom, 24–420 gravitational radii
Idle 6 s Slow automatic drift resumes

A live frame rate sits in the top left. The tune icon opens advanced settings: Image Mode, simulation physics, performance controls, and a device capability readout. Every control states its cost.

For the full telemetry breakdown — per-stage GPU timings, governor state, thermal and battery inputs — enable the overlay in Settings → Performance, or watch it from a terminal:

adb logcat -s BlackBox

Image Modes

An Image Mode is a complete display pipeline configuration — colour profile, contrast curve, saturation, bloom weight, brightness mapping, HDR behaviour — not a filter bolted on at the end. All seven share one shader; only a push-constant block and the requested swapchain format differ.

IM1 Reference Neutral filmic sRGB. The calibration baseline every other mode deviates from.
IM2 Deep Contrast LCD-tuned. Crushes the toe to hide backlight glow, raises midtone contrast.
IM3 Vivid Wide Display-P3. Spends the extra gamut on the blue-shifted approaching limb.
IM4 HDR10 BT.2020 + PQ. Maps the photon ring above SDR white instead of into it.
IM5 HLG Scene-referred HDR; degrades gracefully on panels that over-report their peak.
IM6 Spectral Planckian-locus colour, minimal bloom. Colour readable as data.
IM7 Power Saver Lower average picture level, two-level bloom.

Selection is automatic by default, based on what the display reports. It is also always manually overridable, because device HDR and wide-gamut reporting is unreliable enough on mid-range hardware that trusting it blindly produces a visibly wrong image. The renderer independently checks what the swapchain actually granted and downgrades the transfer function rather than encoding PQ into an sRGB surface.

You never need to change a system display setting.


Quality and frame rate

Quality tiers are coherent presets — particle count, render scale, bloom depth, gravity mesh and substeps move together, so the ratio of compute cost to bandwidth cost stays roughly constant.

Tier Particles Render scale Bloom Gravity mesh
Minimum 90,000 50% 2 32×16×32
Low 180,000 60% 3 48×24×48
Medium 380,000 72% 4 64×32×64
High 700,000 85% 4 96×48×96
Ultra 1,250,000 100% 5 128×64×128

Frame rate targets are 30 / 45 / 60 / 90 / uncapped, and each is a genuine trade rather than a cap. A lower target buys density: on mid-range hardware, 30 fps sustains five to six times the particle count of 60 fps. Each target carries its own tuning — starting position on the quality ladder, a display refresh-rate hint so non-divisor rates like 45 pace cleanly, and FIFO versus mailbox presentation.

The governor closes a loop on the 95th-percentile frame interval using additive-increase / multiplicative-decrease: it remembers the quality level that proved unsustainable and refuses to climb back past it, so it converges instead of oscillating. Thermal and battery state bias it, but never override measured reality — vendor thermal reporting is treated with the same scepticism as vendor HDR reporting, and for the same reason.


Physics

Geometrised units throughout: G = M = c = 1, lengths in gravitational radii r_g = GM/c².

  • Paczyński–Wiita pseudo-potential — reproduces the ISCO and the plunge region for the cost of one divide
  • Kerr ISCO (Bardeen–Press–Teukolsky) — 6 r_g at a = 0, 1.24 r_g at a = 0.998
  • Lense–Thirring frame dragging to leading order, ω ≈ 2aM/r³
  • Shakura–Sunyaev α-viscosity on the radial velocity component — the mechanism that makes the disk actually accrete rather than orbit forever
  • T ∝ r^(−3/4) thin-disk temperature profile, with compression heating in dense regions
  • Relativistic Doppler beaming (δ⁴) and gravitational redshift (√(1 − r_s/r))
  • Velocity Verlet on a fixed 1/120 s timestep, decoupled from frame rate

The goal is visual accuracy at the scale a phone screen resolves, not a research-grade astrophysics simulator. Where an approximation is made, ARCHITECTURE.md says exactly what it gets wrong.


Build

Gradle 9.1.0
Android Gradle Plugin 8.13.1
Kotlin 2.2.20
compileSdk / targetSdk 36
minSdk 26
NDK 28.2.13676358
CMake 3.22.1
JDK 21
ABI arm64-v8a
./gradlew :app:assembleDebug          # validation layers + engine assertions
./gradlew :app:assembleRelease        # R8 + resource shrinking
./gradlew :app:testDebugUnitTest      # 26 tests, JNI wire-format contract
./gradlew :app:installDebug

glslc is taken from $ANDROID_NDK/shader-tools/, so no separate Vulkan SDK is required. The 12 GLSL shaders are compiled to SPIR-V at build time and embedded in .rodata — there is no runtime file I/O and no way to ship a binary whose shaders disagree with its pipeline layouts.

The build is arm64-only by design: the NEON kernels use AArch64-only instructions with no runtime dispatch, which is a deliberate trade documented in the architecture notes.

Signing

Release builds read keystore.properties from the repo root (gitignored). Without it, assembleRelease produces an unsigned APK rather than silently falling back to a debug key — Android identifies an app by its signing certificate, so a debug-signed build that reaches users could never be updated by a properly signed one.

storeFile=keystore/release.jks
storePassword=…
keyAlias=…
keyPassword=…

Architecture

Four layers with hard boundaries: Android platform, engine host, simulation, renderer.

Kotlin  ─ UI · settings · display / thermal / power probing
   │ JNI — control plane only, never per frame
C++    ─ render thread, backend selection, frame orchestration
   ├── Simulation   particle store · gravity mesh · Barnes-Hut · integration
   ├── Renderer     Vulkan │ OpenGL ES · camera · Image Modes
   └── Governor     closed loop on frame time, thermal and power aware

The rule that shapes everything: JNI is a control plane, not a data plane. The render thread is created in C++ and never re-enters the JVM, so the frame loop carries no transition cost and cannot be stalled by garbage collection.

ARCHITECTURE.md covers the reasoning in full — bandwidth analysis, memory layout, why Barnes-Hut runs on the mesh instead of the particles, the rendering pipeline, and every significant trade-off stated plainly.


Project layout

app/src/main/kotlin/     platform layer, UI, settings, JNI declarations
app/src/main/cpp/
  engine.cpp             render thread, backend selection, orchestration
  core/                  arena allocator, job system, NEON math, timing
  sim/                   particle store, gravity mesh, Barnes-Hut, integration
  render/vulkan/         loader, context, allocator, renderer
  render/gles/           EGL, runtime GLSL, upload ring
  platform/              adaptive quality governor
  shaders/               GLSL → SPIR-V, embedded at build time
ARCHITECTURE.md          engineering rationale

Known limitations

  • The OpenGL ES backend is untested on real hardware — both development devices have working Vulkan drivers, so the fallback path has never executed.
  • The lensed counter-image (thin arcs hugging the shadow) is a second full vertex pass and is the first thing the governor sheds on mid-range GPUs.
  • Lensing uses a thin-lens approximation, not geodesic integration. It produces the shadow, the ring and the arcs correctly; it does not produce third-and-higher-order images.
  • Colour is close to, but not identical with, published reference renderings — the disk lacks their fine radial striations.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages