Skip to content
Merged
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
13 changes: 11 additions & 2 deletions docs/performance-config-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ strategy is "curate one upstream" rather than "ingest any config from
GameBanana." Read this before touching `performanceConfig.ts`,
`performanceConfigData.ts`, or building the planned manifest/preset UI.

Status: six selectable presets shipped, generated from pinned upstream commits.
Status: seven selectable presets shipped, generated from pinned upstream commits.
The research that drove the scope decision is recorded below and still holds;
"What shipped" records where the delivered design differs from the plan it
replaced.
Expand Down Expand Up @@ -98,7 +98,7 @@ intersection is extractable and safe; everything beyond it is author-specific.

## What shipped

Six presets selected by id: `sqooky-default` (balanced, default), `sqooky-testing`
Seven presets selected by id: `sqooky-default` (balanced, default), `eskay` (light), `sqooky-testing`
(preview), `boot-max-fps` (aggressive), `kaizu-min-spec` (potato), `optilock-fps`
(competitive), `optilock-max` (maximum). Each is a section/key diff of a pinned
upstream `gameinfo.gi` against the stock baseline, generated into
Expand Down Expand Up @@ -133,6 +133,15 @@ edit. The latter access-violates in the current Deadlock build when combined
with boot's convar body; live launch bisection confirmed the full boot preset
stays running without that one edit. No other bundled preset currently sets it.

`MaterialSystem2/RenderModes` is excluded as a whole section because it is a
list (seven `game` entries), not key/value pairs. The parser keeps the last
duplicate and the patcher edits every occurrence of a key, so configs written
before Valve added `ShadowSilhouette` (boot, OptiLock) diffed to
`game "FrontDepth"` and collapsed all seven render modes into one. Track-latest
users on v1.30.x hit this at runtime; `latestAsPreset` re-applies today's
section exclusions to cached bodies so the fix reaches them without a refetch.
Any future list-valued section needs the same treatment.

### Marker grammar

The block header is the authoritative record of what is in the file; the sidecar
Expand Down
11 changes: 10 additions & 1 deletion electron/main/services/performanceConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
removePerformanceConfig,
resetPerformanceConfigOverrides,
} from './performanceConfig';
import { getPreset } from './performanceConfigData';
import { CLASSIFICATION, getPreset } from './performanceConfigData';
import { pathExcluded } from './performancePresetGen';

const STOCK = readFileSync(join(__dirname, '__fixtures__/stock-gameinfo.gi'), 'utf-8');
const STOCK_CRLF = STOCK.split('\n').join('\r\n');
Expand Down Expand Up @@ -176,6 +177,14 @@ describe('preset version history', () => {
}
});

// `perf:presets --check` is not in CI (it needs the network), so this is
// the offline guard against a regenerated body editing an excluded
// section, e.g. the list-valued RenderModes.
it.each(ALL)('%s v%s: no section op lands in an excluded section', (id, version) => {
const ops = getPreset(id, version).sectionOps;
expect(ops.filter((op) => pathExcluded(op.path, CLASSIFICATION.excludeSections))).toEqual([]);
});

it.each(ALL)('%s v%s: apply then remove restores the file byte for byte', (id, version) => {
expect(applyPerformanceConfig(gameRoot, { presetId: id, version }).state).toBe('applied');
expect(read()).not.toBe(STOCK);
Expand Down
360 changes: 339 additions & 21 deletions electron/main/services/performanceConfigData.ts

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions electron/main/services/performanceLatestCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,28 @@ describe('buildLatestRelease gates', () => {
}
});

it('leaves the list-valued RenderModes section alone', () => {
// Stock lists seven `game` render modes; a config written before Valve
// added ShadowSilhouette ends on FrontDepth, which used to diff to a
// `game "FrontDepth"` op that the patcher wrote over every mode.
const withModes = (text: string, modes: string[]) =>
text.replace(
/\n\}\n$/,
`\n\tMaterialSystem2\n\t{\n\t\tRenderModes\n\t\t{\n${modes
.map((m) => `\t\t\tgame "${m}"`)
.join('\n')}\n\t\t}\n\t}\n}\n`
);
const result = buildLatestRelease(
input({
baselineText: withModes(baselineText(), ['Default', 'FrontDepth', 'ShadowSilhouette']),
configText: withModes(configText(), ['Default', 'FrontDepth']),
})
);
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.release.sectionOps.some((op) => op.path[0] === 'MaterialSystem2')).toBe(false);
});

