Skip to content
Closed
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
88 changes: 88 additions & 0 deletions LICENSE-ROXAS2712.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Roxas2712 — Fork Contributions

Version 1.0 · Effective 25 October 2026

Copyright © 2026 Roxas2712, to the extent of rights held.
All rights reserved except as expressly granted below or already granted.

## 1. Scope and ownership

This notice applies only to original contributions made by Roxas2712
(GitHub: Roxas2712) to `Roxas2712/Gen2Recomped`, including original additions
within modified files, insofar as he owns the relevant rights. Git history,
pull-request diffs and source provenance identify contributions; a commit
author or a whole-file change alone does not establish ownership of every
part of that file.

It does not claim ownership of Cedric's work, Gen1Recomp / BOIS CLUB GAMES
work, other contributors' work, third-party dependencies, or ROM-derived
content. Their applicable licences and notices remain in force. It does not
grant permission to distribute the complete combined fork where another
rights holder's permission is required.

## 2. General permission and reserved rights

For contributions covered by this notice and not already licensed more
broadly, you may read and study the published source and make private,
non-commercial modifications and builds for your own use. This grants no
general permission to redistribute, sublicense, sell, or offer those
contributions as a hosted service. Other uses require Roxas2712's
written permission, except for the specific permission below and rights
available independently under applicable law.

Keep the relevant copyright, licence and attribution notices. Do not
represent Roxas2712's contributions as exclusively authored by someone
else. Public source availability alone does not put them in the public
domain or grant an open-source licence.

## 3. Specific permission for Cedric / UNDERdecoded

Roxas2712 expressly grants Cedric (Ced; GitHub: UNDERdecoded /
UNDERdecodedHD) a non-exclusive, worldwide, perpetual, royalty-free
permission to use, reproduce, integrate, modify and distribute the covered
contributions that Roxas2712 submits or expressly offers through pull
requests to `UNDERdecoded/Gen2Recomped`, for Cedric's Gen2Recomped /
RetroRecompiled project. This includes its renamed continuation maintained
by Cedric, its official builds, and its commercial operation.

Cedric may license the incorporated contributions to recipients as part of
that project, subject to preserving this permission's applicable credit and
notice requirements. This permission is not a copyright assignment, is not
exclusive, and does not cover unpublished work, separate repositories,
ModCompass service operation, credentials, or content not offered in those
pull requests. Roxas2712 retains his own rights to his contributions.

For uses relying on this permission:

- retain the relevant source copyright and licence notices;
- give visible credit in the project's README or equivalent public project
page, and in the application's existing About / Credits view, if present;
- use at least the following credit, or a substantially equivalent one:

> Contributions by Roxas2712 —
> https://github.com/Roxas2712

A maintained consolidated contributor credit is sufficient; a separate
credit on every screen or for every change is not required. Adapted
contributions must not be attributed exclusively to another author. Other
contributors' credits must also remain intact.

## 4. Earlier licences and permissions remain effective

This notice does not revoke, narrow, or retrospectively add conditions to
any permission already validly granted, including MIT / GPL grants,
separate agreements, or permissions arising from previously submitted
pull requests. Where an earlier grant permits a use independently of this
notice, its existing terms govern that use. For such earlier uses, the
visible credit above is requested to the extent it is not already required.

For subsequent contributions, this is the default statement of Roxas2712's own reserved rights and the specific PR permission, subject to any
applicable existing licence obligations or separately agreed terms. It
does not change another rights holder's licence or guarantee compatibility
of a particular combination of code.

## 5. No warranty

To the extent permitted by applicable law, these contributions are provided
as is, without warranty. No support obligation, endorsement, or rights to
third-party code, game assets, names, or trademarks are granted.
105 changes: 105 additions & 0 deletions docs/metal-shader-branches.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Bounded Metal shader branch specialization

Large world programs can keep expensive lighting/material branches dynamic even
when their scalar controls stay at zero or one. On Apple-silicon macOS with
LÖVE 12/Metal, the native compiler adapter specializes eligible `if` reads of
positive float zero/one and booleans. It preserves declarations, arithmetic,
source artwork, render resolution and post-processing. It uses no mod names or
private shader snippets. Other backends, smaller programs and unsupported
source/value shapes keep their original programs.

The caller retains the real original Shader userdata and interface. Every
successful `send`/`sendColor` updates that C shader first; original errors,
return arity and nil values remain intact. Bounded immutable snapshots preserve
partial uniform-array tails and per-send matrix layout. Eager variant writes
preserve batch ordering and release replaced C texture references. Missing or
released replay resources, mutable Data, buffers and unknown values fall back
to the original. A later foreign Shader-method owner also disables optimization.

