Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ build/
*.dylib
*.log
.ralph/

# Compiled shader artifacts (generated by cleanroom/build_embedded.py)
*.spv
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,22 @@ cmake -S . -B build/android-arm64 \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake
cmake --build build/android-arm64 -j
```

## Shaders

The compute shaders are a clean-room reimplementation of the shader *code*:
functional specifications were derived from disassembly of the runtime-traced
shaders, and the GLSL in `cleanroom/glsl/` was written from those
specifications alone, by implementers who did not see the originals, then
compiled with glslc. See `cleanroom/PROCESS.md`.

The numeric weight constants are carried over from the original shaders as
functional parameters — the reimplementation reproduces their behavior
bit-exactly. No third-party compiled shader artifacts are shipped.

## License

GPL-3.0. See `LICENSE`.

Originally written by xXJSONDeruloXx (https://github.com/xXJSONDeruloXx/bionic-fg)
with contributions from The412Banner; extended for GameNative by Utkarsh Dalal.
52 changes: 52 additions & 0 deletions cleanroom/PROCESS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Clean-room shader reimplementation process

Goal: replace the 51 runtime-traced SPIR-V shaders that were embedded in
`src/shaders_embedded.hpp` with independently written GLSL, compiled with our
own toolchain (glslc, NDK 27.3), so the shipped layer contains no third-party
compiled artifacts.

Scope: this reimplements the shader **code**. The numeric weight constants of
the networks are carried over from the originals as functional parameters, so
the result reproduces their behavior bit-exactly. Reimplementing those from
scratch would mean retraining the networks; the specifications document their
architecture in enough detail to do so.

## Separation

Two roles, never combined in one actor:

- **Specification team** ("dirty"): reads the disassembled originals
(spirv-dis output, kept outside this repository) and produces the functional
specifications in `specs/`. Specifications describe observable behavior:
descriptor interface, workgroup dimensions, the mathematics computed, and the
weight tables. They contain no code, no structure, and no naming from the
originals (the originals are stripped SPIR-V; they contain no names to copy).
Constants are extracted mechanically and machine-verified to round-trip
exactly through fp16.
- **Implementation team** ("clean"): receives the specification files and
previously written clean GLSL as style reference, and is instructed not to
consult the originals or their disassembly — writing original GLSL from the
spec alone. Each shader is compiled with glslc and validated against the
layer's pipeline interface.

Both roles were performed by AI agents in separate contexts: the implementing
agents' inputs contained the specification text and GLSL authoring conventions
only. The separation is procedural — enforced by the agents' instructions and
inputs, not by a technical barrier.

## Validation

- `glslc` compile + `spirv-val`.
- Interface check: bindings, storage image formats, and workgroup sizes are
verified mechanically against the spec by the build script.
- On-device A/B against the traced pipeline (visual parity + telemetry) before
the traced set is removed. Both models were verified on device: identical
graph shape and pass counts, equivalent frame pacing and throughput, and
motion-responsive flow telemetry.

## Layout

- `specs/NN[-family].md` — functional specs (one per shader family; family
members differ only by listed parameters)
- `glsl/shader_NN.comp` — clean implementations, one per shader index
- `build_embedded.py` — compiles glsl/ and regenerates the embedded header
102 changes: 102 additions & 0 deletions cleanroom/build_embedded.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Compile cleanroom/glsl/shader_NN.comp with glslc and regenerate an embedded
shader header with the same interface as src/shaders_embedded.hpp.

Usage: python3 cleanroom/build_embedded.py [--out src/shaders_embedded_clean.hpp]
Env: GLSLC / SPIRV_VAL override tool paths (default: NDK 27.3 shader-tools).
"""
import argparse
import os
import subprocess
import sys

REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
GLSL_DIR = os.path.join(REPO, "cleanroom", "glsl")
NDK_TOOLS = os.path.expanduser(
"~/Library/Android/sdk/ndk/27.3.13750724/shader-tools/darwin-x86_64")
GLSLC = os.environ.get("GLSLC", os.path.join(NDK_TOOLS, "glslc"))
SPIRV_VAL = os.environ.get("SPIRV_VAL", os.path.join(NDK_TOOLS, "spirv-val"))
REGISTRY_SLOTS = 56


def compile_one(src: str) -> bytes:
out = src[:-5] + ".spv"
subprocess.run(
[GLSLC, "-fshader-stage=comp", "--target-env=vulkan1.1", "-O",
src, "-o", out],
check=True)
subprocess.run([SPIRV_VAL, out], check=True)
with open(out, "rb") as f:
return f.read()


def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--out", default=os.path.join(REPO, "src", "shaders_embedded_clean.hpp"))
args = ap.parse_args()

blobs = {}
for name in sorted(os.listdir(GLSL_DIR)):
if not name.endswith(".comp"):
continue
idx = int(name.replace("shader_", "").replace(".comp", ""))
blobs[idx] = compile_one(os.path.join(GLSL_DIR, name))
print(f" shader_{idx:02d}: {len(blobs[idx])} bytes")

lines = []
lines.append("#pragma once")
lines.append("")
lines.append("// Generated by cleanroom/build_embedded.py from cleanroom/glsl/.")
lines.append("// Clean-room reimplementation; see cleanroom/PROCESS.md.")
lines.append("")
lines.append("#include <array>")
lines.append("#include <cstddef>")
lines.append("#include <cstdint>")
lines.append("")
lines.append("namespace bionic_fg::embedded {")
lines.append("")
lines.append("struct ShaderBlob {")
lines.append(" const char* name;")
lines.append(" const std::uint8_t* data;")
lines.append(" std::size_t size;")
lines.append("};")
lines.append("")
lines.append("inline constexpr std::uint32_t kSpirvMagic = 0x07230203u;")
lines.append("")
for idx in sorted(blobs):
data = blobs[idx]
lines.append(f"inline constexpr std::uint8_t shader_{idx:02d}_data[] = {{")
for i in range(0, len(data), 12):
chunk = ", ".join(f"0x{b:02x}" for b in data[i:i + 12])
lines.append(f" {chunk},")
lines[-1] = lines[-1].rstrip(",")
lines.append("};")
lines.append("")
lines.append(f"inline constexpr std::array<ShaderBlob, {REGISTRY_SLOTS}> kShaderRegistry = {{{{")
for idx in range(REGISTRY_SLOTS):
if idx in blobs:
lines.append(f' ShaderBlob{{"shader_{idx:02d}.spv", shader_{idx:02d}_data, sizeof(shader_{idx:02d}_data)}},')
else:
lines.append(f' ShaderBlob{{"shader_{idx:02d}.spv", nullptr, 0}},')
lines[-1] = lines[-1].rstrip(",")
lines.append("}};")
lines.append("")
lines.append("inline bool IsValidSpirv(const ShaderBlob& blob) {")
lines.append(" if (blob.size < sizeof(std::uint32_t)) return false;")
lines.append(" const std::uint32_t magic =")
lines.append(" static_cast<std::uint32_t>(blob.data[0]) |")
lines.append(" (static_cast<std::uint32_t>(blob.data[1]) << 8u) |")
lines.append(" (static_cast<std::uint32_t>(blob.data[2]) << 16u) |")
lines.append(" (static_cast<std::uint32_t>(blob.data[3]) << 24u);")
lines.append(" return magic == kSpirvMagic;")
lines.append("}")
lines.append("")
lines.append("} // namespace bionic_fg::embedded")
with open(args.out, "w") as f:
f.write("\n".join(lines) + "\n")
print(f"wrote {args.out} with {len(blobs)}/{REGISTRY_SLOTS} populated slots")
return 0


if __name__ == "__main__":
sys.exit(main())
82 changes: 82 additions & 0 deletions cleanroom/glsl/shader_03.comp
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#version 460
#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in;

layout(set = 0, binding = 0, std140) uniform Params {
float srcCoordScale;
float unused0;
float unused1;
} params;

layout(set = 0, binding = 32) uniform sampler2D srcTex;

layout(set = 0, binding = 48, r8) uniform writeonly image2D pyr0;
layout(set = 0, binding = 49, r8) uniform writeonly image2D pyr1;
layout(set = 0, binding = 50, r8) uniform writeonly image2D pyr2;
layout(set = 0, binding = 51, r8) uniform writeonly image2D pyr3;
layout(set = 0, binding = 52, r8) uniform writeonly image2D pyr4;
layout(set = 0, binding = 53, r8) uniform writeonly image2D pyr5;
layout(set = 0, binding = 54, r8) uniform writeonly image2D pyr6;

shared float16_t tile[32][32];

const f16vec3 LUMA_W = f16vec3(0.298828125hf, 0.5869140625hf, 0.11395263671875hf);

float16_t sampleLuma(uvec2 texel, vec2 srcSize) {
vec2 uv = (vec2(texel) + 0.5) * params.srcCoordScale / srcSize;
return dot(f16vec3(textureLod(srcTex, uv, 0.0).rgb), LUMA_W);
}

#define REDUCE_ROUND(N, dstImg) \
barrier(); \
if (lid.x < N && lid.y < N) { \
float16_t a = tile[2u * lid.x][2u * lid.y]; \
float16_t b = tile[2u * lid.x + 1u][2u * lid.y]; \
float16_t c = tile[2u * lid.x][2u * lid.y + 1u]; \
float16_t d = tile[2u * lid.x + 1u][2u * lid.y + 1u]; \
float16_t m = 0.25hf * (((a + b) + c) + d); \
ivec2 oc = ivec2(wg * N + lid); \
if (all(lessThan(oc, imageSize(dstImg)))) \
imageStore(dstImg, oc, vec4(float(m))); \
tile[lid.x][lid.y] = m; \
}

void main() {
uvec2 wg = gl_WorkGroupID.xy;
uvec2 lid = gl_LocalInvocationID.xy;
vec2 srcSize = vec2(textureSize(srcTex, 0));

for (uint j = 0u; j < 2u; ++j) {
for (uint i = 0u; i < 2u; ++i) {
uvec2 p = wg * 32u + lid * 2u + uvec2(i, j);
uvec2 q = p * 2u;

float16_t l00 = sampleLuma(q + uvec2(0u, 0u), srcSize);
float16_t l10 = sampleLuma(q + uvec2(1u, 0u), srcSize);
float16_t l11 = sampleLuma(q + uvec2(1u, 1u), srcSize);
float16_t l01 = sampleLuma(q + uvec2(0u, 1u), srcSize);

if (all(lessThan(ivec2(q + uvec2(0u, 0u)), imageSize(pyr0))))
imageStore(pyr0, ivec2(q + uvec2(0u, 0u)), vec4(float(l00)));
if (all(lessThan(ivec2(q + uvec2(1u, 0u)), imageSize(pyr0))))
imageStore(pyr0, ivec2(q + uvec2(1u, 0u)), vec4(float(l10)));
if (all(lessThan(ivec2(q + uvec2(1u, 1u)), imageSize(pyr0))))
imageStore(pyr0, ivec2(q + uvec2(1u, 1u)), vec4(float(l11)));
if (all(lessThan(ivec2(q + uvec2(0u, 1u)), imageSize(pyr0))))
imageStore(pyr0, ivec2(q + uvec2(0u, 1u)), vec4(float(l01)));

float16_t m = 0.25hf * (((l00 + l10) + l11) + l01);
if (all(lessThan(ivec2(p), imageSize(pyr1))))
imageStore(pyr1, ivec2(p), vec4(float(m)));

tile[lid.x * 2u + i][lid.y * 2u + j] = m;
}
}

REDUCE_ROUND(16u, pyr2)
REDUCE_ROUND(8u, pyr3)
REDUCE_ROUND(4u, pyr4)
REDUCE_ROUND(2u, pyr5)
REDUCE_ROUND(1u, pyr6)
}
56 changes: 56 additions & 0 deletions cleanroom/glsl/shader_04.comp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#version 460

layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in;

layout(set = 0, binding = 0, std140) uniform Params {
float flowScale;
float alpha;
float epsilon; // present in layout, never read
} u;

layout(set = 0, binding = 32) uniform sampler2D prevFrame;
layout(set = 0, binding = 33) uniform sampler2D currFrame;
layout(set = 0, binding = 34) uniform sampler2D flowA;
layout(set = 0, binding = 35) uniform sampler2D flowB;
layout(set = 0, binding = 36) uniform sampler2D confidenceTex;
layout(set = 0, binding = 48) uniform writeonly image2D outFrame;

void main() {
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
if (any(greaterThanEqual(p, imageSize(outFrame)))) {
return;
}

vec2 S = vec2(textureSize(prevFrame, 0));
vec2 pc = vec2(gl_GlobalInvocationID.xy) + vec2(0.5, 0.5);
vec2 uv = pc / S;

vec4 fA = textureLod(flowA, uv, 0.0) * u.flowScale;
vec4 fB = textureLod(flowB, uv, 0.0) * u.flowScale;
float t = u.alpha;
float omt = 1.0 - t;
float a = 2.0 * t;
float b = 2.0 * omt;

vec2 uv1 = (pc + a * fA.xy) / S;
vec2 uv2 = (pc + b * fA.zw) / S;
vec2 uv3 = (pc + a * fB.xy) / S;
vec2 uv4 = (pc + b * fB.zw) / S;

vec4 c = vec4(textureLod(confidenceTex, uv1, 0.0).x,
textureLod(confidenceTex, uv2, 0.0).y,
textureLod(confidenceTex, uv3, 0.0).z,
textureLod(confidenceTex, uv4, 0.0).w);

vec4 e = exp(c);
vec4 w = e / (((e.x + e.y) + e.z) + e.w);

vec4 num = ((textureLod(prevFrame, uv1, 0.0) * omt) * w.x)
+ ((textureLod(currFrame, uv2, 0.0) * t) * w.y)
+ ((textureLod(prevFrame, uv3, 0.0) * omt) * w.z)
+ ((textureLod(currFrame, uv4, 0.0) * t) * w.w);

float den = omt * w.x + t * w.y + omt * w.z + t * w.w + 1e-8;

imageStore(outFrame, p, num / vec4(den));
}
50 changes: 50 additions & 0 deletions cleanroom/glsl/shader_05.comp
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#version 460
#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in;

layout(set = 0, binding = 32) uniform sampler2D srcTex;
layout(set = 0, binding = 48, rgba8) uniform writeonly image2D dst;

const float16_t IN_MEAN = 0.2080078125hf;
const float16_t IN_SCALE = 1.49609375hf;
const float16_t IN_BIAS = 0.76953125hf;

const f16vec4 W1 = f16vec4(0.2138671875hf, -0.061798095703125hf, -0.2091064453125hf, -1.0029296875hf);
const f16vec4 W2 = f16vec4(0.343994140625hf, -0.04486083984375hf, -0.327880859375hf, -0.62060546875hf);
const f16vec4 W3 = f16vec4(0.1617431640625hf, -0.023727416992188hf, -0.16748046875hf, -0.256103515625hf);
const f16vec4 W4 = f16vec4(0.34033203125hf, -0.0031566619873047hf, -0.315673828125hf, 0.0011692047119141hf);
const f16vec4 W5 = f16vec4(0.70654296875hf, 0.04998779296875hf, -0.666015625hf, 0.1534423828125hf);
const f16vec4 W6 = f16vec4(0.441650390625hf, 0.060455322265625hf, -0.435546875hf, 0.2081298828125hf);
const f16vec4 W7 = f16vec4(0.14453125hf, 0.0061836242675781hf, -0.154296875hf, -0.07080078125hf);
const f16vec4 W8 = f16vec4(0.44677734375hf, 0.05987548828125hf, -0.418701171875hf, 0.0965576171875hf);
const f16vec4 W9 = f16vec4(0.352783203125hf, 0.047119140625hf, -0.324951171875hf, 0.1416015625hf);

const f16vec4 OUT_B1 = f16vec4(2.408203125hf, 0.069763183593750hf, -2.306640625hf, -1.0205078125hf);
const f16vec4 OUT_G = f16vec4(0.19494628906hf, 0.29223632812hf, 0.43359375hf, 0.6943359375hf);
const f16vec4 OUT_B2 = f16vec4(0.08825683594hf, -0.25756835938hf, -0.18615722656hf, -0.52392578125hf);

#define TAP(ox, oy) \
((float16_t(textureLodOffset(srcTex, uv, 0.0, ivec2(ox, oy)).r) - IN_MEAN) * IN_SCALE + IN_BIAS)

void main() {
ivec2 gid = ivec2(gl_GlobalInvocationID.xy);
if (any(greaterThanEqual(gid, imageSize(dst))))
return;

vec2 srcSize = vec2(textureSize(srcTex, 0));
vec2 uv = (vec2(gid * 2) + 0.5) / srcSize;

f16vec4 acc = W1 * TAP(-1, -1);
acc += W2 * TAP(-1, 0);
acc += W3 * TAP(-1, 1);
acc += W4 * TAP(0, -1);
acc += W5 * TAP(0, 0);
acc += W6 * TAP(0, 1);
acc += W7 * TAP(1, -1);
acc += W8 * TAP(1, 0);
acc += W9 * TAP(1, 1);

f16vec4 res = (acc - OUT_B1) * OUT_G + OUT_B2;
imageStore(dst, gid, vec4(res));
}
Loading