it('refuses implausible texts', () => {
expect(buildLatestRelease(input({ configText: 'tiny' })).ok).toBe(false);
expect(
Expand Down Expand Up @@ -269,6 +291,18 @@ describe('cache round-trip and resolution', () => {
expect(preset.convars).toEqual(r.convars);
expect(preset.optIn[0].group).toBe('visibility');
});

it('drops ops an older build cached for a section that is now excluded', () => {
const r = release();
const renderModes = { path: ['MaterialSystem2', 'RenderModes'], key: 'game', value: 'FrontDepth' };
writeLatestCache(dir, {
byPreset: { [r.presetId]: { ...r, sectionOps: [...r.sectionOps, renderModes] } },
checkedAt: {},
history: {},
});
const preset = resolveCachedPreset(dir, r.presetId, 'latest');
expect(preset?.sectionOps).toEqual(r.sectionOps);
});
});

// ---------------------------------------------------------------------------
Expand Down
8 changes: 7 additions & 1 deletion electron/main/services/performanceLatestCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
import {
generatePresetBody,
parseConfig,
pathExcluded,
validateGameinfoText,
validateGeneratedBody,
} from './performancePresetGen';
Expand Down Expand Up @@ -249,7 +250,12 @@ export function latestAsPreset(release: LatestRelease): PerformancePreset {
refKind: release.refKind,
commit: release.commit,
},
sectionOps: release.sectionOps,
// A cached body was classified by whichever Grimoire fetched it.
// Today's section exclusions still apply: RenderModes was excluded
// after older builds had cached bodies that wrote to it.
sectionOps: release.sectionOps.filter(
(op) => !pathExcluded(op.path, CLASSIFICATION.excludeSections)
),
convars: release.convars,
// Groups come from the same manifest tables the bundled data was
// generated with, so the cast is between two views of one source.
Expand Down
2 changes: 1 addition & 1 deletion electron/main/services/performancePresetGen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export function parseConfig(text: string): ParsedConfig {
// Diff + classification
// ---------------------------------------------------------------------------

function pathExcluded(path: string[], excludedSections: ReadonlyArray<readonly string[]>): boolean {
export function pathExcluded(path: string[], excludedSections: ReadonlyArray<readonly string[]>): boolean {
return excludedSections.some((ex) => ex.every((seg, i) => path[i] === seg));
}

Expand Down
47 changes: 41 additions & 6 deletions scripts/gen-performance-presets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,15 @@ function fail(msg) {
// Fetch
// ---------------------------------------------------------------------------

async function fetchAt(repo, commit, path) {
// `optional` turns a 404 into null instead of a failure. Only --refresh uses
// it, to record a preset whose file did not exist yet at an older release.
async function fetchAt(repo, commit, path, { optional = false } = {}) {
const url = `https://raw.githubusercontent.com/${repo}/${commit}/${path
.split('/')
.map(encodeURIComponent)
.join('/')}`;
const res = await fetch(url);
if (optional && res.status === 404) return null;
if (!res.ok) fail(`Fetch failed (${res.status}) for ${repo}@${commit.slice(0, 8)} ${path}`);
return res.text();
}
Expand Down Expand Up @@ -514,15 +517,38 @@ async function main() {
// release changes relative to today's stock file", which is the only
// thing that can be patched into the file the user actually has.
const releases = [];
let absentAt = null;
for (const release of source.releases) {
const expected = entry.sha256[release.ref];
if (!expected) {
if (expected === undefined) {
fail(
`Preset ${entry.id} has no sha256 for release ${release.ref}.\n` +
` Every release listed on source "${entry.source}" needs one, or the\n` +
` content gate silently stops covering that version.`
);
}
// An explicit null records that the file did not exist upstream
// yet at this release (a config added after the source's older
// bundled releases). Only a trailing run of older releases may be
// absent: a file missing at the newest release was moved or
// deleted, which needs a pathByRef, not a silent skip.
if (expected === null) {
if (!releases.length) {
fail(
`Preset ${entry.id} has no file at ${release.ref}, the newest release of "${entry.source}".\n` +
` Upstream moved or deleted ${pathFor(entry, release.ref)}. Point "path" at its new\n` +
` location (with a pathByRef for older releases) or drop the preset.`
);
}
absentAt ??= release.ref;
continue;
}
if (absentAt) {
fail(
`Preset ${entry.id} is absent at ${absentAt} but present at the older ${release.ref}.\n` +
` Only releases older than the preset's first appearance may be null.`
);
}
const path = pathFor(entry, release.ref);
const text = await fetchAt(source.repo, release.commit, path);
verify(text, expected, `${entry.id} (${path}) at ${release.ref}`);
Expand Down Expand Up @@ -613,7 +639,7 @@ async function main() {
}
}

// The opt-in list is only as good as the last hand-audit of six upstream
// The opt-in list is only as good as the last hand-audit of the upstream
// files. The pattern check (now inside generatePresetBody) is what makes it
// hold across a `--refresh`: a key that looks like a visibility or framing
// setting has to be classified on purpose before it can ship in a preset
Expand Down Expand Up @@ -736,9 +762,18 @@ async function refreshPins(manifest, target) {
const source = manifest.sources[entry.source];
const hashes = {};
for (const release of source.releases) {
hashes[release.ref] = sha256(
await fetchAt(source.repo, release.commit, pathFor(entry, release.ref))
);
const text = await fetchAt(source.repo, release.commit, pathFor(entry, release.ref), {
optional: true,
});
hashes[release.ref] = text === null ? null : sha256(text);
// A 404 is either "not written yet" or "lived at another path",
// and only the first is a correct null.
if (text === null) {
console.log(
` ${entry.id}: no ${pathFor(entry, release.ref)} at ${release.ref}, recorded null.\n` +
` ^ if the file lived elsewhere then, add a pathByRef instead.`
);
}
}
entry.sha256 = hashes;
}
Expand Down
41 changes: 31 additions & 10 deletions scripts/performance-presets.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,17 @@
"",
"To update a pin deliberately: pnpm perf:presets --refresh <presetId|all>",
"then pnpm perf:presets to regenerate, and review the diff.",
"Never hand-edit electron/main/services/performanceConfigData.ts."
"Never hand-edit electron/main/services/performanceConfigData.ts.",
"",
"A preset added upstream after a source's older releases records null for",
"those releases: the file did not exist yet, so there is nothing to bundle."
],
"historyDepth": 3,
"$historyDepthComment": "How many upstream releases to bundle per source, newest first. --refresh prepends the new release and ages out the oldest. Raising it grows the main-process bundle by roughly 25 KB per preset per release; lowering it shortens how far a user can roll back.",
"baseline": {
"$comment": "Stock gameinfo.gi. Every preset is stored as a diff against this, which is what lets Grimoire patch in place instead of replacing the user's file.",
"repo": "Sqooky/OptimizationLock",
"commit": "2b994f62a84ba90cd666c7c63674a7aab3127aca",
"commit": "e2e9925cdd9d564a1a0bb19f28185211fb0dea75",
"path": "clean gameinfo.gi/gameinfo.gi",
"sha256": "b96954f5607e381b94b3d5ba2214bf1bcee2b65a16bb1a231b5f840586466955"
},
Expand All @@ -46,8 +49,8 @@
"releases": [
{
"ref": "3.4",
"commit": "2b994f62a84ba90cd666c7c63674a7aab3127aca",
"date": "2026-10-03"
"commit": "e2e9925cdd9d564a1a0bb19f28185211fb0dea75",
"date": "2026-10-05"
},
{
"ref": "3.3",
Expand All @@ -60,7 +63,7 @@
"date": "2026-09-30"
}
],
"$comment": "Upstream publishes no git tags or releases; each 'ref' is the version stated in that release's commit message and in the config's own header comment. Pins are the commit SHAs. Sqooky follows a release with fix-up pushes (3.3 was fixed by \"IWRONG VALUE\" minutes later), so each pin is the last commit of that release's push burst, not the commit titled with the version. The newest release is also the baseline pin. If Sqooky starts tagging, switch refKind to 'tag' and pin the tags' resolved SHAs."
"$comment": "Upstream publishes no git tags or releases; each 'ref' is the version stated in that release's commit message and in the config's own header comment. Pins are the commit SHAs. Sqooky follows a release with fix-up pushes (3.3 was fixed by \"IWRONG VALUE\" minutes later), so each pin is the last commit of that release's push burst, not the commit titled with the version. The newest release is also the baseline pin. If Sqooky starts tagging, switch refKind to 'tag' and pin the tags' resolved SHAs. 3.4 is pinned past its push burst to e2e9925c (2026-10-05): those later commits only reformatted the bundled files (no key changes) and added Eskay's config, and upstream still states 3.4."
},
"optilock": {
"repo": "dacooderr/OptiLock",
Expand Down Expand Up @@ -97,20 +100,34 @@
"source": "optimizationlock",
"path": "Sqooky's .gi/gameinfo.gi",
"sha256": {
"3.4": "041ebe91c92548c7a45dc4832d22383cf5ce190d8d75b0bb734075f203c02113",
"3.4": "c1e451134cd1d4d57d0f665673f9bbe732d6335c2b48ee6617079c23254222c7",
"3.3": "dbb91f6e0ba60e9e9493fa823108036f13f2cf90997766f589296520319fb925",
"3.2": "6bcc472f5f4132f9c39efbf88e747f266805952b3d00d8ab96bec5ae87d181f5"
},
"author": "Sqooky"
},
{
"id": "eskay",
"name": "Eskay's Config",
"tier": "light",
"source": "optimizationlock",
"path": "Eskay's config/gameinfo.gi",
"$comment": "Added upstream after 3.3, so older releases are null. A minor fork of Sqooky's default that drops the far-clip and fade-distance culling (r_farz, r_mapextents, sc_fade_distance_scale_override), so no building or player pop-in.",
"sha256": {
"3.4": "c7a496cd9c4b62de03ae03f4bbfda9cd8228bf97cdd1a0824aafe441809dd3ba",
"3.3": null,
"3.2": null
},
"author": "Eskay"
},
{
"id": "sqooky-testing",
"name": "Sqooky's Testing",
"tier": "preview",
"source": "optimizationlock",
"path": "test_cfg/gameinfo.gi",
"sha256": {
"3.4": "9f77fb7a2ece33cb2a085a0cc2c23be6dd9e5bb61fa212e5ebe90dcb998e0037",
"3.4": "96ed6e4ceb5ec482cadb38756012cd317203694fe3aa6dcbb082c39d41f98ecc",
"3.3": "b7b2d540fdb74d91ca8544cb50354ca6ef1cc78dd9a24bc59ad0f5180e29abe1",
"3.2": "93a375143673eae81e98adb0ddf7f24aeeb428a9bd7c696717d2db6ddaf46808"
},
Expand All @@ -137,7 +154,7 @@
"source": "optimizationlock",
"path": "kaizuchanerus minimum spec/gameinfo.gi",
"sha256": {
"3.4": "afda3762de652cf04f325015cfe16496be4a0080e587de83e46baf753c3a36ad",
"3.4": "46abb89c15bebdabef6234959cbe597df289176d3e2a19cf826381107e103bc6",
"3.3": "e85b31c337752597faaf25a4e628d687f67684fdf5284607141b13e2554c7b74",
"3.2": "e1a58a6dc07df47a0e08bdc031fefec90da89070094e5c4a248872bc020cc80d"
},
Expand Down Expand Up @@ -184,7 +201,7 @@
}
],
"exclude": {
"$comment": "Never emitted into a preset, whatever upstream sets.",
"$comment": "Never emitted into a preset, whatever upstream sets. MaterialSystem2/RenderModes is a list of seven `game` entries, not key/value pairs: a config written before Valve added ShadowSilhouette diffed to a single `game \"FrontDepth\"` op, and the patcher, which edits every occurrence of a key, collapsed all seven render modes into FrontDepth.",
"patterns": [
{
"pattern": "^(host_)?timescale$",
Expand Down Expand Up @@ -213,6 +230,10 @@
],
[
"SteamAudio"
],
[
"MaterialSystem2",
"RenderModes"
]
],
"keys": [
Expand Down Expand Up @@ -297,7 +318,7 @@
},
{
"key": "citadel_unit_status_old_update_rate",
"why": "Caught by the citadel_unit_status_ pattern, but it is the one member of that family that is genuinely a frame-rate setting: it caps how often health bars redraw. All six presets set 15 against a stock default of 30. It changes redraw cost, not what the bars show."
"why": "Caught by the citadel_unit_status_ pattern, but it is the one member of that family that is genuinely a frame-rate setting: it caps how often health bars redraw. Every bundled preset except kaizuchanerus' sets 15 against a stock default of 30. It changes redraw cost, not what the bars show."
},
{
"key": "citadel_hud_objective_health_debug_show_midboss",
Expand Down
Binary file added src/assets/performance-authors/eskay.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/performance-authors/kaizuchanerus.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions src/components/performance/PerformanceConfigCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,11 @@ describe('PerformanceConfigCard', () => {

it('shows bundled pictures for verified authors and an initial for the rest', async () => {
await render();
for (const tier of ['balanced', 'competitive', 'maximum', 'preview']) {
for (const tier of ['balanced', 'competitive', 'maximum', 'potato', 'preview']) {
expect(card(tier)?.querySelector('img')).toBeTruthy();
}
expect(card('aggressive')?.querySelector('img')).toBeNull();
expect(card('aggressive')?.textContent).toContain('b');
expect(card('potato')?.querySelector('img')).toBeNull();
});

it('never shows commit hashes or raw status prose on the main card', async () => {
Expand Down
4 changes: 4 additions & 0 deletions src/components/performance/PerformanceConfigCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
import { savedOptIns, savedVersion, sortPresetsByTier } from '../../lib/performanceSelection';
import sqookyAvatar from '../../assets/performance-authors/sqooky.png';
import dacooderAvatar from '../../assets/performance-authors/dacooder.jpg';
import eskayAvatar from '../../assets/performance-authors/eskay.png';
import kaizuchanerusAvatar from '../../assets/performance-authors/kaizuchanerus.jpg';
import type {
PerformanceConfigStatus,
PerformanceLatestInfo,
Expand All @@ -49,6 +51,8 @@ const SQOOKY_ARTIST: BrowseArtistRef = {
const AUTHOR_AVATARS: Record<string, string> = {
Sqooky: sqookyAvatar,
dacooder: dacooderAvatar,
Eskay: eskayAvatar,
kaizuchanerus: kaizuchanerusAvatar,
};

interface Notice {
Expand Down
4 changes: 3 additions & 1 deletion src/lib/performanceSelection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,19 @@ function preset(
const settings = (partial: Partial<AppSettings>) => partial as AppSettings;

describe('sortPresetsByTier', () => {
it('orders mildest to strongest with the preview config last', () => {
it('puts the recommended default first, then mildest to strongest with the preview config last', () => {
const sorted = sortPresetsByTier([
preset('p', 'preview'),
preset('m', 'maximum'),
preset('b', 'balanced'),
preset('pot', 'potato'),
preset('a', 'aggressive'),
preset('c', 'competitive'),
preset('l', 'light'),
]);
expect(sorted.map((p) => p.tier)).toEqual([
'balanced',
'light',
'competitive',
'aggressive',
'maximum',
Expand Down
Loading
Loading