An unnamed Channel holds the original C shader without keeping its Lua userdata
alive. Retirement queries the real native graphics-stack depth, including raw C
push/pop calls. Disabled/orphan records retire only at an empty-stack boundary;
hidden native stack frames keep their mappings and C leases. Public `getShader`
returns the original live interface, including native reconstruction after
explicit release. No public reference-count API is assumed.

Sampler declarations remain visible to the Metal stage linker behind a private
zero-valued guard. Vertex-only declarations stay in their stage; unsupported
syntax or compilation failure retains the original program. The optional canvas
compatibility adapter provides its preceding constructor as `compileNative`:
derived programs must not have their already admitted clip convention analyzed
again after their forwarding entry is modified. An older installed adapter
without this seam leaves specialization disabled. No producer macro is injected.

Limits: 64–256 KiB per eligible source, 16 live sources, 2 MiB combined source,
16 variants per source, 64 live variants, 256 observed uniforms and 32,768
snapshot nodes per source. Source shadowing/macros and unsupported shapes stay
conservative. `POKEPORT_SHADER_BRANCHES=0` restores the original compiler path.

## Native qualification

Frozen native source `a5c7462ef0`, committed compiler `4344eb05` on fork main
`0191734c`, unchanged private Origami 1.0.9 `ae59c77c`: baseline `562ad4a1f9`
versus default-enabled native adapter `de603b9839`. Both use actual Metal on
M3/16 GB, High, 1024×768 scene/window, active FXAA, render scale one,
supersampling off and native VSync off. No QA Shader prototype is installed.

The QA input adapter replaces only the deterministic driver's direct Game
update call in memory with the real `PlatformHooks.update(Game, 1/60)` owner.
Engine and mod files remain unchanged. The complete outer mod hook chain runs
exactly once per measured step; native renderer serial also advances once.
Each view measures 120 ordinary directional-input frames after settling and a
separate collection/recovery test. The player occupies 17 / 13 / 13 distinct
native positions in open/Third/First, and scene clocks match across both runs.
The shared hardware lease covers source setup through final cleanup.

| View | Original mean ms | Adapter mean ms | Adapter p95 ms | Adapter max ms |
| --- | ---: | ---: | ---: | ---: |
| open | 6.77 | 6.21 | 10.61 | 12.07 |
| third | 21.29 | 11.43 | 12.64 | 14.24 |
| first | 20.07 | 11.85 | 12.89 | 15.65 |

All three complete scene captures are pixel-exact, excluding only the changing
FPS HUD (`x >= 890 && y < 56`). Twelve actual variants are used, with no
compiler/replay failures, unsupported values or fallback draws. The original
failed native integration (`a67197449f`) double-adapted clip space; its failure
is preserved and corrected through the constructor seam, not an image flip.

Actual default-policy GPU fixtures `586e40dc29` (composed) and `efda7ed3c9`
(standalone upstream implementation) each pass 73 checks, including spatial projection,
real Shader identity, multiple/nil returns from a prior owner, partial arrays,
matrix layout, mutable color inputs, sampler release, raw native stack frames,
Shader release/GC, Data fallback, source caps and later foreign method ownership.
Both the deliberate wrong-branch control and the separate double-projection
negative `189f31ddff` are detected. Independent public CPU qualification
`c303516873` passes 38 source / 40 policy / 128 canvas-seam assertions. Focused native engine,
orientation, JIT, map, follower, options, pacing and Crystal VRAM checks pass in
the same frozen composition.

These are internal rendering observations while the console is locked. They
are not OS-window, interactive 60-FPS, outdoor, full-game or all-platform
acceptance. Cold compilation is synchronous. Initial control `55614c8cc0` accumulated
7.21 seconds across twelve first-used variants; the full-frame repeat records
1.92 seconds. Both are outside the steady samples; no cache-cause attribution
is claimed.
First-use hitches remain a limitation; there is no asynchronous-compilation
claim. The existing overworld-battle Card fails in both baseline and adapter
runs; this change does not qualify complete battle support. Player profiles and saves are not used. QA enforces 20 GiB free, above
the requested 8 GiB minimum.

## Repeating the API fixture

`tests/native_shader` is a ROM-free LÖVE fixture. Its engine root comes from
`SHADER_TEST_ENGINE` (default: the repository containing the fixture); a frozen
runner must set it to the exact copied source. Use a disposable QA identity,
the project's managed storage/hardware runner and Metal. Set
`QA_NATIVE_SHADER_ADAPTER=1`; `QA_SHADER_MULTIPLE_RETURNS=1` additionally exercises
an existing method owner. `QA_SHADER_WRONG_COMPILER=1` deliberately re-enters the
actual canvas adapter and must fail the spatial pixel comparison. That negative
requires the canvas adapter; the standalone compiler fixture also runs without
it. Never use a player identity for this fixture.

Contributions by Roxas2712 — https://github.com/Roxas2712.
See [the contribution terms](../LICENSE-ROXAS2712.md) for Cedric / UNDERdecoded's
specific incorporation permission and applicable credit requirements. Existing
notices remain intact.
13 changes: 13 additions & 0 deletions docs/qa/native-foundation/shader-planning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Bounded work for large shader planning

The old branch planner searched the entire program for each uniform, and its shadow check repeatedly searched identifier suffixes. Its rewriter rebuilt the complete program for each condition. Both now index identifiers and assemble changed source spans in a single pass. Existing conservative shadow, member, macro, qualifier and literal guards remain intact. Rendering resolution, shader arithmetic, uniforms, material branches and post-processing are unchanged.

On two actual world shader inputs (98,913 and 96,648 bytes, 23/22 admitted flags, 273/266 conditions), the exact predecessor and the new planner produce equal plans and byte-identical rewrites for all-zero, all-one and mixed flag banks. Seven measured CPU repetitions after two warm-ups give median planning 418.659 to 5.157 ms and 402.507 to 4.702 ms. Rewrite medians are 7.104 to 0.193 ms and 6.267 to 0.185 ms. These are isolated CPU planning measurements, not a claimed frame-rate multiplier. No private shader text is published.

The planner fixture passes 48 checks, including exact output for a generated nested large program and unchanged shadow/macro rejection. Platform/passthrough policy checks pass 40 assertions. The first optimization composition passed 246 frozen CPU groups, and both the fork and older independent public implementation passed 73 real Metal Shader-contract checks. The final token-index pass passes the actual-program byte-equivalence comparison and both independent real Metal contract repetitions (73 checks each). The later composed current-main regression, including the door descriptor integration, passes 250 frozen CPU groups. The public implementation retains its original upstream controllers.

A separate native FireRed water-view diagnostic, with the first optimization, observed an 844-ms frame (840 ms in drawing, under 1 ms in presentation). Native compilation remains synchronous and dominated the sampled slow setup. A 120-frame warm interval without the Lua sampler averaged 18.309 ms (about 55 FPS), p95 27.346 ms, maximum 42.186 ms. It used High quality, 1024x768 and a 512x384 scene reflection target. No capture, texture readback, file write or forced collection ran in either frame window. This short fixed-camera diagnostic is not stable 60-FPS, rain, Third/First-person, interactive-window or full-game acceptance. Existing first-use driver compilation remains unresolved.

QA used owned profiles, preserved private source and player saves, and enforced 20 GiB free (above the requested 8 GiB reserve).

Contributions by Roxas2712 — https://github.com/Roxas2712. See LICENSE-ROXAS2712.md for the specific integration/distribution permission for Cedric / UNDERdecoded with visible credit. Existing upstream/third-party licences and rights remain intact.
1 change: 1 addition & 0 deletions main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ end
function love.load(args)
BootTrace.mark("love.load enter")
GraphicsStack.install()
require("src.render.ShaderSpecialization").install()
pcall(function() require('src.core.SwitchAudioCheck').run() end)

-- Teach SDL about the pads its built-in database misses (Switch Pro over
Expand Down
2 changes: 2 additions & 0 deletions src/core/Performance.lua
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
-- tests, the same way src/core/GameVersion.lua does.

local Performance = {}
-- Bounded Metal branch specialization; original programs own every fallback.
Performance.shaderBranchSpecialization = true

-- Option-row order (auto first, then most to least capable).
Performance.TIERS = { "auto", "high", "balanced", "low" }
Expand Down
136 changes: 136 additions & 0 deletions src/render/ShaderBranches.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
-- Copyright (c) 2026 Roxas2712. See LICENSE-ROXAS2712.md.
-- Pure source planning for an optional compiler optimization. This module does
-- not change graphics state. Unknown syntax keeps its original runtime branch.
local B={}
local scalar={float=true,bool=true}
local qualifier={highp=true,mediump=true,lowp=true,LOVE_HIGHP_OR_MEDIUMP=true}
local function blank(text)return(text:gsub('[^\n]',' '))end
local function uncomment(source)
local pieces,at={},1
while at<=#source do
local first=source:find('/',at,true)
if not first then pieces[#pieces+1]=source:sub(at);break end
pieces[#pieces+1]=source:sub(at,first-1)
local pair=source:sub(first,first+1);local finish
if pair=='//'then finish=source:find('\n',first+2,true)or(#source+1)
elseif pair=='/*'then local last=source:find('*/',first+2,true);if not last then return end;finish=last+2 end
if finish then pieces[#pieces+1]=blank(source:sub(first,finish-1));at=finish
else pieces[#pieces+1]='/';at=first+1 end
end
return table.concat(pieces)
end
local function withoutDirectives(clean)
local continued=false;local out={}
for line in(clean..'\n'):gmatch('(.-)\n')do
local directive=continued or line:match('^%s*#')~=nil
continued=directive and line:match('\\%s*$')~=nil
out[#out+1]=directive and blank(line)or line
end
return table.concat(out,'\n')
end
function B.plan(source)
if type(source)~='string'then return nil,'source is not text'end
local clean=uncomment(source);if not clean then return nil,'unfinished comment'end
local code=withoutDirectives(clean)
local declarations,names={},{}
local at=1
while true do
local first,last,body=code:find('%f[%w_]uniform%s+([^;{}]+);',at)
if not first then break end
local q,kind,name=body:match('^([%w_]+)%s+([%w_]+)%s+([%a_][%w_]*)%s*$')
if not(q and qualifier[q])then kind,name=body:match('^([%w_]+)%s+([%a_][%w_]*)%s*$')end
if scalar[kind]and name and not name:match('^love_')and not name:match('^pokeport')then
if names[name]and names[name]~=kind then return nil,'conflicting uniform kinds'end
names[name]=kind;declarations[#declarations+1]={first,last}
end
at=last+1
end
local conditions={};at=1
while true do
local first,last=code:find('%f[%w_]if%f[^%w_]%s*%(',at)
if not first then break end
local depth,finish=1,last+1
while finish<=#clean and depth>0 do
local c=clean:sub(finish,finish)
if c=='('then depth=depth+1 elseif c==')'then depth=depth-1 end
finish=finish+1
end
if depth~=0 then return nil,'unfinished branch condition'end
conditions[#conditions+1]={last,finish-1};at=finish
end
-- Uniform arithmetic remains dynamic. Reject possible shadow declarations,
-- member names and macros before substituting any read in an if condition.
-- A comma may introduce another local declaration; conservatively reject
-- that spelling even when it is merely a function argument.
local pieces,previous={},1
for _,r in ipairs(declarations)do
pieces[#pieces+1]=clean:sub(previous,r[1]-1)
pieces[#pieces+1]=blank(clean:sub(r[1],r[2]));previous=r[2]+1
end
pieces[#pieces+1]=clean:sub(previous)
local outside=table.concat(pieces)
local used={}
for _,r in ipairs(conditions)do
for name in clean:sub(r[1],r[2]):gmatch('%f[%w_][%w_]+%f[^%w_]')do used[name]=true end
end
local rejected={};local pos=1
while true do
local a,b=outside:find('%f[%w_][%w_]+%f[^%w_]',pos);if not a then break end
local name=outside:sub(a,b)
if names[name]and not rejected[name]then
-- Inspect the predecessor of this actual token. Searching an unanchored
-- word+whitespace pattern rescans every suffix of long GLSL identifiers
-- for every candidate, even when the candidate is absent there.
local before=a-1
while before>0 and outside:sub(before,before):match('%s')do before=before-1 end
local predecessor=outside:sub(before,before)
if predecessor:match('[,%.]')or before<a-1 and predecessor:match('[%w_]')
or code:sub(a,b)~=clean:sub(a,b)then rejected[name]=true end
end
pos=b+1
end
local order={}
for name in pairs(names)do
if rejected[name]then names[name]=nil
else
if used[name]then order[#order+1]=name else names[name]=nil end
end
end
table.sort(order)
return{source=source,clean=clean,kinds=names,order=order,conditions=conditions}
end
function B.literal(kind,value)
if kind=='bool'and type(value)=='boolean'then return tostring(value)end
if kind=='float'and type(value)=='number'then
if value==1 then return'1.0'end
if value==0 and 1/value==math.huge then return'0.0'end
end
end
function B.key(plan,values)
local constants,key={},{}
for _,name in ipairs(plan.order)do
local literal=B.literal(plan.kinds[name],values[name])
if literal then constants[name]=literal;key[#key+1]=name..'='..literal end
end
return constants,table.concat(key,';'),#key
end
function B.rewrite(plan,constants)
local source,pieces,previous=plan.source,{},1
for _,range in ipairs(plan.conditions)do
local first,last=range[1],range[2]
local lexical=plan.clean:sub(first,last);local at=1
while true do
local a,b=lexical:find('%f[%w_][%w_]+%f[^%w_]',at);if not a then break end
local name=lexical:sub(a,b);local value=plan.kinds[name]and constants[name]
if value then
assert(value=='0.0'or value=='1.0'or value=='true'or value=='false','unqualified literal')
local start,finish=first+a-1,first+b-1
pieces[#pieces+1]=source:sub(previous,start-1);pieces[#pieces+1]=value;previous=finish+1
end
at=b+1
end
end
pieces[#pieces+1]=source:sub(previous)
return table.concat(pieces)
end
return B
Loading