Author SHA1 Message Date
ed 4fbf550d3c hot-reload attempt (unreviewed, not working) 2026-08-06 10:44:34 -04:00
76 changed files with 8985 additions and 27722 deletions
-26
View File
@@ -1,26 +0,0 @@
# Cozy and Windy
Editor theme ported from the Rider scheme of the same name.
It colors the editor surface, C/C++ syntax, and tape-atom DSL keywords
emitted by `local.tape-atom-syntax`. It does not change workbench chrome.
## Install
```powershell
cd C:\projects\Pikuma\ps1\.vscode\cozy-and-windy
npm run package
code --install-extension .\cozy-and-windy-0.1.0.vsix --force
```
Reload the window. Select **Cozy and Windy** as the color theme, or set
`workbench.colorTheme` to `Cozy and Windy` in the PS1 workspace settings.
Keep `local.tape-atom-syntax` installed. This theme colors those token
types; it does not classify them.
## Inspect
Open `hello_camera.atom.c` and run **Developer: Inspect Editor Tokens and Scopes**
on `MipsAtom_`, an atom name, `atom_info`, `R_PrimCursor`, a `gte_*` call,
and a `mac_*` call.
Binary file not shown.
-25
View File
@@ -1,25 +0,0 @@
{
"name": "cozy-and-windy",
"displayName": "Cozy and Windy",
"description": "Editor theme ported from the Rider Cozy and Windy scheme. Colors C/C++ and tape-atom DSL keywords.",
"publisher": "local",
"version": "0.1.0",
"engines": {
"vscode": "^1.80.0"
},
"categories": [
"Themes"
],
"scripts": {
"package": "npx --yes @vscode/vsce@3.6.1 package --allow-missing-repository --skip-license --out cozy-and-windy-0.1.0.vsix"
},
"contributes": {
"themes": [
{
"label": "Cozy and Windy",
"uiTheme": "vs-dark",
"path": "./themes/cozy-and-windy-color-theme.json"
}
]
}
}
@@ -1,132 +0,0 @@
{
"name": "Cozy and Windy",
"type": "dark",
"semanticHighlighting": true,
"colors": {
// 121212
// 111212
// 211f1e
// 191817
"editor.background": "#191817",
"editor.foreground": "#dfc6ba",
"editor.lineHighlightBackground": "#1c1c1c",
"editor.selectionBackground": "#164371",
"editor.selectionForeground": "#c8c8c8",
"editorLineNumber.foreground": "#43c3c3",
"editorLineNumber.activeForeground": "#00fff4",
"editorIndentGuide.background1": "#181818",
"editorIndentGuide.activeBackground1": "#202020",
"editorRuler.foreground": "#505050",
"editorGutter.background": "#211f1e",
"editorBracketMatch.background": "#3b514d",
"editor.foldBackground": "#0c0c0c6a",
"editor.wordHighlightBackground": "#211f1e4d",
"editor.wordHighlightStrongBackground": "#303030",
"editorCursor.foreground": "#00fff4",
"editorWhitespace.foreground": "#181818",
// "editorLineHighlightBorder": "#1c1c1c",
"editorWidget.background": "#211f1e",
"editorSuggestWidget.background": "#2c334b",
"editorHoverWidget.background": "#2c334b"
},
"semanticTokenColors": {
"comment": { "foreground": "#868686", "fontStyle": "italic" },
"keyword": { "foreground": "#d8bd5b" },
"string": { "foreground": "#d46a54" },
"number": { "foreground": "#b5cea8" },
"operator": { "foreground": "#be8e78" },
"class": { "foreground": "#54a4d6" },
"struct": { "foreground": "#54a4d6" },
"enum": { "foreground": "#54a4d6" },
"type": { "foreground": "#54a4d6" },
"interface": { "foreground": "#7984ab" },
"function": { "foreground": "#cccab5" },
// "function": { "foreground": "#6090a9" },
"method": { "foreground": "#6090a9" },
"variable": { "foreground": "#bc966c" },
"parameter": { "foreground": "#ce8365" },
"property": { "foreground": "#acb8c8" },
"*.static": { "foreground": "#9e95c6" },
"macro": { "foreground": "#5ea852" },
"namespace": { "foreground": "#8e8e8e" },
"typeParameter": { "foreground": "#b8d7a3" },
"enumMember": { "foreground": "#a373b0" },
"label": { "foreground": "#c8c8c8", "fontStyle": "bold" },
"tapeAtomKeyword": { "foreground": "#d8bd5b", "fontStyle": "bold" },
"tapeAtomName": { "foreground": "#b1b7d6", "fontStyle": "bold" },
"tapeComponentKeyword": { "foreground": "#d68a36", "fontStyle": "bold" },
"tapeComponentName": { "foreground": "#b1b7d6", "fontStyle": "bold" },
"tapeAnnotation": { "foreground": "#d8bd5b" },
"tapeBindType": { "foreground": "#54a4d6" },
"tapePhase": { "foreground": "#b8d7a3", "fontStyle": "italic" },
"tapeLabel": { "foreground": "#959595", "fontStyle": "bold" },
// "tapeCpuInstruction": { "foreground": "#6d9aa0" },
// "tapeCpuInstruction": { "foreground": "#cf7539" },
// "tapeCpuInstruction": { "foreground": "#d16b3a" },
"tapeCpuInstruction": { "foreground": "#d5895a" },
"tapeGteInstruction": { "foreground": "#988bcb" },
"tapeGpuInstruction": { "foreground": "#bf7dac" },
"tapeComponentInstruction": { "foreground": "#8baa5d" },
// "tapeGprRegister": { "foreground": "#92d4d9" },
"tapeGprRegister": { "foreground": "#a2bfa8" },
"tapeCop2Register": { "foreground": "#945cd9" },
"tapeDuffleType": { "foreground": "#54a4d6" },
"tapeAttribute": { "foreground": "#73a07c" },
"tapeGprRegister.tapeRead": { "foreground": "#5bb8b0", "fontStyle": "italic" },
"tapeGprRegister.tapeWrite": { "foreground": "#2d8f8c", "fontStyle": "bold" },
"tapeCop2Register.tapeRead": { "foreground": "#b08ae0", "fontStyle": "italic" },
"tapeCop2Register.tapeWrite": { "foreground": "#7b3ec4", "fontStyle": "bold" },
// "*.tapeAuto": { },
"tapeControlFlow": { "foreground": "#63d169", "fontStyle": "bold" },
"tapeDelaySlot": { "foreground": "#ff5647" }
},
"tokenColors": [
{ "scope": ["comment", "comment.block", "comment.line", "comment.block.documentation"], "settings": { "foreground": "#868686", "fontStyle": "italic" } },
{ "scope": ["keyword", "keyword.control", "keyword.other"], "settings": { "foreground": "#d8bd5b" } },
{ "scope": ["string", "string.quoted"], "settings": { "foreground": "#d46a54" } },
{ "scope": ["string.quoted.other"], "settings": { "foreground": "#d69d85" } },
{ "scope": ["constant.numeric"], "settings": { "foreground": "#b5cea8" } },
{ "scope": ["punctuation", "keyword.operator"], "settings": { "foreground": "#be8e78" } },
{ "scope": ["keyword.operator.overload"], "settings": { "foreground": "#b87e76" } },
{ "scope": ["entity.name.type", "entity.name.type.class", "entity.name.type.struct", "entity.name.type.enum"], "settings": { "foreground": "#54a4d6" } },
{ "scope": ["entity.name.type.interface"], "settings": { "foreground": "#7984ab" } },
{ "scope": ["entity.name.function"], "settings": { "foreground": "#cccab5" } },
{ "scope": ["entity.name.function.member"], "settings": { "foreground": "#6090a9" } },
{ "scope": ["variable.other.local"], "settings": { "foreground": "#bc966c" } },
{ "scope": ["variable.parameter"], "settings": { "foreground": "#ce8365" } },
{ "scope": ["variable.other.property"], "settings": { "foreground": "#acb8c8" } },
{ "scope": ["variable.other.constant"], "settings": { "foreground": "#9e95c6" } },
{ "scope": ["variable.other.global", "variable.other.defaultLibrary"], "settings": { "foreground": "#bf7dac" } },
{ "scope": ["support.type", "support.function"], "settings": { "foreground": "#8baa5d" } },
{ "scope": ["meta.preprocessor"], "settings": { "foreground": "#5ea852" } },
// { "scope": ["variable.parameter.preprocessor"], "settings": { "foreground": "#636363" } },
{ "scope": ["variable.parameter.preprocessor"], "settings": { "foreground": "#bc966c" } },
{ "scope": ["keyword.control.directive"], "settings": { "foreground": "#d68a36" } },
{ "scope": ["entity.name.namespace"], "settings": { "foreground": "#8e8e8e" } },
{ "scope": ["entity.name.type.parameter"], "settings": { "foreground": "#b8d7a3" } },
{ "scope": ["variable.other.enummember"], "settings": { "foreground": "#a373b0" } },
{ "scope": ["entity.name.type.concept"], "settings": { "foreground": "#76ff7d" } },
{ "scope": ["entity.name.type.dependent"], "settings": { "foreground": "#448b5a", "fontStyle": "bold" } },
{ "scope": ["entity.name.label"], "settings": { "foreground": "#c8c8c8", "fontStyle": "bold" } },
{ "scope": ["invalid"], "settings": { "foreground": "#ff5647" } },
{ "scope": ["keyword.codetag.todo"], "settings": { "foreground": "#c10000", "fontStyle": "bold italic" } },
{ "scope": ["keyword.control.duffle.atom"], "settings": { "foreground": "#d8bd5b", "fontStyle": "bold" } },
{ "scope": ["entity.name.function.duffle.atom"], "settings": { "foreground": "#cccab5", "fontStyle": "bold" } },
{ "scope": ["keyword.control.duffle.component"], "settings": { "foreground": "#d68a36", "fontStyle": "bold" } },
{ "scope": ["entity.name.function.duffle.component"], "settings": { "foreground": "#6090a9", "fontStyle": "bold" } },
{ "scope": ["support.function.duffle.annotation"], "settings": { "foreground": "#d8bd5b" } },
{ "scope": ["entity.name.type.duffle.bind"], "settings": { "foreground": "#54a4d6" } },
{ "scope": ["entity.name.tag.duffle.phase"], "settings": { "foreground": "#b8d7a3", "fontStyle": "italic" } },
{ "scope": ["entity.name.label.duffle.atom"], "settings": { "foreground": "#c8c8c8", "fontStyle": "bold" } },
{ "scope": ["support.function.duffle.cpu"], "settings": { "foreground": "#6d9aa0" } },
{ "scope": ["support.function.duffle.gte"], "settings": { "foreground": "#988bcb" } },
{ "scope": ["support.function.duffle.gpu"], "settings": { "foreground": "#bf7dac" } },
{ "scope": ["support.function.duffle.component"], "settings": { "foreground": "#8baa5d" } },
{ "scope": ["keyword.control.duffle.branch"], "settings": { "foreground": "#76ff7d", "fontStyle": "bold" } },
{ "scope": ["keyword.operator.duffle.delayslot"], "settings": { "foreground": "#ff5647" } },
{ "scope": ["variable.other.constant.duffle.gpr"], "settings": { "foreground": "#3fa8a6" } },
{ "scope": ["variable.other.constant.duffle.cop2"], "settings": { "foreground": "#945cd9" } },
{ "scope": ["storage.type.duffle.type"], "settings": { "foreground": "#54a4d6" } },
{ "scope": ["storage.modifier.duffle.attr"], "settings": { "foreground": "#73a07c" } }
]
}
-43
View File
@@ -1,43 +0,0 @@
# Package and install the local VS Code Insiders extensions under .vscode/.
# Usage:
# .\install_extensions.ps1
# .\install_extensions.ps1 -SkipPackage
param([switch] $SkipPackage)
$path_vscode = $PSScriptRoot
$code_insiders = "C:\apps\Microsoft VS Code Insiders\bin\code-insiders.cmd"
if (-not (test-path -literalpath $code_insiders)) {
$found = get-command code-insiders -erroraction silentlycontinue
if ($found) { $code_insiders = $found.source }
}
if (-not (test-path -literalpath $code_insiders)) { throw "code-insiders not found. Install VS Code Insiders or add it to PATH." }
$extensions = @(
(join-path $path_vscode "tape-atom-syntax"),
(join-path $path_vscode "cozy-and-windy")
)
foreach ($extension in $extensions) {
$package_json = join-path $extension "package.json"
if (-not (test-path -literalpath $package_json)) { throw "missing $package_json" }
$manifest = get-content -literalpath $package_json -raw | convertfrom-json
$vsix = join-path $extension ("{0}-{1}.vsix" -f $manifest.name, $manifest.version)
if (-not $SkipPackage) {
if (-not $manifest.scripts.package) { throw "$package_json has no scripts.package" }
write-host "packaging $($manifest.displayName) ($($manifest.name)@$($manifest.version))"
& npm --prefix $extension run package
if ($LASTEXITCODE -ne 0) { throw "npm run package failed for $extension" }
}
if (-not (test-path -literalpath $vsix)) { throw "missing $vsix" }
write-host "installing $vsix"
& $code_insiders --install-extension $vsix --force
if ($LASTEXITCODE -ne 0) { throw "install failed for $vsix" }
}
write-host "done. reload the Insiders window (Developer: Reload Window)."
+33
View File
@@ -177,6 +177,39 @@
"tbreak main", "tbreak main",
"continue" "continue"
] ]
},
{
"name": "Debug: Hello Camera! (attach only)",
"type": "gdb",
"request": "attach",
"target": "localhost:3333",
"remote": true,
"cwd": "${workspaceRoot}",
"valuesFormatting": "parseText",
"registerLimit": "1-32",
"frameFilters": false,
"showDevDebugOutput": false,
"printCalls": false,
"stopAtConnect": true,
"gdbpath": "gdb-multiarch",
"windows": {
"gdbpath": "gdb-multiarch.exe"
},
"osx": {
"gdbpath": "gdb"
},
"executable": "${workspaceRoot}/build/hello_camera.dwarf-injected.elf",
"setupCommands": [
{ "text": "set mi-async off" },
{ "text": "set remotetimeout 0" },
{ "text": "set logging file build/gen/hello_camera.gdb.log" },
{ "text": "set logging redirect on" }
],
"autorun": [
"source scripts/gdb/gdb_tape_atoms.gdb",
"tbreak hot_reload_entry",
"continue"
]
} }
] ]
} }
Binary file not shown.
-222
View File
@@ -1,222 +0,0 @@
"use strict";
const { nearestCall } = require("./lexer");
const { mergeIndexes, scanSource } = require("./source-index");
const TOKEN_TYPES = [
"tapeAtomKeyword",
"tapeAtomName",
"tapeComponentKeyword",
"tapeComponentName",
"tapeAnnotation",
"tapeBindType",
"tapePhase",
"tapeLabel",
"tapeCpuInstruction",
"tapeControlFlow",
"tapeGteInstruction",
"tapeGpuInstruction",
"tapeComponentInstruction",
"tapeDelaySlot",
"tapeGprRegister",
"tapeCop2Register",
"tapeDuffleType",
"tapeAttribute",
"keyword",
"macro",
];
const TOKEN_MODIFIERS = ["declaration", "tapeRead", "tapeWrite", "tapeAuto"];
const TOKEN_TYPE_INDEX = new Map(TOKEN_TYPES.map((name, index) => [name, index]));
const TOKEN_MODIFIER_INDEX = new Map(TOKEN_MODIFIERS.map((name, index) => [name, index]));
const ATOM_KEYWORDS = new Set(["MipsAtom_", "MipsAtom_Proc_"]);
const COMPONENT_KEYWORDS = new Set(["MipsAtomComp_", "MipsAtomComp_Proc_"]);
const ANNOTATIONS = new Set([
"atom_info", "atom_bind", "atom_reads", "atom_writes", "atom_label",
"atom_offset", "atom_reg", "atom_type", "atom_ctx", "atom_phase",
"atom_auto_reg", "phase_auto_reg", "atom_dbg_skip",
]);
const DSL_KEYWORDS = new Set([
"FI_", "I_", "NI_", "Relative_", "Struct_", "Enum_", "Union_", "Array_",
"Slice_", "TypeR_", "TypeV_", "align_", "internal", "local_persist", "global",
"RO_", "LP_", "gknown", "expect_", "cexpr_",
"asm", "asm_words", "asm_rpins", "asm_clobber",
"O_", "S_", "C_", "T_", "tmpl", "glue", "r_", "v_", "tr_", "tv_",
"rgcc", "r_use", "r_set", "r_mod", "r_imm", "r_mem",
"u1_", "u2_", "u4_", "u8_", "s1_", "s2_", "s4_", "s8_",
"u1_r", "u2_r", "u4_r", "u8_r", "u1_v", "u2_v", "u4_v", "u8_v",
]);
const DELAY_SLOT_KEYWORDS = new Set(["LdSlot_", "BdSlot_", "DmaSlot_", "GteDelay_"]);
const CONTROL_FLOW_PREFIXES = /^(?:branch_|jump_|call_)/;
const ROLE_TO_TYPE = {
atomName: "tapeAtomName",
componentName: "tapeComponentName",
bindType: "tapeBindType",
duffleType: "tapeDuffleType",
gprRegister: "tapeGprRegister",
cop2Register: "tapeCop2Register",
};
function registerType(name, index) {
const kind = index.registers.get(name);
if (kind === "gpr" || /^R_[A-Za-z0-9_]+$/.test(name)) return "tapeGprRegister";
if (kind === "cop2" || /^(?:C2_|gte_cr_)[A-Za-z0-9_]+$/.test(name)) return "tapeCop2Register";
return null;
}
function instructionType(name, index) {
const domain = index.macros.get(name);
if (domain === "control") return "tapeControlFlow";
if (domain === "cpu") return "tapeCpuInstruction";
if (domain === "gte") return "tapeGteInstruction";
if (domain === "gpu") return "tapeGpuInstruction";
if (domain === "component") {
if (/^mac_gte_/.test(name)) return "tapeGteInstruction";
if (/^mac_gp/.test(name)) return "tapeGpuInstruction";
if (/^mac_/.test(name)) return "tapeComponentInstruction";
return "macro";
}
if (domain === "utility") return "macro";
if (/^gte_(?!cr_)/.test(name)) return "tapeGteInstruction";
if (/^gp[01]_/.test(name)) return "tapeGpuInstruction";
if (/^mac_gte_/.test(name)) return "tapeGteInstruction";
if (/^mac_gp/.test(name)) return "tapeGpuInstruction";
if (/^mac_/.test(name)) return "tapeComponentInstruction";
return null;
}
function modifierMask(modifiers) {
let mask = 0;
for (const modifier of modifiers) {
const index = TOKEN_MODIFIER_INDEX.get(modifier);
if (index !== undefined) mask |= (1 << index);
}
return mask;
}
function isRegUseAccess(tokens, tokenIndex) {
const prev = tokens[tokenIndex - 1];
if (!prev || prev.text !== ".") return false;
const prevPrev = tokens[tokenIndex - 2];
if (!prevPrev || prevPrev.kind !== "identifier") return false;
const next = tokens[tokenIndex + 1];
if (next && next.text === ".") return false;
if (prevPrev.text === "r") return true;
const prev3 = tokens[tokenIndex - 3];
const prev4 = tokens[tokenIndex - 4];
if (prev3 && prev3.text === "." && prev4 && prev4.kind === "identifier" && prev4.text === "r") return true;
return false;
}
function classifyDocument(source, filePath, workspaceIndex, shouldCancel = () => false) {
const scanned = scanSource(source, filePath);
const index = mergeIndexes(workspaceIndex, scanned.index);
const spans = [];
for (let tokenIndex = 0; tokenIndex < scanned.tokens.length; tokenIndex += 1) {
if (shouldCancel()) break;
const token = scanned.tokens[tokenIndex];
if (token.kind !== "identifier") continue;
let type = null;
let modifiers = [];
const declaration = scanned.declarations.get(token.start);
const context = nearestCall(scanned.contexts, tokenIndex);
if (declaration) {
type = ROLE_TO_TYPE[declaration.role] || null;
modifiers = declaration.modifiers.slice();
} else if (ATOM_KEYWORDS.has(token.text)) {
type = "tapeAtomKeyword";
} else if (COMPONENT_KEYWORDS.has(token.text)) {
type = "keyword";
} else if (ANNOTATIONS.has(token.text)) {
type = "tapeAnnotation";
} else if (context && context.callee === "atom_bind" && context.argIndex === 0) {
type = "tapeBindType";
} else if (context && context.callee === "atom_phase" && context.argIndex === 0) {
type = "tapePhase";
modifiers = ["declaration"];
} else if (context && context.callee === "atom_ctx" && context.argIndex === 0) {
type = "tapeAtomName";
} else if (context && context.callee === "atom_label" && context.argIndex === 0) {
type = "tapeLabel";
modifiers = ["declaration"];
} else if (context && context.callee === "atom_offset" && context.argIndex <= 1) {
type = "tapeLabel";
} else if (context && context.callee === "atom_reads") {
type = registerType(token.text, index);
if (type) modifiers = ["tapeRead"];
} else if (context && context.callee === "atom_writes") {
type = registerType(token.text, index);
if (type) modifiers = ["tapeWrite"];
} else if (context && context.callee === "atom_auto_reg") {
if (context.argIndex === 0) type = "tapeAtomName";
if (context.argIndex === 1) {
type = "tapeGprRegister";
modifiers = ["declaration", "tapeAuto"];
}
} else if (context && context.callee === "phase_auto_reg") {
if (context.argIndex === 0) type = "tapePhase";
if (context.argIndex === 1) {
type = "tapeGprRegister";
modifiers = ["declaration", "tapeAuto"];
}
}
if (!type && index.bindTypes.has(token.text)) type = "tapeBindType";
if (!type && DSL_KEYWORDS.has(token.text)) type = "keyword";
if (!type && index.types.has(token.text)) type = "tapeDuffleType";
if (!type && index.attributes.has(token.text)) type = "tapeAttribute";
if (!type) type = registerType(token.text, index);
if (!type && DELAY_SLOT_KEYWORDS.has(token.text)) type = "tapeDelaySlot";
if (!type) {
const domain = index.macros.get(token.text);
if (domain === "control" || (domain && CONTROL_FLOW_PREFIXES.test(token.text))) {
type = "tapeControlFlow";
}
}
if (!type && isRegUseAccess(scanned.tokens, tokenIndex)) type = "tapeGprRegister";
if (!type) type = instructionType(token.text, index);
if (!type && /^(?:Slice_|A[0-9]+_)/.test(token.text)) type = "tapeDuffleType";
if (!type && /_[RV]$/.test(token.text)) type = "tapeDuffleType";
if (!type && index.atoms.has(token.text)) type = "tapeAtomName";
if (!type && index.components.has(token.text)) type = "tapeComponentName";
if (!type && index.phases.has(token.text)) type = "tapePhase";
if (!type && index.labels.has(token.text)) type = "tapeLabel";
if (!type) continue;
spans.push({
text: token.text,
type,
typeIndex: TOKEN_TYPE_INDEX.get(type),
modifiers,
modifierMask: modifierMask(modifiers),
start: token.start,
length: token.end - token.start,
line: token.line,
character: token.character,
});
}
spans.sort((left, right) => left.start - right.start || left.length - right.length);
const nonOverlapping = [];
for (const span of spans) {
const previous = nonOverlapping[nonOverlapping.length - 1];
if (!previous || previous.start + previous.length <= span.start) nonOverlapping.push(span);
}
return { spans: nonOverlapping, errors: scanned.errors };
}
module.exports = {
TOKEN_MODIFIERS,
TOKEN_TYPES,
classifyDocument,
modifierMask,
};
-111
View File
@@ -1,111 +0,0 @@
"use strict";
const vscode = require("vscode");
const { TOKEN_MODIFIERS, TOKEN_TYPES, classifyDocument } = require("./classifier");
const { createIndex, mergeIndexes, scanSource } = require("./source-index");
const SOURCE_GLOB = "**/*.{c,h,cc,cpp,cxx,hh,hpp,hxx}";
const EXCLUDE_GLOB = "**/{gen,build,.slop_cache,toolchain,node_modules}/**";
const EXCLUDED_SEGMENTS = new Set(["gen", "build", ".slop_cache", "toolchain", "node_modules"]);
function isExcluded(uri) {
const segments = uri.fsPath.replaceAll("\\", "/").split("/");
return segments.some((segment) => EXCLUDED_SEGMENTS.has(segment));
}
function formatError(filePath, error) {
return `${filePath}:${error.offset}: ${error.kind}`;
}
async function activate(context) {
const output = vscode.window.createOutputChannel("Tape Atom DSL");
const emitter = new vscode.EventEmitter();
const legend = new vscode.SemanticTokensLegend(TOKEN_TYPES, TOKEN_MODIFIERS);
let workspaceIndex = createIndex();
let rebuildGeneration = 0;
let debounceHandle = null;
async function rebuildIndex() {
const generation = ++rebuildGeneration;
const files = await vscode.workspace.findFiles(SOURCE_GLOB, EXCLUDE_GLOB);
let nextIndex = createIndex();
for (const uri of files) {
if (generation !== rebuildGeneration) return;
if (isExcluded(uri)) continue;
try {
const bytes = await vscode.workspace.fs.readFile(uri);
const source = Buffer.from(bytes).toString("utf8");
const result = scanSource(source, uri.fsPath);
nextIndex = mergeIndexes(nextIndex, result.index);
for (const error of result.errors) output.appendLine(formatError(uri.fsPath, error));
} catch (error) {
output.appendLine(`${uri.fsPath}: ${error.stack || error.message || error}`);
}
}
if (generation !== rebuildGeneration) return;
workspaceIndex = nextIndex;
emitter.fire();
}
function scheduleRebuild(uri) {
if (uri && isExcluded(uri)) return;
if (debounceHandle !== null) clearTimeout(debounceHandle);
debounceHandle = setTimeout(() => {
debounceHandle = null;
rebuildIndex().catch((error) => output.appendLine(error.stack || String(error)));
}, 100);
}
const provider = {
onDidChangeSemanticTokens: emitter.event,
provideDocumentSemanticTokens(document, cancellationToken) {
try {
const result = classifyDocument(
document.getText(),
document.uri.fsPath,
workspaceIndex,
() => cancellationToken.isCancellationRequested
);
const builder = new vscode.SemanticTokensBuilder(legend);
for (const span of result.spans) {
if (cancellationToken.isCancellationRequested) break;
builder.push(span.line, span.character, span.length, span.typeIndex, span.modifierMask);
}
for (const error of result.errors) {
output.appendLine(formatError(document.uri.fsPath || document.uri.toString(), error));
}
return builder.build();
} catch (error) {
output.appendLine(`${document.uri}: ${error.stack || error.message || error}`);
return new vscode.SemanticTokensBuilder(legend).build();
}
},
};
const selector = [
{ language: "c", scheme: "file" },
{ language: "c", scheme: "untitled" },
{ language: "cpp", scheme: "file" },
{ language: "cpp", scheme: "untitled" },
];
const watcher = vscode.workspace.createFileSystemWatcher(SOURCE_GLOB);
context.subscriptions.push(
output,
emitter,
watcher,
watcher.onDidCreate(scheduleRebuild),
watcher.onDidChange(scheduleRebuild),
watcher.onDidDelete(scheduleRebuild),
vscode.languages.registerDocumentSemanticTokensProvider(selector, provider, legend),
{ dispose() { if (debounceHandle !== null) clearTimeout(debounceHandle); } }
);
await rebuildIndex();
}
function deactivate() {}
module.exports = { activate, deactivate };
-186
View File
@@ -1,186 +0,0 @@
"use strict";
function isIdentifierStart(code) {
return code === 95 ||
(code >= 65 && code <= 90) ||
(code >= 97 && code <= 122);
}
function isIdentifierContinue(code) {
return isIdentifierStart(code) || (code >= 48 && code <= 57);
}
function lex(source) {
if (typeof source !== "string") throw new TypeError("source must be a string");
const tokens = [];
const errors = [];
let offset = 0;
let line = 0;
let character = 0;
function advance() {
if (source[offset] === "\r" && source[offset + 1] === "\n") {
offset += 2;
line += 1;
character = 0;
return;
}
if (source[offset] === "\n") {
offset += 1;
line += 1;
character = 0;
return;
}
offset += 1;
character += 1;
}
function pushToken(kind, start, startLine, startCharacter) {
tokens.push({
kind,
text: source.slice(start, offset),
start,
end: offset,
line: startLine,
character: startCharacter,
});
}
while (offset < source.length) {
const ch = source[offset];
if (/\s/.test(ch)) {
advance();
continue;
}
if (ch === "/" && source[offset + 1] === "/") {
while (offset < source.length && source[offset] !== "\r" && source[offset] !== "\n") advance();
continue;
}
if (ch === "/" && source[offset + 1] === "*") {
const start = offset;
advance();
advance();
let closed = false;
while (offset < source.length) {
if (source[offset] === "*" && source[offset + 1] === "/") {
advance();
advance();
closed = true;
break;
}
advance();
}
if (!closed) errors.push({ kind: "unterminated-block-comment", offset: start });
continue;
}
if (ch === "\"" || ch === "'") {
const quote = ch;
const start = offset;
advance();
let closed = false;
while (offset < source.length) {
if (source[offset] === "\\") {
advance();
if (offset < source.length) advance();
continue;
}
if (source[offset] === quote) {
advance();
closed = true;
break;
}
if (source[offset] === "\n" || source[offset] === "\r") break;
advance();
}
if (!closed) errors.push({ kind: "unterminated-literal", offset: start });
continue;
}
const code = source.charCodeAt(offset);
if (isIdentifierStart(code)) {
const start = offset;
const startLine = line;
const startCharacter = character;
advance();
while (offset < source.length && isIdentifierContinue(source.charCodeAt(offset))) advance();
pushToken("identifier", start, startLine, startCharacter);
continue;
}
const start = offset;
const startLine = line;
const startCharacter = character;
advance();
pushToken("punctuation", start, startLine, startCharacter);
}
return { tokens, errors };
}
function buildCallContexts(tokens) {
const contexts = Array.from({ length: tokens.length }, () => []);
const calls = [];
const errors = [];
const stack = [];
for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
const token = tokens[tokenIndex];
if (token.text === ")") {
if (stack.length === 0) {
errors.push({ kind: "unmatched-close-paren", offset: token.start });
} else {
const frame = stack.pop();
if (frame.callee !== null) calls.push({ ...frame, closeTokenIndex: tokenIndex });
}
}
contexts[tokenIndex] = stack
.filter((frame) => frame.callee !== null)
.map((frame) => ({
callee: frame.callee,
calleeTokenIndex: frame.calleeTokenIndex,
openTokenIndex: frame.openTokenIndex,
argIndex: frame.argIndex,
}));
if (token.text === "(") {
const previous = tokens[tokenIndex - 1];
const hasCallee = previous && previous.kind === "identifier";
stack.push({
callee: hasCallee ? previous.text : null,
calleeTokenIndex: hasCallee ? tokenIndex - 1 : -1,
openTokenIndex: tokenIndex,
argIndex: 0,
});
continue;
}
if (token.text === "," && stack.length > 0) {
const frame = stack[stack.length - 1];
if (frame.callee !== null) frame.argIndex += 1;
}
}
for (const frame of stack) {
errors.push({ kind: "unmatched-open-paren", offset: tokens[frame.openTokenIndex].start });
}
return { contexts, calls, errors };
}
function nearestCall(contexts, tokenIndex, callee) {
const entries = contexts[tokenIndex] || [];
for (let contextIndex = entries.length - 1; contextIndex >= 0; contextIndex -= 1) {
const entry = entries[contextIndex];
if (callee === undefined || entry.callee === callee) return entry;
}
return null;
}
module.exports = { buildCallContexts, lex, nearestCall };
-85
View File
@@ -1,85 +0,0 @@
{
"name": "atomasm-psx",
"displayName": "AtomAsm-PSX",
"description": "Semantic highlighting for the PS1 Tape/Atom MIPS macro DSL",
"publisher": "local",
"version": "0.3.0",
"engines": { "vscode": "^1.80.0" },
"categories": ["Programming Languages"],
"activationEvents": ["onLanguage:c", "onLanguage:cpp"],
"main": "./extension.js",
"files": [
"classifier.js",
"extension.js",
"lexer.js",
"source-index.js",
"syntaxes/tape_atom.tmLanguage.json"
],
"scripts": {
"test": "node --test test/*.test.js",
"package": "npx --yes @vscode/vsce@3.6.1 package --allow-missing-repository --skip-license --out atomasm-psx-0.3.0.vsix"
},
"contributes": {
"semanticTokenTypes": [
{ "id": "tapeAtomKeyword", "superType": "keyword", "description": "Tape atom declaration keyword" },
{ "id": "tapeAtomName", "superType": "function", "description": "Tape atom name" },
{ "id": "tapeComponentKeyword", "superType": "keyword", "description": "Tape atom component declaration keyword" },
{ "id": "tapeComponentName", "superType": "function", "description": "Tape atom component name" },
{ "id": "tapeAnnotation", "superType": "macro", "description": "Tape atom annotation" },
{ "id": "tapeBindType", "superType": "type", "description": "Tape bind structure type" },
{ "id": "tapePhase", "superType": "label", "description": "Tape atom phase" },
{ "id": "tapeLabel", "superType": "label", "description": "Tape atom branch label" },
{ "id": "tapeCpuInstruction", "superType": "macro", "description": "MIPS CPU instruction emitter" },
{ "id": "tapeControlFlow", "superType": "keyword", "description": "MIPS branch or jump instruction" },
{ "id": "tapeGteInstruction", "superType": "macro", "description": "GTE instruction emitter" },
{ "id": "tapeGpuInstruction", "superType": "macro", "description": "GPU command emitter" },
{ "id": "tapeComponentInstruction", "superType": "macro", "description": "Tape atom component invocation" },
{ "id": "tapeDelaySlot", "superType": "keyword", "description": "Load or branch delay slot annotation" },
{ "id": "tapeGprRegister", "superType": "variable", "description": "MIPS GPR alias" },
{ "id": "tapeCop2Register", "superType": "variable", "description": "COP2 data or control register alias" },
{ "id": "tapeDuffleType", "superType": "type", "description": "Duffle type or type constructor" },
{ "id": "tapeAttribute", "superType": "keyword", "description": "Duffle linkage or storage attribute" },
{ "id": "keyword", "description": "Standard keyword (DSL built-in macros)" },
{ "id": "macro", "description": "Standard macro (utility #define with no instruction domain)" }
],
"semanticTokenModifiers": [
{ "id": "tapeRead", "description": "Register declared in atom_reads" },
{ "id": "tapeWrite", "description": "Register declared in atom_writes" },
{ "id": "tapeAuto", "description": "Auto-allocated register" }
],
"semanticTokenScopes": [
{
"language": "c",
"scopes": {
"tapeAtomKeyword": ["keyword.control.duffle.atom"],
"tapeAtomName": ["entity.name.function.duffle.atom"],
"tapeComponentKeyword": ["keyword.control.duffle.component"],
"tapeComponentName": ["entity.name.function.duffle.component"],
"tapeAnnotation": ["support.function.duffle.annotation"],
"tapeBindType": ["entity.name.type.duffle.bind"],
"tapePhase": ["entity.name.tag.duffle.phase"],
"tapeLabel": ["entity.name.label.duffle.atom"],
"tapeCpuInstruction": ["support.function.duffle.cpu"],
"tapeControlFlow": ["keyword.control.duffle.branch"],
"tapeGteInstruction": ["support.function.duffle.gte"],
"tapeGpuInstruction": ["support.function.duffle.gpu"],
"tapeComponentInstruction": ["support.function.duffle.component"],
"tapeDelaySlot": ["keyword.operator.duffle.delayslot"],
"tapeGprRegister": ["variable.other.constant.duffle.gpr"],
"tapeCop2Register": ["variable.other.constant.duffle.cop2"],
"tapeDuffleType": ["storage.type.duffle.type"],
"tapeAttribute": ["storage.modifier.duffle.attr"],
"keyword": ["keyword"],
"macro": ["entity.name.function.preprocessor"]
}
}
],
"grammars": [
{
"scopeName": "tape_atom.injection",
"path": "./syntaxes/tape_atom.tmLanguage.json",
"injectTo": ["source.c", "source.cpp"]
}
]
}
}
-341
View File
@@ -1,341 +0,0 @@
"use strict";
const path = require("node:path");
const { buildCallContexts, lex, nearestCall } = require("./lexer");
const BASE_TYPES = [
"B1", "B2", "B4", "B8", "F4", "F8", "S1", "S2", "S4", "S8",
"U1", "U2", "U4", "U8", "MipsAtom", "MipsCode", "Reg",
];
const C_BUILTINS = new Set([
"void", "type", "char", "short", "int", "long", "float", "double",
"unsigned", "signed", "bool", "size_t", "uint8_t", "uint16_t", "uint32_t",
"int8_t", "int16_t", "int32_t",
]);
const BASE_ATTRIBUTES = [
"FI_", "I_", "NI_", "Relative_", "Struct_", "Enum_", "Union_", "Array_",
"Slice_", "TypeR_", "TypeV_", "align_", "internal", "local_persist", "global",
"RO_", "LP_", "gknown", "expect_", "cexpr_",
"asm", "asm_words", "asm_rpins", "asm_clobber",
"O_", "S_", "C_", "T_", "tmpl", "glue", "r_", "v_", "tr_", "tv_",
"rgcc", "r_use", "r_set", "r_mod", "r_imm", "r_mem",
"u1_", "u2_", "u4_", "u8_", "s1_", "s2_", "s4_", "s8_",
"u1_r", "u2_r", "u4_r", "u8_r", "u1_v", "u2_v", "u4_v", "u8_v",
];
function createIndex() {
return {
atoms: new Set(),
components: new Set(),
componentAliases: new Set(),
macros: new Map(),
registers: new Map(),
bindTypes: new Set(),
types: new Set(BASE_TYPES),
phases: new Set(),
labels: new Set(),
attributes: new Set(BASE_ATTRIBUTES),
componentCallees: new Map(),
};
}
function cloneIndex(source) {
const result = createIndex();
for (const key of ["atoms", "components", "componentAliases", "bindTypes", "types", "phases", "labels", "attributes"]) {
for (const value of source[key]) result[key].add(value);
}
for (const [name, domain] of source.macros) result.macros.set(name, domain);
for (const [name, domain] of source.registers) result.registers.set(name, domain);
for (const [name, callees] of source.componentCallees) result.componentCallees.set(name, callees.slice());
return result;
}
function mergeIndexes(...sources) {
const result = createIndex();
for (const source of sources) {
if (!source) continue;
for (const key of ["atoms", "components", "componentAliases", "bindTypes", "types", "phases", "labels", "attributes"]) {
for (const value of source[key]) result[key].add(value);
}
for (const [name, domain] of source.macros) {
const existing = result.macros.get(name);
if (!existing || domainRank(domain) >= domainRank(existing)) result.macros.set(name, domain);
}
for (const [name, domain] of source.registers) result.registers.set(name, domain);
for (const [name, callees] of source.componentCallees) {
const existing = result.componentCallees.get(name) || [];
result.componentCallees.set(name, existing.concat(callees));
}
}
return resolveComponentDomains(result);
}
function domainFromPath(filePath) {
const base = path.basename(filePath.replaceAll("\\", "/")).toLowerCase();
if (base === "mips.h") return "cpu";
if (base === "gte.h") return "gte";
if (base === "gp.h") return "gpu";
return null;
}
function prefixDomain(name) {
if (/^(?:branch_|jump_|call_)/.test(name)) return "control";
if (/^gte_(?!cr_)/.test(name) || name.startsWith("mac_gte_") || name.startsWith("ac_gte_")) return "gte";
if (/^gp[01]_/.test(name) || name.startsWith("mac_gp_") || name.startsWith("ac_gp_")) return "gpu";
return null;
}
function collectBraceIdentifiers(tokens, openBraceIndex) {
const names = [];
let depth = 0;
for (let tokenIndex = openBraceIndex; tokenIndex < tokens.length; tokenIndex += 1) {
if (tokens[tokenIndex].text === "{") depth += 1;
if (tokens[tokenIndex].text === "}") {
depth -= 1;
if (depth === 0) break;
}
if (tokens[tokenIndex].kind === "identifier") names.push(tokens[tokenIndex].text);
}
return names;
}
function resolveComponentDomains(index) {
const hardwareRank = { cpu: 1, gpu: 2, gte: 3, control: 4 };
let changed = true;
while (changed) {
changed = false;
for (const [alias, callees] of index.componentCallees) {
let best = index.macros.get(alias) || "component";
let bestRank = hardwareRank[best] || 0;
for (const callee of callees) {
const domain = prefixDomain(callee) || index.macros.get(callee);
const rank = hardwareRank[domain] || 0;
if (rank > bestRank) {
best = domain;
bestRank = rank;
}
}
if (bestRank > 0 && index.macros.get(alias) !== best) {
index.macros.set(alias, best);
changed = true;
}
}
}
return index;
}
function domainRank(domain) {
if (domain === "control") return 4;
if (domain === "cpu" || domain === "gte" || domain === "gpu") return 3;
if (domain === "component") return 2;
return 1;
}
function registerKind(name) {
if (/^R_[A-Za-z0-9_]+$/.test(name)) return "gpr";
if (/^(?:C2_|gte_cr_)[A-Za-z0-9_]+$/.test(name)) return "cop2";
return null;
}
function componentAlias(name) {
return name.startsWith("ac_") ? `mac_${name.slice(3)}` : null;
}
function findFunctionNameBefore(tokens, calleeTokenIndex) {
let closeIndex = calleeTokenIndex - 1;
while (closeIndex >= 0 && tokens[closeIndex].kind === "identifier" && tokens[closeIndex].text === "atom_dbg_skip") {
closeIndex -= 1;
}
if (!tokens[closeIndex] || tokens[closeIndex].text !== ")") return null;
let depth = 1;
for (let tokenIndex = closeIndex - 1; tokenIndex >= 0; tokenIndex -= 1) {
if (tokens[tokenIndex].text === ")") depth += 1;
if (tokens[tokenIndex].text === "(") depth -= 1;
if (depth !== 0) continue;
const name = tokens[tokenIndex - 1];
return name && name.kind === "identifier" ? name : null;
}
return null;
}
function scanSource(source, filePath) {
const lexical = lex(source);
const balanced = buildCallContexts(lexical.tokens);
const tokens = lexical.tokens;
const contexts = balanced.contexts;
const index = createIndex();
const declarations = new Map();
const domain = domainFromPath(filePath);
function mark(token, role, modifiers = ["declaration"]) {
declarations.set(token.start, { role, modifiers });
}
function addComponent(token) {
index.components.add(token.text);
mark(token, "componentName");
const alias = componentAlias(token.text);
if (alias) {
index.componentAliases.add(alias);
index.macros.set(alias, prefixDomain(alias) || prefixDomain(token.text) || "component");
}
}
function bindComponentCallees(alias, callees) {
if (!alias) return;
index.componentAliases.add(alias);
index.componentCallees.set(alias, callees);
if (!index.macros.has(alias)) index.macros.set(alias, "component");
}
for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
const token = tokens[tokenIndex];
if (token.kind !== "identifier") continue;
const kind = registerKind(token.text);
if (kind) {
index.registers.set(token.text, kind);
if (tokens[tokenIndex + 1] && tokens[tokenIndex + 1].text === "=") {
mark(token, kind === "gpr" ? "gprRegister" : "cop2Register");
}
}
const context = nearestCall(contexts, tokenIndex);
if (context && context.argIndex === 0) {
if (context.callee === "MipsAtom_") {
index.atoms.add(token.text);
mark(token, "atomName");
}
if (context.callee === "MipsAtomComp_") addComponent(token);
if (context.callee === "atom_bind") index.bindTypes.add(token.text);
if (context.callee === "atom_phase" || context.callee === "phase_auto_reg") index.phases.add(token.text);
if (context.callee === "atom_label" || context.callee === "atom_offset") index.labels.add(token.text);
}
const isWrappedType = context && (
((context.callee === "Struct_" || context.callee === "Union_") && context.argIndex === 0) ||
(context.callee === "Enum_" && context.argIndex === 1)
);
if (isWrappedType) {
index.types.add(token.text);
mark(token, token.text.startsWith("Binds_") ? "bindType" : "duffleType");
if (token.text.startsWith("Binds_")) index.bindTypes.add(token.text);
}
if (context && context.callee === "atom_offset" && context.argIndex === 1) index.labels.add(token.text);
if (context && context.callee === "atom_auto_reg") {
if (context.argIndex === 0) index.atoms.add(token.text);
if (context.argIndex === 1) {
index.registers.set(token.text, "gpr");
mark(token, "gprRegister", ["declaration", "tapeAuto"]);
}
}
if (context && context.callee === "phase_auto_reg" && context.argIndex === 1) {
index.registers.set(token.text, "gpr");
mark(token, "gprRegister", ["declaration", "tapeAuto"]);
}
if (token.text === "define" && tokens[tokenIndex - 1] && tokens[tokenIndex - 1].text === "#") {
const name = tokens[tokenIndex + 1];
if (name && name.kind === "identifier" && name.line === token.line) {
if (/^(?:RegUse_|Struct_|Enum_|Union_|TypeR_|TypeV_|Relative_|Binds_)/.test(name.text)) {
index.types.add(name.text);
} else if (/^(?:ac_|mac_)/.test(name.text)) {
const alias = name.text.startsWith("ac_") ? componentAlias(name.text) : name.text;
const rest = [];
for (let restIndex = tokenIndex + 2; restIndex < tokens.length && tokens[restIndex].line === name.line; restIndex += 1) {
if (tokens[restIndex].kind === "identifier") rest.push(tokens[restIndex].text);
}
if (alias) {
index.componentAliases.add(alias);
index.macros.set(alias, prefixDomain(alias) || "component");
if (rest.length) index.componentCallees.set(alias, rest);
}
} else {
index.macros.set(name.text, domain || "utility");
}
}
}
if (token.text === "typedef") {
let endIndex = tokenIndex + 1;
let hasBrace = false;
let lastIdentifier = null;
while (endIndex < tokens.length && tokens[endIndex].text !== ";") {
if (tokens[endIndex].text === "{") hasBrace = true;
if (tokens[endIndex].kind === "identifier" && !C_BUILTINS.has(tokens[endIndex].text)) lastIdentifier = tokens[endIndex];
endIndex += 1;
}
if (!hasBrace && lastIdentifier && !C_BUILTINS.has(lastIdentifier.text)) {
index.types.add(lastIdentifier.text);
mark(lastIdentifier, "duffleType");
}
}
if (token.text === "MipsAtom_Proc_") {
const functionName = findFunctionNameBefore(tokens, tokenIndex);
if (functionName) {
const atomName = functionName.text.endsWith("_proc")
? functionName.text.slice(0, -5)
: functionName.text;
index.atoms.add(atomName);
index.atoms.add(functionName.text);
mark(functionName, "atomName");
}
}
if (token.text === "MipsAtomComp_Proc_") {
const functionName = findFunctionNameBefore(tokens, tokenIndex);
if (functionName) addComponent(functionName);
}
}
for (const call of balanced.calls) {
if (call.callee === "MipsAtomComp_") {
const name = tokens[call.openTokenIndex + 1];
const brace = tokens[call.closeTokenIndex + 1];
if (name && name.kind === "identifier" && brace && brace.text === "{") {
bindComponentCallees(componentAlias(name.text), collectBraceIdentifiers(tokens, call.closeTokenIndex + 1));
}
}
if (call.callee === "MipsAtomComp_Proc_") {
const functionName = findFunctionNameBefore(tokens, call.calleeTokenIndex);
let braceIndex = -1;
for (let tokenIndex = call.openTokenIndex + 1; tokenIndex < call.closeTokenIndex; tokenIndex += 1) {
if (tokens[tokenIndex].text === "{") {
braceIndex = tokenIndex;
break;
}
}
if (functionName && braceIndex >= 0) {
bindComponentCallees(componentAlias(functionName.text), collectBraceIdentifiers(tokens, braceIndex));
}
}
if (!domain) continue;
const name = tokens[call.calleeTokenIndex];
const after = tokens[call.closeTokenIndex + 1];
if (!name || !after || after.text !== "{") continue;
if (/^(?:gp0_|gp1_|gte_|mac_)/.test(name.text)) index.macros.set(name.text, domain);
}
return {
index: resolveComponentDomains(cloneIndex(index)),
declarations,
tokens,
contexts,
errors: [...lexical.errors, ...balanced.errors],
};
}
module.exports = {
createIndex,
domainFromPath,
mergeIndexes,
resolveComponentDomains,
scanSource,
};
@@ -1,71 +0,0 @@
{
"scopeName": "tape_atom.injection",
"injectionSelector": "L:source.c -comment -string, L:source.cpp -comment -string",
"patterns": [
{ "include": "#atom-declarations" },
{ "include": "#component-declarations" },
{ "include": "#annotation-arguments" },
{ "include": "#annotations" },
{ "include": "#delay-slots" },
{ "include": "#types" },
{ "include": "#attributes" }
],
"repository": {
"atom-declarations": {
"patterns": [
{
"match": "\\b(MipsAtom_)\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": { "name": "keyword.control.duffle.atom" },
"2": { "name": "entity.name.function.duffle.atom" }
}
},
{ "match": "\\bMipsAtom_Proc_\\b", "name": "keyword.control.duffle.atom" },
{ "match": "\\b[A-Za-z_][A-Za-z0-9_]*_proc\\b", "name": "entity.name.function.duffle.atom" }
]
},
"component-declarations": {
"patterns": [
{
"match": "\\b(MipsAtomComp_)\\s*\\(\\s*(ac_[A-Za-z0-9_]*)",
"captures": {
"1": { "name": "keyword" },
"2": { "name": "entity.name.function.duffle.component" }
}
},
{ "match": "\\bMipsAtomComp_Proc_\\b", "name": "keyword" }
]
},
"annotation-arguments": {
"patterns": [
{
"match": "\\b(atom_offset)\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*,\\s*([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": { "name": "support.function.duffle.annotation" },
"2": { "name": "entity.name.label.duffle.atom" },
"3": { "name": "entity.name.label.duffle.atom" }
}
},
{ "match": "(?<=\\batom_bind\\()\\s*Binds_[A-Za-z0-9_]+", "name": "entity.name.type.duffle.bind" },
{ "match": "(?<=\\batom_phase\\()\\s*[A-Za-z_][A-Za-z0-9_]*", "name": "entity.name.tag.duffle.phase" },
{ "match": "(?<=\\batom_label\\()\\s*[A-Za-z_][A-Za-z0-9_]*", "name": "entity.name.label.duffle.atom" }
]
},
"annotations": {
"match": "\\b(atom_info|atom_bind|atom_reads|atom_writes|atom_label|atom_offset|atom_reg|atom_type|atom_ctx|atom_phase|atom_auto_reg|phase_auto_reg|atom_dbg_skip)\\b",
"name": "support.function.duffle.annotation"
},
"delay-slots": {
"match": "\\b(LdSlot_|BdSlot_|DmaSlot_|GteDelay_)\\b",
"name": "keyword.operator.duffle.delayslot"
},
"types": {
"match": "\\b(?:Binds_[A-Za-z0-9_]+|RegUse_[A-Za-z0-9_]+)\\b",
"name": "storage.type.duffle.type"
},
"attributes": {
"match": "\\b(?:FI_|I_|NI_|Relative_|Struct_|Enum_|Union_|Array_|Slice_|TypeR_|TypeV_|align_|internal|local_persist|global|RO_|LP_|gknown|expect_|cexpr_|asm|asm_words|asm_rpins|asm_clobber|O_|S_|C_|T_|tmpl|glue|r_|v_|tr_|tv_|rgcc|r_use|r_set|r_mod|r_imm|r_mem|u[1248]_|u[1248]_r|u[1248]_v|s[1248]_)\\b",
"name": "keyword"
}
}
}
Binary file not shown.
-128
View File
@@ -1,128 +0,0 @@
"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const { classifyDocument } = require("../classifier");
const { createIndex } = require("../source-index");
function byText(result, text) {
return result.spans.filter((span) => span.text === text);
}
test("classifyDocument distinguishes declaration, annotation, phase, bind, and label roles", () => {
const source = [
"typedef Struct_(Binds_CubeTri) { U4 PrimCursor; };",
"MipsAtom_(cube_g4_face) atom_info(atom_bind(Binds_CubeTri), atom_phase(cube_g4),",
"\tatom_reads(R_PrimCursor), atom_writes(R_FaceCursor)) {",
"\tbranch_le_zero(R_T0, atom_offset(cull, exit)),",
"\tatom_label(exit)",
"};",
].join("\n");
const result = classifyDocument(source, "C:/x/code/hello_camera/hello_camera.atom.c", createIndex());
assert.equal(byText(result, "MipsAtom_")[0].type, "tapeAtomKeyword");
assert.deepEqual(byText(result, "cube_g4_face")[0].modifiers, ["declaration"]);
assert.equal(byText(result, "atom_bind")[0].type, "tapeAnnotation");
assert.equal(byText(result, "Binds_CubeTri").at(-1).type, "tapeBindType");
assert.equal(byText(result, "cube_g4")[0].type, "tapePhase");
assert.deepEqual(byText(result, "cube_g4")[0].modifiers, ["declaration"]);
assert.equal(byText(result, "cull")[0].type, "tapeLabel");
assert.equal(byText(result, "exit").every((span) => span.type === "tapeLabel"), true);
});
test("classifyDocument applies read and write modifiers to GPRs", () => {
const source = "atom_info(atom_reads(R_PrimCursor), atom_writes(R_FaceCursor))";
const result = classifyDocument(source, "C:/x/code/test.atom.c", createIndex());
assert.deepEqual(byText(result, "R_PrimCursor")[0].modifiers, ["tapeRead"]);
assert.deepEqual(byText(result, "R_FaceCursor")[0].modifiers, ["tapeWrite"]);
});
test("classifyDocument separates CPU, GTE, GPU, and component domains", () => {
const workspace = createIndex();
workspace.macros.set("load_word", "cpu");
workspace.macros.set("gte_cmdw_rtpt", "gte");
workspace.macros.set("gp1_word_DisplayOn", "gpu");
workspace.macros.set("mac_yield", "control");
workspace.componentAliases.add("mac_yield");
const source = "load_word(R_T0, R_T1, 0), gte_cmdw_rtpt, gp1_word_DisplayOn(), mac_yield(), C2_MAC0, gte_cr_OFX_Code";
const result = classifyDocument(source, "C:/x/code/test.c", workspace);
assert.equal(byText(result, "load_word")[0].type, "tapeCpuInstruction");
assert.equal(byText(result, "gte_cmdw_rtpt")[0].type, "tapeGteInstruction");
assert.equal(byText(result, "gp1_word_DisplayOn")[0].type, "tapeGpuInstruction");
assert.equal(byText(result, "mac_yield")[0].type, "tapeControlFlow");
assert.equal(byText(result, "C2_MAC0")[0].type, "tapeCop2Register");
assert.equal(byText(result, "gte_cr_OFX_Code")[0].type, "tapeCop2Register");
});
test("component invocations keep the domain resolved from their emitted instructions", () => {
const workspace = createIndex();
workspace.macros.set("mac_load_word_imm", "cpu");
workspace.macros.set("mac_gcmd_push", "gpu");
workspace.macros.set("mac_gte_store_f3", "gte");
workspace.macros.set("mac_load_v3s4", "cpu");
const source = "mac_load_word_imm(dst, imm), mac_gcmd_push(cmd), mac_gte_store_f3(cursor), mac_load_v3s4()";
const result = classifyDocument(source, "C:/x/code/hello_camera/hello_camera.atom.c", workspace);
assert.equal(byText(result, "mac_load_word_imm")[0].type, "tapeCpuInstruction");
assert.equal(byText(result, "mac_gcmd_push")[0].type, "tapeGpuInstruction");
assert.equal(byText(result, "mac_gte_store_f3")[0].type, "tapeGteInstruction");
assert.equal(byText(result, "mac_load_v3s4")[0].type, "tapeCpuInstruction");
});
test("utility macros without a hardware domain use the standard macro token", () => {
const workspace = createIndex();
workspace.macros.set("load_word", "cpu");
workspace.macros.set("assert", "utility");
workspace.macros.set("stringify", "utility");
workspace.macros.set("u4_hi", "utility");
const source = "load_word(R_T0, R_T1, 0), assert(ok), stringify(name), u4_hi(imm)";
const result = classifyDocument(source, "C:/x/code/hello_camera/hello_camera.c", workspace);
assert.equal(byText(result, "load_word")[0].type, "tapeCpuInstruction");
assert.equal(byText(result, "assert")[0].type, "macro");
assert.equal(byText(result, "stringify")[0].type, "macro");
assert.equal(byText(result, "u4_hi")[0].type, "macro");
});
test("document-local declarations override an empty workspace index", () => {
const source = [
"MipsAtomComp_(ac_new_component) { nop };",
"MipsAtomComp_Proc_(ab, { nop })",
"mac_new_component(),",
].join("\n");
const result = classifyDocument(source, "C:/x/code/duffle/math.atom.c", createIndex());
assert.equal(byText(result, "MipsAtomComp_")[0].type, "keyword");
assert.equal(byText(result, "MipsAtomComp_Proc_")[0].type, "keyword");
assert.equal(byText(result, "ac_new_component")[0].type, "tapeComponentName");
assert.equal(byText(result, "mac_new_component")[0].type, "tapeComponentInstruction");
});
test("delay slot markers share the tapeDelaySlot token", () => {
const source = "LdSlot_ nop, BdSlot_ nop, DmaSlot_ nop2, GteDelay_ nop";
const result = classifyDocument(source, "C:/x/code/duffle/gte.atom.c", createIndex());
assert.equal(byText(result, "LdSlot_")[0].type, "tapeDelaySlot");
assert.equal(byText(result, "BdSlot_")[0].type, "tapeDelaySlot");
assert.equal(byText(result, "DmaSlot_")[0].type, "tapeDelaySlot");
assert.equal(byText(result, "GteDelay_")[0].type, "tapeDelaySlot");
});
test("classifier returns ordered non-overlapping spans and partial malformed output", () => {
const source = "atom_reads(R_A /* broken";
const result = classifyDocument(source, "C:/x/code/test.atom.c", createIndex());
assert.equal(result.errors.some((error) => error.kind === "unterminated-block-comment"), true);
for (let spanIndex = 1; spanIndex < result.spans.length; spanIndex += 1) {
const previous = result.spans[spanIndex - 1];
const current = result.spans[spanIndex];
assert.equal(previous.start + previous.length <= current.start, true);
}
});
-88
View File
@@ -1,88 +0,0 @@
"use strict";
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const { TOKEN_MODIFIERS, TOKEN_TYPES } = require("../classifier");
const ROOT = path.resolve(__dirname, "..");
function readJson(filePath) {
const raw = fs.readFileSync(filePath, "utf8");
const stripped = raw.replace(/\/\/.*$/gm, "").replace(/,\s*([}\]])/g, "$1");
return JSON.parse(stripped);
}
function collectScopeNames(value, output = new Set()) {
if (Array.isArray(value)) {
for (const entry of value) collectScopeNames(entry, output);
return output;
}
if (!value || typeof value !== "object") return output;
if (typeof value.name === "string") output.add(value.name);
for (const child of Object.values(value)) collectScopeNames(child, output);
return output;
}
test("package semantic legend matches classifier exports", () => {
const packageJson = readJson(path.join(ROOT, "package.json"));
const contributedTypes = packageJson.contributes.semanticTokenTypes.map((entry) => entry.id);
const contributedModifiers = packageJson.contributes.semanticTokenModifiers.map((entry) => entry.id);
assert.equal(packageJson.version, "0.3.0");
assert.deepEqual(contributedTypes, TOKEN_TYPES);
assert.deepEqual(contributedModifiers, TOKEN_MODIFIERS.filter((name) => name !== "declaration"));
});
test("package includes runtime files only and acknowledges local-only metadata", () => {
const packageJson = readJson(path.join(ROOT, "package.json"));
assert.deepEqual(packageJson.files, [
"classifier.js",
"extension.js",
"lexer.js",
"source-index.js",
"syntaxes/tape_atom.tmLanguage.json",
]);
assert.equal(packageJson.scripts.package.includes("--allow-missing-repository"), true);
assert.equal(packageJson.scripts.package.includes("--skip-license"), true);
});
test("every semantic token has a scope mapping; DSL-specific tokens also have grammar scopes", () => {
const packageJson = readJson(path.join(ROOT, "package.json"));
const grammar = readJson(path.join(ROOT, "syntaxes", "tape_atom.tmLanguage.json"));
const mappings = packageJson.contributes.semanticTokenScopes[0].scopes;
const grammarScopes = collectScopeNames(grammar);
const grammarRequired = new Set([
"tapeAtomKeyword", "tapeAtomName", "tapeComponentName",
"tapeAnnotation", "tapeBindType", "tapePhase", "tapeLabel",
"tapeDelaySlot", "tapeDuffleType", "keyword",
]);
for (const tokenType of TOKEN_TYPES) {
assert.equal(Array.isArray(mappings[tokenType]), true, `missing scope mapping: ${tokenType}`);
if (grammarRequired.has(tokenType)) {
assert.equal(mappings[tokenType].some((scope) => grammarScopes.has(scope)), true, `grammar does not emit: ${tokenType}`);
}
}
});
test("TextMate offset labels stay scoped to atom_offset calls", () => {
const grammar = readJson(path.join(ROOT, "syntaxes", "tape_atom.tmLanguage.json"));
const serialized = JSON.stringify(grammar);
const offsetRule = grammar.repository["annotation-arguments"].patterns
.find((rule) => rule.match.includes("atom_offset"));
assert.equal(serialized.includes("(?<=,)"), false);
assert.equal(offsetRule.captures[1].name, "support.function.duffle.annotation");
assert.equal(offsetRule.captures[2].name, "entity.name.label.duffle.atom");
assert.equal(offsetRule.captures[3].name, "entity.name.label.duffle.atom");
});
test("workspace enables semantic highlighting", () => {
const settings = readJson(path.resolve(ROOT, "..", "settings.json"));
assert.equal(settings["editor.semanticHighlighting.enabled"], true);
});
-77
View File
@@ -1,77 +0,0 @@
"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const { buildCallContexts, lex, nearestCall } = require("../lexer");
test("lex skips comments, strings, and character literals", () => {
const source = [
"MipsAtom_(visible)",
"// MipsAtom_(line_comment)",
"const char *s = \"atom_reads(R_Hidden)\";",
"char c = '\\''; /* gte_cmdw_hidden */",
"atom_reads(R_Visible)",
].join("\n");
const result = lex(source);
const identifiers = result.tokens
.filter((token) => token.kind === "identifier")
.map((token) => token.text);
assert.deepEqual(result.errors, []);
assert.equal(identifiers.includes("visible"), true);
assert.equal(identifiers.includes("R_Visible"), true);
assert.equal(identifiers.includes("line_comment"), false);
assert.equal(identifiers.includes("R_Hidden"), false);
assert.equal(identifiers.includes("gte_cmdw_hidden"), false);
});
test("lex reports unterminated block comments without returning comment tokens", () => {
const result = lex("R_Visible /* atom_reads(R_Hidden)");
assert.equal(result.tokens.some((token) => token.text === "R_Visible"), true);
assert.equal(result.tokens.some((token) => token.text === "R_Hidden"), false);
assert.deepEqual(result.errors.map((error) => error.kind), ["unterminated-block-comment"]);
});
test("line comments stop at CRLF boundaries", () => {
const result = lex("// atom_reads(R_Hidden)\r\natom_reads(R_Visible)\r\n");
const identifiers = result.tokens
.filter((token) => token.kind === "identifier")
.map((token) => token.text);
assert.equal(identifiers.includes("R_Hidden"), false);
assert.equal(identifiers.includes("R_Visible"), true);
});
test("balanced contexts retain multiline nesting and argument indexes", () => {
const source = [
"atom_info(",
"\tatom_phase(cube_g4),",
"\tatom_reads(R_A, nested(R_B, R_C)),",
"\tatom_writes(R_D)",
")",
].join("\n");
const lexical = lex(source);
const balanced = buildCallContexts(lexical.tokens);
const byText = new Map();
lexical.tokens.forEach((token, index) => {
if (token.kind === "identifier") byText.set(token.text, index);
});
assert.equal(nearestCall(balanced.contexts, byText.get("cube_g4")).callee, "atom_phase");
assert.equal(nearestCall(balanced.contexts, byText.get("R_A")).callee, "atom_reads");
assert.equal(nearestCall(balanced.contexts, byText.get("R_A")).argIndex, 0);
assert.equal(nearestCall(balanced.contexts, byText.get("R_C")).callee, "nested");
assert.equal(nearestCall(balanced.contexts, byText.get("R_D")).callee, "atom_writes");
assert.deepEqual(balanced.errors, []);
});
test("balanced contexts report unmatched parentheses", () => {
const lexical = lex("atom_reads(R_A");
const balanced = buildCallContexts(lexical.tokens);
assert.deepEqual(balanced.errors.map((error) => error.kind), ["unmatched-open-paren"]);
});
-134
View File
@@ -1,134 +0,0 @@
"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const {
createIndex,
domainFromPath,
mergeIndexes,
scanSource,
} = require("../source-index");
test("scanSource discovers current atom and component forms", () => {
const source = [
"MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4), atom_reads(R_PrimCursor)) { mac_yield() };",
"MipsAtomComp_(ac_load_pair) { load_word(R_T0, R_T1, 0) };",
"internal MipsAtom* normalize_proc(AtomArena_R aa) MipsAtom_Proc_(aa, { mac_yield() })",
"FI_ void ac_store_pair(MipsAtomBuilder_R ab) atom_dbg_skip MipsAtomComp_Proc_(ab, { store_word(R_T0, R_T1, 0) })",
].join("\n");
const result = scanSource(source, "C:/projects/Pikuma/ps1/code/duffle/mips.atom.c");
assert.equal(result.index.atoms.has("cube_g4_face"), true);
assert.equal(result.index.atoms.has("normalize"), true);
assert.equal(result.index.components.has("ac_load_pair"), true);
assert.equal(result.index.components.has("ac_store_pair"), true);
assert.equal(result.index.componentAliases.has("mac_load_pair"), true);
assert.equal(result.index.componentAliases.has("mac_store_pair"), true);
assert.equal(result.index.macros.get("mac_store_pair"), "component");
assert.equal(result.index.componentCallees.get("mac_store_pair").includes("store_word"), true);
assert.equal(result.index.phases.has("cube_g4"), true);
assert.equal(result.index.registers.get("R_PrimCursor"), "gpr");
assert.deepEqual(result.errors, []);
});
test("scanSource discovers binds, labels, registers, typedefs, and macro domains", () => {
const source = [
"typedef Struct_(Binds_CubeTri) { U4 PrimCursor; };",
"typedef Enum_(U4, PadStatus) { PadStatus_Ok };",
"typedef U4 const MipsCode;",
"enum { R_PrimCursor = R_T7 atom_reg, C2_Custom = 12, gte_cr_Custom = 13 };",
"#define load_word(rt, base, off) enc_i(rt, base, off)",
"atom_bind(Binds_CubeTri)",
"atom_label(exit)",
"atom_offset(entry, exit)",
].join("\n");
const result = scanSource(source, "C:/projects/Pikuma/ps1/code/duffle/mips.h");
assert.equal(result.index.bindTypes.has("Binds_CubeTri"), true);
assert.equal(result.index.types.has("PadStatus"), true);
assert.equal(result.index.types.has("MipsCode"), true);
assert.equal(result.index.registers.get("R_PrimCursor"), "gpr");
assert.equal(result.index.registers.get("C2_Custom"), "cop2");
assert.equal(result.index.registers.get("gte_cr_Custom"), "cop2");
assert.equal(result.index.macros.get("load_word"), "cpu");
assert.equal(result.index.labels.has("entry"), true);
assert.equal(result.index.labels.has("exit"), true);
});
test("domainFromPath uses the declaration file rather than parent directory names", () => {
assert.equal(domainFromPath("C:/x/code/hello_gte/hello_gte.atom.c"), null);
assert.equal(domainFromPath("C:/x/code/duffle/mips.h"), "cpu");
assert.equal(domainFromPath("C:/x/code/duffle/gte.h"), "gte");
assert.equal(domainFromPath("C:/x/code/duffle/gp.h"), "gpu");
});
test("component aliases inherit the domain of the instructions they emit", () => {
const headers = mergeIndexes(
scanSource("#define load_word(a,b,c) 1\n#define store_word(a,b,c) 1\n#define shift_aright_var(a,b,c) 1\n#define jump_reg(rd) 1\n", "C:/x/code/duffle/mips.h").index,
scanSource("#define gte_sw(rt, base, off) 1\n", "C:/x/code/duffle/gte.h").index
);
const math = scanSource(
[
"MipsAtomComp_(ac_load_v3s4) { load_word(R_T0, R_T1, 0) };",
"#define mac_load_p3s4 mac_load_v3s4",
].join("\n"),
"C:/x/code/duffle/math.atom.c"
);
const shift = scanSource(
"MipsAtomComp_(ac_shift_aright_var_v3_self) { shift_aright_var(R_T0, R_T0, R_T1) };",
"C:/x/code/duffle/gte.atom.c"
);
const gte = scanSource(
"MipsAtomComp_(ac_gte_store_f3) { gte_sw(C2_SXY0, R_T0, 0) };",
"C:/x/code/duffle/gte.atom.c"
);
const yieldAtom = scanSource(
"MipsAtomComp_(ac_yield) { load_word(R_AtomJmp, R_TapePtr, 0), jump_reg(R_AtomJmp), nop };",
"C:/x/code/duffle/lottes_tape.h"
);
const merged = mergeIndexes(headers, math.index, shift.index, gte.index, yieldAtom.index);
assert.equal(merged.macros.get("mac_load_v3s4"), "cpu");
assert.equal(merged.macros.get("mac_load_p3s4"), "cpu");
assert.equal(merged.macros.get("mac_shift_aright_var_v3_self"), "cpu");
assert.equal(merged.macros.get("mac_gte_store_f3"), "gte");
assert.equal(merged.macros.get("mac_yield"), "control");
});
test("scanSource tags utility header defines as utility, not a hardware domain", () => {
const source = [
"#define assert(cond) ((void)(cond))",
"#define stringify(name) #name",
"#define u4_hi(imm) ((imm) >> 16)",
].join("\n");
const result = scanSource(source, "C:/projects/Pikuma/ps1/code/duffle/dsl.h");
assert.equal(result.index.macros.get("assert"), "utility");
assert.equal(result.index.macros.get("stringify"), "utility");
assert.equal(result.index.macros.get("u4_hi"), "utility");
});
test("mergeIndexes prefers a hardware domain over a later utility define", () => {
const left = createIndex();
left.macros.set("sub_s", "utility");
const right = createIndex();
right.macros.set("sub_s", "cpu");
assert.equal(mergeIndexes(left, right).macros.get("sub_s"), "cpu");
assert.equal(mergeIndexes(right, left).macros.get("sub_s"), "cpu");
});
test("mergeIndexes preserves domain-specific aliases", () => {
const left = createIndex();
left.macros.set("load_word", "cpu");
const right = createIndex();
right.componentAliases.add("mac_gte_store");
right.macros.set("mac_gte_store", "gte");
const merged = mergeIndexes(left, right);
assert.equal(merged.macros.get("load_word"), "cpu");
assert.equal(merged.macros.get("mac_gte_store"), "gte");
});
+21 -14
View File
@@ -1,17 +1,24 @@
Copyright (C) 2026 Edward R. Gonzalez This is free and unencumbered software released into the public domain.
This software is provided 'as-is', without any express or implied Anyone is free to copy, modify, publish, use, compile, sell, or
warranty. In no event will the authors be held liable for any damages distribute this software, either in source code form or as a compiled
arising from the use of this software. binary, for any purpose, commercial or non-commercial, and by any
means.
Permission is granted to anyone to use this software for any purpose, In jurisdictions that recognize copyright laws, the author or authors
including commercial applications, and to alter it and redistribute it of this software dedicate any and all copyright interest in the
freely, subject to the following restrictions: software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
1. The origin of this software must not be misrepresented; you must not THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
claim that you wrote the original software. If you use this software EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
in a product, an acknowledgment in the product documentation would be MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
appreciated but is not required. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
2. Altered source versions must be plainly marked as such, and must not be OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
misrepresented as being the original software. ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
3. This notice may not be removed or altered from any source distribution. OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>
-15
View File
@@ -1,15 +0,0 @@
#ifdef INTELLISENSE_DIRECTIVES
# pragma once
#endif
enum {
bios_init_pad_2 = 0x12,
bios_start_pad_2 = 0x13,
bios_flushcache = 0x44,
bios_table_addr = 0xA0,
bios_btable_addr = 0xB0,
};
enum {
bios_pad_buffer_size = 0x22,
};
+2 -24
View File
@@ -70,31 +70,11 @@
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* atom_reg (per-enum opt-in marker for the DWARF register-alias registry) * atom_reg (per-enum opt-in marker for the DWARF register-alias registry)
* *
* Bare `atom_reg` token adjacent to an enum entry that alias as debug-visible for scan_source's register_alias_registry. * The bare `atom_reg` token adjacent to an enum entry in mips.h / lottes_tape.h flags that alias as debug-visible for scan_source's register_alias_registry.
* Lua scanner reads the bare token. * The C preprocessor strips it to a comment so no runtime symbol is created; the Lua scanner reads the bare token.
* ----------------------------------------------------------------------------*/ * ----------------------------------------------------------------------------*/
#define atom_reg /* atom_reg: opt the preceding enum entry into the DWARF registry */ #define atom_reg /* atom_reg: opt the preceding enum entry into the DWARF registry */
// ----------------------------------------------------------------------------
// atom_auto_reg(atom, sym) — per-atom auto-allocated GPR binding.
// enum {
// atom_auto_reg(cube_g4_face, R_Fwdx), // expands to: R_Fwdx = R_Fwdx_Code /* atom_auto_reg: cube_g4_face */,
// atom_auto_reg(cube_g4_face, R_Eye_z) atom_type(S4), // atom_type chains after
// };
// (The macro IS the entire enum entry — no separate LHS=RHS. The `atom` scope is
// preserved in a trailing C-comment on the RHS so the Lua scanner can recover
// it after preprocessing strips the macro form. R_<Sym>_Code is resolved from gen/auto_reg.h which the .c file #include's before the enum declaration.)
#define atom_auto_reg(atom, sym) sym = sym ## _Code /* atom_auto_reg: atom */
// ----------------------------------------------------------------------------
// phase_auto_reg(phase, sym) — per-phase auto-allocated GPR binding.
// enum {
// phase_auto_reg(cube_g4, R_Temp0), // expands to: R_Temp0 = R_Temp0_Code /* phase_auto_reg: cube_g4 */,
// phase_auto_reg(cube_g4, R_Temp1),
// };
// (Same macro-as-enum-entry form as atom_auto_reg above; the `phase` scope is preserved in a trailing C-comment on the RHS for the Lua scanner to recover.)
#define phase_auto_reg(phase, sym) sym = sym ## _Code /* phase_auto_reg: phase */
/* ============================================================================ /* ============================================================================
* atom_info : * atom_info :
* MipsAtom_(cube_tri) atom_info( * MipsAtom_(cube_tri) atom_info(
@@ -160,8 +140,6 @@
* ----------------------------------------------------------------------------*/ * ----------------------------------------------------------------------------*/
#define atom_bind(binds_struct) /* atom_bind(binds_struct) */ #define atom_bind(binds_struct) /* atom_bind(binds_struct) */
#define Binds_(type) (tmpl(Binds,type)) // TODO(Ed): Do we want to use this?
/* ============================================================================ /* ============================================================================
* atom_label / atom_offset — branch target machinery * atom_label / atom_offset — branch target machinery
* *
+18 -25
View File
@@ -3,7 +3,7 @@
# include "assert.h" # include "assert.h"
#endif #endif
#define offset_of(type, member) cast(U8,__builtin_offsetof(type,member)) // Compiler builtin version of O_ #define offset_of(type, member) cast(U8,__builtin_offsetof(type,member))
#define static_assert _Static_assert #define static_assert _Static_assert
#define typeof __typeof__ #define typeof __typeof__
#define typeof_ptr(ptr) typeof((ptr)[0]) #define typeof_ptr(ptr) typeof((ptr)[0])
@@ -28,9 +28,8 @@
#define internal static // internal #define internal static // internal
#define asm __asm__ #define asm __asm__
#define A_(data) (& data)
#define align_(value) __attribute__((aligned (value))) // for easy alignment #define align_(value) __attribute__((aligned (value))) // for easy alignment
#define align_(value) __attribute__((aligned (value))) // for easy alignment #define align_(value) __attribute__((aligned (value))) // for easy alignment
#define C_(type,data) ((type)(data)) // for enforced precedence #define C_(type,data) ((type)(data)) // for enforced precedence
#define expect_(x, y) __builtin_expect(x, y) // so compiler knows the common path #define expect_(x, y) __builtin_expect(x, y) // so compiler knows the common path
@@ -91,13 +90,12 @@
#define PtrSet_(type) TypeR_(type); typedef TypeV_(type) #define PtrSet_(type) TypeR_(type); typedef TypeV_(type)
#define TSet_(type) type; typedef PtrSet_(type) #define TSet_(type) type; typedef PtrSet_(type)
#define Array_len(a) (U4)(sizeof(a) / sizeof(typeof((a)[0]))) #define array_len(a) (U4)(sizeof(a) / sizeof(typeof((a)[0])))
#define Array_decl(type, ...) (type[]){__VA_ARGS__} #define array_decl(type, ...) (type[]){__VA_ARGS__}
#define Array_sym(type,len) A ## len ## _ ## type #define Array_sym(type,len) A ## len ## _ ## type
#define Array_expand(type,len) type Array_sym(type, len)[len]; typedef PtrSet_(Array_sym(type, len)) #define Array_expand(type,len) type Array_sym(type, len)[len]; typedef PtrSet_(Array_sym(type, len))
#define Array_(type,len) Array_expand(type,len) #define Array_(type,len) Array_expand(type,len)
#define Bit_(id,b) id = (1 << b), tmpl(id,pos) = b #define Bit_(id,b) id = (1 << b), tmpl(id,pos) = b
#define Bitmask_(b) (1u << b)
#define Enum_(underlying_type, symbol) underlying_type TSet_(symbol); enum symbol #define Enum_(underlying_type, symbol) underlying_type TSet_(symbol); enum symbol
#define Proc_(symbol) symbol #define Proc_(symbol) symbol
#define Relative_(symbol) // Does nothing but annotate that a symbol is associated with another. #define Relative_(symbol) // Does nothing but annotate that a symbol is associated with another.
@@ -135,21 +133,21 @@ typedef __UINT32_TYPE__ TSet_(B4);
#define u4_v(value) C_(U4 V_*, value) #define u4_v(value) C_(U4 V_*, value)
enum { false = 0, true = 1, true_overflow, }; enum { false = 0, true = 1, true_overflow, };
#define u4_lo(value) (u4_(value) & 0xFFFFU) #define u4_lo(value) ((value) & 0xFFFFU)
#define u4_hi(value) (u4_(value) >> (S_(U2) * 8)) #define u4_hi(value) ((value) >> 12)
typedef void Proc_(VoidFn) (void); typedef void Proc_(VoidFn) (void);
#define Kilo_(n) (C_(U4, n) << 10) #define kilo(n) (C_(U4, n) << 10)
#define Mega_(n) (C_(U4, n) << 20) #define mega(n) (C_(U4, n) << 20)
#define Giga_(n) (C_(U4, n) << 30) #define giga(n) (C_(U4, n) << 30)
#define Tera_(n) (C_(U4, n) << 40) #define tera(n) (C_(U4, n) << 40)
#define null C_(U4, 0) #define null C_(U4, 0)
#define nullptr C_(void*, 0) #define nullptr C_(void*, 0)
#define O_(type, field) C_(U4, & C_(type*,0)->field) #define O_(type, field) C_(U4, & C_(type*,0)->field)
#define OA_(type, aexpr) C_(U4, & C_(type*,0) aexpr) #define OA_(type, member, idx) C_(U4, & C_(type*,0)->member[idx])
#define OT_(field) O_(typeof_ptr(& field), field)) #define OT_(field) O_(typeof_ptr(& field), filed))
#define S_(data) C_(U4, sizeof(data)) #define S_(data) C_(U4, sizeof(data))
#define sop_1(op,a,b) C_(U1, s1_(a) op s1_(b)) #define sop_1(op,a,b) C_(U1, s1_(a) op s1_(b))
@@ -170,8 +168,6 @@ def_signed_ops(le, <=)
#undef def_signed_ops #undef def_signed_ops
#undef def_signed_op #undef def_signed_op
// Unused, we arent' doing any C-like asm since we have the asm dsl. We'll keep the non-generics if we somehow do.
#if 0
#define def_generic_sop(op, a, ...) _Generic((a), U1: op ## _s1, U2: op ## _s2, U4: op ## _s4) (a, __VA_ARGS__) #define def_generic_sop(op, a, ...) _Generic((a), U1: op ## _s1, U2: op ## _s2, U4: op ## _s4) (a, __VA_ARGS__)
#define add_s(a,b) def_generic_sop(add,a,b) #define add_s(a,b) def_generic_sop(add,a,b)
#define sub_s(a,b) def_generic_sop(sub,a,b) #define sub_s(a,b) def_generic_sop(sub,a,b)
@@ -181,12 +177,11 @@ def_signed_ops(le, <=)
#define ge_s(a,b) def_generic_sop(ge, a,b) #define ge_s(a,b) def_generic_sop(ge, a,b)
#define le_s(a,b) def_generic_sop(le, a,b) #define le_s(a,b) def_generic_sop(le, a,b)
#undef def_generic_sop #undef def_generic_sop
#endif
#define alignas _Alignas #define alignas _Alignas
#define alignof _Alignof #define alignof _Alignof
#define byte_pad(amount, ...) B1 glue(_PAD_, __VA_ARGS__) [amount] #define byte_pad(amount, ...) B1 glue(_PAD_, __VA_ARGS__) [amount]
#define C_ptr(type, data) (C_(type*, & (data)) [0]) #define pcast(type, data) (C_(type*, & (data)) [0])
#define dbg_args(...) __VA_ARGS__ #define dbg_args(...) __VA_ARGS__
@@ -201,8 +196,6 @@ def_signed_ops(le, <=)
#define defer_info(type,expr, ...) for(type info= {__VA_ARGS__}; info.once!=1;++info.once,(expr)) // Defer with tracked state #define defer_info(type,expr, ...) for(type info= {__VA_ARGS__}; info.once!=1;++info.once,(expr)) // Defer with tracked state
#define do_while(cond) for (U8 once=0; once!=1 || (cond); ++once) #define do_while(cond) for (U8 once=0; once!=1 || (cond); ++once)
#define Jmp_nZero_(cond,label) if (cond) goto label;
#pragma endregion Control Flow & Iteration #pragma endregion Control Flow & Iteration
#define span_iter(type, iter, m_begin, op, m_end) ( \ #define span_iter(type, iter, m_begin, op, m_end) ( \
@@ -219,16 +212,16 @@ def_signed_ops(le, <=)
typedef Span_(S4); typedef Span_(S4);
typedef Span_(U4); typedef Span_(U4);
#if 0
#pragma region Debug #pragma region Debug
#define debug_trap() __builtin_trap() #define debug_trap() __builtin_debugtrap()
#if BUILD_DEBUG #if BUILD_DEBUG
#define assert(cond) if(cond == false){debug_trap();} IA_ void assert(U8 cond) { if(cond){return;} else{debug_trap(); ms_exit_process(1);} }
#else #else
# ifndef assert #define assert(cond)
# include <assert.h>
# endif
#endif #endif
#pragma endregion Debug #pragma endregion Debug
#endif
#define GCC_OPTIMIZATION_DISABLE _Pragma("GCC push_options") _Pragma("GCC optimize(\"O0\")") #define GCC_OPTIMIZATION_DISABLE _Pragma("GCC push_options") _Pragma("GCC optimize(\"O0\")")
#define GCC_OPTIMIZATION_ENABLE _Pragma("GCC pop_options") #define GCC_OPTIMIZATION_ENABLE _Pragma("GCC pop_options")
+36 -260
View File
@@ -14,10 +14,8 @@
// source: C:\projects\Pikuma\ps1\code\duffle\pad.h // source: C:\projects\Pikuma\ps1\code\duffle\pad.h
// source: C:\projects\Pikuma\ps1\code\duffle\dsl.atom.h // source: C:\projects\Pikuma\ps1\code\duffle\dsl.atom.h
// source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h // source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h
// source: C:\projects\Pikuma\ps1\code\duffle\bios.h
// source: C:\projects\Pikuma\ps1\code\duffle\psyq.h // source: C:\projects\Pikuma\ps1\code\duffle\psyq.h
// source: C:\projects\Pikuma\ps1\code\duffle\pad.c // source: C:\projects\Pikuma\ps1\code\duffle\math.atom.c
// source: C:\projects\Pikuma\ps1\code\duffle\math.atom.h
// source: C:\projects\Pikuma\ps1\code\duffle\mips.atom.c // source: C:\projects\Pikuma\ps1\code\duffle\mips.atom.c
// source: C:\projects\Pikuma\ps1\code\duffle\gte.atom.c // source: C:\projects\Pikuma\ps1\code\duffle\gte.atom.c
// source: C:\projects\Pikuma\ps1\code\duffle\gp.atom.c // source: C:\projects\Pikuma\ps1\code\duffle\gp.atom.c
@@ -35,12 +33,15 @@
* These do NOT yield. They are expanded inline inside Tape Atoms. * These do NOT yield. They are expanded inline inside Tape Atoms.
* ---------------------------------------------------------------------------*/ * ---------------------------------------------------------------------------*/
// The 'Yield' sequence for Tape Atoms (mac_yield). // The 'Yield' sequence for Tape Atoms (mac_yield).
// - mac_yield() is the safe default for atom-endings: 4 words, BD-slot of jr is mandatory nop.
// - mac_yield_load() + mac_yield_tail():
// - unconditional branch: mac_yield_load fills the branch's BD-slot (replaces a nop);
// - mac_yield_tail runs at the branch target (does NOT re-load R_AtomJmp).
#define mac_yield(...) \ #define mac_yield(...) \
load_word(R_AtomJmp, R_TapePtr, 0) \ load_word(R_AtomJmp, R_TapePtr, 0) \
LdSlot_ \
, add_ui_self( R_TapePtr, S_(MipsCode)) \ , add_ui_self( R_TapePtr, S_(MipsCode)) \
, jump_reg( R_AtomJmp) \ , jump_reg( R_AtomJmp) \
, BdSlot_ nop , nop
WORD_COUNT(mac_yield, 4) WORD_COUNT(mac_yield, 4)
/* atom_dbg_skip */ /* atom_dbg_skip */
@@ -52,24 +53,13 @@ WORD_COUNT(mac_yield_load, 1)
#define mac_yield_tail(...) \ #define mac_yield_tail(...) \
add_ui_self(R_TapePtr, S_(MipsCode)) \ add_ui_self(R_TapePtr, S_(MipsCode)) \
, jump_reg( R_AtomJmp) \ , jump_reg( R_AtomJmp) \
, BdSlot_ nop , nop
WORD_COUNT(mac_yield_tail, 3) WORD_COUNT(mac_yield_tail, 3)
/* atom_dbg_skip */
#define mac_load_half_v3(tx, ty, tz, base, offset) \
load_half(tx, base, offset + OA_(U2,[0])) \
, load_half(ty, base, offset + OA_(U2,[1])) \
, load_half(tz, base, offset + OA_(U2,[2]))
WORD_COUNT(mac_load_half_v3, 3)
#define mac_load_v3s2(transfer, base, offset) \
mac_load_half_v3(transfer.x, transfer.y, transfer.z, base, offset)
WORD_COUNT(mac_load_v3s2, 3)
/* atom_dbg_skip */ /* atom_dbg_skip */
#define mac_load_v2s2(rs_x, rs_y, r_base, offset) \ #define mac_load_v2s2(rs_x, rs_y, r_base, offset) \
load_half(rs_x, r_base, offset + O_(V3_S2,x)) \ load_half( rs_x, r_base, O_(V3_S2,x)) \
, load_half(rs_y, r_base, offset + O_(V3_S2,y)) , load_half( rs_y, r_base, O_(V3_S2,y))
WORD_COUNT(mac_load_v2s2, 2) WORD_COUNT(mac_load_v2s2, 2)
/* atom_dbg_skip */ /* atom_dbg_skip */
@@ -78,76 +68,6 @@ WORD_COUNT(mac_load_v2s2, 2)
, store_half(rt_y, base, offset + O_(V2_S2,y)) , store_half(rt_y, base, offset + O_(V2_S2,y))
WORD_COUNT(mac_store_v2s2, 2) WORD_COUNT(mac_store_v2s2, 2)
/* atom_dbg_skip */
#define mac_load_word_v3(tx, ty, tz, base, offset) \
load_word(tx, base, offset + OA_(U4,[0])) \
, load_word(ty, base, offset + OA_(U4,[1])) \
, load_word(tz, base, offset + OA_(U4,[2]))
WORD_COUNT(mac_load_word_v3, 3)
#define mac_load_v3s4(transfer, base, offset) \
mac_load_word_v3(transfer.x, transfer.y, transfer.z, base, offset)
WORD_COUNT(mac_load_v3s4, 3)
#define mac_load_p3s4(transfer, base, offset) \
mac_load_word_v3(transfer.x, transfer.y, transfer.z, base, offset)
WORD_COUNT(mac_load_p3s4, 3)
/* atom_dbg_skip */
#define mac_store_half_v3(tx, ty, tz, base, offset) \
store_half(tx, base, offset + OA_(U2,[0])) \
, store_half(ty, base, offset + OA_(U2,[1])) \
, store_half(tz, base, offset + OA_(U2,[2]))
WORD_COUNT(mac_store_half_v3, 3)
#define mac_store_v3s2(transfer, base, offset) \
mac_store_half_v3(transfer.x, transfer.y, transfer.z, base, offset)
WORD_COUNT(mac_store_v3s2, 3)
/* atom_dbg_skip */
#define mac_store_word_v3(tx, ty, tz, base, offset) \
store_word(tx, base, offset + OA_(U4,[0])) \
, store_word(ty, base, offset + OA_(U4,[1])) \
, store_word(tz, base, offset + OA_(U4,[2]))
WORD_COUNT(mac_store_word_v3, 3)
#define mac_store_v3s4(transfer, base, offset) \
mac_store_word_v3(transfer.x, transfer.y, transfer.z, base, offset)
WORD_COUNT(mac_store_v3s4, 3)
#define mac_store_p3s4(transfer, base, offset) \
mac_store_word_v3(transfer.x, transfer.y, transfer.z, base, offset)
WORD_COUNT(mac_store_p3s4, 3)
/* atom_dbg_skip */
#define mac_add_si_v3s4(rt_x, rt_y, rt_z, base, offset) \
add_si(rt_x, base, O_(V3_S4,x)) \
, add_si(rt_y, base, O_(V3_S4,y)) \
, add_si(rt_z, base, O_(V3_S4,z))
WORD_COUNT(mac_add_si_v3s4, 3)
/* atom_dbg_skip */
#define mac_sub_s_v3(dx, dy, dz, sx, sy, sz, tx, ty, tz) \
sub_s(dx, sx, tx) \
, sub_s(dy, sy, ty) \
, sub_s(dz, sz, tz)
WORD_COUNT(mac_sub_s_v3, 3)
#define mac_sub_v3s4(d, s, t) \
mac_sub_s_v3(d.x, d.y, d.z, s.x, s.y, s.z, t.x, t.y, t.z)
WORD_COUNT(mac_sub_v3s4, 3)
/* atom_dbg_skip */
#define mac_sub_s_v3_self(ds_x, ds_y, ds_z, tx, ty, tz) \
sub_s(ds_x, ds_x, tx) \
, sub_s(ds_y, ds_y, ty) \
, sub_s(ds_z, ds_z, tz)
WORD_COUNT(mac_sub_s_v3_self, 3)
#define mac_sub_v3s4_self(ds, t) \
mac_sub_s_v3_self(ds.x, ds.y, ds.z, t.x, t.y, t.z)
WORD_COUNT(mac_sub_v3s4_self, 3)
/* atom_dbg_skip */ /* atom_dbg_skip */
#define mac_store_rects2(rt_x, rt_y, rt_width, rt_height, base, offset) \ #define mac_store_rects2(rt_x, rt_y, rt_width, rt_height, base, offset) \
store_half(rt_x, base, offset + O_(Rect_S2,x)) \ store_half(rt_x, base, offset + O_(Rect_S2,x)) \
@@ -156,39 +76,6 @@ WORD_COUNT(mac_sub_v3s4_self, 3)
, store_half(rt_height, base, offset + O_(Rect_S2,height)) , store_half(rt_height, base, offset + O_(Rect_S2,height))
WORD_COUNT(mac_store_rects2, 4) WORD_COUNT(mac_store_rects2, 4)
/* atom_dbg_skip */
#define mac_load_word_imm(dst, imm) \
load_upper_i(dst, u4_hi(imm)) \
, or_i_self( dst, u4_lo(imm))
WORD_COUNT(mac_load_word_imm, 2)
#define mac_shift_aright_v3_self(dt_x, dt_y, dt_z, shift_amount) \
shift_aright(dt_x, dt_x, shift_amount) \
, shift_aright(dt_y, dt_y, shift_amount) \
, shift_aright(dt_z, dt_z, shift_amount)
WORD_COUNT(mac_shift_aright_v3_self, 3)
#define mac_shift_aright_v3s4_self(dt, shift) \
mac_shift_aright_v3_self(dt.x, dt.y, dt.z, shift)
WORD_COUNT(mac_shift_aright_v3s4_self, 3)
#define mac_shift_aright_var_v3(rd_v0, rd_v1, rd_v2, rs_v0, rs_v1, rs_v2, r_shift) \
shift_aright_var(rd_v0, rs_v0, r_shift) \
, shift_aright_var(rd_v1, rs_v1, r_shift) \
, shift_aright_var(rd_v2, rs_v2, r_shift)
WORD_COUNT(mac_shift_aright_var_v3, 3)
/* atom_dbg_skip */
#define mac_shift_aright_var_v3_self(rds_v0, rds_v1, rds_v2, r_shift) \
shift_aright_var(rds_v0, rds_v0, r_shift) \
, shift_aright_var(rds_v1, rds_v1, r_shift) \
, shift_aright_var(rds_v2, rds_v2, r_shift)
WORD_COUNT(mac_shift_aright_var_v3_self, 3)
#define mac_shift_aright_var_v3s4_self(ds, shift) \
mac_shift_aright_var_v3_self(ds.x, ds.y, ds.z, shift)
WORD_COUNT(mac_shift_aright_var_v3s4_self, 3)
/* atom_dbg_skip */ /* atom_dbg_skip */
#define mac_load_tri_indices(r_face_cusor, r_i0, r_i1, r_i2) \ #define mac_load_tri_indices(r_face_cusor, r_i0, r_i1, r_i2) \
load_half_u(r_i0, r_face_cusor, 0 * S_(S2)) \ load_half_u(r_i0, r_face_cusor, 0 * S_(S2)) \
@@ -196,30 +83,6 @@ WORD_COUNT(mac_shift_aright_var_v3s4_self, 3)
, load_half_u(r_i2, r_face_cusor, 2 * S_(S2)) , load_half_u(r_i2, r_face_cusor, 2 * S_(S2))
WORD_COUNT(mac_load_tri_indices, 3) WORD_COUNT(mac_load_tri_indices, 3)
#define mac_gte_mv_to_cr_diag_v3s4(v) \
gte_mv_to_ctrl_r(v.y, gte_cr_RT13) \
, gte_mv_to_ctrl_r(v.z, gte_cr_RT22) \
, gte_mv_to_ctrl_r(v.x, gte_cr_RT11)
WORD_COUNT(mac_gte_mv_to_cr_diag_v3s4, 3)
#define mac_gte_ld_ir123_v3s4(v) \
gte_mv_to_data_r(v.x, C2_IR1) \
, gte_mv_to_data_r(v.y, C2_IR2) \
, gte_mv_to_data_r(v.z, C2_IR3)
WORD_COUNT(mac_gte_ld_ir123_v3s4, 3)
/* atom_dbg_skip */
#define mac_gte_op_cross_v3s4(a, b) \
mac_gte_mv_to_cr_diag_v3s4(a) \
GteDelay_ /* RT diagonal: D1 = a.x, D2 = a.y, D3 = a.z */ \
, mac_gte_ld_ir123_v3s4(b) \
GteDelay_ /* IR: second operand (b.xyz) */ \
, gte_cmdw_cross /* OP: MAC1/2/3 = a × b (S12.20) */ \
, mac_gte_mv_from_mac123_v3s4(a) \
GteDelay_ /* Read MAC1/2/3 → a.xyz (overwrites source-A's load targets) */ \
, mac_shift_aright_v3s4_self(a, 12) /* Right-shift MAC by 12 (S12.20 → S12.0 OuterProduct12) */
WORD_COUNT(mac_gte_op_cross_v3s4, 13)
/* atom_dbg_skip */ /* atom_dbg_skip */
#define mac_gte_store_f3(r_primitive_cursor) \ #define mac_gte_store_f3(r_primitive_cursor) \
gte_sw(C2_SXY0, r_primitive_cursor, O_(Poly_F3,p0)) \ gte_sw(C2_SXY0, r_primitive_cursor, O_(Poly_F3,p0)) \
@@ -228,24 +91,24 @@ WORD_COUNT(mac_gte_op_cross_v3s4, 13)
WORD_COUNT(mac_gte_store_f3, 3) WORD_COUNT(mac_gte_store_f3, 3)
/* atom_dbg_skip */ /* atom_dbg_skip */
#define mac_gte_load_tri_verts(vbase, v0, v1, v2) \ #define mac_gte_load_tri_verts(r_vert_base, r_v0, r_v1, r_v2) \
shift_lleft(R_AT, v0, v3s2_byteoff) \ shift_lleft(R_AT, r_v0, v3s2_byteoff) \
, add_u_self(R_AT, vbase) \ , add_u_self(R_AT, r_vert_base) \
, load_word(R_V0, R_AT, O_(V3_S2,x)) \ , load_word(R_V0, R_AT, O_(V3_S2,x)) \
, load_word(R_V1, R_AT, O_(V3_S2,z)) \ , load_word(R_V1, R_AT, O_(V3_S2,z)) \
, LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY0) \ , gte_mv_to_data_r(R_V0, C2_VXY0) \
, gte_mv_to_data_r(R_V1, C2_VZ0) \ , gte_mv_to_data_r(R_V1, C2_VZ0) \
, shift_lleft(R_AT, v1, v3s2_byteoff) \ , shift_lleft(R_AT, r_v1, v3s2_byteoff) \
, add_u_self(R_AT, vbase) \ , add_u_self(R_AT, r_vert_base) \
, load_word(R_V0, R_AT, O_(V3_S2,x)) \ , load_word(R_V0, R_AT, O_(V3_S2,x)) \
, load_word(R_V1, R_AT, O_(V3_S2,z)) \ , load_word(R_V1, R_AT, O_(V3_S2,z)) \
, LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY1) \ , gte_mv_to_data_r(R_V0, C2_VXY1) \
, gte_mv_to_data_r(R_V1, C2_VZ1) \ , gte_mv_to_data_r(R_V1, C2_VZ1) \
, shift_lleft(R_AT, v2, v3s2_byteoff) \ , shift_lleft(R_AT, r_v2, v3s2_byteoff) \
, add_u_self(R_AT, vbase) \ , add_u_self(R_AT, r_vert_base) \
, load_word(R_V0, R_AT, O_(V3_S2,x)) \ , load_word(R_V0, R_AT, O_(V3_S2,x)) \
, load_word(R_V1, R_AT, O_(V3_S2,z)) \ , load_word(R_V1, R_AT, O_(V3_S2,z)) \
, LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY2) \ , gte_mv_to_data_r(R_V0, C2_VXY2) \
, gte_mv_to_data_r(R_V1, C2_VZ2) , gte_mv_to_data_r(R_V1, C2_VZ2)
WORD_COUNT(mac_gte_load_tri_verts, 18) WORD_COUNT(mac_gte_load_tri_verts, 18)
@@ -261,83 +124,9 @@ WORD_COUNT(mac_gte_store_g4_p012, 3)
gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_G4,p3)) gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_G4,p3))
WORD_COUNT(mac_gte_store_g4_p3, 1) WORD_COUNT(mac_gte_store_g4_p3, 1)
/* atom_dbg_skip */
#define mac_gte_sqr_v3(r_sx, r_sy, r_sz, r_sq_x, r_sq_y, r_sq_z) \
mac_gte_sqr_v3s4(r_sx, r_sy, r_sz, nop) \
, gte_mv_from_data_r(r_sq_x, C2_MAC1) \
, gte_mv_from_data_r(r_sq_y, C2_MAC2) \
, gte_mv_from_data_r(r_sq_z, C2_MAC3)
WORD_COUNT(mac_gte_sqr_v3, 8)
/* atom_dbg_skip */
#define mac_gte_sqr_v3s4(r_sx, r_sy, r_sz, delay_slot) \
gte_mv_to_data_r(r_sx, C2_IR1) \
, gte_mv_to_data_r(r_sy, C2_IR2) \
, gte_mv_to_data_r(r_sz, C2_IR3) \
, delay_slot \
, gte_cmdw_sqr
WORD_COUNT(mac_gte_sqr_v3s4, 5)
/* atom_dbg_skip */
#define mac_gte_gpf_scale(r_sx, r_sy, r_sz, r_recip_est, r_shift, r_dx, r_dy, r_dz) \
gte_mv_to_data_r(r_recip_est, C2_IR0) \
, gte_mv_to_data_r(r_sx, C2_IR1) \
, gte_mv_to_data_r(r_sy, C2_IR2) \
, gte_mv_to_data_r(r_sz, C2_IR3) \
, GteDelay_ nop2 /* retire IR0..IR3 → GPF input pre-fill (matches libgte 0x80016134..0x80016138) */ \
, gte_cmdw_gpf \
, gte_mv_from_data_r(r_dx, C2_MAC1) \
, gte_mv_from_data_r(r_dy, C2_MAC2) \
, gte_mv_from_data_r(r_dz, C2_MAC3) \
, shift_aright_var(r_dx, r_dx, r_shift) \
, shift_aright_var(r_dy, r_dy, r_shift) \
, shift_aright_var(r_dz, r_dz, r_shift)
WORD_COUNT(mac_gte_gpf_scale, 13)
#define mac_trans_mt3s3s4(r_mtx, r_off, r_t0, r_t1, r_t2) \
load_word( r_t0, r_off, O_(V3_S4,x)) \
, load_word( r_t1, r_off, O_(V3_S4,y)) \
, load_word( r_t2, r_off, O_(V3_S4,z)) \
, store_word(r_t0, r_mtx, O_(MT3_S2S4,t[0])) \
, store_word(r_t1, r_mtx, O_(MT3_S2S4,t[1])) \
, store_word(r_t2, r_mtx, O_(MT3_S2S4,t[2]))
WORD_COUNT(mac_trans_mt3s3s4, 6)
/* atom_dbg_skip */
#define mac_lzcr_round_even_half_shift(r_shift, r_mag_sq, r_mag_sq_copy) \
and_i(r_shift, r_shift, gte_lzcr_even_mask) \
, or_u(r_mag_sq_copy, r_mag_sq, 0) \
, li_s( r_mag_sq, 31) \
, sub_s( r_mag_sq, r_mag_sq, r_shift) \
, shift_aright(r_mag_sq, r_mag_sq, 1)
WORD_COUNT(mac_lzcr_round_even_half_shift, 5)
#define mac_gte_general_purpose_interopolation(to_ir0, to_ir1, to_ir2, to_ir3, fr_mac1, fr_mac2, fr_mac3, nop_slot1, nop_slot2) \
gte_mv_to_data_r(to_ir0, C2_IR0) \
, gte_mv_to_data_r(to_ir1, C2_IR1) /* IR1 = src.x (preserved in r_tmp — r_mac2_scratch was clobbered to MAC2 in stage 1.5) */ \
, gte_mv_to_data_r(to_ir2, C2_IR2) \
, gte_mv_to_data_r(to_ir3, C2_IR3) /* IR3 = src.z (reloaded) */ \
, GteDelay_ nop_slot1 \
, GteDelay_ nop_slot2 \
, gte_cmdw_gpf \
, gte_mv_from_data_r(fr_mac1, C2_MAC1) \
, gte_mv_from_data_r(fr_mac2, C2_MAC2) \
, gte_mv_from_data_r(fr_mac3, C2_MAC3)
WORD_COUNT(mac_gte_general_purpose_interopolation, 10)
#define mac_gte_mv_from_data_r_mac123(fr_mac1, fr_mac2, fr_mac3) \
gte_mv_from_data_r(fr_mac1, C2_MAC1) \
, gte_mv_from_data_r(fr_mac2, C2_MAC2) \
, gte_mv_from_data_r(fr_mac3, C2_MAC3)
WORD_COUNT(mac_gte_mv_from_data_r_mac123, 3)
#define mac_gte_mv_from_mac123_v3s4(v) \
mac_gte_mv_from_data_r_mac123(v.x, v.y, v.z)
WORD_COUNT(mac_gte_mv_from_mac123_v3s4, 3)
/* atom_dbg_skip */
#define mac_gcmd_push(cmd, reg_transfer, reg_base, port) \ #define mac_gcmd_push(cmd, reg_transfer, reg_base, port) \
mac_load_word_imm(reg_transfer, cmd) \ load_upper_i(reg_transfer, cmd >> 16) \
, or_i_self( reg_transfer, cmd & 0xFFFF) \
, store_word( reg_transfer, reg_base, port) , store_word( reg_transfer, reg_base, port)
WORD_COUNT(mac_gcmd_push, 3) WORD_COUNT(mac_gcmd_push, 3)
@@ -360,7 +149,6 @@ WORD_COUNT(mac_pack_color_word, 3)
mac_pack_color_word(r_base, O_(Poly_F3,color), gp0_cmd_poly_f3, r, g, b) mac_pack_color_word(r_base, O_(Poly_F3,color), gp0_cmd_poly_f3, r, g, b)
WORD_COUNT(mac_format_f3_color, 3) WORD_COUNT(mac_format_f3_color, 3)
/* atom_dbg_skip */
#define mac_format_g4_color(r_prim_cursor, r0, g0, b0, r1, g1, b1, r2, g2, b2, r3, g3, b3) \ #define mac_format_g4_color(r_prim_cursor, r0, g0, b0, r1, g1, b1, r2, g2, b2, r3, g3, b3) \
mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c0), gp0_cmd_poly_g4, r0,g0,b0) \ mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c0), gp0_cmd_poly_g4, r0,g0,b0) \
, mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c1), 0, r1,g1,b1) \ , mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c1), 0, r1,g1,b1) \
@@ -368,41 +156,29 @@ WORD_COUNT(mac_format_f3_color, 3)
, mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c3), 0, r3,g3,b3) , mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c3), 0, r3,g3,b3)
WORD_COUNT(mac_format_g4_color, 12) WORD_COUNT(mac_format_g4_color, 12)
#define mac_insert_ot_tag(r_ot_base, r_prim_cursor, poly_size) \ #define mac_insert_ot_tag_f3(r_ot_base, r_prim_cursor) \
shift_lleft( R_T1, R_T1, S_(U4)/2) /* T1 = otz * S_(U4) (otz arg is implicit R_T1) */ \ shift_lleft( R_T1, R_T1, S_(U4)/2) /* T1 = otz * S_(U4) (otz arg is implicit R_T1) */ \
, add_u_self( R_T1, r_ot_base) /* T1 = & OrderingTable[OTZ] */ \ , add_u_self( R_T1, r_ot_base) /* T1 = & OrderingTable[OTZ] */ \
, load_word( R_AT, R_T1, O_(PolyTag,code)) /* AT = old_ot_head */ \ , load_word( R_AT, R_T1, O_(PolyTag,code)) /* AT = old_ot_head */ \
, load_upper_i(R_V0, (poly_size/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits) \ , load_upper_i(R_V0, (S_(Poly_F3)/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits) /* V0 = (5 - 1) << 24 = 4 << 24 */ \
, mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)) /* Strip upper 8 bits (length from prev cell) → keep only low 24 */ \ , mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)) /* Strip upper 8 bits (length from prev cell) → keep only low 24 */ \
, or_u( R_AT, R_AT, R_V0) /* Merge length */ \ , or_u( R_AT, R_AT, R_V0) /* Merge length */ \
, store_word( R_AT, r_prim_cursor, O_(PolyTag,code)) /* prim->tag = packed(prim_length, old_addr) */ \ , store_word( R_AT, r_prim_cursor, O_(PolyTag,code)) /* prim->tag = packed(prim_length, old_addr) */ \
, shift_lleft( R_AT, r_prim_cursor, S_(PolyTag_len_bits)) /* AT = (prim_length << 24) | old_addr */ \ , shift_lleft( R_AT, r_prim_cursor, S_(PolyTag_len_bits)) /* AT = (prim_length << 24) | old_addr */ \
, shift_lright(R_AT, R_AT, S_(PolyTag_len_bits)) \ , shift_lright(R_AT, R_AT, S_(PolyTag_len_bits)) \
, store_word( R_AT, R_T1, O_(PolyTag,code)) /* OrderingTable[OTZ] = PrimCursor */ , store_word( R_AT, R_T1, O_(PolyTag,code)) /* OrderingTable[OTZ] = PrimCursor */
WORD_COUNT(mac_insert_ot_tag, 11) WORD_COUNT(mac_insert_ot_tag_f3, 11)
/* atom_dbg_skip */ #define mac_insert_ot_tag_g4(r_ot_base, r_prim_cursor) \
#define mac_pad_set_centered_axes(state, scratch) \ shift_lleft( R_T1, R_T1, S_(U4)/2) /* T1 = otz * S_(U4) (otz arg is implicit R_T1) */ \
load_upper_i(scratch, (PadAxis_Centered >> 16) & 0xFFFF) \ , add_u_self( R_T1, r_ot_base) /* T1 = & OrderingTable[OTZ] */ \
, or_i_self( scratch, PadAxis_Centered & 0xFFFF) /* mac_load_word_imm(scratch, PadAxis_Centered), */ \ , load_word( R_AT, R_T1, O_(PolyTag,code)) /* AT = old_ot_head */ \
, store_word( scratch, state, O_(PadState,axes)) , load_upper_i(R_V0, (S_(Poly_G4)/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits) /* V0 = (9 - 1) << 24 = 8 << 24 */ \
WORD_COUNT(mac_pad_set_centered_axes, 3) , mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)) /* Strip upper 8 bits (length from prev cell) → keep only low 24 */ \
, or_u( R_AT, R_AT, R_V0) /* Merge length */ \
/* atom_dbg_skip */ , store_word( R_AT, r_prim_cursor, O_(PolyTag,code)) /* prim->tag = packed(prim_length, old_addr) */ \
#define mac_pad_set_id_byte(state, r_id, id_value) \ , shift_lleft( R_AT, r_prim_cursor, S_(PolyTag_len_bits)) /* AT = (prim_length << 24) | old_addr */ \
add_ui( r_id, R_0, id_value) \ , shift_lright(R_AT, R_AT, S_(PolyTag_len_bits)) \
, store_byte(r_id, state, O_(PadState,id)) , store_word( R_AT, R_T1, O_(PolyTag,code)) /* OrderingTable[OTZ] = PrimCursor */
WORD_COUNT(mac_pad_set_id_byte, 2) WORD_COUNT(mac_insert_ot_tag_g4, 11)
/* atom_dbg_skip */
#define mac_pad_set_status(r_tmp, r_state, pad_status) \
add_ui( r_tmp, R_0, pad_status) \
, store_word(r_tmp, r_state, O_(PadState,status))
WORD_COUNT(mac_pad_set_status, 2)
/* atom_dbg_skip */
#define mac_pad_store_inverted_buttons(r_buttons, r_pad_state) \
nor_u( r_buttons, r_buttons, R_0) \
, store_half(r_buttons, r_pad_state, O_(PadState,buttons))
WORD_COUNT(mac_pad_store_inverted_buttons, 2)
+11 -31
View File
@@ -11,10 +11,8 @@
// source: C:\projects\Pikuma\ps1\code\duffle\pad.h // source: C:\projects\Pikuma\ps1\code\duffle\pad.h
// source: C:\projects\Pikuma\ps1\code\duffle\dsl.atom.h // source: C:\projects\Pikuma\ps1\code\duffle\dsl.atom.h
// source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h // source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h
// source: C:\projects\Pikuma\ps1\code\duffle\bios.h
// source: C:\projects\Pikuma\ps1\code\duffle\psyq.h // source: C:\projects\Pikuma\ps1\code\duffle\psyq.h
// source: C:\projects\Pikuma\ps1\code\duffle\pad.c // source: C:\projects\Pikuma\ps1\code\duffle\math.atom.c
// source: C:\projects\Pikuma\ps1\code\duffle\math.atom.h
// source: C:\projects\Pikuma\ps1\code\duffle\mips.atom.c // source: C:\projects\Pikuma\ps1\code\duffle\mips.atom.c
// source: C:\projects\Pikuma\ps1\code\duffle\gte.atom.c // source: C:\projects\Pikuma\ps1\code\duffle\gte.atom.c
// source: C:\projects\Pikuma\ps1\code\duffle\gp.atom.c // source: C:\projects\Pikuma\ps1\code\duffle\gp.atom.c
@@ -25,35 +23,17 @@
#pragma region duffle #pragma region duffle
// --- atom: example_atom_proc (10 words) --- // --- atom: pad_bios_snapshot (78 words) ---
#define _atom_offset_example_atom_proc_skip 2 #define _atom_offset_snap_root_skip_disconnected 8
#define _atom_offset_disconnected_snap_end 61
enum { #define _atom_offset_case_2_id_dispatch 8
atom_offset_example_atom_proc_skip = _atom_offset_example_atom_proc_skip, #define _atom_offset_pending_snap_end 51
}; #define _atom_offset_id_dispatch_try_analog_stick 11
#define _atom_offset_id_dispatch_snap_end 38
// --- atom: normalize_v3s4 (62 words) --- #define _atom_offset_try_analog_stick_try_analog_pad 12
#define _atom_offset_analog_stick_snap_end 24
#define _atom_offset_aligned_done_srav_path 3 #define _atom_offset_try_analog_pad_try_unsupported 11
#define _atom_offset_srav_path_aligned_done 4
enum {
atom_offset_aligned_done_srav_path = _atom_offset_aligned_done_srav_path,
atom_offset_srav_path_aligned_done = _atom_offset_srav_path_aligned_done,
};
// --- atom: pad_bios_snapshot (84 words) ---
#define _atom_offset_snap_root_skip_disconnected 10
#define _atom_offset_disconnected_snap_end 65
#define _atom_offset_case_2_id_dispatch 9
#define _atom_offset_pending_snap_end 54
#define _atom_offset_id_dispatch_try_analog_stick 12
#define _atom_offset_id_dispatch_snap_end 40
#define _atom_offset_try_analog_stick_try_analog_pad 13
#define _atom_offset_analog_stick_snap_end 25
#define _atom_offset_try_analog_pad_try_unsupported 12
#define _atom_offset_analog_pad_snap_end 10 #define _atom_offset_analog_pad_snap_end 10
enum { enum {
+36 -15
View File
@@ -8,48 +8,69 @@ ATOM_FILE_DEBUGGER_LINE_MARKER(gp_atom_c);
#pragma region MACs (Mips Atom Components) #pragma region MACs (Mips Atom Components)
FI_ Slice_MipsCode ac_gcmd_push(AtomBuilder_R ab, U4 cmd, U4 reg_transfer, U4 reg_base, U2 port) FI_ Slice_MipsCode ac_gcmd_push(U4 cmd, U4 reg_transfer, U4 reg_base, U2 port)
atom_dbg_skip MipsAtomComp_Proc_(ab, { MipsAtomComp_Proc_(ac_gcmd_push, {
mac_load_word_imm(reg_transfer, cmd), load_upper_i(reg_transfer, cmd >> 16),
or_i_self( reg_transfer, cmd & 0xFFFF),
store_word( reg_transfer, reg_base, port), store_word( reg_transfer, reg_base, port),
}) })
FI_ Slice_MipsCode ac_store_rgb8(AtomBuilder_R ab, U1 rr, U1 rg, U1 rb, U4 base, U4 offset) FI_ Slice_MipsCode ac_store_rgb8(U1 rr, U1 rg, U1 rb, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_rgb8, {
atom_dbg_skip MipsAtomComp_Proc_(ab, {
store_byte(rr, base, offset + O_(RGB8,r)), store_byte(rr, base, offset + O_(RGB8,r)),
store_byte(rg, base, offset + O_(RGB8,g)), store_byte(rg, base, offset + O_(RGB8,g)),
store_byte(rb, base, offset + O_(RGB8,b)), store_byte(rb, base, offset + O_(RGB8,b)),
}) })
FI_ Slice_MipsCode ac_pack_color_word(AtomBuilder_R ab, U4 r_base, U4 off, U4 cmd, U1 r, U1 g, U1 b) /* Words: 3; Emits one (cmd|color) word to R_PrimCursor at the given
atom_dbg_skip MipsAtomComp_Proc_(ab, { * byte offset. Internal helper used by the *_format_*_color macros. */
FI_ Slice_MipsCode ac_pack_color_word(U4 r_base, U4 off, U4 cmd, U1 r, U1 g, U1 b)
atom_dbg_skip MipsAtomComp_Proc_(ac_pack_color_word, {
load_upper_i(R_AT, (cmd) << 8 | (b)), load_upper_i(R_AT, (cmd) << 8 | (b)),
or_i_self( R_AT, ((g) << 8) | (r)), or_i_self( R_AT, ((g) << 8) | (r)),
store_word( R_AT, r_base, (off)), store_word( R_AT, r_base, (off)),
}) })
FI_ Slice_MipsCode ac_format_f3_color(AtomBuilder_R ab, U4 r_base, U1 r, U1 g, U1 b) /* Words: 3; Emits the F3 command+color word (cmd byte | BLUE | GREEN | RED)
atom_dbg_skip MipsAtomComp_Proc_(ab, { mac_pack_color_word(r_base, O_(Poly_F3,color), gp0_cmd_poly_f3, r, g, b) }) * Args: _r, _g, _b are 8-bit RGB byte values (not raw 16-bit fields). */
FI_ Slice_MipsCode ac_format_f3_color(U4 r_base, U1 r, U1 g, U1 b)
atom_dbg_skip MipsAtomComp_Proc_(ac_format_f3_color, { mac_pack_color_word(r_base, O_(Poly_F3,color), gp0_cmd_poly_f3, r, g, b) })
FI_ Slice_MipsCode ac_format_g4_color(AtomBuilder_R ab, U4 r_prim_cursor, /* Words: 12; Emits the four (code|color) words of a Poly_G4.
* Args: rN,gN,bN are 8-bit RGB byte values for each of the 4 vertices. */
FI_ Slice_MipsCode ac_format_g4_color(U4 r_prim_cursor,
U1 r0, U1 g0, U1 b0, U1 r0, U1 g0, U1 b0,
U1 r1, U1 g1, U1 b1, U1 r1, U1 g1, U1 b1,
U1 r2, U1 g2, U1 b2, U1 r2, U1 g2, U1 b2,
U1 r3, U1 g3, U1 b3) U1 r3, U1 g3, U1 b3)
atom_dbg_skip MipsAtomComp_Proc_(ab, { MipsAtomComp_Proc_(ac_format_g4_color, {
mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c0), gp0_cmd_poly_g4, r0,g0,b0), mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c0), gp0_cmd_poly_g4, r0,g0,b0),
mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c1), 0, r1,g1,b1), mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c1), 0, r1,g1,b1),
mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c2), 0, r2,g2,b2), mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c2), 0, r2,g2,b2),
mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c3), 0, r3,g3,b3), mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c3), 0, r3,g3,b3),
}) })
/* Words: 11; Correctly inserts a primitive into the Ordering Table linked list. */ /* Words: 11; Correctly inserts a primitive into the Ordering Table linked list.
// TODO(Ed): Expose R_T1 as a r_t0, r_V0 as r_t2 * Hardcoded for Poly_F3 (5 words). For Poly_G4, use ac_insert_ot_tag_g4. */
I_ Slice_MipsCode ac_insert_ot_tag(AtomBuilder_R ab, Reg r_ot_base, Reg r_prim_cursor, U2 poly_size) MipsAtomComp_Proc_(ab, { I_ Slice_MipsCode ac_insert_ot_tag_f3(U4 r_ot_base, U4 r_prim_cursor) MipsAtomComp_Proc_(ac_insert_ot_tag_f3, {
shift_lleft( R_T1, R_T1, S_(U4)/2), // T1 = otz * S_(U4) (otz arg is implicit R_T1) shift_lleft( R_T1, R_T1, S_(U4)/2), // T1 = otz * S_(U4) (otz arg is implicit R_T1)
add_u_self( R_T1, r_ot_base), // T1 = & OrderingTable[OTZ] add_u_self( R_T1, r_ot_base), // T1 = & OrderingTable[OTZ]
load_word( R_AT, R_T1, O_(PolyTag,code)), // AT = old_ot_head load_word( R_AT, R_T1, O_(PolyTag,code)), // AT = old_ot_head
load_upper_i(R_V0, (poly_size/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits), load_upper_i(R_V0, (S_(Poly_F3)/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits), // V0 = (5 - 1) << 24 = 4 << 24
mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)), // Strip upper 8 bits (length from prev cell) → keep only low 24
or_u( R_AT, R_AT, R_V0), // Merge length
store_word( R_AT, r_prim_cursor, O_(PolyTag,code)), // prim->tag = packed(prim_length, old_addr)
shift_lleft( R_AT, r_prim_cursor, S_(PolyTag_len_bits)), // AT = (prim_length << 24) | old_addr
shift_lright(R_AT, R_AT, S_(PolyTag_len_bits)),
store_word( R_AT, R_T1, O_(PolyTag,code)), // OrderingTable[OTZ] = PrimCursor
})
/* Words: 11; Correctly inserts a primitive into the Ordering Table linked list.
* Hardcoded for Poly_G4 (9 words). For Poly_F3, use ac_insert_ot_tag_f3. */
I_ Slice_MipsCode ac_insert_ot_tag_g4(U4 r_ot_base, U4 r_prim_cursor) MipsAtomComp_Proc_(ac_insert_ot_tag_g4, {
shift_lleft( R_T1, R_T1, S_(U4)/2), // T1 = otz * S_(U4) (otz arg is implicit R_T1)
add_u_self( R_T1, r_ot_base), // T1 = & OrderingTable[OTZ]
load_word( R_AT, R_T1, O_(PolyTag,code)), // AT = old_ot_head
load_upper_i(R_V0, (S_(Poly_G4)/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits), // V0 = (9 - 1) << 24 = 8 << 24
mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)), // Strip upper 8 bits (length from prev cell) → keep only low 24 mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)), // Strip upper 8 bits (length from prev cell) → keep only low 24
or_u( R_AT, R_AT, R_V0), // Merge length or_u( R_AT, R_AT, R_V0), // Merge length
store_word( R_AT, r_prim_cursor, O_(PolyTag,code)), // prim->tag = packed(prim_length, old_addr) store_word( R_AT, r_prim_cursor, O_(PolyTag,code)), // prim->tag = packed(prim_length, old_addr)
+63 -60
View File
@@ -21,7 +21,7 @@
* 4. Semantic encoders gp0_word_poly_f3(r,g,b) * 4. Semantic encoders gp0_word_poly_f3(r,g,b)
* 3. Composite encoders enc_color_word(cmd, r, g, b) * 3. Composite encoders enc_color_word(cmd, r, g, b)
* 2. Per-field encoders enc_gp0_color_r(r), enc_gp0_color_g(g), ... * 2. Per-field encoders enc_gp0_color_r(r), enc_gp0_color_g(g), ...
* 1. Bitfield layout consts gp0_color_red_pos = 0, gp0_color_red_width = 8 * 1. Bitfield layout consts gp0_color_red_shift = 0, gp0_color_red_mask = 0xFF
* 0. Opcode IDs gp0_cmd_poly_f3 = 0x20 * 0. Opcode IDs gp0_cmd_poly_f3 = 0x20
* *
* Vendor mnemonics (gte_mtc2, gte_mfc2, etc.) are NOT in this header. * Vendor mnemonics (gte_mtc2, gte_mfc2, etc.) are NOT in this header.
@@ -68,14 +68,13 @@ enum {
#define gp0_send(word) (HW_GP0[0] = (word)) #define gp0_send(word) (HW_GP0[0] = (word))
#define gp1_send(word) (HW_GP1[0] = (word)) #define gp1_send(word) (HW_GP1[0] = (word))
#define DmaSlot_ // Annotate an instruction as filling a CPU <-> Command DMA delay slot/s
/* ============================================================================ /* ============================================================================
* GP0 command byte constants + Layer 1 (GPU bitfield shifts) * GP0 command byte constants + Layer 1 (GPU bitfield shifts)
* ============================================================================ * ============================================================================
* 8-bit GP0 opcodes (the upper byte of a primitive's first word). These are the BYTE only. * 8-bit GP0 opcodes (the upper byte of a primitive's first word). These are the BYTE only.
* NO macro body past this point uses a raw shift or raw mask. * NO macro body past this point uses a raw shift or raw mask.
* Mirrors the OPCODE_POS / RS_POS convention from mips.h. * Mirrors the OPCODE_SHIFT / RS_SHIFT / REG_MASK convention from mips.h.
* ============================================================================ */ * ============================================================================ */
enum { enum {
gp0_cmd_Nop = 0x00, gp0_cmd_Nop = 0x00,
@@ -117,20 +116,21 @@ enum {
gp0_cmd_SetDrawOffset = 0xE5, gp0_cmd_SetDrawOffset = 0xE5,
gp0_cmd_SetMaskBit = 0xE6, gp0_cmd_SetMaskBit = 0xE6,
/* bitfield offset pos / widths ---- /* bitfield shifts / widths / masks ----
* Generic GP0/GP1 command byte (upper 8 bits of every word sent to either port). */ * Generic GP0/GP1 command byte (upper 8 bits of every word sent to either port). */
gp0_cmd_pos = 24, gp0_cmd_shift = 24,
gp0_cmd_width = 8, gp0_cmd_width = 8,
gp0_cmd_mask = 0xFF,
/* Color word layout (lives in Poly_F3.color, Poly_G4.c0..c3, etc.): /* Color word layout (lives in Poly_F3.color, Poly_G4.c0..c3, etc.):
* bits 31..24 = command byte * bits 31..24 = command byte
* bits 23..16 = BLUE * bits 23..16 = BLUE
* bits 15..08 = GREEN * bits 15..08 = GREEN
* bits 07..00 = RED (PSX GPU is BGR, NOT RGB) */ * bits 07..00 = RED (PSX GPU is BGR, NOT RGB) */
gp0_color_cmd_pos = 24, gp0_color_cmd_width = 8, gp0_color_cmd_shift = 24, gp0_color_cmd_width = 8, gp0_color_cmd_mask = 0xFF,
gp0_color_blue_pos = 16, gp0_color_blue_width = 8, gp0_color_blue_shift = 16, gp0_color_blue_width = 8, gp0_color_blue_mask = 0xFF,
gp0_color_green_pos = 8, gp0_color_green_width = 8, gp0_color_green_shift = 8, gp0_color_green_width = 8, gp0_color_green_mask = 0xFF,
gp0_color_red_pos = 0, gp0_color_red_width = 8, gp0_color_red_shift = 0, gp0_color_red_width = 8, gp0_color_red_mask = 0xFF,
}; };
/* ============================================================================ /* ============================================================================
@@ -143,12 +143,12 @@ enum {
* ============================================================================ */ * ============================================================================ */
/* ---- Layer 1.5: per-field encoders ---- */ /* ---- Layer 1.5: per-field encoders ---- */
#define enc_gp0_cmd(cmd) ((cmd) << gp0_cmd_pos) #define enc_gp0_cmd(cmd) (((cmd) & gp0_cmd_mask) << gp0_cmd_shift)
#define enc_gp0_color_cmd(cmd) ((cmd) << gp0_color_cmd_pos) #define enc_gp0_color_cmd(cmd) (((cmd) & gp0_color_cmd_mask) << gp0_color_cmd_shift)
#define enc_gp0_color_r(r) ((r) << gp0_color_red_pos) #define enc_gp0_color_r(r) (((r) & gp0_color_red_mask) << gp0_color_red_shift)
#define enc_gp0_color_g(g) ((g) << gp0_color_green_pos) #define enc_gp0_color_g(g) (((g) & gp0_color_green_mask) << gp0_color_green_shift)
#define enc_gp0_color_b(b) ((b) << gp0_color_blue_pos) #define enc_gp0_color_b(b) (((b) & gp0_color_blue_mask) << gp0_color_blue_shift)
/* ---- Layer 2: composite encoders ---- */ /* ---- Layer 2: composite encoders ---- */
#define enc_color_word(cmd, r, g, b) (enc_gp0_color_cmd(cmd) | enc_gp0_color_r(r) | enc_gp0_color_g(g) | enc_gp0_color_b(b)) #define enc_color_word(cmd, r, g, b) (enc_gp0_color_cmd(cmd) | enc_gp0_color_r(r) | enc_gp0_color_g(g) | enc_gp0_color_b(b))
@@ -211,38 +211,38 @@ enum {
gp1_disp_Color24 = 0x1, gp1_disp_Color24 = 0x1,
gp1_disp_VInterlace = 0x1, gp1_disp_VInterlace = 0x1,
/* ---- Layer 1: GP1 display-mode + range + draw-area shifts/widths ---- */ /* ---- Layer 1: GP1 display-mode + range + draw-area shifts/masks ---- */
gp1_disp_hres_pos = 0, gp1_disp_hres_width = 2, gp1_disp_hres_shift = 0, gp1_disp_hres_width = 2, gp1_disp_hres_mask = 0x3,
gp1_disp_vres_pos = 2, gp1_disp_vres_width = 1, gp1_disp_vres_shift = 2, gp1_disp_vres_width = 1, gp1_disp_vres_mask = 0x1,
gp1_disp_color_pos = 4, gp1_disp_color_width = 1, gp1_disp_color_shift = 4, gp1_disp_color_width = 1, gp1_disp_color_mask = 0x1,
gp1_disp_interlace_pos = 5, gp1_disp_interlace_width = 1, gp1_disp_interlace_shift = 5, gp1_disp_interlace_width = 1, gp1_disp_interlace_mask = 0x1,
/* GP1 horizontal display range: bits 0..11 = X2, bits 12..23 = X1 */ /* GP1 horizontal display range: bits 0..11 = X2, bits 12..23 = X1 */
gp1_hrange_x1_pos = 12, gp1_hrange_x1_width = 12, gp1_hrange_x1_shift = 12, gp1_hrange_x1_width = 12, gp1_hrange_x1_mask = 0xFFF,
gp1_hrange_x2_pos = 0, gp1_hrange_x2_width = 12, gp1_hrange_x2_shift = 0, gp1_hrange_x2_width = 12, gp1_hrange_x2_mask = 0xFFF,
/* GP1 vertical display range: bits 0..9 = Y2, bits 10..19 = Y1 */ /* GP1 vertical display range: bits 0..9 = Y2, bits 10..19 = Y1 */
gp1_vrange_y1_pos = 10, gp1_vrange_y1_width = 10, gp1_vrange_y1_shift = 10, gp1_vrange_y1_width = 10, gp1_vrange_y1_mask = 0x3FF,
gp1_vrange_y2_pos = 0, gp1_vrange_y2_width = 10, gp1_vrange_y2_shift = 0, gp1_vrange_y2_width = 10, gp1_vrange_y2_mask = 0x3FF,
/* GP1 draw area (top-left or bottom-right): bits 0..9 = X, bits 10..19 = Y /* GP1 draw area (top-left or bottom-right): bits 0..9 = X, bits 10..19 = Y
* (10-bit signed — caller pre-signs) */ * (10-bit signed — caller pre-signs and masks with the named mask) */
gp1_draw_x_pos = 0, gp1_draw_x_width = 10, gp1_draw_x_shift = 0, gp1_draw_x_width = 10, gp1_draw_x_mask = 0x3FF,
gp1_draw_y_pos = 10, gp1_draw_y_width = 10, gp1_draw_y_shift = 10, gp1_draw_y_width = 10, gp1_draw_y_mask = 0x3FF,
}; };
/* ---- Layer 1.5: GP1 per-field encoders ---- */ /* ---- Layer 1.5: GP1 per-field encoders ---- */
#define enc_gp1_disp_hres(h) ((h) << gp1_disp_hres_pos) #define enc_gp1_disp_hres(h) (((h) & gp1_disp_hres_mask) << gp1_disp_hres_shift)
#define enc_gp1_disp_vres(v) ((v) << gp1_disp_vres_pos) #define enc_gp1_disp_vres(v) (((v) & gp1_disp_vres_mask) << gp1_disp_vres_shift)
#define enc_gp1_disp_color(c) ((c) << gp1_disp_color_pos) #define enc_gp1_disp_color(c) (((c) & gp1_disp_color_mask) << gp1_disp_color_shift)
#define enc_gp1_disp_interlace(i) ((i) << gp1_disp_interlace_pos) #define enc_gp1_disp_interlace(i) (((i) & gp1_disp_interlace_mask) << gp1_disp_interlace_shift)
#define enc_gp1_hrange_x1(x1) ((x1) << gp1_hrange_x1_pos) #define enc_gp1_hrange_x1(x1) (((x1) & gp1_hrange_x1_mask) << gp1_hrange_x1_shift)
#define enc_gp1_hrange_x2(x2) ((x2) << gp1_hrange_x2_pos) #define enc_gp1_hrange_x2(x2) (((x2) & gp1_hrange_x2_mask) << gp1_hrange_x2_shift)
#define enc_gp1_vrange_y1(y1) ((y1) << gp1_vrange_y1_pos) #define enc_gp1_vrange_y1(y1) (((y1) & gp1_vrange_y1_mask) << gp1_vrange_y1_shift)
#define enc_gp1_vrange_y2(y2) ((y2) << gp1_vrange_y2_pos) #define enc_gp1_vrange_y2(y2) (((y2) & gp1_vrange_y2_mask) << gp1_vrange_y2_shift)
#define enc_gp1_draw_x(x) ((x) << gp1_draw_x_pos) #define enc_gp1_draw_x(x) (((x) & gp1_draw_x_mask) << gp1_draw_x_shift)
#define enc_gp1_draw_y(y) ((y) << gp1_draw_y_pos) #define enc_gp1_draw_y(y) (((y) & gp1_draw_y_mask) << gp1_draw_y_shift)
/* ---- Layer 2: GP1 composite encoders ---- */ /* ---- Layer 2: GP1 composite encoders ---- */
#define enc_gp1_disp_mode_word(h, v, c, i) (enc_gp0_cmd(gp1_cmd_DisplayMode) | enc_gp1_disp_hres(h) | enc_gp1_disp_vres(v) | enc_gp1_disp_color(c) | enc_gp1_disp_interlace(i)) #define enc_gp1_disp_mode_word(h, v, c, i) (enc_gp0_cmd(gp1_cmd_DisplayMode) | enc_gp1_disp_hres(h) | enc_gp1_disp_vres(v) | enc_gp1_disp_color(c) | enc_gp1_disp_interlace(i))
@@ -419,11 +419,14 @@ typedef Struct_(PolyTag) {
}; };
}; };
/* DSL cast convention: every cast uses `C_()`, every pointer qualifier is `R_` (restrict) or `V_` (volatile).
* No raw C-style casts. RHS values are assumed to be `U4` — caller passes a `U4` directly. */
#define set_len(tag,v) (C_(PolyTag_R,tag)->len = u4_(v)) #define set_len(tag,v) (C_(PolyTag_R,tag)->len = u4_(v))
#define set_addr(tag,v) (C_(PolyTag_R,tag)->addr = u4_(v)) #define set_addr(tag,v) (C_(PolyTag_R,tag)->addr = u4_(v))
/* `set_code` is no longer in the new PolyTag design /* `set_code` is no longer in the new PolyTag design — the code byte lives in the primitive body
* (e.g. `((Poly_F3*)(p))->code`), not in the tag. * (e.g. `((Poly_F3*)(p))->code`), not in the tag.
* Use the typed primitive structs (Poly_F3, Poly_G4, etc.) and the `set_poly_*` setters, which set both the tag's length and the code. */ * Use the typed primitive structs (Poly_F3, Poly_G4, etc.) and the `set_poly_*` setters,
* which set both the tag's length and the code. */
#define get_len(tag) C_(U4,C_(PolyTag_R,tag)->len) #define get_len(tag) C_(U4,C_(PolyTag_R,tag)->len)
#define get_addr(tag) C_(U4,C_(PolyTag_R,tag)->addr) #define get_addr(tag) C_(U4,C_(PolyTag_R,tag)->addr)
@@ -552,17 +555,17 @@ typedef Struct_(Poly_GT4) {
* bits 12..31 = reserved (zero) * bits 12..31 = reserved (zero)
* ============================================================================ */ * ============================================================================ */
enum { enum {
/* ---- Layer 1: TPage bitfield shifts / widths ---- */ /* ---- Layer 1: TPage bitfield shifts / widths / masks ---- */
gp0_tpage_x_pos = 0, gp0_tpage_x_width = 4, gp0_tpage_x_shift = 0, gp0_tpage_x_width = 4, gp0_tpage_x_mask = 0xF,
gp0_tpage_y_pos = 4, gp0_tpage_y_width = 1, gp0_tpage_y_shift = 4, gp0_tpage_y_width = 1, gp0_tpage_y_mask = 0x1,
gp0_tpage_semi_trans_pos = 5, gp0_tpage_semi_trans_width = 2, gp0_tpage_semi_trans_shift = 5, gp0_tpage_semi_trans_width = 2, gp0_tpage_semi_trans_mask = 0x3,
gp0_tpage_color_depth_pos = 7, gp0_tpage_color_depth_width = 2, gp0_tpage_color_depth_shift = 7, gp0_tpage_color_depth_width = 2, gp0_tpage_color_depth_mask = 0x3,
gp0_tpage_dither_pos = 9, gp0_tpage_dither_width = 1, gp0_tpage_dither_shift = 9, gp0_tpage_dither_width = 1, gp0_tpage_dither_mask = 0x1,
gp0_tpage_draw_to_disp_pos = 10, gp0_tpage_draw_to_disp_width = 1, gp0_tpage_draw_to_disp_shift = 10, gp0_tpage_draw_to_disp_width = 1, gp0_tpage_draw_to_disp_mask = 0x1,
gp0_tpage_tex_disable_pos = 11, gp0_tpage_tex_disable_width = 1, gp0_tpage_tex_disable_shift = 11, gp0_tpage_tex_disable_width = 1, gp0_tpage_tex_disable_mask = 0x1,
/* TPage color-depth payload values (NOT bit positions — these go in /* TPage color-depth payload values (NOT bit positions — these go in
* the 2-bit field at gp0_tpage_color_depth_pos). */ * the 2-bit field at gp0_tpage_color_depth_shift). */
gp0_tpage_color_4bpp = 0x0, gp0_tpage_color_4bpp = 0x0,
gp0_tpage_color_8bpp = 0x1, gp0_tpage_color_8bpp = 0x1,
gp0_tpage_color_16bpp = 0x2, gp0_tpage_color_16bpp = 0x2,
@@ -570,7 +573,7 @@ enum {
/* Default TPage value libpsyx's SetDefDrawEnv writes (matches the `li v1, 10; sh v1, 20(v0)` sequence at C11_only.elf:0x8001273C). */ /* Default TPage value libpsyx's SetDefDrawEnv writes (matches the `li v1, 10; sh v1, 20(v0)` sequence at C11_only.elf:0x8001273C). */
gp0_tpage_default = 10, gp0_tpage_default = 10,
/* TPage semi-transparency mode payload values. */ /* TPage semi-transparency mode payload values (NOT bit positions). */
gp0_tpage_semi_trans_none = 0x0, gp0_tpage_semi_trans_none = 0x0,
gp0_tpage_semi_trans_alpha = 0x1, gp0_tpage_semi_trans_alpha = 0x1,
gp0_tpage_semi_trans_add = 0x2, gp0_tpage_semi_trans_add = 0x2,
@@ -578,13 +581,13 @@ enum {
}; };
/* ---- Layer 1.5: TPage per-field encoders. Mirrors enc_gte_sf/mx/v in gte.h. ---- */ /* ---- Layer 1.5: TPage per-field encoders. Mirrors enc_gte_sf/mx/v in gte.h. ---- */
#define enc_gp0_tpage_x(x) ((x) << gp0_tpage_x_pos) #define enc_gp0_tpage_x(x) (((x) & gp0_tpage_x_mask) << gp0_tpage_x_shift)
#define enc_gp0_tpage_y(y) ((y) << gp0_tpage_y_pos) #define enc_gp0_tpage_y(y) (((y) & gp0_tpage_y_mask) << gp0_tpage_y_shift)
#define enc_gp0_tpage_semi_trans(s) ((s) << gp0_tpage_semi_trans_pos) #define enc_gp0_tpage_semi_trans(s) (((s) & gp0_tpage_semi_trans_mask) << gp0_tpage_semi_trans_shift)
#define enc_gp0_tpage_color_depth(c) ((c) << gp0_tpage_color_depth_pos) #define enc_gp0_tpage_color_depth(c) (((c) & gp0_tpage_color_depth_mask) << gp0_tpage_color_depth_shift)
#define enc_gp0_tpage_dither(d) ((d) << gp0_tpage_dither_pos) #define enc_gp0_tpage_dither(d) (((d) & gp0_tpage_dither_mask) << gp0_tpage_dither_shift)
#define enc_gp0_tpage_draw_to_disp(d) ((d) << gp0_tpage_draw_to_disp_pos) #define enc_gp0_tpage_draw_to_disp(d) (((d) & gp0_tpage_draw_to_disp_mask) << gp0_tpage_draw_to_disp_shift)
#define enc_gp0_tpage_tex_disable(t) ((t) << gp0_tpage_tex_disable_pos) #define enc_gp0_tpage_tex_disable(t) (((t) & gp0_tpage_tex_disable_mask) << gp0_tpage_tex_disable_shift)
/* ---- Layer 2: TPage composite encoder. Mirrors enc_gte_cmdw in gte.h ---- */ /* ---- Layer 2: TPage composite encoder. Mirrors enc_gte_cmdw in gte.h ---- */
#define enc_gp0_tpage_word(x, y, semi_trans, color_depth, dither, draw_to_disp, tex_disable) \ #define enc_gp0_tpage_word(x, y, semi_trans, color_depth, dither, draw_to_disp, tex_disable) \
@@ -614,17 +617,17 @@ typedef Struct_(TexturePage) { U4 raw; };
* bits 24..31 = command byte — 0x20 (4bpp load) or 0x25 (8bpp load) * bits 24..31 = command byte — 0x20 (4bpp load) or 0x25 (8bpp load)
* ============================================================================ */ * ============================================================================ */
enum { enum {
/* ---- Layer 1: CLUT bitfield shifts / widths ---- */ /* ---- Layer 1: CLUT bitfield shifts / widths / masks ---- */
gp0_clut_y_pos = 0, gp0_clut_y_width = 6, gp0_clut_y_shift = 0, gp0_clut_y_width = 6, gp0_clut_y_mask = 0x3F,
gp0_clut_x_pos = 6, gp0_clut_x_width = 9, gp0_clut_x_shift = 6, gp0_clut_x_width = 9, gp0_clut_x_mask = 0x1FF,
/* CLUT-load cmd-byte variants — the upper byte of the GP0 word. */ /* CLUT-load cmd-byte variants — the upper byte of the GP0 word. */
gp0_clut_cmd_Load4bpp = 0x20, gp0_clut_cmd_Load4bpp = 0x20,
gp0_clut_cmd_Load8bpp = 0x25, gp0_clut_cmd_Load8bpp = 0x25,
}; };
/* ---- Layer 1.5: CLUT per-field encoders ---- */ /* ---- Layer 1.5: CLUT per-field encoders ---- */
#define enc_gp0_clut_x(x) ((x) << gp0_clut_x_pos) #define enc_gp0_clut_x(x) (((x) & gp0_clut_x_mask) << gp0_clut_x_shift)
#define enc_gp0_clut_y(y) ((y) << gp0_clut_y_pos) #define enc_gp0_clut_y(y) (((y) & gp0_clut_y_mask) << gp0_clut_y_shift)
/* ---- Layer 2: CLUT composite encoder ---- */ /* ---- Layer 2: CLUT composite encoder ---- */
#define enc_gp0_clut_word(cmd, x, y) (enc_gp0_cmd(cmd) | enc_gp0_clut_x(x) | enc_gp0_clut_y(y)) #define enc_gp0_clut_word(cmd, x, y) (enc_gp0_cmd(cmd) | enc_gp0_clut_x(x) | enc_gp0_clut_y(y))
+21 -339
View File
@@ -11,62 +11,25 @@ ATOM_FILE_DEBUGGER_LINE_MARKER(gte_atom_c);
#pragma region MACs (Mips Atom Components) #pragma region MACs (Mips Atom Components)
/* Words: 3; Loads 3 S2 indices from the face array */ /* Words: 3; Loads 3 S2 indices from the face array */
FI_ Slice_MipsCode ac_load_tri_indices(AtomBuilder_R ab, U4 r_face_cusor, U4 r_i0, U4 r_i1, U4 r_i2) FI_ Slice_MipsCode ac_load_tri_indices(U4 r_face_cusor, U4 r_i0, U4 r_i1, U4 r_i2) atom_dbg_skip MipsAtomComp_Proc_(ac_load_tri_indices, {
atom_dbg_skip MipsAtomComp_Proc_(ab, {
load_half_u(r_i0, r_face_cusor, 0 * S_(S2)), load_half_u(r_i0, r_face_cusor, 0 * S_(S2)),
load_half_u(r_i1, r_face_cusor, 1 * S_(S2)), load_half_u(r_i1, r_face_cusor, 1 * S_(S2)),
load_half_u(r_i2, r_face_cusor, 2 * S_(S2)), load_half_u(r_i2, r_face_cusor, 2 * S_(S2)),
}) })
FI_ Slice_MipsCode ac_gte_mv_to_cr_diag_v3s4(AtomBuilder_R ab, Reg_(V3_S4) v) MipsAtomComp_Proc_(ab, {
gte_mv_to_ctrl_r(v.y, gte_cr_RT13),
gte_mv_to_ctrl_r(v.z, gte_cr_RT22),
gte_mv_to_ctrl_r(v.x, gte_cr_RT11),
})
FI_ Slice_MipsCode ac_gte_ld_ir123_v3s4(AtomBuilder_R ab, Reg_(V3_S4) v) MipsAtomComp_Proc_(ab, {
gte_mv_to_data_r(v.x, C2_IR1),
gte_mv_to_data_r(v.y, C2_IR2),
gte_mv_to_data_r(v.z, C2_IR3),
})
/* ─── GTE OP cross product (a × b → a) ───
* Sets up RT diagonal from a.xyz, IR1/2/3 from b.xyz, fires OP,
* reads MAC1/2/3, shifts right 12 (S12.20 → S12.0 OuterProduct12), writes back to a.xyz.
* Composes the three sub-primitives (RT-load, IR-load, OP, MAC-read, shift)
* into one component for use by atoms that need the cross product inline.
*
* Output gpr (a) aliases source-A gpr; MAC read clobbers source-A's load targets,
* but by that point the RT load is complete and source A is dead.
* Pipeline: clobbers IR1..3, MAC1..3, RT11..33.
*
* The CPU→COP2 transfer chains (3 ctc2, 3 mtc2) require a 2-slot retirement gap,
* and the MFC2→GPR chain (3 mfc2) requires a 1-slot retirement gap, before the GPR can be read.
* The hazard nops are inlined below — same convention as ac_gte_gpf_scale — so any atom body inlining this component inherits them.
*
* Words: 18 (3 ctc2 + 2 nop + 3 mtc2 + 2 nop + 1 op + 3 mfc2 + 1 nop + 3 sra).
*/
FI_ Slice_MipsCode ac_gte_op_cross_v3s4(AtomBuilder_R ab, Reg_(V3_S4) a, Reg_(V3_S4) b) atom_dbg_skip MipsAtomComp_Proc_(ab, {
mac_gte_mv_to_cr_diag_v3s4(a), GteDelay_ /* RT diagonal: D1 = a.x, D2 = a.y, D3 = a.z */
mac_gte_ld_ir123_v3s4(b), GteDelay_ /* IR: second operand (b.xyz) */
gte_cmdw_cross, /* OP: MAC1/2/3 = a × b (S12.20) */
mac_gte_mv_from_mac123_v3s4(a), GteDelay_ /* Read MAC1/2/3 → a.xyz (overwrites source-A's load targets) */
mac_shift_aright_v3s4_self(a, 12), /* Right-shift MAC by 12 (S12.20 → S12.0 OuterProduct12) */
})
/* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices to the F3. /* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices to the F3.
* PIPELINE: post-RTPT (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen). */ * PIPELINE: post-RTPT (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen). */
FI_ Slice_MipsCode ac_gte_store_f3(AtomBuilder_R ab, U4 r_primitive_cursor) atom_dbg_skip MipsAtomComp_Proc_(ab, { FI_ Slice_MipsCode ac_gte_store_f3(U4 r_primitive_cursor) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_store_f3, {
gte_sw(C2_SXY0, r_primitive_cursor, O_(Poly_F3,p0)), gte_sw(C2_SXY0, r_primitive_cursor, O_(Poly_F3,p0)),
gte_sw(C2_SXY1, r_primitive_cursor, O_(Poly_F3,p1)), gte_sw(C2_SXY1, r_primitive_cursor, O_(Poly_F3,p1)),
gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_F3,p2)), gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_F3,p2)),
}) })
/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */ /* Words: 18; Translates indices to vertex addresses and pushes them to GTE */
I_ Slice_MipsCode ac_gte_load_tri_verts(AtomBuilder_R ab, Reg vbase, Reg v0, Reg v1, Reg v2) atom_dbg_skip MipsAtomComp_Proc_(ab, { I_ Slice_MipsCode ac_gte_load_tri_verts(U4 r_vert_base, U4 r_v0, U4 r_v1, U4 r_v2) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_load_tri_verts, {
shift_lleft(R_AT, v0, v3s2_byteoff), add_u_self(R_AT, vbase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0), shift_lleft(R_AT, r_v0, v3s2_byteoff), add_u_self(R_AT, r_vert_base), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0),
shift_lleft(R_AT, v1, v3s2_byteoff), add_u_self(R_AT, vbase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY1), gte_mv_to_data_r(R_V1, C2_VZ1), shift_lleft(R_AT, r_v1, v3s2_byteoff), add_u_self(R_AT, r_vert_base), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY1), gte_mv_to_data_r(R_V1, C2_VZ1),
shift_lleft(R_AT, v2, v3s2_byteoff), add_u_self(R_AT, vbase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY2), gte_mv_to_data_r(R_V1, C2_VZ2), shift_lleft(R_AT, r_v2, v3s2_byteoff), add_u_self(R_AT, r_vert_base), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY2), gte_mv_to_data_r(R_V1, C2_VZ2),
}) })
/* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices of the /* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices of the
@@ -74,7 +37,7 @@ I_ Slice_MipsCode ac_gte_load_tri_verts(AtomBuilder_R ab, Reg vbase, Reg v0, Reg
* PIPELINE: post-RTPT, pre-RTPS (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen). * PIPELINE: post-RTPT, pre-RTPS (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen).
* MUST be called BEFORE V3-RTPS, otherwise SXY0/1/2 get overwritten with v3 * MUST be called BEFORE V3-RTPS, otherwise SXY0/1/2 get overwritten with v3
* (RTPS writes only to SXY2, but to keep the three registers aligned with v0/v1/v2 you must store before RTPS). */ * (RTPS writes only to SXY2, but to keep the three registers aligned with v0/v1/v2 you must store before RTPS). */
FI_ Slice_MipsCode ac_gte_store_g4_p012(AtomBuilder_R ab, Reg r_primitive_cursor) atom_dbg_skip MipsAtomComp_Proc_(ab, { FI_ Slice_MipsCode ac_gte_store_g4_p012(U4 r_primitive_cursor) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_store_g4_p012, {
gte_sw(C2_SXY0, r_primitive_cursor, O_(Poly_G4,p0)), gte_sw(C2_SXY0, r_primitive_cursor, O_(Poly_G4,p0)),
gte_sw(C2_SXY1, r_primitive_cursor, O_(Poly_G4,p1)), gte_sw(C2_SXY1, r_primitive_cursor, O_(Poly_G4,p1)),
gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_G4,p2)), gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_G4,p2)),
@@ -84,310 +47,29 @@ FI_ Slice_MipsCode ac_gte_store_g4_p012(AtomBuilder_R ab, Reg r_primitive_cursor
* PIPELINE: post-RTPS (SXY2 holds v3.screen because RTPS writes its single-vertex result to SXY2; * PIPELINE: post-RTPS (SXY2 holds v3.screen because RTPS writes its single-vertex result to SXY2;
* SXY0 still holds v0.screen from the earlier RTPT. * SXY0 still holds v0.screen from the earlier RTPT.
*/ */
FI_ Slice_MipsCode ac_gte_store_g4_p3(AtomBuilder_R ab, U4 r_primitive_cursor) atom_dbg_skip MipsAtomComp_Proc_(ab, { gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_G4,p3)) }) FI_ Slice_MipsCode ac_gte_store_g4_p3(U4 r_primitive_cursor) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_store_g4_p3, { gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_G4,p3)) })
/* ─── STAGE 1 of normalize: SQR + mfc2 MAC1/2/3 ───
* Emits squared magnitude per component (in MAC1/2/3) into caller-provided scratch regs. */
FI_ Slice_MipsCode ac_gte_sqr_v3(AtomBuilder_R ab, U4 r_sx, U4 r_sy, U4 r_sz, U4 r_sq_x, U4 r_sq_y, U4 r_sq_z) atom_dbg_skip MipsAtomComp_Proc_(ab, {
mac_gte_sqr_v3s4(r_sx, r_sy, r_sz, nop),
gte_mv_from_data_r(r_sq_x, C2_MAC1),
gte_mv_from_data_r(r_sq_y, C2_MAC2),
gte_mv_from_data_r(r_sq_z, C2_MAC3),
})
/* ─── SQR FIRE — mtc2 3 GPRs into IR1/IR2/IR3, then fire SQR. ─── */
FI_ Slice_MipsCode ac_gte_sqr_v3s4(AtomBuilder_R ab, Reg r_sx, Reg r_sy, Reg r_sz, MipsCode delay_slot)
atom_dbg_skip MipsAtomComp_Proc_(ab, {
gte_mv_to_data_r(r_sx, C2_IR1),
gte_mv_to_data_r(r_sy, C2_IR2),
gte_mv_to_data_r(r_sz, C2_IR3),
delay_slot, gte_cmdw_sqr,
})
/* ─── STAGE 4 of normalize: mtc2 IR0..3 + GPF + mfc2 MAC + srav finalize ───
* Reusable standalone — given an IR0 = 1/|v| estimate (typically from a sqrtbl lookup) and a shift count
* (typically (31 - LZCR)/2), multiplies IR0*IR[i] via GPF and shifts right to produce the normalized output.
* Used standalone for "scale vector by scalar".
* Words: 11. Clobbers: IR0..3, MAC1..3. Uses gte_cmdw_gpf (sf=0, lm=0). */
FI_ Slice_MipsCode ac_gte_gpf_scale(AtomBuilder_R ab,
U4 r_sx, U4 r_sy, U4 r_sz,
U4 r_recip_est, U4 r_shift,
U4 r_dx, U4 r_dy, U4 r_dz)
atom_dbg_skip MipsAtomComp_Proc_(ab, {
gte_mv_to_data_r(r_recip_est, C2_IR0),
gte_mv_to_data_r(r_sx, C2_IR1),
gte_mv_to_data_r(r_sy, C2_IR2),
gte_mv_to_data_r(r_sz, C2_IR3),
GteDelay_ nop2, /* retire IR0..IR3 → GPF input pre-fill (matches libgte 0x80016134..0x80016138) */
gte_cmdw_gpf,
gte_mv_from_data_r(r_dx, C2_MAC1),
gte_mv_from_data_r(r_dy, C2_MAC2),
gte_mv_from_data_r(r_dz, C2_MAC3),
shift_aright_var(r_dx, r_dx, r_shift),
shift_aright_var(r_dy, r_dy, r_shift),
shift_aright_var(r_dz, r_dz, r_shift),
})
/* ─── TRANS MATRIX (libgte TransMatrix port) ───
* Atom component — auto-generates mac_trans_matrix Mac composer macro.
* m->t = v (struct copy; libgte's TransMatrix at 0x8001a540 is just 3 store_words, no GTE, no add).
* Uses 1 GPR (r_t1 = off value) per axis; per-axis load-delay-slot pattern.
* Words: 9. Clobbers: r_t1. */
FI_ Slice_MipsCode ac_trans_mt3s3s4(AtomBuilder_R ab
, U4 r_mtx, U4 r_off
, U4 r_t0, U4 r_t1, U4 r_t2
) MipsAtomComp_Proc_(ab, {
load_word( r_t0, r_off, O_(V3_S4,x)),
load_word( r_t1, r_off, O_(V3_S4,y)),
load_word( r_t2, r_off, O_(V3_S4,z)),
store_word(r_t0, r_mtx, O_(MT3_S2S4,t[0])),
store_word(r_t1, r_mtx, O_(MT3_S2S4,t[1])),
store_word(r_t2, r_mtx, O_(MT3_S2S4,t[2])),
})
/* ─── LZCR ROUND EVEN + HALF-SHIFT ───
* Takes the raw LZCR leading-zero/ones count (from mfc2 C2_LZCR, range 1..32 per PSX-SPX cop2r31) and the |v|² sum (in r_mag_sq from the MAC1+MAC2+MAC3 add).
* Produces:
* r_shift ← LZCR rounded down to even (clear bit 0)
* r_mag_sq_copy ← |v|² sum (moved out of r_mag_sq before it's overwritten)
* r_mag_sq ← (31 - even_LZCR) / 2 = the final srav/GPF shift amount
*
* Rounding to even ensures (31 - LZCR) is always odd, so the >> 1 division is consistent — no 0.5 loss.
* The caller branches on LZCR < 24 to decide left-shift vs right-shift of r_mag_sq_copy, then saves the shift count.
*
* Note: C2_LZCR (cop2r31) is a fixed read-only C2 data register — the caller must read it via mfc2 from C2_LZCR;
* there is no register choice at the hardware level. Only the GPR that holds the result is caller-determined. */
FI_ Slice_MipsCode ac_lzcr_round_even_half_shift(AtomBuilder_R ab,
U4 r_shift,
U4 r_mag_sq,
U4 r_mag_sq_copy)
atom_dbg_skip MipsAtomComp_Proc_(ab, {
and_i(r_shift, r_shift, gte_lzcr_even_mask),
or_u(r_mag_sq_copy, r_mag_sq, 0),
li_s( r_mag_sq, 31),
sub_s( r_mag_sq, r_mag_sq, r_shift),
shift_aright(r_mag_sq, r_mag_sq, 1),
})
FI_ Slice_MipsCode ac_gte_general_purpose_interopolation(AtomBuilder_R ab
, Reg to_ir0, Reg to_ir1, Reg to_ir2, Reg to_ir3
, Reg fr_mac1, Reg fr_mac2, Reg fr_mac3
, MipsCode nop_slot1, MipsCode nop_slot2)
MipsAtomComp_Proc_(ab, {
gte_mv_to_data_r(to_ir0, C2_IR0),
gte_mv_to_data_r(to_ir1, C2_IR1), /* IR1 = src.x (preserved in r_tmp — r_mac2_scratch was clobbered to MAC2 in stage 1.5) */
gte_mv_to_data_r(to_ir2, C2_IR2),
gte_mv_to_data_r(to_ir3, C2_IR3), /* IR3 = src.z (reloaded) */
GteDelay_ nop_slot1,
GteDelay_ nop_slot2,
gte_cmdw_gpf,
gte_mv_from_data_r(fr_mac1, C2_MAC1),
gte_mv_from_data_r(fr_mac2, C2_MAC2),
gte_mv_from_data_r(fr_mac3, C2_MAC3),
})
FI_ Slice_MipsCode ac_gte_mv_from_data_r_mac123(AtomBuilder_R ab
, Reg fr_mac1, Reg fr_mac2, Reg fr_mac3)
MipsAtomComp_Proc_(ab, {
gte_mv_from_data_r(fr_mac1, C2_MAC1),
gte_mv_from_data_r(fr_mac2, C2_MAC2),
gte_mv_from_data_r(fr_mac3, C2_MAC3),
})
FI_ Slice_MipsCode ac_gte_mv_from_mac123_v3s4(AtomBuilder_R ab, Reg_(V3_S4) v) MipsAtomComp_ProcMap_(ab, mac_gte_mv_from_data_r_mac123(v.x, v.y, v.z))
#pragma endregion MACs (Mips Atom Components) #pragma endregion MACs (Mips Atom Components)
#pragma region Atom Procs #pragma region Bsked Atoms
/* ─── Local copy of PSYQ's sqrtbl (1/sqrt lookup table for VectorNormal). ─── typedef Struct_(Binds_SetGteWorld) {
* Source: PSYQ 4.7 libgte sqrtbl at 0x800185B4 in hello_camera.elf. M3_S2* transform;
* objdump -s --start-address=0x800185B4 --stop-address=0x800185F4 hello_camera.elf → 192 entries × 16-bit signed, in 1.12 fixed-point (max value 0x1000 = 1.0).
*
* Data is identical to the libgte original (byte-for-byte verified).
*
* ─── Per-entry semantics (decoded from libgte msc02 VectorNormal) ───
* Each entry is `1/sqrt(x)` in 1.12 fixed point (value / 4096).
* The 192 entries span 4 octaves of the input magnitude, with 48 entries per octave:
* Octave 0 (entries 0- 47): mantissa in [0x8000, 0x10000) output ~[1.000, 0.707]
* Octave 1 (entries 48- 95): mantissa in [0x10000, 0x20000) output ~[0.707, 0.500]
* Octave 2 (entries 96-143): mantissa in [0x20000, 0x40000) output ~[0.500, 0.354]
* Octave 3 (entries144-191): mantissa in [0x40000, 0x80000) output ~[0.354, 0.251]
* Within each octave, 8 sub-entries interpolate over the 8 fractional bits of the mantissa
* (the byte `(0x80 | (i mod 8))` for the lower-byte of the aligned value).
* Sampling the first value of each octave:
* [0] 0x1000 = 1.0000 ; 1 / sqrt(1.0000)
* [48] 0x0e4f = 0.8940 ; 1 / sqrt(1.2500)
* [96] 0x0d10 = 0.8164 ; 1 / sqrt(1.5000)
* [144] 0x0c0a = 0.7520 ; 1 / sqrt(1.7500)
* And representative sub-entries within octave 0 (mantissa in [0x8000, 0x8100)):
* [0] 0x1000 = 1.0000 ; 1 / sqrt(0x8000)
* [1] 0x0fe0 = 0.9922 ; 1 / sqrt(0x8100)
* [2] 0x0fc1 = 0.9846 ; 1 / sqrt(0x8200)
* [3] 0x0fa3 = 0.9773 ; 1 / sqrt(0x8300)
* [4] 0x0f85 = 0.9700 ; 1 / sqrt(0x8400)
* [5] 0x0f68 = 0.9629 ; 1 / sqrt(0x8500)
* [6] 0x0f4c = 0.9561 ; 1 / sqrt(0x8600)
* [7] 0x0f30 = 0.9492 ; 1 / sqrt(0x8700)
*
* The algorithm's `addi -64 / sll 1 / lh` selects the entry at `(aligned - 64) * 2` for the case where `aligned` has its top bit at bit 24.
* After the sllv/srav pair, `aligned` always lands in `[0x80, 0x100)`
* (with top bit at bit 24 → after `sub $aligned - 64`, the index sits in `[0x40, 0x80) * 2 = [0x80, 0x100)` bytes = entries [64, 128) within the sqrtbl).
* The earlier 64 entries (octave 0) are reached when the magnitude after shifting puts the top bit below bit 24 (the `sllv` branch),
* and the load upper_halves of the table bracket the input range.
* The later 64 entries (octaves 2-3) are the `srav` branch when the magnitude's top bit is well above bit 24.
*
* Reproduced verbatim from libgte (verified against libpsn00b/psxgte/vector.s:100-123 — 24 rows × 8 halfwords, last entry 0x0804).
* */
internal S2 const gte_normalize_sqr_tbl[192] align_(2) = {
0x1000, 0x0fe0, 0x0fc1, 0x0fa3, 0x0f85, 0x0f68, 0x0f4c, 0x0f30,
0x0f15, 0x0efb, 0x0ee1, 0x0ec7, 0x0eae, 0x0e96, 0x0e7e, 0x0e66,
0x0e4f, 0x0e38, 0x0e22, 0x0e0c, 0x0df7, 0x0de2, 0x0dcd, 0x0db9,
0x0da5, 0x0d91, 0x0d7e, 0x0d6b, 0x0d58, 0x0d45, 0x0d33, 0x0d21,
0x0d10, 0x0cff, 0x0cee, 0x0cdd, 0x0ccc, 0x0cbc, 0x0cac, 0x0c9c,
0x0c8d, 0x0c7d, 0x0c6e, 0x0c5f, 0x0c51, 0x0c42, 0x0c34, 0x0c26,
0x0c18, 0x0c0a, 0x0bfd, 0x0bef, 0x0be2, 0x0bd5, 0x0bc8, 0x0bbb,
0x0baf, 0x0ba2, 0x0b96, 0x0b8a, 0x0b7e, 0x0b72, 0x0b67, 0x0b5b,
0x0b50, 0x0b45, 0x0b39, 0x0b2e, 0x0b24, 0x0b19, 0x0b0e, 0x0b04,
0x0af9, 0x0aef, 0x0ae5, 0x0adb, 0x0ad1, 0x0ac7, 0x0abd, 0x0ab4,
0x0aaa, 0x0aa1, 0x0a97, 0x0a8e, 0x0a85, 0x0a7c, 0x0a73, 0x0a6a,
0x0a61, 0x0a59, 0x0a50, 0x0a47, 0x0a3f, 0x0a37, 0x0a2e, 0x0a26,
0x0a1e, 0x0a16, 0x0a0e, 0x0a06, 0x09fe, 0x09f6, 0x09ef, 0x09e7,
0x09e0, 0x09d8, 0x09d1, 0x09c9, 0x09c2, 0x09bb, 0x09b4, 0x09ad,
0x09a5, 0x099e, 0x0998, 0x0991, 0x098a, 0x0983, 0x097c, 0x0976,
0x096f, 0x0969, 0x0962, 0x095c, 0x0955, 0x094f, 0x0949, 0x0943,
0x093c, 0x0936, 0x0930, 0x092a, 0x0924, 0x091e, 0x0918, 0x0912,
0x090d, 0x0907, 0x0901, 0x08fb, 0x08f6, 0x08f0, 0x08eb, 0x08e5,
0x08e0, 0x08da, 0x08d5, 0x08cf, 0x08ca, 0x08c5, 0x08bf, 0x08ba,
0x08b5, 0x08b0, 0x08ab, 0x08a6, 0x08a1, 0x089c, 0x0897, 0x0892,
0x088d, 0x0888, 0x0883, 0x087e, 0x087a, 0x0875, 0x0870, 0x086b,
0x0867, 0x0862, 0x085e, 0x0859, 0x0855, 0x0850, 0x084c, 0x0847,
0x0843, 0x083e, 0x083a, 0x0836, 0x0831, 0x082d, 0x0829, 0x0824,
0x0820, 0x081c, 0x0818, 0x0814, 0x0810, 0x080c, 0x0808, 0x0804,
}; };
internal MipsAtom_(set_gte_world) atom_info(
typedef Struct_(Binds_normalize_v3s4) { atom_bind(Binds_SetGteWorld)
U2 src_offset; /* offset of src V3_S4 within the BIOS scratchpad */
U2 dst_offset; /* offset of dst V3_S4 within the BIOS scratchpad */
};
typedef Struct_(RegUse_normalize_v3s4) {
union { Reg_(V3_S4) res, src; };
union { Reg r0, src_ptr, mac2; };
union { Reg r1, dst_ptr; };
union { Reg r2, dst_offset, mac1, v_sqr_aligned; };
union { Reg r3, src_offset, btarget, shift_count, sqrtbl_index; };
union { Reg r4, mac3, v_sqr_sum, scale_exp, srav_shift; };
union { Reg r5, lzcr, inv_len; };
};
/* ─── Full normalize (all 4 stages inline) ───
* Generic 4-stage GTE normalize (SQR → sum+LZCR → align+sqrtbl → GPF+srav). */
internal MipsAtom* normalize_v3s4(AtomArena_R aa, RegUse_normalize_v3s4 r)
MipsAtom_Proc_(aa, {
load_half(r.src_offset, R_TapePtr, O_(Binds_normalize_v3s4, src_offset)),
load_half(r.dst_offset, R_TapePtr, O_(Binds_normalize_v3s4, dst_offset)),
LdSlot_ add_u(r.src_ptr, R_ScratchBase, r.src_offset),
LdSlot_ add_u(r.dst_ptr, R_ScratchBase, r.dst_offset),
LdSlot_ add_ui_self(R_TapePtr, S_(Binds_normalize_v3s4)),
mac_load_v3s4(r.src, r.src_ptr, 0),
/* Stage 1: mtc2 src → IR1/2/3, SQR fires. */
LdSlot_ mac_gte_sqr_v3s4(r.src.x, r.src.y, r.src.z, LdSlot_ nop),
/* Stage 2: mfc2 MAC1/2/3, sum, mtc2 LZCS. src_ptr is dead; reuse as mac2. */
mac_gte_mv_from_data_r_mac123(r.mac1, r.mac2, r.mac3), LdSlot_ nop,
add_u_self( r.v_sqr_sum, r.mac1),
add_u_self( r.v_sqr_sum, r.mac2),
gte_mv_to_data_r( r.v_sqr_sum, C2_LZCS), GteDelay_ nop2,
gte_mv_from_data_r(r.lzcr, C2_LZCR), GteDelay_ nop,
/* Stage 3: even(LZCR), half-shift, align |v|² to bit 24. */
mac_lzcr_round_even_half_shift(r.lzcr, r.v_sqr_sum, r.v_sqr_aligned),
add_si( r.btarget, r.lzcr, -24),
branch_lt_zero(r.btarget, atom_offset(aligned_done, srav_path)), BdSlot_ nop, /* bltz → srav_path (LZCR < 24 path) */
jump_rel(atom_offset(srav_path, aligned_done)), /* b → aligned_done (LZCR >= 24 path) */
BdSlot_ shift_lleft_var(r.v_sqr_aligned, r.v_sqr_aligned, r.btarget),
atom_label(srav_path)
li_s( r.shift_count, 24),
sub_s(r.shift_count, r.shift_count, r.lzcr),
shift_aright_var(r.v_sqr_aligned, r.v_sqr_aligned, r.shift_count),
atom_label(aligned_done)
add_si( r.v_sqr_aligned, r.v_sqr_aligned, -64),
shift_lleft(r.v_sqr_aligned, r.v_sqr_aligned, 1),
mac_load_word_imm(r.sqrtbl_index, & gte_normalize_sqr_tbl), add_u_self(r.sqrtbl_index, r.v_sqr_aligned),
load_half(r.inv_len, r.sqrtbl_index, 0),
LdSlot_ nop,
mac_gte_general_purpose_interopolation(r.inv_len,
r.src.x, r.src.y, r.src.z,
r.res.x, r.res.y, r.res.z,
GteDelay_ load_word(R_AtomJmp, R_TapePtr, 0), LdSlot_ // ac_yield: word 1
GteDelay_ add_ui_self( R_TapePtr, S_(MipsCode)) // ac_yield: word 2
),
mac_shift_aright_var_v3s4_self(r.res, r.srav_shift),
mac_store_v3s4(r.res, r.dst_ptr, 0),
jump_reg(R_AtomJmp), BdSlot_ nop // ac_yield: word 3-4
})
/* ─── GTE OP cross product (a × b → out) ───
* Generalized V3_S4 cross product via GTE OP (OuterProduct12 libpsyx convention).
* The >> 12 shift converts S12.20 → S12.0 OuterProduct12. */
typedef Struct_(Binds_gte_cross_v3s4) { V3_S4* src_a; V3_S4* src_b; V3_S4* out; };
typedef Struct_(RegUse_gte_cross_v3s4) {
Reg_(V3_S4) a;
Reg_(V3_S4) b;
union { Reg out, t0; } x;
union { Reg src_a, t1, rt11; } y;
union { Reg src_b, t2, rt22; } z;
};
internal MipsAtom* gte_cross_v3s4(AtomArena_R aa, RegUse_gte_cross_v3s4 r)
atom_info(atom_bind(Binds_gte_cross_v3s4)) MipsAtom_Proc_(aa, {
load_word(r.y.src_a, R_TapePtr, O_(Binds_gte_cross_v3s4,src_a)),
load_word(r.z.src_b, R_TapePtr, O_(Binds_gte_cross_v3s4,src_b)),
load_word(r.x.out, R_TapePtr, O_(Binds_gte_cross_v3s4,out)),
LdSlot_ add_ui_self( R_TapePtr, S_(Binds_gte_cross_v3s4)),
mac_load_v3s4(r.a, r.y.src_a, 0), LdSlot_
mac_load_v3s4(r.b, r.z.src_b, 0), LdSlot_
mac_gte_op_cross_v3s4(r.a, r.b), /* RT diagonal + IR + OP + MAC read + shift */
mac_store_v3s4(r.a, r.x.out, 0),
mac_yield()
})
#pragma endregion Atom Procs
#pragma region Baked Atoms
typedef Struct_(Binds_SetGteMT3S2S4) {
MT3_S2S4* transform;
};
internal MipsAtom_(set_gte_mt3s2s4) atom_info(
atom_bind(Binds_SetGteMT3S2S4)
, atom_reads(R_TapePtr) , atom_reads(R_TapePtr)
){ ){
/* Pop matrix address from tape into R_T3 ($11) */ /* Pop matrix address from tape into R_T3 ($11) */
load_word(R_T3, R_TapePtr, O_(Binds_SetGteMT3S2S4,transform)), load_word(R_T3, R_TapePtr, O_(Binds_SetGteWorld,transform)),
add_ui_self( R_TapePtr, S_(Binds_SetGteMT3S2S4)), add_ui_self( R_TapePtr, S_(Binds_SetGteWorld)),
/* Load 3x3 Rotation + 3x1 Translation from R_T3 into GTE CONTROL Regs (ctc2) */ /* Load 3x3 Rotation + 3x1 Translation from R_T3 into GTE CONTROL Regs (ctc2) */
load_word(R_T0, R_T3, 0), load_word(R_T0, R_T3, 0), load_word(R_T1, R_T3, 4),
load_word(R_T1, R_T3, 4), gte_mv_to_ctrl_r(R_T0, gte_cr_RT11), gte_mv_to_ctrl_r(R_T1, gte_cr_RT12),
gte_mv_to_ctrl_r(R_T0, gte_cr_RT11), load_word(R_T0, R_T3, 8), load_word(R_T1, R_T3, 12), load_word(R_T2, R_T3, 16),
gte_mv_to_ctrl_r(R_T1, gte_cr_RT12), gte_mv_to_ctrl_r(R_T0, gte_cr_RT13), gte_mv_to_ctrl_r(R_T1, gte_cr_RT21), gte_mv_to_ctrl_r(R_T2, gte_cr_RT22),
load_word(R_T0, R_T3, 8), load_word(R_T0, R_T3, 20), load_word(R_T1, R_T3, 24), load_word(R_T2, R_T3, 28),
load_word(R_T1, R_T3, 12), gte_mv_to_ctrl_r(R_T0, gte_cr_TRX), gte_mv_to_ctrl_r(R_T1, gte_cr_TRY), gte_mv_to_ctrl_r(R_T2, gte_cr_TRZ),
load_word(R_T2, R_T3, 16),
gte_mv_to_ctrl_r(R_T0, gte_cr_RT13),
gte_mv_to_ctrl_r(R_T1, gte_cr_RT21),
gte_mv_to_ctrl_r(R_T2, gte_cr_RT22),
load_word(R_T0, R_T3, 20),
load_word(R_T1, R_T3, 24),
load_word(R_T2, R_T3, 28),
gte_mv_to_ctrl_r(R_T0, gte_cr_TRX),
gte_mv_to_ctrl_r(R_T1, gte_cr_TRY),
gte_mv_to_ctrl_r(R_T2, gte_cr_TRZ),
mac_yield() mac_yield()
}; };
+54 -152
View File
@@ -16,6 +16,9 @@
* gte_mv_to_data_r (gte + mv + to + data + register) * gte_mv_to_data_r (gte + mv + to + data + register)
* gte_lw_v0_xy(base) (gte + lw + v0 + xy) * gte_lw_v0_xy(base) (gte + lw + v0 + xy)
* load_upper_i (load-upper + immediate, unique verb) * load_upper_i (load-upper + immediate, unique verb)
*
* Vendor mnemonics (gte_mtc2, gte_mfc2, gte_lwc2, gte_swc2, etc.) are NOT in this header.
* They are in the opt-in `gte_vendor_sym.h` for users who prefer the textbook MIPS assembly mnemonics.
* ============================================================================ */ * ============================================================================ */
#ifdef INTELLISENSE_DIRECTIVES #ifdef INTELLISENSE_DIRECTIVES
@@ -30,7 +33,7 @@
* gte.h — Geometry Transformation Engine (COP2) for the PS1 * gte.h — Geometry Transformation Engine (COP2) for the PS1
* ============================================================================ * ============================================================================
* *
* Hand-rolled DSL for emitting GTE/MIPS instruction words from C. * Hand-rolled DSL for emitting GTE/MIPS instruction words as raw `.word` constants from C.
* No GCC inline-assembly string syntax in the code body. * No GCC inline-assembly string syntax in the code body.
* *
* STYLE NOTES * STYLE NOTES
@@ -98,20 +101,20 @@ enum {
/* Semantic Aliases for GTE Data Registers */ /* Semantic Aliases for GTE Data Registers */
enum { enum {
C2_InV0_XY = C2_VXY0, /* Input Vector 0 (X, Y) */ gte_in_v0_xy = C2_VXY0, /* Input Vector 0 (X, Y) */
C2_InV0_Z = C2_VZ0, /* Input Vector 0 (Z) */ gte_in_v0_z = C2_VZ0, /* Input Vector 0 (Z) */
C2_InV1_XY = C2_VXY1, /* Input Vector 1 (X, Y) */ gte_in_v1_xy = C2_VXY1, /* Input Vector 1 (X, Y) */
C2_InV1_Z = C2_VZ1, /* Input Vector 1 (Z) */ gte_in_v1_z = C2_VZ1, /* Input Vector 1 (Z) */
C2_InV2_XY = C2_VXY2, /* Input Vector 2 (X, Y) */ gte_in_v2_xy = C2_VXY2, /* Input Vector 2 (X, Y) */
C2_InV2_Z = C2_VZ2, /* Input Vector 2 (Z) */ gte_in_v2_z = C2_VZ2, /* Input Vector 2 (Z) */
C2_In_RGB = C2_RGB, /* Input Color (R, G, B, MipsCode) */ gte_in_rgb = C2_RGB, /* Input Color (R, G, B, MipsCode) */
C2_OutSrc_XY0 = C2_SXY0, /* Output Screen Coord 0 (X, Y) */ gte_out_scr_xy0 = C2_SXY0, /* Output Screen Coord 0 (X, Y) */
C2_OutSrc_XY1 = C2_SXY1, /* Output Screen Coord 1 (X, Y) */ gte_out_scr_xy1 = C2_SXY1, /* Output Screen Coord 1 (X, Y) */
C2_OutSrc_XY2 = C2_SXY2, /* Output Screen Coord 2 (X, Y) */ gte_out_scr_xy2 = C2_SXY2, /* Output Screen Coord 2 (X, Y) */
C2_OutDepth = C2_OTZ, /* Output Ordering Table Z (Depth) */ gte_out_depth = C2_OTZ, /* Output Ordering Table Z (Depth) */
C2_MathAccu0 = C2_MAC0, /* Math Accumulator 0 */ gte_math_accum0 = C2_MAC0, /* Math Accumulator 0 */
C2_MathAccu1 = C2_MAC1, /* Math Accumulator 1 */ gte_math_accum1 = C2_MAC1, /* Math Accumulator 1 */
C2_MathAccu2 = C2_MAC2, /* Math Accumulator 2 */ gte_math_accum2 = C2_MAC2, /* Math Accumulator 2 */
}; };
/* --- GTE Command Semantics (The Bitfield Meanings) --- /* --- GTE Command Semantics (The Bitfield Meanings) ---
@@ -158,8 +161,6 @@ enum {
gte_cmd_nclip = 0x06, /* Normal Clipping (Backface culling) */ gte_cmd_nclip = 0x06, /* Normal Clipping (Backface culling) */
gte_cmd_op = 0x0C, /* Outer Product */ gte_cmd_op = 0x0C, /* Outer Product */
gte_cmd_mvmva = 0x12, /* Matrix Vector Multiply & Add (Custom math) */ gte_cmd_mvmva = 0x12, /* Matrix Vector Multiply & Add (Custom math) */
gte_cmd_sqr = 0x28, /* Square vector — MAC[i] = IR[i]²; IR[i] ← MAC[i] saturated */
gte_cmd_gpf = 0x3D, /* General-purpose Interpolation — MAC[i] = IR0 * IR[i] */
/* --- GTE Command Bit-Field Layout --- /* --- GTE Command Bit-Field Layout ---
* A GTE command word (sent to COP2 with RS=1) is laid out as: * A GTE command word (sent to COP2 with RS=1) is laid out as:
@@ -170,42 +171,19 @@ enum {
* +------------+--+-----+------+------+------+------+---+--------+----------+ * +------------+--+-----+------+------+------+------+---+--------+----------+
* \_____ GTE_PAYLOAD _____/ \__ GTE_CMD __/ * \_____ GTE_PAYLOAD _____/ \__ GTE_CMD __/
* *
* Offset position & masks below are the *bit positions* and *bit widths* of each configurable field, used by the ENC_GTE_CMD encoder. * Shifts/masks below are the *bit positions* and *bit widths* of each
* Mirrors the OPCODE_POS / RS_POS convention used in mips.h. * configurable field, used by the ENC_GTE_CMD encoder.
* Mirrors the OPCODE_SHIFT / RS_SHIFT convention used in mips.h.
*/ */
gte_pos_sf = 19, gte_width_sf = 1, gte_shift_sf = 19, gte_width_sf = 1, gte_mask_sf = 0x1,
gte_pos_mx = 17, gte_width_mx = 2, gte_shift_mx = 17, gte_width_mx = 2, gte_mask_mx = 0x3,
gte_pos_v = 15, gte_width_v = 2, gte_shift_v = 15, gte_width_v = 2, gte_mask_v = 0x3,
gte_pos_cv = 13, gte_width_cv = 2, gte_shift_cv = 13, gte_width_cv = 2, gte_mask_cv = 0x3,
gte_pos_lm = 10, gte_width_lm = 1, gte_shift_lm = 10, gte_width_lm = 1, gte_mask_lm = 0x1,
gte_pos_cmd = 0, gte_width_cmd = 6, gte_shift_cmd = 0, gte_width_cmd = 6, gte_mask_cmd = 0x3F,
/* Fake command number (bits 24-20) — IGNORED by the GTE hardware per PSX-SPX `geometrytransformationenginegte.md` line 48.
* libgte's compiler emits non-zero values in this field as a disassembly signature. */
gte_pos_fake_cmd = 20,
gte_width_fake_cmd = 5,
}; };
/* --- GTE Control Register Aliases (Pitfall 1) ---
* Three pairs of aliases map to the C2 control-register slot:
* C2[24] = gte_cr_RBK (background R) | gte_cr_OFX (screen offset X)
* C2[25] = gte_cr_GBK (background G) | gte_cr_OFY (screen offset Y)
* C2[26] = gte_cr_BBK (background B) | gte_cr_H (projection plane distance H)
* Cross-alias writes inside one atom body, or across the wave-context boundary, silently clobber each other.
* The metaprogram's check_gte_cr_alias_writes (CHECK_RULES row) warns about each pair per source.
* See psx-spx docs/gte_reference.md §"Control-register alias table" for the silicon rationale and the libgte outer-product convention.
*/
/* --- RT-matrix packed-slot convention (Pitfall 4) ---
* The silicon packs two 16-bit RT elements per 32-bit C2 slot:
* C2[2] = (RT22 << 16) | RT13 (gte_cr_RT13 writes the low half, gte_cr_RT22 writes the high half)
* C2[4] = (RT33 << 16) | RT22 (gte_cr_RT22 writes the low half — clobbers prior RT22 value if RT13 was also written)
* OP and MVMVA read D1/D2/D3 from these packed slots.
* The libgte outer-product convention (see ac_apply_matrix_lv at gte.atom.c:108-122) writes C2[2] then C2[4] in sequence;
* the SECOND write's low half is RT22, not RT13.
*/
/* --- GTE Control Register Indices (for ctc2/cfc2) --- /* --- GTE Control Register Indices (for ctc2/cfc2) ---
* Preprocessor-visible integer ids for the COP2 control register file. * Preprocessor-visible integer ids for the COP2 control register file.
* Each enum value is bound to a parallel `_Code` `#define` so the preprocessor can stringify the integer (for `reg_str`/`rgcc` paths). * Each enum value is bound to a parallel `_Code` `#define` so the preprocessor can stringify the integer (for `reg_str`/`rgcc` paths).
@@ -265,10 +243,10 @@ enum { _C2_OPS_ = 0
* bit 1 (0x02): register class — 0 = data, 1 = control * bit 1 (0x02): register class — 0 = data, 1 = control
* bit 2 (0x04): direction — 0 = read, 1 = write * bit 2 (0x04): direction — 0 = read, 1 = write
* *
* The values 0x00 (sub_mfc2) and 0x04 (sub_mtc2) are the same 5-bit numbers as general MIPS `cop_mf` / `cop_mt` defined in mips.h * The values 0x00 (sub_mfc2) and 0x04 (sub_mtc2) are the same 5-bit numbers as the general MIPS `cop_mf` / `cop_mt` defined in mips.h
* (which target the data register file on any coprocessor). * (which target the data register file on any coprocessor).
* They are re-aliased here so the four-way table reads like the spec mnemonics (MFC2 / CFC2 / MTC2 / CTC2) * They are re-aliased here so the four-way table reads like the spec mnemonics (MFC2 / CFC2 / MTC2 / CTC2)
* and so the encoding is next to its only consumer (this header). * and so the encoding lives next to its only consumer (this header).
* *
* Vendor mnemonic aliases (gte_mfc2 / gte_mtc2 / gte_cfc2 / gte_ctc2) live in gte_vendor_sym.h. */ * Vendor mnemonic aliases (gte_mfc2 / gte_mtc2 / gte_cfc2 / gte_ctc2) live in gte_vendor_sym.h. */
enum { _C2_TX_SUBS_ = 0 enum { _C2_TX_SUBS_ = 0
@@ -292,7 +270,8 @@ enum { _C2_TX_SUBS_ = 0
// #define gte_mv_from_data_r(rt, rd) enc_gte_tx(cop_mf, (rt), (rd)) /* Move GTE Control Register (rd) to GPR (rt) */ // #define gte_mv_from_data_r(rt, rd) enc_gte_tx(cop_mf, (rt), (rd)) /* Move GTE Control Register (rd) to GPR (rt) */
/* GTE Data vs Control Register Transfers /* GTE Data vs Control Register Transfers
* Each macro emits a single instruction for one of MFC2/CFC2/MTC2/CTC2. *
* Each macro emits a single .word constant for one of MFC2/CFC2/MTC2/CTC2.
* *
* `rd` is the C2 register index in the file the sub-opcode names: * `rd` is the C2 register index in the file the sub-opcode names:
* gte_mv_from_data_r / gte_mv_to_data_r → C2 data register file * gte_mv_from_data_r / gte_mv_to_data_r → C2 data register file
@@ -307,14 +286,14 @@ enum { _C2_TX_SUBS_ = 0
#define gte_mv_from_ctrl_r(rt, rd) enc_gte_tx(sub_cfc2, (rt), (rd)) /* Copy From ctrl reg */ #define gte_mv_from_ctrl_r(rt, rd) enc_gte_tx(sub_cfc2, (rt), (rd)) /* Copy From ctrl reg */
#define gte_mv_to_data_r(rt, rd) enc_gte_tx(sub_mtc2, (rt), (rd)) /* Move To data reg */ #define gte_mv_to_data_r(rt, rd) enc_gte_tx(sub_mtc2, (rt), (rd)) /* Move To data reg */
#define gte_mv_to_ctrl_r(rt, rd) enc_gte_tx(sub_ctc2, (rt), (rd)) /* Copy To ctrl reg */ #define gte_mv_to_ctrl_r(rt, rd) enc_gte_tx(sub_ctc2, (rt), (rd)) /* Copy To ctrl reg */
#define GteDelay_ // Annotate an instruction as filling a CPU <-> GTE DMA delay slot/s
/* COP2 Data Load (lwc2): `lwc2 rt, off(rs)` /* COP2 Data Load (lwc2): `lwc2 rt, off(rs)`
* Layout: [op_lwc2:6][rs:5][rt:5][imm:16] * Layout: [op_lwc2:6][rs:5][rt:5][imm:16]
* - rs: GPR base address * - rs: GPR base address
* - rt: COP2 data register index (0..31) * - rt: COP2 data register index (0..31)
* - imm: signed 16-bit offset * - imm: signed 16-bit offset
* NOTE: When `rs` is a runtime register, the encoding cannot be pre-baked into a .word — use the string-style `gte_load_v0` macro below instead. */ * NOTE: When `rs` is a runtime register, the encoding cannot be pre-baked
* into a .word — use the string-style `gte_load_v0` macro below instead. */
#define enc_gte_lw(rt, base, off) enc_i(op_lwc2, (base), (rt), (off)) #define enc_gte_lw(rt, base, off) enc_i(op_lwc2, (base), (rt), (off))
/* Store Word */ /* Store Word */
#define enc_gte_sw(rt, base, off) enc_i(op_swc2, (base), (rt), (off)) #define enc_gte_sw(rt, base, off) enc_i(op_swc2, (base), (rt), (off))
@@ -323,30 +302,30 @@ enum { _C2_TX_SUBS_ = 0
* `swc2` is redundant when we're already inside the `gte_` namespace. * `swc2` is redundant when we're already inside the `gte_` namespace.
* gte_lw rt, base, off → lwc2 rt, off(base) * gte_lw rt, base, off → lwc2 rt, off(base)
* gte_sw rt, base, off → swc2 rt, off(base) * gte_sw rt, base, off → swc2 rt, off(base)
* For the typical user-facing vector-level load (xy + z as two instructions), use the higher-level `gte_load_vN` macros below. */ * For the typical user-facing vector-level load (xy + z as two instructions),
* use the higher-level `gte_load_vN` macros below. */
#define gte_lw(rt, base, off) enc_gte_lw(rt, base, off) #define gte_lw(rt, base, off) enc_gte_lw(rt, base, off)
#define gte_sw(rt, base, off) enc_gte_sw(rt, base, off) #define gte_sw(rt, base, off) enc_gte_sw(rt, base, off)
/* GTE Command Format /* GTE Command Format
* Opcode is always MIPS_OP_COP2, RS is always 1 (CO). * Opcode is always MIPS_OP_COP2, RS is always 1 (CO).
* Lower 25 bits are GTE-specific command payload. * The lower 25 bits are the GTE-specific command payload.
* *
* The `enc_gte_<field>(x)` macros below mirror the `enc_op`/`enc_rs` pattern in mips.h: * The granular `enc_gte_<field>(x)` macros below mirror the `enc_op`/`enc_rs` pattern in mips.h:
* Each one self-masks and shifts its own field, so a caller can build up a GTE command piece by piece * Each one self-masks and shifts its own field, so a caller can build up a GTE command piece by piece
* (handy for state-driven MVMVA emitters that vary one field at a time). * (handy for state-driven MVMVA emitters that vary one field at a time).
* *
* `ENC_GTE_CMD` is an all-in-one convenience for emitting a full command word. * `ENC_GTE_CMD` is the all-in-one convenience for emitting a full command word in one go.
* It just ORs the per-field encoders together. */ * It just ORs the per-field encoders together. */
#define gte_cmd_base (enc_op(op_cop2) | (1 << 25)) #define gte_cmd_base (enc_op(op_cop2) | (1 << 25))
/* Per-field encoders. Each one does (value & mask) << shift on its own. */ /* Per-field encoders. Each one does (value & mask) << shift on its own. */
#define enc_gte_sf(sf) ((sf) << gte_pos_sf ) #define enc_gte_sf(sf) (((sf) & gte_mask_sf ) << gte_shift_sf )
#define enc_gte_mx(mx) ((mx) << gte_pos_mx ) #define enc_gte_mx(mx) (((mx) & gte_mask_mx ) << gte_shift_mx )
#define enc_gte_v(v) ((v) << gte_pos_v ) #define enc_gte_v(v) (((v) & gte_mask_v ) << gte_shift_v )
#define enc_gte_cv(cv) ((cv) << gte_pos_cv ) #define enc_gte_cv(cv) (((cv) & gte_mask_cv ) << gte_shift_cv )
#define enc_gte_lm(lm) ((lm) << gte_pos_lm ) #define enc_gte_lm(lm) (((lm) & gte_mask_lm ) << gte_shift_lm )
#define enc_gte_cmd(cmd) ((cmd) << gte_pos_cmd ) #define enc_gte_cmd(cmd) (((cmd) & gte_mask_cmd) << gte_shift_cmd)
#define enc_gte_fake_cmd(x) ((x) << gte_pos_fake_cmd)
/* Composite: all six GTE fields + the COP2/CO base. */ /* Composite: all six GTE fields + the COP2/CO base. */
#define enc_gte_cmdw(sf, mx, v, cv, lm, cmd) ( \ #define enc_gte_cmdw(sf, mx, v, cv, lm, cmd) ( \
@@ -384,11 +363,11 @@ enum { _C2_TX_SUBS_ = 0
* (the perspective divide happens regardless of `sf`). * (the perspective divide happens regardless of `sf`).
* *
* If we emit a strictly-spec-compliant word (`sf=0`, reserved bits clear), * If we emit a strictly-spec-compliant word (`sf=0`, reserved bits clear),
* PCSX-Redux's GTE checks those bits more strictly than the silicon does and RTPT silently no-ops. * PCSX-Redux's GTE checks those bits more strictly than the silicon does and RTPT silently no-ops
* The floor's screen coordinates come out as raw projection-of-rotation (Z never divided), * the floor's screen coordinates come out as raw projection-of-rotation (Z never divided),
* `nclip` ends up wrong, and the triangle is culled. * `nclip` ends up wrong, and the triangle is culled.
* *
* So for RTPS and RTPT we OR-in the `0x28` "PsyQ compat" pattern to match the working bit pattern. * So for RTPS and RTPT we OR-in the `0x28` "PsyQ compat" pattern to match the working bit pattern everyone has shipped for 25 years.
* NCLIP / OP / MVMVA stay spec-clean — their reserved bits really are zero in the original PsyQ source. * NCLIP / OP / MVMVA stay spec-clean — their reserved bits really are zero in the original PsyQ source.
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
*/ */
@@ -398,91 +377,12 @@ enum { _C2_TX_SUBS_ = 0
#define gte_cmdw_rtpt (gte_cmd_base | enc_gte_cmd(gte_cmd_rtpt ) | gte_cmdw_psyq_compat) #define gte_cmdw_rtpt (gte_cmd_base | enc_gte_cmd(gte_cmd_rtpt ) | gte_cmdw_psyq_compat)
#define gte_cmdw_nclip (gte_cmd_base | enc_gte_cmd(gte_cmd_nclip)) #define gte_cmdw_nclip (gte_cmd_base | enc_gte_cmd(gte_cmd_nclip))
#define gte_cmdw_op (gte_cmd_base | enc_gte_cmd(gte_cmd_op )) #define gte_cmdw_op (gte_cmd_base | enc_gte_cmd(gte_cmd_op ))
#define gte_cmdw_outer_product gte_cmdw_op /* "outer product" -- PSY-Q terminology */ #define gte_cmdw_outer_product gte_cmdw_op /* "outer product" -- NOCASH/Sdk terminology */
#define gte_cmdw_wedge gte_cmdw_op /* "wedge product" -- geometric-algebra terminology. */ #define gte_cmdw_wedge gte_cmdw_op /* "wedge product" -- geometric-algebra terminology */
#define gte_cmdw_cross gte_cmdw_op /* "cross product" -- geometric-algebra terminology.
* RGA(Lengyel): The GTE OP is a 3D signed-16-bit D x IR cross, not a generic RGA exterior product.
* The wedge alias is a 3D complement interpretation of the same 3 scalars (MAC1..MAC3). */
#define gte_cmdw_mvmva (gte_cmd_base | enc_gte_cmd(gte_cmd_mvmva)) #define gte_cmdw_mvmva (gte_cmd_base | enc_gte_cmd(gte_cmd_mvmva))
/* MVMVA with sf=0 (no shift, full-integer), cv=3 (no translation), v=3 (IR vector input).
* Reads input from IR1/2/3 (loaded via mtc2 rt, C2_IRx). MAC1/2/3 = RT row · IR (full product, no >>12).
* Per PSX-SPX: SAR (sf*12) with sf=0 = SAR 0 = no shift. */
#define gte_cmdw_mvmva_sf0_ir (gte_cmd_base | enc_gte_cv(3) | enc_gte_v(3) | enc_gte_cmd(gte_cmd_mvmva))
/* MVMVA with sf=1 (>>12 shift, 4.12 fixed-point), cv=3 (no translation), v=3 (IR): for ApplyMatrixLV.
* Reads input from IR1/2/3 (loaded via mtc2 rt, C2_IRx). MAC1/2/3 = (RT row · IR) >> 12.
* Per PSX-SPX: SAR (sf*12) with sf=1 = SAR 12 = arithmetic right-shift by 12.
* This matches the libgte C-side ApplyMatrixLV output (R*pos >> 12). */
#define gte_cmdw_mvmva_ir (gte_cmd_base | enc_gte_sf(1) | enc_gte_cv(3) | enc_gte_v(3) | enc_gte_cmd(gte_cmd_mvmva))
/* MVMVA: sf=0, mx=3 (Light matrix), v=3 (IR), cv=3 (no TR).
* For pass1 of the C11 two-pass decomposition. Reads L matrix.
* Since L matrix is typically zero, pass1 contributes 0 to the combine. */
#define gte_cmdw_mvmva_sf0_mx3_v3_cv3 (gte_cmd_base | enc_gte_sf(0) | enc_gte_cv(3) | enc_gte_v(3) | enc_gte_mx(3) | enc_gte_cmd(gte_cmd_mvmva))
/* MVMVA: sf=1 (>>12), mx=3 (Light matrix), v=2 (V0), cv=0 (with TR).
* Matches the C11 ApplyMatrixLV pass 2 command word (0x49E012) exactly.
* The combine is (pass1 << 3) + pass2. */
#define gte_cmdw_mvmva_pass2_c11 (gte_cmd_base | enc_gte_sf(1) | enc_gte_v(2) | enc_gte_mx(3) | enc_gte_cmd(gte_cmd_mvmva))
/* MVMVA: sf=0, mx=3, v=2, cv=0. Matches the C11 pass 1 command. */
#define gte_cmdw_mvmva_pass1_c11 (gte_cmd_base | enc_gte_v(2) | enc_gte_mx(3) | enc_gte_cmd(gte_cmd_mvmva))
#define gte_cmdw_mvmva_no_tr gte_cmdw_mvmva_ir
/* MVMVA pass 2 — C11 ApplyMatrixLV command.
* Decoded: op_cop2 | CO | fake_cmd=4 | sf=1 (>>12) | mx=0 (RT matrix) | v=3 (IR) | cv=3 (no translation) | lm=0 | cmd=MVMVA.
* Reads (RT row · IR) >> 12 into MAC1/2/3. Per-field composition (no opaque literal)
* keeps the bit layout visible at the call site + matches the libgte C-side byte-exact. */
#define gte_cmdw_mvmva_c11_pass2 (gte_cmd_base | enc_gte_fake_cmd(4) | enc_gte_sf(1) | enc_gte_v(3) | enc_gte_mx(0) | enc_gte_cv(3) | enc_gte_cmd(gte_cmd_mvmva))
/* MVMVA: sf=1 (>>12), mx=0 (RT matrix), v=0 (V0), cv=3 (no TR). */
#define gte_cmdw_mvmva_sf1_mx0_v0_cv3 (gte_cmd_base | enc_gte_sf(1) | enc_gte_cv(3) | enc_gte_v(0) | enc_gte_mx(0) | enc_gte_cmd(gte_cmd_mvmva))
/* RTPS with sf=1 (12-bit shift, no translation): matches the output of libgte's ApplyMatrixLV when the GTE pipeline expects R*pos >> 12.
* The shift produces values like (-270, 710, 1713) which match the C11 reference path. */
#define gte_cmdw_rtps_sf1 (gte_cmd_base | enc_gte_sf(1) | enc_gte_cv(3) | enc_gte_cmd(gte_cmd_rtps))
/* SQR / GPF cosmetic-bits compat helpers.
* Each command's `_compat` macro ORs in the `fake_cmd` field value libgte happens to emit.
* The hardware ignores these bits (per PSX-SPX line 48). */
#define gte_cmdw_sqr_fake_sig enc_gte_fake_cmd(0x0A)
#define gte_cmdw_gpf_fake_sig enc_gte_fake_cmd(0x19)
/* SQR — Square Vector.
* PSX-SPX `geometrytransformationenginegte.md` §"SQR":
* [MAC1,MAC2,MAC3] = [IR1*IR1, IR2*IR2, IR3*IR3] SHR (sf*12)
* [IR1,IR2,IR3] = [MAC1,MAC2,MAC3] (saturated to 0x7FFF when lm=1)
* Sourced verbatim from libgte msc02 VectorNormal disassembly at 0x800160b0:
* 0x4AA00428 = gte_cmd_base | gte_cmdw_sqr_compat | enc_gte_lm(1) | enc_gte_cmd(0x28)
* bit 19 sf=0
* bit 10 lm=1
* bits 5-0 cmd=0x28=SQR
* bits 24-20 = 0x0A (libgte "nonsense SDK command number" signature) */
#define gte_cmdw_sqr (gte_cmd_base | enc_gte_cmd(gte_cmd_sqr) | enc_gte_lm(1) | gte_cmdw_sqr_fake_sig)
/* GPF — General-purpose Interpolation.
* PSX-SPX `geometrytransformationenginegte.md` §"GPF":
* [MAC1,MAC2,MAC3] = (([IR1,IR2,IR3] * IR0) + [MAC1,MAC2,MAC3]) SAR (sf * 12)
* [IR1,IR2,IR3] = [MAC1,MAC2,MAC3]
* Sourced verbatim from libgte msc02 VectorNormal disassembly at 0x8001613c:
* 0x4B90003D = gte_cmd_base | gte_cmdw_gpf_compat | enc_gte_cmd(0x3D)
* bit 19 sf = 0
* bit 10 lm = 0
* bits 5-0 cmd = 0x3D = GPF
* bits 24-20 = 0x19 (libgte "nonsense SDK command number" signature) */
#define gte_cmdw_gpf (gte_cmd_base | enc_gte_cmd(gte_cmd_gpf) | gte_cmdw_gpf_fake_sig)
/* Mask to round LZCR (leading-zero/ones count, range 1..32 per PSX-SPX cop2r31) down to even.
* The normalize_v3s4 half-shift logic computes (31 - LZCR) >> 1; clearing bit 0 ensures the subtraction result is always odd, so the >> 1 division is consistent (no 0.5 loss). */
enum {
gte_lzcr_even_mask = 0xFFFE, /* all bits except bit 0 */
};
#define gte_cmdw_rotate_translate_perspective_single gte_cmdw_rtps #define gte_cmdw_rotate_translate_perspective_single gte_cmdw_rtps
#define gte_cmdw_rotate_translate_perspective_triple gte_cmdw_rtpt #define gte_cmdw_rotate_translate_perspective_triple gte_cmdw_rtpt
/* RGA(Lengyel): RTPS/RTPT consume the matrix expansion of a rigid transformation (rotation matrix + translation vector) loaded into the RT/TR control registers.
* For unitized points the same result equals the motor antiproduct; the GTE executes the LA form, not a symbolic antiproduct. */
/* PsyQ compatibility bits for AVSZ3 (Bits 20, 22, 24 must be set) */ /* PsyQ compatibility bits for AVSZ3 (Bits 20, 22, 24 must be set) */
#define gte_cmdw_psyq_avsz3_compat (0x15 << 20) #define gte_cmdw_psyq_avsz3_compat (0x15 << 20)
@@ -533,6 +433,7 @@ enum {
#define gte_lw_v2_z(base) enc_gte_lw(gte_in_v2_z, (base), GTE_Z_Offset) #define gte_lw_v2_z(base) enc_gte_lw(gte_in_v2_z, (base), GTE_Z_Offset)
/* gte_load_vN(r_ptr, base) — placeholder-punned lwc2 loaders /* gte_load_vN(r_ptr, base) — placeholder-punned lwc2 loaders
*
* Emits `.word` constants encoding `lwc2 $N, off(<base>)` for the chosen GTE vector register, where `<base>` is the GPR number you pass in * Emits `.word` constants encoding `lwc2 $N, off(<base>)` for the chosen GTE vector register, where `<base>` is the GPR number you pass in
* (typically one of R_T4..R_T9 for the standard "3-pointer" pattern). * (typically one of R_T4..R_T9 for the standard "3-pointer" pattern).
* *
@@ -581,8 +482,8 @@ enum {
/* gte_load_v0v1v2(p0, p1, p2, b0, b1, b2) — prelude to gte_cmd_rtpt. /* gte_load_v0v1v2(p0, p1, p2, b0, b1, b2) — prelude to gte_cmd_rtpt.
* *
* Loads all three GTE input vectors (6 words) from three separate pointers, one per GTE vector register, each loaded from its own base GPR. * Loads all three GTE input vectors (6 words) from three separate pointers, one per GTE vector register,
* Caller must bind each `pN` to `bN` via a register variable. * each loaded from its own base GPR. Caller must bind each `pN` to `bN` via a register variable.
* register V3_S2* p0 rgcc(R_T4) = verts[0].ptr; // → __asm__("$12") * register V3_S2* p0 rgcc(R_T4) = verts[0].ptr; // → __asm__("$12")
* register V3_S2* p1 rgcc(R_T5) = verts[1].ptr; // → __asm__("$13") * register V3_S2* p1 rgcc(R_T5) = verts[1].ptr; // → __asm__("$13")
* register V3_S2* p2 rgcc(R_T6) = verts[2].ptr; // → __asm__("$14") * register V3_S2* p2 rgcc(R_T6) = verts[2].ptr; // → __asm__("$14")
@@ -676,7 +577,8 @@ enum {
* Loads the 3x3 rotation matrix at `r0` into the GTE's rotation-matrix control registers (RT11..RT22, indices 0..4) via ctc2. * Loads the 3x3 rotation matrix at `r0` into the GTE's rotation-matrix control registers (RT11..RT22, indices 0..4) via ctc2.
* *
* Memory layout at r0: five contiguous 32-bit words (offsets 0..16), each holding two packed 16-bit matrix elements. * Memory layout at r0: five contiguous 32-bit words (offsets 0..16), each holding two packed 16-bit matrix elements.
* The first 1.5 rows of a standard PSX SDK MATRIX struct (where each row is laid out as [RT_xx, RT_xy] | [RT_xz, pad] | ...). * The first 1.5 rows of a standard PSX SDK MATRIX struct (where each row is laid out as
* [RT_xx, RT_xy] | [RT_xz, pad] | ...).
* *
* Generated MIPS (mirrors the source macro): * Generated MIPS (mirrors the source macro):
* lw $12, 0( %0 ) ; word 0 * lw $12, 0( %0 ) ; word 0
+164 -354
View File
@@ -12,249 +12,190 @@
#endif #endif
#pragma region Tape Drive #pragma region Tape Drive
/* ----------------------------------------------------------------------------------------------------------- /* -----------------------------------------------------------------------------
* TAPE DRIVE ABI * TAPE DRIVE ABI
* ----------------------------------------------------------------------------------------------------------- * -----------------------------------------------------------------------------
* Note(Ed): One of the main purposes of this codebase is to help me learn this, * Note(Ed): One of the main purposes of this codebase is to help me
* as such the information below may not* be entirely realized or finalized conceptually. * learn this, as such the information below may be entirely realized
* ----------------------------------------------------------------------------------------------------------- * or finalized conceptually.
* This ABI and its associated legos were directly inspired by researching the work of * -----------------------------------------------------------------------------
* Timothy Lottes and Onat Türkçüoğlu; along with many others. It's the simplest bootstrap of a * This ABI and its associated legos were directly inspired by researching
* directly executed chain of assemby arrays (Atoms) that terminate with a yield sequence to the next atom. * the work of Timothy Lottes and Onat Türkçüoğlu; along with many others.
* These eventually lead to a terminal atom for the tape which is defined below as "tape_exit". * It's the simplest bootstrap of a a directly executed chain of assemby
* arrays (Atoms) that terminate with a yield sequence to the next atom.
* These eventually lead to a terminal atom for the tape which is defined
* below as "tape_exit".
* *
* It behaves as one of the simplest runtime harnesses ontop of a host-enviornment's execution engine * This behaves as one of the simplest runtime harnesses ontop of a
* to author and compose programs with. From here various conventions can be further applied. * host-enviornment's execution engine to author and compose programs with.
* To make things easier to understand it may be better to focus on what this ABI does not have. * From here various conventions can be further applied.
* It does not have have any branching within the tape but relative branches within atoms or between atoms. * To make things easier to understand it may be better to focus on what this
* Branching nearly is always downstream. Automatic stack usage is non-existent. * ABI does not have. It does not have have any branching within the tape but
* Push/Pop, FIFO, or Arena/Bump data structures are used by atoms explicitly. * relative branches between atoms. Branching nearly is always downstream.
* In it's current form with the C11 macro DSL, the user also has fullfill manual register allocation per atom. * Stack usage is non-existent. Push/Pop, FIFO, or Arena/Bump data structures
* are used by atoms explicitly. In it's current form withe C11 macro dsl,
* the user also has to do manual register allocation per atom.
* *
* One of the remarkable things about utilizing this ABI is its essentially interopable with CPUs, GPUs, FPGA, * One of the remarkable things about utilizing this abi is its essentially
* or, basically anything from the 5th generation consoles and onward. * interopable with CPUs, GPUs, FPGA, or, basically anything
* The ABI directly reflects how all computational hardware must be architected in order to execute * from the 5th generation consoles and onward.
* digital logic effectively on current era tech. * The ABI directly reflects how all computational hardware must be architected
* On the PS1 we don't have access to a few features like multi-threading, speculative execution, or L3 cache; * in order to execute digital logic effectively on current era tech.
* but, we can set the foundation for legoing whats required for eventually expanding this ABI's paradigm * On the PS1 we don't have access to a few features like multi-threading,
* and core atoms to take those newer hardware features into account. For example, you can easily expand * speculative execution, or L3 cache; but, we can set the foundation for legoing
* this to support wave-based execution model on a PS2 or PS3. Not having a stack or * whats required baseline wise for eventually expanding the harness and core atoms
* automatic register allocation means the user cannot ignore excessive argument shuffle across workload or * to take those newer hardware features into account. For example, you can easily
* waves and thier phases. Crossing ABI boundaries to other runtimes that do has obviouss penalties. * expand this to support wave-based execution model on a PS2 or PS3.
* Not having a stack or automatic register allocation means the user can't ignore
* excessive argument shuffle across workload or waves and thier phases.
* Crossing ABI boundaries to other runtimes that do has an obviouss penalties.
* *
* Learning data-oriented code becomes a natural progression. Your not fighting a stack-based procedural * Learning data-oreinted code becomes a natural progression. Your not fighting
* paradigm that wants to argument shuffle. There is no ambiguity due to the lack of constraints, for example, * a stack-based procedural paradigm that wants to argument shuffle on the stack
* on how the user may "call" a procedure in traditional random dispatch runtimes. The user does have to * by lack of constraints on how the user may "call" a procedure. The user doesn't
* hammer down "rules" or patterns for massaging the compiler to dissolve those call frames; just to get * have to hammer down "rules" or patterns to know how to massage the compiler
* the asesmbly into its desired form. The form is obvious, and once the user gets to author these compoonents * to get the asesmbly into its natural form. The form is obvious, and once
* it becomes a game of tetris. * the user gets to author their compoonents it becomes a game of tetris.
* *
* Another feature is this ABI is very compatible with bootstrapping and developing simple toolchains built off * Another feature is this ABI is very compatible with bootstrapping and developing
* of bit-packed annotated command streams the user can directly author, maintatain, and immediately execute. * simple toolchains built off of bit-packed annotated command streams the user can
* That being like a color forth, or maybe something more familar like an immediate mode library * directly author, maintatain, and immediately execute. That being a color forth.
* for various systems such as GUIs. This can make the tetris less of a chore with some helpful policy * This can make the tetris less of a chore with some helpful policy generation for
* generation for allocation of registers, helping to choose resuable components, designing DSL on the fly, etc. * allocation of registers, helping to choose resuable components, designing DSL on
* ----------------------------------------------------------------------------------------------------------- * the fly, etc.
* TODO(Ed): We need pretty ascii diagrams and proper guides, articles, etc. * -----------------------------------------------------------------------------
* ----------------------------------------------------------------------------------------------------------- * TODO(Ed): We ned pretty ascii diagrams and proper guides, articles, etc.
* For now this ideation has just started functioning. I'm abusing C11 & a lua metaprogram to help establish * -----------------------------------------------------------------------------
* a hybrid toolchain to ideate on a traditional text-based authoring UX for this paradigm. * For now this thing is just functioning and I'm abusing C11 + a lua metaprogram
* If pcsx-redux provides viable hot-reload and persistent data storage beyond save-states * to help establish a hybrid toolchain to ideate on a traditional text-based
* (just copying ram to filesystem), I can author a color forth to mess around with. * authoring UX for this paradigm.
* With either an editor in-emulator or on the actual machine itself. Assembly is tedius, * If pcsx-redux gets me viable hot-reload and persistent data storage beyond
* but I think this codebase most likely has a pretty ergonomic flavor worst case... * save-states (just copying ram to filesystem). I can author a color forth to
* mess around with, with an editor in-emulator or on the actual machine itself.
* Assembly is tedius, but I think this codebase most likely has some of the most,
* ergonomic you can come across..
* */ * */
/* Register Allocation Info */ /* Register Allocation Info */
enum { enum {
R_ScratchBase = R_SP atom_reg, /* Scratchpad base address (host frame top) */ R_AtomJmp = R_T8 atom_reg, /* debug-visible; tape yield handshake scratch */
R_AtomJmp = R_FP atom_reg, /* Next atom target (yield handshake scratch) */ R_TapePtr = R_T9 atom_reg, /* The Instruction Stream Pointer */
R_TapePtr = R_RA atom_reg, /* The Instruction Stream Pointer */
/* Stringification codes for the GCC inline assembler clobber lists. */ /* Stringification codes for the GCC inline assembler clobber lists. */
#define R_ScratchBase_Code R_SP_Code #define R_AtomJmp_Code R_T8_Code
#define R_AtomJmp_Code R_FP_Code #define R_TapePtr_Code R_T9_Code
#define R_TapePtr_Code R_RA_Code
// R_InCursor = R_T4, // R_InCursor = R_T4,
// #define R_InCursor_Code R_T4_Code // #define R_InCursor_Code R_T4_Code
// Reserved Registers (Callee-saved across the host ABI transition): // Reserved Registers (Callee-saved):
// - R_SP: Holds the scratchpad base while tape code executes. // - R_T9: Holds the Tape Ptr which we need to increment
// - R_FP: Holds the next atom target. // If we hit a wall with register allocations we can clobber V0 & V1 (return values), defering as opt-in by user.
// - R_RA: Holds the tape cursor. // - R_RA: Not sure??
// All atom-body allocations must stay out of these. // Needed by ac_yield but can be used as atom scratch:
// Atom bodies may freely use R2-R25. // - R_T8: Will be used as the atom jump register.
// All allocatable registers for atom bodies (R2-R25, 24 registers): // All allocatable registers for mips atoms:
R_TScratchVolatile = R_AT, // This one is reserved for psuedo instructions, but you can technically use it.
R_PsuedoVolatile = R_AT, // Assembler temporary; never allocate. R_TScratch0 = R_T0,
R_TScratch1 = R_T1,
// Atom Allocation Pool R_TScratch2 = R_T2,
R_Atom0 = R_T0, R_TScratch3 = R_T3,
R_Atom1 = R_T1, R_TScratch4 = R_T4,
R_Atom2 = R_T2, R_TScratch5 = R_T5,
R_Atom3 = R_T3, R_TScratch6 = R_T6,
R_Atom4 = R_T4, R_TScratch7 = R_T7,
R_Atom5 = R_T5, R_TScratch8 = R_T8,
R_Atom6 = R_T6, R_TScratch10 = R_V0, // Tend to be used with gte DMAs
R_Atom7 = R_T7, R_TScratch11 = R_V1, // Tend to be used with gte DMAs
R_Atom8 = R_T8, // Note(Ed): We can technically clobber these, but don't unless we hit a bottleneck.
R_Atom9 = R_T9, // A 0-2
R_Atom10 = R_V0, // Tend to be used with gte moves // S 0-7
R_Atom11 = R_V1, // Tend to be used with gte moves
R_Atom12 = R_A0,
R_Atom13 = R_A1,
R_Atom14 = R_A2,
R_Atom15 = R_A3,
R_Atom16 = R_S0,
R_Atom17 = R_S1,
R_Atom18 = R_S2,
R_Atom19 = R_S3,
R_Atom20 = R_S4,
R_Atom21 = R_S5,
R_Atom22 = R_S6,
R_Atom23 = R_S7,
}; };
typedef U2 Reg; // Register parameter used with atom or atom component procedures
#define Reg_(type) tmpl(Reg,type) // Just a way to template register allocations of C-struct types.
typedef U4 const MipsCode; // Underlying type to mips asm words. typedef U4 const MipsCode; // Underlying type to mips asm words.
typedef Slice_(MipsCode); typedef Slice_(MipsCode);
typedef U4 const MipsAtom; // Underlying type to a mips atom defnition typedef U4 const MipsAtom; // Underlying type to an array of mips asm words that must terminate with an ac_yield.
typedef Slice_(MipsAtom);
// Sometimes a user will define a bundle of atoms that represent a procedure of work as:
// MipsAtom* <identifier>[...];
// Unfortuantely if using slice_from_array it will make the slice's pointer: MipsAtom** so this enforce its defined as MipsAtom*
// TODO(Ed): Alternatively we can make the MipsAtom an opaque pointer to the atom... so that the proc returns 'MipsAtom'.
#define atombundle_from_array(array) (Slice_MipsAtom){.ptr=array[0],.len=Array_len(array)}
// Underlying type to an ptr to an array of mips asm words that must terminate with an ac_yield.
#define MipsAtom_(sym) MipsCode sym [] align_(4) = #define MipsAtom_(sym) MipsCode sym [] align_(4) =
// Used for atoms with value-args
// internal MipsAtom* X_proc(AtomArena_R aa, args) MipsAtom_Proc_(X, aa, { body })
// expands to:
// internal MipsAtom* X_proc(AtomArena_R aa, args) { MipsCode atom_comp_code[] align_(4) = { body }; return atomarena_push(aa, slice_from_array(MipsCode, atom_comp_code)); }
// The atom name is derived by the Lua metaprogram from the preceding
// `MipsAtom* X_proc(...)` declaration (backward walk from the macro site,
// strips the `_proc` suffix).
#define MipsAtom_Proc_(aa, ...) { MipsCode atom_comp_code[] align_(4) = __VA_ARGS__; return atomarena_push(aa, slice_from_array(MipsCode, atom_comp_code)); }
// Used for components with no args (e.g., ac_load_tri_indices) or identifier-args (hardcoded register names). // Used for components with no args (e.g., ac_load_tri_indices) or identifier-args (hardcoded register names).
// MipsAtomComp_(ac_X) { body } // MipsAtomComp_(ac_X) { body }
// expands to: // expands to:
// MipsCode ac_X[] align_(4) = { body }; // MipsCode ac_X[] align_(4) = { body };
#define MipsAtomComp_(sym) MipsCode sym [] align_(4) = #define MipsAtomComp_(sym) MipsCode sym [] align_(4) =
// Used for components with value-args (mandatory `ab` (atom-builder) arg). // Used for components with value-args (e.g., ac_format_f3_color).
// FI_ void ac_X(MipsAtomBuilder_R ab, args) MipsAtomComp_Proc_(ab, { body }) // FI_ Slice_MipsCode ac_X(args) MipsAtomComp_Proc_(ac_X, { body })
// expands to: // expands to:
// FI_ void ac_X(MipsAtomBuilder_R ab, args) { // FI_ Slice_MipsCode ac_X(args) { MipsCode ac_X[] align_(4) = { body }; return slice_from_array(MipsCode, ac_X); }
// MipsCode atom_comp_code[] align_(4) = { body }; #define MipsAtomComp_Proc_(sym, ...) { MipsCode sym [] align_(4) = __VA_ARGS__; return slice_from_array(MipsCode, sym); }
// atombuilder_push(ab, slice_from_array(MipsCode, atom_comp_code));
// }
// The body must NOT include mac_yield() (the parent atom yields).
// The component name is derived by the Lua metaprogram from the preceding `FI_ Slice_MipsCode ac_X(...)` declaration (backward walk from the macro site).
// Inline-only callers (the generated `mac_<name>` aliases) skip the `ab` arg via metaprogram filtering; escape callers (ac_<name> invoked as a function) pass a long-lived builder.
#define MipsAtomComp_Proc_(ab, ...) { MipsCode atom_comp_code[] align_(4) = __VA_ARGS__; atombuilder_push(ab, slice_from_array(MipsCode, atom_comp_code)); }
// Used for trivial mappings from one atom component proc to the command of a more baser (meant for type-mapping) /* Line-table anchor: gcc only adds a file to the .debug_line file table when the
#define MipsAtomComp_ProcMap_(ab, base_command) atom_dbg_skip MipsAtomComp_Proc_(ab, {base_command }) file contains line-numbered content. Files containing only:
- `MipsAtomComp_` static-array declarations, or
- `MipsAtomComp_Proc_` (force-inline) function bodies whose line info gets
attributed to the call site at the include point are otherwise omitted from the file table,
which breaks the DWARF injection when it tries to resolve atom-component provenance paths.
// WIP: Atoms Assocated closely with each other to form a tape procedure. (Maybe also a phase in a procedure/pipeline?)
#define AtomBundle_(name) Struct_(tmpl(AtomBundle,name))
#define AtomBundle_Len(name) S_(tmpl(AtomBundle,name))/S_(MipsAtom*)
#define AtomBundleEntry_(bundle,entry) tmpl(bundle,entry)
/* Line-table anchor: gcc only adds a file to the .debug_line file table when the contains line-numbered content.
Files containing only atoms and atom components.
Place `ATOM_FILE_LINE_MARKER();` once at file scope in any `.atom.c` that defines atoms. Place `ATOM_FILE_LINE_MARKER();` once at file scope in any `.atom.c` that defines atoms.
Macro expands to a file-scope `internal U4 const` declaration keeps the file in the line table. The macro expands to a file-scope `internal U4 const` declaration keeps the file in the line table.
The constant is in `.rodata` so the linker may eliminate it. */ The constant is in `.rodata` and unreferenced; the linker may eliminate it.
The two-level concat + `__LINE__` suffix makes the identifier unique per call site
(the identifier embeds the source line, so duplicates across `#include`d files don't collide). */
#define ATOM_FILE_DEBUGGER_LINE_MARKER(file_name) internal U4 const tmpl(atom_file_debugger_line_marker,file_name) = 0 #define ATOM_FILE_DEBUGGER_LINE_MARKER(file_name) internal U4 const tmpl(atom_file_debugger_line_marker,file_name) = 0
typedef Slice_MipsAtom Tape; typedef Slice_(MipsAtom); typedef Slice_MipsAtom Tape;
typedef Struct_(TapeHostFrame) { /* The 'Exit' Atom */
U4 s0; atom_dbg_skip MipsAtom_(tape_exit) { jump_reg(rret_addr), nop };
U4 s1;
U4 s2;
U4 s3;
U4 s4;
U4 s5;
U4 s6;
U4 s7;
U4 fp;
U4 sp;
U4 ra;
};
enum { // TODO(Ed): When we have a substantial workload/throughput, profile each of these to see impact at ABI boundaries.
TapeHostFrame_Loc = Scratchpad_End - S_(TapeHostFrame),
TapeScratch_Len = TapeHostFrame_Loc - Scratchpad_Loc,
};
static_assert(S_(TapeHostFrame) == 11 * S_(U4));
static_assert(TapeHostFrame_Loc == 0x1F8003D4);
atom_dbg_skip MipsAtom_(tape_enter) { /* Tape Runner (Default) */
mac_load_word_imm(R_V0, u4_(TapeHostFrame_Loc)), FI_ void tape_run(Tape tape) { register U4* tape_ptr rgcc(R_TapePtr) = u4_r(tape.ptr); asm volatile(
store_word(R_S0, R_V0, O_(TapeHostFrame,s0)), asm_words(
store_word(R_S1, R_V0, O_(TapeHostFrame,s1)), load_word( R_AtomJmp, R_TapePtr, 0) /* Bootstrap the first jump */
store_word(R_S2, R_V0, O_(TapeHostFrame,s2)), , add_ui_self(R_TapePtr, S_(MipsAtom)) /* Advance tape */
store_word(R_S3, R_V0, O_(TapeHostFrame,s3)), , call_reg( R_AtomJmp) /* jalr $t9 */
store_word(R_S4, R_V0, O_(TapeHostFrame,s4)), , nop /* Branch delay slot */
store_word(R_S5, R_V0, O_(TapeHostFrame,s5)), )
store_word(R_S6, R_V0, O_(TapeHostFrame,s6)), asm_rpins, r_use(tape_ptr)
store_word(R_S7, R_V0, O_(TapeHostFrame,s7)), asm_clobber:
store_word(R_FP, R_V0, O_(TapeHostFrame,fp)), rlit(R_AT),
store_word(R_SP, R_V0, O_(TapeHostFrame,sp)), rlit(R_V0), rlit(R_V1), // We clobber these for GTE ACs (that don't expose register selection, might expose them in the future...)
store_word(R_RA, R_V0, O_(TapeHostFrame,ra)), rlit(R_T0), rlit(R_T1), rlit(R_T2), rlit(R_T3), rlit(R_T4),
add_ui(R_TapePtr, R_A0, 0), rlit(R_T5), rlit(R_T6), rlit(R_T7), rlit(R_T8),
load_upper_i(R_ScratchBase, u4_hi(Scratchpad_Loc)), clb_mem_drain
load_word(R_AtomJmp, R_TapePtr, 0), ); }
add_ui_self( R_TapePtr, S_(MipsAtom)),
jump_reg(R_AtomJmp), BdSlot_ nop,
};
atom_dbg_skip MipsAtom_(tape_exit) { /* Tape Runner (Static and Arg Clobbers) */
mac_load_word_imm(R_V0, u4_(TapeHostFrame_Loc)), FI_ void tape_run_a02_s07(Tape tape) { register U4* tape_ptr rgcc(R_TapePtr) = u4_r(tape.ptr); asm volatile(
load_word(R_S0, R_V0, O_(TapeHostFrame,s0)), asm_words(
load_word(R_S1, R_V0, O_(TapeHostFrame,s1)), load_word( R_AtomJmp, R_TapePtr, 0) /* Bootstrap the first jump */
load_word(R_S2, R_V0, O_(TapeHostFrame,s2)), , add_ui_self(R_TapePtr, S_(MipsAtom)) /* Advance tape */
load_word(R_S3, R_V0, O_(TapeHostFrame,s3)), , call_reg( R_AtomJmp) /* jalr $t9 */
load_word(R_S4, R_V0, O_(TapeHostFrame,s4)), , nop /* Branch delay slot */
load_word(R_S5, R_V0, O_(TapeHostFrame,s5)), )
load_word(R_S6, R_V0, O_(TapeHostFrame,s6)), asm_rpins, r_use(tape_ptr)
load_word(R_S7, R_V0, O_(TapeHostFrame,s7)), asm_clobber:
load_word(R_RA, R_V0, O_(TapeHostFrame,ra)), rlit(R_AT),
load_word(R_FP, R_V0, O_(TapeHostFrame,fp)), rlit(R_V0), rlit(R_V1), rlit(R_A0), rlit(R_A1), rlit(R_A2),
load_word(R_SP, R_V0, O_(TapeHostFrame,sp)), rlit(R_T0), rlit(R_T1), rlit(R_T2), rlit(R_T3), rlit(R_T4),
jump_reg(R_RA), BdSlot_ nop, rlit(R_T5), rlit(R_T6), rlit(R_T7), rlit(R_T8),
}; rlit(R_S0), rlit(R_S1), rlit(R_S2), rlit(R_S3), rlit(R_S4),
rlit(R_S5), rlit(R_S6), rlit(R_S7),
typedef void Proc_(TapeEntryFn)(MipsAtom* tape_ptr); clb_mem_drain
); }
FI_ void tape_run(Tape tape) { C_(TapeEntryFn*, tape_enter)(tape.ptr); }
// Procedural authoring of tapes: // Procedural authoring of tapes:
typedef Relative_(FArena) Struct_(TapeBuilder) { U4 ptr; U4 capacity; U4 used; }; typedef Relative_(FArena) Struct_(TapeBuilder) { U4 ptr; U4 capacity; U4 used; };
FI_ void tb_init(TapeBuilder* tb, FArena* arena) { tb->ptr = arena->start; tb->used = 0; } FI_ void tb_init(TapeBuilder* tb, FArena* arena) { tb->ptr = arena->start; tb->used = 0; }
FI_ TapeBuilder tb_make_old( FArena* arena) { return (TapeBuilder){ arena->start, 0 }; } FI_ TapeBuilder tb_make_old( FArena* arena) { return (TapeBuilder){ arena->start, 0 }; }
FI_ TapeBuilder tb_make(Slice mem) { return (TapeBuilder){ u4_(mem.ptr), mem.len, 0 }; } /* capacity in elements (matches used units) */ FI_ TapeBuilder tb_make(Slice mem) { return (TapeBuilder){ mem.ptr, mem.len, 0 }; }
FI_ void tb_emit(TapeBuilder* tb, MipsAtom* atom) { u4_r(tb->ptr)[tb->used] = u4_(atom); ++ tb->used; } FI_ void tb_emit(TapeBuilder* tb, MipsCode* atom) { u4_r(tb->ptr)[tb->used] = u4_(atom); ++ tb->used; }
FI_ void tb_data(TapeBuilder* tb, U4 data) { u4_r(tb->ptr)[tb->used] = u4_(data); ++ tb->used; } FI_ void tb_data(TapeBuilder* tb, U4 data) { u4_r(tb->ptr)[tb->used] = u4_(data); ++ tb->used; }
#define tb_emit_(atom) tb_emit(& tb, atom) #define tb_emit_(atom) tb_emit(& tb, atom)
#define tb_data_(field, data) tb_data(& tb, u4_(data))
FI_ void tb_bind(TapeBuilder* tb, Slice data) { mem_copy(tb->ptr + tb->used * S_(MipsCode), u4_(data.ptr), data.len); tb->used += data.len / S_(MipsCode); }
#define tb_bind_(tb,type,...) tb_bind(tb, (Slice){ (B1*)(& (type){__VA_ARGS__}), S_(type) }); static_assert(S_(type) % S_(MipsCode) == 0)
// NOTE(Ed): Wip still ideating convention. Possibly will never use a composite.
#define tb_emit_wbind_(tb,atom,...) tb_emit(tb,atom); tb_bind_(tb,tmpl(Binds,atom),__VA_ARGS__)
#define tb_emit_wbind2_(tb,atom,type,...) tb_emit(tb,atom); tb_bind_(tb,type,__VA_ARGS__)
FI_ Tape tb_end (TapeBuilder* tb) { tb_emit(tb,tape_exit); return (Tape){ C_(U4*,tb->ptr), tb->used }; } FI_ Tape tb_end (TapeBuilder* tb) { tb_emit(tb,tape_exit); return (Tape){ C_(U4*,tb->ptr), tb->used }; }
FI_ Tape tb_slice(TapeBuilder tb) { return (Tape){ C_(U4*,tb.ptr), tb.used }; } FI_ Tape tb_slice(TapeBuilder tb) { return (Tape){ C_(U4*,tb.ptr), tb.used }; }
@@ -271,11 +212,15 @@ FI_ void tb_scope_run_end(TapeBuilder* tb) { tb_emit(tb,tape_exit); tape_run(tb_
* ---------------------------------------------------------------------------*/ * ---------------------------------------------------------------------------*/
// The 'Yield' sequence for Tape Atoms (mac_yield). // The 'Yield' sequence for Tape Atoms (mac_yield).
// - mac_yield() is the safe default for atom-endings: 4 words, BD-slot of jr is mandatory nop.
// - mac_yield_load() + mac_yield_tail():
// - unconditional branch: mac_yield_load fills the branch's BD-slot (replaces a nop);
// - mac_yield_tail runs at the branch target (does NOT re-load R_AtomJmp).
atom_dbg_skip MipsAtomComp_(ac_yield) { atom_dbg_skip MipsAtomComp_(ac_yield) {
load_word(R_AtomJmp, R_TapePtr, 0), LdSlot_ load_word(R_AtomJmp, R_TapePtr, 0),
add_ui_self( R_TapePtr, S_(MipsCode)), add_ui_self( R_TapePtr, S_(MipsCode)),
jump_reg( R_AtomJmp), BdSlot_ nop, jump_reg( R_AtomJmp), nop,
}; };
atom_dbg_skip MipsAtomComp_(ac_yield_load) { atom_dbg_skip MipsAtomComp_(ac_yield_load) {
@@ -284,172 +229,37 @@ atom_dbg_skip MipsAtomComp_(ac_yield_load) {
atom_dbg_skip MipsAtomComp_(ac_yield_tail) { atom_dbg_skip MipsAtomComp_(ac_yield_tail) {
add_ui_self(R_TapePtr, S_(MipsCode)), add_ui_self(R_TapePtr, S_(MipsCode)),
jump_reg( R_AtomJmp), BdSlot_ nop, jump_reg( R_AtomJmp), nop,
}; };
#pragma endregion Macro Atom Components #pragma endregion Macro Atom Components
#pragma region Atom Builder #pragma region Mips Atom Builder
// This helps with runtime procedural authoring of mips atoms. // This helps with runtime procedural authoring of mips atoms.
typedef Relative_(FArena) Struct_(AtomBuilder) { U4 start; U4 capacity; U4 used; };
// Usual way to resolve an atom after the bulder is done. typedef Struct_(FMipsAtom512) { U4 data[512]; U4 used; };
#define atom_from_atombuilder(ab) C_(MipsAtom*, (ab).start)
FI_ void atombuilder_push(AtomBuilder_R ab, Slice_MipsCode code) { // FArena Related
assert(ab->capacity - ab->used - code.len); typedef Relative_(FArena) Struct_(MipsAtomBuilder) { U4 start; U4 capacity; U4 used; };
U4 dest = ab->start + ab->used * S_(MipsCode); U4 size = S_slice(code); // Whatever the builder is writting to should most likely coresspond
mem_copy(dest, u4_(code.ptr), size); ab->used += size; // to something that can fit within instruction cache?
FI_ void atombuilder_unroll(MipsAtomBuilder_R ab, Slice_MipsCode_R code) {
assert(ab->capacity - ab->used - code->len);
mem_copy(ab->start, u4_(code->ptr), code->len);
mem_bump(ab->start, ab->capacity, & ab->used, code->len);
} }
#define atombuilder_push_mac(ab, mac) atombuilder_push(ab, slice_arg_from_array(Slice_MipsCode, mac)) #define atombuilder_unroll_mac(ab, mac) atombuilder_unroll(ab, slice_arg_from_array(Slice_MipsCode, mac))
// When done authoring, utilize this to cap-off the atom (if not utilizing a MipsAtom_Proc). // When done authoring, utilize this to cap-off the atom
FI_ void atombuilder_end(AtomBuilder_R ab) { atombuilder_push(ab, slice_from_array(MipsCode, ac_yield)); } FI_ void atombuilder_end(MipsAtomBuilder_R ab) {
mem_copy(ab->start, u4_(ac_yield), S_(ac_yield));
mem_bump(ab->start, ab->capacity, & ab->used, S_(ac_yield));
}
FI_ void tb_emit_atombuilder(TapeBuilder_R tb, AtomBuilder_R ab) { tb_emit(tb, atom_from_atombuilder(ab[0])); } #define mipsatom_from_builder(ab) (Slice_MipsCode){ab.start, ab.used}
#pragma endregion Mips Atom Builder #pragma endregion Mips Atom Builder
#pragma region Atom Arena
// Just a dedicated FArena that is meant to mem_copy and return atom definitions made with MipsAtom_Proc_
typedef Relative_(FArena) Struct_(AtomArena) { U4 start; U4 capacity; U4 used; };
#define atomarena_unused_start(ab) ((ab).start + (ab).used)
FI_ void atomarena_init(AtomArena_R arena, Slice mem) { assert(arena != nullptr);
arena->start = u4_(mem.ptr);
arena->capacity = mem.len;
arena->used = 0;
}
FI_ AtomArena atomarena_make(Slice mem) { AtomArena a; atomarena_init(& a, mem); return a; }
FI_ MipsAtom* atomarena_push(AtomArena_R aa, Slice_MipsCode code) {
assert(aa->capacity - aa->used - code.len);
U4 dest = atomarena_unused_start(aa[0]); U4 size = S_slice(code);
mem_copy(dest, u4_(code.ptr), size); aa->used += size;
return C_(MipsAtom*, dest);
}
FI_ void atomarena_reset(AtomArena_R aa) { aa->used = 0; }
#pragma endregion Atom Arena
#pragma region RegFile (Register File Allocator)
// A specialized allocator utilized to help the user track which registers are bound to values
// that must be preserved for the arena's bounds.
// TODO(Ed): Technically we can do this at comp-time with the metaprogram, but we may have namespace conflicts.
// Unless we follow a convention for #define <Scope_Prefix> or something per register allocation boundary.
/* ABI reserves that are never handed out by alloc.
* R_AT is the assembler temporary (per the MIPS O32 ABI).
* R_K0/K1 are kernel reserves.
* R_GP stays the host global pointer.
* R_SP/R_FP/R_RA are tape runtime carriers between tape_enter and tape_exit. */
U4 const regfile_abi_mask =
(1u << R_0) | (1u << R_AT) |
(1u << R_K0) | (1u << R_K1) |
(1u << R_GP) | (1u << R_SP) |
(1u << R_FP) | (1u << R_RA);
internal Reg const regfile_alloc_order[] = {
R_V0, R_V1,
R_A0, R_A1, R_A2, R_A3,
R_T0, R_T1, R_T2, R_T3, R_T4, R_T5, R_T6, R_T7,
R_S0, R_S1, R_S2, R_S3, R_S4, R_S5, R_S6, R_S7,
R_T8, R_T9,
};
typedef Struct_(RegFile) {
A2_U2 GPR;
A2_U2 GTE;
};
#define regfile(pin_mask) {.GPR={u4_lo(pin_mask), u4_hi(pin_mask)} }
FI_ void regfile_init(RegFile_R rf) {
/* pack the 32-bit ABI mask into the two U2s */
rf->GPR[0] = u4_lo(regfile_abi_mask);
rf->GPR[1] = u4_hi(regfile_abi_mask);
rf->GTE[0] = rf->GTE[1] = 0;
}
FI_ RegFile regfile_make(void) { RegFile rf; regfile_init(& rf); return rf; }
typedef Struct_(RegFile_RInfo) {
U2_R section;
U2 mask;
B2 occupied;
};
FI_ RegFile_RInfo regfile_rinfo(A2_U2 file, Reg r_id) {
U2 s_id = r_id >> 4;
U2_R section = & file[s_id];
U2 mask = u2_(1u << (r_id & 15));
B2 occupied = (section[0] & mask) != 0;
return (RegFile_RInfo){section, mask, occupied};
}
FI_ Reg regfile__alloc_helper(A2_U2 file, Reg r_id) {
Reg result = 0; RegFile_RInfo info = regfile_rinfo(file, r_id);
if (info.occupied == false) {
info.section[0] |= info.mask;
result = r_id;
}
return result;
}
I_ Reg regfile_alloc(RegFile_R rf) {
Reg allocated = 0;
for index_iter(U4, r_id, R_V0, <, R_T9) {
allocated = regfile__alloc_helper(rf->GPR, r_id);
Jmp_nZero_(allocated,resolved);
}
assert(allocated != 0);
resolved: return allocated;
}
FI_ Reg regfile_pin(RegFile_R rf, Reg r_id) {
RegFile_RInfo info = regfile_rinfo(rf->GPR, r_id);
assert(info.occupied == false);
info.section[0] |= info.mask;
return r_id;
}
FI_ void regfile_pin_mask(RegFile_R rf, U4 mask) {
B4 occupied = u4_r(rf->GPR)[0] & mask;
assert(occupied == false);
u4_r(rf->GPR)[0] |= mask;
}
FI_ void regfile_free_mask(RegFile_R rf, U4 mask) {
if (regfile_abi_mask & mask) return;
u4_r(rf->GPR)[0] &= ~mask;
}
FI_ void regfile_free_reg(RegFile_R rf, Reg r_id) {
/* never free the ABI set */
if (regfile_abi_mask & (1u << r_id)) return;
RegFile_RInfo info = regfile_rinfo(rf->GPR, r_id);
info.section[0] &= ~info.mask;
}
FI_ void regfile_reset(RegFile_R rf) {
rf->GPR[0] = u4_lo(regfile_abi_mask);
rf->GPR[1] = u4_hi(regfile_abi_mask);
}
FI_ void regfile_reset_to_mask(RegFile_R rf, U4 mask) {
rf->GPR[0] = u4_lo(mask);
rf->GPR[1] = u4_hi(mask);
}
#pragma endregion RegFileArena (Register File Allocator)
#pragma region Mips Atom Procs
/* RegUse structs are a convention to organize register allocations for a mips atom procedure.
Unlike the usual enum-based declarations, they provide a namespaced scope and have view types via union declarations. */
#define RegUse_(proc_name) (tmpl(RegUse,proc_name))
typedef Struct_(RegUse_example_atom_proc) {
Reg const ro_register; // Scratch base carrier.
Reg usual_modifiable;
union { Reg view_1, view_2, view_3; } t1;
};
internal MipsAtom* example_atom_proc(AtomArena_R aa, U2 offset, RegUse_example_atom_proc r)
MipsAtom_Proc_(aa, {
add_si(r.usual_modifiable, r.ro_register, offset),
or_u(r.t1.view_1, r.ro_register, 0),
branch_lt_zero(r.t1.view_1, atom_offset(example_atom_proc, skip)), BdSlot_ nop,
li_s(r.t1.view_2, 100),
atom_label(skip)
add_si(r.t1.view_3, r.usual_modifiable, 10),
mac_yield(),
})
#pragma endregion Mips Atom Procs
#pragma region Baked Mips Atoms #pragma region Baked Mips Atoms
// These atoms are resolved at compile time and are (usually) statically linked readonly data. // These atoms are resolved at compile time and are (usually) statically linked readonly data.
+29
View File
@@ -0,0 +1,29 @@
#ifdef INTELLISENSE_DIRECTIVES
# include "gen/macs.h"
# include "gen/offsets.h"
# include "math.h"
# include "lottes_tape.h"
#endif
ATOM_FILE_DEBUGGER_LINE_MARKER(math_atom_c);
#pragma region MACs (Mips Atom Component)
FI_ Slice_MipsCode ac_load_v2s2(U4 rs_x, U4 rs_y, U4 r_base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_load_v2s2, {
load_half( rs_x, r_base, O_(V3_S2,x)),
load_half( rs_y, r_base, O_(V3_S2,y)),
})
FI_ Slice_MipsCode ac_store_v2s2(U4 rt_x, U4 rt_y, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_v2s2, {
store_half(rt_x, base, offset + O_(V2_S2,x)),
store_half(rt_y, base, offset + O_(V2_S2,y)),
})
FI_ Slice_MipsCode ac_store_rects2(U4 rt_x, U4 rt_y, U4 rt_width, U4 rt_height, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_rects2, {
store_half(rt_x, base, offset + O_(Rect_S2,x)),
store_half(rt_y, base, offset + O_(Rect_S2,y)),
store_half(rt_width, base, offset + O_(Rect_S2,width)),
store_half(rt_height, base, offset + O_(Rect_S2,height)),
})
#pragma endregion MACs (Mips Atom Component)
-96
View File
@@ -1,96 +0,0 @@
#ifdef INTELLISENSE_DIRECTIVES
# include "gen/macs.h"
# include "gen/offsets.h"
# include "math.h"
# include "lottes_tape.h"
#endif
ATOM_FILE_DEBUGGER_LINE_MARKER(math_atom_c);
#define v3s4_R_0() ((Reg_(V3_S4)){R_0,R_0,R_0})
typedef Struct_(Reg_V3_S2) { Reg x, y, z; };
typedef Struct_(Reg_V3_S4) { Reg x, y, z; }; // Register allocation of a V3_S4
typedef Struct_(Reg_P3_S4) { Reg x, y, z; }; // Register allocation of a P3_S4
#pragma region MACs (Mips Atom Component)
FI_ Slice_MipsCode ac_load_half_v3(AtomBuilder_R ab, Reg tx, Reg ty, Reg tz, Reg base, U2 offset) atom_dbg_skip MipsAtomComp_Proc_(ab, {
load_half(tx, base, offset + OA_(U2,[0])),
load_half(ty, base, offset + OA_(U2,[1])),
load_half(tz, base, offset + OA_(U2,[2])),
})
FI_ Slice_MipsCode ac_load_v3s2(AtomBuilder_R ab, Reg_(V3_S2) transfer, Reg base, U2 offset) MipsAtomComp_ProcMap_(ab, mac_load_half_v3(transfer.x, transfer.y, transfer.z, base, offset))
FI_ Slice_MipsCode ac_load_v2s2(AtomBuilder_R ab, U4 rs_x, U4 rs_y, U4 r_base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ab, {
load_half(rs_x, r_base, offset + O_(V3_S2,x)),
load_half(rs_y, r_base, offset + O_(V3_S2,y)),
})
FI_ Slice_MipsCode ac_store_v2s2(AtomBuilder_R ab, U4 rt_x, U4 rt_y, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ab, {
store_half(rt_x, base, offset + O_(V2_S2,x)),
store_half(rt_y, base, offset + O_(V2_S2,y)),
})
FI_ Slice_MipsCode ac_load_word_v3(AtomBuilder_R ab, Reg tx, Reg ty, Reg tz, Reg base, U2 offset) atom_dbg_skip MipsAtomComp_Proc_(ab, {
load_word(tx, base, offset + OA_(U4,[0])),
load_word(ty, base, offset + OA_(U4,[1])),
load_word(tz, base, offset + OA_(U4,[2])),
})
FI_ Slice_MipsCode ac_load_v3s4(AtomBuilder_R ab, Reg_(V3_S4) transfer, Reg base, U2 offset) MipsAtomComp_ProcMap_(ab, mac_load_word_v3(transfer.x, transfer.y, transfer.z, base, offset))
FI_ Slice_MipsCode ac_load_p3s4(AtomBuilder_R ab, Reg_(P3_S4) transfer, Reg base, U2 offset) MipsAtomComp_ProcMap_(ab, mac_load_word_v3(transfer.x, transfer.y, transfer.z, base, offset))
FI_ Slice_MipsCode ac_store_half_v3(AtomBuilder_R ab, Reg tx, Reg ty, Reg tz, Reg base, U2 offset) atom_dbg_skip MipsAtomComp_Proc_(ab, {
store_half(tx, base, offset + OA_(U2,[0])),
store_half(ty, base, offset + OA_(U2,[1])),
store_half(tz, base, offset + OA_(U2,[2])),
})
FI_ Slice_MipsCode ac_store_v3s2(AtomBuilder_R ab, Reg_(V3_S2) transfer, Reg base, U2 offset) MipsAtomComp_ProcMap_(ab, mac_store_half_v3(transfer.x, transfer.y, transfer.z, base, offset))
FI_ Slice_MipsCode ac_store_word_v3(AtomBuilder_R ab, Reg tx, Reg ty, Reg tz, Reg base, U2 offset) atom_dbg_skip MipsAtomComp_Proc_(ab, {
store_word(tx, base, offset + OA_(U4,[0])),
store_word(ty, base, offset + OA_(U4,[1])),
store_word(tz, base, offset + OA_(U4,[2])),
})
FI_ Slice_MipsCode ac_store_v3s4(AtomBuilder_R ab, Reg_(V3_S4) transfer, Reg base, U2 offset) MipsAtomComp_ProcMap_(ab, mac_store_word_v3(transfer.x, transfer.y, transfer.z, base, offset))
FI_ Slice_MipsCode ac_store_p3s4(AtomBuilder_R ab, Reg_(P3_S4) transfer, Reg base, U2 offset) MipsAtomComp_ProcMap_(ab, mac_store_word_v3(transfer.x, transfer.y, transfer.z, base, offset))
FI_ Slice_MipsCode ac_add_si_v3s4(AtomBuilder_R ab, Reg rt_x, Reg rt_y, Reg rt_z, Reg base, U2 offset)
atom_dbg_skip MipsAtomComp_Proc_(ab, {
add_si(rt_x, base, O_(V3_S4,x)),
add_si(rt_y, base, O_(V3_S4,y)),
add_si(rt_z, base, O_(V3_S4,z)),
})
FI_ Slice_MipsCode ac_sub_s_v3(AtomBuilder_R ab
, Reg dx, Reg dy, Reg dz
, Reg sx, Reg sy, Reg sz
, Reg tx, Reg ty, Reg tz
) atom_dbg_skip MipsAtomComp_Proc_(ab, {
sub_s(dx, sx, tx),
sub_s(dy, sy, ty),
sub_s(dz, sz, tz),
})
FI_ Slice_MipsCode ac_sub_v3s4(AtomBuilder_R ab, Reg_(V3_S4) d, Reg_(V3_S4) s, Reg_(V3_S4) t) MipsAtomComp_ProcMap_(ab, mac_sub_s_v3(d.x, d.y, d.z, s.x, s.y, s.z, t.x, t.y, t.z))
FI_ Slice_MipsCode ac_sub_s_v3_self(AtomBuilder_R ab, Reg ds_x, Reg ds_y, Reg ds_z, Reg tx, Reg ty, Reg tz) atom_dbg_skip MipsAtomComp_Proc_(ab, {
sub_s(ds_x, ds_x, tx),
sub_s(ds_y, ds_y, ty),
sub_s(ds_z, ds_z, tz),
})
FI_ Slice_MipsCode ac_sub_v3s4_self(AtomBuilder_R ab, Reg_(V3_S4) ds, Reg_(V3_S4) t) MipsAtomComp_ProcMap_(ab, mac_sub_s_v3_self(ds.x, ds.y, ds.z, t.x, t.y, t.z))
FI_ Slice_MipsCode ac_store_rects2(AtomBuilder_R ab, U4 rt_x, U4 rt_y, U4 rt_width, U4 rt_height, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ab, {
store_half(rt_x, base, offset + O_(Rect_S2,x)),
store_half(rt_y, base, offset + O_(Rect_S2,y)),
store_half(rt_width, base, offset + O_(Rect_S2,width)),
store_half(rt_height, base, offset + O_(Rect_S2,height)),
})
#pragma endregion MACs (Mips Atom Component)
+7 -63
View File
@@ -7,24 +7,11 @@
#define max(A, B) (((A) > (B)) ? (A) : (B)) #define max(A, B) (((A) > (B)) ? (A) : (B))
#define clamp_bot(X, B) max(X, B) #define clamp_bot(X, B) max(X, B)
/* Convention
<Type> ## <Width> _ <Component Type> ## <Component Width>
For types with compound data (Ex: Rotation Matrix & Translation):
<TypeA> ## <TypeB> ## <Width> _ <ComponentTypeA> ## <ComponentWidthA> ## <ComponentTypeB> ## <ComponentWidthB>
A: Array
V: Vector
R: Range
M: Matrix
T: Translation
*/
enum { enum {
v3s2_byteoff = 3, // log2(8), used with shift_left_logical op for index via byte offset. v3s2_byteoff = 3, // log2(8), used with shift_left_logical op for index via byte offset.
}; };
typedef Array_(U1, 2); typedef Array_(U1, 2);
typedef Array_(U2, 2);
typedef Array_(U4, 2); typedef Array_(U4, 2);
typedef Array_(S2, 2); typedef Array_(S2, 2);
typedef Array_(S2, 3); typedef Array_(S2, 3);
@@ -39,43 +26,23 @@ typedef Struct_(Extent2_S4) { S4 width; S4 height; };
typedef Struct_(V2_U1) { U1 x; U1 y; }; typedef Struct_(V2_U1) { U1 x; U1 y; };
typedef Struct_(V2_S2) { S2 x; S2 y; }; typedef Struct_(V2_S2) { S2 x; S2 y; };
typedef Struct_(V2_S4) { S4 x; S4 y; }; typedef Struct_(V2_S4) { S4 x; S4 y; };
typedef Struct_(V3_S2) { S2 x; S2 y; S2 z; S2 pad; }; // PSY-Q: SVECTOR typedef Struct_(V3_S2) { S2 x; S2 y; S2 z; S2 pad; };
typedef Struct_(V3_S4) { S4 x; S4 y; S4 z; S4 pad; }; // PSY-Q: VECTOR. RGA(Lengyel): Euclidean vector or direction. A zero-weight RGA point is stored as a V3_S4 with the implicit weight dropped. typedef Struct_(V3_S4) { S4 x; S4 y; S4 z; S4 pad; };
typedef Struct_(V4_S2) { S2 x; S2 y; S2 z; S2 w; }; typedef Struct_(V4_S2) { S2 x; S2 y; S2 z; S2 w; };
typedef Struct_(V4_S4) { S4 x; S4 y; S4 z; S4 w; }; typedef Struct_(V4_S4) { S4 x; S4 y; S4 z; S4 w; };
// typedef Struct_(P3_S4) { S4 x; S4 y; S4 z; S4 w1; }; // RGA(Lengyel): Affine point with implicit weight one. Storage alias of V3_S4. Use P3_S4 when the value is a point. typedef Struct_(R2_S2) { V2_S2 p0; V2_S2 p1; };
typedef V3_S4 P3_S4; typedef Struct_(R2_S4) { V2_S4 p0; V2_S4 p1; };
typedef Struct_(R1_U2) { U2 p0; U2 p1; };
typedef Struct_(R1_S2) { S2 p0; S2 p1; };
typedef Struct_(R2_S2) { V2_S2 p0; V2_S2 p1; }; // Range-2 Signed 2-Byte (16-bit)
typedef Struct_(R2_S4) { V2_S4 p0; V2_S4 p1; }; // Range-2 Signed 4-Byte (32-bit)
typedef Struct_(Rect_S2) { S2 x; S2 y; S2 width; S2 height; }; typedef Struct_(Rect_S2) { S2 x; S2 y; S2 width; S2 height; };
typedef Struct_(Rect_S4) { S4 x; S4 y; S4 width; S4 height; }; typedef Struct_(Rect_S4) { S4 x; S4 y; S4 width; S4 height; };
typedef Struct_(MT3_S2S4) { A3x3_S2 m; A3_S4 t; }; // PSY-Q: MATRIX. RGA(Lengyel): Matrix expansion of a rigid transformation. GTE utilizes this representation; corresponding motor not constructed here. typedef Struct_(M3_S2) { A3x3_S2 m; A3_S4 t; };
/* RGA(Lengyel) reserved names (deferred):
* P4_S4 - future flat point with explicit weight (Lengyel/TML FlatPoint3D analog).
* B3_S4 - future 3D bivector (callers store a Complement(Wedge(...)) as a V3_S4).
* Mo8_S4 - future motor. Not introduced until a course operation actually needs composition, interpolation, or inversion. */
typedef Array_(V2_U1, 2);
typedef Array_(V2_S2, 2); typedef Array_(V2_S2, 2);
typedef Array_(V2_S2, 3); typedef Array_(V2_S2, 3);
typedef Array_(V2_S2, 4); typedef Array_(V2_S2, 4);
#define r1u2(p0,p1) (R1_U2){p0,p1}
enum {
fp_one = (1 << 12),
};
#define v3s4_fp_one() v3s4(fp_one, fp_one, fp_one)
#define v2s2(x,y) (V2_S2){x,y} #define v2s2(x,y) (V2_S2){x,y}
#define v3s2(x,y,z) (V3_S2){x,y,z,0} #define v3s2(x,y,z) (V3_S2){x,y,z,0}
#define v3s4(x,y,z) (V3_S4){x,y,z,0} #define v3s4(x,y,z) (V3_S4){x,y,z,0}
@@ -94,28 +61,5 @@ FI_ void add_a3s4_fp(A3_S4_R out_a, A3_S4 b) {
(out_a[0])[2] += b[2] >> 1; (out_a[0])[2] += b[2] >> 1;
} }
FI_ void sub_a3s4(A3_S4_R out_a, A3_S4 b) { FI_ void add_v3s4 (V3_S4_R out_a, V3_S4 b) { add_a3s4 (pcast(A3_S4_R, out_a), pcast(A3_S4, b)); }
(out_a[0])[0] -= b[0]; FI_ void add_v3s4_fp(V3_S4_R out_a, V3_S4 b) { add_a3s4_fp(pcast(A3_S4_R, out_a), pcast(A3_S4, b)); }
(out_a[0])[1] -= b[1];
(out_a[0])[2] -= b[2];
}
FI_ void sub_a3s4_fp(A3_S4_R out_a, A3_S4 b) {
(out_a[0])[0] -= b[0] >> 1;
(out_a[0])[1] -= b[1] >> 1;
(out_a[0])[2] -= b[2] >> 1;
}
FI_ void mul_a3s4(A3_S4_R out_a, A3_S4 b) {
(out_a[0])[0] *= b[0];
(out_a[0])[1] *= b[1];
(out_a[0])[2] *= b[2];
}
FI_ void add_v3s4 (V3_S4_R out_a, V3_S4 b) { add_a3s4 (C_ptr(A3_S4_R, out_a), C_ptr(A3_S4, b)); }
FI_ void add_v3s4_fp(V3_S4_R out_a, V3_S4 b) { add_a3s4_fp(C_ptr(A3_S4_R, out_a), C_ptr(A3_S4, b)); }
FI_ void sub_v3s4 (V3_S4_R out_a, V3_S4 b) { sub_a3s4 (C_ptr(A3_S4_R, out_a), C_ptr(A3_S4, b)); }
FI_ void sub_v3s4_fp(V3_S4_R out_a, V3_S4 b) { sub_a3s4_fp(C_ptr(A3_S4_R, out_a), C_ptr(A3_S4, b)); }
FI_ void mul_v3s4 (V3_S4_R out_a, V3_S4 b) { mul_a3s4 (C_ptr(A3_S4_R, out_a), C_ptr(A3_S4, b)); }
+12 -33
View File
@@ -18,7 +18,7 @@ I_ U4 align_pow2(U4 x, U4 b) {
#define align_struct(type_width) ((U4)(((type_width) + 3) & ~3)) #define align_struct(type_width) ((U4)(((type_width) + 3) & ~3))
FI_ void mem_bump(U4 cap, U4*R_ used, U4 amount) { FI_ void mem_bump(U4 start, U4 cap, U4*R_ used, U4 amount) {
assert(amount <= (cap - used[0])); assert(amount <= (cap - used[0]));
used[0] += amount; used[0] += amount;
} }
@@ -58,13 +58,13 @@ typedef Struct_(Str8) { UTF8* ptr; U4 len; };
typedef Struct_(Slice_Str8) { Str8* ptr; U4 len; }; typedef Struct_(Slice_Str8) { Str8* ptr; U4 len; };
#define slit(string_literal) (Str8){ (UTF8*) string_literal, S_(string_literal) - 1 } #define slit(string_literal) (Str8){ (UTF8*) string_literal, S_(string_literal) - 1 }
typedef Struct_(Slice) { B1* ptr; U4 len; }; // Untyped Slice (byte-addressable; .len in elements) typedef Struct_(Slice) { U4 ptr, len; }; // Untyped Slice
FI_ Slice slice_ut_(U4 ptr, U4 len) { return (Slice){(B1*)ptr, len}; } FI_ Slice slice_ut_(U4 ptr, U4 len) { return (Slice){ptr, len}; }
#define Slice_(type) Struct_(tmpl(Slice,type)) { type* ptr; U4 len; } #define Slice_(type) Struct_(tmpl(Slice,type)) { type* ptr; U4 len; }
typedef Slice_(B1); typedef Slice_(B1);
#define slice_assert(s) do { assert((s).ptr != 0); assert((s).len > 0); } while(0) #define slice_assert(s) do { assert((s).ptr != 0); assert((s).len > 0); } while(0)
#define slice_end(slice) ((slice).ptr + S_slice(slice) / S_(B1)) /* byte-ptr arithmetic; .len is in elements per slice convention */ #define slice_end(slice) ((slice).ptr + (slice).len)
#define S_slice(s) ((s).len * S_((s).ptr[0])) #define S_slice(s) ((s).len * S_((s).ptr[0]))
#define slice_ut(ptr,len) slice_ut_(u4_(ptr), u4_(len)) #define slice_ut(ptr,len) slice_ut_(u4_(ptr), u4_(len))
@@ -72,30 +72,23 @@ typedef Slice_(B1);
#define slice_to_ut(s) slice_ut_(u4_((s).ptr), S_slice(s)) #define slice_to_ut(s) slice_ut_(u4_((s).ptr), S_slice(s))
#define slice_iter(container, iter) (T_((container).ptr) iter = (container).ptr; iter != slice_end(container); ++ iter) #define slice_iter(container, iter) (T_((container).ptr) iter = (container).ptr; iter != slice_end(container); ++ iter)
#define slice_arg_from_array(type, ...) & (tmpl(Slice,type)) { .ptr = Array_decl(type,__VA_ARGS__), .len = Array_len( Array_decl(type,__VA_ARGS__)) } #define slice_arg_from_array(type, ...) & (tmpl(Slice,type)) { .ptr = array_decl(type,__VA_ARGS__), .len = array_len( array_decl(type,__VA_ARGS__)) }
#define slice_from_array(type, array) (tmpl(Slice,type)) { .ptr = array, .len = Array_len(array) } #define slice_from_array(type, array) (tmpl(Slice,type)) { .ptr = array, .len = S_(array) }
FI_ void slice_zero_(Slice s) { slice_assert(s); mem_zero(u4_(s.ptr), s.len); } FI_ void slice_zero_(Slice s) { slice_assert(s); mem_zero(s.ptr, s.len); }
#define slice_zero(s) slice_zero_(slice_to_ut(s)) #define slice_zero(s) slice_zero_(slice_to_ut(s))
FI_ void slice_copy_(Slice dest, Slice src) { FI_ void slice_copy_(Slice dest, Slice src) {
assert(S_slice(dest) >= S_slice(src)); assert(dest.len >= src.len);
slice_assert(dest); slice_assert(dest);
slice_assert(src); slice_assert(src);
mem_copy(u4_(dest.ptr), u4_(src.ptr), S_slice(src)); mem_copy(dest.ptr, src.ptr, src.len);
} }
#define slice_copy(dest, src) do { \ #define slice_copy(dest, src) do { \
static_assert(T_same(dest, src)); \ static_assert(T_same(dest, src)); \
slice_copy_(slice_to_ut(dest), slice_to_ut(src)); \ slice_copy_(slice_to_ut(dest), slice_to_ut(src)); \
} while(0) } while(0)
FI_ Slice slice_bump(U4_R used, U4 start, U4 len, U4 amount) {
assert(len - used[0] - amount);
U4 ptr = start + used[0]; used[0] += amount;
return slice_ut(ptr, amount);
}
typedef Slice_(U1);
typedef Slice_(U4); typedef Slice_(U4);
#pragma endregion Slice #pragma endregion Slice
@@ -105,19 +98,18 @@ typedef Slice_(U4);
typedef Opt_(farena) { U4 alignment, type_width; }; typedef Opt_(farena) { U4 alignment, type_width; };
typedef Struct_(FArena) { U4 start, capacity, used; }; typedef Struct_(FArena) { U4 start, capacity, used; };
FI_ void farena_init(FArena_R arena, Slice mem) { assert(arena != nullptr); FI_ void farena_init(FArena_R arena, Slice mem) { assert(arena != nullptr);
arena->start = u4_(mem.ptr); arena->start = mem.ptr;
arena->capacity = mem.len; arena->capacity = mem.len;
arena->used = 0; arena->used = 0;
} }
FI_ FArena farena_make(Slice mem) { FArena a; farena_init(& a, mem); return a; } FI_ FArena farena_make(Slice mem) { FArena a; farena_init(& a, mem); return a; }
FI_ Slice farena_bump(FArena_R a, U4 amount) { return slice_bump(& a->used, a->start, a->capacity, amount); }
I_ Slice farena_push(FArena_R arena, U4 amount, Opt_farena o) { I_ Slice farena_push(FArena_R arena, U4 amount, Opt_farena o) {
if (amount == 0) { return (Slice){}; } if (amount == 0) { return (Slice){}; }
U4 desired = amount * (o.type_width == 0 ? 1 : o.type_width); U4 desired = amount * (o.type_width == 0 ? 1 : o.type_width);
U4 to_commit = align_pow2(desired, o.alignment ? o.alignment : MEM_ALIGNMENT_DEFAULT); U4 to_commit = align_pow2(desired, o.alignment ? o.alignment : MEM_ALIGNMENT_DEFAULT);
U4 ptr = arena->start + arena->used; U4 ptr = arena->start + arena->used;
mem_bump(arena->capacity, & arena->used, to_commit); mem_bump(arena->start, arena->capacity, & arena->used, to_commit);
return (Slice){ (B1*)ptr, to_commit }; return (Slice){ ptr, to_commit };
} }
FI_ void farena_reset (FArena_R arena) { arena->used = 0; } FI_ void farena_reset (FArena_R arena) { arena->used = 0; }
FI_ void farena_rewind(FArena_R arena, U4 save_point) { FI_ void farena_rewind(FArena_R arena, U4 save_point) {
@@ -125,21 +117,8 @@ FI_ void farena_rewind(FArena_R arena, U4 save_point) {
arena->used -= save_point - arena->start; arena->used -= save_point - arena->start;
} }
FI_ U4 farena_save(FArena arena) { return arena.used; } FI_ U4 farena_save(FArena arena) { return arena.used; }
FI_ U4 farena_unused_start(FArena arena) { return arena.start + arena.used; }
#define farena_push_(arena, amount, ...) farena_push((arena), (amount), opt_(farena, __VA_ARGS__)) #define farena_push_(arena, amount, ...) farena_push((arena), (amount), opt_(farena, __VA_ARGS__))
#define farena_push_type(arena, type, ...) C_(type*, farena_push((arena), 1, opt_(farena, .type_width=S_(type), __VA_ARGS__)).ptr) #define farena_push_type(arena, type, ...) C_(type*, farena_push((arena), 1, opt_(farena, .type_width=S_(type), __VA_ARGS__)).ptr)
#define farena_push_array(arena, type, amount, ...) (tmpl(Slice,type)){ C_(type*, farena_push((arena), (amount), opt_(farena, .type_width=S_(type), __VA_ARGS__)).ptr), (amount) } #define farena_push_array(arena, type, amount, ...) (tmpl(Slice,type)){ C_(type*, farena_push((arena), (amount), opt_(farena, .type_width=S_(type), __VA_ARGS__)).ptr), (amount) }
#pragma endregion FArena #pragma endregion FArena
#pragma region BIOS Scratchpad
/* BIOS scratchpad location. 1 KB at 0x1F800000.
* TapeHostFrame occupies the final 44 bytes while tape code executes.
* Atom scratch is bounded by the TapeHostFrame_Loc declaration in lottes_tape.h. */
enum {
Scratchpad_Loc = 0x1F800000,
Scratchpad_Len = 0x400, /* 1 KB */
Scratchpad_End = Scratchpad_Loc + Scratchpad_Len, /* 0x1F800400 */
};
#define C_scratch(type) C_(type, Scratchpad_Loc)
#pragma endregion BIOS Scratchpad
+16 -58
View File
@@ -1,53 +1,18 @@
#ifdef INTELLISENSE_DIRECTIVES #ifdef INTELLISENSE_DIRECTIVES
# include "gen/macs.h" # include "gen/macs.h"
# include "gen/offsets.h" # include "gen/offsets.h"
# include "bios.h"
# include "mips.h"
# include "lottes_tape.h" # include "lottes_tape.h"
#endif #endif
ATOM_FILE_DEBUGGER_LINE_MARKER(mips_atom_c); ATOM_FILE_DEBUGGER_LINE_MARKER(mips_atom_c);
#pragma region MACs (Mips Atom Components)
FI_ Slice_MipsCode ac_load_word_imm(AtomBuilder_R ab, Reg dst, U4 imm)
atom_dbg_skip MipsAtomComp_Proc_(ab, {
load_upper_i(dst, u4_hi(imm)),
or_i_self( dst, u4_lo(imm)),
})
FI_ Slice_MipsCode ac_shift_aright_v3_self(AtomBuilder_R ab, Reg dt_x, Reg dt_y, Reg dt_z, U2 shift_amount)
MipsAtomComp_Proc_( ab, {
shift_aright(dt_x, dt_x, shift_amount),
shift_aright(dt_y, dt_y, shift_amount),
shift_aright(dt_z, dt_z, shift_amount),
})
FI_ Slice_MipsCode ac_shift_aright_v3s4_self(AtomBuilder_R ab, Reg_(V3_S4) dt, U2 shift) MipsAtomComp_ProcMap_(ab, mac_shift_aright_v3_self(dt.x, dt.y, dt.z, shift))
FI_ Slice_MipsCode ac_shift_aright_var_v3(AtomBuilder_R ab
, Reg rd_v0, Reg rd_v1, Reg rd_v2
, Reg rs_v0, Reg rs_v1, Reg rs_v2
, Reg r_shift)
MipsAtomComp_Proc_(ab, {
shift_aright_var(rd_v0, rs_v0, r_shift),
shift_aright_var(rd_v1, rs_v1, r_shift),
shift_aright_var(rd_v2, rs_v2, r_shift),
})
FI_ Slice_MipsCode ac_shift_aright_var_v3_self(AtomBuilder_R ab, Reg rds_v0, Reg rds_v1, Reg rds_v2, Reg r_shift)
atom_dbg_skip MipsAtomComp_Proc_(ab, {
shift_aright_var(rds_v0, rds_v0, r_shift),
shift_aright_var(rds_v1, rds_v1, r_shift),
shift_aright_var(rds_v2, rds_v2, r_shift),
})
FI_ Slice_MipsCode ac_shift_aright_var_v3s4_self(AtomBuilder_R ab, Reg_(V3_S4) ds, Reg shift) MipsAtomComp_ProcMap_(ab, mac_shift_aright_var_v3_self(ds.x, ds.y, ds.z, shift))
#pragma endregion MACs (Mips Atom Components)
#pragma region Baked Atoms #pragma region Baked Atoms
enum {
bios_flushcache = 0x44,
bios_table_addr = 0xA0,
};
/* Flushes the Instruction Cache (PSX A-function 0x44 via BIOS stub at 0xA0). /* Flushes the Instruction Cache (PSX A-function 0x44 via BIOS stub at 0xA0).
* Sequence (per MIPS ABI; arguments in arg registers, RA pushed to stack): * Sequence (per MIPS ABI; arguments in arg registers, RA pushed to stack):
* 1. sp -= 8; sw $ra, 4($sp) ; save RA * 1. sp -= 8; sw $ra, 4($sp) ; save RA
@@ -55,26 +20,19 @@ FI_ Slice_MipsCode ac_shift_aright_var_v3s4_self(AtomBuilder_R ab, Reg_(V3_S4) d
* 3. $t0 = bios_table_addr ; t0 = &BIOS A-function table * 3. $t0 = bios_table_addr ; t0 = &BIOS A-function table
* 4. jalr $t0, $ra ; call BIOS(flushcache) * 4. jalr $t0, $ra ; call BIOS(flushcache)
* nop ; branch delay slot * nop ; branch delay slot
* 5. lw $ra, 4($sp) * 5. lw $ra, 4($sp); jr $ra ; restore & return
* 6. sp += 8 ; load-delay * 6. sp += 8
* 7. jr $ra
* nop ; BD
*/ */
#if 0
// Note: Can't do this without having a way to do C-Runtime frame call from Tape ABI.
// Don't support this without adjusting scratchpad to save tape frame in some way.
internal MipsAtom_(mips_flush_icache) { internal MipsAtom_(mips_flush_icache) {
add_ui(R_SP, R_SP, -MipsStackAlignment), // sp -= 8 add_ui(rstack_ptr, rstack_ptr, -MipsStackAlignment), // sp -= 8
store_word(R_RA, R_SP, S_(U4)), // sw $ra, 4($sp) store_word(rret_addr, rstack_ptr, S_(U4)), // sw $ra, 4($sp)
add_ui(R_V0, R_0, bios_flushcache), // addiu $a0, $0, 0x44 add_ui(rret_0, rdiscard, bios_flushcache), // addiu $a0, $0, 0x44
add_ui(R_T0, R_0, bios_table_addr), // addiu $t0, $0, 0xA0 add_ui(rtmp_0, rdiscard, bios_table_addr), // addiu $t0, $0, 0xA0
jump_link(R_T0, R_RA), nop, // jalr $t0, $ra, BD slot jump_link(rtmp_0, rret_addr), nop, // jalr $t0, $ra, BD slot
load_word(R_RA, R_SP, S_(U4)), // lw $ra, 4($sp) load_word(rret_addr, rstack_ptr, S_(U4)), // lw $ra, 4($sp)
add_ui(R_SP, R_SP, MipsStackAlignment), // sp += 8 (load-delay) jump_reg(rret_addr), // jr $ra
jump_reg(R_RA), nop, // jr $ra, BD slot add_ui(rstack_ptr, rstack_ptr, MipsStackAlignment), // sp += 8 (BD)
// mac_yield(), mac_yield(),
}; };
#endif
#pragma endregion Baked Atoms #pragma endregion Baked Atoms
+56 -66
View File
@@ -136,31 +136,31 @@ enum {
/* Semantic Aliases for MIPS Registers (O32 ABI) */ /* Semantic Aliases for MIPS Registers (O32 ABI) */
// , rdiscard = R_0 /* Hardwired to 0 */ , rdiscard = R_0 /* Hardwired to 0 */
// , rasm_tmp = R_AT /* Assembler temporary (destroyed by some assembler pseudoinstructions!) */ , rasm_tmp = R_AT /* Assembler temporary (destroyed by some assembler pseudoinstructions!) */
// , rret_0 = R_V0 /* Function return value */ , rret_0 = R_V0 /* Function return value */
// , rret_1 = R_V1 /* Second return value (e.g., 64-bit) */ , rret_1 = R_V1 /* Second return value (e.g., 64-bit) */
// , rarg_0 = R_A0 /* First function argument */ , rarg_0 = R_A0 /* First function argument */
// , rarg_1 = R_A1 /* Second function argument */ , rarg_1 = R_A1 /* Second function argument */
// , rarg_2 = R_A2 /* Third function argument */ , rarg_2 = R_A2 /* Third function argument */
// , rarg_3 = R_A3 /* Fourth function argument */ , rarg_3 = R_A3 /* Fourth function argument */
// , rtmp_0 = R_T0 /* Temporary (Caller saved) */ , rtmp_0 = R_T0 /* Temporary (Caller saved) */
// , rtmp_1 = R_T1 /* Temporary (Caller saved) */ , rtmp_1 = R_T1 /* Temporary (Caller saved) */
// , rtmp_2 = R_T2 /* Temporary (Caller saved) */ , rtmp_2 = R_T2 /* Temporary (Caller saved) */
// , rtmp_3 = R_T3 /* Temporary (Caller saved) */ , rtmp_3 = R_T3 /* Temporary (Caller saved) */
// , rtmp_4 = R_T4 /* Temporary (Caller saved) — common GTE base pointer */ , rtmp_4 = R_T4 /* Temporary (Caller saved) — common GTE base pointer */
// , rtmp_9 = R_T9 /* Temporary (Caller saved) — common GTE base pointer */ , rtmp_9 = R_T9 /* Temporary (Caller saved) — common GTE base pointer */
// , rstatic_0 = R_S0 /* Static (Callee saved, preserved across calls) */ , rstatic_0 = R_S0 /* Static (Callee saved, preserved across calls) */
// , rstatic_1 = R_S1 , rstatic_1 = R_S1
// , rstatic_2 = R_S2 , rstatic_2 = R_S2
// , rstatic_3 = R_S3 , rstatic_3 = R_S3
// , rstatic_4 = R_S4 , rstatic_4 = R_S4
// , rstatic_5 = R_S5 , rstatic_5 = R_S5
// , rstatic_6 = R_S6 , rstatic_6 = R_S6
// , rstatic_7 = R_S7 , rstatic_7 = R_S7
// , rsaved_0 = R_S0 /* Alias for rstatic_0 (alternate vocabulary) */ , rsaved_0 = R_S0 /* Alias for rstatic_0 (alternate vocabulary) */
// , rstack_ptr = R_SP /* Stack Pointer */ , rstack_ptr = R_SP /* Stack Pointer */
// , rret_addr = R_RA /* Return Address (populated by JAL) */ , rret_addr = R_RA /* Return Address (populated by JAL) */
/* --- MIPS CPU Opcodes (Bits 31-26) --- */ /* --- MIPS CPU Opcodes (Bits 31-26) --- */
@@ -252,29 +252,29 @@ enum {
enum { _BitOffsets = 0 enum { _BitOffsets = 0
/* Bit Offsets for MIPS Instruction Fields */ /* Bit Offsets for MIPS Instruction Fields */
, OPCODE_POS = 26 , OPCODE_SHIFT = 26
, RS_POS = 21 , RS_SHIFT = 21
, RT_POS = 16 , RT_SHIFT = 16
, RD_POS = 11 , RD_SHIFT = 11
, SHAMT_POS = 6 /* Shift Amount: Offset Position */ , SHAMT_SHIFT = 6 /* Shift Amount */
, FC_POS = 0 , FC_SHIFT = 0
/* IMM_MASK is the 16-bit two's-complement truncation for the immediate field. /* Bit Masks to prevent overflow into adjacent fields */
* It is NOT a range guard — it is load-bearing for negative branch offsets
* (the metaprogram emits raw signed offsets; the mask truncates them to the
* 16-bit representation the hardware expects). The static analysis
* `immediate_field_width` check validates ranges at build time. */
, OPCODE_MASK = 0x3F
, REG_MASK = 0x1F
, SHAMT_MASK = 0x1F /* Shift Amount */
, FC_MASK = 0x3F
, IMM_MASK = 0xFFFF , IMM_MASK = 0xFFFF
}; };
#define enc_op(op) ((op) << OPCODE_POS) #define enc_op(op) (((op) & OPCODE_MASK) << OPCODE_SHIFT)
#define enc_rs(rs) ((rs) << RS_POS) #define enc_rs(rs) (((rs) & REG_MASK) << RS_SHIFT)
#define enc_rt(rt) ((rt) << RT_POS) #define enc_rt(rt) (((rt) & REG_MASK) << RT_SHIFT)
#define enc_rd(rd) ((rd) << RD_POS) #define enc_rd(rd) (((rd) & REG_MASK) << RD_SHIFT)
#define enc_shamt(shamt) ((shamt) << SHAMT_POS) #define enc_shamt(shamt) (((shamt) & SHAMT_MASK) << SHAMT_SHIFT)
#define enc_fc(fc) ((fc) << FC_POS) #define enc_fc(fc) (((fc) & FC_MASK) << FC_SHIFT)
#define enc_imm(imm) ((imm) & IMM_MASK) #define enc_imm(imm) (((imm) & IMM_MASK))
/* MIPS R-Type Instruction Format (Register-to-Register) */ /* MIPS R-Type Instruction Format (Register-to-Register) */
#define enc_r(op, rs, rt, rd, shamt, fc) (enc_op(op) | enc_rs(rs) | enc_rt(rt) | enc_rd(rd) | enc_shamt(shamt) | enc_fc(fc)) #define enc_r(op, rs, rt, rd, shamt, fc) (enc_op(op) | enc_rs(rs) | enc_rt(rt) | enc_rd(rd) | enc_shamt(shamt) | enc_fc(fc))
@@ -318,10 +318,7 @@ enum { _BitOffsets = 0
#define load_half(rt, base, off) enc_i(op_lh, (base), (rt), (off)) #define load_half(rt, base, off) enc_i(op_lh, (base), (rt), (off))
#define load_byte_u(rt, base, off) enc_i(op_lbu, (base), (rt), (off)) #define load_byte_u(rt, base, off) enc_i(op_lbu, (base), (rt), (off))
#define load_half_u(rt, base, off) enc_i(op_lhu, (base), (rt), (off)) #define load_half_u(rt, base, off) enc_i(op_lhu, (base), (rt), (off))
#define LdSlot_
#define store_word(rt, base, off) enc_i(op_sw, (base), (rt), (off)) #define store_word(rt, base, off) enc_i(op_sw, (base), (rt), (off))
#define add_ui(rt, rs, imm) enc_i(op_addiu, (rs), (rt), (imm)) #define add_ui(rt, rs, imm) enc_i(op_addiu, (rs), (rt), (imm))
#define and_i(rt, rs, imm) enc_i(op_andi, (rs), (rt), (imm)) #define and_i(rt, rs, imm) enc_i(op_andi, (rs), (rt), (imm))
// #define and_si and_i // #define and_si and_i
@@ -351,12 +348,6 @@ enum { _BitOffsets = 0
#define shift_lright(rd, rt, shamt) enc_r(op_special, R_0, (rt), (rd), (shamt), fc_srl) #define shift_lright(rd, rt, shamt) enc_r(op_special, R_0, (rt), (rd), (shamt), fc_srl)
#define shift_aright(rd, rt, shamt) enc_r(op_special, R_0, (rt), (rd), (shamt), fc_sra) #define shift_aright(rd, rt, shamt) enc_r(op_special, R_0, (rt), (rd), (shamt), fc_sra)
/* Shift Variable — register-shift forms.
* shift_lleft_var(rd, rt, rs) → sllv rd, rt, rs (shamt in low 5 bits of rs)
* shift_aright_var(rd, rt, rs) → srav rd, rt, rs */
#define shift_lleft_var(rd, rt, rs) enc_r(op_special, (rs), (rt), (rd), 0, fc_sllv)
#define shift_aright_var(rd, rt, rs) enc_r(op_special, (rs), (rt), (rd), 0, fc_srav)
#define shift_lleft_self(rd_rt, shamt) enc_r(op_special, R_0, (rd_rt), (rd_rt), (shamt), fc_sll) #define shift_lleft_self(rd_rt, shamt) enc_r(op_special, R_0, (rd_rt), (rd_rt), (shamt), fc_sll)
#define mask_upper(rd, rt, shamt) shift_lleft(rd, rt, shamt), shift_lright(rd, rt, shamt) #define mask_upper(rd, rt, shamt) shift_lleft(rd, rt, shamt), shift_lright(rd, rt, shamt)
@@ -375,21 +366,20 @@ enum { _BitOffsets = 0
* WARNING: `jump(off)` CANNOT BE USED for within-atom jumps in the current pipeline. * WARNING: `jump(off)` CANNOT BE USED for within-atom jumps in the current pipeline.
* The MIPS j opcode encodes `(target_addr >> 2)` in its 26-bit immediate field; an ABSOLUTE byte address, not a relative word offset. * The MIPS j opcode encodes `(target_addr >> 2)` in its 26-bit immediate field; an ABSOLUTE byte address, not a relative word offset.
* The metaprogram computes `off` as a relative word offset (`target_word_idx - branch_word_idx - 1`), which the assembler/linker does NOT resolve. * The metaprogram computes `off` as a relative word offset (`target_word_idx - branch_word_idx - 1`), which the assembler/linker does NOT resolve.
*
* `jump(off)` is only safe when the BUILD PIPELINE owns the absolute position of the emitted code — i.e. when: s * `jump(off)` is only safe when the BUILD PIPELINE owns the absolute position of the emitted code — i.e. when: s
* - the build emits a symbol-relative `.word` expression that the linker resolvess via `R_MIPS_26`, OR * - the build emits a symbol-relative `.word` expression that the linker resolvess via `R_MIPS_26`, OR
* - the code is hand-assembled with explicit absolute targets, OR a custom post-build patcher resolves the 26-bit field. * - the code is hand-assembled with explicit absolute targets, OR a custom post-build patcher resolves the 26-bit field.
* TODO(Ed): Review this.. technically we can resolve aboslute jumps on baked atoms? (Even proedurally generated ones...)
*/ */
#define jump(off) enc_i(op_j, R_0, R_0, (off)) #define jump(off) enc_i(op_j, R_0, R_0, (off))
// Annotate an instruction as filling a branch-delay slot.
#define BdSlot_
/* jump_rel off — unconditional relative jump (the within-atom-safe `jump`). /* jump_rel off — unconditional relative jump (the within-atom-safe `jump`).
* MIPS I R3000A has no "branch always" opcode. The idiom for an unconditional relative jump is `beq $0, $0, off`. */ * MIPS I R3000A has no "branch always" opcode. The idiom for an unconditional relative jump is `beq $0, $0, off`.
*/
#define jump_rel(off) branch_equal(R_0, R_0, (off)) #define jump_rel(off) branch_equal(R_0, R_0, (off))
/* call_addr off — jump-and-link to immediate address. /* call_addr off — jump-and-link to immediate address.
*
* Same WARNING as `jump(off)` above: the jal opcode also encodes an absolute 26-bit target. * Same WARNING as `jump(off)` above: the jal opcode also encodes an absolute 26-bit target.
* For within-atom calls, the current pipeline has no equivalent always-taken call-and-link idiom. * For within-atom calls, the current pipeline has no equivalent always-taken call-and-link idiom.
* Workaround: `branch_link` (always-taken branch + explicit `la $ra, next_word_addr; jr $ra`), or just use `call_reg($tmp)` after loading the target into a register. * Workaround: `branch_link` (always-taken branch + explicit `la $ra, next_word_addr; jr $ra`), or just use `call_reg($tmp)` after loading the target into a register.
@@ -407,7 +397,13 @@ enum { _BitOffsets = 0
* sub_s / sub_u → sub / subu * sub_s / sub_u → sub / subu
* mult_s / mult_u → mult / multu (writes HI/LO; result in LO) * mult_s / mult_u → mult / multu (writes HI/LO; result in LO)
* div_s / div_u → div / divu (LO = quot, HI = rem) * div_s / div_u → div / divu (LO = quot, HI = rem)
*/ *
* NOTE: dsl.h defines `add_s`/`sub_s`/`mut_s`/`gt_s`/etc. as _Generic-based signed integer-arithmetic helpers for U1/U2/U4.
* Those live in a different conceptual layer (generic arithmetic on DSL types) and would collide with the instruction encoders here.
* The `#undef` below lets the gas-style names below win; if a file needs both, the dsl.h versions can be reached via their long forms
* (e.g. `def_signed_op`-style or the underlying `add_s1/s2/s4`). */
#undef add_s
#undef sub_s
#define add_s(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_add) #define add_s(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_add)
#define add_u(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_addu) #define add_u(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_addu)
#define sub_s(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_sub) #define sub_s(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_sub)
@@ -417,7 +413,6 @@ enum { _BitOffsets = 0
#define div_s(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_div) #define div_s(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_div)
#define div_u(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_divu) #define div_u(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_divu)
// TODO(Ed): Change convention of 'self' to ds for (destination is source)?
#define add_u_self(rd_rs, rt) add_u(rd_rs, rd_rs, rt) #define add_u_self(rd_rs, rt) add_u(rd_rs, rd_rs, rt)
/* --- Arithmetic I-type (immediate) --- */ /* --- Arithmetic I-type (immediate) --- */
@@ -460,13 +455,9 @@ enum { _BitOffsets = 0
#define shift_amount(rd, rt, n) shift_lleft(rd, rt, n) #define shift_amount(rd, rt, n) shift_lleft(rd, rt, n)
/* nop — sll $0, $0, 0 */ /* nop — sll $0, $0, 0 */
#define nop shift_lleft(R_0, R_0, 0) #define nop shift_lleft(rdiscard, rdiscard, 0)
#define nop2 nop, nop #define nop2 nop, nop
// li_s — load signed 16-bit immediate into GPR (addiu rt, $0, imm — sign-extends).
#define li_s(rt, imm) add_ui((rt), R_0, (imm))
// #define load_imm_s(rt, imm) add_ui((rt), R_0, (imm))
#define load_imm_1w(rt, imm) add_ui((rt), R_0, (imm)) #define load_imm_1w(rt, imm) add_ui((rt), R_0, (imm))
#define load_imm_1w_s0(rt, imm) add_si((rt)), R_0, (imm)) #define load_imm_1w_s0(rt, imm) add_si((rt)), R_0, (imm))
@@ -586,7 +577,6 @@ enum { _BitOffsets = 0
, jump_link(rtmp_0, rret_addr) \ , jump_link(rtmp_0, rret_addr) \
, nop \ , nop \
, load_word(rret_addr, rstack_ptr, 4) \ , load_word(rret_addr, rstack_ptr, 4) \
, add_ui(rstack_ptr, rstack_ptr, MipsStackAlignment) \
, jump_reg(rret_addr) \ , jump_reg(rret_addr) \
, nop \ , add_ui(rstack_ptr, rstack_ptr, MipsStackAlignment) \
) asm_clobber: clbr_volatile_gprs ) ) asm_clobber: clbr_volatile_gprs )
+79 -91
View File
@@ -9,34 +9,6 @@
ATOM_FILE_DEBUGGER_LINE_MARKER(pad_atom_c); ATOM_FILE_DEBUGGER_LINE_MARKER(pad_atom_c);
#pragma region MACs (Mips Atom Components)
FI_ Slice_MipsCode ac_pad_set_centered_axes(AtomBuilder_R ab, Reg state, Reg scratch) atom_dbg_skip MipsAtomComp_Proc_(ab, {
load_upper_i(scratch, (PadAxis_Centered >> 16) & 0xFFFF),
or_i_self( scratch, PadAxis_Centered & 0xFFFF), // mac_load_word_imm(scratch, PadAxis_Centered),
store_word( scratch, state, O_(PadState,axes)),
})
FI_ Slice_MipsCode ac_pad_set_id_byte(AtomBuilder_R ab, Reg state, Reg r_id, U1 id_value) atom_dbg_skip MipsAtomComp_Proc_(ab, {
add_ui( r_id, R_0, id_value),
store_byte(r_id, state, O_(PadState,id)),
})
FI_ Slice_MipsCode ac_pad_set_status(AtomBuilder_R ab, U4 r_tmp, U1 r_state, U4 pad_status) atom_dbg_skip MipsAtomComp_Proc_(ab, {
add_ui( r_tmp, R_0, pad_status),
store_word(r_tmp, r_state, O_(PadState,status)),
})
/* Invert r_buttons (active-low → active-high) and store to PadState.buttons.
* r_buttons must already be loaded (the caller is responsible for filling the load-delay slot of
* the preceding load_half_u with an instruction that doesn't read r_buttons). */
FI_ Slice_MipsCode ac_pad_store_inverted_buttons(AtomBuilder_R ab, U1 r_buttons, U1 r_pad_state) atom_dbg_skip MipsAtomComp_Proc_(ab, {
nor_u( r_buttons, r_buttons, R_0),
store_half(r_buttons, r_pad_state, O_(PadState,buttons)),
})
#pragma endregion MACs (Mips Atom Components)
#pragma region Baked Atoms #pragma region Baked Atoms
/* ----- pad_bios_snapshot ----- /* ----- pad_bios_snapshot -----
@@ -54,16 +26,16 @@ FI_ Slice_MipsCode ac_pad_store_inverted_buttons(AtomBuilder_R ab, U1 r_buttons,
* byte_swap16(x) = (x >> 8) | (x << 8); nor(x, R_0) = ~x. store_half truncates to 16 bits so the upper-16 mask is implicit in the store. * byte_swap16(x) = (x >> 8) | (x << 8); nor(x, R_0) = ~x. store_half truncates to 16 bits so the upper-16 mask is implicit in the store.
* *
* Register use (atom-local; no wave-context touched): * Register use (atom-local; no wave-context touched):
* R_T0 = raw base : Kept throughout; axes loads read raw[4..7] from R_T0. * R_T0 = raw base (kept throughout; axes loads read raw[4..7] from R_T0)
* R_T1 = state base : Kept throughout; all stores go through R_T1. * R_T1 = state base (kept throughout; all stores go through R_T1)
* R_T2 = raw[0] status : Alive across the disc/pending/id dispatch, then dead. * R_T2 = raw[0] status (alive across the disc/pending/id dispatch, then dead)
* R_T3 = raw[1] id : Alive across the id dispatch, then dead. * R_T3 = raw[1] id (alive across the id dispatch, then dead)
* R_T4 = scratch : Shifts, compares, immediate loads, store values. * R_T4 = scratch (shifts, compares, immediate loads, store values)
* R_T5 = scratch : Parallel lui + ori for the 0x80808080 axes constant + byte-swap target. * R_T5 = scratch (parallel lui+ori for the 0x80808080 axes constant + byte-swap target)
*/ */
enum { enum {
R_PadRaw = R_T0 atom_reg atom_type(U1), R_PadRaw = R_T0 atom_reg atom_type(U1),
R_PadState = R_T1 atom_reg atom_type(PadState*), R_PadState = R_T1 atom_reg,
R_RawStatus = R_T2 atom_reg, R_RawStatus = R_T2 atom_reg,
R_RawId = R_T3 atom_reg, R_RawId = R_T3 atom_reg,
}; };
@@ -72,8 +44,8 @@ typedef Struct_(Binds_PadBiosSnapshot) {
PadState* state; PadState* state;
}; };
internal MipsAtom_(pad_bios_snapshot) atom_info(atom_bind(Binds_PadBiosSnapshot) internal MipsAtom_(pad_bios_snapshot) atom_info(atom_bind(Binds_PadBiosSnapshot)
, atom_reads( R_PadRaw, R_PadState, R_RawStatus, R_RawId) , atom_reads( R_PadRaw, R_PadState, R_RawStatus, R_RawId, R_T4, R_T5, R_TapePtr)
, atom_writes(R_PadRaw, R_PadState, R_RawStatus, R_RawId) , atom_writes(R_PadRaw, R_PadState, R_RawStatus, R_RawId, R_T4, R_T5, R_TapePtr)
) { ) {
/* === Bind consumption: T0 = raw, T1 = state, advance R_TapePtr by 8. */ /* === Bind consumption: T0 = raw, T1 = state, advance R_TapePtr by 8. */
load_word(R_PadRaw, R_TapePtr, O_(Binds_PadBiosSnapshot,raw)), load_word(R_PadRaw, R_TapePtr, O_(Binds_PadBiosSnapshot,raw)),
@@ -81,98 +53,111 @@ internal MipsAtom_(pad_bios_snapshot) atom_info(atom_bind(Binds_PadBiosSnapshot)
add_ui_self( R_TapePtr, S_(Binds_PadBiosSnapshot)), add_ui_self( R_TapePtr, S_(Binds_PadBiosSnapshot)),
/* === Read raw[0] (status) + raw[1] (id) */ /* === Read raw[0] (status) + raw[1] (id) */
load_byte_u(R_RawStatus, R_PadRaw, O_(PadBiosRaw,status)), load_byte_u(R_RawStatus, R_PadRaw, 0),
load_byte_u(R_RawId, R_PadRaw, O_(PadBiosRaw,id)), load_byte_u(R_RawId, R_PadRaw, 1),
atom_label(snap_root) /* === Case 1: Disconnected (status == 0xFF). */ atom_label(snap_root) /* === Case 1: Disconnected (status == 0xFF). */
add_ui(R_T4, R_0, PadRawStatus_Timeout), branch_ne(R_RawStatus, R_T4, atom_offset(snap_root, skip_disconnected)), add_ui(R_T4, R_0, 0xFF), branch_ne(R_RawStatus, R_T4, atom_offset(snap_root, skip_disconnected)),
/* BD-slot: pre-compute PadStatus_Disconnected. Branch reads R_T4=0xFF in EX before this WB completes. /* BD-slot: pre-compute PadStatus_Disconnected. Branch reads R_T4=0xFF in EX before this WB completes.
* If branch NOT taken (fall through to pending/id_dispatch), R_T4 is overwritten by the next case body's add_ui — harmless. */ * If branch NOT taken (fall through to pending/id_dispatch), R_T4 is overwritten by the next case body's add_ui — harmless. */
atom_label(disconnected) /* === Disconnected body. */ atom_label(disconnected) /* === Disconnected body. */
mac_pad_set_status(R_T4, R_PadState, PadStatus_Disconnected), /* R_T4 = PadStatus_Disconnected from snap_root BD-slot. */
store_half( R_0, R_PadState, O_(PadState,buttons)), store_word(R_T4, R_PadState, O_(PadState,status)),
mac_pad_set_centered_axes(R_PadState, R_T4), store_half(R_0, R_PadState, O_(PadState,buttons)),
mac_pad_set_id_byte(R_PadState, R_RawId, PadRawStatus_Timeout), /* axes = 0x80808080 (centered) — single sw writes the 4-byte axes block at offset 8 (left_x, left_y, right_x, right_y). */
load_upper_i(R_T4, 0x8080), or_i_self(R_T4, 0x8080),
store_word( R_T4, R_PadState, O_(PadState,left_x)),
store_byte( R_RawId, R_PadState, O_(PadState,id)),
jump_rel(atom_offset(disconnected, snap_end)), jump_rel(atom_offset(disconnected, snap_end)),
/* BD-slot: load next atom's entry point (replaces the nop). /* BD-slot: load next atom's entry point (replaces the nop).
* Always jumps to snap_end, where mac_yield_tail() transfers control to R_AtomJmp without re-loading it. */ * The unconditional branch always jumps to snap_end, where mac_yield_tail()
* transfers control to R_AtomJmp without re-loading it. */
mac_yield_load(), mac_yield_load(),
atom_label(skip_disconnected) atom_label(skip_disconnected)
/* === Case 2: Pending (status == 0 && id == 0) /* === Case 2: Pending (status == 0 && id == 0)
* Combined check: if (status | id) != 0 then skip to id_dispatch. Falls through to the Pending case only when both are zero. */ * Combined check: if (status | id) != 0 then skip to id_dispatch.
* Falls through to the Pending case only when both are zero. */
or_u_self(R_RawStatus, R_RawId), branch_ne(R_RawStatus, R_0, atom_offset(case_2, id_dispatch)), or_u_self(R_RawStatus, R_RawId), branch_ne(R_RawStatus, R_0, atom_offset(case_2, id_dispatch)),
/* BD-slot: pre-compute PadStatus_Pending. Branch reads R_RawStatus in EX before this WB completes. /* BD-slot: pre-compute PadStatus_Pending. Branch reads R_RawStatus in EX before this WB completes.
* If branch NOT taken (fall through to id_dispatch), R_T4 is overwritten by the digital/analog body add_ui - harmless. */ * If branch NOT taken (fall through to id_dispatch), R_T4 is overwritten by the digital/analog body add_ui harmless. */
atom_label(pending) /* === Pending body (status=0, id=0 — pre-IRQ-empty buffer). */ atom_label(pending) /* === Pending body */
mac_pad_set_status(R_T4, R_PadState, PadStatus_Pending), /* R_T4 = PadStatus_Pending from case_2 BD-slot. */
store_half( R_0, R_PadState, O_(PadState,buttons)), store_word(R_T4, R_PadState, O_(PadState,status)),
mac_pad_set_centered_axes(R_PadState, R_T4), store_half(R_0, R_PadState, O_(PadState,buttons)),
store_byte(R_RawId, R_PadState, O_(PadState,id)), /* axes = 0x80808080 (centered) — single sw writes the 4-byte axes block at offset 8 (left_x, left_y, right_x, right_y). */
load_upper_i(R_T4, 0x8080), or_i_self(R_T4, 0x8080),
store_word( R_T4, R_PadState, O_(PadState,left_x)),
store_byte( R_RawId, R_PadState, O_(PadState,id)),
jump_rel(atom_offset(pending, snap_end)), jump_rel(atom_offset(pending, snap_end)),
mac_yield_load(), mac_yield_load(),
atom_label(id_dispatch) /* === Case 3-6: ID dispatch */ atom_label(id_dispatch) /* === Case 3-6: ID dispatch */
add_ui(R_T4, R_0, PadRawId_Digital), branch_ne(R_RawId, R_T4, atom_offset(id_dispatch, try_analog_stick)), add_ui(R_T4, R_0, 0x41), branch_ne(R_RawId, R_T4, atom_offset(id_dispatch, try_analog_stick)),
/* BD-slot: pre-compute PadStatus_Digital. Branch reads R_RawId in EX before this WB completes. /* BD-slot: pre-compute PadStatus_Digital. Branch reads R_RawId in EX before this WB completes.
* If branch NOT taken (fall through to try_analog_stick), R_T4 is overwritten by the analog body add_ui. */ * If branch NOT taken (fall through to try_analog_stick), R_T4 is overwritten by the analog body add_ui. */
/* === Digital body (status, buttons normalize, axes=0x80, id, branch. /* === Digital body (status, buttons normalize, axes=0x80, id, branch. */
* R_T5 holds the 0x80808080 axes constant (loaded into the load-delay slot of the buttons-load). /* R_T4 = PadStatus_Digital from id_dispatch BD-slot. */
* R_T5 is then "dead" — only consumed at the analog_pad range check downstream. */ store_word( R_T4, R_PadState, O_(PadState,status)),
mac_pad_set_status(R_T4, R_PadState, PadStatus_Digital), load_half_u(R_T4, R_PadRaw, 2 * S_(U1)),
load_half_u( R_T4, R_PadRaw, O_(PadBiosRaw, buttons)), /* R_T4 = raw_buttons; */ /* Fill R_T4's load-delay slot with the 0x80808080 axes constant into R_T5
mac_load_word_imm( R_T5, PadAxis_Centered), /* fills the buttons-load's delay slot (doesn't read R_T4) */ * (R_T5 is dead on this path; it's only consumed at the analog_pad range check). */
// load_upper_i(R_T5, PadAxis_Centered_Hi), or_i_self(R_T5, PadAxis_Centered_Lo), load_upper_i(R_T5, 0x8080), or_i_self(R_T5, 0x8080),
mac_pad_store_inverted_buttons(R_T4, R_PadState), /* R_T4 settled: nor + sh writes ~raw_buttons to state.buttons */ nor_u( R_T4, R_T4, R_0), /* raw_buttons is already in host bit order; no swap needed */
store_word(R_T5, R_PadState, O_(PadState, axes)), /* single sw writes the 4-byte axes block at offset 8 (left_x, left_y, right_x, right_y) */ store_half( R_T4, R_PadState, O_(PadState,buttons)),
mac_pad_set_id_byte(R_PadState, R_T4, PadRawId_Digital),
/* axes = 0x80808080 (centered) — single sw writes the 4-byte axes block at offset 8 (left_x, left_y, right_x, right_y). */
store_word( R_T5, R_PadState, O_(PadState,left_x)),
add_ui( R_T4, R_0, 0x41),
store_byte( R_T4, R_PadState, O_(PadState,id)),
jump_rel(atom_offset(id_dispatch, snap_end)), jump_rel(atom_offset(id_dispatch, snap_end)),
mac_yield_load(), mac_yield_load(),
atom_label(try_analog_stick) /* === Case 4: AnalogStick (id == 0x53)*/ atom_label(try_analog_stick) /* === Case 4: AnalogStick (id == 0x53)*/
add_ui(R_T4, R_0, PadRawId_AnalogStick), branch_ne(R_RawId, R_T4, atom_offset(try_analog_stick, try_analog_pad)), add_ui(R_T4, R_0, 0x53), branch_ne(R_RawId, R_T4, atom_offset(try_analog_stick, try_analog_pad)),
/* BD-slot: pre-compute PadStatus_AnalogStick. Branch reads R_RawId in EX before this WB completes. /* BD-slot: pre-compute PadStatus_AnalogStick. Branch reads R_RawId in EX before this WB completes.
* If branch NOT taken (fall through to try_analog_pad), R_T4 is overwritten by the analog_pad body add_ui. */ * If branch NOT taken (fall through to try_analog_pad), R_T4 is overwritten by the analog_pad body add_ui. */
atom_label(analog_stick) /* === AnalogStick body atom_label(analog_stick) /* === AnalogStick body
* R_T5 holds left_xy (loaded into the load-delay slot of the buttons-load via the left-axis load_half_u). * Axes are loaded as two halfwords: raw[6..7] → left_xy (sh at offset 8), raw[4..5] → right_xy (sh at offset 10).
* R_T4 holds right_xy (loaded into the load-delay slot of the left-load). * R_T5 holds left_xy / id-value in turn (it's dead on this path — only consumed at the analog_pad range check). */
* R_T5 is then "dead" — reused for the id-byte value load in mac_pad_write_id_byte. /* R_T4 = PadStatus_AnalogStick from try_analog_stick BD-slot. */
* The buttons invert+store happens BEFORE R_T4 is overwritten by the right_xy load. */ store_word( R_T4, R_PadState, O_(PadState,status)),
mac_pad_set_status(R_T4, R_PadState, PadStatus_AnalogStick), load_half_u( R_T4, R_PadRaw, 2 * S_(U1)), /* R_T4 = raw_buttons */
load_half_u( R_T4, R_PadRaw, O_(PadBiosRaw,buttons)), /* R_T4 = raw_buttons; delay slot at the next instruction */ load_half_u( R_T5, R_PadRaw, 6 * S_(U1)), /* R_T5 = left_xy; fills R_T4's load-delay slot (doesn't read R_T4) */
load_half_u( R_T5, R_PadRaw, O_(PadBiosRaw,left)), /* fills the buttons-load's delay slot (doesn't read R_T4) */ nor_u( R_T4, R_T4, R_0), /* R_T4 = ~raw_buttons */
mac_pad_store_inverted_buttons(R_T4, R_PadState), /* R_T4 settled: nor + sh writes ~raw_buttons to state.buttons */ store_half( R_T4, R_PadState, O_(PadState,buttons)),
load_half_u( R_T4, R_PadRaw, O_(PadBiosRaw,right)), /* fills R_T5's load-delay slot (doesn't read R_T5); overwrites R_T4 (was buttons) with right_xy */ load_half_u( R_T4, R_PadRaw, 4 * S_(U1)), /* R_T4 = right_xy; fills R_T5's load-delay slot */
store_half( R_T5, R_PadState, O_(PadState, left)), store_half( R_T5, R_PadState, O_(PadState,left_x)), /* R_T5 settled, store left_xy */
store_half( R_T4, R_PadState, O_(PadState, right)), store_half( R_T4, R_PadState, O_(PadState,right_x)),
mac_pad_set_id_byte(R_PadState, R_T5, PadRawId_AnalogStick), add_ui( R_T5, R_0, 0x53), /* R_T5 = id value (clobbers left_xy, already stored) */
store_byte( R_T5, R_PadState, O_(PadState,id)),
jump_rel(atom_offset(analog_stick, snap_end)), jump_rel(atom_offset(analog_stick, snap_end)),
mac_yield_load(), mac_yield_load(),
atom_label(try_analog_pad) /* === Case 5-6: AnalogPad (id & 0xF0 == 0x70) */ atom_label(try_analog_pad) /* === Case 5-6: AnalogPad (id & 0xF0 == 0x70) */
and_i( R_T4, R_RawId, PadRawId_AnalogPadMask), and_i( R_T4, R_RawId, 0xF0),
add_ui( R_T5, R_0, PadRawId_AnalogPadValue), add_ui( R_T5, R_0, 0x70),
branch_ne(R_T4, R_T5, atom_offset(try_analog_pad, try_unsupported)), branch_ne(R_T4, R_T5, atom_offset(try_analog_pad, try_unsupported)),
/* BD-slot: pre-compute PadStatus_AnalogPad. Branch reads R_T4 in EX before this WB completes. /* BD-slot: pre-compute PadStatus_AnalogPad. Branch reads R_T4 in EX before this WB completes.
* If branch NOT taken (fall through to try_unsupported), R_T4 is overwritten by the unsupported body add_ui. */ * If branch NOT taken (fall through to try_unsupported), R_T4 is overwritten by the unsupported body add_ui. */
atom_label(analog_pad) /* === AnalogPad body atom_label(analog_pad) /* === AnalogPad body
* Same shape as AnalogStick with AnalogPad status. R_T5 holds left_xy (it's dead on this path). * Same shape as AnalogStick with AnalogPad status. R_T5 holds left_xy (it's dead on this path). */
* The id byte is raw id from the BIOS buffer (R_RawId already holds raw[1]). /* R_T4 = PadStatus_AnalogPad from try_analog_pad BD-slot. */
* Buttons invert + store happens before R_T4 is overwritten by the right_xy load. */ store_word( R_T4, R_PadState, O_(PadState,status)),
mac_pad_set_status(R_T4, R_PadState, PadStatus_AnalogPad), load_half_u(R_T4, R_PadRaw, 2 * S_(U1)), /* R_T4 = raw_buttons */
load_half_u( R_T4, R_PadRaw, O_(PadBiosRaw,buttons)), /* R_T4 = raw_buttons; delay slot at the next instruction */ load_half_u(R_T5, R_PadRaw, 6 * S_(U1)), /* R_T5 = left_xy; fills R_T4's load-delay slot */
load_half_u( R_T5, R_PadRaw, O_(PadBiosRaw,left)), /* fills the buttons-load's delay slot (doesn't read R_T4) */ nor_u( R_T4, R_T4, R_0), /* R_T4 = ~raw_buttons */
mac_pad_store_inverted_buttons(R_T4, R_PadState), /* R_T4 settled: nor + sh writes ~raw_buttons to state.buttons */ store_half( R_T4, R_PadState, O_(PadState,buttons)),
load_half_u(R_T4, R_PadRaw, O_(PadBiosRaw,right)), /* fills R_T5's load-delay slot (doesn't read R_T5); overwrites R_T4 with right_xy */ load_half_u(R_T4, R_PadRaw, 4 * S_(U1)), /* R_T4 = right_xy; fills R_T5's load-delay slot */
store_half( R_T5, R_PadState, O_(PadState, left)), store_half( R_T5, R_PadState, O_(PadState,left_x)), /* R_T5 settled, store left_xy */
store_half( R_T4, R_PadState, O_(PadState, right)), store_half( R_T4, R_PadState, O_(PadState,right_x)),
store_byte( R_RawId, R_PadState, O_(PadState, id)), store_byte( R_RawId, R_PadState, O_(PadState,id)),
jump_rel(atom_offset(analog_pad, snap_end)), jump_rel(atom_offset(analog_pad, snap_end)),
mac_yield_load(), mac_yield_load(),
@@ -181,8 +166,11 @@ atom_label(try_unsupported) /* === Case 7: Unsupported — fall through from the
add_ui( R_T4, R_0, PadStatus_Unsupported), add_ui( R_T4, R_0, PadStatus_Unsupported),
store_word(R_T4, R_PadState, O_(PadState,status)), store_word(R_T4, R_PadState, O_(PadState,status)),
store_half(R_0, R_PadState, O_(PadState,buttons)), store_half(R_0, R_PadState, O_(PadState,buttons)),
mac_pad_set_centered_axes(R_PadState, R_T4), /* axes = 0x80808080 (centered) — single sw writes the 4-byte axes block at offset 8 (left_x, left_y, right_x, right_y). */
mac_pad_set_id_byte(R_PadState, R_RawId, PadUnknownId_Sentinel), load_upper_i(R_T4, 0x8080), or_i_self(R_T4, 0x8080),
store_word( R_T4, R_PadState, O_(PadState,left_x)),
add_ui( R_T4, R_0, 0xFF), /* 0xFF sentinel: "unknown id" */
store_byte( R_T4, R_PadState, O_(PadState,id)),
/* Fall through to snap_end. */ /* Fall through to snap_end. */
atom_label(no_jump_fallthrough) atom_label(no_jump_fallthrough)
-78
View File
@@ -1,78 +0,0 @@
#ifdef INTELLISENSE_DIRECTIVES
# include "dsl.h"
# include "gcc_asm.h"
# include "mips.h"
# include "bios.h"
# include "pad.h"
#endif
/* Uses ONE 8-byte frame allocated via the compiler's standard prologue.
* 4 wasted-arg words for B(12h) InitPAD2 are at [SP+0..15] but are not explicitly allocated.
* Compiler handles the MIPS O32 "wasted stack" convention for us by treating the B-call as a 4-arg call.
*
* The buffer pointers are passed as arguments so the compiler keeps them in callee-saved registers;
* The B(12h) asm volatile block does NOT clobber those registers (it clobbers only the volatile GPRs + B-table arg registers explicitly).
* The C-level writes after the call re-load the pointers from their callee-saved homes.
*
* The clobber list for both B-calls names the full BIOS destroy set documented in kernelbios.md:167-174 (R1..R15, R24..R25, R31, HI/LO).
* The kernel-ABI "volatile GPRs" subset is clb_mem_drain; the rest of the destroy set is enumerated explicitly here. */
NI_ void pad_bios_init_start(PadBiosRaw* raw0, PadBiosRaw* raw1)
{
/* Pin raw0 + raw1 to $a0 + $a1 via rgcc; the B(12h) call uses these directly.
* The `(void)` casts mark them as unread after the call so the compiler doesn't need to move them back. */
register PadBiosRaw* p0 rgcc(R_A0) = raw0;
register PadBiosRaw* p1 rgcc(R_A1) = raw1;
(void)p0; (void)p1;
// TODO(Ed): Properly annotate the raw values in the inline asm instructions.
// Use enums.
/* B(12h) InitPAD2(raw0, 0x22, raw1, 0x22)
* $a0 = raw0 (rgcc-bound; survives the sequence below)
* $a1 = raw1 (preserved into $a2 before $a1 is overwritten)
* $a2 = raw1 (moved from $a1; survives $a1's overwrite)
* $a3 = 0x22 (immediate)
* $t1 = 0x12 (function number)
* $t2 = 0xB0 (BIOS B-table address) */
asm volatile(
asm_words(
or_u( R_A2, R_A1, R_0), /* $a2 = $a1 = raw1 */
add_ui( R_A1, R_0, bios_pad_buffer_size), /* $a1 = 0x22 */
add_ui( R_A3, R_0, bios_pad_buffer_size), /* $a3 = 0x22 */
add_ui( R_T1, R_0, bios_init_pad_2), /* $t1 = 0x12 */
add_ui( R_T2, R_0, bios_btable_addr), /* $t2 = 0xB0 */
call_reg(R_T2), /* jalr $t2, $ra */
nop /* BD slot */
)
asm_rpins, r_use(p0), r_use(p1)
asm_clobber:
rlit(R_AT),
rlit(R_V0), rlit(R_V1),
rlit(R_T0), rlit(R_T1), rlit(R_T2), rlit(R_T3), rlit(R_T4),
rlit(R_T5), rlit(R_T6), rlit(R_T7), rlit(R_T8), rlit(R_T9),
rlit(R_RA),
clb_mem_drain
);
/* The C-level writes re-load the pointers via the parameter names and write 0xFF to each
* buffer's status byte to mark the initial-state hazard documented in kernelbios.md:1621-1624. */
u1_v(raw0)[0] = 0xFF;
u1_v(raw1)[0] = 0xFF;
/* B(13h) StartPAD2() — no args. The BIOS preserves $sp. */
asm volatile(
asm_words(
add_ui( R_T1, R_0, bios_start_pad_2), /* $t1 = 0x13 */
add_ui( R_T2, R_0, bios_btable_addr), /* $t2 = 0xB0 (re-load) */
call_reg(R_T2), /* jalr $t2, $ra */
nop /* BD slot */
)
asm_clobber:
rlit(R_AT),
rlit(R_V0), rlit(R_V1),
rlit(R_T0), rlit(R_T1), rlit(R_T2), rlit(R_T3), rlit(R_T4),
rlit(R_T5), rlit(R_T6), rlit(R_T7), rlit(R_T8), rlit(R_T9),
rlit(R_RA),
clb_mem_drain
);
}
+23 -58
View File
@@ -1,11 +1,12 @@
#ifdef INTELLISENSE_DIRECTIVES #ifdef INTELLISENSE_DIRECTIVES
# pragma once # pragma once
# include "dsl.h" # include "dsl.h"
# include "math.h"
#endif #endif
// PSX button bit positions: PSX-SPX docs/psx-spx/docs/controllersandmemorycards.md:405-421. /* PSX button bit positions — 1:1 with PSX-SPX docs at docs/psx-spx/docs/controllersandmemorycards.md:405-421.
typedef Enum_(U2, PadBtns) { * Wire is active-low (0 = pressed).
* The decoder atom computes buttons = (~raw_buttons) & 0xFFFF; the active-low-to-active-high inversion is applied bit-by-bit. */
enum {
Bit_(Pad_Select, 0), Bit_(Pad_Select, 0),
Bit_(Pad_L3, 1), Bit_(Pad_L3, 1),
Bit_(Pad_R3, 2), Bit_(Pad_R3, 2),
@@ -31,22 +32,18 @@ enum {
Pad1 = 1 << PadId_Offset, Pad1 = 1 << PadId_Offset,
}; };
/* ============================================================================= #define pad0_(btn_id) (btn_id << Pad0)
#define pad1_(btn_id) (btn_id << Pad1)
/* ============================================================
* BIOS pad-buffer subsystem: docs/psx-spx/docs/kernelbios.md (B(12h) + B(13h)) * BIOS pad-buffer subsystem: docs/psx-spx/docs/kernelbios.md (B(12h) + B(13h))
* ============================================================================= */ * ============================================================ */
enum { enum {
PAD_BIOS_RAW_SIZE = 0x22, PAD_BIOS_RAW_SIZE = 0x22,
}; };
// BIOS pad buffer layout (docs/psx-spx/docs/kernelbios.md (InitPAD2 returns 0x22 = 34 bytes per port)).
// Bytes 0..7 are the named snapshot region; bytes 8..33 are reserved (the BIOS writes the buffer raw; we only read bytes 0..7 via O_(PadBiosRaw, ...)).
typedef Struct_(PadBiosRaw) { typedef Struct_(PadBiosRaw) {
U1 status; /* offset 0 (PadRawStatus_Ok / PadRawStatus_Timeout) */ U1 bytes[PAD_BIOS_RAW_SIZE];
U1 id; /* offset 1 (PadRawId_Digital / PadRawId_AnalogStick / 0x7x AnalogPad) */
U2 buttons; /* offset 2-3 (active-low 16-bit button map) */
V2_U1 right; /* offset 4-5 (right stick x, y) */
V2_U1 left; /* offset 6-7 (left stick x, y) */
U1 reserved[PAD_BIOS_RAW_SIZE - 8]; /* offset 8..33 */
}; };
typedef Enum_(U4, PadStatus) { typedef Enum_(U4, PadStatus) {
@@ -59,50 +56,18 @@ typedef Enum_(U4, PadStatus) {
PadStatus_Invalid, PadStatus_Invalid,
}; };
// Distinct from the game-facing PadStatus enum: PadRawStatus_Ok and PadRawStatus_Timeout are raw BIOS values /* PadState — per-port normalized runtime state.
typedef Enum_(U1, PadRawStatus) { * Field order is chosen so that the 4 axes (left_x, left_y, right_x, right_y)
PadRawStatus_Ok = 0x00, * form a contiguous 4-byte block at offset 8, allowing a single `store_word` to clear-or-write all 4 axes in one MIPS instruction.
PadRawStatus_Timeout = 0xFF, * The struct size stays 12 bytes (unchanged from the prior order,
}; * which left the C compiler to insert 1 byte of trailing pad to reach the 4-byte struct alignment). */
typedef Enum_(U1, PadRawId) {
PadRawId_Digital = 0x41,
PadRawId_AnalogStick = 0x53,
PadRawId_AnalogPadMask = 0xF0,
PadRawId_AnalogPadValue = 0x70,
};
typedef Enum_(U1, PadUnknownId) {
PadUnknownId_Sentinel = 0xFF,
};
typedef Enum_(U4, PadAxisCentered) {
PadAxis_Centered_Hi = 0x8080,
PadAxis_Centered_Lo = 0x8080,
PadAxis_Centered = 0x80808080U,
};
typedef Enum_(U1, PadDeadZone) {
PadDeadZone_LowBound = 0x70, /* left_x < LowBound → active; delta = 0x80 - left_x > 0 (rightward pull) */
PadDeadZone_Center = 0x80, /* analog rest position; left_x == Center → delta = 0 (no rotation) */
PadDeadZone_HighBound = 0x90, /* left_x > HighBound → active; delta = 0x80 - left_x < 0 (leftward pull) */
};
typedef Struct_(PadAxes) {
V2_U1 left; /* offset 8-9 */
V2_U1 right; /* offset 10-11 */
};
// Field order is chosen so that the 4 axes (left_x, left_y, right_x, right_y)
// form a contiguous 4-byte block at offset 8, allowing a single `store_word` to clear-or-write all 4 axes in one MIPS instruction.
typedef Struct_(PadState) { typedef Struct_(PadState) {
PadStatus status; /* offset 0, (U4) */ PadStatus status; /* offset 0, size 4 (U4) */
PadBtns buttons; /* offset 4, */ U2 buttons; /* offset 4, size 2 */
U1 id; /* offset 6, */ U1 id; /* offset 6, size 1 */
byte_pad(1); /* offset 7, explicit pad to align the axes block */ U1 pad; /* offset 7, size 1 — explicit pad to align the axes block */
union { U1 left_x; /* offset 8, size 1 — store_word target (4-byte aligned) */
A2_V2_U1 axes; /* offset 8-11 store_target (4-byte aligned)*/ U1 left_y; /* offset 9, size 1 */
struct { U1 right_x; /* offset 10, size 1 */
V2_U1 left; /* offset 8-9 */ U1 right_y; /* offset 11, size 1 */
V2_U1 right; /* offset 10-11 */
};
};
}; };
internal void pad_bios_init_start(PadBiosRaw* raw0, PadBiosRaw* raw1);
+5 -23
View File
@@ -64,9 +64,9 @@ typedef Struct_(Tile) {
Linear Algebra Linear Algebra
*/ */
MT3_S2S4* mt3s2s4_rotation (V3_S2* vec, MT3_S2S4* mat) asm("RotMatrix"); M3_S2* m3s2_rotation (V3_S2* vec, M3_S2* mat) asm("RotMatrix");
MT3_S2S4* mt3s2s4_translation(MT3_S2S4* mat, V3_S4* vec) asm("TransMatrix"); M3_S2* m3s2_translation(M3_S2* mat, V3_S4* vec) asm("TransMatrix");
MT3_S2S4* mt3s2s4_scale (MT3_S2S4* mat, V3_S4* vec) asm("ScaleMatrix"); M3_S2* m3s2_scale (M3_S2* mat, V3_S4* vec) asm("ScaleMatrix");
// Rotation, Translation, Perspective // Rotation, Translation, Perspective
@@ -99,23 +99,5 @@ FI_ S4 rtp_avg_nclip_a4_v3s2(
); );
} }
void gte_matrix_set_rotation (MT3_S2S4* mat) asm("SetRotMatrix"); void gte_matrix_set_rotation (M3_S2* mat) asm("SetRotMatrix");
void gte_matrix_set_translation(MT3_S2S4* mat) asm("SetTransMatrix"); void gte_matrix_set_translation(M3_S2* mat) asm("SetTransMatrix");
// Einheit, Metrication to unit vector. "Normalization", not Orthogonal "Normal, Normalis". Directionalization.
// RGA(Lengyel): Normalize the bulk of a zero-weight direction. This is not finite-point unitization (which forces w=1).
S4 psy_normalize_v3s4(V3_S4* v0, V3_S4* v1) asm("VectorNormal");
// RGA(Lengyel): Apply the matrix expansion of a rigid transformation.
// Motor antiproduct is equivalent for unitized points; LA form is what GTE consumes.
V3_S4* mul_m3s2_v3s4(MT3_S2S4* m, V3_S4* v, V3_S4* result) asm("ApplyMatrixLV");
// RGA(Lengyel): Store the full translation column. The motor translator would store half this displacement in m.xyz.
MT3_S2S4* trans_m3s2(MT3_S2S4* m, V3_S4* off) asm("TransMatrix");
MT3_S2S4* gte_comp_coord_m3s2(MT3_S2S4* m0, MT3_S2S4* m1, MT3_S2S4* result) asm("CompMatrixLV");
// RGA(Lengyel): Complement(Wedge(a,b)), i.e. the Euclidean 3D complement of the exterior product, stored as a V3_S4.
// The underlying GTE OP is a specialized signed-16-bit D x IR command; the wedge interpretation is a 3D dual of the same 3 scalars.
void cross_v3s4(V3_S4* v0, V3_S4* v1, V3_S4* result) asm("OuterProduct12");
-12
View File
@@ -15,8 +15,6 @@
#define WORD_COUNT(name, count) enum { words_##name = (count) }; #define WORD_COUNT(name, count) enum { words_##name = (count) };
WORD_COUNT(nop, 1) WORD_COUNT(nop, 1)
WORD_COUNT(atom_label, 0)
WORD_COUNT(atom_offset, 0)
WORD_COUNT(load_upper_i, 1) WORD_COUNT(load_upper_i, 1)
WORD_COUNT(jump_reg, 1) WORD_COUNT(jump_reg, 1)
WORD_COUNT(jump_link, 1) WORD_COUNT(jump_link, 1)
@@ -55,17 +53,7 @@ WORD_COUNT(gte_mv_to_ctrl_r, 1)
WORD_COUNT(gte_sw, 1) WORD_COUNT(gte_sw, 1)
WORD_COUNT(gte_cmdw_rtpt, 1) WORD_COUNT(gte_cmdw_rtpt, 1)
WORD_COUNT(gte_cmdw_nclip, 1) WORD_COUNT(gte_cmdw_nclip, 1)
WORD_COUNT(gte_cmdw_op, 1)
WORD_COUNT(gte_avg_sort_z3, 1) WORD_COUNT(gte_avg_sort_z3, 1)
WORD_COUNT(gte_cmdw_sqr, 1)
WORD_COUNT(gte_cmdw_gpf, 1)
WORD_COUNT(shift_lleft_var, 1)
WORD_COUNT(shift_aright_var, 1)
WORD_COUNT(li_s, 1)
WORD_COUNT(and_i, 1)
WORD_COUNT(add_si, 1)
WORD_COUNT(branch_lt_zero, 1)
WORD_COUNT(sub_s, 1)
WORD_COUNT(sub_u, 1) WORD_COUNT(sub_u, 1)
WORD_COUNT(nop2, 2) WORD_COUNT(nop2, 2)
-13
View File
@@ -1,13 +0,0 @@
#ifdef INTELLISENSE_DIRECTIVES
#pragma once
#endif
// Auto-generated by ps1_meta.lua (passes/auto_reg.lua) — DO NOT EDIT
// Directory: C:\projects\Pikuma\ps1\code\hello_camera
// source: C:/projects/Pikuma/ps1/code/hello_camera/hello_camera.c
// source: C:/projects/Pikuma/ps1/code/hello_camera/hello_camera.h
// source: C:/projects/Pikuma/ps1/code/hello_camera/hello_camera.atom.c
// Per-phase register allocations resolved by the lua pass.
// R_<Sym>_Code = <chosen GPR's _Code constant> for every marker in this directory.
#define R_GpTmp_Code R_V0_Code
+3 -21
View File
@@ -8,7 +8,7 @@
#pragma region hello_camera #pragma region hello_camera
// --- atom: pad_input_cube_rotation (60 words) --- // --- atom: pad_apply_input (60 words) ---
#define _atom_offset_dpad_left_exit_dpad_left 6 #define _atom_offset_dpad_left_exit_dpad_left 6
#define _atom_offset_dpad_right_exit_dpad_right 6 #define _atom_offset_dpad_right_exit_dpad_right 6
@@ -26,25 +26,7 @@ enum {
atom_offset_end_low_exit_stick = _atom_offset_end_low_exit_stick, atom_offset_end_low_exit_stick = _atom_offset_end_low_exit_stick,
}; };
// --- atom: pad_input_cam (39 words) --- // --- atom: cube_g4_face (76 words) ---
#define _atom_offset_left_x_exit_left_x 3
#define _atom_offset_right_x_exit_right_x 3
#define _atom_offset_up_y_exit_up_y 3
#define _atom_offset_down_y_exit_down_y 3
#define _atom_offset_cross_z_exit_cross_z 3
#define _atom_offset_circle_z_exit_circle_z 3
enum {
atom_offset_left_x_exit_left_x = _atom_offset_left_x_exit_left_x,
atom_offset_right_x_exit_right_x = _atom_offset_right_x_exit_right_x,
atom_offset_up_y_exit_up_y = _atom_offset_up_y_exit_up_y,
atom_offset_down_y_exit_down_y = _atom_offset_down_y_exit_down_y,
atom_offset_cross_z_exit_cross_z = _atom_offset_cross_z_exit_cross_z,
atom_offset_circle_z_exit_circle_z = _atom_offset_circle_z_exit_circle_z,
};
// --- atom: cube_g4_face (73 words) ---
#define _atom_offset_cull_cube_g4_face_exit 41 #define _atom_offset_cull_cube_g4_face_exit 41
#define _atom_offset_bounds_chk_cube_g4_face_exit 24 #define _atom_offset_bounds_chk_cube_g4_face_exit 24
@@ -54,7 +36,7 @@ enum {
atom_offset_bounds_chk_cube_g4_face_exit = _atom_offset_bounds_chk_cube_g4_face_exit, atom_offset_bounds_chk_cube_g4_face_exit = _atom_offset_bounds_chk_cube_g4_face_exit,
}; };
// --- atom: floor_f3_face (56 words) --- // --- atom: floor_f3_face (58 words) ---
#define _atom_offset_culling_floor_f3_face_exit 25 #define _atom_offset_culling_floor_f3_face_exit 25
#define _atom_offset_bounds_chk_floor_f3_face_exit 16 #define _atom_offset_bounds_chk_floor_f3_face_exit 16
+111 -286
View File
@@ -10,14 +10,13 @@
# include "duffle/pad.h" # include "duffle/pad.h"
# include "duffle/word_count.metadata.h" # include "duffle/word_count.metadata.h"
# include "duffle/psyq.h" # include "duffle/psyq.h"
# include "duffle/math.atom.h" # include "duffle/math.atom.c"
# include "duffle/mips.atom.c" # include "duffle/mips.atom.c"
# include "duffle/gte.atom.c" # include "duffle/gte.atom.c"
# include "duffle/gp.atom.c" # include "duffle/gp.atom.c"
# include "duffle/psyq.atom.c" # include "duffle/psyq.atom.c"
# include "gen/offsets.h" # include "gen/offsets.h"
# include "gen/macs.h" # include "gen/macs.h"
# include "gen/auto_reg.h"
# include "hello_camera.h" # include "hello_camera.h"
#endif #endif
@@ -25,8 +24,8 @@ ATOM_FILE_DEBUGGER_LINE_MARKER(hello_joypad_atom_c);
#pragma region MACs (Mips Atom components) #pragma region MACs (Mips Atom components)
FI_ Slice_MipsCode ac_put_disp_env(AtomBuilder_R ab, U4 reg_transfer, U4 reg_base, U2 port) FI_ Slice_MipsCode ac_put_disp_env(U4 reg_transfer, U4 reg_base, U2 port)
MipsAtomComp_Proc_(ab, { MipsAtomComp_Proc_(ac_put_disp_env, {
// Emits 5 GP0 commands for buffer 0 (display_area = (0,0,320,240)). // Emits 5 GP0 commands for buffer 0 (display_area = (0,0,320,240)).
// Sequence per libpsyx PutDispEnv: DrawArea TL → DrawArea BR → Mask → DrawArea TL → DrawArea BR // Sequence per libpsyx PutDispEnv: DrawArea TL → DrawArea BR → Mask → DrawArea TL → DrawArea BR
mac_gcmd_push(gp0_word_draw_area_top_left_origin, reg_transfer, reg_base, port), mac_gcmd_push(gp0_word_draw_area_top_left_origin, reg_transfer, reg_base, port),
@@ -36,8 +35,8 @@ MipsAtomComp_Proc_(ab, {
mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port), mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port),
}) })
I_ Slice_MipsCode ac_put_draw_env(AtomBuilder_R ab, U4 reg_transfer, U4 reg_base, U2 port) FI_ Slice_MipsCode ac_put_draw_env(U4 reg_transfer, U4 reg_base, U2 port)
MipsAtomComp_Proc_(ab, { MipsAtomComp_Proc_(ac_put_draw_env, {
/* /*
* ORIGIN: each code word corresponds to the EXACT value libpsyx's PutDrawEnv function would compute for the same DrawEnv settings. * ORIGIN: each code word corresponds to the EXACT value libpsyx's PutDrawEnv function would compute for the same DrawEnv settings.
* References: * References:
@@ -91,142 +90,6 @@ MipsAtomComp_Proc_(ab, {
#pragma endregion MACs #pragma endregion MACs
#pragma region Atom Procs
#pragma region resolve_look_at
/* ─── resolve_look_at bundle chain atoms ──────────────────────────── */
typedef AtomBundle_(resolve_look_at) { MipsAtom
*input_and_sub,
*normalize_fwd_uz,
*cross_to_right,
*normalize_right_ux,
*cross_to_up,
*normalize_up_uy,
*populate_mt3s4s2;
};
typedef Struct_(ResolveLookAtScratch) {
V3_S4 fwd;
V3_S4 uz;
V3_S4 right;
V3_S4 ux;
V3_S4 up;
V3_S4 uy;
P3_S4 eye;
P3_S4 target;
V3_S4 up_in;
};
typedef Struct_(Binds_ResolveLookAtSub) {
P3_S4* target;
P3_S4* eye;
V3_S4* up_in;
};
typedef Struct_(RegUse_resolve_look_at_input_and_sub) {
Reg target_ptr;
Reg eye_ptr;
Reg up_in_ptr;
union { Reg_(V3_S4) r012, up_in, eye; };
union { Reg_(V3_S4) r345, target, fwd; };
};
/* Atom 0 in the bundle: input_and_sub. Stages C-side inputs into the scratchpad and computes fwd = target - eye. */
internal MipsAtom* AtomBundleEntry_(resolve_look_at,input_and_sub)(AtomArena_R aa, RegUse_resolve_look_at_input_and_sub r)
atom_info(atom_bind(Binds_ResolveLookAtSub)) MipsAtom_Proc_(aa, {
load_word(r.target_ptr, R_TapePtr, O_(Binds_ResolveLookAtSub,target)),
load_word(r.eye_ptr, R_TapePtr, O_(Binds_ResolveLookAtSub,eye)),
load_word(r.up_in_ptr, R_TapePtr, O_(Binds_ResolveLookAtSub,up_in)),
LdSlot_ add_ui_self( R_TapePtr, S_(Binds_ResolveLookAtSub)),
/* Stage up_in.x/y/z into the scratchpad. R_ScratchBase = R_SP = 0x1F800000. */
mac_load_v3s4( r.up_in, r.up_in_ptr, 0), LdSlot_
mac_store_v3s4(r.up_in, R_ScratchBase, O_(ResolveLookAtScratch,up_in)),
// Stage eye.x/y/z into the scratchpad (atom 6 reads these for the translation column).
mac_load_v3s4( r.eye, r.eye_ptr, 0), LdSlot_
mac_store_v3s4(r.eye, R_ScratchBase, O_(ResolveLookAtScratch,eye)),
/* Compute fwd = target - eye. */
mac_load_v3s4( r.target, r.target_ptr, 0), LdSlot_
mac_sub_v3s4_self(r.fwd, r.eye),
mac_store_v3s4( r.fwd, R_ScratchBase, O_(ResolveLookAtScratch,fwd)),
mac_yield()
})
typedef Struct_(Binds_ResolveLookAt_PopulateMT3S4S2) {
MT3_S2S4* look_at; /* MT3_S2S4* — destination matrix address */
};
typedef Struct_(RegUse_resolve_look_at_populate_mt3s4s2) {
Reg look_at;
Reg eye; /* matrix_vector phase: load -eye */
Reg_(V3_S4) row; /* populate phase: load ux/uy/uz */
union { Reg r0, ux, vx; }; /* populate addr → matrix_vector v_x */
union { Reg r1, uy, vy; }; /* populate uy → matrix_vector v_y */
union { Reg r2, uz, vz; }; /* populate uz → matrix_vector v_z */
};
/* write look_at->m[][] from ux/uy/uz as packed S2 (populate),
* ctc2 RT chain into C2[0..4] (matrix_vector), MVMVA RT*(-eye)>>12, store off
* directly to look_at->t[] (trans_matrix).
*
* C11 ApplyMatrixLV semantics (gte.atom.c ac_apply_matrix_lv; libgte reference):
* 1. ctc2 RT matrix (5 ctc2s to C2[0..4])
* 2. lw -eye from memory
* 3. S15 decomposition (eliminated here — the fused body takes the >>12 path
* directly via mtc2 IR + MVMVA pass2, matching the libgte canonical output)
* 4. mtc2 to IR1/2/3, nop2, MVMVA pass2 (sf=1, mx=0, v=3, cv=3)
* 5. mfc2 MACs → off
* 6. store off to look_at->t[] (skip scratch.eye intermediate)
*/
internal MipsAtom* AtomBundleEntry_(resolve_look_at,populate_mt3s4s2)(AtomArena_R aa, RegUse_resolve_look_at_populate_mt3s4s2 r)
atom_info(atom_bind(Binds_ResolveLookAt_PopulateMT3S4S2)) MipsAtom_Proc_(aa, {
/* --- Tape pop: look_at pointer --- */
load_word(r.look_at, R_TapePtr, O_(Binds_ResolveLookAt_PopulateMT3S4S2,look_at)),
LdSlot_ add_ui_self( R_TapePtr, S_(Binds_ResolveLookAt_PopulateMT3S4S2)),
add_si(r.ux, R_ScratchBase, O_(ResolveLookAtScratch, ux)), LdSlot_
add_si(r.uy, R_ScratchBase, O_(ResolveLookAtScratch, uy)),
add_si(r.uz, R_ScratchBase, O_(ResolveLookAtScratch, uz)),
add_si(r.eye, R_ScratchBase, O_(ResolveLookAtScratch, eye)),
/* write look_at->m[][] from ux/uy/uz as packed S2 */
mac_load_v3s4(r.row, r.ux, 0), LdSlot_ mac_store_v3s2(r.row, r.look_at, O_(MT3_S2S4, m[0])),
mac_load_v3s4(r.row, r.uy, 0), LdSlot_ mac_store_v3s2(r.row, r.look_at, O_(MT3_S2S4, m[1])),
mac_load_v3s4(r.row, r.uz, 0), LdSlot_ mac_store_v3s2(r.row, r.look_at, O_(MT3_S2S4, m[2])),
/* ctc2 RT chain + MVMVA RT * (-eye) >> 12 */
/* C2[0] = (RT12<<16)|RT11 ← ctc2 RT11 from m[0][0..1]
* C2[1] = (RT21<<16)|RT13 ← ctc2 RT12 from m[0][2..3]
* C2[2] = (RT23<<16)|RT22 ← ctc2 RT13 from m[1][1..2]
* C2[3] = (RT32<<16)|RT31 ← ctc2 RT21 from m[2][0..1]
* C2[4] = (RT33<<16)|junk ← ctc2 RT22 from m[2][2] (half) */
load_word( r.vx, r.look_at, O_(MT3_S2S4, m[0][0])), /* RT11|RT12 */ LdSlot_
load_word( r.vy, r.look_at, O_(MT3_S2S4, m[0][2])), /* RT13|RT21 */ LdSlot_ gte_mv_to_ctrl_r(r.vx, gte_cr_RT11),
load_word( r.vz, r.look_at, O_(MT3_S2S4, m[1][1])), /* RT22|RT23 */ LdSlot_ gte_mv_to_ctrl_r(r.vy, gte_cr_RT12),
load_word( r.vx, r.look_at, O_(MT3_S2S4, m[2][0])), /* RT31|RT32 */ LdSlot_ gte_mv_to_ctrl_r(r.vz, gte_cr_RT13),
load_half_u(r.vy, r.look_at, O_(MT3_S2S4, m[2][2])), /* RT33 */ LdSlot_ gte_mv_to_ctrl_r(r.vx, gte_cr_RT21),
GteDelay_ mac_load_word_v3(r.vx, r.vy, r.vz, r.eye, 0), LdSlot_
mac_sub_s_v3(r.vx, r.vy, r.vz, R_0, R_0, R_0, r.vx, r.vy, r.vz),
gte_mv_to_data_r(r.vx, C2_IR1),
gte_mv_to_data_r(r.vy, C2_IR2),
gte_mv_to_data_r(r.vz, C2_IR3),
GteDelay_ nop2,
/* MVMVA pass 2 — C11 ApplyMatrixLV command. sf=1, mx=0 (RT), v=3 (IR), cv=3. Reads RT × IR >> 12. */
gte_cmdw_mvmva_c11_pass2, GteDelay_ load_word(R_AtomJmp, R_TapePtr, 0), // ac_yield: word 1
mac_gte_mv_from_data_r_mac123(r.vx, r.vy, r.vz), GteDelay_ add_ui_self( R_TapePtr, S_(MipsCode)), // ac_yield: word 2
/* store off directly to look_at->t[] (skip scratch.eye intermediate) */
mac_store_word_v3(r.vx, r.vy, r.vz, r.look_at, O_(MT3_S2S4, t)),
jump_reg(R_AtomJmp), BdSlot_ nop, // ac_yield: word 3-4
})
#pragma endregion resolve_look_at
#pragma endregion Atom Procs
#pragma region Baked Atoms #pragma region Baked Atoms
enum { enum {
@@ -242,84 +105,101 @@ internal MipsAtom_(screen_env_init) atom_info(atom_phase(screen_init)
) { ) {
/* display[0] = (0, 0, 320, 240); rest of struct zeroed. */ /* display[0] = (0, 0, 320, 240); rest of struct zeroed. */
add_ui(R_ScreenX, R_0, ScreenRes_X), add_ui(R_ScreenY, R_0, ScreenRes_Y), add_ui(R_ScreenX, R_0, ScreenRes_X), add_ui(R_ScreenY, R_0, ScreenRes_Y),
mac_store_v2s2(R_ScreenX, R_ScreenY, R_ScreenBuf, O_(DisplayEnv,display_area.width) + O_(DoubleBuffer,display[0])), mac_store_v2s2(R_ScreenX, R_ScreenY, R_ScreenBuf, O_(DisplayEnv,display_area.width) + OA_(DoubleBuffer,display,0)),
store_word(R_0, R_ScreenBuf, O_(DisplayEnv,display_area) + O_(DoubleBuffer,display[0])), store_word(R_0, R_ScreenBuf, O_(DisplayEnv,display_area) + OA_(DoubleBuffer,display,0)),
store_word(R_0, R_ScreenBuf, O_(DisplayEnv,screen) + O_(DoubleBuffer,display[0])), store_word(R_0, R_ScreenBuf, O_(DisplayEnv,screen) + OA_(DoubleBuffer,display,0)),
store_word(R_0, R_ScreenBuf, O_(DisplayEnv,vinterlace) + O_(DoubleBuffer,display[0])), store_word(R_0, R_ScreenBuf, O_(DisplayEnv,vinterlace) + OA_(DoubleBuffer,display,0)),
/* display[1] = (0, 240, 320, 240); rest of struct zeroed. */ /* display[1] = (0, 240, 320, 240); rest of struct zeroed. */
mac_store_rects2(R_0, R_ScreenY, R_ScreenX, R_ScreenY, R_ScreenBuf, O_(DisplayEnv,display_area) + O_(DoubleBuffer,display[1])), mac_store_rects2(R_0, R_ScreenY, R_ScreenX, R_ScreenY, R_ScreenBuf, O_(DisplayEnv,display_area) + OA_(DoubleBuffer,display,1)),
store_word(R_0, R_ScreenBuf, O_(DisplayEnv,screen) + O_(DoubleBuffer,display[1])), store_word(R_0, R_ScreenBuf, O_(DisplayEnv,screen) + OA_(DoubleBuffer,display,1)),
store_word(R_0, R_ScreenBuf, O_(DisplayEnv,vinterlace) + O_(DoubleBuffer,display[1])), store_word(R_0, R_ScreenBuf, O_(DisplayEnv,vinterlace) + OA_(DoubleBuffer,display,1)),
mac_store_rects2(R_0, R_ScreenY, R_ScreenX, R_ScreenY, R_ScreenBuf, O_(DrawEnv,clip_area) + O_(DoubleBuffer,draw[0])), /* draw[0].clip_area = (0, 240, 320, 240). C11's SetDefDrawEnv writes clip.y = y_arg. */ mac_store_rects2(R_0, R_ScreenY, R_ScreenX, R_ScreenY, R_ScreenBuf, O_(DrawEnv,clip_area) + OA_(DoubleBuffer,draw,0)), /* draw[0].clip_area = (0, 240, 320, 240). C11's SetDefDrawEnv writes clip.y = y_arg. */
mac_store_v2s2( R_0, R_ScreenY, R_ScreenBuf, O_(DrawEnv,drawing_offset[0]) + O_(DoubleBuffer,draw[0])), /* draw[0].drawing_offset[0] = (0, 240); C11 passes y_arg as ofs. */ mac_store_v2s2( R_0, R_ScreenY, R_ScreenBuf, O_(DrawEnv,drawing_offset[0]) + OA_(DoubleBuffer,draw,0)), /* draw[0].drawing_offset[0] = (0, 240); C11 passes y_arg as ofs. */
mac_store_v2s2(R_ScreenX, R_ScreenY, R_ScreenBuf, O_(DrawEnv,clip_area.width) + O_(DoubleBuffer,draw[1])), mac_store_v2s2(R_ScreenX, R_ScreenY, R_ScreenBuf, O_(DrawEnv,clip_area.width) + OA_(DoubleBuffer,draw,1)),
/* draw[0].texture_window = (0, 0, 0, 0); two word-zeroes cover the full 8-byte tw field. */ /* draw[0].texture_window = (0, 0, 0, 0); two word-zeroes cover the full 8-byte tw field. */
store_word(R_0, R_ScreenBuf, O_(DrawEnv,texture_window.x) + O_(DoubleBuffer,draw[0])), store_word(R_0, R_ScreenBuf, O_(DrawEnv,texture_window.x) + OA_(DoubleBuffer,draw,0)),
store_word(R_0, R_ScreenBuf, O_(DrawEnv,texture_window.width) + O_(DoubleBuffer,draw[0])), store_word(R_0, R_ScreenBuf, O_(DrawEnv,texture_window.width) + OA_(DoubleBuffer,draw,0)),
store_word(R_0, R_ScreenBuf, O_(DrawEnv,drawing_offset[0].x) + O_(DoubleBuffer,draw[1])), store_word(R_0, R_ScreenBuf, O_(DrawEnv,drawing_offset[0].x) + OA_(DoubleBuffer,draw,1)),
store_word(R_0, R_ScreenBuf, O_(DrawEnv,texture_window.x) + O_(DoubleBuffer,draw[1])), store_word(R_0, R_ScreenBuf, O_(DrawEnv,texture_window.x) + OA_(DoubleBuffer,draw,1)),
store_word(R_0, R_ScreenBuf, O_(DrawEnv,texture_window.width) + O_(DoubleBuffer,draw[1])), store_word(R_0, R_ScreenBuf, O_(DrawEnv,texture_window.width) + OA_(DoubleBuffer,draw,1)),
/* draw[0].texture_page = 10 (gp0_tpage_default). C11 SetDefDrawEnv at C11_only.elf:0x8001273C writes the same 0x0A. . */ /* draw[0].texture_page = 10 (gp0_tpage_default). C11 SetDefDrawEnv at C11_only.elf:0x8001273C writes the same 0x0A. . */
add_ui(R_T0, R_0, gp0_tpage_default), add_ui(R_T0, R_0, gp0_tpage_default),
store_half(R_T0, R_ScreenBuf, O_(DrawEnv,texture_page) + O_(DoubleBuffer,draw[0])), store_half(R_T0, R_ScreenBuf, O_(DrawEnv,texture_page) + OA_(DoubleBuffer,draw,0)),
store_half(R_T0, R_ScreenBuf, O_(DrawEnv,texture_page) + O_(DoubleBuffer,draw[1])), store_half(R_T0, R_ScreenBuf, O_(DrawEnv,texture_page) + OA_(DoubleBuffer,draw,1)),
/* draw[0] control bytes: flag_dither=1, flag_draw_on_display=1 (the dfe bit per psx-spx; libpsyx sets it via `SetDefDrawEnv`'s conditional at C11_only.elf:0x80012728), enable_auto_clear=1. Each byte is named; /* draw[0] control bytes: flag_dither=1, flag_draw_on_display=1 (the dfe bit per psx-spx; libpsyx sets it via `SetDefDrawEnv`'s conditional at C11_only.elf:0x80012728), enable_auto_clear=1. Each byte is named;
* the previous `store_word(R_0, ..., +20)` overwrote all four with zero. */ * the previous `store_word(R_0, ..., +20)` overwrote all four with zero. */
add_ui(R_T0, R_0, 1), add_ui(R_T0, R_0, 1),
store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,flag_dither) + O_(DoubleBuffer,draw[0])), store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,flag_dither) + OA_(DoubleBuffer,draw,0)),
store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,flag_draw_on_display) + O_(DoubleBuffer,draw[0])), store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,flag_draw_on_display) + OA_(DoubleBuffer,draw,0)),
store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,enable_auto_clear) + O_(DoubleBuffer,draw[0])), store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,enable_auto_clear) + OA_(DoubleBuffer,draw,0)),
store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,flag_dither) + O_(DoubleBuffer,draw[1])), store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,flag_dither) + OA_(DoubleBuffer,draw,1)),
store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,flag_draw_on_display) + O_(DoubleBuffer,draw[1])), store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,flag_draw_on_display) + OA_(DoubleBuffer,draw,1)),
store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,enable_auto_clear) + O_(DoubleBuffer,draw[1])), store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,enable_auto_clear) + OA_(DoubleBuffer,draw,1)),
/* draw[0].initial_bg_color = (r=7, g=7, b=7). */ /* draw[0].initial_bg_color = (r=7, g=7, b=7). */
add_ui(R_T0, R_0, 7), add_ui(R_T0, R_0, 7),
mac_store_rgb8(R_T0,R_T0,R_T0, R_ScreenBuf, O_(DrawEnv,initial_bg_color) + O_(DoubleBuffer,draw[0])), mac_store_rgb8(R_T0,R_T0,R_T0, R_ScreenBuf, O_(DrawEnv,initial_bg_color) + OA_(DoubleBuffer,draw,0)),
mac_store_rgb8(R_T0,R_T0,R_T0, R_ScreenBuf, O_(DrawEnv,initial_bg_color) + O_(DoubleBuffer,draw[1])), mac_store_rgb8(R_T0,R_T0,R_T0, R_ScreenBuf, O_(DrawEnv,initial_bg_color) + OA_(DoubleBuffer,draw,1)),
mac_yield(), mac_yield(),
}; };
enum { enum {
R_IO_BaseAddr = R_T4 atom_reg, /* Caller-pinned: IO_BASE_ADDR = 0x1F800000 */ R_IO_BaseAddr = R_T4 atom_reg, /* Caller-pinned: IO_BASE_ADDR = 0x1F800000 */
R_GP1_Offset = R_T2 atom_reg, /* Caller-pinned: GPIO_PORT1_OFFSET = 0x10 */
atom_auto_reg(gp_screen_init, R_GpTmp), /* Auto-allocated scratch; resolved to a free pool GPR by the lua pass. C-preprocessor expands to R_GpTmp = R_GpTmp_Code with an atom_auto_reg trailing comment. */
#define R_IO_BaseAddr_Code R_T4_Code #define R_IO_BaseAddr_Code R_T4_Code
#define R_GP1_Offset_Code R_T2_Code
}; };
internal MipsAtom_(gp_screen_init) atom_info(atom_phase(screen_init), atom_reads(R_IO_BaseAddr)) { internal MipsAtom_(gp_screen_init) atom_info(atom_phase(screen_init), atom_reads(R_IO_BaseAddr)) {
store_word(R_0, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(00h) Reset */ store_word(R_0, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(00h) Reset */
mac_gcmd_push(gp1_word_ResetCmdBuffer(), R_ScreenX, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(01h) ClearFIFO; uses pinned R_ScreenX as the transfer reg. */ mac_gcmd_push(gp1_word_ResetCmdBuffer(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(01h) ClearFIFO */
mac_gcmd_push(gp1_word_AcknowledgeIRQ(), R_ScreenX, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(02h) AckIRQ; uses pinned R_ScreenX as the transfer reg. */ mac_gcmd_push(gp1_word_AcknowledgeIRQ(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(02h) AckIRQ */
mac_gcmd_push(gp1_word_DisplayOn(), R_ScreenX, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(03h) Display ON; uses pinned R_ScreenX as the transfer reg. */ mac_gcmd_push(gp1_word_DisplayOn(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(03h) Display ON */
mac_gcmd_push(gp1_word_dma_to_gpu(), R_GpTmp, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(04h) DMADirection=2 (CPU->GPU). libpsyx's per-frame PutDrawEnv/DrawOTag use DMA2; without this the DMA queue never drains. Uses auto-allocated R_GpTmp. */ mac_gcmd_push(gp1_word_dma_to_gpu(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(04h) DMADirection=2 (CPUGPU). libpsyx's per-frame PutDrawEnv/DrawOTag use DMA2; without this the DMA queue never drains. */
mac_gcmd_push(gp1_word_StartDisplayArea(), R_GpTmp, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(05h) StartDisplayArea (X=0, Y=0); uses auto-allocated R_GpTmp. */ mac_gcmd_push(gp1_word_StartDisplayArea(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(05h) StartDisplayArea (X=0, Y=0) */
/* GP1: DisplayMode + Display Ranges. */ /* GP1: DisplayMode + Display Ranges */
mac_gcmd_push(gp1_word_display_mode_320x240_15bit_ntsc, R_ScreenX, R_IO_BaseAddr, GPIO_PORT1_OFFSET), mac_gcmd_push(gp1_word_display_mode_320x240_15bit_ntsc, R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET),
mac_gcmd_push(gp1_word_horizontal_range_ntsc, R_ScreenX, R_IO_BaseAddr, GPIO_PORT1_OFFSET), mac_gcmd_push(gp1_word_horizontal_range_ntsc, R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET),
mac_gcmd_push(gp1_word_vertical_range_ntsc, R_ScreenX, R_IO_BaseAddr, GPIO_PORT1_OFFSET), mac_gcmd_push(gp1_word_vertical_range_ntsc, R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET),
/* GTE: SetGeomOffset (OFX, OFY) — ScreenRes_CenterX, ScreenRes_CenterY. */ /* GTE: SetGeomOffset (OFX, OFY) — ScreenRes_CenterX, ScreenRes_CenterY. */
load_upper_i(R_ScreenX, ScreenRes_CenterX), gte_mv_to_ctrl_r(R_ScreenX, gte_cr_OFX_Code), load_upper_i(R_T5, ScreenRes_CenterX), gte_mv_to_ctrl_r(R_T5, gte_cr_OFX_Code),
load_upper_i(R_ScreenX, ScreenRes_CenterY), gte_mv_to_ctrl_r(R_ScreenX, gte_cr_OFY_Code), load_upper_i(R_T5, ScreenRes_CenterY), gte_mv_to_ctrl_r(R_T5, gte_cr_OFY_Code),
/* GTE: SetGeomScreen (H) — CR26 (per PSX-SPX / libpsyx), value is the raw projection-plane distance, NOT shifted. */ /* GTE: SetGeomScreen (H) — CR26 (per PSX-SPX / libpsyx), value is the raw projection-plane distance, NOT shifted. */
add_ui(R_ScreenX, R_0, ScreenZ), gte_mv_to_ctrl_r(R_ScreenX, gte_cr_H_Code), add_ui(R_T5, R_0, ScreenZ), gte_mv_to_ctrl_r(R_T5, gte_cr_H_Code),
/* GP1: DisplayEnable — bit 0 = 0 (Display ON). */ /* GP1: DisplayEnable — bit 0 = 0 (Display ON). */
mac_gcmd_push(gp1_word_DisplayOn(), R_GpTmp, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* Uses auto-allocated R_GpTmp. */ mac_gcmd_push(gp1_word_DisplayOn(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET),
mac_yield(), mac_yield(),
}; };
/* ----- pad_apply_input -----
* Reads pad[0].buttons + pad[0].left_x;
* Applies the input-semantics deltas to cube_rot.y + floor_rot.y:
* - D-pad Left: cube_rot.y += 30, floor_rot.y += 5
* - D-pad Right: cube_rot.y -= 30, floor_rot.y -= 5
* - Analog stick X (dead zone 0x70..0x90):
* cube delta = (0x80 - left_x) >> 2 (range approx -32..+32)
* floor delta = (0x80 - left_x) >> 5 (range approx -4..+4)
* - D-pad + analog deltas add when used together.
*
* Convention:
* pad_state = 0 means no buttons active.
* The fail-safe zero-button value flows through unchanged, so a disconnected/fresh pad produces no rotation.
* The branch_le_zero pattern below matches the existing pad_input_demo convention (atom body lines 248/257).
*
* Signed-delta trick:
* load_byte_u zero-extends left_x to 32 bits; sub_u from 0x80 wraps to a SIGNED two's-complement value in the negative range;
* shift_aright (sra) then correctly sign-extends the shift for both positive (left_x < 0x80) and negative (left_x > 0x80) cases.
* Digital pads publish left_x = 0x80 → delta = 0 → no rotation, so the analog step is naturally a no-op for digital controllers.
*/
typedef Struct_(Binds_PadApplyInput) { typedef Struct_(Binds_PadApplyInput) {
PadState* state; PadState* state;
V3_S2* cube_rot; V3_S2* cube_rot;
@@ -330,7 +210,7 @@ enum {
R_CubeRot = R_T1 atom_reg, R_CubeRot = R_T1 atom_reg,
R_FloorRot = R_T2 atom_reg, R_FloorRot = R_T2 atom_reg,
}; };
internal MipsAtom_(pad_input_cube_rotation) atom_info(atom_bind(Binds_PadApplyInput) internal MipsAtom_(pad_apply_input) atom_info(atom_bind(Binds_PadApplyInput)
, atom_reads(R_T0, R_CubeRot, R_FloorRot, R_T3, R_T4, R_PadStateT5, R_TapePtr) , atom_reads(R_T0, R_CubeRot, R_FloorRot, R_T3, R_T4, R_PadStateT5, R_TapePtr)
, atom_writes( R_CubeRot, R_FloorRot) , atom_writes( R_CubeRot, R_FloorRot)
) { ) {
@@ -338,15 +218,15 @@ internal MipsAtom_(pad_input_cube_rotation) atom_info(atom_bind(Binds_PadApplyIn
load_word(R_PadStateT5, R_TapePtr, O_(Binds_PadApplyInput,state)), load_word(R_PadStateT5, R_TapePtr, O_(Binds_PadApplyInput,state)),
load_word(R_CubeRot, R_TapePtr, O_(Binds_PadApplyInput,cube_rot)), load_word(R_CubeRot, R_TapePtr, O_(Binds_PadApplyInput,cube_rot)),
load_word(R_FloorRot, R_TapePtr, O_(Binds_PadApplyInput,floor_rot)), load_word(R_FloorRot, R_TapePtr, O_(Binds_PadApplyInput,floor_rot)),
LdSlot_ add_ui_self( R_TapePtr, S_(Binds_PadApplyInput)), add_ui_self( R_TapePtr, S_(Binds_PadApplyInput)),
/* Load pad[0].buttons into R_T0. */ /* Load pad[0].buttons into R_T0. */
load_word(R_T0, R_PadStateT5, O_(PadState,buttons)), LdSlot_ nop, load_word(R_T0, R_PadStateT5, O_(PadState,buttons)), nop,
// Note(Ed): Potential op with delay slot? // Note(Ed): Potential op with delay slot?
/* D-pad Left: cube_rot.y += 30, floor_rot.y += 5. */ /* D-pad Left: cube_rot.y += 30, floor_rot.y += 5. */
and_i(R_T3, R_T0, Pad_Left), branch_le_zero(R_T3, atom_offset(dpad_left, exit_dpad_left)), BdSlot_ and_i(R_T3, R_T0, pad0_(Pad_Left)), branch_le_zero(R_T3, atom_offset(dpad_left, exit_dpad_left)),
load_half( R_T4, R_CubeRot, O_(V3_S2,y)), LdSlot_ load_half( R_T4, R_CubeRot, O_(V3_S2,y)), /* BD-slot */
load_half( R_T3, R_FloorRot, O_(V3_S2,y)), load_half( R_T3, R_FloorRot, O_(V3_S2,y)),
add_si( R_T4, R_T4, 30), add_si( R_T4, R_T4, 30),
add_si( R_T3, R_T3, 5), add_si( R_T3, R_T3, 5),
@@ -355,8 +235,8 @@ internal MipsAtom_(pad_input_cube_rotation) atom_info(atom_bind(Binds_PadApplyIn
atom_label(exit_dpad_left) atom_label(exit_dpad_left)
/* D-pad Right: cube_rot.y -= 30, floor_rot.y -= 5. */ /* D-pad Right: cube_rot.y -= 30, floor_rot.y -= 5. */
and_i(R_T3, R_T0, Pad_Right), branch_le_zero(R_T3, atom_offset(dpad_right, exit_dpad_right)), BdSlot_ and_i(R_T3, R_T0, pad0_(Pad_Right)), branch_le_zero(R_T3, atom_offset(dpad_right, exit_dpad_right)),
load_half( R_T4, R_CubeRot, O_(V3_S2,y)), LdSlot_ load_half( R_T4, R_CubeRot, O_(V3_S2,y)), /* BD-slot */
load_half( R_T3, R_FloorRot, O_(V3_S2,y)), load_half( R_T3, R_FloorRot, O_(V3_S2,y)),
add_si( R_T4, R_T4, -30), add_si( R_T4, R_T4, -30),
add_si( R_T3, R_T3, -5), add_si( R_T3, R_T3, -5),
@@ -366,23 +246,23 @@ internal MipsAtom_(pad_input_cube_rotation) atom_info(atom_bind(Binds_PadApplyIn
/* Analog left-stick X: dead zone 0x70..0x90. /* Analog left-stick X: dead zone 0x70..0x90.
* Cube delta = (0x80 - left_x) >> 2; floor delta = (0x80 - left_x) >> 5. */ * Cube delta = (0x80 - left_x) >> 2; floor delta = (0x80 - left_x) >> 5. */
load_byte_u(R_T3, R_PadStateT5, O_(PadState,left.x)), LdSlot_ //? load_byte_u(R_T3, R_PadStateT5, O_(PadState,left_x)),
/* Dead-zone check: skip analog if left_x in [0x70, 0x90] inclusive. Outside dead zone on LOW side: left_x < 0x70 (strictly). /* Dead-zone check: skip analog if left_x in [0x70, 0x90] inclusive. Outside dead zone on LOW side: left_x < 0x70 (strictly).
* set_lt_u(R_T4, R_T3, R_T4=0x70) → R_T4 = (left_x < 0x70) ? 1 : 0. */ * set_lt_u(R_T4, R_T3, R_T4=0x70) → R_T4 = (left_x < 0x70) ? 1 : 0. */
add_ui(R_T4, R_0, PadDeadZone_HighBound), set_lt_u(R_T4, R_T3, R_T4), branch_ne(R_T4, R_0, atom_offset(dead_zone_low_check, dead_low_active)), add_ui(R_T4, R_0, 0x70), set_lt_u(R_T4, R_T3, R_T4), branch_ne(R_T4, R_0, atom_offset(dead_zone_low_check, dead_low_active)),
add_ui(R_T4, R_0, PadDeadZone_Center), /* BD-slot: pre-load 0x80 for dead_low_active */ add_ui(R_T4, R_0, 0x80), /* BD-slot: pre-load 0x80 for dead_low_active */
atom_label(dead_check_upper) atom_label(dead_check_upper)
/* left_x >= 0x70 → check upper bound. */ /* left_x >= 0x70 → check upper bound. */
load_byte_u(R_T3, R_PadStateT5, O_(PadState,left.x)), /* reload */ LdSlot_ //? load_byte_u(R_T3, R_PadStateT5, O_(PadState,left_x)), /* reload */
add_ui( R_T4, R_0, PadDeadZone_HighBound), add_ui( R_T4, R_0, 0x90),
/* R_T4 = (0x90 < left_x) ? 1 : 0 → (left_x > 0x90) ? 1 : 0 */ /* R_T4 = (0x90 < left_x) ? 1 : 0 → (left_x > 0x90) ? 1 : 0 */
set_lt_u(R_T4, R_T4, R_T3), branch_ne(R_T4, R_0, atom_offset(dead_zone_high_check, dead_high_active)), BdSlot_ set_lt_u(R_T4, R_T4, R_T3), branch_ne(R_T4, R_0, atom_offset(dead_zone_high_check, dead_high_active)),
add_ui( R_T4, R_0, PadDeadZone_Center), /* BD-slot: pre-load 0x80 for dead_high_active */ add_ui( R_T4, R_0, 0x80), /* BD-slot: pre-load 0x80 for dead_high_active */
jump_rel(atom_offset(dead_zone_skip, exit_stick)), jump_rel(atom_offset(dead_zone_skip, exit_stick)),
BdSlot_ mac_yield_load(), LdSlot_ mac_yield_load(),
atom_label(dead_low_active) atom_label(dead_low_active)
/* R_T3 = left_x (from line 632 lbu; not clobbered between dead_zone_low_check branch + its BD-slot `add_ui R_T4, 0x80`). /* R_T3 = left_x (from line 632 lbu; not clobbered between dead_zone_low_check branch + its BD-slot `add_ui R_T4, 0x80`).
@@ -393,18 +273,19 @@ atom_label(dead_low_active)
/* R_T4 = cube_delta */ /* R_T4 = cube_delta */
shift_aright(R_T4, R_T3, 2), shift_aright(R_T4, R_T3, 2),
load_half( R_T0, R_CubeRot, O_(V3_S2,y)), LdSlot_ nop, load_half( R_T0, R_CubeRot, O_(V3_S2,y)),
nop,
add_u( R_T0, R_T0, R_T4), add_u( R_T0, R_T0, R_T4),
store_half( R_T0, R_CubeRot, O_(V3_S2,y)), store_half( R_T0, R_CubeRot, O_(V3_S2,y)),
/* R_T4 = floor_delta — moved into the load-delay slot of the floor load below (fills the 1-instruction gap; /* R_T4 = floor_delta — moved into the load-delay slot of the floor load below (fills the 1-instruction gap;
* doesn't read R_T0; R_T4 settles by the subsequent add_u). */ * doesn't read R_T0; R_T4 settles by the subsequent add_u). */
load_half( R_T0, R_FloorRot, O_(V3_S2,y)), LdSlot_ load_half( R_T0, R_FloorRot, O_(V3_S2,y)),
shift_aright(R_T4, R_T3, 5), shift_aright(R_T4, R_T3, 5),
add_u( R_T0, R_T0, R_T4), add_u( R_T0, R_T0, R_T4),
store_half( R_T0, R_FloorRot, O_(V3_S2,y)), store_half( R_T0, R_FloorRot, O_(V3_S2,y)),
jump_rel(atom_offset(end_low, exit_stick)), jump_rel(atom_offset(end_low, exit_stick)),
BdSlot_ mac_yield_load(), LdSlot_ mac_yield_load(),
atom_label(dead_high_active) atom_label(dead_high_active)
/* R_T3 = left_x (from line 641 lbu in dead_check_upper; not clobbered between dead_zone_high_check branch + its BD-slot `add_ui R_T4, 0x80`). /* R_T3 = left_x (from line 641 lbu in dead_check_upper; not clobbered between dead_zone_high_check branch + its BD-slot `add_ui R_T4, 0x80`).
@@ -414,18 +295,19 @@ atom_label(dead_high_active)
/* delta = 0x80 - left_x (signed negative). */ /* delta = 0x80 - left_x (signed negative). */
shift_aright(R_T4, R_T3, 2), /* R_T4 = cube_delta (signed) */ shift_aright(R_T4, R_T3, 2), /* R_T4 = cube_delta (signed) */
load_half( R_T0, R_CubeRot, O_(V3_S2,y)), LdSlot_ nop, load_half( R_T0, R_CubeRot, O_(V3_S2,y)),
nop,
add_u( R_T0, R_T0, R_T4), add_u( R_T0, R_T0, R_T4),
store_half( R_T0, R_CubeRot, O_(V3_S2,y)), store_half( R_T0, R_CubeRot, O_(V3_S2,y)),
/* R_T4 = floor_delta (signed) — moved into the load-delay slot of the floor load below. */ /* R_T4 = floor_delta (signed) — moved into the load-delay slot of the floor load below. */
load_half( R_T0, R_FloorRot, O_(V3_S2,y)), LdSlot_ load_half( R_T0, R_FloorRot, O_(V3_S2,y)),
shift_aright(R_T4, R_T3, 5), shift_aright(R_T4, R_T3, 5),
add_u( R_T0, R_T0, R_T4), add_u( R_T0, R_T0, R_T4),
store_half( R_T0, R_FloorRot, O_(V3_S2,y)), store_half( R_T0, R_FloorRot, O_(V3_S2,y)),
atom_label(no_jump_fallthrough) atom_label(no_jump_fallthrough)
mac_yield_load(), LdSlot_ mac_yield_load(),
atom_label(exit_stick) atom_label(exit_stick)
/* NOT mac_yield() — R_AtomJmp was already loaded in the BD-slot of the dead-zone/exit branch. */ /* NOT mac_yield() — R_AtomJmp was already loaded in the BD-slot of the dead-zone/exit branch. */
@@ -433,63 +315,7 @@ atom_label(exit_stick)
}; };
enum { enum {
R_Cam = R_T4 atom_reg, R_PrimCursor = R_T7 atom_reg atom_type(U4*), /* VRAM output cursor (primitive buffer) */
R_CamPadState = R_T5 atom_reg,
};
typedef Struct_(Binds_PadInputCam) {
PadState* state;
Camera* cam;
};
internal MipsAtom_(pad_input_cam) atom_info(atom_bind(Binds_PadInputCam)
, atom_reads( R_Cam, R_CamPadState, R_TapePtr)
, atom_writes(R_Cam)
) {
/* Bind pop: state → R_CamPadState (R_T5), cam → R_Cam (R_T4), advance R_TapePtr by 8. */
load_word(R_CamPadState, R_TapePtr, O_(Binds_PadInputCam,state)),
load_word(R_Cam, R_TapePtr, O_(Binds_PadInputCam,cam)),
LdSlot_ add_ui_self( R_TapePtr, S_(Binds_PadInputCam)),
/* Load pad[0].buttons into R_T0; nop fills the load-delay slot. */
load_word(R_T0, R_CamPadState, O_(PadState,buttons)), LdSlot_
load_word(R_T1, R_Cam, O_(Camera,pos.x)),
// D-pad Left → cam.pos.x -= 50. and_i fulfills BD-slot for load on R_Cam.
LdSlot_ and_i(R_T3, R_T0, Pad_Left), branch_le_zero(R_T3, atom_offset(left_x, exit_left_x)), BdSlot_ nop,
add_si(R_T1, R_T1, -50), store_word(R_T1, R_Cam, O_(Camera,pos.x)),
atom_label(exit_left_x)
/* D-pad Right → cam.pos.x += 50. Reuses R_T1 from Left. */
and_i(R_T3, R_T0, Pad_Right), branch_le_zero(R_T3, atom_offset(right_x, exit_right_x)), BdSlot_ nop,
add_si(R_T1, R_T1, 50), store_word(R_T1, R_Cam, O_(Camera,pos.x)),
atom_label(exit_right_x)
/* D-pad Up → cam.pos.y -= 50. Load pos.y BEFORE the andi. */
load_word(R_T1, R_Cam, O_(Camera,pos.y)), LdSlot_
and_i(R_T3, R_T0, Pad_Up), branch_le_zero(R_T3, atom_offset(up_y, exit_up_y)), BdSlot_ nop,
add_si(R_T1, R_T1, -50), store_word(R_T1, R_Cam, O_(Camera,pos.y)),
atom_label(exit_up_y)
/* D-pad Down → cam.pos.y += 50. Reuses R_T1 from Up. */
and_i(R_T3, R_T0, Pad_Down), branch_le_zero(R_T3, atom_offset(down_y, exit_down_y)), BdSlot_ nop,
add_si(R_T1, R_T1, 50), store_word(R_T1, R_Cam, O_(Camera,pos.y)),
atom_label(exit_down_y)
/* D-pad Cross → cam.pos.z -= 50. Load pos.z BEFORE the andi. */
load_word(R_T1, R_Cam, O_(Camera,pos.z)), LdSlot_
and_i(R_T3, R_T0, Pad_Cross), branch_le_zero(R_T3, atom_offset(cross_z, exit_cross_z)), BdSlot_ load_word(R_AtomJmp, R_TapePtr, 0), LdSlot_ // ac_yield: word 1
add_si(R_T1, R_T1, -50), store_word(R_T1, R_Cam, O_(Camera,pos.z)),
atom_label(exit_cross_z)
/* D-pad Circle → cam.pos.z += 50. Reuses R_T1 from Cross. */
and_i(R_T3, R_T0, Pad_Circle), branch_le_zero(R_T3, atom_offset(circle_z, exit_circle_z)), BdSlot_ add_ui_self(R_TapePtr, S_(MipsCode)), // ac_yield: word 2
add_si(R_T1, R_T1, 50), store_word(R_T1, R_Cam, O_(Camera,pos.z)),
atom_label(exit_circle_z)
jump_reg(R_AtomJmp), BdSlot_ nop // ac_yield: word 3-4
};
enum {
R_PrimCursor = R_T7 atom_reg atom_type(U4*), /* Output cursor (primitive buffer) */
R_FaceCursor = R_T4 atom_reg atom_type(V4_S2*), /* Cube face-index cursor (V4_S2*); floor context switches to V3_S2* via atom_phase */ R_FaceCursor = R_T4 atom_reg atom_type(V4_S2*), /* Cube face-index cursor (V4_S2*); floor context switches to V3_S2* via atom_phase */
R_VertBase = R_T5 atom_reg atom_type(V3_S2*), /* Base address of the vertex array */ R_VertBase = R_T5 atom_reg atom_type(V3_S2*), /* Base address of the vertex array */
R_OtBase = R_T6 atom_reg atom_type(U4*), /* Base address of the Ordering Table */ R_OtBase = R_T6 atom_reg atom_type(U4*), /* Base address of the Ordering Table */
@@ -498,6 +324,7 @@ enum {
#define R_VertBase_Code R_T5_Code #define R_VertBase_Code R_T5_Code
#define R_OtBase_Code R_T6_Code #define R_OtBase_Code R_T6_Code
}; };
typedef Struct_(Binds_CubeTri) { typedef Struct_(Binds_CubeTri) {
U4 PrimCursor; U4 PrimCursor;
V4_S2* FaceCursor; V4_S2* FaceCursor;
@@ -513,11 +340,11 @@ internal MipsAtom_(rbind_cube_g4_face) atom_info(atom_bind(Binds_CubeTri), atom_
load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,FaceCursor)), load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,FaceCursor)),
load_word(R_VertBase, R_TapePtr, O_(Binds_CubeTri,VertBase)), load_word(R_VertBase, R_TapePtr, O_(Binds_CubeTri,VertBase)),
load_word(R_OtBase, R_TapePtr, O_(Binds_CubeTri,OtBase)), load_word(R_OtBase, R_TapePtr, O_(Binds_CubeTri,OtBase)),
LdSlot_ add_ui_self( R_TapePtr, S_(Binds_CubeTri)), add_ui_self( R_TapePtr, S_(Binds_CubeTri)),
mac_yield() mac_yield()
}; };
// cube_g4_face — Draw one cube face (Gouraud-shaded quad) via the GTE tape pipeline // cube_g4_face — Draw one cube face (Gouraud-shaded quad) via the GTE tape pipeline
internal internal
MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4), MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4),
atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase), atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase),
@@ -526,22 +353,20 @@ MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4),
load_half_u(R_T0, R_FaceCursor, 0 * S_(S2)), load_half_u(R_T0, R_FaceCursor, 0 * S_(S2)),
load_half_u(R_T1, R_FaceCursor, 1 * S_(S2)), load_half_u(R_T1, R_FaceCursor, 1 * S_(S2)),
load_half_u(R_T2, R_FaceCursor, 2 * S_(S2)), load_half_u(R_T2, R_FaceCursor, 2 * S_(S2)),
// load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)), load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)),
LdSlot_ mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2), mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2),
GteDelay_ load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)), LdSlot_ nop2, gte_cmdw_rotate_translate_perspective_triple, // required cpu -> gte delay slot
GteDelay_ load_word(R_AtomJmp, R_TapePtr, 0), LdSlot_ //ac_yield: word 2,
gte_cmdw_rotate_translate_perspective_triple,
gte_cmdw_nclip, gte_cmdw_nclip,
gte_mv_from_data_r(R_T0, C2_MAC0), GteDelay_ add_ui_self(R_TapePtr, S_(MipsCode)), // ac_yield: word 1 gte_mv_from_data_r(R_T0, C2_MAC0), nop,
branch_le_zero(R_T0, atom_offset(cull, cube_g4_face_exit)), branch_le_zero(R_T0, atom_offset(cull, cube_g4_face_exit)),
/* BD-slot: Write the prim tag (R_0=0; overwrites the legacy tag word in the prim_buffer). /* BD-slot: write the prim tag (R_0=0; overwrites the legacy tag word in the prim_buffer).
* If branch IS taken (face culled), the body is skipped and this 0-tag is stranded — * If branch IS taken (face culled), the body is skipped and this 0-tag is stranded —
* harmless because the OT entry that points to this prim is created later. */ * harmless because the OT entry that points to this prim is created later, only on the body path. */
BdSlot_ store_word(R_0, R_PrimCursor, O_(Poly_G4, tag)), store_word(R_0, R_PrimCursor, O_(Poly_G4, tag)),
shift_lleft(R_AT, R_T3, v3s2_byteoff), add_u(R_AT, R_AT, R_VertBase), shift_lleft(R_AT, R_T3, v3s2_byteoff), add_u(R_AT, R_AT, R_VertBase),
load_word(R_V0, R_AT, O_(V3_S2, x)), load_word(R_V1, R_AT, O_(V3_S2, z)), LdSlot_ load_word(R_V0, R_AT, O_(V3_S2, x)), load_word(R_V1, R_AT, O_(V3_S2, z)),
gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0), gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0),
mac_gte_store_g4_p012(R_PrimCursor), mac_gte_store_g4_p012(R_PrimCursor),
@@ -553,8 +378,8 @@ MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4),
add_ui( R_AT, R_0, OrderingTbl_Len), add_ui( R_AT, R_0, OrderingTbl_Len),
set_lt_u( R_AT, R_T1, R_AT), set_lt_u( R_AT, R_T1, R_AT),
branch_equal(R_AT, R_0, atom_offset(bounds_chk, cube_g4_face_exit)), BdSlot_ nop, branch_equal(R_AT, R_0, atom_offset(bounds_chk, cube_g4_face_exit)), nop,
mac_insert_ot_tag(R_OtBase, R_PrimCursor, S_(Poly_G4)), mac_insert_ot_tag_g4(R_OtBase, R_PrimCursor),
mac_format_g4_color(R_PrimCursor, mac_format_g4_color(R_PrimCursor,
/* c0 magenta */ 0xFF, 0x00, 0xFF, /* c0 magenta */ 0xFF, 0x00, 0xFF,
/* c1 yellow */ 0xFF, 0xFF, 0x00, /* c1 yellow */ 0xFF, 0xFF, 0x00,
@@ -566,7 +391,7 @@ MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4),
atom_label(cube_g4_face_exit) atom_label(cube_g4_face_exit)
add_ui_self(R_PrimCursor, S_(Poly_G4)), /* 9 words = Poly_G4 */ add_ui_self(R_PrimCursor, S_(Poly_G4)), /* 9 words = Poly_G4 */
add_ui_self(R_FaceCursor, S_(S2) * 4), /* 4 × S2 = 8 bytes */ add_ui_self(R_FaceCursor, S_(S2) * 4), /* 4 × S2 = 8 bytes */
jump_reg(R_AtomJmp), BdSlot_ nop // ac_yield: word 3-4 mac_yield()
}; };
typedef Struct_(Binds_FloorTri) { typedef Struct_(Binds_FloorTri) {
@@ -585,7 +410,7 @@ MipsAtom_(rbind_floor_f3_face) atom_info(atom_bind(Binds_FloorTri), atom_phase(f
load_word(R_FaceCursor, R_TapePtr, O_(Binds_FloorTri,FaceCursor)), load_word(R_FaceCursor, R_TapePtr, O_(Binds_FloorTri,FaceCursor)),
load_word(R_VertBase, R_TapePtr, O_(Binds_FloorTri,VertBase)), load_word(R_VertBase, R_TapePtr, O_(Binds_FloorTri,VertBase)),
load_word(R_OtBase, R_TapePtr, O_(Binds_FloorTri,OtBase)), load_word(R_OtBase, R_TapePtr, O_(Binds_FloorTri,OtBase)),
LdSlot_ add_ui_self( R_TapePtr, S_(Binds_FloorTri)), add_ui_self( R_TapePtr, S_(Binds_FloorTri)),
mac_yield() mac_yield()
}; };
@@ -595,14 +420,14 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
, atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase) , atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
, atom_writes(R_PrimCursor, R_FaceCursor) , atom_writes(R_PrimCursor, R_FaceCursor)
) { ) {
mac_load_tri_indices(R_FaceCursor, R_T0, R_T1, R_T2), mac_load_tri_indices( R_FaceCursor, R_T0, R_T1, R_T2),
mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2), GteDelay_ nop2, mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2),
gte_cmdw_rotate_translate_perspective_triple, // 2 nops retire the final cpu -> gte writes before RTPT nop2, gte_cmdw_rotate_translate_perspective_triple, // 2 nops retire the final cpu -> gte writes before RTPT
gte_cmdw_nclip, gte_cmdw_nclip,
/* Culling (Branch forward if Backface) */ /* Culling (Branch forward if Backface) */
gte_mv_from_data_r(R_T0, C2_MAC0), GteDelay_ load_word(R_AtomJmp, R_TapePtr, 0), // ac_yield: word 1 gte_mv_from_data_r(R_T0, C2_MAC0),
branch_le_zero(R_T0, atom_offset(culling, floor_f3_face_exit)), BdSlot_ add_ui_self(R_TapePtr, S_(MipsCode)), // ac_yield: word 2 nop, branch_le_zero(R_T0, atom_offset(culling, floor_f3_face_exit)), nop, // required gte -> cpu load-delay slot.
/* Format Primitive */ /* Format Primitive */
mac_gte_store_f3(R_PrimCursor), mac_gte_store_f3(R_PrimCursor),
@@ -612,9 +437,9 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
/* Bounds Check OTZ < 2048 (Branch forward to skip insertion) */ /* Bounds Check OTZ < 2048 (Branch forward to skip insertion) */
add_ui( R_AT, R_0, OrderingTbl_Len), add_ui( R_AT, R_0, OrderingTbl_Len),
set_lt_u( R_AT, R_T1, R_AT), set_lt_u( R_AT, R_T1, R_AT),
branch_equal(R_AT, R_0, atom_offset(bounds_chk, floor_f3_face_exit)), BdSlot_ nop, branch_equal(R_AT, R_0, atom_offset(bounds_chk, floor_f3_face_exit)), nop,
mac_format_f3_color(R_PrimCursor, 0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white) mac_format_f3_color(R_PrimCursor, 0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white)
mac_insert_ot_tag(R_OtBase, R_PrimCursor, S_(Poly_F3)), /* Insert into Ordering Table Linked List */ mac_insert_ot_tag_f3(R_OtBase, R_PrimCursor), /* Insert into Ordering Table Linked List */
add_ui_self(R_PrimCursor, S_(Poly_F3)), /* Advance Prim Cursor (5 words) */ add_ui_self(R_PrimCursor, S_(Poly_F3)), /* Advance Prim Cursor (5 words) */
// Note(Ed): No bounds checking, should be checked before atom runs. // Note(Ed): No bounds checking, should be checked before atom runs.
// end: branch(bounds_chk) // end: branch(bounds_chk)
@@ -623,7 +448,7 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
/* Advance Input Cursor & Yield (Both branch targets land here) */ /* Advance Input Cursor & Yield (Both branch targets land here) */
atom_label(floor_f3_face_exit) atom_label(floor_f3_face_exit)
add_ui_self(R_FaceCursor, S_(S2) * 4), /* Advance Face Cursor (4 * S2 = 8 bytes) */ add_ui_self(R_FaceCursor, S_(S2) * 4), /* Advance Face Cursor (4 * S2 = 8 bytes) */
jump_reg(R_AtomJmp), BdSlot_ nop // ac_yield: word 3-4 mac_yield()
}; };
typedef Struct_(Binds_SyncPrimitiveArena) { U4 used; U4 cursor; }; typedef Struct_(Binds_SyncPrimitiveArena) { U4 used; U4 cursor; };
@@ -632,7 +457,7 @@ internal MipsAtom_(sync_primitive_arena) atom_info(atom_bind(Binds_SyncPrimitive
, atom_writes(R_TapePtr) , atom_writes(R_TapePtr)
){ ){
load_word(R_AT, R_TapePtr, O_(Binds_SyncPrimitiveArena,used)), load_word(R_AT, R_TapePtr, O_(Binds_SyncPrimitiveArena,used)),
load_word(R_T0, R_TapePtr, O_(Binds_SyncPrimitiveArena,cursor)), LdSlot_ load_word(R_T0, R_TapePtr, O_(Binds_SyncPrimitiveArena,cursor)),
add_ui_self( R_TapePtr, S_(Binds_SyncPrimitiveArena)), add_ui_self( R_TapePtr, S_(Binds_SyncPrimitiveArena)),
/* Calculate byte offset and store directly back to RAM */ /* Calculate byte offset and store directly back to RAM */
sub_u( R_T0, R_PrimCursor, R_T0), // R_T0 = R_PrimCursor - binds.cursor sub_u( R_T0, R_PrimCursor, R_T0), // R_T0 = R_PrimCursor - binds.cursor
+120 -214
View File
@@ -1,7 +1,7 @@
#pragma region Vendors #pragma region Vendors
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
// #include <assert.h> #include <assert.h>
// #include "libgpu.h" // #include "libgpu.h"
// #include "libetc.h" // #include "libetc.h"
// #include "libgte.h" // #include "libgte.h"
@@ -26,13 +26,11 @@
#include "duffle/dsl.atom.h" #include "duffle/dsl.atom.h"
#include "duffle/lottes_tape.h" #include "duffle/lottes_tape.h"
#include "duffle/bios.h"
#include "duffle/psyq.h" #include "duffle/psyq.h"
#pragma endregion Duffle Headers #pragma endregion Duffle Headers
#pragma region Duffle TUs #pragma region Duffle TUs
#include "duffle/pad.c" #include "duffle/math.atom.c"
#include "duffle/math.atom.h"
#include "duffle/mips.atom.c" #include "duffle/mips.atom.c"
#include "duffle/gte.atom.c" #include "duffle/gte.atom.c"
#include "duffle/gp.atom.c" #include "duffle/gp.atom.c"
@@ -43,7 +41,6 @@
#pragma region Hello Camera Headers #pragma region Hello Camera Headers
# include "gen/macs.h" # include "gen/macs.h"
# include "gen/offsets.h" # include "gen/offsets.h"
# include "gen/auto_reg.h"
#include "hello_camera.h" #include "hello_camera.h"
#pragma endregion Hello Camera Headers #pragma endregion Hello Camera Headers
@@ -53,13 +50,8 @@
#pragma endregion Hello Joypad TUs #pragma endregion Hello Joypad TUs
enum { enum {
Scratchpad_Len = 1024,
MemTape_Len = 512, MemTape_Len = 512,
ResolveLookAtArena_Words = 1024,
ResolveLookAtArena_Size = ResolveLookAtArena_Words * S_(MipsCode),
CT_InitAtomMem_Words = Kilo_(4),
CT_InitAtomMem_Size = CT_InitAtomMem_Words * S_(MipsCode),
}; };
typedef Struct_(SMemory) { typedef Struct_(SMemory) {
PrimitiveArena primitives; PrimitiveArena primitives;
@@ -69,10 +61,7 @@ typedef Struct_(SMemory) {
U4 MemTape[MemTape_Len]; U4 MemTape[MemTape_Len];
MT3_S2S4 tform_world; M3_S2 tform_world;
MT3_S2S4 tform_view;
Camera cam;
Ent_Cube cube; Ent_Cube cube;
Ent_Floor floor; Ent_Floor floor;
@@ -80,19 +69,11 @@ typedef Struct_(SMemory) {
PadBiosRaw pad_raw[2]; PadBiosRaw pad_raw[2];
PadState pad[2]; PadState pad[2];
U1 ct_init_atom_mem[CT_InitAtomMem_Size]; U4_V scratchpad; // d-cache
MipsAtom* normalize_v3s4;
MipsAtom* gte_cross_v3s4;
U1 resolve_look_at_mem[ResolveLookAtArena_Size];
MipsAtom* resolve_look_at_bundle[AtomBundle_Len(resolve_look_at)];
}; };
global SMemory smem; global SMemory smem;
extern SMemory smem; extern SMemory smem;
#define pad0_btn_(btn) btn & smem.pad[0].buttons
#define pad1_btn_(btn) btn & smem.pad[1].buttons
I_ B1* prim__alloc(U4 type_width, Str8 type_name) { I_ B1* prim__alloc(U4 type_width, Str8 type_name) {
gknown PrimitiveArena* pa = & smem.primitives; gknown PrimitiveArena* pa = & smem.primitives;
gknown B1* buf = (B1*) r_(smem.primitives.buf)[smem.active_buf_id]; gknown B1* buf = (B1*) r_(smem.primitives.buf)[smem.active_buf_id];
@@ -103,164 +84,97 @@ I_ B1* prim__alloc(U4 type_width, Str8 type_name) {
} }
#define prim_alloc(type) (type*)prim__alloc(S_(type), slit( stringify(type))) #define prim_alloc(type) (type*)prim__alloc(S_(type), slit( stringify(type)))
I_ void resolve_look_at_c11(MT3_S2S4* look_at, P3_S4* eye, P3_S4* target, V3_S4* up_in) { /* Uses ONE 8-byte frame allocated via the compiler's standard prologue.
// RGA(Lengyel): Build matrix expansion of a rigid transformation. Corresponding motor is not constructed; we write the LA form for GTE. * The 4 wasted-arg words for B(12h) InitPAD2 live at [SP+0..15] but are not explicitly allocated.
// Preconditions: eye != target, up_in not collinear with (target - eye). * The compiler handles the MIPS O32 "wasted stack" convention for us by treating the B-call as a 4-arg call.
V3_S4 right, up, forward; *
V3_S4 ux, uy, uz; * The buffer pointers are passed as arguments so the compiler keeps them in callee-saved registers;
V3_S4 pos, off; * The B(12h) asm volatile block does NOT clobber those registers (it clobbers only the volatile GPRs + the B-table arg registers explicitly).
* The C-level writes after the call re-load the pointers from their callee-saved homes.
*
* The clobber list for both B-calls names the full BIOS destroy set documented in kernelbios.md:167-174 (R1..R15, R24..R25, R31, HI/LO).
* The kernel-ABI "volatile GPRs" subset is clb_system; the rest of the destroy set is enumerated explicitly here. */
NI_ void pad_bios_init_start(PadBiosRaw* raw0, PadBiosRaw* raw1)
{
/* Pin raw0 + raw1 to $a0 + $a1 via rgcc; the B(12h) call uses these directly.
* The `(void)` casts mark them as unread after the call so the compiler doesn't need to move them back. */
register PadBiosRaw* p0 rgcc(R_A0) = raw0;
register PadBiosRaw* p1 rgcc(R_A1) = raw1;
(void)p0; (void)p1;
forward = target[0]; sub_v3s4(& forward, eye[0]); // RGA(Lengyel): Affine point - point = zero-weight direction. // TODO(Ed): Properly annotate the raw values in the inline asm instructions.
psy_normalize_v3s4(& forward, & uz); // RGA(Lengyel): Normalize the direction bulk. Not finite-point unitization. // Use enums.
cross_v3s4(& uz, up_in, & right); psy_normalize_v3s4(& right, & ux); // RGA(Lengyel): Complement(Wedge(forward, up_in)) -> right axis. /* B(12h) InitPAD2(raw0, 0x22, raw1, 0x22)
cross_v3s4(& uz, & ux, & up); psy_normalize_v3s4(& up, & uy); // RGA(Lengyel): Complement(Wedge(forward, right)) -> up axis. * $a0 = raw0 (rgcc-bound; survives the sequence below)
* $a1 = raw1 (preserved into $a2 before $a1 is overwritten)
// RGA(Lengyel): matrix expansion of the world-to-camera rotation (basis rows). * $a2 = raw1 (moved from $a1; survives $a1's overwrite)
look_at->m[0][0] = ux.x; look_at->m[0][1] = ux.y; look_at->m[0][2] = ux.z; * $a3 = 0x22 (immediate)
look_at->m[1][0] = uy.x; look_at->m[1][1] = uy.y; look_at->m[1][2] = uy.z; * $t1 = 0x12 (function number)
look_at->m[2][0] = uz.x; look_at->m[2][1] = uz.y; look_at->m[2][2] = uz.z; * $t2 = 0xB0 (BIOS B-table address) */
asm volatile(
pos = eye[0]; mul_v3s4(& pos, v3s4(-1,-1,-1)); // RGA(Lengyel): -eye in world coordinates (spatial bulk only; implicit weight is dropped). asm_words(
or_u( rarg_2, rarg_1, rdiscard), /* $a2 = $a1 = raw1 */
// RGA(Lengyel): R * (-eye) is the full matrix translation column. add_ui( rarg_1, rdiscard, 0x22), /* $a1 = 0x22 */
// Motor translator would store half this displacement in m.xyz; GTE consumes full column. add_ui( rarg_3, rdiscard, 0x22), /* $a3 = 0x22 */
mul_m3s2_v3s4(look_at, & pos, & off); add_ui( rtmp_1, rdiscard, 0x12), /* $t1 = 0x12 */
trans_m3s2( look_at, & off); add_ui( rtmp_2, rdiscard, 0xB0), /* $t2 = 0xB0 */
} call_reg(rtmp_2), /* jalr $t2, $ra */
FI_ void camera_look_at_c11(Camera* c, P3_S4* target, V3_S4* up_in) { resolve_look_at_c11(& c->look_at, & c->pos, target, up_in); } nop /* BD slot */
)
internal void compile_init_atoms(void) { asm_rpins, r_use(p0), r_use(p1)
AtomArena ab = atomarena_make(slice_ut_arr(smem.ct_init_atom_mem)); asm_clobber:
RegFile rf = regfile(regfile_abi_mask); rlit(R_AT),
#define ralloc() regfile_alloc(& rf) rlit(R_V0), rlit(R_V1),
#define ralloc_v3() { ralloc(), ralloc(), ralloc() } rlit(R_T0), rlit(R_T1), rlit(R_T2), rlit(R_T3), rlit(R_T4),
rlit(R_T5), rlit(R_T6), rlit(R_T7), rlit(R_T8), rlit(R_T9),
smem.gte_cross_v3s4 = gte_cross_v3s4(& ab, rlit(R_RA),
RegUse_(gte_cross_v3s4) { clb_mem_drain
.a = ralloc_v3(),
.b = ralloc_v3(),
.x = ralloc(),
.y = ralloc(),
.z = ralloc(),
});
regfile_reset(& rf);
smem.normalize_v3s4 = normalize_v3s4(& ab,
RegUse_(normalize_v3s4) {
.res = ralloc_v3(),
.r0 = ralloc(),
.r1 = ralloc(),
.r2 = ralloc(),
.r3 = ralloc(),
.r4 = ralloc(),
.r5 = ralloc(),
});
regfile_reset(& rf);
assert(ab.used <= CT_InitAtomMem_Size);
#undef ralloc
#undef ralloc_v3
}
internal void compile_resolve_look_at(void) {
AtomBundle_resolve_look_at_R bundle = C_(void*, smem.resolve_look_at_bundle);
AtomArena ab = atomarena_make(slice_ut_arr(smem.resolve_look_at_mem));
RegFile rf = regfile(regfile_abi_mask);
#define ralloc() regfile_alloc(& rf)
#define ralloc_v3() { ralloc(), ralloc(), ralloc() }
bundle->input_and_sub = AtomBundleEntry_(resolve_look_at, input_and_sub)(& ab,
RegUse_(resolve_look_at_input_and_sub) {
.target_ptr = ralloc(),
.eye_ptr = ralloc(),
.up_in_ptr = ralloc(),
.up_in = ralloc_v3(),
.r012 = ralloc_v3(),
.r345 = {ralloc(), R_AT, ralloc() },
});
regfile_reset(& rf);
bundle->normalize_fwd_uz = smem.normalize_v3s4;
bundle->cross_to_right = smem.gte_cross_v3s4;
bundle->normalize_right_ux = smem.normalize_v3s4;
bundle->cross_to_up = smem.gte_cross_v3s4;
bundle->normalize_up_uy = smem.normalize_v3s4;
bundle->populate_mt3s4s2 = AtomBundleEntry_(resolve_look_at,populate_mt3s4s2)(& ab,
RegUse_(resolve_look_at_populate_mt3s4s2){
.look_at = ralloc(),
.eye = ralloc(),
.row = ralloc_v3(),
.r0 = ralloc(),
.r1 = ralloc(),
.r2 = ralloc(),
});
assert(ab.used <= ResolveLookAtArena_Size); // Sanity check: arena didn't overflow.
#undef ralloc
}
// Emit the resolve_look_at bundle into the tape. Called once per frame from update().
I_ void resolve_look_at(TapeBuilder_R tb, MT3_S2S4* look_at, P3_S4* eye, P3_S4* target, V3_S4* up_in) {
/* Typed view of the scratchpad for field-address arithmetic. */
ResolveLookAtScratch* sp = C_scratch(ResolveLookAtScratch*);
AtomBundle_resolve_look_at_R bundle = C_(void*, smem.resolve_look_at_bundle);
tb_emit(tb, bundle->input_and_sub); tb_bind_(tb, Binds_ResolveLookAtSub,
.target = target,
.eye = eye,
.up_in = up_in,
); );
tb_emit(tb, bundle->normalize_fwd_uz); tb_bind_(tb, Binds_normalize_v3s4,
.src_offset = O_(ResolveLookAtScratch,fwd), /* The C-level writes re-load the pointers via the parameter names and write 0xFF to each
.dst_offset = O_(ResolveLookAtScratch,uz), * buffer's status byte to mark the initial-state hazard documented in kernelbios.md:1621-1624. */
); u1_v(raw0)[0] = 0xFF;
tb_emit(tb, bundle->cross_to_right); tb_bind_(tb, Binds_gte_cross_v3s4, u1_v(raw1)[0] = 0xFF;
.src_a = & sp->uz,
.src_b = & sp->up_in, /* B(13h) StartPAD2() — no args. The BIOS preserves $sp. */
.out = & sp->right, asm volatile(
); asm_words(
tb_emit(tb, bundle->normalize_right_ux); tb_bind_(tb, Binds_normalize_v3s4, add_ui( rtmp_1, rdiscard, 0x13), /* $t1 = 0x13 */
.src_offset = O_(ResolveLookAtScratch,right), add_ui( rtmp_2, rdiscard, 0xB0), /* $t2 = 0xB0 (re-load) */
.dst_offset = O_(ResolveLookAtScratch,ux), call_reg(rtmp_2), /* jalr $t2, $ra */
); nop /* BD slot */
tb_emit(tb, bundle->cross_to_up); tb_bind_(tb, Binds_gte_cross_v3s4, )
.src_a = & sp->uz, asm_clobber:
.src_b = & sp->ux, rlit(R_AT),
.out = & sp->up, rlit(R_V0), rlit(R_V1),
); rlit(R_T0), rlit(R_T1), rlit(R_T2), rlit(R_T3), rlit(R_T4),
tb_emit(tb, bundle->normalize_up_uy); tb_bind_(tb, Binds_normalize_v3s4, rlit(R_T5), rlit(R_T6), rlit(R_T7), rlit(R_T8), rlit(R_T9),
.src_offset = O_(ResolveLookAtScratch,up), rlit(R_RA),
.dst_offset = O_(ResolveLookAtScratch,uy), clb_mem_drain
);
tb_emit(tb, bundle->populate_mt3s4s2); tb_bind_(tb, Binds_ResolveLookAt_PopulateMT3S4S2,
.look_at = look_at,
); );
} }
FI_ void camera_look_at(TapeBuilder_R tb, Camera* c, P3_S4* target, V3_S4* up_in) { resolve_look_at(tb, & c->look_at, & c->pos, target, up_in); }
GCC_OPTIMIZATION_DISABLE
void update(PrimitiveArena* pa, U4* ordering_buf) void update(PrimitiveArena* pa, U4* ordering_buf)
{ {
TapeBuilder tb = tb_make(slice_ut_arr(smem.MemTape)); TapeBuilder tb = tb_make(slice_ut_arr(smem.MemTape));
// Pad Input if (1) // Pad Input
{ {
tb.used = 0; tb_scope_run(& tb) { tb.used = 0; tb_scope_run(& tb) {
// Grab latest state from bios. /* BIOS-owned polling: per-frame snapshot of both ports. */
tb_emit_(pad_bios_snapshot); tb_emit_(pad_bios_snapshot);
tb_data(& tb, u4_(& smem.pad_raw[0])); tb_data_(raw, & smem.pad_raw[0]);
tb_data(& tb, u4_(& smem.pad[0])); tb_data_(state, & smem.pad[0]);
// tb_emit_(pad_bios_snapshot); tb_emit_(pad_bios_snapshot);
// tb_data_(raw, & smem.pad_raw[1]); tb_data_(raw, & smem.pad_raw[1]);
// tb_data_(state, & smem.pad[1]); tb_data_(state, & smem.pad[1]);
/* Per-frame rotation apply: consume pad[0].buttons + pad[0].left_x */
tb_emit_(pad_input_cam); tb_emit_(pad_apply_input);
tb_data(& tb, u4_(& smem.pad[0])); tb_data_(state, & smem.pad[0]);
tb_data(& tb, u4_(& smem.cam)); tb_data_(cube_rot, & smem.cube.rot);
tb_data_(floor_rot, & smem.floor.rot);
// tb_emit_(pad_input_cube_rotation);
// tb_data_(state, & smem.pad[0]);
// tb_data_(cube_rot, & smem.cube.rot);
// tb_data_(floor_rot, & smem.floor.rot);
} }
} }
@@ -272,6 +186,12 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
gknown V3_S4_R acc = & smem.cube.accel; gknown V3_S4_R acc = & smem.cube.accel;
add_v3s4(vel, acc[0]); add_v3s4(vel, acc[0]);
add_v3s4_fp(pos, vel[0]); add_v3s4_fp(pos, vel[0]);
// vel->x += acc->x;
// vel->y += acc->y;
// vel->z += acc->z;
// pos->x += vel->x;
// pos->y += vel->y;
// pos->z += vel->z;
if (pos->y + 150 > smem.floor.pos.y) vel->y *= -1; if (pos->y + 150 > smem.floor.pos.y) vel->y *= -1;
@@ -281,28 +201,15 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
A2_S2 p; //??? A2_S2 p; //???
S4 flag; //???? S4 flag; //????
B4 use_c11_path = false;
if (use_c11_path) {
camera_look_at_c11(& smem.cam, & smem.cube.pos, & v3s4(0, -fp_one, 0));
}
if (use_c11_path == false)
{
tb.used = 0; tb_scope_run(& tb) {
camera_look_at(& tb, & smem.cam, & smem.cube.pos, & v3s4(0, -fp_one, 0));
}
}
// Draw cube // Draw cube
if (1) if (1)
{ {
mt3s2s4_rotation (& smem.cube.rot, & smem.tform_world); m3s2_rotation (& smem.cube.rot, & smem.tform_world);
mt3s2s4_translation(& smem.tform_world, & smem.cube.pos); m3s2_translation(& smem.tform_world, & smem.cube.pos);
mt3s2s4_scale (& smem.tform_world, & smem.cube.scale); m3s2_scale (& smem.tform_world, & smem.cube.scale);
gte_matrix_set_rotation (& smem.tform_world);
// Combine world and look_at matrix. gte_matrix_set_translation(& smem.tform_world);
gte_comp_coord_m3s2(& smem.cam.look_at, & smem.tform_world, & smem.tform_view);
gte_matrix_set_rotation (& smem.tform_view);
gte_matrix_set_translation(& smem.tform_view);
U4 prim_base = u4_(pa->buf[smem.active_buf_id]); U4 prim_base = u4_(pa->buf[smem.active_buf_id]);
U4 prim_cursor = prim_base + pa->used; U4 prim_cursor = prim_base + pa->used;
@@ -323,22 +230,16 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
tb_data(& tb, u4_(& pa->used)); tb_data(& tb, u4_(& pa->used));
tb_data(& tb, prim_base); tb_data(& tb, prim_base);
} }
tape_run(tb_slice(tb));// Fire off the tape (bigger-clobber variant). tape_run(tb_slice(tb));
// smem.cube.rot.y += 30; // smem.cube.rot.y += 30;
} }
// Draw floor // Draw floor
if (1) if (1)
{ {
mt3s2s4_rotation (& smem.floor.rot, & smem.tform_world); m3s2_rotation (& smem.floor.rot, & smem.tform_world);
mt3s2s4_translation(& smem.tform_world, & smem.floor.pos); m3s2_translation(& smem.tform_world, & smem.floor.pos);
mt3s2s4_scale (& smem.tform_world, & smem.floor.scale); m3s2_scale (& smem.tform_world, & smem.floor.scale);
// Combine world and look_at matrix.
gte_comp_coord_m3s2(& smem.cam.look_at, & smem.tform_world, & smem.tform_view);
gte_matrix_set_rotation (& smem.tform_view);
gte_matrix_set_translation(& smem.tform_view);
U4 prim_base = u4_(pa->buf[smem.active_buf_id]); U4 prim_base = u4_(pa->buf[smem.active_buf_id]);
U4 prim_cursor = prim_base + pa->used; U4 prim_cursor = prim_base + pa->used;
@@ -348,11 +249,11 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
// Prepare the tape. (Push protocol to tape) // Prepare the tape. (Push protocol to tape)
tb.used = 0; tb_scope(& tb) { tb.used = 0; tb_scope(& tb) {
// tb_emit(& tb, set_gte_mt3s2s4); tb_emit(& tb, set_gte_world);
// tb_data(& tb, u4_(& smem.tform_view)); tb_data(& tb, u4_(& smem.tform_world));
tb_emit(& tb, rbind_floor_f3_face); tb_emit(& tb, rbind_floor_f3_face);
// TODO(Ed): Just use a single context struct ref? // TODO(Ed): Just use a single context struct ref
tb_data(& tb, prim_cursor); tb_data(& tb, prim_cursor);
tb_data(& tb, u4_(smem.floor.faces)); tb_data(& tb, u4_(smem.floor.faces));
tb_data(& tb, u4_(smem.floor.verts)); tb_data(& tb, u4_(smem.floor.verts));
@@ -365,12 +266,13 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
tb_data(& tb, u4_(& pa->used)); tb_data(& tb, u4_(& pa->used));
tb_data(& tb, prim_base); tb_data(& tb, prim_base);
} }
tape_run(tb_slice(tb));// Fire off the tape (bigger-clobber variant). tape_run(tb_slice(tb));// Fire off the tape.
// C-side state (pa->used) has already been updated by the tape! // C-side state (pa->used) has already been updated by the tape!
// smem.floor.rot.y += 5; // smem.floor.rot.y += 5;
} }
} }
GCC_OPTIMIZATION_ENABLE
void render(void) { void render(void) {
} }
@@ -387,12 +289,26 @@ void gp_display_frame(DoubleBuffer* screen_buf, S4* active_buf_id, U4* ordering_
active_buf_id[0] = ! active_buf_id[0]; // Swap current buffer active_buf_id[0] = ! active_buf_id[0]; // Swap current buffer
} }
GCC_OPTIMIZATION_DISABLE
void hot_reload_entry(void)
{
smem.primitives.used = 0;
while (1) {
gknown S4* active_buf_id = & smem.active_buf_id;
gknown U4* ordering_buf = r_(smem.ordering_tbl)[active_buf_id[0]];
gknown PrimitiveArena* pa = & smem.primitives;
update(pa, ordering_buf);
render();
gp_display_frame(& smem.screen_buf, active_buf_id, ordering_buf, pa);
}
}
int main(void) int main(void)
{ {
smem = (SMemory){0}; smem = (SMemory){0};
smem.scratchpad = C_(U4_V, 0x1F800000);
// smem.primitives.used = 0; // smem.primitives.used = 0;
// smem.active_buf_id = 0; // smem.active_buf_id = 0;
smem.cam.pos = v3s4(500, -1000, -1500);
/*Persistent Entity Setup*/{ /*Persistent Entity Setup*/{
ent_cube128_init(& smem.cube.verts, & smem.cube.faces); { ent_cube128_init(& smem.cube.verts, & smem.cube.faces); {
Ent_Cube* cube = & smem.cube; Ent_Cube* cube = & smem.cube;
@@ -412,10 +328,6 @@ int main(void)
reset_graph(0); reset_graph(0);
/* Direct BIOS: poll both ports during VBlank. */ /* Direct BIOS: poll both ports during VBlank. */
pad_bios_init_start(& smem.pad_raw[0], & smem.pad_raw[1]); pad_bios_init_start(& smem.pad_raw[0], & smem.pad_raw[1]);
compile_init_atoms();
compile_resolve_look_at();
/* Pinned registers for the GPU init atom. */ /* Pinned registers for the GPU init atom. */
register U4* io_base_addr rgcc(R_IO_BaseAddr) = u4_r(IO_BASE_ADDR); register U4* io_base_addr rgcc(R_IO_BaseAddr) = u4_r(IO_BASE_ADDR);
register DoubleBuffer* screen_buf rgcc(R_ScreenBuf) = & smem.screen_buf; register DoubleBuffer* screen_buf rgcc(R_ScreenBuf) = & smem.screen_buf;
@@ -424,13 +336,7 @@ int main(void)
tb_emit(& tb, gp_screen_init); tb_emit(& tb, gp_screen_init);
} }
} }
while (1) { hot_reload_entry();
gknown S4* active_buf_id = & smem.active_buf_id;
gknown U4* ordering_buf = r_(smem.ordering_tbl)[active_buf_id[0]];
gknown PrimitiveArena* pa = & smem.primitives;
update(pa, ordering_buf);
render();
gp_display_frame(& smem.screen_buf, active_buf_id, ordering_buf, pa);
};
return 0; return 0;
} }
GCC_OPTIMIZATION_ENABLE
+8 -8
View File
@@ -21,6 +21,12 @@ enum {
ScreenRes_CenterY = (ScreenRes_Y >> 1), ScreenRes_CenterY = (ScreenRes_Y >> 1),
}; };
enum {
fp_one = (1 << 12),
};
#define v3s4_fp_one() v3s4(fp_one, fp_one, fp_one)
typedef U4 OrderingTable_Buffer[OrderingTbl_Len]; typedef U4 OrderingTable_Buffer[OrderingTbl_Len];
typedef Array_(OrderingTable_Buffer, 2); typedef Array_(OrderingTable_Buffer, 2);
@@ -61,7 +67,7 @@ I_ void ent_cube128_init(A8_V3_S2* verts, A6_V4_S2* faces) {
typedef Struct_(Ent_Cube) { typedef Struct_(Ent_Cube) {
V3_S4 accel; V3_S4 accel;
V3_S4 vel; V3_S4 vel;
V3_S4 pos; // RGA(Lengyel): affine point with implicit weight one. Storage alias of V3_S4. V3_S4 pos;
V3_S4 scale; V3_S4 scale;
V3_S2 rot; V3_S2 rot;
A8_V3_S2 verts; A8_V3_S2 verts;
@@ -88,15 +94,9 @@ I_ void ent_floor_init(A4_V3_S2* verts, A2_V3_S2* faces) {
}; };
typedef Struct_(Ent_Floor) { typedef Struct_(Ent_Floor) {
V3_S4 accel; V3_S4 accel;
V3_S4 pos; // RGA(Lengyel): affine point with implicit weight one. Storage alias of V3_S4. V3_S4 pos;
V3_S4 scale; V3_S4 scale;
V3_S2 rot; V3_S2 rot;
A4_V3_S2 verts; A4_V3_S2 verts;
A2_V3_S2 faces; A2_V3_S2 faces;
}; };
typedef Struct_(Camera) {
P3_S4 pos; // RGA(Lengyel): affine point with implicit weight one. Storage alias of V3_S4.
V3_S2 rot;
MT3_S2S4 look_at;
};
+6 -6
View File
@@ -24,8 +24,8 @@ ATOM_FILE_DEBUGGER_LINE_MARKER(hello_joypad_atom_c);
#pragma region MACs (Mips Atom components) #pragma region MACs (Mips Atom components)
FI_ Slice_MipsCode ac_put_disp_env(MipsAtomBuilder_R ab, U4 reg_transfer, U4 reg_base, U2 port) FI_ Slice_MipsCode ac_put_disp_env(U4 reg_transfer, U4 reg_base, U2 port)
MipsAtomComp_Proc_(ab, { MipsAtomComp_Proc_(ac_put_disp_env, {
// Emits 5 GP0 commands for buffer 0 (display_area = (0,0,320,240)). // Emits 5 GP0 commands for buffer 0 (display_area = (0,0,320,240)).
// Sequence per libpsyx PutDispEnv: DrawArea TL → DrawArea BR → Mask → DrawArea TL → DrawArea BR // Sequence per libpsyx PutDispEnv: DrawArea TL → DrawArea BR → Mask → DrawArea TL → DrawArea BR
mac_gcmd_push(gp0_word_draw_area_top_left_origin, reg_transfer, reg_base, port), mac_gcmd_push(gp0_word_draw_area_top_left_origin, reg_transfer, reg_base, port),
@@ -35,8 +35,8 @@ MipsAtomComp_Proc_(ab, {
mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port), mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port),
}) })
FI_ Slice_MipsCode ac_put_draw_env(MipsAtomBuilder_R ab, U4 reg_transfer, U4 reg_base, U2 port) FI_ Slice_MipsCode ac_put_draw_env(U4 reg_transfer, U4 reg_base, U2 port)
MipsAtomComp_Proc_(ab, { MipsAtomComp_Proc_(ac_put_draw_env, {
/* /*
* ORIGIN: each code word corresponds to the EXACT value libpsyx's PutDrawEnv function would compute for the same DrawEnv settings. * ORIGIN: each code word corresponds to the EXACT value libpsyx's PutDrawEnv function would compute for the same DrawEnv settings.
* References: * References:
@@ -116,7 +116,7 @@ internal MipsAtom_(screen_env_init) atom_info(atom_phase(screen_init)
store_word(R_0, R_ScreenBuf, O_(DisplayEnv,vinterlace) + OA_(DoubleBuffer,display,1)), store_word(R_0, R_ScreenBuf, O_(DisplayEnv,vinterlace) + OA_(DoubleBuffer,display,1)),
mac_store_rects2(R_0, R_ScreenY, R_ScreenX, R_ScreenY, R_ScreenBuf, O_(DrawEnv,clip_area) + OA_(DoubleBuffer,draw,0)), /* draw[0].clip_area = (0, 240, 320, 240). C11's SetDefDrawEnv writes clip.y = y_arg. */ mac_store_rects2(R_0, R_ScreenY, R_ScreenX, R_ScreenY, R_ScreenBuf, O_(DrawEnv,clip_area) + OA_(DoubleBuffer,draw,0)), /* draw[0].clip_area = (0, 240, 320, 240). C11's SetDefDrawEnv writes clip.y = y_arg. */
mac_store_v2s2(R_0, R_ScreenY, R_ScreenBuf, O_(DrawEnv,drawing_offset[0]) + OA_(DoubleBuffer,draw,0)), /* draw[0].drawing_offset[0] = (0, 240); C11 passes y_arg as ofs. */ mac_store_v2s2( R_0, R_ScreenY, R_ScreenBuf, O_(DrawEnv,drawing_offset[0]) + OA_(DoubleBuffer,draw,0)), /* draw[0].drawing_offset[0] = (0, 240); C11 passes y_arg as ofs. */
mac_store_v2s2(R_ScreenX, R_ScreenY, R_ScreenBuf, O_(DrawEnv,clip_area.width) + OA_(DoubleBuffer,draw,1)), mac_store_v2s2(R_ScreenX, R_ScreenY, R_ScreenBuf, O_(DrawEnv,clip_area.width) + OA_(DoubleBuffer,draw,1)),
@@ -286,7 +286,7 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
, atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase) , atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
, atom_writes(R_PrimCursor, R_FaceCursor) , atom_writes(R_PrimCursor, R_FaceCursor)
) { ) {
mac_load_tri_indices(R_FaceCursor, R_T0, R_T1, R_T2), mac_load_tri_indices( R_FaceCursor, R_T0, R_T1, R_T2),
mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2), mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2),
nop2, gte_cmdw_rotate_translate_perspective_triple, // 2 nops retire the final cpu -> gte writes before RTPT nop2, gte_cmdw_rotate_translate_perspective_triple, // 2 nops retire the final cpu -> gte writes before RTPT
gte_cmdw_nclip, gte_cmdw_nclip,
+2 -2
View File
@@ -24,8 +24,8 @@
* Emits 9 instructions (status/buttons/axes/attempt stores plus the * Emits 9 instructions (status/buttons/axes/attempt stores plus the
* two-instruction zero-extended buttons load). * two-instruction zero-extended buttons load).
*/ */
FI_ Slice_MipsCode ac_pad_sio_write_pad_state(MipsAtomBuilder_R ab, U4 status_val, U4 state_ptr_reg, U4 scratch_reg) FI_ Slice_MipsCode ac_pad_sio_write_pad_state(U4 status_val, U4 state_ptr_reg, U4 scratch_reg)
MipsAtomComp_Proc_(ac_pad_sio_write_pad_state, ab, { MipsAtomComp_Proc_(ac_pad_sio_write_pad_state, {
add_ui(scratch_reg, R_0, status_val), add_ui(scratch_reg, R_0, status_val),
store_word(scratch_reg, state_ptr_reg, O_(PadState,status)), store_word(scratch_reg, state_ptr_reg, O_(PadState,status)),
/* FIX 2026-08-02: buttons = 0x0000FFFF = "no buttons pressed" in /* FIX 2026-08-02: buttons = 0x0000FFFF = "no buttons pressed" in
-10625
View File
File diff suppressed because one or more lines are too long
+258 -39
View File
@@ -1,3 +1,13 @@
# --- Parameter Surface (Task 8) -----------------------------------------
# -Reload : After a successful build, invoke reload.ps1 as a child pwsh and propagate its exit code.
# -HelperZipOnly : Skip the build entirely; regenerate the helper zip and exit. Honors -HelperZipOutput for out-of-tree paths.
# -HelperZipOutput: When -HelperZipOnly is set, writes the archive to this path instead of the scripts/pcsx_debug_helper.zip.
param(
[switch]$Reload,
[switch]$HelperZipOnly,
[string]$HelperZipOutput = ''
)
$path_root = split-path -Path $PSScriptRoot -Parent $path_root = split-path -Path $PSScriptRoot -Parent
$path_build = join-path $path_root 'build' $path_build = join-path $path_root 'build'
$path_code = join-path $path_root 'code' $path_code = join-path $path_root 'code'
@@ -8,6 +18,98 @@ if ((test-path $path_build) -eq $false) {
new-item -itemtype directory -path $path_build new-item -itemtype directory -path $path_build
} }
# --- HelperZipOnly short-circuit ----------------------------------------
# Must run before any compile/link work.
# Inlines the same logic as Make-HelperZip below to avoid an extra pwsh process spawn (~200 ms).
#The helper zip is small and the BCL call is in-process; cold ~14 ms, warm ~10 ms (assembly load + tiny zip write).
if ($HelperZipOnly) {
$zipDest = if ([string]::IsNullOrEmpty($HelperZipOutput)) {
join-path $path_scripts 'pcsx_debug_helper.zip'
}
else {
$HelperZipOutput
}
$HelperDir = join-path $path_scripts 'pcsx_debug_helper'
$elf32Src = join-path $path_scripts 'elf32.lua'
$elf32Dest = join-path $HelperDir 'elf32.lua'
if (-not (test-path -LiteralPath $HelperDir)) {
write-error "helper dir not found: $HelperDir"
exit 1
}
if (-not (test-path -LiteralPath $elf32Src)) {
write-error "elf32.lua not found at $elf32Src"
exit 1
}
write-host "[build] HelperZipOnly mode -> $zipDest"
# --- Timestamp gate (Fix 1) -------------------------------------------
# PCSX-Redux holds pcsx_debug_helper.zip open via -archive at startup.
# The zip is consumed once at startup; the reload endpoint reads it
# from package.loaded on subsequent calls. Writing it on every build
# is dead work that fights the file lock. Skip the rewrite when the
# three sources (autoexec.lua, reload.lua, elf32.lua) are all older
# than the existing zip.
$sources = @(
(join-path $HelperDir 'autoexec.lua'),
(join-path $HelperDir 'reload.lua'),
$elf32Src
)
$zipMtime = $null
if (test-path -LiteralPath $zipDest) {
$zipMtime = (Get-Item -LiteralPath $zipDest).LastWriteTime
}
$needsRewrite = $false
if ($null -eq $zipMtime) {
$needsRewrite = $true
}
else {
foreach ($s in $sources) {
if (-not (test-path -LiteralPath $s)) { continue }
if ((Get-Item -LiteralPath $s).LastWriteTime -gt $zipMtime) {
$needsRewrite = $true
break
}
}
}
if (-not $needsRewrite) {
$sz = (Get-Item -LiteralPath $zipDest).Length
Write-Host "[build] helper zip up to date: $zipDest ($sz bytes); skipping"
return
}
Copy-Item -LiteralPath $elf32Src -Destination $elf32Dest -Force
try {
# Force the inode release so CreateFromDirectory can write fresh.
# ZipFile.CreateFromDirectory throws if the destination exists.
# If PCSX-Redux holds the file open, Remove-Item raises — fall
# back to writing pcsx_debug_helper.zip.new alongside. The next
# PCSX-Redux restart will read the canonical path; the .new file
# is a hint for the optional launch-script patch in fix 3.
if (test-path -LiteralPath $zipDest) {
try {
# -ErrorAction Stop is required so the catch below fires.
# Remove-Item raises a non-terminating error by default
# (ErrorActionPreference=Continue), which bypasses catch.
Remove-Item -LiteralPath $zipDest -Force -ErrorAction Stop
}
catch {
$zipDest = [System.IO.Path]::ChangeExtension($zipDest, '.zip.new')
Write-Warning "[build] canonical helper zip is locked; writing to $zipDest instead"
}
}
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::CreateFromDirectory(
$HelperDir, $zipDest,
[System.IO.Compression.CompressionLevel]::Optimal, $false) | Out-Null
$sz = (Get-Item -LiteralPath $zipDest).Length
Write-Host "[build] wrote $sz bytes to $zipDest"
}
finally {
if (test-path -LiteralPath $elf32Dest) { Remove-Item -LiteralPath $elf32Dest -Force }
}
return
}
# --- Toolchain Definition --- # --- Toolchain Definition ---
# Assumes 'mipsel-none-elf' toolchain is in your system's PATH. # Assumes 'mipsel-none-elf' toolchain is in your system's PATH.
$Prefix = "mipsel-none-elf" $Prefix = "mipsel-none-elf"
@@ -180,9 +282,12 @@ function link-modules { param([string[]]$link_modules, [string] $elf, [string[]
$link_args += ($f_link_pass_through_prefix + $f_link_mapfile + $map) $link_args += ($f_link_pass_through_prefix + $f_link_mapfile + $map)
$link_args += ($f_link_pass_through_prefix + $f_link_start_group) $link_args += ($f_link_pass_through_prefix + $f_link_start_group)
# 16 removed entries (c2, card, cd, comb, ds, gs, gun, hmd, math, mcrd, mcx, press, sio, snd, spu, tap) # raw_sio_pad_poll_20260802 — Task 5.1c surgical library-list trim.
# had LOAD lines in the map but ZERO .o files pulled in — they were unused. # The 16 removed entries (c2, card, cd, comb, ds, gs, gun, hmd, math,
# 5 kept libraries (api, c, etc, gpu, gte) are required by the C-side calls in hello_joypad.c (reset_graph, draw_sync, vsync, etc.). # mcrd, mcx, press, sio, snd, spu, tap) had LOAD lines in the map but
# ZERO .o files pulled in — they were unused. The 5 kept libraries
# (api, c, etc, gpu, gte) are required by the C-side calls in
# hello_joypad.c (reset_graph, draw_sync, vsync, etc.).
$libraries = @( $libraries = @(
"api", "api",
"c", "c",
@@ -217,14 +322,16 @@ function make-binary { param([string]$elf, [string]$exe)
} }
function ps1-meta { param( function ps1-meta { param(
[string]$unity_root, [string] $unity_root,
[string[]]$sources, [string[]]$sources,
[Parameter(Mandatory=$true)][string]$metadata, [Parameter(Mandatory=$true)][string]$metadata,
[string]$out_root = (join-path $path_build 'gen'), [string] $out_root = (join-path $path_build 'gen'),
[string[]]$passes = @('--pre-link'), [string[]]$passes = @('--pre-link'),
[string[]]$extra_args = @() [string[]]$extra_args = @()
) )
# `--unity-root` and `--source` are mutually exclusive. Exactly one of `$unity_root` / `$sources` must be supplied; the other must be absent. # `--unity-root` and `--source` are
# mutually exclusive. Exactly one of `$unity_root` / `$sources` must
# be supplied; the other must be absent.
if ($null -ne $unity_root -and $unity_root -ne '') if ($null -ne $unity_root -and $unity_root -ne '')
{ {
if ($null -ne $sources -and $sources.Count -gt 0) { if ($null -ne $sources -and $sources.Count -gt 0) {
@@ -237,6 +344,40 @@ function ps1-meta { param(
exit 2 exit 2
} }
# --- Defensive attribute clear on tracked gen files ------------------------
# Git tracks code/<dir>/gen/*.h files and Windows keeps the Archive bit set
# on them. Combined with transient editor locks or co-running processes,
# this can make io.open(path, "wb") fail with Access Denied / Sharing
# Violation even though Get-ChildItem shows IsReadOnly = False. Clearing
# the Read-only + Archive bits locally is safe; git re-asserts them on
# the next operation but the metaprogram write always wins.
#
# Derived from the caller's parameters: $metadata lives in $path_duffle
# (so its parent is the duffle dir), and $unity_root / $sources[0] lives
# in $path_module (so its parent is the module dir).
$pathToDuffle = split-path -Path $metadata -Parent
$pathToModule = $null
if ($null -ne $unity_root -and $unity_root -ne '') {
$pathToModule = split-path -Path $unity_root -Parent
}
elseif ($null -ne $sources -and $sources.Count -gt 0) {
$pathToModule = split-path -Path $sources[0] -Parent
}
$genFiles = @(
join-path $pathToDuffle 'gen\macs.h'
join-path $pathToDuffle 'gen\offsets.h'
)
if ($null -ne $pathToModule) {
$genFiles += join-path $pathToModule 'gen\macs.h'
$genFiles += join-path $pathToModule 'gen\offsets.h'
}
foreach ($f in $genFiles) {
if (test-path -LiteralPath $f) {
attrib -R $f 2>&1 | Out-Null
attrib -A $f 2>&1 | Out-Null
}
}
$script = join-path $path_scripts 'ps1_meta.lua' $script = join-path $path_scripts 'ps1_meta.lua'
$input_summary = if ($null -ne $unity_root -and $unity_root -ne '') { $input_summary = if ($null -ne $unity_root -and $unity_root -ne '') {
"unity=$unity_root" "unity=$unity_root"
@@ -517,7 +658,7 @@ function build-hello_camera {
$path_build_gen = join-path $path_build 'gen' $path_build_gen = join-path $path_build 'gen'
$src_c = join-path $path_module 'hello_camera.c' $src_c = join-path $path_module 'hello_camera.c'
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen -passes @('--pre-link') ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen
$assemble_args = @() $assemble_args = @()
$assemble_args += $f_debug $assemble_args += $f_debug
@@ -532,7 +673,6 @@ function build-hello_camera {
$compile_args = @() $compile_args = @()
$compile_args += $f_debug $compile_args += $f_debug
$compile_args += ($f_define + 'BUILD_DEBUG')
$compile_args += $f_optimize_none $compile_args += $f_optimize_none
# $compile_args += $f_optimize_intrinsics # $compile_args += $f_optimize_intrinsics
# $compile_args += $f_optimize_size # $compile_args += $f_optimize_size
@@ -553,50 +693,129 @@ function build-hello_camera {
link-modules $link_modules $elf $link_args link-modules $link_modules $elf $link_args
make-binary $elf $exe make-binary $elf $exe
# Post-link: gdb-runtime + dwarf-injection in a single Lua invocation (one luajit cold start).
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen -passes @('--post-link') ` -extra_args @('--elf', $elf) ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen -passes @('--post-link') ` -extra_args @('--elf', $elf)
inject-dwarf $elf $path_build_gen inject-dwarf $elf $path_build_gen
} }
build-hello_camera build-hello_camera
# NO idea if this works yet... # ── Helper-zip + reload helpers (Task 8) ──
function Send-ToEmulator { param( [string]$exePath ) # Defined right after the final build-hello_camera function so they're in scope for the post-build calls below.
$uri = "http://localhost:8080/api/v1/load-exec" # The Make-HelperZip function is also reused by the -HelperZipOnly short-circuit at the top of this script.
# Both call the in-process BCL CreateFromDirectory rather than spawning a child pwsh to avoid the ~200 ms process-spawn overhead.
function Make-HelperZip {
param([string]$OutputPath = '')
# Absolute path is safest for the emulator web server $dest = if ([string]::IsNullOrEmpty($OutputPath)) {
$absolutePath = [System.IO.Path]::GetFullPath($exePath) join-path $path_scripts 'pcsx_debug_helper.zip'
}
else {
$OutputPath
}
# Create JSON payload pointing to your compiled .ps-exe $HelperDir = join-path $path_scripts 'pcsx_debug_helper'
$body = @{ filename = $absolutePath } | ConvertTo-Json $elf32Src = join-path $path_scripts 'elf32.lua'
$elf32Dest = join-path $HelperDir 'elf32.lua'
if (-not (test-path -LiteralPath $HelperDir)) {
write-warning "[build] helper dir not found: $HelperDir; skipping helper zip"
return
}
if (-not (test-path -LiteralPath $elf32Src)) {
write-warning "[build] elf32.lua not found at $elf32Src; skipping helper zip"
return
}
Write-Host "Pushing hot-reload to PCSX-Redux..." -ForegroundColor Magenta # --- Timestamp gate (Fix 1) -------------------------------------------
# PCSX-Redux holds pcsx_debug_helper.zip open via -archive at startup.
# The zip is consumed once at startup; the reload endpoint reads it
# from package.loaded on subsequent calls. Writing it on every build
# is dead work that fights the file lock. Skip the rewrite when the
# three sources (autoexec.lua, reload.lua, elf32.lua) are all older
# than the existing zip.
$sources = @(
(join-path $HelperDir 'autoexec.lua'),
(join-path $HelperDir 'reload.lua'),
$elf32Src
)
$zipMtime = $null
if (test-path -LiteralPath $dest) {
$zipMtime = (Get-Item -LiteralPath $dest).LastWriteTime
}
$needsRewrite = $false
if ($null -eq $zipMtime) {
$needsRewrite = $true
}
else {
foreach ($s in $sources) {
if (-not (test-path -LiteralPath $s)) { continue }
if ((Get-Item -LiteralPath $s).LastWriteTime -gt $zipMtime) {
$needsRewrite = $true
break
}
}
}
if (-not $needsRewrite) {
$sz = (Get-Item -LiteralPath $dest).Length
Write-Host "[build] helper zip up to date: $dest ($sz bytes); skipping"
return
}
write-host "[build] regenerating helper zip -> $dest"
Copy-Item -LiteralPath $elf32Src -Destination $elf32Dest -Force
try { try {
$response = Invoke-RestMethod -Uri $uri -Method Post -Body $body -ContentType "application/json" # Force the inode release so CreateFromDirectory can write fresh.
Write-Host "Hot-reload successful!" -ForegroundColor Green # ZipFile.CreateFromDirectory throws if the destination exists.
} catch { # If PCSX-Redux holds the file open, Remove-Item raises — fall
Write-Warning "Could not connect to PCSX-Redux web server. Ensure the emulator is running and Web Server is enabled." # back to writing pcsx_debug_helper.zip.new alongside. The next
# PCSX-Redux restart will read the canonical path; the .new file
# is a hint for the optional launch-script patch in fix 3.
if (test-path -LiteralPath $dest) {
try {
# -ErrorAction Stop is required so the catch below fires.
# Remove-Item raises a non-terminating error by default
# (ErrorActionPreference=Continue), which bypasses catch.
Remove-Item -LiteralPath $dest -Force -ErrorAction Stop
}
catch {
$dest = [System.IO.Path]::ChangeExtension($dest, '.zip.new')
Write-Warning "[build] canonical helper zip is locked; writing to $dest instead"
}
}
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::CreateFromDirectory(
$HelperDir, $dest,
[System.IO.Compression.CompressionLevel]::Optimal, $false) | Out-Null
$sz = (Get-Item -LiteralPath $dest).Length
Write-Host "[build] wrote $sz bytes to $dest"
}
finally {
if (test-path -LiteralPath $elf32Dest) { Remove-Item -LiteralPath $elf32Dest -Force }
} }
} }
# # Automatically hot-reloads it into the running emulator # Invokes reload.ps1 as a child pwsh instead of POSTing to the nonexistent /api/v1/load-exec endpoint.
# Send-ToEmulator (join-path $path_build 'hello_gte.ps-exe') # Exit code is propagated so the build fails loud if the reload fails.
function Send-ToEmulator {
param([string]$ElfPath = (join-path $path_build 'hello_camera.elf'))
# --- Hot Reload via PCSX-Redux Web Server --- $reloadScript = join-path $path_scripts 'reload.ps1'
# $exe_path = join-path $path_build 'hello_gte.ps-exe' if (-not (test-path -LiteralPath $reloadScript)) {
# $absolute_path = [System.IO.Path]::GetFullPath($exe_path) write-error "[build] reload.ps1 not found at $reloadScript"
exit 1
}
# PCSX-Redux expects the file location in the URL query string? write-host "[build] hot-reloading $ElfPath via reload.ps1" -ForegroundColor Magenta
# We URL-encode the path to ensure backslashes and spaces don't break the HTTP request? & pwsh -NoProfile -File $reloadScript -Mode elf -Target hello_camera -ElfPath $ElfPath
# $encoded_path = [uri]::EscapeDataString($absolute_path) if ($LASTEXITCODE -ne 0) {
# $uri = "http://localhost:8080/api/v1/load-exec?path=$encoded_path" write-error "[build] reload.ps1 failed (exit $LASTEXITCODE)"
exit $LASTEXITCODE
}
}
# Write-Host "Pushing hot-reload to PCSX-Redux..." -ForegroundColor Magenta # Post-build: Regenerate the helper zip (canonical output) and, if -Reload was passed, kick a hot-reload against the just-built ELF.
# try { # Any future targets compiled by this script should add their own Make-HelperZip call after their build step; today's only target is hello_camera.
# # Send the request with the query string included Make-HelperZip
# Invoke-RestMethod -Uri $uri -Method Post if ($Reload) {
# Write-Host "Hot-reload successful!" -ForegroundColor Green Send-ToEmulator
# } catch { }
# Write-Host "Failed to hot-reload." -ForegroundColor Red
# # This will print the *actual* HTTP error instead of our generic warning
# Write-Host $_.Exception.Message -ForegroundColor Yellow
# }
+2597 -73
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-866
View File
@@ -1,866 +0,0 @@
--- duffle_isa.lua — encoder / GTE / hardware tables.
--- @class InstructionImm
--- @field arg integer
--- @field signed boolean|nil
--- @field width integer
--- @class InstructionValue
--- @field dest integer
--- @field op string
--- @field sources integer[]|nil
--- @field immediate integer|nil
--- @field source integer|nil
--- @class InstructionRow
--- @field cycles integer
--- @field kind string
--- @field reads integer[]|nil
--- @field writes integer[]|nil
--- @field imm InstructionImm[]|nil
--- @field value InstructionValue|nil
--- @field delay_slot boolean|nil
--- @field suppress_arg1 table<string, string>|nil -- bag: GPR ident -> reason
--- @class TapeAtomMacroRow
--- @field kind string
--- @field binds boolean
--- @class GteCommandPort
--- @field register string
--- @field role string
--- @class GteCommandLatch
--- @field register string
--- @field required integer
--- @class GteCommandRow
--- @field aliases string[]
--- @field cycles integer
--- @field inputs string[]
--- @field outputs GteCommandPort[]
--- @field latch GteCommandLatch[]
--- @class GteCrAliasGroup
--- @field [1] integer -- C2 control-register slot
--- @field [2] string[] -- aliases that share that slot
--- @class GtePackedSlotRelation
--- @field slot integer
--- @field first string
--- @field second string
--- @class HardwareRelationPort
--- @field domain string
--- @field arg integer
--- @class HardwareRelationVisibility
--- @field kind string
--- @field required integer
--- @class HardwareRelationEvidence
--- @field confidence string
--- @field source string
--- @class HardwareRelationRow
--- @field id string
--- @field semantic string
--- @field consumer string
--- @field token string
--- @field direction string
--- @field reads HardwareRelationPort
--- @field writes HardwareRelationPort
--- @field visibility HardwareRelationVisibility|nil
--- @field evidence HardwareRelationEvidence
--- @field violation_kind string
--- @field destination_match string|nil
--- @field fanout_to string[]|nil
--- @field required integer|nil
--- @field clear_on_consumer boolean|nil
--- @field stage boolean|nil
--- @field cu2_transition boolean|nil
--- @field status_register integer|nil
--- @class Cu2TransitionPolicy
--- @field status_register integer
--- @field enable_bit integer
--- @field required integer
--- @field visibility_kind string
--- @field evidence HardwareRelationEvidence
--- @class GprRole
--- @field name string
--- @field pool boolean
--- @field optional boolean
--- @field carrier boolean
--- @class DuffleIsa
--- @field GPR_ROLE table<string, GprRole>
--- @field TAPE_ATOM_MACROS table<string, TapeAtomMacroRow>
--- @field DELAY_MARKERS table<string, boolean>
--- @field INSTRUCTION table<string, InstructionRow>
--- @field GTE_COMMAND table<string, GteCommandRow>
--- @field ALIAS_TO_CANONICAL table<string, string>
--- @field instr fun(ident: string): InstructionRow|nil
--- @field gte_canon fun(ident: string): string
--- @field gte fun(ident: string): GteCommandRow|nil
--- @field GTE_CR_ALIAS_GROUPS GteCrAliasGroup[]
--- @field GTE_PACKED_SLOT_RELATIONS GtePackedSlotRelation[]
--- @field OPERAND_READ_POSITIONS table<string, integer[]>
--- @field GP0_CMD_SIZE table<integer, integer>
--- @field GP0_CMD_BY_SHAPE table<string, integer>
--- @field UNKNOWN_INSTRUCTION_CYCLES integer
--- @field HARDWARE_RELATIONS HardwareRelationRow[]
--- @field CU2_TRANSITION_POLICY Cu2TransitionPolicy
local M = {} ---@type DuffleIsa
-- Section 7: domain tables
-- ════════════════════════════════════════════════════════════════════════════
-- One GprRole row per name. Construction order is the auto_reg pool order,
-- then R_AT, then the three carriers. Index by name into M.GPR_ROLE.
--- @type table<string, GprRole>
M.GPR_ROLE = {
{ name = "R_V0", pool = true, optional = true, carrier = false },
{ name = "R_V1", pool = true, optional = true, carrier = false },
{ name = "R_T0", pool = true, optional = true, carrier = false },
{ name = "R_T1", pool = true, optional = true, carrier = false },
{ name = "R_T2", pool = true, optional = true, carrier = false },
{ name = "R_T3", pool = true, optional = true, carrier = false },
{ name = "R_T4", pool = true, optional = true, carrier = false },
{ name = "R_T5", pool = true, optional = true, carrier = false },
{ name = "R_T6", pool = true, optional = true, carrier = false },
{ name = "R_T7", pool = true, optional = true, carrier = false },
{ name = "R_A0", pool = true, optional = true, carrier = false },
{ name = "R_A1", pool = true, optional = true, carrier = false },
{ name = "R_A2", pool = true, optional = true, carrier = false },
{ name = "R_A3", pool = true, optional = true, carrier = false },
{ name = "R_S0", pool = true, optional = true, carrier = false },
{ name = "R_S1", pool = true, optional = true, carrier = false },
{ name = "R_S2", pool = true, optional = true, carrier = false },
{ name = "R_S3", pool = true, optional = true, carrier = false },
{ name = "R_S4", pool = true, optional = true, carrier = false },
{ name = "R_S5", pool = true, optional = true, carrier = false },
{ name = "R_S6", pool = true, optional = true, carrier = false },
{ name = "R_S7", pool = true, optional = true, carrier = false },
{ name = "R_T8", pool = true, optional = true, carrier = false },
{ name = "R_T9", pool = true, optional = true, carrier = false },
{ name = "R_AT", pool = false, optional = true, carrier = false },
{ name = "R_TapePtr", pool = false, optional = true, carrier = true },
{ name = "R_AtomJmp", pool = false, optional = true, carrier = true },
{ name = "R_ScratchBase", pool = false, optional = true, carrier = true },
}
for _, row in ipairs(M.GPR_ROLE) do ---@type integer, GprRole
M.GPR_ROLE[row.name] = row
end
-- atom_info sub-calls: atom_bind, atom_reads, atom_writes, atom_view, atom_reg_types, atom_ctx, atom_phase.
--- @type table<string, TapeAtomMacroRow>
M.TAPE_ATOM_MACROS = {
["atom_info"] = { kind = "info", binds = false },
}
-- Empty C macros that prefix the next encoder. Zero words.
-- BdSlot_ nop is one nop word. The marker is not the BD instruction.
--- @type table<string, boolean> -- bag: marker prefix -> true
M.DELAY_MARKERS = {
["GteDelay_"] = true,
["LdSlot_"] = true,
["BdSlot_"] = true,
["DmaSlot_"] = true,
}
-- One row per encoder. Read through duffle.instr.
--- @type table<string, InstructionRow>
M.INSTRUCTION = {
["BdSlot_"] = { cycles = 0, kind = "marker", },
["LdSlot_"] = { cycles = 0, kind = "marker", },
["add_s"] = { cycles = 1, kind = "alu", },
["add_si"] = { cycles = 1, kind = "alu", reads = { 1, 2 }, writes = { 1 }, imm = { { arg = 3, signed = true, width = 16, },}, },
["add_u"] = { cycles = 1, kind = "alu", reads = { 2, 3 }, writes = { 1 }, },
["add_u_self"] = { cycles = 1, kind = "alu", reads = { 1, 2 }, writes = { 1 }, value = { dest = 1, op = "add_u", sources = { 1, 2 }, }, },
["add_ui"] = { cycles = 1, kind = "alu", reads = { 1, 2 }, writes = { 1 }, imm = { { arg = 3, signed = true, width = 16, }, }, value = { dest = 1, immediate = 3, op = "add_ui", source = 2, }, },
["add_ui_self"] = { cycles = 1, kind = "alu", reads = { 1 }, writes = { 1 }, imm = { { arg = 2, signed = true, width = 16, }, }, value = { dest = 1, immediate = 2, op = "add_ui", source = 1, }, },
["and"] = { cycles = 1, kind = "alu", },
["and_i"] = { cycles = 1, kind = "alu", reads = { 1, 2 }, writes = { 1 }, imm = { { arg = 3, width = 16, }, }, value = { dest = 1, immediate = 3, op = "and_i", source = 2, }, },
["and_u"] = { cycles = 1, kind = "alu", reads = { 2, 3 }, writes = { 1 }, },
["atom_bind"] = { cycles = 0, kind = "marker", reads = {}, writes = {}, },
["atom_info"] = { cycles = 0, kind = "marker", reads = {}, writes = {}, },
["atom_label"] = { cycles = 0, kind = "marker", reads = {}, writes = {}, },
["atom_offset"] = { cycles = 0, kind = "marker", reads = {}, writes = {}, },
["atom_reads"] = { cycles = 0, kind = "marker", reads = {}, writes = {}, },
["atom_writes"] = { cycles = 0, kind = "marker", reads = {}, writes = {}, },
["branch_equal"] = { cycles = 2, kind = "branch", reads = { 1, 2 }, writes = {}, imm = { { arg = 3, signed = true, width = 16, }, }, },
["branch_ge_zero"] = { cycles = 2, kind = "branch", reads = { 1 }, writes = {}, imm = { { arg = 2, signed = true, width = 16, }, }, },
["branch_gt_zero"] = { cycles = 2, kind = "branch", reads = { 1 }, writes = {}, imm = { { arg = 2, signed = true, width = 16, }, }, },
["branch_le_zero"] = { cycles = 2, kind = "branch", reads = { 1 }, writes = {}, imm = { { arg = 2, signed = true, width = 16, }, }, },
["branch_lt_zero"] = { cycles = 2, kind = "branch", reads = { 1 }, writes = {}, imm = { { arg = 2, signed = true, width = 16, }, }, },
["branch_ne"] = { cycles = 2, kind = "branch", reads = { 1, 2 }, writes = {}, imm = { { arg = 3, signed = true, width = 16, }, }, },
["call_addr"] = { cycles = 2, kind = "call", reads = {}, writes = { 1 }, },
["call_reg"] = { cycles = 2, kind = "call", reads = { 1 }, writes = { 2 }, },
["div_s"] = { cycles = 35, kind = "alu", reads = { 1, 2 }, writes = {}, },
["div_u"] = { cycles = 35, kind = "alu", reads = { 1, 2 }, writes = {}, },
["gte_load_v0"] = { cycles = 2, kind = "cop2_xfer", reads = { 2 }, writes = {}, },
["gte_load_v0v1v2"] = { cycles = 6, kind = "cop2_xfer", reads = { 2 }, writes = {}, },
["gte_load_v1"] = { cycles = 2, kind = "cop2_xfer", reads = { 2 }, writes = {}, },
["gte_load_v2"] = { cycles = 2, kind = "cop2_xfer", reads = { 2 }, writes = {}, },
["gte_lw"] = { cycles = 1, kind = "load", reads = { 2 }, writes = {}, },
["gte_lwc2"] = { cycles = 1, kind = "load", },
["gte_mv_from_ctrl_r"] = { cycles = 1, kind = "cop2_xfer", reads = {}, writes = { 1 }, },
["gte_mv_from_data_r"] = { cycles = 1, kind = "cop2_xfer", reads = {}, writes = { 1 }, },
["gte_mv_to_ctrl_r"] = { cycles = 1, kind = "cop2_xfer", reads = { 1 }, writes = {}, },
["gte_mv_to_data_r"] = { cycles = 1, kind = "cop2_xfer", reads = { 1 }, writes = {}, },
["gte_stotz"] = { cycles = 1, kind = "cop2_xfer", reads = {}, writes = {}, },
["gte_stsxy3"] = { cycles = 1, kind = "cop2_xfer", reads = {}, writes = {}, },
["gte_sw"] = { cycles = 1, kind = "store", reads = { 2 }, writes = {}, },
["gte_swc2"] = { cycles = 1, kind = "store", },
["jump"] = { cycles = 2, kind = "jump", reads = {}, writes = {}, },
["jump_link"] = { cycles = 2, kind = "call", reads = { 1 }, writes = { 2 }, },
["jump_reg"] = { cycles = 2, kind = "jump", reads = { 1 }, writes = {}, suppress_arg1 = { R_AtomJmp = "fixed mac_yield handshake", }, },
["jump_rel"] = { cycles = 2, kind = "branch", delay_slot = true, },
["li_s"] = { cycles = 1, kind = "alu", reads = { 1, 2 }, writes = { 1 }, value = { dest = 1, immediate = 3, op = "add_ui", source = 2, }, },
["load_byte"] = { cycles = 1, kind = "load", reads = { 2 }, writes = { 1 }, imm = { { arg = 3, signed = true, width = 16, }, }, },
["load_byte_u"] = { cycles = 1, kind = "load", reads = { 2 }, writes = { 1 }, imm = { { arg = 3, signed = true, width = 16, }, }, },
["load_half"] = { cycles = 1, kind = "load", reads = { 2 }, writes = { 1 }, imm = { { arg = 3, signed = true, width = 16, }, }, },
["load_half_u"] = { cycles = 1, kind = "load", reads = { 2 }, writes = { 1 }, imm = { { arg = 3, signed = true, width = 16, }, }, },
["load_imm"] = { cycles = 2, kind = "alu", reads = {}, writes = { 1 }, },
["load_ui"] = { cycles = 1, kind = "alu", reads = {}, writes = { 1 }, },
["load_upper_i"] = { cycles = 1, kind = "alu", reads = {}, writes = { 1 }, imm = { { arg = 2, width = 16, }, }, value = { dest = 1, immediate = 2, op = "load_upper_i", }, },
["load_word"] = { cycles = 1, kind = "load", reads = { 2 }, writes = { 1 }, imm = { { arg = 3, signed = true, width = 16, }, }, },
["mac_yield"] = { cycles = 0, kind = "marker", reads = {}, writes = {}, },
["mask_upper"] = { cycles = 1, kind = "alu", reads = { 1, 2 }, writes = { 1 }, },
["mov_from_high"] = { cycles = 2, kind = "alu", reads = {}, writes = { 1 }, },
["mov_from_low"] = { cycles = 2, kind = "alu", reads = {}, writes = { 1 }, },
["mov_to_high"] = { cycles = 1, kind = "alu", reads = { 1 }, writes = {}, },
["mov_to_low"] = { cycles = 1, kind = "alu", reads = { 1 }, writes = {}, },
["mult_s"] = { cycles = 12, kind = "alu", reads = { 1, 2 }, writes = {}, },
["mult_u"] = { cycles = 12, kind = "alu", reads = { 1, 2 }, writes = {}, },
["nop"] = { cycles = 1, kind = "nop", reads = {}, writes = {}, },
["nop2"] = { cycles = 2, kind = "nop", reads = {}, writes = {}, },
["nor_u"] = { cycles = 1, kind = "alu", },
["or_i"] = { cycles = 1, kind = "alu", reads = { 1, 2 }, writes = { 1 }, imm = { { arg = 3, width = 16, }, }, value = { dest = 1, immediate = 3, op = "or_i", source = 2, }, },
["or_i_self"] = { cycles = 1, kind = "alu", reads = { 1 }, writes = { 1 }, imm = { { arg = 2, width = 16, }, }, value = { dest = 1, immediate = 2, op = "or_i", source = 1, }, },
["or_u"] = { cycles = 1, kind = "alu", reads = { 2, 3 }, writes = { 1 }, },
["or_u_self"] = { cycles = 1, kind = "alu", reads = { 1, 2 }, writes = { 1 }, value = { dest = 1, op = "or", sources = { 1, 2 }, }, },
["set_lt_s"] = { cycles = 1, kind = "alu", reads = { 2, 3 }, writes = { 1 }, },
["set_lt_si"] = { cycles = 1, kind = "alu", reads = { 1, 2 }, writes = { 1 }, },
["set_lt_u"] = { cycles = 1, kind = "alu", reads = { 2, 3 }, writes = { 1 }, },
["set_lt_ui"] = { cycles = 1, kind = "alu", reads = { 1, 2 }, writes = { 1 }, },
["shift_aright"] = { cycles = 1, kind = "alu", reads = { 2 }, writes = { 1 }, imm = { { arg = 3, width = 5, }, }, },
["shift_aright_var"] = { cycles = 1, kind = "alu", reads = { 2, 3 }, writes = { 1 }, imm = { { arg = 3, width = 5, }, }, },
["shift_lleft"] = { cycles = 1, kind = "alu", reads = { 2 }, writes = { 1 }, imm = { { arg = 3, width = 5, }, }, },
["shift_lleft_self"] = { cycles = 1, kind = "alu", reads = { 1 }, writes = { 1 }, imm = { { arg = 2, width = 5, }, }, value = { dest = 1, immediate = 2, op = "shift_lleft", source = 1, }, },
["shift_lleft_var"] = { cycles = 1, kind = "alu", reads = { 2, 3 }, writes = { 1 }, },
["shift_lright"] = { cycles = 1, kind = "alu", reads = { 2 }, writes = { 1 }, imm = { { arg = 3, width = 5, }, }, },
["slt_s"] = { cycles = 1, kind = "alu", reads = { 2, 3 }, writes = { 1 }, },
["slt_si"] = { cycles = 1, kind = "alu", reads = { 1, 2 }, writes = { 1 }, imm = { { arg = 3, signed = true, width = 16, }, }, },
["slt_u"] = { cycles = 1, kind = "alu", reads = { 2, 3 }, writes = { 1 }, },
["slt_ui"] = { cycles = 1, kind = "alu", reads = { 1, 2 }, writes = { 1 }, imm = { { arg = 3, signed = true, width = 16,}, }, },
["store_byte"] = { cycles = 1, kind = "store", reads = { 1, 2 }, writes = {}, imm = { { arg = 3, signed = true, width = 16, }, }, },
["store_half"] = { cycles = 1, kind = "store", reads = { 1, 2 }, writes = {}, imm = { { arg = 3, signed = true, width = 16, }, }, },
["store_word"] = { cycles = 1, kind = "store", reads = { 1, 2 }, writes = {}, imm = { { arg = 3, signed = true, width = 16, }, }, },
["sub_s"] = { cycles = 1, kind = "alu", reads = { 2, 3 }, writes = { 1 }, },
["sub_u"] = { cycles = 1, kind = "alu", reads = { 2, 3 }, writes = { 1 }, },
["sys_mov_from_cop0"] = { cycles = 1, kind = "cop0_xfer", reads = {}, writes = { 1 }, },
["sys_mov_to_cop0"] = { cycles = 1, kind = "cop0_xfer", reads = { 1 }, writes = {}, },
["xor_i"] = { cycles = 1, kind = "alu", reads = { 1, 2 }, writes = { 1 }, imm = { { arg = 3, width = 16, }, }, value = { dest = 1, immediate = 3, op = "xor_i", source = 2, }, },
["xor_u"] = { cycles = 1, kind = "alu", reads = { 2, 3 }, writes = { 1 }, },
}
-- One row per GTE command. Alias cycle numbers live here, not on INSTRUCTION.
--- @type table<string, GteCommandRow>
M.GTE_COMMAND = {
["gte_cmdw_avsz3"] = {
aliases = { "gte_avg_sort_z3", "gte_avsz3", "gte_cmdw_avg_sort_z3" },
cycles = 5,
inputs = { "C2_SZ0", "C2_SZ1", "C2_SZ2", "C2_SZ3", "gte_cr_ZSF3" },
outputs = {
{ register = "C2_OTZ", role = "otz", },
},
latch = {
{ register = "C2_OTZ", required = 4, },
},
},
["gte_cmdw_avsz4"] = {
aliases = { "gte_avg_sort_z4", "gte_avsz4", "gte_cmdw_avg_sort_z4" },
cycles = 6,
inputs = { "C2_SZ0", "C2_SZ1", "C2_SZ2", "C2_SZ3", "gte_cr_ZSF4" },
outputs = {
{ register = "C2_OTZ", role = "otz", },
},
latch = {
{ register = "C2_OTZ", required = 4, },
},
},
["gte_cmdw_gpf"] = {
aliases = {},
cycles = 5,
inputs = { "C2_IR0", "C2_IR1", "C2_IR2", "C2_IR3" },
outputs = {
{ register = "C2_MAC1", role = "mac_result", },
{ register = "C2_MAC2", role = "mac_result", },
{ register = "C2_MAC3", role = "mac_result", },
{ register = "C2_IR1", role = "latest_color", },
{ register = "C2_IR2", role = "latest_color", },
{ register = "C2_IR3", role = "latest_color", },
},
latch = {
{ register = "C2_MAC1", required = 4, },
{ register = "C2_MAC2", required = 4, },
{ register = "C2_MAC3", required = 4, },
{ register = "C2_IR1", required = 4, },
{ register = "C2_IR2", required = 4, },
{ register = "C2_IR3", required = 4, },
},
},
["gte_cmdw_mvmva"] = {
aliases = {},
cycles = 8,
inputs = {
"C2_VXY0", "C2_VZ0",
"C2_VXY1", "C2_VZ1",
"C2_VXY2", "C2_VZ2",
"C2_IR1", "C2_IR2", "C2_IR3",
"gte_cr_RT11", "gte_cr_RT12", "gte_cr_RT13",
"gte_cr_RT21", "gte_cr_RT22", "gte_cr_RT23",
"gte_cr_RT31", "gte_cr_RT32", "gte_cr_RT33",
"gte_cr_TRX", "gte_cr_TRY", "gte_cr_TRZ"
},
outputs = {
{ register = "C2_IR1", role = "latest_color", },
{ register = "C2_IR2", role = "latest_color", },
{ register = "C2_IR3", role = "latest_color", },
},
latch = {
{ register = "C2_IR1", required = 4, },
{ register = "C2_IR2", required = 4, },
{ register = "C2_IR3", required = 4, },
},
},
["gte_cmdw_nclip"] = {
aliases = { "gte_nclip" },
cycles = 8,
inputs = { "C2_SXY0", "C2_SXY1", "C2_SXY2" },
outputs = {
{ register = "C2_SZ3", role = "mac_result", },
},
latch = {
{ register = "C2_SZ3", required = 4, },
},
},
["gte_cmdw_op"] = {
aliases = { "gte_cmdw_outer_product", "gte_cmdw_wedge" },
cycles = 6,
inputs = {},
outputs = {
{ register = "C2_IR1", role = "latest_color", },
{ register = "C2_IR2", role = "latest_color", },
{ register = "C2_IR3", role = "latest_color", },
},
latch = {
{ register = "C2_IR1", required = 4, },
{ register = "C2_IR2", required = 4, },
{ register = "C2_IR3", required = 4, },
},
},
["gte_cmdw_rtps"] = {
aliases = { "gte_cmdw_rotate_translate_perspective_single", "gte_rtps" },
cycles = 15,
inputs = {
"C2_VXY0", "C2_VZ0",
"C2_VXY1", "C2_VZ1",
"C2_VXY2", "C2_VZ2",
"C2_RGB", "C2_OTZ",
"C2_IR0", "C2_IR1", "C2_IR2", "C2_IR3",
"C2_SZ0", "C2_SZ1", "C2_SZ2", "C2_SZ3",
"gte_cr_RT11", "gte_cr_RT12", "gte_cr_RT13",
"gte_cr_RT21", "gte_cr_RT22", "gte_cr_RT23",
"gte_cr_RT31", "gte_cr_RT32", "gte_cr_RT33",
"gte_cr_TRX", "gte_cr_TRY", "gte_cr_TRZ",
"gte_cr_OFX", "gte_cr_OFY",
"gte_cr_H",
"gte_cr_DQA", "gte_cr_DQB"
},
outputs = {
{ register = "C2_SXY2", role = "latest_screen_xy", },
{ register = "C2_SZ2", role = "latest_screen_z", },
{ register = "C2_OTZ", role = "otz", },
{ register = "C2_IR0", role = "latest_color", },
},
latch = {
{ register = "C2_SXY2", required = 4, },
{ register = "C2_SZ2", required = 4, },
{ register = "C2_OTZ", required = 4, },
{ register = "C2_IR0", required = 4, },
},
},
["gte_cmdw_rtpt"] = {
aliases = { "gte_cmdw_rotate_translate_perspective_triple", "gte_rtpt" },
cycles = 23,
inputs = {
"C2_VXY0", "C2_VZ0",
"C2_VXY1", "C2_VZ1",
"C2_VXY2", "C2_VZ2",
"C2_RGB", "C2_OTZ",
"C2_IR0", "C2_IR1", "C2_IR2", "C2_IR3",
"C2_SZ0", "C2_SZ1", "C2_SZ2", "C2_SZ3",
"gte_cr_RT11", "gte_cr_RT12", "gte_cr_RT13",
"gte_cr_RT21", "gte_cr_RT22", "gte_cr_RT23",
"gte_cr_RT31", "gte_cr_RT32", "gte_cr_RT33",
"gte_cr_TRX", "gte_cr_TRY", "gte_cr_TRZ",
"gte_cr_OFX", "gte_cr_OFY",
"gte_cr_H",
"gte_cr_DQA", "gte_cr_DQB"
},
outputs = {
{ register = "C2_SXY0", role = "screen_xy[0]", },
{ register = "C2_SXY1", role = "screen_xy[1]", },
{ register = "C2_SXY2", role = "latest_screen_xy", },
{ register = "C2_SZ3", role = "latest_screen_z", },
{ register = "C2_OTZ", role = "otz", },
},
latch = {
{ register = "C2_SXY0", required = 4, },
{ register = "C2_SXY1", required = 4, },
{ register = "C2_SXY2", required = 4, },
{ register = "C2_SZ3", required = 4, },
{ register = "C2_OTZ", required = 4, },
},
},
["gte_cmdw_sqr"] = {
aliases = {},
cycles = 5,
inputs = { "C2_IR1", "C2_IR2", "C2_IR3" },
outputs = {
{ register = "C2_MAC1", role = "mac_result", },
{ register = "C2_MAC2", role = "mac_result", },
{ register = "C2_MAC3", role = "mac_result", },
{ register = "C2_IR1", role = "latest_color", },
{ register = "C2_IR2", role = "latest_color", },
{ register = "C2_IR3", role = "latest_color", },
},
latch = {
{ register = "C2_MAC1", required = 4, },
{ register = "C2_MAC2", required = 4, },
{ register = "C2_MAC3", required = 4, },
{ register = "C2_IR1", required = 4, },
{ register = "C2_IR2", required = 4, },
{ register = "C2_IR3", required = 4, },
},
},
}
--- @param ident string
--- @return InstructionRow|nil
function M.instr (ident) return M.INSTRUCTION [ident] end
--- @param ident string
--- @return string
function M.gte_canon(ident) return M.ALIAS_TO_CANONICAL [ident] or ident end
--- @param ident string
--- @return GteCommandRow|nil
function M.gte (ident) return M.GTE_COMMAND[M.gte_canon(ident)] end
--- @return nil
local function build_alias_map()
--- @type table<string, string> -- bag: alias or canon -> canon
M.ALIAS_TO_CANONICAL = {}
for canon, row in pairs(M.GTE_COMMAND) do ---@type string, GteCommandRow
M.ALIAS_TO_CANONICAL[canon] = canon
for _, alias in ipairs(row.aliases or {}) do ---@type integer, string
M.ALIAS_TO_CANONICAL[alias] = canon
end
end
end
build_alias_map()
--- GTE control-register alias groups.
--- Aliases within a group write to the same C2 control-register slot (the HW double-maps some C2 slots across multiple PSX SDK / libgte conventions).
--- Aliases across groups write to distinct C2 slots.
---
--- Cross-alias writes inside one atom body, or across the wave-context boundary, silently clobber each other.
--- The `check_gte_cr_alias_writes` check warns about each pair per source. See `docs/gte_reference.md` §"Control-register alias table"
--- for the HW rationale and the libgte outer-product convention.
--- @type GteCrAliasGroup[]
M.GTE_CR_ALIAS_GROUPS = {
{ 24, { "gte_cr_RBK", "gte_cr_OFX" } }, -- background R vs screen offset X
{ 25, { "gte_cr_GBK", "gte_cr_OFY" } }, -- background G vs screen offset Y
{ 26, { "gte_cr_BBK", "gte_cr_H" } }, -- background B vs projection plane distance H
}
-- Packed RT slots named by the gte.h packed-slot comment. First must be written before second.
--- @type GtePackedSlotRelation[]
M.GTE_PACKED_SLOT_RELATIONS = {
{ slot = 2, first = "gte_cr_RT13", second = "gte_cr_RT22" },
}
-- Operand-class table for the COP2->GPR load-delay check.
-- Maps each emitting-token ident to the set of GPR operand positions it reads.
-- Covers the current encoder vocabulary (`code/duffle/mips.h` + `code/duffle/gte.h`); add rows here as new encoders land.
--
-- Semantics:
-- * A "GPR operand position" is the textual slot in the macro's argument list, 1-based; e.g. `load_word(rt, base, off)` has positional operands 1 (rt), 2 (base), 3 (off).
-- The table reads operands 1 + 2 + 3 to find what GPRs the macro touches.
-- * The check tracks one entry per destination GPR per MFC2 / CFC2 event.
-- A subsequent event counts as a "use" iff any of its read operand positions reference that destination GPR's ident (e.g. `R_T0`).
-- * Branch delay slots are out of scope (MIPS control-flow; tracked separately).
--- @type table<string, integer[]> -- bag: encoder ident -> GPR operand positions
M.OPERAND_READ_POSITIONS = {
-- CPU ALU with one or two GPR operands. Reads every GPR operand.
["add_ui"] = {1, 2},
["li_s"] = {1, 2}, -- rt (write), imm16 (immediate)
["add_ui_self"] = {1},
["add_si"] = {1, 2},
["add_u"] = {1, 2, 3},
["add_u_self"] = {1, 2},
["sub_s"] = {1, 2, 3},
["sub_u"] = {1, 2, 3},
["and_i"] = {1, 2},
["and"] = {1, 2, 3},
["or_i"] = {1, 2},
["or_i_self"] = {1},
["or"] = {1, 2, 3},
["or_self"] = {1, 2},
["xor_i"] = {1, 2},
["xor"] = {1, 2, 3},
["slt_s"] = {1, 2, 3},
["slt_u"] = {1, 2, 3},
["slt_si"] = {1, 2},
["slt_ui"] = {1, 2},
["mult_s"] = {1, 2},
["mult_u"] = {1, 2},
["div_s"] = {1, 2},
["div_u"] = {1, 2},
-- Shifts: shift_lleft(rd, rt, shamt); the rt operand is the value, rd is dest.
["shift_lleft"] = {1, 2},
["shift_lright"] = {1, 2},
["shift_aright"] = {1, 2},
["shift_lleft_self"] = {1},
-- Loads: load_word(rt, base, off); the rt operand is the destination (it's written, not read) and base + off are non-GPR operands.
-- The check treats the rt operand as a write, so the read-positions table for `load_*` is empty.
["load_word"] = {},
["load_half_u"] = {},
["load_byte_u"] = {},
["load_half"] = {},
["load_byte"] = {},
["load_upper_i"] = {},
["load_ui"] = {},
-- Stores write to memory; base + rt operands are non-read for load-delay purposes.
["store_word"] = {},
["store_half"] = {},
["store_byte"] = {},
-- Branches read rs (+ rt for beq/bne). The branch delay slot is out of scope.
["branch_equal"] = {1, 2},
["branch_ne"] = {1, 2},
["branch_le_zero"] = {1},
["branch_lt_zero"] = {1},
["branch_ge_zero"] = {1},
["branch_gt_zero"] = {1},
-- Jumps / link: jr / jalr read rs only (the target). RD is the destination link.
["jump_reg"] = {1},
["jump_link"] = {1},
["call_reg"] = {1},
["call_addr"] = {},
["jump"] = {},
-- mask_upper is a 2-word macro: shift_lleft then shift_lright. The first reads rt.
["mask_upper"] = {1, 2},
-- move from/to HI/LO.
["mov_from_high"] = {},
["mov_from_low"] = {},
["mov_to_high"] = {1},
["mov_to_low"] = {1},
-- GTE transfers / loads / stores / commands: the relevant table values live in the check itself.
-- `gte_mv_to_*` writes its rt operand; `gte_mv_from_*` writes its rt operand; `gte_*` commands are atomic-from-the-CPU-POV
-- once they issue (the CPU holds until the command completes, so load-delay violations don't surface here).
["gte_mv_from_data_r"] = {},
["gte_mv_from_ctrl_r"] = {},
["gte_mv_to_data_r"] = {},
["gte_mv_to_ctrl_r"] = {},
["gte_lw"] = {},
["gte_sw"] = {},
["shift_lleft_var"] = {1, 2, 3}, -- rd, rt, rs (variable shift amount)
["shift_aright_var"] = {1, 2, 3},
}
-- GP0 packet sizes (total words including the 1-word tag) per GP0 cmd byte.
-- Per PSX-SPX `docs/psx-spx/docs/graphicsprocessingunitgpu.md` §"GPU Render Polygon Commands":
-- Each polygon command's word count = 1 (tag/cmd) + per-vertex (vertex + optional color + optional UV).
-- F3: cmd + 3 vertices = 4 words; +1 tag = 5
-- F4: cmd + 4 vertices = 5 words; +1 tag = 6
-- G3: cmd + 3×(color + vertex) = 6 words; +1 tag = 7
-- G4: cmd + 4×(color + vertex) = 8 words; +1 tag = 9
-- FT3: cmd + tpage + clut + 3×(vertex + UV) = 7 words; +1 tag = 8
-- FT4: cmd + tpage + clut + 4×(vertex + UV) = 9 words; +1 tag = 10
-- GT3: cmd + tpage + clut + 3×(color + vertex + UV) = 9 words; +1 tag = 10
-- GT4: cmd + tpage + clut + 4×(color + vertex + UV) = 12 words; +1 tag = 13
--
-- Cross-checked against code/duffle/gp.h struct sizes + the set_poly_* macros
-- (which encode "len" = "words after tag"):
-- set_poly_f3(p) -> set_len(p, 4) -> 5 total GP0 0x20
-- set_poly_ft3(p) -> set_len(p, 7) -> 8 total GP0 0x24
-- set_poly_f4(p) -> set_len(p, 5) -> 6 total GP0 0x28
-- set_poly_ft4(p) -> set_len(p, 9) -> 10 total GP0 0x2C
-- set_poly_g3(p) -> set_len(p, 6) -> 7 total GP0 0x30
-- set_poly_gt3(p) -> set_len(p, 9) -> 10 total GP0 0x34
-- set_poly_g4(p) -> set_len(p, 8) -> 9 total GP0 0x38
-- set_poly_gt4(p) -> set_len(p, 12) -> 13 total GP0 0x3C
--- @type table<integer, integer> -- bag: GP0 cmd byte -> word count
M.GP0_CMD_SIZE = {
[0x20] = 5, -- Poly_F3
[0x24] = 8, -- Poly_FT3
[0x28] = 6, -- Poly_F4
[0x2C] = 10, -- Poly_FT4
[0x30] = 7, -- Poly_G3
[0x34] = 10, -- Poly_GT3
[0x38] = 9, -- Poly_G4
[0x3C] = 13, -- Poly_GT4
}
-- Shape suffix (after `ac_format_` / `mac_format_` prefix) -> GP0 cmd byte.
-- Lets the static-analysis check derive the cmd byte from a macro name like `mac_format_g4_color` -> `g4` -> 0x38 -> 9 expected words.
--- @type table<string, integer> -- bag: shape suffix -> GP0 cmd byte
M.GP0_CMD_BY_SHAPE = {
["f3"] = 0x20, ["ft3"] = 0x24,
["f4"] = 0x28, ["ft4"] = 0x2C,
["g3"] = 0x30, ["gt3"] = 0x34,
["g4"] = 0x38, ["gt4"] = 0x3C,
}
--- @type integer
M.UNKNOWN_INSTRUCTION_CYCLES = 1
-- Hardware-relation policy table.
--
-- The forward walker in `passes/static_analysis.lua::analyze_hardware_relations` reads every emitted word_event, matches its `encoder` against `row.token`, and:
-- * stages the event as a producer in `atom.paths.forward_state`; or
-- * matches it as a consumer against pending producers and records a hazard on `atom.paths.hazards` when the gap is below `visibility.required`.
--
-- Each row is the contract for one CPU-to-coprocessor transfer semantic (the coprocessor-to-CPU path mirrors the same shape).
-- The `reads` / `writes` sub-tables carry the argument positions the analyzer inspects:
-- * `writes.arg` is the destination operand (the producer's effect); the analyzer stages this register as a pending producer.
-- * `reads` (when present) lists the operand positions the same token reads back from hardware; for MTC2 / CTC2 the producer reads the GPR source it is loading from.
-- The `fanout_to` field (MTC2-IRGB row only) tells the consumer-match logic which downstream COP2 registers are transitively updated by the write.
--
-- Visibility semantics:
-- * `kind = "post_producer_words"` means the consumer observes the producer's effect after `required` independent emitted words that are
-- strictly between the producer and the consumer. The producer's own emitted slot is implicit (it counts as the slot of issue, not toward `required`)
-- per the PSX-SPX rule: "Store delays are counted in numbers of clock cycles (not in numbers of opcodes).
-- For 3 cycle delay, one must usually insert 3 cached opcodes (or one uncached opcode)."
-- * `required` is the minimum count of intervening emitted words between producer and consumer.
-- `required = 0` permits the consumer on the very next slot; `required < 0` would place the consumer on the same slot as the producer
-- and is reserved for future "self-retires" relations.
--
-- Evidence:
-- * `evidence.confidence` is one of `"exact"`, `"conservative"`, `"unknown"`. The severity comes from `violation_kind`;
-- A hardware measurement that the vendor caveats may still classify as `"conservative"` even when the underlying timing is numerically known.
-- * `evidence.source` is the upstream reference (file + line range) the row is sourced from. New rows must carry this citation.
--
-- Consumers:
-- * passes/static_analysis.lua::analyze_hardware_relations (forward walker).
-- * passes/static_analysis.lua::transfer_hazards CHECK_RULES reader (renders hazards onto `findings`).
-- This table is consumed by the hardware-relation analyzer and hazard renderer.
--- @type HardwareRelationRow[]
M.HARDWARE_RELATIONS = {
-- CPU → COP2 data register (MTC2). The ordinary default is 2 cached words between producer and consumer (cpuspecifications.md:407-419).
{
id = "mtc2_gpr_visibility",
semantic = "MTC2",
consumer = "cop2_input",
token = "gte_mv_to_data_r",
direction = "gpr_to_cop2_data",
reads = { domain = "gpr", arg = 1 },
writes = { domain = "cop2.data", arg = 2 },
visibility = { kind = "post_producer_words", required = 2 },
evidence = {
confidence = "exact",
source = "cpuspecifications.md:407-419",
},
violation_kind = "error",
},
-- CPU → COP2 data register when the destination is C2_IRGB (data 28).
-- C2_IRGB drives the IR1/IR2/IR3 color-conversion fan-out, which extends the propagation delay to 3 cached words.
-- `destination_match = "C2_IRGB"` is the row's filter; the analyzer consults this when the producer's destination operand equals "C2_IRGB".
-- C2_ORGB (data 29) is read-only and is never classified as a writable fan-out destination.
{
id = "mtc2_irgb_visibility",
semantic = "MTC2",
consumer = "cop2_input",
token = "gte_mv_to_data_r",
direction = "gpr_to_cop2_data",
reads = { domain = "gpr", arg = 1 },
writes = { domain = "cop2.data", arg = 2 },
destination_match = "C2_IRGB",
fanout_to = { "C2_IR1", "C2_IR2", "C2_IR3" },
visibility = { kind = "post_producer_words", required = 3 },
evidence = {
confidence = "exact",
source = "cpuspecifications.md:407-419",
},
violation_kind = "error",
},
-- CPU → COP2 control register (CTC2). Ordinary minimum 2;
-- no IRGB-style fan-out exists for control registers (per spec §3.6: only C2_IRGB has the 3-cycle fan-out on the data side).
{
id = "ctc2_gpr_visibility",
semantic = "CTC2",
consumer = "cop2_input",
token = "gte_mv_to_ctrl_r",
direction = "gpr_to_cop2_control",
reads = { domain = "gpr", arg = 1 },
writes = { domain = "cop2.ctrl", arg = 2 },
visibility = { kind = "post_producer_words", required = 2 },
evidence = {
confidence = "exact",
source = "cpuspecifications.md:407-419",
},
violation_kind = "error",
},
-- COP2 data → GPR (MFC2). One cached slot between the transfer and the first GPR consumer;
-- the GPR is not updated until the instruction AFTER the MFC2 completes (geometrytransformationenginegte.md:29-32).
{
id = "mfc2_gpr_visibility",
semantic = "MFC2",
consumer = "gpr_read",
token = "gte_mv_from_data_r",
direction = "cop2_data_to_gpr",
reads = { domain = "cop2.data", arg = 2 },
writes = { domain = "gpr", arg = 1 },
visibility = { kind = "post_producer_words", required = 1 },
evidence = {
confidence = "exact",
source = "geometrytransformationenginegte.md:29-32",
},
violation_kind = "error",
},
-- COP2 control → GPR (CFC2). Same delay as MFC2 (cpuspecifications.md treats the two load-from-COP2 paths symmetrically).
{
id = "cfc2_gpr_visibility",
semantic = "CFC2",
consumer = "gpr_read",
token = "gte_mv_from_ctrl_r",
direction = "cop2_control_to_gpr",
reads = { domain = "cop2.ctrl", arg = 2 },
writes = { domain = "gpr", arg = 1 },
visibility = { kind = "post_producer_words", required = 1 },
evidence = {
confidence = "exact",
source = "cpuspecifications.md:382-419",
},
violation_kind = "error",
},
-- COP0 control → GPR (MFC0).
-- One cached slot; the analyzer treats `sys_mov_from_cop0(rt, 12)` (the SR/CU2 transfer) as the same shape as the COP2 load-delay path.
-- The semantic-level SR/CU2 transition models the load delay;
-- SR.CU2 bounded-value propagation is modeled separately).
{
id = "mfc0_gpr_visibility",
semantic = "MFC0",
consumer = "gpr_read",
token = "sys_mov_from_cop0",
direction = "cop0_control_to_gpr",
reads = { domain = "cop0.ctrl", arg = 2 },
writes = { domain = "gpr", arg = 1 },
visibility = { kind = "post_producer_words", required = 1 },
evidence = {
confidence = "exact",
source = "cpuspecifications.md:171-178",
},
violation_kind = "error",
},
-- Memory -> COP2 data register (LWC2).
-- The memory-side timing is not measured by the vendored GTE latch experiment, so this relation has no numeric retirement threshold.
-- The LWC2 destination has TWO retirement regimes (per PSX-SPX):
-- * GTE-command consumer (`gte_cmdw_*`): the GTE pipeline LATCHES the LWC2 result, so a `gte_cmdw_*`
-- in the very next slot uses the latched value. Gap = 0 is allowed. (Per `docs/psx-spx/docs/gtepipelinetimings.md:271-274`.)
-- * Any other consumer: standard MIPS load delay applies. Gap = 1 required. (Per `docs/psx-spx/docs/cpuspecifications.md:407-419`.)
-- Two separate relations so the walker can dispatch by consumer type and emit different severities
-- (the GTE-command path is `info` because the latch is intentional; the non-GTE-consumer path is `error` because the missing nop is a real bug).
{
id = "lwc2_to_gte_command",
semantic = "LWC2_to_GTE",
consumer = "cop2_input",
token = "gte_lw",
direction = "memory_to_cop2_data",
reads = { domain = "memory", arg = 2 },
writes = { domain = "cop2.data", arg = 1 },
required = 0, -- GTE-command consumer: gap = 0 OK (latched).
evidence = {
confidence = "measured",
source = "gtepipelinetimings.md:271-274",
},
violation_kind = "info",
clear_on_consumer = true,
},
{
id = "lwc2_to_other_consumer",
semantic = "LWC2_to_other",
consumer = "cop2_input",
token = "gte_lw",
direction = "memory_to_cop2_data",
reads = { domain = "memory", arg = 2 },
writes = { domain = "cop2.data", arg = 1 },
required = 1, -- Non-GTE-consumer: standard MIPS load delay.
evidence = {
confidence = "inferred",
source = "cpuspecifications.md:407-419",
},
violation_kind = "error",
clear_on_consumer = true,
},
-- COP2 data register -> memory (SWC2). A read of C2 state, not a CPU-to-COP2 write.
-- The policy row stays in for direction/provenance; staging it as a later command-input producer is suppressed.
{
id = "swc2_memory_write",
semantic = "SWC2",
consumer = "gpr_read",
token = "gte_sw",
direction = "cop2_data_to_memory",
reads = { domain = "cop2.data", arg = 1 },
writes = { domain = "memory", arg = 2 },
visibility = { kind = "none", required = 0 },
evidence = {
confidence = "exact",
source = "cpuspecifications.md:79",
},
violation_kind = "info",
stage = false,
},
-- MTC0 Status/SR.CU2. The ordinary COP0 store has no general store-delay relation;
-- this row feeds the dedicated CU2 transition logic in the same forward walk and is therefore not staged in `pending`.
{
id = "mtc0_cu2_visibility",
semantic = "MTC0",
consumer = "gpr_read",
token = "sys_mov_to_cop0",
direction = "gpr_to_cop0_status",
reads = { domain = "gpr", arg = 1 },
writes = { domain = "cop0.status", arg = 2 },
status_register = 12,
visibility = { kind = "post_producer_words", required = 2 },
evidence = {
confidence = "conservative",
source = "cpuspecifications.md:543,625-628",
},
violation_kind = "warning",
stage = false,
cu2_transition = true,
},
}
-- Bounded Status/SR.CU2 transition policy.
-- The value lattice and the transition consumer both read this immutable row; no second value pass is permitted.
-- The source says the enable/disable transition takes "2 clock cycles or so", so the boundary is conservative rather than exact.
--- @type Cu2TransitionPolicy
M.CU2_TRANSITION_POLICY = {
status_register = 12,
enable_bit = 0x40000000,
required = 2,
visibility_kind = "post_producer_words",
evidence = {
confidence = "conservative",
source = "cpuspecifications.md:543,625-628",
},
}
return M
+27 -24
View File
@@ -16,31 +16,30 @@
--- Net effect: the caller gets the duffle module in one statement; no separate `dofile(...)` + `require("duffle")` dance. --- Net effect: the caller gets the duffle module in one statement; no separate `dofile(...)` + `require("duffle")` dance.
--- ---
--- @class DufflePaths local M = {}
--- @field setup fun(): nil
local M = {} ---@type DufflePaths
-- Cache key for the repo root. Stored in `package.loaded` (process-global) so all 8 entry scripts + passes scripts share one resolution. -- Cache key for the repo root. Stored in `package.loaded` (process-global) so all 8 entry scripts + passes scripts share one resolution.
local CACHE_KEY = "__duffle_repo_root__" ---@type string local CACHE_KEY = "__duffle_repo_root__"
--- Resolve the repo root from this script's own path. Zero shell spawn. --- Resolve the repo root from this script's own path. Zero shell spawn.
--- `duffle_paths.lua` always lives at `<repo>/scripts/duffle_paths.lua`, so the repo root is the parent of the directory containing this script. --- `duffle_paths.lua` always lives at `<repo>/scripts/duffle_paths.lua`, so the repo root is the
--- We derive it directly from `debug.getinfo(1, "S").source` (returns `@<path>` for the currently-running chunk). --- parent of the directory containing this script. We derive it directly from `debug.getinfo(1, "S").source`
--- (returns `@<path>` for the currently-running chunk).
--- ---
--- If `debug.getinfo` can't parse this script's path (shouldn't happen — dofile always populates source), return nil and let `M.setup()` fail loud. --- If `debug.getinfo` can't parse this script's path (shouldn't happen — dofile always populates source),
--- return nil and let `M.setup()` fail loud.
--- @return string|nil --- @return string|nil
local function find_repo_root() local function find_repo_root()
if package.loaded[CACHE_KEY] then return package.loaded[CACHE_KEY] end if package.loaded[CACHE_KEY] then return package.loaded[CACHE_KEY] end
local source = debug.getinfo(1, "S").source ---@type string local source = debug.getinfo(1, "S").source
-- Strip the leading `@` (Lua's dofile marker) and the trailing `/duffle_paths.lua` filename. -- Strip the leading `@` (Lua's dofile marker) and the trailing `/duffle_paths.lua` filename.
-- What remains is the directory containing this script, i.e. `<repo>/scripts/`. -- What remains is the directory containing this script, i.e. `<repo>/scripts/`.
local scripts_dir = source and source:match("^@?(.*)[/\\]duffle_paths%.lua$") ---@type string|nil local scripts_dir = source and source:match("^@?(.*)[/\\]duffle_paths%.lua$")
if not scripts_dir then return nil end if not scripts_dir then return nil end
-- The repo root is the parent of `scripts/`. Strip the trailing `scripts/` (with or without trailing slash). -- The repo root is the parent of `scripts/`. Strip the trailing `scripts/` (with or without trailing slash).
local root = scripts_dir:gsub("scripts[\\/]?$", "") ---@type string local root = scripts_dir:gsub("scripts[\\/]?$", "")
root = root:gsub("\\", "/") root = root:gsub("\\", "/")
if root == "" then root = "./" end if root == "" then root = "./" end
if not root:match("/$") then root = root .. "/" end if not root:match("/$") then root = root .. "/" end
@@ -48,23 +47,27 @@ local function find_repo_root()
return root return root
end end
--- Set `package.path` (for `require("duffle")` + `require("passes.X")`) and `package.cpath` (for `lpeg.dll`). --- Set `package.path` (for `require("duffle")` + `require("passes.X")`) and
--- `package.cpath` (for `lpeg.dll`).
--- ---
--- This script does NOT touch the OS environment: no `os.setenv`, no `os.putenv`, no `$PATH` mods. --- This script does NOT touch the OS environment: no `os.setenv`, no `os.putenv`, no `$PATH` mods.
--- It just sets `package.path` and `package.cpath` (the standard Lua way to register module search dirs). --- It just sets `package.path` and `package.cpath` (the standard Lua way to register module search dirs).
--- lpeg is built by `update_deps.ps1` to `toolchain/lpeg/`, which we wire into `package.cpath` here (so `require("lpeg")` from `duffle.lua` resolves without any global state). --- lpeg is built by `update_deps.ps1` to `toolchain/lpeg/`,
--- @return nil --- which we wire into `package.cpath` here (so `require("lpeg")` from `duffle.lua` resolves without any global state).
function M.setup() function M.setup()
local repo_root = find_repo_root() ---@type string|nil local repo_root = find_repo_root()
if not repo_root then if not repo_root then
-- Unreachable in practice: find_repo_root() derives the repo root from this script's own source path via debug.getinfo(1, "S").source (no subprocess, no git CLI, <1ms). -- Unreachable in practice: find_repo_root() derives the repo root from this script's
-- A nil return means the source path did not match the expected <repo>/scripts/duffle_paths.lua layout — a packaging bug, not a "missing git repo" condition. -- own source path via debug.getinfo(1, "S").source (no subprocess, no git CLI, <1ms).
-- os.exit(2) is retained so a real failure surfaces loud rather than silently producing an unconfigured module table. -- A nil return means the source path did not match the expected
-- <repo>/scripts/duffle_paths.lua layout — a packaging bug, not a "missing git repo"
-- condition. os.exit(2) is retained so a real failure surfaces loud rather than
-- silently producing an unconfigured module table.
os.exit(2) os.exit(2)
end end
local scripts_dir = repo_root .. "scripts/" ---@type string local scripts_dir = repo_root .. "scripts/"
local passes_dir = repo_root .. "scripts/passes/" ---@type string local passes_dir = repo_root .. "scripts/passes/"
package.path = scripts_dir .. "?.lua;" package.path = scripts_dir .. "?.lua;"
.. scripts_dir .. "?/init.lua;" .. scripts_dir .. "?/init.lua;"
.. passes_dir .. "?.lua;" .. passes_dir .. "?.lua;"
@@ -74,8 +77,8 @@ function M.setup()
-- lpeg: built by `update_deps.ps1` to `toolchain/lpeg/lpeg.dll`. -- lpeg: built by `update_deps.ps1` to `toolchain/lpeg/lpeg.dll`.
-- lfs: compiled from pcsx-redux's vendored luafilesystem source to `toolchain/lfs/lfs.dll`. -- lfs: compiled from pcsx-redux's vendored luafilesystem source to `toolchain/lfs/lfs.dll`.
-- Wire both directories into cpath so `require("lpeg")` and `require("lfs")` resolve. -- Wire both directories into cpath so `require("lpeg")` and `require("lfs")` resolve.
local lpeg_dir = repo_root .. "toolchain/lpeg/" ---@type string local lpeg_dir = repo_root .. "toolchain/lpeg/"
local lfs_dir = repo_root .. "toolchain/lfs/" ---@type string local lfs_dir = repo_root .. "toolchain/lfs/"
package.cpath = lpeg_dir .. "?.dll;" package.cpath = lpeg_dir .. "?.dll;"
.. lfs_dir .. "?.dll;" .. lfs_dir .. "?.dll;"
.. package.cpath .. package.cpath
@@ -84,6 +87,6 @@ end
-- Run the setup as a side effect. -- Run the setup as a side effect.
M.setup() M.setup()
-- Now that package.path includes scripts/, `require("duffle")` resolves. -- Now that package.path includes scripts/, `require("duffle")` resolves. Return the duffle module
-- Return the duffle module so callers can do `local duffle = dofile(...duffle_paths.lua)` in one line. -- so callers can do `local duffle = dofile(...duffle_paths.lua)` in one line.
return require("duffle") return require("duffle")
File diff suppressed because it is too large Load Diff
-511
View File
@@ -1,511 +0,0 @@
-- elf32.lua — Pure-Lua ELF32 format helpers with no lfs / no lpeg dependency.
-- The reload helper's `parse_manifest` (scripts/pcsx_debug_helper/reload.lua)
-- and the metaprogram's `read_elf_sections` + `read_nm` (scripts/elf_dwarf.lua)
-- both parsed ELF32 headers from wire bytes.
--
-- This module contains the format constants and the byte-level walker.
--- The metaprogram side keeps `read_u32_le` / `read_u16_le` as local forwarders; the helper side calls `E.*` directly.
--
-- **Adapter contract (explicit pass style):**
-- The helper VM's `Support.File` exposes byte-read methods that require `self` (fileffi.lua:225-227),
-- so callers wrap once in a 1-line adapter that strips `self`.
-- The parsers here operate on the unwrapped form.
-- Reads are flat function calls — `E.read_u8(adapter, off)`, `E.read_u32(adapter, off)`, `E.size(adapter)`.
-- read_u8(adapter, off) -> integer | nil
-- read_u16(adapter, off) -> integer | nil
-- read_u32(adapter, off) -> integer | nil
-- size(adapter) -> integer
--
-- **Convention:** every offset in the constants tables is a zero-based wire offset.
-- The `+ 1` conversion happens only at the `string.byte` boundary inside the readers.
--
-- spec: System V ABI gABI v1.2 §"ELF Header" (Table 1) + §"Section Header Table"
-- spec: System V ABI gABI v1.2 §"Symbol Table" (Elf32_Sym layout)
--- @class Elf32Adapter
--- @field read_u8_at fun(off: integer): integer|nil
--- @field read_u16_at fun(off: integer): integer|nil
--- @field read_u32_at fun(off: integer): integer|nil
--- @field read_size fun(): integer
--- @class Elf32Header
--- @field e_entry integer
--- @field e_shoff integer
--- @field e_shentsize integer
--- @field e_shnum integer
--- @field e_shstrndx integer
--- @field error string|nil
--- @class Elf32Section
--- @field sh_name integer
--- @field sh_type integer
--- @field sh_flags integer
--- @field sh_addr integer
--- @field sh_offset integer
--- @field sh_size integer
--- @field sh_link integer
--- @field name string
--- @class Elf32Sym
--- @field value integer
--- @field size integer
--- @field info integer
--- @field shndx integer
--- @class Elf32HeaderLayout
--- @field magic_offset integer
--- @field magic string
--- @field class_offset integer
--- @field endian_offset integer
--- @field header_bytes integer
--- @field e_entry_offset integer
--- @field e_shoff_offset integer
--- @field e_shentsize_offset integer
--- @field e_shnum_offset integer
--- @field e_shstrndx_offset integer
--- @class Elf32SectionLayout
--- @field sh_name_offset integer
--- @field sh_type_offset integer
--- @field sh_flags_offset integer
--- @field sh_addr_offset integer
--- @field sh_offset_offset integer
--- @field sh_size_offset integer
--- @field sh_link_offset integer
--- @field sh_entsize_bytes integer
--- @class Elf32SymLayout
--- @field st_name integer
--- @field st_value integer
--- @field st_size integer
--- @field st_info integer
--- @field sym_entry_bytes integer
--- @class Elf32Mod
--- @field ELFCLASS32 integer
--- @field ELFDATA2LSB integer
--- @field EM_MIPS integer
--- @field SHT_SYMTAB integer
--- @field SHT_STRTAB integer
--- @field SHT_NOBITS integer
--- @field SHF_WRITE integer
--- @field SHF_ALLOC integer
--- @field SHF_EXECINSTR integer
--- @field ELF32_HEADER Elf32HeaderLayout
--- @field ELF32_SECTION Elf32SectionLayout
--- @field ELF32_SYM Elf32SymLayout
--- @field dw_dwarf32_terminator integer
--- @field read_u32 fun(adapter: Elf32Adapter, off: integer): integer|nil
--- @field read_u16 fun(adapter: Elf32Adapter, off: integer): integer|nil
--- @field read_u8 fun(adapter: Elf32Adapter, off: integer): integer|nil
--- @field size fun(adapter: Elf32Adapter): integer
--- @field read_u32_le fun(buf: string, off: integer): integer
--- @field read_u16_le fun(buf: string, off: integer): integer
--- @field validate_adapter fun(adapter: any): boolean, string|nil
--- @field get_str fun(strtab: string, off: integer): string|nil
--- @field parse_elf32_headers fun(adapter: Elf32Adapter): Elf32Header|nil, string|nil
--- @field walk_sections fun(adapter: Elf32Adapter, hdr: Elf32Header): Elf32Section[]|nil, string|nil
--- @field read_section_bytes fun(adapter: Elf32Adapter, section: Elf32Section): string|nil
--- @field read_named_section fun(adapter: Elf32Adapter, sections: Elf32Section[], name: string): string|nil, string|nil
--- @field collect_symbols fun(adapter: Elf32Adapter, sections: Elf32Section[]): table<string, Elf32Sym>|nil, string|nil
local M = {} ---@type Elf32Mod
-- ════════════════════════════════════════════════════════════════════════════
-- Little-endian readers (bit-weighted accumulator, math.floor only)
-- ════════════════════════════════════════════════════════════════════════════
--- Read a 4-byte little-endian unsigned integer from `adapter` at zero-based wire offset `off`.
---
--- Bit weights are written as `0x100`, `0x10000`, `0x1000000` (i.e. 2^8, 2^16, 2^24) so the LE byte positions are visually explicit:
--- byte 0 contributes its value directly;
--- byte 1 is shifted left by 8; byte 2 by 16; byte 3 by 24.
---
--- math.floor (not LuaJIT's `>>`) keeps the body portable across LuaJIT 2.0/2.1 and plain Lua 5.x. `string.byte` receives `+ 1` at the boundary.
---
--- **Call form:** explicit-pass. The reader receives `adapter` as the first positional argument and the offset as the second; no `self` is passed.
--- Test fixtures declare `function(offset) ... end` and the parsers call them via dot syntax `adapter.read_u8_at(off)`.
--- The colon form `adapter:read_u8_at(off)` would prepend the adapter table as `offset` and break the contract.
--- @param adapter Elf32Adapter
--- @param off integer -- zero-based wire offset
--- @return integer|nil
function M.read_u32(adapter, off)
return adapter.read_u8_at(off)
+ adapter.read_u8_at(off + 0x01) * 0x00000100
+ adapter.read_u8_at(off + 0x02) * 0x00010000
+ adapter.read_u8_at(off + 0x03) * 0x01000000
end
--- Read a 2-byte little-endian unsigned integer from `adapter` at zero-based wire offset `off`.
--- @param adapter Elf32Adapter
--- @param off integer -- zero-based wire offset
--- @return integer|nil
function M.read_u16(adapter, off)
return adapter.read_u8_at(off)
+ adapter.read_u8_at(off + 0x01) * 0x00000100
end
--- Read a 1-byte unsigned integer from `adapter` at zero-based wire offset `off`.
--- @param adapter Elf32Adapter
--- @param off integer -- zero-based wire offset
--- @return integer|nil
function M.read_u8(adapter, off)
return adapter.read_u8_at(off)
end
--- Total adapter byte length.
--- @param adapter Elf32Adapter
--- @return integer
function M.size(adapter)
return adapter.read_size()
end
--- Forwarders kept for backward compat with scripts/elf_dwarf.lua.
--- The metaprogram side keeps `read_u32_le` / `read_u16_le`;
--- both layers now use the same byte-level helpers under the hood.
--- @param buf string
--- @param off integer
--- @return integer
function M.read_u32_le(buf, off)
local byte_off = off + 1 ---@type integer
return buf:byte(byte_off)
+ buf:byte(byte_off + 0x01) * 0x00000100
+ buf:byte(byte_off + 0x02) * 0x00010000
+ buf:byte(byte_off + 0x03) * 0x01000000
end
--- Read a 2-byte little-endian unsigned integer from `buf` at zero-based wire offset `off`.
--- @param buf string
--- @param off integer -- zero-based wire offset
--- @return integer
function M.read_u16_le(buf, off)
local byte_off = off + 1 ---@type integer
return buf:byte(byte_off) + buf:byte(byte_off + 0x01) * 0x00000100
end
-- ════════════════════════════════════════════════════════════════════════════
-- Format constants
-- ════════════════════════════════════════════════════════════════════════════
-- ELF format constants (System V ABI gABI v1.2).
M.ELFCLASS32 = 1 -- spec: gABI v1.2 §"ELF Header" — EI_CLASS byte
M.ELFDATA2LSB = 1 -- spec: gABI v1.2 §"ELF Header" — EI_DATA byte
M.EM_MIPS = 8 -- spec: gABI v1.2 §"Machine Information" — MIPS architecture
-- Section type constants (System V ABI gABI v1.2 §"Section Header Table").
M.SHT_SYMTAB = 2 -- spec: gABI v1.2 §"Section Types" — symbol table
M.SHT_STRTAB = 3 -- spec: gABI v1.2 §"Section Types" — string table
M.SHT_NOBITS = 8 -- spec: gABI v1.2 §"Section Types" — no space in file
-- Section flag constants (System V ABI gABI v1.2 §"Section Header Table").
M.SHF_WRITE = 0x1 -- spec: gABI v1.2 §"Section Attributes" — writable
M.SHF_ALLOC = 0x2 -- spec: gABI v1.2 §"Section Attributes" — occupies memory
M.SHF_EXECINSTR = 0x4 -- spec: gABI v1.2 §"Section Attributes" — executable
-- ---------------------------------------------------------------------------
-- ELF32 header layout (System V ABI gABI v1.2 §"ELF Header" Table 1)
-- ---------------------------------------------------------------------------
-- All offsets are zero-based wire offsets. The header is 52 bytes total (header_bytes = 0x34 = 52).
--- @type Elf32HeaderLayout
M.ELF32_HEADER = {
magic_offset = 0x00, -- 4 bytes; expected "\127ELF"
magic = "\127ELF",
class_offset = 0x04, -- 1 byte; 1 = ELF32, 2 = ELF64
endian_offset = 0x05, -- 1 byte; 1 = little-endian, 2 = big-endian
header_bytes = 0x34, -- ELF32 header is 52 bytes total
e_entry_offset = 0x18, -- 4-byte LE; entry-point virtual address
e_shoff_offset = 0x20, -- 4-byte LE; section-header table file offset
e_shentsize_offset = 0x2E, -- 2-byte LE; section-header entry size in bytes
e_shnum_offset = 0x30, -- 2-byte LE; number of section headers
e_shstrndx_offset = 0x32, -- 2-byte LE; index of section-name string table
}
-- ---------------------------------------------------------------------------
-- ELF32 section-header layout (System V ABI gABI v1.2 §"Section Header Table")
-- ---------------------------------------------------------------------------
-- Each entry is 40 bytes (sh_entsize_bytes = 0x28 = 40);
-- zero-based, field offsets relative to the start of the entry.
--- @type Elf32SectionLayout
M.ELF32_SECTION = {
sh_name_offset = 0x00, -- 4-byte LE; offset into .shstrtab
sh_type_offset = 0x04, -- 4-byte LE; section type (SHT_*)
sh_flags_offset = 0x08, -- 4-byte LE; section flags (SHF_*)
sh_addr_offset = 0x0C, -- 4-byte LE; virtual address at execution
sh_offset_offset = 0x10, -- 4-byte LE; section's file offset
sh_size_offset = 0x14, -- 4-byte LE; section's size in bytes
sh_link_offset = 0x18, -- 4-byte LE; link to a related section
sh_entsize_bytes = 0x28, -- spec: gABI v1.2 §"Section Header Table" — 40 bytes per entry
}
-- ---------------------------------------------------------------------------
-- ELF32 symbol-table entry layout (System V ABI gABI v1.2 §"Symbol Table")
-- ---------------------------------------------------------------------------
-- Each entry is 16 bytes (sym_entry_bytes = 0x10 = 16);
-- zero-based, field offsets relative to the start of the entry.
--- @type Elf32SymLayout
M.ELF32_SYM = {
st_name = 0x00, -- 4-byte LE; offset into the linked string table
st_value = 0x04, -- 4-byte LE; symbol value (address / absolute)
st_size = 0x08, -- 4-byte LE; symbol size in bytes
st_info = 0x0C, -- 1 byte; binding (high nibble) + type (low nibble)
sym_entry_bytes = 0x10, -- spec: gABI v1.2 §"Symbol Table" — 16 bytes per entry
}
-- DWARF32 initial-length terminator (DWARF4 §7.4) — kept here so the metaprogram's elf_dwarf.lua can drop its own copy of the same constant.
M.dw_dwarf32_terminator = 0xFFFFFFFF
-- ════════════════════════════════════════════════════════════════════════════
-- Adapter validation
-- ════════════════════════════════════════════════════════════════════════════
--- Validate that `adapter` exposes the byte-read surface.
--- Returns true on success, false + a stable error code on failure.
--- The helper side calls this before parse_manifest to reject callers before any byte is read.
--- @param adapter any
--- @return boolean, string|nil
function M.validate_adapter(adapter)
if type(adapter) ~= "table" then return false, "bad_file_adapter" end
if type(adapter.read_u8_at) ~= "function" then return false, "bad_file_adapter" end
if type(adapter.read_u16_at) ~= "function" then return false, "bad_file_adapter" end
if type(adapter.read_u32_at) ~= "function" then return false, "bad_file_adapter" end
if type(adapter.read_size) ~= "function" then return false, "bad_file_adapter" end
return true, nil
end
-- ════════════════════════════════════════════════════════════════════════════
-- String-table reader
-- ════════════════════════════════════════════════════════════════════════════
--- Extract a NUL-terminated C string from `strtab` at zero-based offset `off`.
--- Returns nil if `off` is out of range or the string is not NUL-terminated.
--- @param strtab string
--- @param off integer
--- @return string|nil
function M.get_str(strtab, off)
if off < 0 or off >= #strtab then return nil end
local end_pos = strtab:find("\0", off + 1, true) ---@type integer|nil
if not end_pos then return nil end
return strtab:sub(off + 1, end_pos - 1)
end
-- ════════════════════════════════════════════════════════════════════════════
-- Header / section / symbol walkers
-- ════════════════════════════════════════════════════════════════════════════
--- Read the ELF32 header through `adapter` and validate the magic, class, and data encoding.
--- Returns a table on success:
--- { e_entry, e_shoff, e_shentsize, e_shnum, e_shstrndx, error = nil }
--- On failure returns nil + a stable error code:
--- bad_magic, unsupported_elf_class, unsupported_elf_data, truncated_header
--- The header's machine field is NOT validated here — callers (e.g. the helper's prime path) decide whether to require EM_MIPS before symbol reads.
--- @param adapter Elf32Adapter
--- @return Elf32Header|nil, string|nil
function M.parse_elf32_headers(adapter)
local ok, err = M.validate_adapter(adapter) ---@type boolean, string|nil
if not ok then return nil, err end
-- 4-byte magic: 0x7F 'E' 'L' 'F'.
-- The byte readers take the adapter explicitly.
-- The production `Support.File` adapter is wrapped by the caller to drop its implicit `self` so the parser shape is flat pass-style.
local b1 = M.read_u8(adapter, 0) ---@type integer|nil
local b2 = M.read_u8(adapter, 1) ---@type integer|nil
local b3 = M.read_u8(adapter, 2) ---@type integer|nil
local b4 = M.read_u8(adapter, 3) ---@type integer|nil
if not (b1 and b2 and b3 and b4)
or not (b1 == 0x7f and b2 == 0x45 and b3 == 0x4c and b4 == 0x46) then
return nil, "bad_magic"
end
local class = M.read_u8(adapter, M.ELF32_HEADER.class_offset) ---@type integer|nil
if class ~= M.ELFCLASS32 then
return nil, "unsupported_elf_class"
end
local data = M.read_u8(adapter, M.ELF32_HEADER.endian_offset) ---@type integer|nil
if data ~= M.ELFDATA2LSB then
return nil, "unsupported_elf_data"
end
local e_entry = M.read_u32(adapter, M.ELF32_HEADER.e_entry_offset) ---@type integer|nil
local e_shoff = M.read_u32(adapter, M.ELF32_HEADER.e_shoff_offset) ---@type integer|nil
local e_shentsize = M.read_u16(adapter, M.ELF32_HEADER.e_shentsize_offset) ---@type integer|nil
local e_shnum = M.read_u16(adapter, M.ELF32_HEADER.e_shnum_offset) ---@type integer|nil
local e_shstrndx = M.read_u16(adapter, M.ELF32_HEADER.e_shstrndx_offset) ---@type integer|nil
if not (e_entry and e_shoff and e_shentsize and e_shnum and e_shstrndx) then
return nil, "truncated_header"
end
return {
e_entry = e_entry,
e_shoff = e_shoff,
e_shentsize = e_shentsize,
e_shnum = e_shnum,
e_shstrndx = e_shstrndx,
error = nil,
}
end
--- Read one section-header entry from `adapter` at `sh_off`.
--- Returns a table with the wire fields plus a (yet-unresolved) `name` field.
--- @param adapter Elf32Adapter
--- @param sh_off integer
--- @return Elf32Section|nil, string|nil
local function read_section_entry(adapter, sh_off)
local entry = { ---@type Elf32Section
sh_name = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_name_offset),
sh_type = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_type_offset),
sh_flags = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_flags_offset),
sh_addr = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_addr_offset),
sh_offset = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_offset_offset),
sh_size = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_size_offset),
sh_link = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_link_offset),
name = "",
}
if not (entry.sh_name and entry.sh_type and entry.sh_flags and entry.sh_addr
and entry.sh_offset and entry.sh_size and entry.sh_link) then
return nil, "truncated_section_headers"
end
return entry, nil
end
--- Walk every section header in `hdr` and return a 1-based array of entries
--- (the section at logical index 0 is at array position 1, etc.).
--- Each entry has the wire fields plus a resolved `name` derived from `.shstrtab`.
--- Returns nil + a stable error code on failure: truncated_section_headers, missing_shstrtab, truncated_strtab
--- @param adapter Elf32Adapter
--- @param hdr Elf32Header
--- @return Elf32Section[]|nil, string|nil
function M.walk_sections(adapter, hdr)
if not hdr or hdr.error then return nil, hdr and hdr.error or "truncated_section_headers" end
local file_size = M.size(adapter) ---@type integer
if hdr.e_shoff + hdr.e_shnum * hdr.e_shentsize > file_size then
return nil, "truncated_section_headers"
end
-- Read every section header first; we need .shstrtab to resolve names.
local sections = {} ---@type Elf32Section[]
for i = 0, hdr.e_shnum - 1 do ---@type integer
local sh_off = hdr.e_shoff + i * hdr.e_shentsize ---@type integer
local entry, err = read_section_entry(adapter, sh_off) ---@type Elf32Section|nil, string|nil
if not entry then return nil, err end
sections[i + 1] = entry
end
if hdr.e_shstrndx >= hdr.e_shnum then
return nil, "missing_shstrtab"
end
local shstrtab = sections[hdr.e_shstrndx + 1] ---@type Elf32Section|nil
if not shstrtab or shstrtab.sh_type ~= M.SHT_STRTAB then
return nil, "missing_shstrtab"
end
if shstrtab.sh_offset + shstrtab.sh_size > file_size then
return nil, "truncated_section_headers"
end
local shstrtab_bytes = M.read_section_bytes(adapter, shstrtab) ---@type string|nil
if not shstrtab_bytes then return nil, "truncated_section_headers" end
for _, s in ipairs(sections) do ---@type integer, Elf32Section
s.name = M.get_str(shstrtab_bytes, s.sh_name) or ""
end
return sections, nil
end
--- Read the bytes of one section. Returns a string, or nil if the adapter returns nil for any byte (out-of-bounds).
--- The caller is responsible fors sizing the buffer (the section's sh_offset + sh_size must fit in adapter.size).
--- @param adapter Elf32Adapter
--- @param section Elf32Section
--- @return string|nil
function M.read_section_bytes(adapter, section)
local size = section.sh_size ---@type integer
if size == 0 then return "" end
local out = {} ---@type string[]
for i = 0, size - 1 do ---@type integer
local b = M.read_u8(adapter, section.sh_offset + i) ---@type integer|nil
if b == nil then return nil end
out[#out + 1] = string.char(b)
end
return table.concat(out)
end
--- Convenience: walk sections, then look up the named section, then read its bytes.
--- Returns nil + a stable error code if the section is absent or out-of-bounds.
--- @param adapter Elf32Adapter
--- @param sections Elf32Section[]
--- @param name string
--- @return string|nil, string|nil
function M.read_named_section(adapter, sections, name)
if not sections then return nil, "missing_section" end
for _, s in ipairs(sections) do ---@type integer, Elf32Section
if s.name == name then
local bytes = M.read_section_bytes(adapter, s) ---@type string|nil
if not bytes then return nil, "truncated_section_data" end
return bytes, nil
end
end
return nil, "missing_section"
end
--- Walk every SHT_SYMTAB section in `sections` and accumulate symbols by name.
--- Each stored entry is `{ value = st_value, size = st_size, info = st_info, shndx = st_shndx }`.
--- Both STB_LOCAL and STB_GLOBAL symbols are included; the live ELF stores `smem` as a local symbol.
--- Returns nil + a stable error code on failure: missing_symtab_strtab, truncated_section_headers
--- @param adapter Elf32Adapter
--- @param sections Elf32Section[]
--- @return table<string, Elf32Sym>|nil, string|nil
function M.collect_symbols(adapter, sections)
if not sections then return nil, "missing_sections" end
local symbols = {} ---@type table<string, Elf32Sym> -- bag: symbol name -> Elf32Sym
local file_size = M.size(adapter) ---@type integer
for _, s in ipairs(sections) do ---@type integer, Elf32Section
if s.sh_type == M.SHT_SYMTAB then
local strtab = sections[s.sh_link + 1] ---@type Elf32Section|nil
if not strtab or strtab.sh_type ~= M.SHT_STRTAB then
return nil, "missing_symtab_strtab"
end
if strtab.sh_offset + strtab.sh_size > file_size then
return nil, "truncated_section_headers"
end
local strtab_bytes = M.read_section_bytes(adapter, strtab) ---@type string|nil
if not strtab_bytes then return nil, "truncated_section_headers" end
if s.sh_offset + s.sh_size > file_size then
return nil, "truncated_section_headers"
end
local symtab_bytes = M.read_section_bytes(adapter, s) ---@type string|nil
if not symtab_bytes then return nil, "truncated_section_headers" end
local n = #symtab_bytes / M.ELF32_SYM.sym_entry_bytes ---@type number
for j = 0, n - 1 do ---@type integer
local e = s.sh_offset + j * M.ELF32_SYM.sym_entry_bytes ---@type integer
local st_name = M.read_u32(adapter, e + M.ELF32_SYM.st_name) ---@type integer|nil
if st_name then
local st_value = M.read_u32(adapter, e + M.ELF32_SYM.st_value) ---@type integer|nil
local st_size = M.read_u32(adapter, e + M.ELF32_SYM.st_size) ---@type integer|nil
local st_info = M.read_u8(adapter, e + M.ELF32_SYM.st_info) ---@type integer|nil
-- st_shndx is at offset 14 (2 bytes) — derived from the layout
-- the metaprogram reads too. Inline the read to keep the
-- adapter as the only I/O surface.
local b1 = M.read_u8(adapter, e + 14) ---@type integer|nil
local b2 = M.read_u8(adapter, e + 15) ---@type integer|nil
if not (b1 and b2) then
return nil, "truncated_section_headers"
end
local st_shndx = b1 + b2 * 0x100 ---@type integer
local name = M.get_str(strtab_bytes, st_name) or "" ---@type string
if name ~= "" then
symbols[name] = {
value = st_value,
size = st_size,
info = st_info,
shndx = st_shndx,
}
end
end
end
end
end
return symbols, nil
end
return M
+287 -544
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -16,7 +16,7 @@ define tape_atoms
echo "[gdb_tape_atoms] STUB: run .\\build_psyq.ps1 to regenerate, then re-source this file." echo "[gdb_tape_atoms] STUB: run .\\build_psyq.ps1 to regenerate, then re-source this file."
end end
document tape_atoms document tape_atoms
List every tape atom symbol in the loaded ELF with its .rodata address and word count. List every tape atom symbol in the loaded ELF (code_<name>) with its .rodata address and word count.
STUB state: runtime file not sourced. Run build_psyq.ps1 to regenerate. STUB state: runtime file not sourced. Run build_psyq.ps1 to regenerate.
end end
+97 -17
View File
@@ -1,40 +1,77 @@
# scripts/launch_pcsx_debug.ps1 # scripts/launch_pcsx_debug.ps1
# #
# One-shot launcher for debug sessions: # One-shot launcher for debug sessions: starts pcsx-redux with the .ps-exe
# Starts pcsx-redux with the .ps-exe loaded, the gdb stub enabled, # loaded, the gdb stub enabled, the web server enabled, AND the
# AND the pcsx_debug_helper Lua plugin loaded so external CLI tools (gdb's `shell` command, etc.) # pcsx_debug_helper Lua plugin loaded so external CLI tools can drive
# can read GTE state via http://localhost:8080/api/v1/lua/gte (the gdb stub doesn't expose COP2 at all). # reloads via http://localhost:8080/api/v1/lua/reload.
#
# usage:
# .\scripts\launch_pcsx_debug.ps1
# .\scripts\launch_pcsx_debug.ps1 -ExePath build\hello_gte.ps-exe
# .\scripts\launch_pcsx_debug.ps1 -HelperZip scripts\pcsx_debug_helper.zip
# #
# After launch: # After launch:
# - gdb: target remote localhost:3333 # - gdb: target remote localhost:3333
# - web: curl http://localhost:8080/api/v1/lua/gte # - web: POST http://localhost:8080/api/v1/lua/reload?mode=prime&...
#
# usage:
# .\scripts\launch_pcsx_debug.ps1
# .\scripts\launch_pcsx_debug.ps1 -ExePath build\hello_camera.ps-exe
# .\scripts\launch_pcsx_debug.ps1 -Cpu dynarec
# .\scripts\launch_pcsx_debug.ps1 -ElfPath build\hello_camera.elf
# #
# Companion: scripts/debug_psyq.ps1 (bare launch — no .ps-exe, no helper). # Companion: scripts/debug_psyq.ps1 (bare launch — no .ps-exe, no helper).
[CmdletBinding()] [CmdletBinding()]
param( param(
[string]$PcsxPath = (Join-Path $PSScriptRoot '..\toolchain\pcsx-redux\vsprojects\x64\Release\pcsx-redux.exe'), [string]$PcsxPath = (Join-Path $PSScriptRoot '..\toolchain\pcsx-redux\vsprojects\x64\Release\pcsx-redux.exe'),
[string]$ExePath = (Join-Path $PSScriptRoot '..\build\hello_gte.ps-exe'), [string]$ExePath = (Join-Path $PSScriptRoot '..\build\hello_camera.ps-exe'),
[string]$ElfPath = '',
[string]$HelperZip = (Join-Path $PSScriptRoot 'pcsx_debug_helper.zip'), [string]$HelperZip = (Join-Path $PSScriptRoot 'pcsx_debug_helper.zip'),
[int] $GdbPort = 3333, [int] $GdbPort = 3333,
[int] $WebPort = 8080 [int] $WebPort = 8080,
[ValidateSet('interpreter', 'dynarec')][string]$Cpu = 'interpreter'
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
# ── Derive -ElfPath when absent ──
# Convention: the .elf sits beside the .ps-exe with the same stem.
if ([string]::IsNullOrEmpty($ElfPath)) {
$exeFull = [System.IO.Path]::GetFullPath($ExePath)
$stem = [System.IO.Path]::GetFileNameWithoutExtension($exeFull)
$exeDir = [System.IO.Path]::GetDirectoryName($exeFull)
$ElfPath = Join-Path $exeDir "$stem.elf"
}
# ── Pre-checks ── # ── Pre-checks ──
foreach ($p in @($PcsxPath, $ExePath, $HelperZip)) { foreach ($p in @($PcsxPath, $ExePath, $ElfPath, $HelperZip)) {
if (-not (Test-Path $p)) { if (-not (Test-Path -LiteralPath $p)) {
Write-Error "Missing: $p" Write-Error "Missing: $p"
exit 1 exit 1
} }
} }
# ── Reject a stale helper zip (Task 8) ──
# The helper zip must be newer than every .lua source that contributes
# to it. A stale zip means the running plugin does not match the on-disk
# source, which makes the reload contract meaningless.
$helperDir = Join-Path $PSScriptRoot 'pcsx_debug_helper'
$elf32Src = Join-Path $PSScriptRoot 'elf32.lua'
$sourceLuas = @(
(Join-Path $helperDir 'autoexec.lua'),
(Join-Path $helperDir 'reload.lua'),
$elf32Src
) | Where-Object { Test-Path -LiteralPath $_ }
$zipTime = (Get-Item -LiteralPath $HelperZip).LastWriteTime
$stale = $false
foreach ($src in $sourceLuas) {
$srcTime = (Get-Item -LiteralPath $src).LastWriteTime
if ($srcTime -gt $zipTime) {
Write-Error "helper zip is older than source: $src (zip=$($zipTime.ToString('o')) src=$($srcTime.ToString('o')); rerun build_psyq.ps1 to regenerate."
$stale = $true
}
}
if ($stale) {
exit 1
}
# Kill any existing pcsx-redux so the archive file isn't locked. # Kill any existing pcsx-redux so the archive file isn't locked.
Get-Process pcsx-redux -ErrorAction SilentlyContinue | Stop-Process -Force Get-Process pcsx-redux -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 2 Start-Sleep -Seconds 2
@@ -43,17 +80,23 @@ Start-Sleep -Seconds 2
$absExe = [System.IO.Path]::GetFullPath($ExePath) $absExe = [System.IO.Path]::GetFullPath($ExePath)
$absZip = [System.IO.Path]::GetFullPath($HelperZip) $absZip = [System.IO.Path]::GetFullPath($HelperZip)
$cpuFlag = if ($Cpu -eq 'dynarec') { '-dynarec' } else { '-interpreter' }
$args = @( $args = @(
'-gdb', '-run' '-gdb', '-run'
'-loadexe', "`"$absExe`"" '-loadexe', "`"$absExe`""
'-archive', "`"$absZip`"" '-archive', "`"$absZip`""
'-webserver'
$cpuFlag
) )
Write-Host "Launching pcsx-redux..." -ForegroundColor Cyan Write-Host "Launching pcsx-redux..." -ForegroundColor Cyan
Write-Host " ps-exe : $absExe" Write-Host " ps-exe : $absExe"
Write-Host " elf : $ElfPath"
Write-Host " helper zip: $absZip" Write-Host " helper zip: $absZip"
Write-Host " gdb : localhost:$GdbPort" Write-Host " gdb : localhost:$GdbPort"
Write-Host " web : localhost:$WebPort/api/v1/lua/gte" Write-Host " web : localhost:$WebPort/api/v1/lua/reload"
Write-Host " cpu : $Cpu ($cpuFlag)"
Write-Host "" Write-Host ""
Start-Process -FilePath $PcsxPath -ArgumentList $args | Out-Null Start-Process -FilePath $PcsxPath -ArgumentList $args | Out-Null
@@ -83,12 +126,49 @@ try {
$r = Invoke-WebRequest -Uri "http://localhost:$WebPort/api/v1/lua/gte" -UseBasicParsing -TimeoutSec 5 $r = Invoke-WebRequest -Uri "http://localhost:$WebPort/api/v1/lua/gte" -UseBasicParsing -TimeoutSec 5
$firstLine = ([System.Text.Encoding]::UTF8.GetString($r.Content) -split "`n")[0] $firstLine = ([System.Text.Encoding]::UTF8.GetString($r.Content) -split "`n")[0]
Write-Host "GTE handler OK: $firstLine" -ForegroundColor Green Write-Host "GTE handler OK: $firstLine" -ForegroundColor Green
} } catch {
catch {
Write-Warning "GTE handler NOT responding: $_" Write-Warning "GTE handler NOT responding: $_"
Write-Host "Check the pcsx-redux Lua Console for debug cli messages." -ForegroundColor Yellow Write-Host "Check the pcsx-redux Lua Console for debug cli messages." -ForegroundColor Yellow
} }
# ── Prime the reload handler (Task 8) ──
# The reload handler keeps an internal ACTIVE manifest of the running
# ELF; reload requests fail with reload_not_primed until prime succeeds.
# We retry until the response carries ok=true or the launch deadline
# expires — the helper may not have finished registering handlers in the
# first web-poll cycle after the gte handler comes up.
$absElf = [System.IO.Path]::GetFullPath($ElfPath)
$encodedPath = [uri]::EscapeDataString($absElf)
$primeUri = "http://localhost:${WebPort}/api/v1/lua/reload?mode=prime&target=hello_camera&path=${encodedPath}"
Write-Host "Priming reload handler: $primeUri" -ForegroundColor Cyan
$primeDeadline = (Get-Date).AddSeconds(15)
$primeOk = $false
while ((Get-Date) -lt $primeDeadline) {
try {
$resp = Invoke-WebRequest -Method Post -Uri $primeUri -UseBasicParsing -TimeoutSec 5
$body = if ($resp.Content -is [byte[]]) {
[System.Text.Encoding]::UTF8.GetString([byte[]]$resp.Content)
} else {
[string]$resp.Content
}
$obj = $body | ConvertFrom-Json
if ($obj.ok) {
Write-Host "Prime OK: $(($obj | ConvertTo-Json -Compress))" -ForegroundColor Green
$primeOk = $true
break
} else {
Write-Host "Prime not yet ready: error=$($obj.error)" -ForegroundColor Yellow
}
} catch {
Write-Host "Prime request failed: $($_.Exception.Message)" -ForegroundColor Yellow
}
Start-Sleep -Milliseconds 500
}
if (-not $primeOk) {
Write-Warning "Prime did not return ok=true before the launch deadline. Reload requests will fail until the user primes manually."
}
Write-Host "" Write-Host ""
Write-Host "pcsx-redux running. PIDs:" -ForegroundColor Cyan Write-Host "pcsx-redux running. PIDs:" -ForegroundColor Cyan
Get-Process pcsx-redux | Select-Object Id, ProcessName | Format-Table Get-Process pcsx-redux | Select-Object Id, ProcessName | Format-Table
+82
View File
@@ -0,0 +1,82 @@
# make_helper_zip.ps1
#
# Regenerate scripts/pcsx_debug_helper.zip from scripts/pcsx_debug_helper/.
# The archive contains exactly three entries at archive root:
#
# autoexec.lua
# elf32.lua (copied in from scripts/elf32.lua before packaging)
# reload.lua
#
# Determinism: CreateFromDirectory on the same set of files produces
# identical bytes. Verified by running the same command twice and
# asserting SHA-256 equality (see plan.md Task 6 Step 4).
#
# Performance: the implementation uses System.IO.Compression.ZipFile
# (BCL, in-process). Benchmarked: ~2 ms cold, ~2 ms warm on this
# workstation. Compress-Archive is rejected because its first call
# takes ~200 ms (assembly load) and subsequent calls take ~16 ms
# (process spawn per invocation). The 50 ms budget documented in
# plan.md Task 8 Step 3 excludes the compiler/assembler toolchain.
#
# Usage:
# pwsh -NoProfile -File scripts\make_helper_zip.ps1
#
# Optional -OutputPath switches the destination. Default is
# scripts/pcsx_debug_helper.zip next to the helper dir.
#
# Companion: scripts/pcsx_debug_helper/{autoexec,elf32,reload}.lua
# tests/reload_helper_zip_regen.ps1 (planned Task 8 verifier)
[CmdletBinding()]
param(
[string]$HelperDir = (Join-Path $PSScriptRoot 'pcsx_debug_helper'),
[string]$SourcesDir = $PSScriptRoot,
[string]$OutputPath = (Join-Path $PSScriptRoot 'pcsx_debug_helper.zip')
)
$ErrorActionPreference = 'Stop'
if (-not (Test-Path -LiteralPath $HelperDir)) {
throw "helper dir not found: $HelperDir"
}
# Stage elf32.lua into the helper dir so the in-process ZipFile walker
# picks it up alongside the helper-local files. elf32.lua is the shared
# ELF32 byte reader; the production reload.lua loads it through
# Support.extra.dofile("elf32.lua") at runtime.
$elf32Src = Join-Path $SourcesDir 'elf32.lua'
$elf32Dest = Join-Path $HelperDir 'elf32.lua'
if (-not (Test-Path -LiteralPath $elf32Src)) {
throw "elf32.lua not found at $elf32Src"
}
Copy-Item -LiteralPath $elf32Src -Destination $elf32Dest -Force
try {
# Remove any existing archive so CreateFromDirectory can write fresh.
# ZipFile.CreateFromDirectory throws if the destination exists.
if (Test-Path -LiteralPath $OutputPath) {
Remove-Item -LiteralPath $OutputPath -Force
}
# In-process zip; ~2 ms cold, ~2 ms warm. BCL compression matches
# Compress-Archive at CompressionLevel Optimal for these small files.
# Assembly is loaded once per pwsh.exe; the first run pays ~14 ms,
# subsequent runs pay ~0.2 ms.
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::CreateFromDirectory(
$HelperDir, $OutputPath,
[System.IO.Compression.CompressionLevel]::Optimal,
$false) | Out-Null
$sha = (Get-FileHash -LiteralPath $OutputPath -Algorithm SHA256).Hash
Write-Output ("[make_helper_zip] wrote {0} bytes, sha256={1}" -f `
(Get-Item -LiteralPath $OutputPath).Length, $sha)
Write-Output "[make_helper_zip] entries: autoexec.lua, elf32.lua, reload.lua"
}
finally {
# Remove the staged elf32.lua so the helper directory only contains
# the files the user expects to see there.
if (Test-Path -LiteralPath $elf32Dest) {
Remove-Item -LiteralPath $elf32Dest -Force
}
}
+211 -135
View File
@@ -10,8 +10,8 @@
-- Bootstrap follows the entry scripts; `scripts/duffle_paths.lua` sets package.path and package.cpath. See `ps1_meta.lua` for the rationale. -- Bootstrap follows the entry scripts; `scripts/duffle_paths.lua` sets package.path and package.cpath. See `ps1_meta.lua` for the rationale.
-- `debug.getinfo(1, "S").source` locates this file for standalone and orchestrated runs, then `duffle_paths.lua` returns the loaded `duffle` module. -- `debug.getinfo(1, "S").source` locates this file for standalone and orchestrated runs, then `duffle_paths.lua` returns the loaded `duffle` module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- The annotation pass reads the source-derived registries from scan_source: -- The annotation pass reads the source-derived registries from scan_source:
-- * pipe_ctx.register_alias_registry — for atom_dbg_reg_default(R_X, ...) and atom_reg_types(R_X, ...) member-identity checks -- * pipe_ctx.register_alias_registry — for atom_dbg_reg_default(R_X, ...) and atom_reg_types(R_X, ...) member-identity checks
@@ -21,40 +21,76 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- Type declarations -- Type declarations
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- SourceFile, PassCtx, PassResult, PassShared, Corpus, Finding: see ps1_meta.lua --- @class SourceFile
-- SourceScan, AtomEntry, AtomInfoEntry, BindsEntry, RegTypeDefault, AtomViewEntry: see scan_source.lua --- @field path string -- Absolute path to the source file
--- @field text string -- Full source text
--- @field dir string -- Directory containing the source
--- @field basename string -- Filename without extension
--- @field scan table -- Pre-scanned SourceScan payload (from duffle.scan_source)
--- @class RegTypeOccurrence --- @class PassCtx
--- @field reg string --- @field sources SourceFile[]
--- @field type_name string --- @field metadata_path string
--- @field source_line integer --- @field shared table
--- @field shared.word_counts table<string, integer>
--- @field out_root string
--- @field project_root string
--- @field upstream table<string, table>
--- @field flags table
--- @field verbose boolean
--- @class PassResult
--- @field outputs table[]
--- @field errors table[]
--- @field warnings table[]
--- @class AtomAnnotation
--- @field line integer -- Source line of the atom_info call
--- @field macro string -- Macro name (always "atom_info" in the new shape)
--- @field name string -- Atom name
--- @field kind string -- Always "info"
--- @field binds string|nil -- Binds_X name if any
--- @field reads string[] -- R_* names (read targets)
--- @field writes string[] -- R_* names (write targets)
--- @field errors string[]|nil -- Parse-time errors from scan_source (atom_info body malformed)
--- @class DebugSkipMarker -- Sub-shape of scan_source.lua's @class DebugSkipMarker
--- @field marker_kind string -- Exact marker ident read from source. Only "atom_dbg_skip" (bare) is positive.
--- @field marker_line integer
--- @field args string|nil -- Trimmed text inside the parens (nil when has_parens is false)
--- @field has_parens boolean
--- @field is_bare boolean -- true iff marker_kind == "atom_dbg_skip" AND has_parens == false (the only positive form)
--- @field pending boolean -- true while awaiting the following declaration
--- @field superseded_by_marker_line integer|nil -- Set on a marker that was bumped out of the pending slot
--- @field target_kind string|nil -- "atom" | "comp_bare" | "comp_proc" | "unrelated" once observed
--- @class Finding
--- @field line integer -- Source line (or 0 for pass-level)
--- @field msg string -- Finding message
--- @class Findings --- @class Findings
--- @field errors Finding[] --- @field errors Finding[]
--- @field warnings Finding[] --- @field warnings Finding[]
--- @field info Finding[] --- @field info Finding[]
-- PassScratch: see ps1_meta.lua --- @class PipeCtx
--- @field atom_index table<string, AtomAnnotation> -- Name -> AtomAnnotation (only kind=="atom")
--- @field binds_index table<string, BindsStruct> -- Name -> BindsStruct
--- @field annot_counts table<string, integer> -- Name -> annotation count (for unique_annotation check)
--- @field types table<string, RegTypeDefault> -- From scan_source
--- @field atom_views table<string, AtomViewEntry> -- From scan_source
--- @field seen_defaults table<string, integer> -- Duplicate atom_dbg_reg_default detection
--- @field seen_field table<string, integer> -- Binds_* -> count of fields (set/checked by check_binds_no_duplicate_fields)
--- @field _scan SourceScan -- Full scan payload (typed-view sub-calls live here)
--- @class AnnotatedResult --- @class AnnotatedResult
--- @field atoms AtomEntry[] --- @field atoms AtomEntry[]
--- @field annots AtomInfoEntry[] --- @field annots AtomAnnotation[]
--- @field macros MacroEntry[] --- @field macros MacroEntry[]
--- @field binds BindsEntry[] --- @field binds BindsEntry[]
--- @field errors Finding[] --- @field errors Finding[]
--- @field warnings Finding[] --- @field warnings Finding[]
--- @field info Finding[] --- @field info Finding[]
--- @field source string|nil
--- @class CheckRule
--- @field per_annot (fun(item: AtomInfoEntry, pipe_ctx: PassScratch, findings: Findings): nil)|nil
--- @class SourceScan
--- @field type_occurrences RegTypeOccurrence[]|nil
--- @class AnnotationPass
--- @field validate fun(ctx: PassCtx, src: SourceFile, corpus_pipe_ctx: PassScratch|nil): AnnotatedResult
--- @field run fun(ctx: PassCtx): PassResult
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Per-check functions (the CHECK_RULES table's payload) -- Per-check functions (the CHECK_RULES table's payload)
@@ -63,27 +99,24 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- `macro_word_drift` writes errors[] for missing or mismatched metadata and info[] for a match. --- `macro_word_drift` writes errors[] for missing or mismatched metadata and info[] for a match.
--- Check: Every annotated atom must have a matching MipsAtom_(name) declaration. --- Check: Every annotated atom must have a matching MipsAtom_(name) declaration.
--- @param info AtomInfoEntry --- @param a AtomAnnotation
--- @param pipe_ctx PassScratch --- @param pipe_ctx PipeCtx
--- @param findings Findings --- @param findings Findings
--- @return nil local function check_atom_decl_exists(a, pipe_ctx, findings)
local function check_atom_decl_exists(info, pipe_ctx, findings) if not pipe_ctx.atom_index[a.name] then
if not pipe_ctx.atom_index[info.atom_name] then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = info.info_line, line = a.line,
msg = string.format("annotation for '%s' has no matching MipsAtom_(%s) { ... }", info.atom_name, info.atom_name), msg = string.format("annotation for '%s' has no matching MipsAtom_(%s) { ... }", a.name, a.name),
} }
end end
end end
--- Check: Every atom may have AT MOST ONE annotation. --- Check: Every atom may have AT MOST ONE annotation.
--- Post-loop: Needs full-corpus `annot_counts` from pipe_ctx. --- Post-loop: Needs full-corpus `annot_counts` from pipe_ctx.
--- @param _item AtomInfoEntry|nil --- @param pipe_ctx PipeCtx
--- @param pipe_ctx PassScratch
--- @param findings Findings --- @param findings Findings
--- @return nil local function check_unique_annotation(pipe_ctx, findings)
local function check_unique_annotation(_item, pipe_ctx, findings) for name, n in pairs(pipe_ctx.annot_counts) do
for name, n in pairs(pipe_ctx.annot_counts) do ---@type string, integer
if n > 1 then if n > 1 then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = pipe_ctx.atom_index[name] and pipe_ctx.atom_index[name].line or 0, line = pipe_ctx.atom_index[name] and pipe_ctx.atom_index[name].line or 0,
@@ -95,30 +128,27 @@ end
--- Check: BIND atoms must reference a real Binds_* struct. --- Check: BIND atoms must reference a real Binds_* struct.
--- I keep this as a warning so the annotation pass can report the common test-fixture case; `check_abi_handoff` in static analysis supplies the build-stopping error. --- I keep this as a warning so the annotation pass can report the common test-fixture case; `check_abi_handoff` in static analysis supplies the build-stopping error.
--- @param info AtomInfoEntry --- @param a AtomAnnotation
--- @param pipe_ctx PassScratch --- @param pipe_ctx PipeCtx
--- @param findings Findings --- @param findings Findings
--- @return nil local function check_binds_struct_exists(a, pipe_ctx, findings)
local function check_binds_struct_exists(info, pipe_ctx, findings) if not a.binds then return end
if not info.binds then return end if pipe_ctx.binds_index[a.binds] then return end
if pipe_ctx.binds_index[info.binds] then return end
findings.warnings[#findings.warnings + 1] = { findings.warnings[#findings.warnings + 1] = {
line = info.info_line, line = a.line,
msg = string.format("'%s' binds '%s' but no Struct_(%s) { ... } " msg = string.format("'%s' binds '%s' but no Struct_(%s) { ... } "
.. "declaration found (also flagged as an error by check_abi_handoff in the static-analysis pass)" .. "declaration found (also flagged as an error by check_abi_handoff in the static-analysis pass)"
, info.atom_name, info.binds, info.binds), , a.name, a.binds, a.binds),
} }
end end
--- Check: TAPE_WORDS(mac_X, N) ↔ WORD_COUNT(mac_X, N) drift. --- Check: TAPE_WORDS(mac_X, N) ↔ WORD_COUNT(mac_X, N) drift.
--- Three outcomes: missing (error), mismatch (error), match (info). --- Three outcomes: missing (error), mismatch (error), match (info).
--- @param m MacroEntry --- @param m MacroEntry
--- @param pipe_ctx PassScratch --- @param wc table<string, integer> -- Shared word-count table (from ctx.shared.word_counts)
--- @param findings Findings --- @param findings Findings
--- @return nil local function check_macro_word_drift(m, wc, findings)
local function check_macro_word_drift(m, pipe_ctx, findings) local declared = wc[m.name]
local wc = (pipe_ctx and pipe_ctx.word_counts) or {} ---@type WordCounts
local declared = wc[m.name] ---@type integer|nil
if not declared then if not declared then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = m.line, line = m.line,
@@ -142,13 +172,12 @@ end
--- Check: atom_dbg_reg_default(R_X, <type>) targets an alias in `pipe_ctx.register_alias_registry` and a type in `pipe_ctx.type_name_registry`. --- Check: atom_dbg_reg_default(R_X, <type>) targets an alias in `pipe_ctx.register_alias_registry` and a type in `pipe_ctx.type_name_registry`.
--- Pointer depth remains bounded to 0 or 1, and duplicate defaults remain errors. --- Pointer depth remains bounded to 0 or 1, and duplicate defaults remain errors.
--- @param _src SourceFile -- unused (kept for the per_source shape) --- @param _src SourceFile -- unused (kept for the per_source shape)
--- @param pipe_ctx PassScratch --- @param pipe_ctx PipeCtx
--- @param findings Findings --- @param findings Findings
--- @return nil
local function check_semantic_reg_defaults(_src, pipe_ctx, findings) local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
-- Detect duplicate defaults using the ordered occurrence list (the out.types hash only retains the last declaration). -- Detect duplicate defaults using the ordered occurrence list (the out.types hash only retains the last declaration).
local seen_first_line = {} ---@type table<string, integer> -- bag: register ident -> first source line local seen_first_line = {}
for _, occ in ipairs(pipe_ctx.type_occurrences or {}) do ---@type integer, RegTypeOccurrence for _, occ in ipairs(pipe_ctx.type_occurrences or {}) do
if seen_first_line[occ.reg] == nil then if seen_first_line[occ.reg] == nil then
seen_first_line[occ.reg] = occ.source_line seen_first_line[occ.reg] = occ.source_line
else else
@@ -160,9 +189,9 @@ local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
} }
end end
end end
local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table<string, AliasEntry> local reg_registry = pipe_ctx.register_alias_registry or {}
local type_registry = pipe_ctx.type_name_registry or {} ---@type table<string, TypeNameEntry> local type_registry = pipe_ctx.type_name_registry or {}
for reg, def in pairs(pipe_ctx.types or {}) do ---@type string, RegTypeDefault for reg, def in pairs(pipe_ctx.types or {}) do
if not reg_registry[reg] then if not reg_registry[reg] then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = def.source_line, line = def.source_line,
@@ -193,15 +222,14 @@ end
--- Check: atom_reg_types(R_X, <type>) entries target an alias in `pipe_ctx.register_alias_registry` and a type in `pipe_ctx.type_name_registry`. --- Check: atom_reg_types(R_X, <type>) entries target an alias in `pipe_ctx.register_alias_registry` and a type in `pipe_ctx.type_name_registry`.
--- A bare `atom_reg` marker opts the `R_<n>` alias into GPR identity; references to R_T0..R_T3 require the same explicit marker. --- A bare `atom_reg` marker opts the `R_<n>` alias into GPR identity; references to R_T0..R_T3 require the same explicit marker.
--- @param _src SourceFile --- @param _src SourceFile
--- @param pipe_ctx PassScratch --- @param pipe_ctx PipeCtx
--- @param findings Findings --- @param findings Findings
--- @return nil
local function check_atom_reg_types(_src, pipe_ctx, findings) local function check_atom_reg_types(_src, pipe_ctx, findings)
local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table<string, AliasEntry> local reg_registry = pipe_ctx.register_alias_registry or {}
local type_registry = pipe_ctx.type_name_registry or {} ---@type table<string, TypeNameEntry> local type_registry = pipe_ctx.type_name_registry or {}
for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do ---@type integer, AtomInfoEntry for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do
if ai.reg_type_overrides then if ai.reg_type_overrides then
for reg, ov in pairs(ai.reg_type_overrides) do ---@type string, RegTypeOverride for reg, ov in pairs(ai.reg_type_overrides) do
if not reg_registry[reg] then if not reg_registry[reg] then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = ai.info_line, line = ai.info_line,
@@ -225,15 +253,14 @@ end
--- Check: atom_view(Binds_X) entries reference a Binds_* struct with at least one field. --- Check: atom_view(Binds_X) entries reference a Binds_* struct with at least one field.
--- @param _src SourceFile --- @param _src SourceFile
--- @param pipe_ctx PassScratch --- @param pipe_ctx PipeCtx
--- @param findings Findings --- @param findings Findings
--- @return nil
local function check_atom_view_layout(_src, pipe_ctx, findings) local function check_atom_view_layout(_src, pipe_ctx, findings)
for atom_name, view in pairs(pipe_ctx.atom_views or {}) do ---@type string, AtomViewEntry for atom_name, view in pairs(pipe_ctx.atom_views or {}) do
if not view.binds_name then if not view.binds_name then
-- The atom had atom_reg_types but no atom_view; no layout check needed. -- The atom had atom_reg_types but no atom_view; no layout check needed.
else else
local bs = pipe_ctx.binds_index[view.binds_name] ---@type BindsEntry|nil local bs = pipe_ctx.binds_index[view.binds_name]
if not bs then if not bs then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = view.info_line, line = view.info_line,
@@ -255,16 +282,15 @@ end
--- Check: Binds_* structs require unique field names because atom_view uses those names for typed-field lookup in gdb. --- Check: Binds_* structs require unique field names because atom_view uses those names for typed-field lookup in gdb.
--- @param _src SourceFile --- @param _src SourceFile
--- @param pipe_ctx PassScratch --- @param pipe_ctx PipeCtx
--- @param findings Findings --- @param findings Findings
--- @return nil
local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings) local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings)
for _, bs in ipairs(pipe_ctx.binds_list or {}) do ---@type integer, BindsEntry for _, bs in ipairs(pipe_ctx.binds_list or {}) do
local seen = {} ---@type table<string, integer> -- bag: field name -> occurrence count local seen = {}
for _, f in ipairs(bs.fields or {}) do ---@type integer, TypeField for _, f in ipairs(bs.fields or {}) do
seen[f.name] = (seen[f.name] or 0) + 1 seen[f.name] = (seen[f.name] or 0) + 1
end end
for name, count in pairs(seen) do ---@type string, integer for name, count in pairs(seen) do
if count > 1 then if count > 1 then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = bs.line, line = bs.line,
@@ -288,12 +314,11 @@ end
--- 6. unsupported target_kind -> marker precedes an unrelated declaration --- 6. unsupported target_kind -> marker precedes an unrelated declaration
--- Valid markers stamp `debug_skip` on whole-atom, bare-component, and proc-component declaration records in scan_source.lua. --- Valid markers stamp `debug_skip` on whole-atom, bare-component, and proc-component declaration records in scan_source.lua.
--- @param marker DebugSkipMarker --- @param marker DebugSkipMarker
--- @param _pipe_ctx PassScratch -- Unused; kept for consistency with per_annot --- @param _pipe_ctx PipeCtx -- Unused; kept for consistency with per_annot // TODO(Ed): Remove?
--- @param findings Findings --- @param findings Findings
--- @return nil
local function check_skip_marker(marker, _pipe_ctx, findings) local function check_skip_marker(marker, _pipe_ctx, findings)
local kind = marker.marker_kind ---@type string local kind = marker.marker_kind
local line = marker.marker_line ---@type integer local line = marker.marker_line
-- Left `scan.debug_skip_markers` with production records for `atom_dbg_skip` only; other identifiers take the walker's unrelated branch. -- Left `scan.debug_skip_markers` with production records for `atom_dbg_skip` only; other identifiers take the walker's unrelated branch.
if marker.has_parens then if marker.has_parens then
@@ -346,16 +371,15 @@ end
--- Warn when a source references an unregistered alias. --- Warn when a source references an unregistered alias.
--- When a source uses an unregistered R_X, this check emits one pass-level info entry for that source and directs C-ABI register names to explicit alias registration. --- When a source uses an unregistered R_X, this check emits one pass-level info entry for that source and directs C-ABI register names to explicit alias registration.
--- @param _src SourceFile --- @param _src SourceFile
--- @param pipe_ctx PassScratch --- @param pipe_ctx PipeCtx
--- @param findings Findings --- @param findings Findings
--- @return nil
local function check_wave_context_migration(_src, pipe_ctx, findings) local function check_wave_context_migration(_src, pipe_ctx, findings)
if not (pipe_ctx.types and next(pipe_ctx.types)) then return end if not (pipe_ctx.types and next(pipe_ctx.types)) then return end
if not (pipe_ctx.atom_infos_list) then return end if not (pipe_ctx.atom_infos_list) then return end
local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table<string, AliasEntry> local reg_registry = pipe_ctx.register_alias_registry or {}
for _, ai in ipairs(pipe_ctx.atom_infos_list) do ---@type integer, AtomInfoEntry for _, ai in ipairs(pipe_ctx.atom_infos_list) do
if ai.reg_type_overrides then if ai.reg_type_overrides then
for reg, _ in pairs(ai.reg_type_overrides) do ---@type string, RegTypeOverride for reg, _ in pairs(ai.reg_type_overrides) do
if not reg_registry[reg] then if not reg_registry[reg] then
findings.warnings[#findings.warnings + 1] = { findings.warnings[#findings.warnings + 1] = {
line = 0, line = 0,
@@ -375,14 +399,14 @@ end
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- --
-- Each rule entry picks one of four "shapes" of dispatch: -- Each rule entry picks one of four "shapes" of dispatch:
-- per_annot(info, pipe_ctx, findings) -- runs once per scan.atom_infos row -- per_annot(annot, pipe_ctx, findings) -- runs once per AtomAnnotation
-- post(pipe_ctx, findings) -- runs once after all per_annot calls complete (full-corpus aggregation) -- post(pipe_ctx, findings) -- runs once after all per_annot calls complete (full-corpus aggregation)
-- per_macro(macro, wc, findings) -- runs once per TAPE_WORDS / _Pragma macro declaration -- per_macro(macro, wc, findings) -- runs once per TAPE_WORDS / _Pragma macro declaration
-- per_skip_marker(marker, pipe_ctx, findings) -- runs once per src.scan.debug_skip_markers entry -- per_skip_marker(marker, pipe_ctx, findings) -- runs once per src.scan.debug_skip_markers entry
-- --
-- Adding a new check = 1 row here + 1 function above. The `validate()` dispatch loop never needs editing. -- Adding a new check = 1 row here + 1 function above. The `validate()` dispatch loop never needs editing.
local CHECK_RULES = { ---@type CheckRule[] local CHECK_RULES = {
{ name = "atom_decl_exists", per_annot = check_atom_decl_exists }, { name = "atom_decl_exists", per_annot = check_atom_decl_exists },
{ name = "binds_struct_exists", per_annot = check_binds_struct_exists }, { name = "binds_struct_exists", per_annot = check_binds_struct_exists },
{ name = "unique_annotation", post = check_unique_annotation }, { name = "unique_annotation", post = check_unique_annotation },
@@ -403,35 +427,81 @@ local CHECK_RULES = { ---@type CheckRule[]
--- Builds one pass-wide pipe_ctx from the merged `corpus.*` registries and source-ordered `corpus.atom_infos`; per-source declarations and bodies remain in `src.scan`. --- Builds one pass-wide pipe_ctx from the merged `corpus.*` registries and source-ordered `corpus.atom_infos`; per-source declarations and bodies remain in `src.scan`.
--- The module ownership contract above requires callers to construct `ctx.shared.corpus` through `build_ctx`; the error message below enforces that gate. --- The module ownership contract above requires callers to construct `ctx.shared.corpus` through `build_ctx`; the error message below enforces that gate.
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassScratch --- @return PipeCtx
local function build_corpus_pipe_ctx(ctx) local function build_corpus_pipe_ctx(ctx)
local view = duffle.corpus_view(ctx) ---@type PassScratch local corpus = ctx.shared and ctx.shared.corpus
local annot_counts = {} ---@type table<string, integer> -- bag: atom name -> annotation count if not corpus then
for _, info in ipairs(view.atom_infos) do ---@type integer, AtomInfoEntry error("annotation requires ctx.shared.corpus "
.. "(the canonical corpus is the source of truth; "
.. "no per-source fallback is supported)", 0)
end
-- `corpus.atom_infos` preserves source order and duplicates; I precompute counts here for `check_unique_annotation` and the per-source checks.
local annot_counts = {}
for _, info in ipairs(corpus.atom_infos or {}) do
if info and info.atom_name then if info and info.atom_name then
annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1 annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1
end end
end end
view.annot_counts = annot_counts
view.atom_infos_list = view.atom_infos -- Every consumer of these fields observes mutations via the canonical corpus without independently mutable registry construction.
view.word_counts = ctx.shared.corpus.word_counts or {} return {
return view -- Cross-source lookup tables from corpus.
register_alias_registry = corpus.register_alias_registry or {},
type_name_registry = corpus.type_name_registry or {},
atom_views = corpus.atom_views or {},
atom_ctxs = corpus.atom_ctxs or {},
atom_phases = corpus.atom_phases or {},
binds_by_name = corpus.binds_by_name or {},
atoms_by_name = corpus.atoms_by_name or {},
-- Corpus-wide ordered list of atom_info records (source-order + duplicates).
atom_infos_list = corpus.atom_infos or {},
-- Corpus-wide annotation count aggregation (post-rule consumes this).
annot_counts = annot_counts,
-- Corpus-wide collisions (recorded by scan_source.merge_corpus_registries).
collisions = corpus.collisions or {},
-- `check_macro_word_drift` reads `corpus.word_counts`, populated by word_count_eval.run.
word_counts = corpus.word_counts or {},
}
end end
--- Validate one source against its pre-scanned SourceScan payload + the corpus-wide pipe_ctx. --- Validate one source against its pre-scanned SourceScan payload + the corpus-wide pipe_ctx.
--- @param ctx PassCtx --- @param ctx PassCtx
--- @param src SourceFile --- @param src SourceFile
--- @param corpus_pipe_ctx PassScratch|nil -- Built once per pass from corpus registries; nil builds the same projection here. --- @param corpus_pipe_ctx PipeCtx|nil -- Built once per pass from corpus registries; nil builds the same projection here.
--- @return AnnotatedResult --- @return AnnotatedResult
local function validate(ctx, src, corpus_pipe_ctx) local function validate(ctx, src, corpus_pipe_ctx)
corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx) corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx)
local scan = src.scan ---@type SourceScan local scan = src.scan
-- Project the pre-scanned atoms to the AtomEntry shape this pass needs.
local atoms = {}
for _, a in ipairs(scan.atoms) do
if a.kind == "atom" then
atoms[#atoms + 1] = { line = a.line, name = a.raw_name }
end
end
-- Project the pre-scanned atom_infos to AtomAnnotation shape.
local annots = {}
for _, info in ipairs(scan.atom_infos) do
annots[#annots + 1] = {
line = info.info_line,
macro = "atom_info",
name = info.atom_name,
kind = "info",
binds = info.binds,
reads = info.reads or {},
writes = info.writes or {},
errors = info.errors,
}
end
-- Build a per-source pipe_ctx: shared lookups come from `corpus_pipe_ctx`, while declarations, bodies, types, views, defaults, and occurrences come from `src.scan`. -- Build a per-source pipe_ctx: shared lookups come from `corpus_pipe_ctx`, while declarations, bodies, types, views, defaults, and occurrences come from `src.scan`.
local seen_defaults = {}; for reg, _ in pairs (scan.types or {}) do seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 end ---@type table<string, integer> -- bag: register ident -> occurrence count local seen_defaults = {}; for reg, _ in pairs (scan.types or {}) do seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 end
local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end ---@type AtomInfoEntry[] local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end
local pipe_ctx = { ---@type PassScratch local pipe_ctx = {
atom_index = {}, atom_index = {},
binds_index = {}, binds_index = {},
annot_counts = corpus_pipe_ctx.annot_counts, annot_counts = corpus_pipe_ctx.annot_counts,
@@ -445,66 +515,70 @@ local function validate(ctx, src, corpus_pipe_ctx)
register_alias_registry = corpus_pipe_ctx.register_alias_registry, register_alias_registry = corpus_pipe_ctx.register_alias_registry,
type_name_registry = corpus_pipe_ctx.type_name_registry, type_name_registry = corpus_pipe_ctx.type_name_registry,
} }
local atoms = {} ---@type AtomEntry[] for _, a in ipairs(atoms) do pipe_ctx.atom_index [a.name] = a end
for _, a in ipairs(scan.atoms) do ---@type integer, AtomEntry for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end
if a.kind == "atom" or a.kind == "atom_proc" then
atoms[#atoms + 1] = a
pipe_ctx.atom_index[a.raw_name or a.name] = a
end
end
for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end ---@type integer, BindsEntry
-- Findings live in a single struct with three lists (errors / warnings / info). -- Findings live in a single struct with three lists (errors / warnings / info).
-- Each check writes to the list appropriate for its severity. -- Each check writes to the list appropriate for its severity.
local findings = { errors = {}, warnings = {}, info = {} } ---@type Findings local findings = { errors = {}, warnings = {}, info = {} }
-- Lift parse-time errors already recorded in scan_source's atom_info payload into this pass's findings list. -- Lift parse-time errors already recorded in scan_source's atom_info payload into this pass's findings list.
for _, info in ipairs(scan.atom_infos) do ---@type integer, AtomInfoEntry for _, a in ipairs(annots) do
if info.errors then if a.errors then
for _, msg in ipairs(info.errors) do ---@type integer, string for _, msg in ipairs(a.errors) do
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = info.info_line, line = a.line,
msg = string.format("'%s': %s", info.atom_name, msg), msg = string.format("'%s': %s", a.name, msg),
} }
end end
end end
end end
-- THE per-annotation pipeline. ONE loop. CHECK_RULES dispatches per_annot rules. -- THE per-annotation pipeline. ONE loop. CHECK_RULES dispatches per_annot rules.
for _, info in ipairs(scan.atom_infos) do ---@type integer, AtomInfoEntry for _, a in ipairs(annots) do
duffle.run_check_rules(CHECK_RULES, "per_annot", info, pipe_ctx, findings) for _, rule in ipairs(CHECK_RULES) do
if rule.per_annot then rule.per_annot(a, pipe_ctx, findings) end
end
end end
-- Post-loop rules (one-shot checks that need full-corpus aggregation in pipe_ctx). -- Post-loop rules (one-shot checks that need full-corpus aggregation in pipe_ctx).
duffle.run_check_rules(CHECK_RULES, "post", nil, pipe_ctx, findings) for _, rule in ipairs(CHECK_RULES) do
if rule.post then rule.post(pipe_ctx, findings) end
end
-- scan_source records each marker in scan.debug_skip_markers; this loop validates each record independently and emits at most one error per marker. -- scan_source records each marker in scan.debug_skip_markers; this loop validates each record independently and emits at most one error per marker.
-- Valid markers stamp `debug_skip = true` on the following atom or component declaration, which downstream consumers read directly. -- Valid markers stamp `debug_skip = true` on the following atom or component declaration, which downstream consumers read directly.
local skip_markers = scan.debug_skip_markers or {} ---@type DebugSkipMarker[] local skip_markers = scan.debug_skip_markers or {}
for _, marker in ipairs(skip_markers) do ---@type integer, DebugSkipMarker for _, marker in ipairs(skip_markers) do
duffle.run_check_rules(CHECK_RULES, "per_skip_marker", marker, pipe_ctx, findings) for _, rule in ipairs(CHECK_RULES) do
if rule.per_skip_marker then rule.per_skip_marker(marker, pipe_ctx, findings) end
end
end end
-- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift). -- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift).
pipe_ctx.word_counts = corpus_pipe_ctx.word_counts local wc = corpus_pipe_ctx.word_counts
for _, m in ipairs(scan.macros) do ---@type integer, MacroEntry for _, m in ipairs(scan.macros) do
duffle.run_check_rules(CHECK_RULES, "per_macro", m, pipe_ctx, findings) for _, rule in ipairs(CHECK_RULES) do
if rule.per_macro then rule.per_macro(m, wc, findings) end
end
end end
-- Per-source rules (reg defaults, atom_view layout, compute-register type overrides, Binds_* field uniqueness). -- Per-source rules (reg defaults, atom_view layout, compute-register type overrides, Binds_* field uniqueness).
-- Each per_source rule sees the full scan payload via pipe_ctx. -- Each per_source rule sees the full scan payload via pipe_ctx.
duffle.run_check_rules(CHECK_RULES, "per_source", src, pipe_ctx, findings) for _, rule in ipairs(CHECK_RULES) do
if rule.per_source then rule.per_source(src, pipe_ctx, findings) end
end
-- Information summary (always emitted). -- Information summary (always emitted).
findings.info[#findings.info + 1] = { findings.info[#findings.info + 1] = {
line = 0, line = 0,
msg = string.format("scanned: %d atom(s), %d annotation(s), %d macro-word-decl(s), %d binds struct(s)" msg = string.format("scanned: %d atom(s), %d annotation(s), %d macro-word-decl(s), %d binds struct(s)"
, #atoms, #scan.atom_infos, #scan.macros, #scan.binds), , #atoms, #annots, #scan.macros, #scan.binds),
} }
return { return {
atoms = atoms, atoms = atoms,
annots = scan.atom_infos, annots = annots,
macros = scan.macros, macros = scan.macros,
binds = scan.binds, binds = scan.binds,
errors = findings.errors, errors = findings.errors,
@@ -517,7 +591,9 @@ end
-- M.run — orchestrator entry -- M.run — orchestrator entry
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
local M = {} ---@type AnnotationPass --- @class M
local M = {}
-- Expose `validate` for downstream passes (e.g. report.lua) that need to re-render the per-source results into a per-MODULE report. -- Expose `validate` for downstream passes (e.g. report.lua) that need to re-render the per-source results into a per-MODULE report.
M.validate = validate M.validate = validate
@@ -525,32 +601,32 @@ M.validate = validate
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
local outputs = {} ---@type PassOutputEntry[] local outputs = {}
local errors = {} ---@type Finding[] local errors = {}
local warnings = {} ---@type Finding[] local warnings = {}
-- Build the shared pipe_ctx once for this run; every validate() call sees the same cross-source registries. -- Build the shared pipe_ctx once for this run; every validate() call sees the same cross-source registries.
-- The corpus owns the canonical cross-source registries; per-source scans retain body / declaration ownership. -- The corpus owns the canonical cross-source registries; per-source scans retain body / declaration ownership.
local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx) ---@type PassScratch local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx)
local corpus = ctx.shared.corpus ---@type Corpus local corpus = ctx.shared.corpus
-- Group `corpus.sources_by_dir` by module, validate every source in each bucket, and emit one errors.h per directory. -- Group `corpus.sources_by_dir` by module, validate every source in each bucket, and emit one errors.h per directory.
local by_dir = (corpus and corpus.sources_by_dir) or {} ---@type table<string, SourceFile[]> local by_dir = (corpus and corpus.sources_by_dir) or {}
for dir, dir_sources in pairs(by_dir) do ---@type string, SourceFile[] for dir, dir_sources in pairs(by_dir) do
local dir_basename = dir:match("([^/\\]+)$") or dir ---@type string local dir_basename = dir:match("([^/\\]+)$") or dir
local dir_atoms = 0 ---@type integer local dir_atoms = 0
local dir_errors = {} ---@type Finding[] local dir_errors = {}
local dir_warnings = {} ---@type Finding[] local dir_warnings = {}
for _, src in ipairs(dir_sources) do ---@type integer, SourceFile for _, src in ipairs(dir_sources) do
local result = validate(ctx, src, corpus_pipe_ctx) ---@type AnnotatedResult local result = validate(ctx, src, corpus_pipe_ctx)
result.source = src.path -- tag for downstream rendering result.source = src.path -- tag for downstream rendering
dir_atoms = dir_atoms + #result.atoms dir_atoms = dir_atoms + #result.atoms
for _, e in ipairs(result.errors) do ---@type integer, Finding for _, e in ipairs(result.errors) do
dir_errors[#dir_errors + 1] = { line = e.line, msg = e.msg, source = src.path } dir_errors[#dir_errors + 1] = { line = e.line, msg = e.msg, source = src.path }
errors [#errors + 1] = { line = e.line, msg = e.msg } errors [#errors + 1] = { line = e.line, msg = e.msg }
end end
for _, w in ipairs(result.warnings) do ---@type integer, Finding for _, w in ipairs(result.warnings) do
dir_warnings[#dir_warnings + 1] = { line = w.line, msg = w.msg } dir_warnings[#dir_warnings + 1] = { line = w.line, msg = w.msg }
warnings [#warnings + 1] = { line = w.line, msg = w.msg } warnings [#warnings + 1] = { line = w.line, msg = w.msg }
end end
+108 -166
View File
@@ -37,9 +37,9 @@
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` -- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source`
-- (works both standalone + when require'd). `duffle_paths.lua` sets package.path then returns `require("duffle")` -- (works both standalone + when require'd). `duffle_paths.lua` sets package.path then returns `require("duffle")`
-- at the bottom, so the dofile value IS the duffle module. -- at the bottom, so the dofile value IS the duffle module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
local elf_dwarf = require("elf_dwarf") ---@type ElfDwarfMod local elf_dwarf = require("elf_dwarf")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Constants -- Constants
@@ -47,78 +47,42 @@ local elf_dwarf = require("elf_dwarf")
-- Format version emitted as the first line. Bump + add a migration test if the format changes; -- Format version emitted as the first line. Bump + add a migration test if the format changes;
-- the gdb runtime loader rejects mismatches (E2). -- the gdb runtime loader rejects mismatches (E2).
local FORMAT_VERSION = 1 ---@type integer local FORMAT_VERSION = 1
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Type declarations -- Type declarations
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @class AtomSourceMapCtx --- @class AtomSourceMapCtx
--- @field shared PassShared --- @field shared table -- `ctx.shared`
--- @field out_root string --- @field shared.corpus table -- source-order registry; single writer is build_ctx
--- @field flags PassFlags --- @field shared.word_counts table
--- @field project_root string|nil --- @field out_root string -- output root (e.g. "build/gen")
--- @field flags table -- `ctx.flags`; reads `flags.gdb_runtime` + `flags.elf_path`
--- @class WordMapEntry
--- @field pos integer
--- @field line integer
--- @field text string
--- @field body_line integer
--- @field gpr_keys string[]|nil
--- @field invocation InvocationRecord|nil
--- @class NmAddr
--- @field [1] integer -- st_value
--- @field [2] integer -- st_size
--- @class GdbAtomRecord
--- @field idx integer|nil
--- @field name string
--- @field src_path string
--- @field file_base string
--- @field addr integer
--- @field size_bytes integer
--- @field words integer
--- @field entries WordMapEntry[]
--- @class ElfDwarfMod
--- @field read_nm fun(elf_path: Path): table<string, NmAddr>
--- @class AtomSourceMapPass
--- @field render_source_map fun(src: SourceFile): string
--- @field render_provenance fun(src: SourceFile, wc: WordCounts): string
--- @field render_atom_source_map fun(atom: AtomEntry): string
--- @field render_atom_provenance fun(atom: AtomEntry, wc: WordCounts, rel_path: string): string
--- @field run fun(ctx: PassCtx): PassResult
--- @class AtomEntry
--- @field paths AtomPaths|nil
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Atom-path renderers -- Atom-path renderers
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- Join word boundaries (from `items`) to per-word call text + source lines (from `word_events`). --- Join word boundaries (from `items`) to per-word call text + source lines (from `word_events`).
--- @param atom AtomEntry --- @param atom table
--- @return WordMapEntry[] --- @return table[], integer
--- @return integer
local function canonical_word_entries(atom) local function canonical_word_entries(atom)
local paths = atom.paths or {} ---@type AtomPaths local paths = atom.paths or {}
local events = paths.word_events or {} ---@type WordEvent[] local events = paths.word_events or {}
local word_items = {} ---@type EmissionItem[] local word_items = {}
for _, item in ipairs(paths.items or {}) do ---@type integer, EmissionItem for _, item in ipairs(paths.items or {}) do
if item.kind == "word" then word_items[#word_items + 1] = item end if item.kind == "word" then word_items[#word_items + 1] = item end
end end
local entries = {} ---@type WordMapEntry[] local entries = {}
for index, event in ipairs(events) do ---@type integer, WordEvent for index, event in ipairs(events) do
local item = word_items[index] or {} ---@type EmissionItem local item = word_items[index] or {}
entries[#entries + 1] = { entries[#entries + 1] = {
pos = event.i or (index - 1), pos = event.i or (index - 1),
line = event.call_line or item.line or 0, line = event.call_line or item.line or 0,
text = event.call_text or item.call_text or "", text = event.call_text or item.call_text or "",
body_line = event.body_line or item.body_line or item.line or 0, body_line = event.body_line or item.body_line or item.line or 0,
gpr_keys = event.gpr_keys,
invocation = (event.outermost_invocation_id invocation = (event.outermost_invocation_id
and paths.invocations and paths.invocations
and paths.invocations[event.outermost_invocation_id]) or nil, and paths.invocations[event.outermost_invocation_id]) or nil,
@@ -132,20 +96,19 @@ end
--- `WORD N CALL <src-path>:<src-line> RAW` (raw `.word` outside any mac_* component) --- `WORD N CALL <src-path>:<src-line> RAW` (raw `.word` outside any mac_* component)
--- Component identity comes from the outermost invocation record; the count-table lookup confirms the component was declared in `corpus.word_counts` --- Component identity comes from the outermost invocation record; the count-table lookup confirms the component was declared in `corpus.word_counts`
--- (populated by word_count_eval + components passes). --- (populated by word_count_eval + components passes).
--- @param src SourceFile --- @param src table
--- @param atom AtomEntry --- @param atom table
--- @param wc WordCounts --- @param wc table -- identity alias of corpus.word_counts
--- @return string[] --- @return string[], integer
--- @return integer
local function emit_provenance_stanza(src, atom, wc) local function emit_provenance_stanza(src, atom, wc)
local lines = {} ---@type string[] local lines = {}
local rel_path = src.path:gsub("\\\\", "/") ---@type string local rel_path = src.path:gsub("\\\\", "/")
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer local entries, total = canonical_word_entries(atom)
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path) lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry for _, entry in ipairs(entries) do
local inv = entry.invocation ---@type InvocationRecord|nil local inv = entry.invocation
local macro_count = inv and wc["mac_" .. inv.component_name] ---@type integer|nil local macro_count = inv and wc["mac_" .. inv.component_name]
if inv and macro_count ~= nil then if inv and macro_count ~= nil then
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d' lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d'
, entry.pos, rel_path, entry.line, inv.component_name , entry.pos, rel_path, entry.line, inv.component_name
@@ -161,11 +124,11 @@ local function emit_provenance_stanza(src, atom, wc)
end end
--- Render the full provenance file content for one source. --- Render the full provenance file content for one source.
--- @param src SourceFile --- @param src table
--- @param wc WordCounts --- @param wc table
--- @return string --- @return string
local function render_provenance(src, wc) local function render_provenance(src, wc)
local lines = {} ---@type string[] local lines = {}
lines[#lines + 1] = "# FORMAT_VERSION 1" lines[#lines + 1] = "# FORMAT_VERSION 1"
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT" lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
lines[#lines + 1] = "# Per-.word provenance: maps each emitted .word to its call site (atom body" lines[#lines + 1] = "# Per-.word provenance: maps each emitted .word to its call site (atom body"
@@ -174,16 +137,14 @@ local function render_provenance(src, wc)
lines[#lines + 1] = "# dwarf_injection to synthesize DW_TAG_inlined_subroutine instances + per-word" lines[#lines + 1] = "# dwarf_injection to synthesize DW_TAG_inlined_subroutine instances + per-word"
lines[#lines + 1] = "# line program rows for native source-level step into component bodies." lines[#lines + 1] = "# line program rows for native source-level step into component bodies."
--- @param atom AtomEntry
--- @return nil
local function append(atom) local function append(atom)
local stanza = emit_provenance_stanza(src, atom, wc) ---@type string[] local stanza = emit_provenance_stanza(src, atom, wc)
for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string for _, line in ipairs(stanza) do lines[#lines + 1] = line end
end end
for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry for _, atom in ipairs(src.scan.atoms or {}) do
if atom.paths then append(atom) end if atom.paths then append(atom) end
end end
for _, atom in ipairs(src.scan.raw_atoms or {}) do ---@type integer, AtomEntry for _, atom in ipairs(src.scan.raw_atoms or {}) do
if atom.paths then append(atom) end if atom.paths then append(atom) end
end end
@@ -192,17 +153,17 @@ end
--- Render one atom's stanza for the sourcemap.txt form (ATOM header line, N WORD lines, ENDATOM marker). --- Render one atom's stanza for the sourcemap.txt form (ATOM header line, N WORD lines, ENDATOM marker).
--- Returns (lines, total_words). --- Returns (lines, total_words).
--- @param src SourceFile --- @param src table
--- @param atom AtomEntry --- @param atom table
--- @return string[] --- @param wc table
--- @return integer --- @return string[], integer
local function emit_atom_stanza(src, atom) local function emit_atom_stanza(src, atom)
local lines = {} ---@type string[] local lines = {}
local rel_path = src.path:gsub("\\\\", "/") ---@type string local rel_path = src.path:gsub("\\\\", "/")
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer local entries, total = canonical_word_entries(atom)
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path) lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry for _, entry in ipairs(entries) do
lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s", lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s",
entry.pos, entry.line, entry.text) entry.pos, entry.line, entry.text)
end end
@@ -213,23 +174,22 @@ end
--- Render the full source map file content for one source (one .atoms.sourcemap.txt per source). Mirrors offsets.lua's --- Render the full source map file content for one source (one .atoms.sourcemap.txt per source). Mirrors offsets.lua's
--- `project_atoms` shape: scan.atoms + scan.raw_atoms, no kind filter. --- `project_atoms` shape: scan.atoms + scan.raw_atoms, no kind filter.
--- @param src SourceFile --- @param src table
--- @param wc table
--- @return string --- @return string
local function render_source_map(src) local function render_source_map(src)
local lines = {} ---@type string[] local lines = {}
lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT" lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
--- @param atom AtomEntry
--- @return nil
local function append(atom) local function append(atom)
local stanza = emit_atom_stanza(src, atom) ---@type string[] local stanza = emit_atom_stanza(src, atom)
for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string for _, line in ipairs(stanza) do lines[#lines + 1] = line end
end end
for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry for _, atom in ipairs(src.scan.atoms or {}) do
if atom.paths then append(atom) end if atom.paths then append(atom) end
end end
for _, atom in ipairs(src.scan.raw_atoms or {}) do ---@type integer, AtomEntry for _, atom in ipairs(src.scan.raw_atoms or {}) do
if atom.paths then append(atom) end if atom.paths then append(atom) end
end end
@@ -250,22 +210,20 @@ end
--- Build the list of atoms with addresses + word entries. Shared helper for the gdb-runtime file emission. --- Build the list of atoms with addresses + word entries. Shared helper for the gdb-runtime file emission.
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return GdbAtomRecord[] --- @return table[] -- list of {idx, name, src_path, file_base, addr, size_bytes, words, entries}
local function build_atom_table(ctx) local function build_atom_table(ctx)
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path) ---@type table<string, NmAddr> local addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil local corpus = ctx.shared and ctx.shared.corpus
local matched = {} ---@type GdbAtomRecord[] local matched = {}
for _, src in ipairs(corpus.source_order or {}) do ---@type integer, SourceFile for _, src in ipairs(corpus.source_order or {}) do
local file_base = src.path:match("([^/\\\\]+)$") or src.path ---@type string local file_base = src.path:match("([^/\\\\]+)$") or src.path
--- @param atom AtomEntry
--- @return nil
local function append(atom) local function append(atom)
if not atom.paths then return end if not atom.paths then return end
local name = atom.raw_name or atom.name ---@type string local name = atom.raw_name or atom.name
local info = addrs[name] ---@type NmAddr|nil local info = addrs[name]
if not info then return end if not info then return end
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer local entries, total = canonical_word_entries(atom)
matched[#matched + 1] = { matched[#matched + 1] = {
name = name, name = name,
src_path = src.path, src_path = src.path,
@@ -276,16 +234,13 @@ local function build_atom_table(ctx)
entries = entries, entries = entries,
} }
end end
for _, atom in ipairs((src.scan or {}).atoms or {}) do append(atom) end ---@type integer, AtomEntry for _, atom in ipairs((src.scan or {}).atoms or {}) do append(atom) end
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end ---@type integer, AtomEntry for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end
end end
-- Deterministic order: sort by address (matches `nm` output ordering). -- Deterministic order: sort by address (matches `nm` output ordering).
--- @param a GdbAtomRecord
--- @param b GdbAtomRecord
--- @return boolean
table.sort(matched, function(a, b) return a.addr < b.addr end) table.sort(matched, function(a, b) return a.addr < b.addr end)
for i, a in ipairs(matched) do a.idx = i - 1 end ---@type integer, GdbAtomRecord for i, a in ipairs(matched) do a.idx = i - 1 end
return matched return matched
end end
@@ -296,29 +251,28 @@ end
--- ---
--- Why hardcoded per-atom: gdb's `$` substitution doesn't concat inside var names — `$__atom_name_$__i` in a `while` --- Why hardcoded per-atom: gdb's `$` substitution doesn't concat inside var names — `$__atom_name_$__i` in a `while`
--- loop resolves to one literal identifier, not `name_i`. Compile-time emission is the only path. --- loop resolves to one literal identifier, not `name_i`. Compile-time emission is the only path.
--- @param lines string[] --- @param lines table -- output line buffer (mutated in place)
--- @param matched GdbAtomRecord[] --- @param matched table -- list of atom records from `build_atom_table`
--- @return nil
local function append_gdb_commands(lines, matched) local function append_gdb_commands(lines, matched)
-- ── tape_atoms ── -- ── tape_atoms ──
-- Hardcoded one printf per atom. No loop. -- Hardcoded one printf per atom. No loop.
lines[#lines + 1] = "define tape_atoms" lines[#lines + 1] = "define tape_atoms"
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord for _, a in ipairs(matched) do
-- gdb 12.1 quirk: literals in printf args require an attached target. -- gdb 12.1 quirk: literals in printf args require an attached target.
-- Use the per-atom convenience vars set above as printf args. -- Use the per-atom convenience vars set above as printf args.
lines[#lines + 1] = string.format(' printf " %%-32s @ 0x%%08x %%4d words\\n", $__atom_name_%d, $__atom_addr_%d, $__atom_words_%d', lines[#lines + 1] = string.format(' printf " code_%%-32s @ 0x%%08x %%4d words\\n", $__atom_name_%d, $__atom_addr_%d, $__atom_words_%d',
a.idx, a.idx, a.idx) a.idx, a.idx, a.idx)
end end
lines[#lines + 1] = "end" lines[#lines + 1] = "end"
lines[#lines + 1] = "document tape_atoms" lines[#lines + 1] = "document tape_atoms"
lines[#lines + 1] = " List every tape atom symbol in the loaded ELF with .rodata addr + word count." lines[#lines + 1] = " List every tape atom symbol in the loaded ELF (code_<name>) with .rodata addr + word count."
lines[#lines + 1] = "end" lines[#lines + 1] = "end"
lines[#lines + 1] = "" lines[#lines + 1] = ""
-- ── break_atom (generic) + per-atom break_atom_X ── -- ── break_atom (generic) + per-atom break_atom_X ──
lines[#lines + 1] = "define break_atom" lines[#lines + 1] = "define break_atom"
lines[#lines + 1] = ' echo "Usage: break_atom_<exact_name> (pick from the list below)"' lines[#lines + 1] = ' echo "Usage: break_atom_<exact_name> (pick from the list below)"'
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord for _, a in ipairs(matched) do
lines[#lines + 1] = string.format(' printf " break_atom_%%-32s\\n", $__atom_name_%d', a.idx) lines[#lines + 1] = string.format(' printf " break_atom_%%-32s\\n", $__atom_name_%d', a.idx)
end end
lines[#lines + 1] = "end" lines[#lines + 1] = "end"
@@ -327,13 +281,13 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = "end" lines[#lines + 1] = "end"
lines[#lines + 1] = "" lines[#lines + 1] = ""
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord for _, a in ipairs(matched) do
lines[#lines + 1] = string.format("define break_atom_%s", a.name) lines[#lines + 1] = string.format("define break_atom_%s", a.name)
lines[#lines + 1] = string.format(" break *$__atom_addr_%d", a.idx) lines[#lines + 1] = string.format(" break *$__atom_addr_%d", a.idx)
lines[#lines + 1] = string.format(' printf " Breakpoint set at %s (0x%%08x)\\n", $__atom_addr_%d', a.name, a.idx) lines[#lines + 1] = string.format(' printf " Breakpoint set at code_%s (0x%%08x)\\n", $__atom_addr_%d', a.name, a.idx)
lines[#lines + 1] = "end" lines[#lines + 1] = "end"
lines[#lines + 1] = string.format("document break_atom_%s", a.name) lines[#lines + 1] = string.format("document break_atom_%s", a.name)
lines[#lines + 1] = string.format(" Set a breakpoint at %s.", a.name) lines[#lines + 1] = string.format(" Set a breakpoint at code_%s.", a.name)
lines[#lines + 1] = "end" lines[#lines + 1] = "end"
lines[#lines + 1] = "" lines[#lines + 1] = ""
end end
@@ -341,7 +295,7 @@ local function append_gdb_commands(lines, matched)
-- ── step_atom / next_atom ── -- ── step_atom / next_atom ──
-- Hardcoded one tbreak per atom. No loop. -- Hardcoded one tbreak per atom. No loop.
lines[#lines + 1] = "define step_atom" lines[#lines + 1] = "define step_atom"
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord for _, a in ipairs(matched) do
lines[#lines + 1] = string.format(" tbreak *$__atom_addr_%d", a.idx) lines[#lines + 1] = string.format(" tbreak *$__atom_addr_%d", a.idx)
end end
lines[#lines + 1] = " continue" lines[#lines + 1] = " continue"
@@ -364,24 +318,24 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = "define where_in_atom" lines[#lines + 1] = "define where_in_atom"
lines[#lines + 1] = " set $__pc = (unsigned int)$pc" lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
lines[#lines + 1] = " set $__matched = 0" lines[#lines + 1] = " set $__matched = 0"
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord for _, a in ipairs(matched) do
-- Precompute end_addr (gdb 12.1's expression evaluator chokes on `addr + words*4`). -- Precompute end_addr (gdb 12.1's expression evaluator chokes on `addr + words*4`).
lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx) lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx)
lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx) lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
lines[#lines + 1] = string.format(' printf "atom: %%s\\n", $__atom_name_%d', a.idx) lines[#lines + 1] = string.format(' printf "atom: code_%%s\\n", $__atom_name_%d', a.idx)
lines[#lines + 1] = ' printf "addr: 0x%08x\\n", $__pc' lines[#lines + 1] = ' printf "addr: 0x%08x\\n", $__pc'
lines[#lines + 1] = string.format(" set $__word = ($__pc - $__atom_addr_%d) / 4", a.idx) lines[#lines + 1] = string.format(" set $__word = ($__pc - $__atom_addr_%d) / 4", a.idx)
lines[#lines + 1] = string.format(' printf "word: %%d/%%d\\n", $__word, $__atom_words_%d', a.idx) lines[#lines + 1] = string.format(' printf "word: %%d/%%d\\n", $__word, $__atom_words_%d', a.idx)
-- One inner-if per WORD entry. Each word's line + text hardcoded. -- One inner-if per WORD entry. Each word's line + text hardcoded.
for _, we in ipairs(a.entries) do ---@type integer, WordMapEntry for _, we in ipairs(a.entries) do
lines[#lines + 1] = string.format(" if $__word == %d", we.pos) lines[#lines + 1] = string.format(" if $__word == %d", we.pos)
-- Escape TEXT for printf format string. -- Escape TEXT for printf format string.
local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"') ---@type string local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"')
lines[#lines + 1] = string.format(' printf "source: %%s:%%d %%s\\n", $__atom_file_%d, %d, "%s"', a.idx, we.line, escaped_text) lines[#lines + 1] = string.format(' printf "source: %%s:%%d %%s\\n", $__atom_file_%d, %d, "%s"', a.idx, we.line, escaped_text)
lines[#lines + 1] = " end" lines[#lines + 1] = " end"
end end
-- Fallback for words beyond the source map (shouldn't happen if nm matches). -- Fallback for words beyond the source map (shouldn't happen if nm matches).
local max_word = 0 ---@type integer local max_word = 0
if #a.entries > 0 then max_word = a.entries[#a.entries].pos end if #a.entries > 0 then max_word = a.entries[#a.entries].pos end
lines[#lines + 1] = string.format(' if $__word > %d', max_word) lines[#lines + 1] = string.format(' if $__word > %d', max_word)
lines[#lines + 1] = ' printf "source: (no source-map entry for word %%d; map may be stale)\\n", $__word' lines[#lines + 1] = ' printf "source: (no source-map entry for word %%d; map may be stale)\\n", $__word'
@@ -406,7 +360,7 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = " set $__in_atom = 0" lines[#lines + 1] = " set $__in_atom = 0"
lines[#lines + 1] = " set $__did_step = 0" lines[#lines + 1] = " set $__did_step = 0"
lines[#lines + 1] = " set $__pc = (unsigned int)$pc" lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord for _, a in ipairs(matched) do
-- Precompute end_addr in the convenience var (single expression gdb handles). -- Precompute end_addr in the convenience var (single expression gdb handles).
lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx) lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx)
lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx) lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
@@ -440,10 +394,9 @@ end
--- Emit the gdb-runtime file (post-link). Pure gdb scripting — addresses come from `mipsel-none-elf-nm -S`, get embedded --- Emit the gdb-runtime file (post-link). Pure gdb scripting — addresses come from `mipsel-none-elf-nm -S`, get embedded
--- in `<ctx.out_root>/gdb_tape_atoms_runtime.gdb`, and load via `set $var = ...` + `define ... end` blocks at gdb source-time. --- in `<ctx.out_root>/gdb_tape_atoms_runtime.gdb`, and load via `set $var = ...` + `define ... end` blocks at gdb source-time.
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return nil
local function emit_gdb_runtime(ctx) local function emit_gdb_runtime(ctx)
if not (ctx.flags and ctx.flags.gdb_runtime) then return end if not (ctx.flags and ctx.flags.gdb_runtime) then return end
local elf_path = ctx.flags.elf_path ---@type string|nil local elf_path = ctx.flags.elf_path
if not elf_path or elf_path == "" then if not elf_path or elf_path == "" then
io.stderr:write("[atoms_source_map] --gdb-runtime requires --elf <elf>\n") io.stderr:write("[atoms_source_map] --gdb-runtime requires --elf <elf>\n")
return return
@@ -454,13 +407,13 @@ local function emit_gdb_runtime(ctx)
return return
end end
local matched = build_atom_table(ctx) ---@type GdbAtomRecord[] local matched = build_atom_table(ctx)
if #matched == 0 then if #matched == 0 then
io.stderr:write("[atoms_source_map] --gdb-runtime: no atoms matched against nm symbols (stale scan?).\n") io.stderr:write("[atoms_source_map] --gdb-runtime: no atoms matched against nm symbols (stale scan?).\n")
return return
end end
local lines = {} ---@type string[] local lines = {}
lines[#lines + 1] = "# Auto-generated by ps1_meta.lua (passes/atoms_source_map.lua)" lines[#lines + 1] = "# Auto-generated by ps1_meta.lua (passes/atoms_source_map.lua)"
lines[#lines + 1] = "# DO NOT EDIT — re-run ps1_meta.lua --atoms-source-map --gdb-runtime to regenerate" lines[#lines + 1] = "# DO NOT EDIT — re-run ps1_meta.lua --atoms-source-map --gdb-runtime to regenerate"
lines[#lines + 1] = "# Sourced by scripts/gdb/gdb_tape_atoms.gdb (the wrapper)." lines[#lines + 1] = "# Sourced by scripts/gdb/gdb_tape_atoms.gdb (the wrapper)."
@@ -481,7 +434,7 @@ local function emit_gdb_runtime(ctx)
-- Per-atom convenience vars (used as printf args; literals aren't accepted -- Per-atom convenience vars (used as printf args; literals aren't accepted
-- without an attached target on gdb 12.1). -- without an attached target on gdb 12.1).
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord for _, a in ipairs(matched) do
lines[#lines + 1] = string.format('set $__atom_name_%d = "%s"', a.idx, gdb_escape(a.name)) lines[#lines + 1] = string.format('set $__atom_name_%d = "%s"', a.idx, gdb_escape(a.name))
lines[#lines + 1] = string.format("set $__atom_addr_%d = 0x%x", a.idx, a.addr) lines[#lines + 1] = string.format("set $__atom_addr_%d = 0x%x", a.idx, a.addr)
lines[#lines + 1] = string.format("set $__atom_words_%d = %d", a.idx, a.words) lines[#lines + 1] = string.format("set $__atom_words_%d = %d", a.idx, a.words)
@@ -497,12 +450,10 @@ local function emit_gdb_runtime(ctx)
-- Confirmation line for the source operator. -- Confirmation line for the source operator.
lines[#lines + 1] = 'printf "[gdb_tape_atoms] runtime loaded %d atoms from %s\\n", $__atom_count, $__elf_path' lines[#lines + 1] = 'printf "[gdb_tape_atoms] runtime loaded %d atoms from %s\\n", $__atom_count, $__elf_path'
local out_path ---@type string local out_path
-- Move out of `<out_root>/gdb_tape_atoms_runtime.gdb` to `<out_root>/../gdb_tape_atoms_runtime.gdb` when the conventional `<out_root>` is `<build>/gen` -- Move out of `<out_root>/gdb_tape_atoms_runtime.gdb` to `<out_root>/../gdb_tape_atoms_runtime.gdb` when the conventional `<out_root>` is `<build>/gen`
-- (any equivalent spelling — relative, absolute backslash, absolute forward-slash, trailing-separator variants). -- (any equivalent spelling — relative, absolute backslash, absolute forward-slash, trailing-separator variants).
-- This puts the gdb runtime alongside the ELF at `build/` rather than under the report subdir. -- This puts the gdb runtime alongside the ELF at `build/` rather than under the report subdir.
--- @param p string
--- @return boolean
local function ends_with_gen_dir(p) local function ends_with_gen_dir(p)
if type(p) ~= "string" then return false end if type(p) ~= "string" then return false end
return p:match("[/\\]gen[/\\]?$") ~= nil or p == "build/gen" or p == "build\\gen" return p:match("[/\\]gen[/\\]?$") ~= nil or p == "build/gen" or p == "build\\gen"
@@ -510,7 +461,7 @@ local function emit_gdb_runtime(ctx)
if ends_with_gen_dir(ctx.out_root) then if ends_with_gen_dir(ctx.out_root) then
-- Strip the trailing `/gen` segment, then write the runtime script under `build/`. -- Strip the trailing `/gen` segment, then write the runtime script under `build/`.
-- e.g. "C:/projects/Pikuma/ps1/build/gen" -> "C:/projects/Pikuma/ps1/build". -- e.g. "C:/projects/Pikuma/ps1/build/gen" -> "C:/projects/Pikuma/ps1/build".
local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "") ---@type string local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "")
out_path = parent .. "/gdb_tape_atoms_runtime.gdb" out_path = parent .. "/gdb_tape_atoms_runtime.gdb"
else else
out_path = ctx.out_root .. "/gdb_tape_atoms_runtime.gdb" out_path = ctx.out_root .. "/gdb_tape_atoms_runtime.gdb"
@@ -524,35 +475,24 @@ end
-- M — module exports -- M — module exports
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
local M = {} ---@type AtomSourceMapPass local M = {}
-- Expose the pure render functions so `report.lua` and the focused tests can call them directly without triggering the file-emit path. -- Expose the pure render functions so `report.lua` and the focused tests can call them directly without triggering the file-emit path.
M.render_source_map = render_source_map M.render_source_map = render_source_map
M.render_provenance = render_provenance M.render_provenance = render_provenance
--- Render ONE atom's sourcemap stanza. --- Render ONE atom's sourcemap stanza.
--- @param atom AtomEntry --- @param atom table -- atom record (must have `atom.paths` populated)
--- @return string --- @return string
function M.render_atom_source_map(atom) function M.render_atom_source_map(atom)
assert(type(atom) == "table", "render_atom_source_map: atom must be a table") assert(type(atom) == "table", "render_atom_source_map: atom must be a table")
assert(type(atom.paths) == "table", "render_atom_source_map: atom.paths must be a table") assert(type(atom.paths) == "table", "render_atom_source_map: atom.paths must be a table")
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer local entries, total = canonical_word_entries(atom)
local lines = {} ---@type string[] local lines = {}
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total) lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total)
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry for _, entry in ipairs(entries) do
local word_line = string.format("WORD %d LINE %d TEXT %s", ---@type string lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s",
entry.pos, entry.line, entry.text) entry.pos, entry.line, entry.text)
local keys = {} ---@type string[]
for pos = 1, 16 do ---@type integer
local k = entry.gpr_keys and entry.gpr_keys[pos] ---@type string|nil
if type(k) == "string" and k:sub(1, 7) == "reguse:" then
keys[#keys + 1] = k
end
end
if #keys > 0 then
word_line = word_line .. " KEYS " .. table.concat(keys, ",")
end
lines[#lines + 1] = word_line
end end
lines[#lines + 1] = "ENDATOM" lines[#lines + 1] = "ENDATOM"
return table.concat(lines, "\n") .. "\n" return table.concat(lines, "\n") .. "\n"
@@ -562,23 +502,25 @@ end
--- ---
--- `rel_path` is the source path (forward-slashes) embedded in every `CALL` line. --- `rel_path` is the source path (forward-slashes) embedded in every `CALL` line.
--- The .md caller (report.lua) is expected to derive this once per `## <source>` heading and pass it down for each atom in that source. --- The .md caller (report.lua) is expected to derive this once per `## <source>` heading and pass it down for each atom in that source.
--- @param atom AtomEntry --- @param atom table -- atom record (must have `atom.paths` populated)
--- @param wc WordCounts --- @param wc table -- identity alias of `corpus.word_counts`
--- @param rel_path string --- @param rel_path string -- source path (forward-slashes) for `CALL` fields
--- @return string --- @return string
function M.render_atom_provenance(atom, wc, rel_path) function M.render_atom_provenance(atom, wc, rel_path)
assert(type(atom) == "table", "render_atom_provenance: atom must be a table") assert(type(atom) == "table", "render_atom_provenance: atom must be a table")
assert(type(atom.paths) == "table", "render_atom_provenance: atom.paths must be a table") assert(type(atom.paths) == "table", "render_atom_provenance: atom.paths must be a table")
assert(type(rel_path) == "string", "render_atom_provenance: rel_path must be a string") assert(type(rel_path) == "string", "render_atom_provenance: rel_path must be a string")
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer local entries, total = canonical_word_entries(atom)
local lines = {} ---@type string[] local lines = {}
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total) lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total)
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry for _, entry in ipairs(entries) do
local inv = entry.invocation ---@type InvocationRecord|nil local inv = entry.invocation
local macro_count = inv and wc and wc["mac_" .. inv.component_name] ---@type integer|nil local macro_count = inv and wc and wc["mac_" .. inv.component_name]
if inv and macro_count ~= nil then if inv and macro_count ~= nil then
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d' lines[#lines + 1] = string.format(
, entry.pos, rel_path, entry.line, inv.component_name, inv.def_path or "", inv.def_line or 0, entry.body_line) 'WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d',
entry.pos, rel_path, entry.line, inv.component_name,
inv.def_path or "", inv.def_line or 0, entry.body_line)
else else
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format(
"WORD %d CALL %s:%d RAW", entry.pos, rel_path, entry.line) "WORD %d CALL %s:%d RAW", entry.pos, rel_path, entry.line)
@@ -587,24 +529,24 @@ function M.render_atom_provenance(atom, wc, rel_path)
return table.concat(lines, "\n") .. "\n" return table.concat(lines, "\n") .. "\n"
end end
--- Pass entry. For each source that declares at least one tape atom, --- Pass entry. For each source that declares at least one `MipsAtom_(name)` / `MipsCode code_<name>`,
--- emit two files in `<out_root>/`: `<basename>.atoms.sourcemap.txt` (per-word call-site map) and `<basename>.atoms.provenance.txt` --- emit two files in `<out_root>/`: `<basename>.atoms.sourcemap.txt` (per-word call-site map) and `<basename>.atoms.provenance.txt`
--- (per-word definition + body line, resolved via the outermost `mac_X(...)` invocation). --- (per-word definition + body line, resolved via the outermost `mac_X(...)` invocation).
--- When `ctx.flags.gdb_runtime` is true and `ctx.flags.elf_path` exists, also emit the post-link gdb script `<ctx.out_root>/gdb_tape_atoms_runtime.gdb`. --- When `ctx.flags.gdb_runtime` is true and `ctx.flags.elf_path` exists, also emit the post-link gdb script `<ctx.out_root>/gdb_tape_atoms_runtime.gdb`.
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
local outputs = {} ---@type PassOutputEntry[] local outputs = {}
local errors = {} ---@type Finding[] local errors = {}
local warnings = {} ---@type Finding[] local warnings = {}
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil local corpus = ctx.shared and ctx.shared.corpus
if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then
error("atoms_source_map.run requires ctx.shared.corpus.source_order (canonical corpus).", 0) error("atoms_source_map.run requires ctx.shared.corpus.source_order (canonical corpus).", 0)
end end
-- Word counts come from `corpus.word_counts` (populated by word_count_eval + components passes). -- Word counts come from `corpus.word_counts` (populated by word_count_eval + components passes).
local wc = corpus.word_counts or {} ---@type WordCounts local wc = corpus.word_counts or {}
if not next(wc) then if not next(wc) then
warnings[#warnings + 1] = { warnings[#warnings + 1] = {
line = 0, line = 0,
-366
View File
@@ -1,366 +0,0 @@
--- passes/auto_reg.lua — Per-phase automatic GPR allocator + gen/auto_reg.h emitter.
---
--- Reads the per-source + corpus-level `atom_auto_regs` + `phase_auto_regs` registries populated by `passes/scan_source.lua`.
--- Runs a deterministic first-fit allocator in the `R_T0..R_T7 + R_V0..R_V1` pool (10 physical GPRs).
--- Emits one `#define R_<Sym>_Code R_Tn_Code` per marker into per-directory `gen/auto_reg.h`.
---
--- User-pinned GPRs : The corpus's `register_alias_registry` is consulted to exclude GPRs the user has pinned via
--- `atom_reg` + `_Code` defs (e.g. carriers like `R_ResolveScratch = R_T4 atom_reg`).
--- These GPRs are unavailable to EVERY atom's source pool.
--- Carriers are preserved across atoms by context discipline and must never be reallocated.
--- Per-atom body parsing also catches alias references (R_<Alias>) and hardcoded R_Tn references,
--- so the user can write either `R_T4` or `R_ResolveScratch` in an atom body and the pass will exclude R_T4 from that atom's pool.
---
--- Conflict detection: If the user hardcodes `R_Tn` in an atom body that shares a phase with an auto-reg that picked `R_Tn`,
--- emit `phase_register_clash` as an info finding (no build stop).
--- Should be unreachable after the user-pinning + body-parsing fix above; kept as a defensive safety net.
---
--- Pool exhaustion: If a phase declares more `R_<Sym>` mappings than the 10-register pool can hold,
--- emit `phase_register_pool_exhausted` as a build-stopping error.
--- @alias GprIdent string
--- @class GprAllocMap
--- @field [string] GprIdent -- bag: auto-reg symbol -> physical GPR
--- @class AutoRegOutput
--- @field auto_reg_h string
--- @class AutoRegResult
--- @field outputs AutoRegOutput[]
--- @field errors Finding[]
--- @field warnings Finding[]
--- @class AutoRegPass
--- @field run fun(ctx: PassCtx): AutoRegResult
--- @field POOL GprIdent[]
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
local isa = require("duffle_isa") ---@type DuffleIsa
--- ════════════════════════════════════════════════════════════════════════════
--- THE GPR ALLOCATION POOL — what's allocatable, and (more importantly) WHY
--- ════════════════════════════════════════════════════════════════════════════
---
--- The auto-reg pass picks physical GPRs for `atom_auto_reg(...)` / `phase_auto_reg(...)` markers.
--- The 24-register pool covers R2-R25 (the user/atom allocatable surface):
--- R_T0..R_T7, R_V0..R_V1, R_A0..A3, R_S0..S7, R_T8..T9.
--- Excluded (and never added to the pool):
--- R_0 (code 0) — Hardwired zero. Cannot be written.
--- R_AT (code 1) — Assembler temporary. Reserved by the MIPS O32 ABI.
--- R_A0..A3 — Explicitly omitted above even though their integer codes map to POOL entries;
--- the pool-construction loop below only references the POOL string literals, never the integer codes, so they are NOT auto-allocated by default.
--- (A0-A3 become available when the user adds them to POOL or hardcodes an R_A0 reference in the atom body.)
--- R_K0/K1 (codes 26-27) — Kernel / interrupt handler reserves. Never touched by user code.
--- R_GP/SP/FP/RA (codes 28-31) — R_SP/R_FP/R_RA are tape-runtime carriers between tape_enter and tape_exit; R_GP stays the host global pointer.
---
local POOL = {} ---@type GprIdent[]
for _, row in ipairs(isa.GPR_ROLE) do ---@type integer, GprRole
if row.pool then
POOL[#POOL + 1] = row.name
end
end
-- Map from integer MIPS GPR code (the `code` field on AliasEntry) to the physical GPR ident in POOL.
-- The standard MIPS O32 ABI register numbering matches mips.h's R_*_Code #defines (mips.h).
-- Only the POOL entries matter for auto_reg — non-pool aliases
-- (R_AT=1, R_A0..A3=4..7, R_T8=24, R_T9=25, R_K0/K1=26..27, R_GP/SP/FP/RA=28..31)
-- are deliberately omitted — see the comment block above for the WHY of each exclusion.
local INT_CODE_TO_POOL_GPR = { ---@type table<integer, GprIdent> -- bag: MIPS GPR code -> POOL ident
[2] = "R_V0", [3] = "R_V1",
[4] = "R_A0", [5] = "R_A1", [6] = "R_A2", [7] = "R_A3",
[8] = "R_T0", [9] = "R_T1", [10] = "R_T2", [11] = "R_T3",
[12] = "R_T4", [13] = "R_T5", [14] = "R_T6", [15] = "R_T7",
[16] = "R_S0", [17] = "R_S1", [18] = "R_S2", [19] = "R_S3",
[20] = "R_S4", [21] = "R_S5", [22] = "R_S6", [23] = "R_S7",
[24] = "R_T8", [25] = "R_T9",
}
-- Stable sort for deterministic allocation order.
--- @param tbl table<string, string> -- bag: key set only; values unused
--- @return string[]
local function stable_sort_keys(tbl)
local keys = {} ---@type string[]
for k in pairs(tbl) do keys[#keys + 1] = k end ---@type string
table.sort(keys)
return keys
end
-- Allocate one phase's auto-reg mappings.
-- Returns (allocated_map, errors). On pool exhaustion, errors is populated and the function halts.
--- @param phase_label string
--- @param decls table<string, string> -- bag: auto-reg symbol -> decl payload
--- @return GprAllocMap
--- @return Finding[]
local function allocate_phase(phase_label, decls)
-- Deep-copy POOL into a fresh sequence table. The original `table.unpack and table.unpack(POOL) or { unpack(POOL) }`
-- idiom wraps the unpacked values in a single inner table under LuaJIT 5.1 (`table.unpack` is nil; the `or` returns one value),
-- which corrupts the pool into `{ {R_T0, R_T1, ...} }` — making `table.remove(pool, 1)` return the inner table on iteration.
local pool = {} ---@type GprIdent[]
for i = 1, #POOL do pool[i] = POOL[i] end ---@type integer
local result = {} ---@type GprAllocMap
local errors = {} ---@type Finding[]
for _, sym in ipairs(stable_sort_keys(decls)) do ---@type integer, string
local next_gpr = table.remove(pool, 1) ---@type GprIdent|nil
if not next_gpr then
errors[#errors + 1] = {
line = 0,
msg = string.format("phase_register_pool_exhausted: "
.. "phase '%s' requested symbol '%s' but the pool has no remaining registers "
.. "(max 24 per phase: R_T0..R_T7 + R_V0..R_V1 + R_A0..R_A3 + R_S0..R_S7 + R_T8..R_T9). Split the phase or use hardcoded GPRs."
, phase_label, sym),
}
return result, errors
end
result[sym] = next_gpr
end
return result, errors
end
-- Build two projections from corpus.register_alias_registry:
-- user_pinned -- { [physical_gpr_ident] = true } -- GPRs unavailable to auto_reg globally (wave-context carriers, file-scope pinned aliases)
-- alias_to_gpr -- { [alias_ident] = physical_gpr_ident } -- for body parsing
-- Both projections are derived from the same set of entries: every AliasEntry in register_alias_registry has `has_atom_reg = true`
-- (only those entries are added to the registry; see passes/scan_source.lua parse_enum_entry).
-- Each entry's `code` is the integer MIPS GPR number (0..31); INT_CODE_TO_POOL_GPR translates it back to the physical GPR ident.
-- Aliases whose `code` points to a non-POOL GPR (e.g. R_S0, R_T8, R_K1) are ignored —
-- they don't affect the auto_reg pool, and they're already excluded from POOL above.
--- @param corpus Corpus
--- @return table<GprIdent, boolean>
--- @return table<string, GprIdent>
local function build_user_pins(corpus)
local user_pinned = {} ---@type table<GprIdent, boolean> -- bag: pinned physical GPR -> true
local alias_to_gpr = {} ---@type table<string, GprIdent> -- bag: alias ident -> physical GPR
if not corpus.register_alias_registry then return user_pinned, alias_to_gpr end
for alias_name, alias_entry in pairs(corpus.register_alias_registry) do ---@type string, AliasEntry
if alias_entry.has_atom_reg and alias_entry.code then
local gpr = INT_CODE_TO_POOL_GPR[alias_entry.code] ---@type GprIdent|nil
if gpr then
user_pinned[gpr] = true
alias_to_gpr[alias_name] = gpr
end
end
end
return user_pinned, alias_to_gpr
end
--- Find every physical GPR referenced in the atom body, via EITHER:
--- (a) A hardcoded physical GPR ident (R_T\d+|R_V\d+|R_A\d+|R_S\d+) — the existing regex;
--- (b) An alias ident (R_<Alias>) resolved via alias_to_gpr back to its physical GPR ident.
--- Returns { [physical_gpr_ident] = count }.
--- Clash-detection and source-pool-exclusion logic only needs the presence of each GPR (boolean test),
--- but keeping count preserves the original find_hardcoded_rn shape so callers can switch without churn.
--- The alias pattern is sorted lexicographically to keep the regex deterministic.
--- @param body_text string
--- @param alias_to_gpr table<string, GprIdent> -- bag: alias ident -> physical GPR
--- @return table<GprIdent, integer>
local function find_used_gprs(body_text, alias_to_gpr)
local found = {} ---@type table<GprIdent, integer> -- bag: physical GPR -> hit count
-- (a) Hardcoded physical GPRs (R_T0..R_T7, R_V0..R_V1, R_A0..R_A3, R_S0..R_S7).
for gpr in body_text:gmatch("(R_T%d+|R_V%d+|R_A%d+|R_S%d+)") do ---@type GprIdent
found[gpr] = (found[gpr] or 0) + 1
end
-- (b) Alias references (R_<Alias>) resolved to physical GPRs via the registry.
-- Sorted by name so the regex is byte-stable across runs.
if alias_to_gpr and next(alias_to_gpr) then
local aliases = {} ---@type string[]
for alias_name in pairs(alias_to_gpr) do ---@type string
aliases[#aliases + 1] = alias_name
end
table.sort(aliases)
local pattern = "(" .. table.concat(aliases, "|") .. ")" ---@type string
for alias_name in body_text:gmatch(pattern) do ---@type string
local gpr = alias_to_gpr[alias_name] ---@type GprIdent|nil
if gpr and not found[gpr] then
found[gpr] = 1
end
end
end
return found
end
-- Emit one gen/auto_reg.h header per directory.
--- @param out_dir string
--- @param dir string
--- @param sources SourceFile[]
--- @param mappings GprAllocMap
--- @return string|nil
local function emit_auto_reg_h(out_dir, dir, sources, mappings)
if not mappings or next(mappings) == nil then return end
local out_path = out_dir .. "/" .. "auto_reg.h" ---@type string
duffle.ensure_dir(out_dir)
local lines = { ---@type string[]
"#ifdef INTELLISENSE_DIRECTIVES",
"#pragma once",
"#endif",
"// Auto-generated by ps1_meta.lua (passes/auto_reg.lua) — DO NOT EDIT",
"// Directory: " .. dir:gsub("/", "\\"),
}
for _, src in ipairs(sources) do ---@type integer, SourceFile
lines[#lines + 1] = "// source: " .. src.path
end
lines[#lines + 1] = "// Per-phase register allocations resolved by the lua pass."
lines[#lines + 1] = "// R_<Sym>_Code = <chosen GPR's _Code constant> for every marker in this directory."
lines[#lines + 1] = ""
for _, sym in ipairs(stable_sort_keys(mappings)) do ---@type integer, string
local gpr = mappings[sym] ---@type GprIdent
local gpr_code = gpr .. "_Code" ---@type string
lines[#lines + 1] = "#define " .. sym .. "_Code " .. gpr_code
end
lines[#lines + 1] = ""
duffle.write_file_lf(out_path, table.concat(lines, "\n") .. "\n")
print(" -> " .. out_path)
return out_path
end
-- ════════════════════════════════════════════════════════════════════════════
-- Pass entry
-- ════════════════════════════════════════════════════════════════════════════
local M = {} ---@type AutoRegPass
--- @param ctx PassCtx
--- @return AutoRegResult
function M.run(ctx)
local outputs = {} ---@type AutoRegOutput[]
local errors = {} ---@type Finding[]
local warnings = {} ---@type Finding[]
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then
error("auto_reg.run requires ctx.shared.corpus", 0)
end
-- 0. Build the user-pinned GPR exclusion set + alias-to-GPR resolution map.
-- Wave-context carriers (e.g. `R_ResolveScratch = R_T4 atom_reg` in hello_camera.atom.c)
-- MUST NOT be allocated to any auto-reg marker — they're preserved across atoms by the wave-context discipline.
-- The corpus's register_alias_registry is the source of truth for these opt-in pins.
-- Body references to those aliases (via alias_to_gpr) are also excluded on a per-atom basis in step 2 below.
local user_pinned, alias_to_gpr = build_user_pins(corpus) ---@type table<GprIdent, boolean>, table<string, GprIdent>
-- 1. Allocate phase pools first (phase declarations take precedence over per-atom declarations).
local phase_allocations = {} ---@type table<string, GprAllocMap> -- bag: phase_label -> alloc map
for phase_label, decls in pairs(corpus.phase_auto_regs or {}) do ---@type string, table<string, string>
local mapping, errs = allocate_phase(phase_label, decls) ---@type GprAllocMap, Finding[]
for sym, gpr in pairs(mapping) do ---@type string, GprIdent
phase_allocations[phase_label] = phase_allocations[phase_label] or {}
phase_allocations[phase_label][sym] = gpr
end
for _, e in ipairs(errs) do ---@type integer, Finding
errors[#errors + 1] = e
end
end
-- 2. Allocate per-atom auto-regs. If the atom scope matches a phase, reuse the phase pool.
-- Otherwise, allocate a private pool for the atom.
-- The phase membership is in `corpus.atom_phases[phase_label].atoms` (an array of atom names declared via `atom_phase(<phase>)`
-- in the atom's `atom_info` line). Build a reverse map `atom_name -> phase_label` so the lookup is O(1) per atom scope.
local atom_name_to_phase = {} ---@type table<AtomName, string> -- bag: atom name -> phase label
for phase_label, entry in pairs(corpus.atom_phases or {}) do ---@type string, AtomPhaseGroup
for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, AtomName
atom_name_to_phase[atom_name] = phase_label
end
end
local atom_allocations = {} ---@type table<AtomName, GprAllocMap> -- bag: atom scope -> alloc map
for atom_scope, decls in pairs(corpus.atom_auto_regs or {}) do ---@type AtomName, table<string, string>
local phase_label = atom_name_to_phase[atom_scope] ---@type string|nil
-- Build the atom's source pool: start with the full POOL, subtract:
-- (a) every GPR already committed (phase allocations + prior atom allocations)
-- (b) every USER-PINNED GPR (wave-context carriers + file-scope pinned aliases)
-- (c) every GPR referenced in the atom's body — either hardcoded R_X or alias R_Xxx
-- (the latter resolved via alias_to_gpr; this catches cases where the user wrote R_ResolveScratch instead of R_T4 directly)
-- Atoms whose scope matches a phase share the global pool with the phase allocations;
-- the original `source_pool = phase_allocations[phase_label]` form used the phase
-- allocation MAP as a pool, but that map has no array part, so `table.remove(source_pool, 1)`
-- returned nil and every atom-with-phase marker errored with `phase_register_pool_exhausted`.
local used = {} ---@type table<GprIdent, boolean> -- bag: committed or body-referenced GPR -> true
for _, m in pairs(phase_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end ---@type integer, GprAllocMap
for _, m in pairs(atom_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end ---@type integer, GprAllocMap
-- (c) Body references — scan the atom body for hardcoded + alias-resolved GPRs.
-- Folded into `used` so the source_pool exclusion is a single check.
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] ---@type AtomEntry|nil
if atom and atom.body then
local body_used = find_used_gprs(atom.body, alias_to_gpr) ---@type table<GprIdent, integer>
for gpr in pairs(body_used) do used[gpr] = true end ---@type GprIdent
end
local source_pool = {} ---@type GprIdent[]
for _, gpr in ipairs(POOL) do ---@type integer, GprIdent
-- Exclude (a) prior commitments, (b) USER-PINNED GPRs (wave-context carriers declared via atom_reg + _Code defs, preserved across atoms globally).
if not used[gpr] and not user_pinned[gpr] then
source_pool[#source_pool + 1] = gpr
end
end
local result = {} ---@type GprAllocMap
for _, sym in ipairs(stable_sort_keys(decls)) do ---@type integer, string
local next_gpr = table.remove(source_pool, 1) ---@type GprIdent|nil
if not next_gpr then
errors[#errors + 1] = {
line = 0,
msg = string.format("phase_register_pool_exhausted: atom '%s' requested symbol '%s' "
.. "but no free registers remain in its scope pool."
, atom_scope, sym),
}
else
result[sym] = next_gpr
end
end
atom_allocations[atom_scope] = result
end
-- 3. Conflict-with-hardcoded detection (defensive — should be unreachable now).
-- The source_pool exclusion in step 2 (b) + (c) already accounts for both user-pinned GPRs
-- and body-referenced GPRs (hardcoded R_Tn OR alias R_<Alias>).
-- An auto-reg allocation that matched an existing body reference would be impossible by construction.
-- This warning is kept as a defensive safety net for cases the body scanner might miss
-- (e.g. macros that expand to register references the scanner cannot resolve).
-- For each resolved (scope, sym) -> R_Tn mapping, scan the atom body source for used GPRs.
for atom_scope, decls in pairs(atom_allocations) do ---@type AtomName, GprAllocMap
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] ---@type AtomEntry|nil
if atom and atom.body then
local used_in_body = find_used_gprs(atom.body, alias_to_gpr) ---@type table<GprIdent, integer>
for sym, allocated_gpr in pairs(decls) do ---@type string, GprIdent
if used_in_body[allocated_gpr] and used_in_body[allocated_gpr] > 0 then
warnings[#warnings + 1] = {
line = atom.line or 0,
msg = string.format("phase_register_clash: atom '%s' has hardcoded '%s' in its body AND an auto-reg marker '%s' "
.. "that was allocated to '%s' (same phase). Resolve by removing the hardcoded reference or renaming the auto-reg."
, atom_scope, allocated_gpr, sym, allocated_gpr),
}
end
end
end
end
-- 4. Emit per-directory gen/auto_reg.h.
-- For each source directory that has atom_auto_regs or phase_auto_regs entries, emit one header.
local sources_by_dir = corpus.sources_by_dir or {} ---@type table<string, SourceFile[]>
for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
local per_dir_mappings = {} ---@type GprAllocMap
for _, src in ipairs(sources) do ---@type integer, SourceFile
-- Collect every (sym -> gpr) entry that originated from a source in this directory.
-- `src.scan.atom_auto_regs` is keyed by ATOM SCOPE NAME; `pairs(t)` iterates KEYS so `scope_name` here is the scope ident (e.g. "cube_g4_face").
-- The previous `for _, scan_atom_auto` form silently assigned the VALUE (a `{sym = sym}` table) to the variable,
-- which made `atom_allocations[scan_atom_auto]` a table-indexed lookup that never resolved.
for scope_name in pairs(src.scan and src.scan.atom_auto_regs or {}) do ---@type string
for sym, gpr in pairs(atom_allocations[scope_name] or {}) do ---@type string, GprIdent
per_dir_mappings[sym] = gpr
end
end
for scope_name in pairs(src.scan and src.scan.phase_auto_regs or {}) do ---@type string
for sym, gpr in pairs(phase_allocations[scope_name] or {}) do ---@type string, GprIdent
per_dir_mappings[sym] = gpr
end
end
end
local out_dir = dir .. "/gen" ---@type string
local out_path = emit_auto_reg_h(out_dir, dir, sources, per_dir_mappings) ---@type string|nil
if out_path then outputs[#outputs + 1] = { auto_reg_h = out_path } end
end
return { outputs = outputs, errors = errors, warnings = warnings }
end
M.POOL = POOL
return M
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+54 -183
View File
@@ -1,13 +1,13 @@
--- passes/emission_model.lua: Per-atom emission projection. --- passes/emission_model.lua: Per-atom emission projection.
--- ---
--- The `emission-model` pass owns `atom.paths`, the canonical per-atom mutable surface for atoms and raw atoms with bodies in `ctx.shared.corpus.source_order`. --- The `emission-model` pass owns `atom.paths`, the canonical per-atom mutable surface for atoms and raw atoms with bodies in `ctx.shared.corpus.source_order`.
--- For each atom, the pass invokes `duffle.project_emission(body_text, components, word_counts, components)`. --- For each atom, the pass invokes `duffle.project_emission(body_text, component_index, word_counts, components)`.
--- It stores the ordered `items` stream plus the dense `word_events` / `markers` / `invocations` views on `atom.paths`. --- It stores the ordered `items` stream plus the dense `word_events` / `markers` / `invocations` views on `atom.paths`.
--- ---
--- Public boundary: --- Public boundary:
--- * `M.run(ctx)` is the only entry point. --- * `M.run(ctx)` is the only entry point.
--- * The pass returns `{outputs = {}, errors = ..., warnings = ...}`. --- * The pass returns `{outputs = {}, errors = ..., warnings = ...}`.
--- Pass kind = `validation`. Findings record on the result; the orchestrator does not exit non-zero. --- Pass kind = `validation` → `PASS_KIND_STOP_ON_ERROR.validation` preserves the existing build-stopping policy.
--- ---
--- Source-order discipline: --- Source-order discipline:
--- * `corpus.source_order` sets the source-record order. --- * `corpus.source_order` sets the source-record order.
@@ -26,94 +26,13 @@
--- ---
--- `passes.scan_source` strips its private `_code_macros` / `_code_macro_bodies` tables before this pass runs. --- `passes.scan_source` strips its private `_code_macros` / `_code_macro_bodies` tables before this pass runs.
--- @class BodyToken local M = {}
--- @field tok string
--- @field rel integer
--- @class EmissionItem
--- @field kind string
--- @field encoder string|nil
--- @field args string[]|nil
--- @field i integer|nil
--- @field word_count integer|nil
--- @field line integer|nil
--- @field call_text string|nil
--- @field root_call_text string|nil
--- @field invocation_ids integer[]|nil
--- @field outermost_invocation_id integer|nil
--- @field gpr_keys string[]|nil
--- @field ident string|nil
--- @field isa_kind string|nil
--- @field nop_words integer|nil
--- @field is_yield boolean|nil
--- @field is_load boolean|nil
--- @field is_branch boolean|nil
--- @field is_unconditional_jump boolean|nil
--- @field is_terminal_jump boolean|nil
--- @field gp0_shape string|nil
--- @field name string|nil
--- @field target string|nil
--- @field word_index integer|nil
--- @field consuming_encoder string|nil
--- @field consuming_arg_pos integer|nil
--- @field invocation_id integer|nil
--- @class WordEvent
--- @field i integer
--- @field encoder string
--- @field args string[]
--- @field def_path string
--- @field def_line integer
--- @field call_text string|nil
--- @field root_call_text string|nil
--- @field invocation_ids integer[]
--- @field outermost_invocation_id integer
--- @field word_count integer
--- @field gpr_keys string[]|nil
--- @field ident string
--- @field kind string
--- @field nop_words integer
--- @field is_yield boolean
--- @field is_load boolean
--- @field is_branch boolean
--- @field is_unconditional_jump boolean
--- @field is_terminal_jump boolean
--- @field gp0_shape string|nil
--- @field body_line integer|nil
--- @field call_line integer|nil
--- @field call_path string|nil
--- @class EmissionMarker
--- @field kind string
--- @field name string
--- @field line integer
--- @field word_index integer
--- @field target string|nil
--- @field consuming_encoder string|nil
--- @field consuming_arg_pos integer|nil
-- Finding: see ps1_meta.lua
--- @class AtomPaths
--- @field tokens BodyToken[]
--- @field line_in_body table<integer, integer> -- bag: body byte offset -> 1-based line
--- @field items EmissionItem[]
--- @field word_events WordEvent[]
--- @field markers EmissionMarker[]
--- @field invocations InvocationRecord[]
--- @field errors Finding[]
--- @field warnings Finding[]
--- @class EmissionModelPass
--- @field run fun(ctx: PassCtx): PassResult
local M = {} ---@type EmissionModelPass
-- ───────────────────────────────────────────────────────────────────────── -- ─────────────────────────────────────────────────────────────────────────
-- Bootstrap: load `duffle_paths.lua` via debug.getinfo so the module works standalone (run as `luajit passes/emission_model.lua`) and when require'd from the orchestrator. -- Bootstrap: load `duffle_paths.lua` via debug.getinfo so the module works standalone (run as `luajit passes/emission_model.lua`) and when require'd from the orchestrator.
-- ───────────────────────────────────────────────────────────────────────── -- ─────────────────────────────────────────────────────────────────────────
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- ───────────────────────────────────────────────────────────────────────── -- ─────────────────────────────────────────────────────────────────────────
-- Helpers -- Helpers
@@ -126,17 +45,12 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- * ROOT invocations (`inv.parent_id == 0`) receive body-relative `call_line` values directly from `M.LineIndex(body_text)` in the walker. -- * ROOT invocations (`inv.parent_id == 0`) receive body-relative `call_line` values directly from `M.LineIndex(body_text)` in the walker.
-- The source `line_of` closure supplies physical lines at the close site, so this function converts each root value exactly once. -- The source `line_of` closure supplies physical lines at the close site, so this function converts each root value exactly once.
-- * INNER invocations (`inv.parent_id ~= 0`) receive physical `call_line` values directly from the COMPONENT's `line_of` in the walker. -- * INNER invocations (`inv.parent_id ~= 0`) receive physical `call_line` values directly from the COMPONENT's `line_of` in the walker.
-- Recursive descent forwards that closure through `corpus.components[name].line_of`; those values arrive physical and remain unchanged. -- Recursive descent forwards that closure through `corpus.component_body_index[name].line_of`; those values arrive physical and remain unchanged.
-- --
-- After this function, every `inv.call_line` is physical. DWARF and provenance output read it directly. -- After this function, every `inv.call_line` is physical. DWARF and provenance output read it directly.
-- The word-event loop forwards the already-physical `outer_inv.call_line` into `we.call_line` for words inside an invocation. -- The word-event loop forwards the already-physical `outer_inv.call_line` into `we.call_line` for words inside an invocation.
--- @param projection EmissionProjection
--- @param atom_record AtomEntry
--- @param src SourceFile
--- @param corpus Corpus
--- @return nil
local function stamp_root_provenance(projection, atom_record, src, corpus) local function stamp_root_provenance(projection, atom_record, src, corpus)
local root_line_of = src.scan and src.scan.line_of ---@type (fun(pos: integer): integer)|nil local root_line_of = src.scan and src.scan.line_of
assert(type(root_line_of) == "function" assert(type(root_line_of) == "function"
, "emission_model: src.scan.line_of is required (canonical LineIndex closure over the source text) to stamp physical provenance") , "emission_model: src.scan.line_of is required (canonical LineIndex closure over the source text) to stamp physical provenance")
assert(type(atom_record.body_off) == "number" assert(type(atom_record.body_off) == "number"
@@ -144,29 +58,26 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- `root_body_line` is the physical source line of the ATOM HEADER byte containing the opening `{`; that byte is one byte BEFORE `atom_record.body_off`. -- `root_body_line` is the physical source line of the ATOM HEADER byte containing the opening `{`; that byte is one byte BEFORE `atom_record.body_off`.
-- The walker assigns line 2 to the body's first content line because line 1 is the trailing `\n` after `{`. Body-text line k therefore maps to `root_body_line + (k - 1)`. -- The walker assigns line 2 to the body's first content line because line 1 is the trailing `\n` after `{`. Body-text line k therefore maps to `root_body_line + (k - 1)`.
-- `body_off - 1` points at the opening `{`, whose line index identifies the header line. `body_off` points after `{` and would shift every word row forward by one line. -- `body_off - 1` points at the opening `{`, whose line index identifies the header line. `body_off` points after `{` and would shift every word row forward by one line.
local root_body_line = root_line_of(atom_record.body_off - 1) or atom_record.line or 0 ---@type integer local root_body_line = root_line_of(atom_record.body_off - 1) or atom_record.line or 0
local components = corpus.components or {} ---@type table<string, Component> local component_index = corpus.component_body_index or {}
local word_items = {} ---@type EmissionItem[] local word_items = {}
for _, item in ipairs(projection.items) do ---@type integer, EmissionItem for _, item in ipairs(projection.items) do
if item.kind == "word" then word_items[#word_items + 1] = item end if item.kind == "word" then word_items[#word_items + 1] = item end
end end
-- Resolve one word's physical body line, where the byte containing that word appears in source. -- Resolve one word's physical body line, where the byte containing that word appears in source.
-- * Component expansions carry `invocation_ids`; the component's full-file `line_of` leaves `item.line` physical. -- * Component expansions carry `invocation_ids`; the component's full-file `line_of` leaves `item.line` physical.
-- * Raw tokens in the root atom body carry an empty `invocation_ids` list and a body-relative `item.line`; convert them here. -- * Raw tokens in the root atom body carry an empty `invocation_ids` list and a body-relative `item.line`; convert them here.
--- @param event WordEvent
--- @param item EmissionItem
--- @return integer
local function body_line_for(event, item) local function body_line_for(event, item)
local ids = event.invocation_ids or {} ---@type integer[] local ids = event.invocation_ids or {}
-- The innermost open invocation identifies which line index the walker used. -- The innermost open invocation identifies which line index the walker used.
-- A component `line_of` makes `item.line` physical; the atom's `body_text` line index makes it body-relative. -- A component `line_of` makes `item.line` physical; the atom's `body_text` line index makes it body-relative.
if ids and #ids > 0 then if ids and #ids > 0 then
local inner_id = ids[#ids] ---@type integer local inner_id = ids[#ids]
local inner_inv = inner_id and projection.invocations[inner_id] ---@type InvocationRecord|nil local inner_inv = inner_id and projection.invocations[inner_id]
if inner_inv then if inner_inv then
local component = components[inner_inv.component_name] ---@type Component|nil local component = component_index[inner_inv.component_name]
if component and component.line_of then if component and component.line_of then
-- Walker used `comp.line_of`, which is the source's physical LineIndex. item.line is already physical. -- Walker used `comp.line_of`, which is the source's physical LineIndex. item.line is already physical.
return item.line or 0 return item.line or 0
@@ -181,8 +92,8 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Stamp the root source path onto invocation records whose `call_path` the walker left empty. -- Stamp the root source path onto invocation records whose `call_path` the walker left empty.
-- The walker passes `body_entry.source` to `emit_invoke_begin`; `M.project_emission` creates the root `body_entry` with source `""`, leaving its `call_path` empty. -- The walker passes `body_entry.source` to `emit_invoke_begin`; `M.project_emission` creates the root `body_entry` with source `""`, leaving its `call_path` empty.
-- This stamp gives every invocation a physical `call_path` matching `passes/atoms_source_map.lua`'s in-memory provenance projection. -- This stamp gives every invocation a physical `call_path` matching `passes/atoms_source_map.lua`'s in-memory provenance projection.
local root_path = src.path or "" ---@type string local root_path = src.path or ""
for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord for _, inv in ipairs(projection.invocations) do
if inv.call_path == nil or inv.call_path == "" then if inv.call_path == nil or inv.call_path == "" then
inv.call_path = root_path inv.call_path = root_path
end end
@@ -191,7 +102,7 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Normalize `inv.call_line` to a physical source line. -- Normalize `inv.call_line` to a physical source line.
-- * ROOT invocations (`parent_id == 0`) carry body-relative `call_line` values from `M.LineIndex(body_text)`; convert them once with `root_body_line`. -- * ROOT invocations (`parent_id == 0`) carry body-relative `call_line` values from `M.LineIndex(body_text)`; convert them once with `root_body_line`.
-- * INNER invocations (`parent_id ~= 0`) carry physical `call_line` values from the component's `line_of`; retain them unchanged. -- * INNER invocations (`parent_id ~= 0`) carry physical `call_line` values from the component's `line_of`; retain them unchanged.
for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord for _, inv in ipairs(projection.invocations) do
if inv.parent_id == 0 then if inv.parent_id == 0 then
inv.call_line = (root_body_line or 0) + (inv.call_line or 1) - 1 inv.call_line = (root_body_line or 0) + (inv.call_line or 1) - 1
end end
@@ -200,14 +111,14 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Build `body_lines` for each invocation. -- Build `body_lines` for each invocation.
-- `atoms_source_map` and `dwarf_injection` read `inv.body_lines[k]` directly from the invocation record created here. -- `atoms_source_map` and `dwarf_injection` read `inv.body_lines[k]` directly from the invocation record created here.
-- Component words already carry physical `item.line` values from the walker's COMPONENT line index, so `body_line_for` returns them unchanged. -- Component words already carry physical `item.line` values from the walker's COMPONENT line index, so `body_line_for` returns them unchanged.
for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord for _, inv in ipairs(projection.invocations) do
local sw = inv.start_word ---@type integer local sw = inv.start_word
local ew = inv.end_word ---@type integer local ew = inv.end_word
local bls = {} ---@type integer[] local bls = {}
for i = sw, ew do ---@type integer for i = sw, ew do
local it = projection.items and projection.items[i] ---@type EmissionItem|nil local it = projection.items and projection.items[i]
if it and it.kind == "word" then if it and it.kind == "word" then
local fake_event = { invocation_ids = { inv.id } } ---@type WordEvent local fake_event = { invocation_ids = { inv.id } }
bls[#bls + 1] = body_line_for(fake_event, it) or 0 bls[#bls + 1] = body_line_for(fake_event, it) or 0
end end
end end
@@ -217,15 +128,15 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Resolve each `word_event`'s physical `body_line` and `call_line`. -- Resolve each `word_event`'s physical `body_line` and `call_line`.
-- For words inside an invocation, `we.call_line` identifies the OUTER atom source line containing the `mac_X(...)` token that triggered expansion. -- For words inside an invocation, `we.call_line` identifies the OUTER atom source line containing the `mac_X(...)` token that triggered expansion.
-- The root-invocation conversion above makes every `inv.call_line` physical; forward it directly and use each raw word's `body_line` as the fallback. -- The root-invocation conversion above makes every `inv.call_line` physical; forward it directly and use each raw word's `body_line` as the fallback.
for index, we in ipairs(projection.word_events) do ---@type integer, WordEvent for index, we in ipairs(projection.word_events) do
local item = word_items[index] or {} ---@type EmissionItem local item = word_items[index] or {}
local body_line = body_line_for(we, item) ---@type integer local body_line = body_line_for(we, item)
item.line = body_line item.line = body_line
we.body_line = body_line we.body_line = body_line
local call_line = body_line ---@type integer local call_line = body_line
local outer_id = we.outermost_invocation_id or 0 ---@type integer local outer_id = we.outermost_invocation_id or 0
local outer_inv = projection.invocations[outer_id] ---@type InvocationRecord|nil local outer_inv = projection.invocations[outer_id]
if outer_inv then if outer_inv then
-- `outer_inv.call_line` is physical after the conversion loop above, so use it directly. -- `outer_inv.call_line` is physical after the conversion loop above, so use it directly.
call_line = outer_inv.call_line call_line = outer_inv.call_line
@@ -240,46 +151,13 @@ end
-- Project one atom record into `atom.paths`. -- Project one atom record into `atom.paths`.
-- Mutates the atom record in-place and returns the projection (for pass-level error/warning accumulation). -- Mutates the atom record in-place and returns the projection (for pass-level error/warning accumulation).
--- @param atom_record AtomEntry
--- @param src SourceFile
--- @param corpus Corpus
--- @return EmissionProjection
local function project_atom(atom_record, src, corpus) local function project_atom(atom_record, src, corpus)
local body = atom_record.body or "" ---@type string local body = atom_record.body or ""
local wc = corpus.word_counts or {} ---@type WordCounts local wc = corpus.word_counts or {}
local comps = corpus.components or {} ---@type table<string, Component> local cbi = corpus.component_body_index or {}
local schema = nil ---@type RegUseSchema|nil
if atom_record.reg_use_schema_name then
schema = corpus.reg_use_schemas and corpus.reg_use_schemas[atom_record.reg_use_schema_name]
end
-- That construction site stamps `invocation.debug_skip` while appending each record to `proj.invocations`. -- That construction site stamps `invocation.debug_skip` while appending each record to `proj.invocations`.
local proj = duffle.project_emission(body, comps, wc, comps, { ---@type EmissionProjection local proj = duffle.project_emission(body, cbi, wc, corpus.components)
reg_use_schema = schema, local paths = {
reg_use_param = atom_record.reg_use_param_name,
atom_name = atom_record.name,
schema_name = atom_record.reg_use_schema_name,
})
if atom_record.reg_use_schema_name and not schema then
proj.errors[#proj.errors + 1] = {
kind = "error",
check = "reguse_missing_schema",
msg = string.format("RegUse schema %q is missing", atom_record.reg_use_schema_name),
schema_name = atom_record.reg_use_schema_name,
}
end
for _, err in ipairs(corpus.reg_use_errors or {}) do ---@type integer, RegUseError
if err.schema_name == atom_record.reg_use_schema_name then
proj.errors[#proj.errors + 1] = {
kind = "error",
check = err.kind,
line = err.line or err.source_line or 0,
msg = err.msg or "",
source = err.source or err.source_file,
schema_name = err.schema_name,
}
end
end
local paths = { ---@type AtomPaths
tokens = atom_record.body_tokens or {}, tokens = atom_record.body_tokens or {},
line_in_body = duffle.build_body_line_index(body), line_in_body = duffle.build_body_line_index(body),
items = proj.items, items = proj.items,
@@ -301,42 +179,35 @@ end
--- @param ctx PassCtx -- { shared = { corpus = ... }, out_root, ... } --- @param ctx PassCtx -- { shared = { corpus = ... }, out_root, ... }
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
local outputs = {} ---@type PassOutputEntry[] local outputs = {}
local errors = {} ---@type Finding[] local errors = {}
local warnings = {} ---@type Finding[] local warnings = {}
local corpus = ctx and ctx.shared and ctx.shared.corpus ---@type Corpus|nil local corpus = ctx and ctx.shared and ctx.shared.corpus
if type(corpus) ~= "table" then error("emission_model: ctx.shared.corpus is required (canonical projection)", 0) end if type(corpus) ~= "table" then error("emission_model: ctx.shared.corpus is required (canonical projection)", 0) end
if type(corpus.source_order) ~= "table" then error("emission_model: ctx.shared.corpus.source_order is required", 0) end if type(corpus.source_order) ~= "table" then error("emission_model: ctx.shared.corpus.source_order is required", 0) end
-- Project once, collect errors + warnings for one atom. -- Project once, collect errors + warnings for one atom.
-- Kind must be one of: atom | atom_proc | raw_atom | comp_bare | comp_proc. -- Kind must be one of: atom | raw_atom | comp_bare | comp_proc.
--- @param atom AtomEntry
--- @param src SourceFile
--- @return nil
local function process_atom(atom, src) local function process_atom(atom, src)
if not (atom and atom.body) then return end if not (atom and atom.body) then return end
local kind = atom.kind ---@type string local kind = atom.kind
if kind ~= "atom" and kind ~= "atom_proc" and kind ~= "raw_atom" and kind ~= "comp_bare" and kind ~= "comp_proc" then if kind ~= "atom" and kind ~= "raw_atom" and kind ~= "comp_bare" and kind ~= "comp_proc" then
return return
end end
local proj = project_atom(atom, src, corpus) ---@type EmissionProjection local proj = project_atom(atom, src, corpus)
for _, e in ipairs(proj.errors) do ---@type integer, Finding for _, e in ipairs(proj.errors) do
-- Finding.kind is severity. Finding.check holds the diagnostic code -- Preserve `kind` (cycle / count_mismatch / unbalanced) so readers dispatch on the diagnostic class and leave the message string as display text.
-- (cycle / count_mismatch / unbalanced / reguse_*).
errors[#errors + 1] = { errors[#errors + 1] = {
kind = "error", kind = e.kind,
check = e.check,
line = e.line, line = e.line,
msg = e.msg, msg = e.msg,
source = e.source or src.path, source = e.source or src.path,
schema_name = e.schema_name,
} }
end end
for _, w in ipairs(proj.warnings) do ---@type integer, Finding for _, w in ipairs(proj.warnings) do
warnings[#warnings + 1] = { warnings[#warnings + 1] = {
kind = "warning", kind = w.kind,
check = w.check,
line = w.line, line = w.line,
msg = w.msg, msg = w.msg,
} }
@@ -344,14 +215,14 @@ function M.run(ctx)
end end
-- Walk `corpus.source_order`; within each source, visit atoms followed by raw_atoms. -- Walk `corpus.source_order`; within each source, visit atoms followed by raw_atoms.
-- Recognized kinds (atom | atom_proc | raw_atom | comp_bare | comp_proc) each receive the atom.paths projection via duffle.project_emission. -- Recognized kinds (atom | raw_atom | comp_bare | comp_proc) each receive the atom.paths projection via duffle.project_emission.
-- Components are macros inlined into atom bodies; focused tests and isolated component analyses consume atom.paths directly. -- Components are macros inlined into atom bodies; focused tests and isolated component analyses consume atom.paths directly.
for _, src in ipairs(corpus.source_order) do ---@type integer, SourceFile for _, src in ipairs(corpus.source_order) do
local scan = src.scan or {} ---@type SourceScan local scan = src.scan or {}
for _, atom in ipairs(scan.atoms or {}) do ---@type integer, AtomEntry for _, atom in ipairs(scan.atoms or {}) do
process_atom(atom, src) process_atom(atom, src)
end end
for _, atom in ipairs(scan.raw_atoms or {}) do ---@type integer, AtomEntry for _, atom in ipairs(scan.raw_atoms or {}) do
process_atom(atom, src) process_atom(atom, src)
end end
end end
+81 -115
View File
@@ -1,11 +1,9 @@
--- passes/offsets.lua — Branch-offset generator. --- passes/offsets.lua — Branch-offset generator.
--- ---
--- Reads the pre-scanned SourceScan payload (produced once upstream by `duffle.scan_source`) --- Reads the pre-scanned SourceScan payload (produced once upstream by `duffle.scan_source`)
--- for `MipsAtom_(name)` and leftover `MipsCode code_*` declarations, computes the word offset --- for `MipsAtom_(name)` and `MipsCode code_<name>` declarations, computes the word offset
--- (ELF symbol is the C ident; raw `code_*` is leftover, not the atom rule)
--- from each `atom_offset(F, T)` marker to its target `atom_label(T)` declaration, and emits --- from each `atom_offset(F, T)` marker to its target `atom_label(T)` declaration, and emits
--- `gen/offsets.h` with one `#define _atom_offset_F_T = N` per branch. --- `gen/offsets.h` with one `#define _atom_offset_F_T = N` per branch.
---
--- Per-directory aggregation: every source in the same directory contributes to the same `gen/offsets.h`. --- Per-directory aggregation: every source in the same directory contributes to the same `gen/offsets.h`.
--- The directory itself is the namespace; the filename does not repeat the module name. --- The directory itself is the namespace; the filename does not repeat the module name.
--- ---
@@ -20,25 +18,41 @@
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator. -- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). -- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. -- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Constants -- Constants
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Offset macro/enum naming prefixes (the emitted header uses these). -- Offset macro/enum naming prefixes (the emitted header uses these).
local OFFSET_MACRO_PREFIX = "_atom_offset_" ---@type string local OFFSET_MACRO_PREFIX = "_atom_offset_"
local OFFSET_ENUM_PREFIX = "atom_offset_" ---@type string local OFFSET_ENUM_PREFIX = "atom_offset_"
-- Column width for the `#define _atom_offset_F_T = N` alignment. -- Column width for the `#define _atom_offset_F_T = N` alignment.
local OFFSET_MACRO_COL = 44 ---@type integer local OFFSET_MACRO_COL = 44
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Type declarations -- Type declarations
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- SourceFile, PassCtx, PassResult: see ps1_meta.lua --- @class SourceFile
--- @field path string -- Absolute path to the source file
--- @field text string -- Full source text
--- @field dir string -- Directory containing the source
--- @field basename string -- Filename without extension
--- @field scan table -- Pre-scanned SourceScan payload (from duffle.scan_source)
--- @class PassCtx
--- @field shared table -- Cross-pass shared state
--- @field shared.corpus table -- Corpus projection
--- @field shared.word_counts table
--- @field out_root string -- Output root (e.g. "build/gen")
--- @class PassResult
--- @field outputs table[] -- {kind=, path=} entries describing emit files
--- @field errors table[] -- {line=, msg=} entries; build-stops
--- @field warnings table[] -- {line=, msg=} entries; build-succeeds
--- @class BranchOffset --- @class BranchOffset
--- @field tag string -- Marker tag (e.g. "F" in `atom_offset(F, T)`) --- @field tag string -- Marker tag (e.g. "F" in `atom_offset(F, T)`)
@@ -53,32 +67,6 @@ local OFFSET_MACRO_COL = 44 ---@type integer
--- @field total_words integer -- Total word count of the atom body --- @field total_words integer -- Total word count of the atom body
--- @field offsets BranchOffset[] -- Per-branch offset list --- @field offsets BranchOffset[] -- Per-branch offset list
--- @class OffsetBranch
--- @field tag string
--- @field target string
--- @field branch_word integer
--- @field consuming_encoder string|nil
--- @field consuming_arg_pos integer|nil
--- @field line integer|nil
--- @class MarkerProjectState
--- @field labels table<string, integer> -- bag: label name -> word index
--- @field branches OffsetBranch[]
--- @class OffsetConst
--- @field macro_name string
--- @field enum_name string
--- @field value integer
--- @class OffsetOutput
--- @field offsets_h string
--- @class OffsetsPass
--- @field run fun(ctx: PassCtx): PassResult
--- @class AtomEntry
--- @field paths AtomPaths|nil
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Canonical marker projection -- Canonical marker projection
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
@@ -86,16 +74,10 @@ local OFFSET_MACRO_COL = 44 ---@type integer
-- MARKER_PROJECTORS is the marker-kind data table. -- MARKER_PROJECTORS is the marker-kind data table.
-- The emission-model pass already records marker word positions + consuming-instruction context; -- The emission-model pass already records marker word positions + consuming-instruction context;
-- this pass only projects those records into the label/branch lookup shape needed by offset computation. -- this pass only projects those records into the label/branch lookup shape needed by offset computation.
local MARKER_PROJECTORS = { ---@type table<string, fun(state: MarkerProjectState, marker: EmissionMarker): nil> local MARKER_PROJECTORS = {
--- @param state MarkerProjectState
--- @param marker EmissionMarker
--- @return nil
label = function(state, marker) label = function(state, marker)
state.labels[marker.name] = marker.word_index state.labels[marker.name] = marker.word_index
end, end,
--- @param state MarkerProjectState
--- @param marker EmissionMarker
--- @return nil
offset = function(state, marker) offset = function(state, marker)
state.branches[#state.branches + 1] = { state.branches[#state.branches + 1] = {
tag = marker.name, tag = marker.name,
@@ -109,13 +91,12 @@ local MARKER_PROJECTORS = { ---@type table<string, fun(state: MarkerProjectState
--- Project canonical marker records into the two lookup tables used by the offset renderer. --- Project canonical marker records into the two lookup tables used by the offset renderer.
--- No source text, body text, or body token is inspected. --- No source text, body text, or body token is inspected.
--- @param markers EmissionMarker[] --- @param markers table[] -- atom.paths.markers
--- @return table<string, integer> --- @return table<string, integer>, table[]
--- @return OffsetBranch[]
local function project_markers(markers) local function project_markers(markers)
local state = { labels = {}, branches = {} } ---@type MarkerProjectState local state = { labels = {}, branches = {} }
for _, marker in ipairs(markers or {}) do ---@type integer, EmissionMarker for _, marker in ipairs(markers or {}) do
local project = MARKER_PROJECTORS[marker.kind] ---@type (fun(state: MarkerProjectState, marker: EmissionMarker): nil)|nil local project = MARKER_PROJECTORS[marker.kind]
if project then project(state, marker) end if project then project(state, marker) end
end end
return state.labels, state.branches return state.labels, state.branches
@@ -134,47 +115,38 @@ end
--- For cross-module `j`/`jal` (atom body in one module, target in another), the linker emits a `R_MIPS_26` relocation against the lower 26 bits; the upper 4 bits come from the PC of the delay slot following the `j`. --- For cross-module `j`/`jal` (atom body in one module, target in another), the linker emits a `R_MIPS_26` relocation against the lower 26 bits; the upper 4 bits come from the PC of the delay slot following the `j`.
--- The metaprogram doesn't know either at compile time, so the emitted value is the relative word offset that the duffle `enc_i` macro places in the immediate field; the toolchain handles the rest. --- The metaprogram doesn't know either at compile time, so the emitted value is the relative word offset that the duffle `enc_i` macro places in the immediate field; the toolchain handles the rest.
--- `jump_reg` / `call_reg` / `jump_link` -> ERROR. Register-form jumps have no offset field; `atom_offset` is invalid. --- `jump_reg` / `call_reg` / `jump_link` -> ERROR. Register-form jumps have no offset field; `atom_offset` is invalid.
--- missing `consuming_encoder` -> ERROR. A lone top-level `atom_offset` is not a branch. ---
--- Top-level `atom_offset(F, T)` markers (where the marker is the entire token — `consuming_encoder` == nil) default to `branch_*` behavior (relative offset).
--- This preserves backward compatibility for any top-level marker that may exist outside a control-transfer instruction.
--- @param labels table<string, integer> --- @param labels table<string, integer>
--- @param branches OffsetBranch[] --- @param branches table[]
--- @param errors Finding[]
--- @return BranchOffset[] --- @return BranchOffset[]
local function compute_offsets(labels, branches, errors) local function compute_offsets(labels, branches)
local results = {} ---@type BranchOffset[] local results = {}
for _, br in ipairs(branches) do ---@type integer, OffsetBranch for _, br in ipairs(branches) do
local target = labels[br.target] ---@type integer|nil local target = labels[br.target]
if not target then if not target then
errors[#errors + 1] = { error("Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")")
line = br.line or 0, end
msg = "Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")", local consuming = br.consuming_encoder
} local offset
else if consuming == "jump_reg" or consuming == "call_reg" or consuming == "jump_link" then
local consuming = br.consuming_encoder ---@type string|nil -- Register-form jumps have no offset field. `atom_offset` cannot be used here.
if consuming == nil or consuming == "" then error("atom_offset cannot be used with " .. consuming
errors[#errors + 1] = { .. " (register-form jumps have no offset field); at word " .. br.branch_word)
line = br.line or 0, end
msg = "atom_offset requires a consuming encoder (branch_*, jump, call_addr); top-level atom_offset is invalid; at word " .. br.branch_word, -- All other consuming instructions (including `branch_*`, `jump`, `call_addr`, and nil for top-level markers) use the same relative offset value.
}
elseif consuming == "jump_reg" or consuming == "call_reg" or consuming == "jump_link" then
errors[#errors + 1] = {
line = br.line or 0,
msg = "atom_offset cannot be used with " .. consuming
.. " (register-form jumps have no offset field); at word " .. br.branch_word,
}
else
-- Consuming instructions with an offset field (`branch_*`, `jump`, `call_addr`) use the same relative offset value.
-- The MIPS encoding differs per opcode but the duffle `enc_i` macro handles the truncation to the immediate-field width. -- The MIPS encoding differs per opcode but the duffle `enc_i` macro handles the truncation to the immediate-field width.
offset = target - br.branch_word - 1
results[#results + 1] = { results[#results + 1] = {
target = br.target, target = br.target,
tag = br.tag, tag = br.tag,
branch_word = br.branch_word, branch_word = br.branch_word,
offset = target - br.branch_word - 1, offset = offset,
consuming_encoder = br.consuming_encoder, consuming_encoder = br.consuming_encoder,
consuming_arg_pos = br.consuming_arg_pos, consuming_arg_pos = br.consuming_arg_pos,
} }
end end
end
end
return results return results
end end
@@ -188,7 +160,7 @@ end
--- (internal) Build a constant-table entry `{macro_name, enum_name, value}` from a BranchOffset. --- (internal) Build a constant-table entry `{macro_name, enum_name, value}` from a BranchOffset.
--- @param bo BranchOffset --- @param bo BranchOffset
--- @return OffsetConst --- @return table
local function make_offset_const(bo) local function make_offset_const(bo)
return { return {
macro_name = OFFSET_MACRO_PREFIX .. bo.tag .. "_" .. bo.target, macro_name = OFFSET_MACRO_PREFIX .. bo.tag .. "_" .. bo.target,
@@ -200,21 +172,20 @@ end
--- (internal) Emit one atom's offset constants + enum into the lines buffer. --- (internal) Emit one atom's offset constants + enum into the lines buffer.
--- @param add fun(s: string) --- @param add fun(s: string)
--- @param atom AtomData --- @param atom AtomData
--- @return nil
local function emit_atom_offsets(add, atom) local function emit_atom_offsets(add, atom)
if #atom.offsets == 0 then return end if #atom.offsets == 0 then return end
add("// --- atom: " .. atom.name .. " (" .. atom.total_words .. " words) ---") add("// --- atom: " .. atom.name .. " (" .. atom.total_words .. " words) ---")
add("") add("")
local consts = {} ---@type OffsetConst[] local consts = {}
for _, r in ipairs(atom.offsets) do ---@type integer, BranchOffset for _, r in ipairs(atom.offsets) do
consts[#consts + 1] = make_offset_const(r) consts[#consts + 1] = make_offset_const(r)
end end
for _, c in ipairs(consts) do ---@type integer, OffsetConst for _, c in ipairs(consts) do
add("#define " .. pad_right(c.macro_name, OFFSET_MACRO_COL) .. " " .. c.value) add("#define " .. pad_right(c.macro_name, OFFSET_MACRO_COL) .. " " .. c.value)
end end
add("") add("")
add("enum {") add("enum {")
for _, c in ipairs(consts) do ---@type integer, OffsetConst for _, c in ipairs(consts) do
add(" " .. c.enum_name .. " = " .. c.macro_name .. ",") add(" " .. c.enum_name .. " = " .. c.macro_name .. ",")
end end
add("};") add("};")
@@ -222,21 +193,19 @@ local function emit_atom_offsets(add, atom)
end end
--- Generate the per-directory .offsets.h header. --- Generate the per-directory .offsets.h header.
--- @param dir string --- @param dir string -- the absolute source directory
--- @param sources SourceFile[] --- @param sources table[] -- sources contributing to this directory (for the header comment)
--- @param atoms_data AtomData[] --- @param atoms_data AtomData[]
--- @return string --- @return string
local function generate_header(dir, sources, atoms_data) local function generate_header(dir, sources, atoms_data)
local dir_basename = duffle.basename_no_ext(dir) ---@type string local dir_basename = duffle.basename_no_ext(dir)
local lines = {} ---@type string[] local lines = {}
--- @param s string
--- @return nil
local function add(s) lines[#lines + 1] = s end local function add(s) lines[#lines + 1] = s end
add("// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT") add("// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT")
add("// Directory: " .. dir:gsub("/", "\\") .. "\\") add("// Directory: " .. dir:gsub("/", "\\") .. "\\")
for _, src in ipairs(sources) do ---@type integer, SourceFile for _, src in ipairs(sources) do
add("// source: " .. src.path:gsub("/", "\\")) add("// source: " .. src.path:gsub("/", "\\"))
end end
add("#pragma once") add("#pragma once")
@@ -244,7 +213,7 @@ local function generate_header(dir, sources, atoms_data)
add("#pragma region " .. dir_basename) add("#pragma region " .. dir_basename)
add("") add("")
add("") add("")
for _, atom in ipairs(atoms_data) do ---@type integer, AtomData for _, atom in ipairs(atoms_data) do
emit_atom_offsets(add, atom) emit_atom_offsets(add, atom)
end end
add("#pragma endregion " .. dir_basename) add("#pragma endregion " .. dir_basename)
@@ -252,39 +221,36 @@ local function generate_header(dir, sources, atoms_data)
return table.concat(lines, "\n") .. "\n" return table.concat(lines, "\n") .. "\n"
end end
local M = {} ---@type OffsetsPass local M = {}
--- (internal) Aggregate atoms from every source in one directory, render the per-directory `offsets.h`. --- (internal) Aggregate atoms from every source in one directory, render the per-directory `offsets.h`.
--- Returns the offsets_h path if a header was written, or nil. --- Returns the offsets_h path if a header was written, or nil.
--- @param ctx PassCtx --- @param ctx PassCtx
--- @param dir string --- @param dir string -- the absolute source directory
--- @param sources SourceFile[] --- @param sources SourceFile[] -- sources in this directory
--- @param errors Finding[] --- @return string|nil -- the offsets_h path
--- @return string|nil local function process_directory(ctx, dir, sources)
local function process_directory(ctx, dir, sources, errors) local atoms_data = {}
local atoms_data = {} ---@type AtomData[]
--- @param atom AtomEntry
--- @return nil
local function append_atom(atom) local function append_atom(atom)
local paths = atom and atom.paths ---@type AtomPaths|nil local paths = atom and atom.paths
if not paths then return end if not paths then return end
local labels, branches = project_markers(paths.markers) ---@type table<string, integer>, OffsetBranch[] local labels, branches = project_markers(paths.markers)
atoms_data[#atoms_data + 1] = { atoms_data[#atoms_data + 1] = {
name = atom.raw_name or atom.name, name = atom.raw_name or atom.name,
total_words = #(paths.word_events or {}), total_words = #(paths.word_events or {}),
offsets = compute_offsets(labels, branches, errors), offsets = compute_offsets(labels, branches),
} }
end end
for _, src in ipairs(sources) do ---@type integer, SourceFile for _, src in ipairs(sources) do
local scan = src.scan or {} ---@type SourceScan local scan = src.scan or {}
for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end ---@type integer, AtomEntry for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end
for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end ---@type integer, AtomEntry for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end
end end
if #atoms_data == 0 then return nil end if #atoms_data == 0 then return nil end
local out_path = dir .. "/gen/offsets.h" ---@type string local out_path = dir .. "/gen/offsets.h"
duffle.ensure_dir(duffle.dirname(out_path)) duffle.ensure_dir(duffle.dirname(out_path))
duffle.write_file(out_path, generate_header(dir, sources, atoms_data)) duffle.write_file(out_path, generate_header(dir, sources, atoms_data))
return out_path return out_path
@@ -296,11 +262,11 @@ end
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
local outputs = {} ---@type OffsetOutput[] local outputs = {}
local errors = {} ---@type Finding[] local errors = {}
local warnings = {} ---@type Finding[] local warnings = {}
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil local corpus = ctx.shared and ctx.shared.corpus
if type(corpus) ~= "table" then if type(corpus) ~= "table" then
error("offsets.run requires ctx.shared.corpus", 0) error("offsets.run requires ctx.shared.corpus", 0)
end end
@@ -309,9 +275,9 @@ function M.run(ctx)
end end
-- Per-directory aggregation: every source in the same directory contributes to one `gen/offsets.h`. -- Per-directory aggregation: every source in the same directory contributes to one `gen/offsets.h`.
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) ---@type table<string, SourceFile[]> local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order)
for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[] for dir, sources in pairs(sources_by_dir) do
local out_path = process_directory(ctx, dir, sources, errors) ---@type string|nil local out_path = process_directory(ctx, dir, sources)
if out_path then if out_path then
outputs[#outputs + 1] = { offsets_h = out_path } outputs[#outputs + 1] = { offsets_h = out_path }
end end
+332 -883
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+32 -16
View File
@@ -4,13 +4,13 @@
--- 1. **Public utility** `M.count_token_words(token, wc)`: Used by `passes/offsets.lua`, `passes/annotation.lua`, and other passes. --- 1. **Public utility** `M.count_token_words(token, wc)`: Used by `passes/offsets.lua`, `passes/annotation.lua`, and other passes.
--- 2. **Pass entry** `M.run(ctx)`: Loads the authored `word_count.metadata.h` into `ctx.shared.corpus.word_counts` for downstream passes. --- 2. **Pass entry** `M.run(ctx)`: Loads the authored `word_count.metadata.h` into `ctx.shared.corpus.word_counts` for downstream passes.
--- The generated `.macs.h` files are OUTPUT artifacts and are NOT inputs to this pass; --- The generated `.macs.h` files are OUTPUT artifacts and are NOT inputs to this pass;
--- Current component counts are owned by `passes/components.lua` (which populates `corpus.word_counts` and `corpus.components` --- Current component counts are owned by `passes/components.lua` (which populates `corpus.word_counts` and `corpus.component_body_index`
--- AFTER computing each current count from the just-built body + `corpus.word_counts`). --- AFTER computing each current count from the just-built body + `corpus.word_counts`).
--- ---
--- **Canonical contract**: --- **Canonical contract**:
--- * `ctx.shared.corpus.word_counts` is the count table. --- * `ctx.shared.corpus.word_counts` is the count table.
--- * `corpus.word_counts` is the sole count table. Consumers read `corpus.word_counts` directly. --- * `corpus.word_counts` is the sole count table. Consumers read `corpus.word_counts` directly.
--- * `ctx.shared.components` is NOT created by this pass (projections only). --- * `ctx.shared.components` and `ctx.shared.component_body_index` are NOT created by this pass (projections only).
--- * No `.macs.h` recursive discovery (no `scan_dir`, no scan cache, no `_invalidate_scan_cache`). --- * No `.macs.h` recursive discovery (no `scan_dir`, no scan cache, no `_invalidate_scan_cache`).
--- ---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, --- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
@@ -23,28 +23,44 @@
-- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath).
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator. -- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. -- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Type declarations -- Type declarations
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @class WordCounts --- @class WordCounts
--- @field [string] integer -- bag: macro name -> word count --- @field [string] integer -- macro name -> word count
--- @class WordCountEval --- @class SourceFile
--- @field count_token_words fun(token: string, wc: WordCounts): integer --- @field path string -- absolute path to the source file
--- @field run fun(ctx: PassCtx): PassResult --- @field text string -- the full source text
--- @field dir string -- the directory containing the source
--- @field basename string -- filename without extension
-- SourceFile, PassCtx, PassResult: see ps1_meta.lua --- @class PassCtx
-- DuffleExport: see duffle.lua (facade returned by duffle_paths.lua) --- @field sources SourceFile[] -- all source files in the build
--- @field metadata_path string -- path to word_count.metadata.h
--- @field shared table -- cross-pass shared state
--- @field shared.corpus table -- canonical corpus (required)
--- @field shared.corpus.word_counts WordCounts -- canonical count table (populated by this pass)
--- @field out_root string -- output root (e.g. "build/gen")
--- @field project_root string -- project root (e.g. "code/")
--- @field upstream table<string, table> -- per-pass upstream outputs
--- @field flags table -- CLI flags
--- @field verbose boolean -- if true, log diagnostic info
--- @class PassResult
--- @field outputs table[] -- {kind=, path=} entries describing emit files
--- @field errors table[] -- {line=, msg=} entries; build-stops
--- @field warnings table[] -- {line=, msg=} entries; build-succeeds
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Module exports -- Module exports
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
local M = {} ---@type WordCountEval local M = {}
-- ┌────────────────────────────────────────────────────────────────────┐ -- ┌────────────────────────────────────────────────────────────────────┐
-- │ Shared utility: count_token_words │ -- │ Shared utility: count_token_words │
@@ -58,12 +74,12 @@ local M = {} ---@type WordCountEval
--- @param wc WordCounts -- the shared word-count table --- @param wc WordCounts -- the shared word-count table
--- @return integer --- @return integer
function M.count_token_words(token, wc) function M.count_token_words(token, wc)
local s = duffle.trim(token) ---@type string local s = duffle.trim(token)
if s == "" then return 0 end if s == "" then return 0 end
local name, after = duffle.read_ident(s, 1) ---@type string|nil, integer local name, after = duffle.read_ident(s, 1)
if not name then return 1 end if not name then return 1 end
if wc[name] then return wc[name] end if wc[name] then return wc[name] end
local paren_pos = duffle.skip_ws_and_cmt(s, after) ---@type integer local paren_pos = duffle.skip_ws_and_cmt(s, after)
if s:sub(paren_pos, paren_pos) == "(" then if s:sub(paren_pos, paren_pos) == "(" then
io.stderr:write(" warning: unknown macro '" .. name .. "', assuming 1 word\n") io.stderr:write(" warning: unknown macro '" .. name .. "', assuming 1 word\n")
end end
@@ -89,7 +105,7 @@ end
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
-- 1. Canonical-corpus ownership gate. -- 1. Canonical-corpus ownership gate.
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil local corpus = ctx.shared and ctx.shared.corpus
if type(corpus) ~= "table" then if type(corpus) ~= "table" then
error("word_count_eval.run requires ctx.shared.corpus (canonical corpus). The fixture must install the corpus before running this pass.", 0) error("word_count_eval.run requires ctx.shared.corpus (canonical corpus). The fixture must install the corpus before running this pass.", 0)
end end
@@ -101,7 +117,7 @@ function M.run(ctx)
-- 3. Load authored metadata. Generated .macs.h files are NOT scanned -- 3. Load authored metadata. Generated .macs.h files are NOT scanned
-- (the pass computes their counts from the just-built bodies after disk emission; see passes/components.lua). -- (the pass computes their counts from the just-built bodies after disk emission; see passes/components.lua).
local wc = duffle.load_word_counts(ctx.metadata_path) ---@type WordCounts local wc = duffle.load_word_counts(ctx.metadata_path)
-- 4. Assign the count table. ONE assignment, no copy. The assignment creates no secondary alias. -- 4. Assign the count table. ONE assignment, no copy. The assignment creates no secondary alias.
corpus.word_counts = wc corpus.word_counts = wc
Binary file not shown.
+24
View File
@@ -49,3 +49,27 @@ local ok, err = pcall(register_handlers)
if ok then print("[pcsx_debug_helper] handlers registered: gte, gp") if ok then print("[pcsx_debug_helper] handlers registered: gte, gp")
else print("[pcsx_debug_helper] registration failed: " .. tostring(err)) else print("[pcsx_debug_helper] registration failed: " .. tostring(err))
end end
-- ── reload handler (Task 6) ──
-- After gte and gp register successfully, load reload.lua through Support.extra.dofile and call its install(pcsx, support).
-- The whole sequence runs inside pcall so a missing zip, missing module table,
-- or throwing install never disturbs the gte and gp handlers already registered above (handler isolation).
--
-- The failure messages are intentionally single-line so the helper's boot log stays scannable.
if type(Support) == "table"
and type(Support.extra) == "table"
and type(Support.extra.dofile) == "function" then
local load_ok, reload_mod = pcall(Support.extra.dofile, "reload.lua")
if load_ok and type(reload_mod) == "table" and type(reload_mod.install) == "function" then
local install_ok, install_err = pcall(reload_mod.install, PCSX, Support)
if install_ok then
print("[pcsx_debug_helper] reload handler registered")
else
print("[pcsx_debug_helper] reload registration failed: " .. tostring(install_err))
end
else
print("[pcsx_debug_helper] reload load failed: " .. tostring(reload_mod))
end
else
print("[pcsx_debug_helper] reload load failed: Support.extra.dofile unavailable")
end
+902
View File
@@ -0,0 +1,902 @@
-- reload.lua - Side-effect-free hot-reload helper for the
-- pcsx_redux_hot_reload track (Task 2). This file owns the HTTP request
-- surface that the launch / reload client targets:
--
-- POST /api/v1/lua/reload?mode=prime&target=hello_camera&path=<encoded-elf>
-- POST /api/v1/lua/reload?mode=elf&target=hello_camera&path=<encoded-elf>
-- POST /api/v1/lua/reload?mode=patch&target=hello_camera&addr=...&hex=...
--
-- This module exposes the public surface used by the contract harness
-- (tests/reload_helper_contract.lua) and the runtime installed by
-- scripts/pcsx_debug_helper/autoexec.lua. The module must not reference
-- the global PCSX table at load time; the host is passed in explicitly
-- through M.new(host) and M.install(pcsx, support).
--
-- Public surface:
-- M.parse_query(query) -> table, nil OR nil, err_string
-- M.json_response(fields) -> string (sorted keys)
-- M.parse_manifest(...) -> Task 3 (real impl uses elf32.lua)
-- M.new(host) -> runtime object (Task 4; stub here)
-- M.install(pcsx, support) -> registers web handler (Task 6; stub here)
--
-- Companion: scripts/pcsx_debug_helper/autoexec.lua.
-- ---------------------------------------------------------------------------
-- Load the shared ELF32 helpers.
--
-- **The bane of this refactor:** the helper VM (PCSX-Redux) does not expose
-- `require` for paths outside the helper zip. The production loader is
-- `Support.extra.dofile("elf32.lua")` — Support.extra.dofile resolves the
-- name against the helper zip's contents (the zip is generated by the
-- build script and includes both `reload.lua` and `elf32.lua` after Task 6).
--
-- The test harness at `tests/reload_helper_contract.lua` loads `reload.lua`
-- via standard Lua `dofile` with an absolute path; it does not install a
-- `Support` object. We detect the runtime context: if `Support.extra.dofile`
-- exists, use it (production path); otherwise fall back to standard `dofile`
-- with an absolute path (test harness path).
-- ---------------------------------------------------------------------------
local function load_elf32()
if type(Support) == "table"
and type(Support.extra) == "table"
and type(Support.extra.dofile) == "function" then
return Support.extra.dofile("elf32.lua")
end
-- Test harness + any other context that supplies standard Lua dofile.
return dofile("C:/projects/Pikuma/ps1/scripts/elf32.lua")
end
local E = load_elf32()
local M = {}
-- ---------------------------------------------------------------------------
-- parse_query(query)
--
-- Parses an application/x-www-form-urlencoded query string into a table.
--
-- Rules (per spec §8 + plan.md Task 2 Step 3):
-- * Each pair is split on the first '='; the key is to the left, the value
-- to the right. A pair without '=' is a malformed_pair.
-- * Percent escapes '%HH' (HH = two hex digits) decode to the corresponding
-- byte. A '%' not followed by two hex digits is a malformed_escape.
-- * '+' decodes to a literal space (applied after percent decode).
-- * A key appearing more than once is a duplicate_key error.
--
-- Returns the parsed table on success. On failure returns nil and a stable
-- error string suitable for the JSON error envelope. An empty / nil query
-- returns an empty table (not an error).
-- ---------------------------------------------------------------------------
local function percent_decode(s)
-- Walk the string once, byte by byte. A '%' must be followed by exactly
-- two hex digits; '+' decodes to ' '; everything else is passed through.
local out = {}
local i = 1
local len = #s
while i <= len do
local c = s:sub(i, i)
if c == "%" then
if i + 2 > len then
return nil -- truncated escape (e.g., '%' at end or '%X')
end
local hex = s:sub(i + 1, i + 2)
local hd1, hd2 = hex:sub(1, 1), hex:sub(2, 2)
-- Validate both characters are hex digits.
if not (hd1:match("[0-9A-Fa-f]") and hd2:match("[0-9A-Fa-f]")) then
return nil -- malformed escape
end
out[#out + 1] = string.char(tonumber(hex, 16))
i = i + 3
else
out[#out + 1] = c
i = i + 1
end
end
return table.concat(out)
end
local function plus_to_space(s)
-- Standalone helper so callers can decode '+' after percent decoding.
return (s:gsub("+", " "))
end
function M.parse_query(query)
if query == nil or query == "" then
return {}, nil
end
local result = {}
local seen = {}
for pair in query:gmatch("[^&]+") do
-- Split on the first '=' only.
local eq = pair:find("=", 1, true)
if not eq then
return nil, "malformed_pair"
end
local raw_key = pair:sub(1, eq - 1)
local raw_value = pair:sub(eq + 1)
-- Percent-decode first, then convert '+' to space. The order matters:
-- a '%2B' should decode to '+' (literal plus), not be re-converted to a
-- space. Per RFC 1866 §8.2.1, '+' is a literal plus in the encoded form
-- only when it represents a space.
local key = percent_decode(raw_key)
if key == nil then
return nil, "malformed_escape"
end
key = plus_to_space(key)
local val = percent_decode(raw_value)
if val == nil then
return nil, "malformed_escape"
end
val = plus_to_space(val)
if seen[key] then
return nil, "duplicate_key"
end
seen[key] = true
result[key] = val
end
return result, nil
end
-- ---------------------------------------------------------------------------
-- json_response(fields)
--
-- Deterministic JSON object encoder. Returns a string. Keys are sorted
-- alphabetically before emission so byte-for-byte equality is testable
-- across runs and across PS1 captures.
--
-- Supported value types: string, number, boolean, nil (encoded as null).
-- Strings escape '\', '"', and the C0 control range (0x00..0x1F). The
-- named escapes use the conventional single-char forms: \\, \", \b, \f,
-- \n, \r, \t. Everything else in 0x00..0x1F is \uXXXX.
-- ---------------------------------------------------------------------------
local function json_escape_string(s)
-- Two passes: first the named escapes, then the catch-all C0 range
-- (%c covers 0x00..0x1F in Lua patterns). Using plain string.gsub
-- with a literal replacement table covers the named escapes; a
-- second gsub handles the rest.
s = s:gsub('[\\"]', {
["\\"] = "\\\\",
['"'] = '\\"',
})
s = s:gsub("\b", "\\b")
s = s:gsub("\f", "\\f")
s = s:gsub("\n", "\\n")
s = s:gsub("\r", "\\r")
s = s:gsub("\t", "\\t")
-- Remaining C0 control characters (0x00..0x1F) become \uXXXX. We
-- intentionally keep the named escapes above (which are already
-- single backslashes in the output) from being re-escaped: gsub on
-- the literal control char bytes doesn't match the backslashes we
-- already inserted.
s = s:gsub("([%c])", function(c)
return string.format("\\u%04x", string.byte(c))
end)
return s
end
function M.json_response(fields)
if type(fields) ~= "table" then
error("json_response: expected table, got " .. type(fields))
end
-- Sort keys for deterministic output. Lua's table.sort is byte-wise
-- and stable for strings; JSON object key order is not significant
-- but tests rely on a fixed order to compare against fixtures.
local keys = {}
for k in pairs(fields) do
keys[#keys + 1] = k
end
table.sort(keys)
local parts = {}
parts[#parts + 1] = "{"
for i = 1, #keys do
local k = keys[i]
if i > 1 then
parts[#parts + 1] = ","
end
parts[#parts + 1] = '"'
parts[#parts + 1] = json_escape_string(k)
parts[#parts + 1] = '":'
local v = fields[k]
local tv = type(v)
if tv == "string" then
parts[#parts + 1] = '"'
parts[#parts + 1] = json_escape_string(v)
parts[#parts + 1] = '"'
elseif tv == "number" then
parts[#parts + 1] = tostring(v)
elseif tv == "boolean" then
parts[#parts + 1] = v and "true" or "false"
elseif v == nil then
parts[#parts + 1] = "null"
else
error("json_response: unsupported value type " .. tv .. " for key " .. tostring(k))
end
end
parts[#parts + 1] = "}"
return table.concat(parts)
end
-- ---------------------------------------------------------------------------
-- ELF32 manifest parser (Task 3).
--
-- Parses a little-endian ELF32 file exposed through a file_adapter that
-- provides read_u8_at/read_u16_at/read_u32_at/read_size. The parser validates the
-- magic, class, data encoding, and machine before reading anything else.
-- It resolves section names through the .shstrtab table and symbols
-- through every SHT_SYMTAB section (and its linked string table).
--
-- The output manifest contains the state ABI the reload gate must
-- preserve plus the addresses the helper writes to the CPU on a reload.
-- Loaded sections (SHF_ALLOC, non-SHT_NOBITS) are recorded so the runtime
-- can reject any ELF whose loaded range overlaps the preserved smem.
--
-- **Refactor:** the format-constant tables + the byte-level walker live in
-- scripts/elf32.lua (loaded above via `load_elf32()`). This module retains
-- only the manifest-specific validation: required symbols, smem size, stack
-- alignment, loaded-section overlap. The net effect is ~80 lines shorter.
--
-- Stable error codes (returned as the second value):
-- bad_magic, unsupported_elf_class, unsupported_elf_data,
-- non_mips_machine, truncated_header, truncated_section_headers,
-- missing_shstrtab, missing_symtab_strtab, missing_smem,
-- missing_data_start, missing_data_end, missing_bss_start,
-- missing_bss_end, missing_stack_top, missing_hot_reload_entry,
-- zero_smem_size, stack_misaligned, stack_out_of_main_ram,
-- section_overlaps_smem, bad_file_adapter
-- ---------------------------------------------------------------------------
-- Convert a KSEG0/KSEG1/physical address to its physical main-RAM offset.
local function to_physical(addr)
if addr >= 0x80000000 and addr < 0x80200000 then
return addr - 0x80000000
elseif addr >= 0xa0000000 and addr < 0xa0200000 then
return addr - 0xa0000000
end
return addr
end
-- Strip KSEG0 / KSEG1 alias from an address and return the physical main-RAM
-- offset. Used by M.elf_reload and M.patch_handler. Returns nil when the
-- address falls outside physical main RAM (0..0x1fffff), KSEG0 main RAM
-- (0x80000000..0x801fffff), or KSEG1 main RAM (0xa0000000..0xa01fffff).
-- Per spec §7 the patch path MUST reject scratchpad (0x1F800000+), BIOS
-- (0x1FC00000+), MMIO, and expansion aliases; this helper centralizes the
-- strip + range check so callers cannot forget the upper bound.
local function strip_kseg(addr)
if type(addr) ~= "number" then return nil end
if addr >= 0x80000000 and addr < 0x80200000 then
return addr - 0x80000000
elseif addr >= 0xa0000000 and addr < 0xa0200000 then
return addr - 0xa0000000
elseif addr >= 0 and addr < 0x200000 then
return addr
end
return nil
end
-- Parse a hex string ("0xHHHH..." or "HHHH...") into a 32-bit unsigned
-- integer. Returns nil + stable error on absent / non-hex / out-of-range.
-- Used for both the patch path's addr/hex query parameters and any other
-- 32-bit hex field the API may add. Accepts up to 8 hex digits.
local function parse_hex_u32(s, missing_err, badhex_err)
if type(s) ~= "string" or #s == 0 then
return nil, missing_err or "missing_hex"
end
local clean = s:match("^0[xX]([0-9A-Fa-f]+)$")
or s:match("^([0-9A-Fa-f]+)$")
if not clean then return nil, badhex_err or "non_hex" end
if #clean > 8 then return nil, badhex_err or "non_hex" end
return tonumber(clean, 16), nil
end
-- Trap on a missing E.* — keeps the existing one-line-error pattern when
-- the helper zip is stale or absent.
local function stack()
io.stderr:write("[reload.parse_manifest] FATAL: scripts/elf32.lua not loaded; aborting\n")
error("elf32 module not loaded")
end
local function parse_manifest_impl(file_adapter, target, path, require_entry)
-- Wrap the body in a pcall so any thrown exception (e.g. a bad
-- adapter method or a malformed section header) surfaces as a
-- parse_error with the message and traceback instead of being lost
-- into the with_busy_guard xpcall as a generic internal_error.
local inner_ok, inner_result, inner_err = pcall(function()
-- Validate the adapter surface. E.validate_adapter returns the same
-- "bad_file_adapter" error code the prior implementation used.
local ok, err = E.validate_adapter(file_adapter)
if not ok then return nil, err end
-- Magic, class, data encoding. E.parse_elf32_headers reads fields at
-- the wire offsets specified in E.ELF32_HEADER.
local hdr, hdr_err = E.parse_elf32_headers(file_adapter)
if not hdr then return nil, hdr_err end
-- Machine check (e.g. EM_MIPS = 8). e_machine is at offset 0x12 (18).
-- The reload helper rejects non-MIPS ELFs before any symbol work.
-- Explicit pass style: E.read_u16(adapter, off). The helper wraps the
-- Support.File adapter once to strip its implicit `self` so the
-- parser shape stays flat-function, not colon-dispatch.
local machine = E.read_u16(file_adapter, 0x12)
if not machine then return nil, "truncated_header" end
if machine ~= E.EM_MIPS then
return nil, "non_mips_machine"
end
-- Walk sections. E.walk_sections also resolves .shstrtab names.
local sections, walk_err = E.walk_sections(file_adapter, hdr)
if not sections then return nil, walk_err end
-- Walk symbols. E.collect_symbols includes both STB_LOCAL and STB_GLOBAL
-- (the live ELF stores smem as a local symbol).
local symbols, sym_err = E.collect_symbols(file_adapter, sections)
if not symbols then return nil, sym_err end
-- Required symbols.
local smem = symbols["smem"]
local data_start = symbols["__data_start"]
local data_end = symbols["__data_end"]
local bss_start = symbols["__bss_start"]
local bss_end = symbols["__bss_end"]
local stack_top_s = symbols["__sp"]
local entry_s = symbols["hot_reload_entry"]
if not smem then return nil, "missing_smem" end
if not data_start then return nil, "missing_data_start" end
if not data_end then return nil, "missing_data_end" end
if not bss_start then return nil, "missing_bss_start" end
if not bss_end then return nil, "missing_bss_end" end
if not stack_top_s then return nil, "missing_stack_top" end
if require_entry and not entry_s then
return nil, "missing_hot_reload_entry"
end
-- Validate smem size.
if smem.size == 0 then
return nil, "zero_smem_size"
end
-- Validate stack alignment and range.
local stack_top = stack_top_s.value
if stack_top % 8 ~= 0 then
return nil, "stack_misaligned"
end
local p = to_physical(stack_top)
if p < 0 or p > 0x1fffff then
return nil, "stack_out_of_main_ram"
end
-- Collect loaded (SHF_ALLOC, non-SHT_NOBITS) sections and check overlap.
local loaded = {}
local smem_lo = smem.value
local smem_hi = smem.value + smem.size
for _, s in ipairs(sections) do
-- bit 1 (SHF_ALLOC = 0x2) of sh_flags. The modulo-4 trick matches
-- the prior implementation; canonicalising on E.SHF_ALLOC would
-- gain readability but lose the exact prior behavior.
local is_alloc = (s.sh_flags % 4) >= 2
if is_alloc and s.sh_type ~= E.SHT_NOBITS and s.sh_size > 0 then
loaded[#loaded + 1] = { name = s.name, addr = s.sh_addr, size = s.sh_size }
local lo = s.sh_addr
local hi = s.sh_addr + s.sh_size
if lo < smem_hi and hi > smem_lo then
return nil, "section_overlaps_smem"
end
end
end
return {
target = target,
elf_path = path,
elf_entry = hdr.e_entry,
smem_addr = smem.value,
smem_size = smem.size,
bss_start = bss_start.value,
bss_end = bss_end.value,
data_start = data_start.value,
data_end = data_end.value,
hot_reload_entry = entry_s and entry_s.value or nil,
stack_top = stack_top,
loaded_sections = loaded,
}
end)
if inner_ok then
return inner_result, inner_err
end
-- pcall captured a thrown error; surface as parse_error with the
-- message + traceback so the caller can render it.
local tb = debug.traceback(inner_result, 2)
local err = {
parse_error = true,
detail = tostring(inner_result),
tb = tb,
}
return nil, err
end
function M.parse_manifest(file_adapter, target, path, require_entry)
if type(E) ~= "table" or type(E.parse_elf32_headers) ~= "function" then
stack()
end
return parse_manifest_impl(file_adapter, target, path, require_entry)
end
-- ---------------------------------------------------------------------------
-- Runtime + dispatch (Task 4)
--
-- M.new(host) returns a runtime object that owns:
-- active -- the most recently primed manifest, or nil
-- busy -- boolean guard; only one request runs at a time
-- host -- the bound host surface (pause / memory_file / open_file
-- / binary_load / invalidate_cache / get_registers)
--
-- runtime:handle(req) parses the query through M.parse_query, validates
-- the mode against a dispatch table, then acquires the busy guard through
-- xpcall so any error inside the handler releases the guard. The response
-- is always a JSON string built by M.json_response.
--
-- M.prime_active and M.elf_reload are the two handler bodies Task 4 ships.
-- prime_active always parses with require_entry=false (Phase 0 binary
-- compatibility). elf_reload always parses with require_entry=true (the
-- new binary must expose hot_reload_entry). Both validate the parsed
-- manifest; elf_reload runs the five-field ABI gate before declaring
-- success. Full host.pause / memory_file / binary_load / invalidate_cache
-- / get_registers sequencing is Task 5.
-- ---------------------------------------------------------------------------
-- Convert a manifest into the JSON-serializable field subset. loaded_sections
-- is excluded because json_response only supports scalars + nil.
local function manifest_to_response(m)
local fields = {
ok = true,
target = m.target,
elf_path = m.elf_path,
elf_entry = m.elf_entry,
smem_addr = m.smem_addr,
smem_size = m.smem_size,
bss_start = m.bss_start,
bss_end = m.bss_end,
data_start = m.data_start,
data_end = m.data_end,
stack_top = m.stack_top,
}
if m.hot_reload_entry then
fields.hot_reload_entry = m.hot_reload_entry
end
return fields
end
-- Open the new ELF through the host and parse its manifest.
-- Returns manifest on success; nil + stable error on failure.
local function parse_manifest_via_host(host, target, path, require_entry)
local adapter = host.open_file(path)
if not adapter then
return nil, "open_file_failed"
end
return M.parse_manifest(adapter, target, path, require_entry)
end
-- prime_active: parse with require_entry=false. Accepts Phase 0 binaries
-- that lack hot_reload_entry. Stores the manifest in runtime.active.
function M.prime_active(runtime, parsed)
local manifest, err = parse_manifest_via_host(
runtime.host, parsed.target, parsed.path, false)
if not manifest then
return M.json_response({ ok = false, error = err, restart_required = true })
end
runtime.active = manifest
return M.json_response(manifest_to_response(manifest))
end
-- elf_reload: full host-driven reload sequence.
--
-- Per conductor/tracks/ps1_pcsx_redux_hot_reload_20260802/spec.md §5 +
-- plan.md Task 5 Step 4. The canonical 11-entry success log is:
--
-- pause, memory_file, state_read, open_new_elf, binary_load,
-- state_restore, invalidate_cache, get_registers, write_sp,
-- write_ra, write_pc
--
-- Sequencing:
--
-- 1. Validate the request (target == active.target, path present).
-- 2. Compute the physical address of `active.smem_addr` via
-- strip_kseg; reject if outside physical main RAM.
-- 3. PARSE PHASE (before pause):
-- a. elf_handle = host.open_file(parsed.path)
-- b. manifest = M.parse_manifest(elf_handle, ..., require_entry=true)
-- c. Run the five-field ABI gate against runtime.active.
-- d. On any rejection here, return BEFORE pause — the runtime
-- has invoked host.open_file once (logging "open_file") and
-- no other host methods.
-- 4. Pause + snapshot:
-- host.pause()
-- mem = host.memory_file()
-- saved = mem:readAtToSlice(active.smem_size, smem_phys)
-- 5. RELOAD PHASE:
-- elf_handle = host.open_new_elf(parsed.path) -- second open
-- loaded = host.binary_load(elf_handle, mem)
-- if loaded == nil then return binary_load_failed
-- 6. Restore state: mem:writeAtMoveSlice(saved, smem_phys)
-- 7. host.invalidate_cache()
-- 8. Rewrite SP / RA / PC through the FFI register pointer.
-- 9. Replace runtime.active last.
-- 10. Return the JSON envelope.
--
-- The two opens are an intentional test-discoverability choice. The
-- PARSE phase uses host.open_file (it is an existing Task 4 surface
-- also used by prime); the RELOAD phase uses host.open_new_elf (a
-- dedicated Task 5 method). In production both methods bind to
-- Support.File.open so the runtime cost is identical to a single open
-- — the distinction lives in the test log for ordering verification.
local function abi_mismatch_response(field, expected, actual)
return M.json_response({
ok = false, error = "state_abi_mismatch", field = field,
expected = expected, actual = actual,
restart_required = true,
})
end
function M.elf_reload(runtime, parsed)
-- 1. Pre-pause request validation. Pure-Lua, no host calls.
if not runtime.active then
return M.json_response({
ok = false, error = "not_primed", restart_required = false })
end
if parsed.target ~= runtime.active.target then
return M.json_response({
ok = false, error = "target_mismatch",
expected = runtime.active.target, actual = parsed.target,
restart_required = true })
end
if type(parsed.path) ~= "string" or parsed.path == "" then
return M.json_response({
ok = false, error = "missing_path",
restart_required = false })
end
-- 2. SMEM range check on `active` (the new ELF has not been
-- parsed yet; the ABI gate below enforces it cannot relocate).
local smem_phys = strip_kseg(runtime.active.smem_addr)
if smem_phys == nil or smem_phys < 0 or smem_phys > 0x1fffff then
return M.json_response({
ok = false, error = "smem_out_of_main_ram",
restart_required = true })
end
-- 3. PARSE PHASE — open + parse + ABI gate. On any rejection here,
-- only host.open_file has been called. Pause and downstream
-- mutations do NOT occur.
local elf_handle_for_parse = runtime.host.open_file(parsed.path)
if not elf_handle_for_parse then
return M.json_response({
ok = false, error = "open_file_failed",
restart_required = true })
end
local manifest, parse_err = M.parse_manifest(
elf_handle_for_parse, parsed.target, parsed.path, true)
if not manifest then
return M.json_response({
ok = false, error = parse_err,
restart_required = true })
end
local active = runtime.active
if manifest.smem_addr ~= active.smem_addr then
return abi_mismatch_response(
"smem_addr", active.smem_addr, manifest.smem_addr)
end
if manifest.smem_size ~= active.smem_size then
return abi_mismatch_response(
"smem_size", active.smem_size, manifest.smem_size)
end
if manifest.bss_start ~= active.bss_start then
return abi_mismatch_response(
"bss_start", active.bss_start, manifest.bss_start)
end
if manifest.bss_end ~= active.bss_end then
return abi_mismatch_response(
"bss_end", active.bss_end, manifest.bss_end)
end
-- 4. Pause + snapshot smem bytes.
runtime.host.pause()
local mem = runtime.host.memory_file()
local saved = mem:readAtToSlice(active.smem_size, smem_phys)
-- 5. RELOAD PHASE — second open for binary_load.
local elf_handle = runtime.host.open_new_elf(parsed.path)
if not elf_handle then
return M.json_response({
ok = false, error = "open_file_failed",
restart_required = true })
end
local loaded = runtime.host.binary_load(elf_handle, mem)
if loaded == nil then
-- Do NOT restore state; PCSX.Binary.load may have partially
-- written RAM. Keep ACTIVE untouched and tell the caller to
-- restart the emulator.
return M.json_response({
ok = false, error = "binary_load_failed",
restart_required = true })
end
-- 6. Restore the smem snapshot over the freshly-loaded code.
mem:writeAtMoveSlice(saved, smem_phys)
-- 7. Flush the CPU instruction cache (.text/.rodata changed).
runtime.host.invalidate_cache()
-- 8. Rewrite SP / RA / PC through the FFI register pointer. The
-- PC write must happen last; the CPU starts consuming
-- instructions at the new PC the moment the emulator resumes.
local regs = runtime.host.get_registers()
regs.GPR.n.sp = manifest.stack_top
regs.GPR.n.ra = 0
regs.pc = manifest.hot_reload_entry
-- 9. Replace ACTIVE last so a failed reload cannot poison the
-- next request's gate.
runtime.active = manifest
-- 10. Return the JSON envelope.
return M.json_response({
ok = true,
target = manifest.target,
elf_path = manifest.elf_path,
elf_entry = manifest.elf_entry,
smem_addr = manifest.smem_addr,
smem_size = manifest.smem_size,
bss_start = manifest.bss_start,
bss_end = manifest.bss_end,
data_start = manifest.data_start,
data_end = manifest.data_end,
hot_reload_entry = manifest.hot_reload_entry,
stack_top = manifest.stack_top,
})
end
-- patch_handler: one-word RAM patch through MemoryAsFile.
--
-- Per spec §7 + plan.md Task 5 Step 5, the order is:
-- 1. Parse addr and hex query parameters
-- 2. Reject non-hex / missing inputs
-- 3. Reject unaligned addresses (addr & 3)
-- 4. Normalize through strip_kseg; reject out-of-main-RAM
-- (scratchpad 0x1F800000+, BIOS 0x1FC00000+, MMIO, expansion)
-- 5. host.pause()
-- 6. mem = host.memory_file()
-- 7. mem:writeU32At(value, physical_offset)
-- 8. host.invalidate_cache()
-- 9. Return JSON envelope ok=true with the requested addr and value.
local function patch_error(err, restart)
return M.json_response({
ok = false, error = err,
restart_required = restart or false,
})
end
function M.patch_handler(runtime, parsed)
local addr_str = parsed.addr
local hex_str = parsed.hex
-- 1. Presence checks.
if type(addr_str) ~= "string" or addr_str == "" then
return patch_error("missing_addr", false)
end
if type(hex_str) ~= "string" or hex_str == "" then
return patch_error("missing_value", false)
end
-- 2. Hex parse.
local addr = parse_hex_u32(addr_str, "missing_addr", "non_hex_addr")
if not addr then
return patch_error(
addr == false and "missing_addr" or "non_hex_addr", false)
end
local value = parse_hex_u32(hex_str, "missing_value", "non_hex_value")
if not value then
return patch_error(
value == false and "missing_value" or "non_hex_value", false)
end
-- 3. Alignment (checked on the canonical KSEG/physical addr).
if addr % 4 ~= 0 then
return patch_error("addr_unaligned", false)
end
-- 4. Range check via strip_kseg (rejects KSEG0 > 0x801fffff, KSEG1 >
-- 0xa01fffff, scratchpad, BIOS, MMIO, expansion, etc.).
local phys = strip_kseg(addr)
if phys == nil then
return patch_error("addr_out_of_main_ram", false)
end
-- 5-8. Pause / write / cache invalidate.
runtime.host.pause()
local mem = runtime.host.memory_file()
mem:writeU32At(value, phys)
runtime.host.invalidate_cache()
-- 9. Return the JSON envelope. Echo the requested address and the
-- value in normalized hex so log captures stay stable across runs.
return M.json_response({
ok = true,
addr = addr_str,
value = "0x" .. string.format("%x", value),
})
end
-- Mode dispatch table. Each handler is invoked with (runtime, parsed).
-- Tasks 5 adds patch (M.patch_handler); the previous placeholder removed.
local DISPATCH = {
prime = M.prime_active,
elf = M.elf_reload,
patch = M.patch_handler,
}
-- Wrap a handler call with the busy guard. The guard is acquired only
-- after the mode is validated, so unknown-mode requests do not deadlock
-- the runtime. xpcall guarantees the guard is released even if the
-- handler throws.
local function with_busy_guard(runtime, fn)
if runtime.busy then
return M.json_response({
ok = false, error = "reload_busy", restart_required = false })
end
runtime.busy = true
-- Capture both the error text and a full Lua traceback so the user
-- can see the actual failing call site instead of a generic
-- "internal_error". debug.traceback("", 2) skips this xpcall frame
-- and the json_response frame so the trace starts at the handler.
local ok, result = xpcall(fn, function(e)
return { msg = tostring(e), tb = debug.traceback("", 2) }
end)
runtime.busy = false
if not ok then
return M.json_response({
ok = false, error = "internal_error",
detail = result.msg, tb = result.tb,
restart_required = true })
end
return result
end
function M.new(host)
if type(host) ~= "table" then
error("M.new: host must be a table, got " .. type(host))
end
local runtime = {
active = nil,
busy = false,
host = host,
}
function runtime:handle(req)
-- 1. Parse the query (M.parse_query returns nil, err on failure).
local query = req and req.urlData and req.urlData.query or ""
local parsed, parse_err = M.parse_query(query)
if not parsed then
return M.json_response({
ok = false, error = parse_err, restart_required = false })
end
-- 2. Validate the mode against the dispatch table.
local mode = parsed.mode
local handler = DISPATCH[mode]
if not handler then
return M.json_response({
ok = false, error = "unknown_mode", restart_required = false })
end
-- 3. Acquire busy and dispatch via xpcall. Mode validation
-- happens BEFORE busy is acquired so unknown-mode requests
-- cannot deadlock the runtime.
return with_busy_guard(self, function()
return handler(self, parsed)
end)
end
return runtime
end
-- Install the reload handler on a PCSX-Redux instance.
--
-- Per plan.md Task 5 Step 5 the adapter binds the canonical host method
-- names to the PCSX-Lua FFI surface:
--
-- pause -> PCSX.pauseEmulator
-- memory_file -> PCSX.getMemoryAsFile
-- open_file -> Support.File.open(path, "READ")
-- binary_load -> PCSX.Binary.load
-- invalidate_cache -> PCSX.invalidateCache
-- get_registers -> PCSX.getRegisters
--
-- The returned closure dispatches each request through M.new(host)'s
-- runtime:handle so the same prime/elf/patch dispatch machinery is used
-- (including the busy guard from Task 4).
--
-- Missing `PCSX.WebServer.Handlers` is created on demand so callers do
-- not have to wire that themselves; if `PCSX` or `Support` is absent a
-- single line is printed and the function returns without registering
-- a handler.
function M.install(pcsx, support)
if type(pcsx) ~= "table" then
print("[reload] install failed: PCSX is not a table")
return
end
if type(support) ~= "table"
or type(support.File) ~= "table"
or type(support.File.open) ~= "function" then
print("[reload] install failed: Support.File.open unavailable")
return
end
if type(pcsx.pauseEmulator) ~= "function" then print("[reload] install failed: PCSX.pauseEmulator missing"); return end
if type(pcsx.getMemoryAsFile) ~= "function" then print("[reload] install failed: PCSX.getMemoryAsFile missing"); return end
if type(pcsx.Binary) ~= "table"
or type(pcsx.Binary.load) ~= "function" then print("[reload] install failed: PCSX.Binary.load missing"); return end
if type(pcsx.invalidateCache) ~= "function" then print("[reload] install failed: PCSX.invalidateCache missing"); return end
if type(pcsx.getRegisters) ~= "function" then print("[reload] install failed: PCSX.getRegisters missing"); return end
-- ---------------------------------------------------------------------------
-- File adapter wrap.
--
-- The production pcsx-redux Support.File wrapper (see
-- toolchain/pcsx-redux/src/lua/fileffi.lua:225-232 + size() around line 203)
-- exposes byte-read methods as colon-syntax closures with camelCase names:
-- readU8At = function(self, pos) ... end
-- readU16At = function(self, pos) ... end
-- readU32At = function(self, pos) ... end
-- size = function(self) ... end
--
-- The ELF32 parser (scripts/elf32.lua) uses an explicit-pass shape with
-- snake_case names:
-- adapter.read_u8_at(off) / adapter.read_u16_at(off) /
-- adapter.read_u32_at(off) / adapter.read_size()
--
-- The install boundary wraps the Support.File return value in a thin
-- adapter whose methods forward to the production closures, stripping
-- the implicit `self` and re-exporting the names the parser validates.
-- Without this wrap, E.validate_adapter returns "bad_file_adapter"
-- because adapter.read_u8_at / read_u16_at / read_u32_at / read_size
-- are not present on the raw Support.File return.
local function wrap_file(f)
return {
read_u8_at = function(off) return f:readU8At(off) end,
read_u16_at = function(off) return f:readU16At(off) end,
read_u32_at = function(off) return f:readU32At(off) end,
read_size = function() return f:size() end,
}
end
local host = {
pause = function() pcsx.pauseEmulator() end,
memory_file = function() return pcsx.getMemoryAsFile() end,
open_file = function(path) return wrap_file(support.File.open(path, "READ")) end,
-- open_new_elf returns the raw Support.File object because the
-- RELOAD phase passes it directly to PCSX.Binary.load which
-- expects a real File (with readAt / size), NOT the elf32
-- parser adapter (read_u8_at / read_u16_at / read_u32_at /
-- read_size). Wrapping it in the adapter here triggers the
-- binffi.lua "Expected a File object as first argument" error.
open_new_elf = function(path) return support.File.open(path, "READ") end,
binary_load = function(elf, mem) return pcsx.Binary.load(elf, mem) end,
invalidate_cache = function() pcsx.invalidateCache() end,
get_registers = function() return pcsx.getRegisters() end,
}
local runtime = M.new(host)
if type(pcsx.WebServer) ~= "table" then pcsx.WebServer = {} end
if type(pcsx.WebServer.Handlers) ~= "table" then pcsx.WebServer.Handlers = {} end
pcsx.WebServer.Handlers.reload = function(req)
return runtime:handle(req)
end
print("[reload] handler installed: reload")
end
return M
+158 -247
View File
@@ -19,8 +19,8 @@
-- fall back to `debug.getinfo(1, "S").source` when this file is being dofile()'d or require()'d (in which case `arg[0]` is the *caller's* path). -- fall back to `debug.getinfo(1, "S").source` when this file is being dofile()'d or require()'d (in which case `arg[0]` is the *caller's* path).
-- That single statement: (a) sets `package.path` + `package.cpath`, (b) at the bottom returns `require("duffle")`. -- That single statement: (a) sets `package.path` + `package.cpath`, (b) at the bottom returns `require("duffle")`.
-- So the dofile's return value is the duffle module. -- So the dofile's return value is the duffle module.
local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil ---@type boolean local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil
local _bootstrap_src ---@type string local _bootstrap_src
if _is_entry_script then if _is_entry_script then
_bootstrap_src = arg[0] _bootstrap_src = arg[0]
else else
@@ -28,26 +28,26 @@ else
-- strip the leading "@" so the directory match works in both cases. -- strip the leading "@" so the directory match works in both cases.
_bootstrap_src = debug.getinfo(1, "S").source:sub(2) _bootstrap_src = debug.getinfo(1, "S").source:sub(2)
end end
local duffle = dofile((_bootstrap_src:match("(.*[/\\])") or "./") .. "duffle_paths.lua") ---@type DuffleExport local duffle = dofile((_bootstrap_src:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Constants -- Constants
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Exit codes (per the --help text and the post-build summary convention). -- Exit codes (per the --help text and the post-build summary convention).
local EXIT_OK = 0 ---@type integer local EXIT_OK = 0
local EXIT_VALIDATION_ERRORS = 1 ---@type integer local EXIT_VALIDATION_ERRORS = 1
local EXIT_INTERNAL_ERROR = 2 ---@type integer local EXIT_INTERNAL_ERROR = 2
-- Default --out-root value if not provided. -- Default --out-root value if not provided.
local DEFAULT_OUT_ROOT = "build/gen" ---@type string local DEFAULT_OUT_ROOT = "build/gen"
-- Sentinel for "all passes" in `PASS_FLAG_TO_NAME`. Distinguishes `--all` from the per-pass flags (which map to individual pass names). -- Sentinel for "all passes" in `PASS_FLAG_TO_NAME`. Distinguishes `--all` from the per-pass flags (which map to individual pass names).
local ALL_PASSES_SENTINEL = "__all__" ---@type string local ALL_PASSES_SENTINEL = "__all__"
-- Sentinel key for the pass-flag dispatcher in `FLAG_HANDLERS`. -- Sentinel key for the pass-flag dispatcher in `FLAG_HANDLERS`.
-- The actual pass names are looked up via `PASS_FLAG_TO_NAME`, not direct dispatch, so this key never matches a real flag. -- The actual pass names are looked up via `PASS_FLAG_TO_NAME`, not direct dispatch, so this key never matches a real flag.
local PASS_FLAG_DISPATCH_KEY = "__pass__" ---@type string local PASS_FLAG_DISPATCH_KEY = "__pass__"
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Type declarations -- Type declarations
@@ -60,98 +60,29 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__" ---@type string
--- @field deps string[] -- Names of upstream passes --- @field deps string[] -- Names of upstream passes
--- @field groups string[]? -- OPTIONAL build-phase groups this pass is a root of (e.g. { "pre-link" }, { "post-link" }); absent ⇒ dependency-only --- @field groups string[]? -- OPTIONAL build-phase groups this pass is a root of (e.g. { "pre-link" }, { "post-link" }); absent ⇒ dependency-only
--- @class Corpus --- @class SourceFile
--- @field unity_root string|nil --- @field path string -- Absolute path to the source file
--- @field project_root string --- @field text string -- Full source text
--- @field code_root string --- @field dir string -- Directory containing the source
--- @field source_order SourceFile[] --- @field basename string -- Filename without extension
--- @field sources_by_path table<Path, SourceFile>
--- @field sources_by_dir table<string, SourceFile[]>
--- @field atoms_by_name table<AtomName, AtomEntry>
--- @field binds_by_name table<string, BindsEntry>
--- @field atom_infos AtomInfoEntry[]
--- @field register_alias_registry table<string, AliasEntry>
--- @field type_name_registry table<string, TypeNameEntry>
--- @field atom_views table<AtomName, AtomViewEntry>
--- @field atom_ctxs table<AtomName, AtomCtxEntry>
--- @field atom_phases table<string, AtomPhaseGroup>
--- @field word_counts WordCounts
--- @field components table<string, Component>
--- @field atom_bundles table<string, AtomBundle>|nil
--- @field tape_emits TapeEmit[]|nil
--- @field collisions CorpusCollision[]
--- @field resolver SourceResolver
--- @field component_atom_infos AtomInfoEntry[]|nil
--- @field atom_auto_regs table<AtomName, table<string, string>>|nil
--- @field phase_auto_regs table<string, table<string, string>>|nil
--- @field reg_use_schemas table<string, RegUseSchema>|nil
--- @field reg_use_errors RegUseError[]|nil
--- @field static_analysis_results table<string, AtomAnalysis>|nil
--- @field tape_chains table<string, TapeChain>|nil
--- @class PassShared
--- @field corpus Corpus
--- @class PassFlags
--- @field gdb_runtime boolean|nil
--- @field dwarf_injection boolean|nil
--- @field elf_path string|nil
--- @class PassCtx --- @class PassCtx
--- @field metadata_path string -- Path to word_count.metadata.h --- @field metadata_path string -- Path to word_count.metadata.h
--- @field shared PassShared -- Cross-pass shared state --- @field shared table -- Cross-pass shared state
--- @field shared.corpus table -- Authored-source/project projection
--- @field out_root string -- Output root (e.g. "build/gen") --- @field out_root string -- Output root (e.g. "build/gen")
--- @field project_root string -- PS1 repository root --- @field project_root string -- PS1 repository root
--- @field flags PassFlags -- CLI flags + per-pass stash --- @field flags table -- CLI flags + per-pass stash
--- @field verbose boolean -- If true, log diagnostic info --- @field verbose boolean -- If true, log diagnostic info
--- CheckName: see static_analysis.lua. AtomName: see duffle.lua.
--- @class Finding --- @class Finding
--- @field line integer --- @field line integer -- Source line (or 0 for pass-level)
--- @field msg string --- @field msg string -- Finding message
--- @field kind string|nil -- error | warning | info
--- @field atom AtomName|nil
--- @field check CheckName|nil
--- @field source string|nil -- optional; emit/reguse path
--- @field schema_name string|nil -- optional; emit/reguse
--- @class PassScratch
--- @field corpus Corpus|nil
--- @field info_by_atom table<string, AtomInfoEntry>|nil
--- @field binds_index table<string, BindsEntry>|nil
--- @field atom_index table<string, AtomEntry>|nil
--- @field annot_counts table<string, integer>|nil -- bag
--- @field types table<string, RegTypeDefault>|nil
--- @field atom_views table<string, AtomViewEntry>|nil
--- @field seen_defaults table<string, integer>|nil -- bag
--- @field seen_field table<string, integer>|nil -- bag
--- @field _scan SourceScan|nil
--- @field word_counts WordCounts|nil
--- @field register_alias_registry table<string, AliasEntry>|nil
--- @field type_name_registry table<string, TypeNameEntry>|nil
--- @field type_occurrences RegTypeOccurrence[]|nil
--- @field atom_infos_list AtomInfoEntry[]|nil
--- @field binds_list BindsEntry[]|nil
--- @field unknown_seen table<string, integer>|nil -- bag
--- @field atoms AtomEntry[]|nil
--- @field components_by_name table<string, Component>|nil
--- @field atoms_by_name table<string, AtomEntry>|nil
--- @field tape_chains table<string, string[]>|nil
--- @field source_order SourceFile[]|nil
--- @field component_atom_infos AtomInfoEntry[]|nil
--- @field atom_infos_all AtomInfoEntry[]|nil
--- @field gte_cr_alias_groups GteCrAliasGroup[]|nil
--- @field line_for_word_event (fun(ev: WordEvent): integer)|nil
--- @class PassOutputEntry
--- @field kind string
--- @field path string
--- @class PassResult --- @class PassResult
--- @field outputs PassOutputEntry[] --- @field outputs PassOutputEntry[] -- Emitted file paths
--- @field errors Finding[] -- Build-stops (per-pass kind policy) --- @field errors Finding[] -- Build-stops (per-pass kind policy)
--- @field warnings Finding[] -- Informational --- @field warnings Finding[] -- Informational
--- @field info Finding[]|nil -- static_analysis only
--- @class ParsedArgs --- @class ParsedArgs
--- @field requested_set string[] -- Pass names to run (explicit --all expanded) --- @field requested_set string[] -- Pass names to run (explicit --all expanded)
@@ -161,18 +92,6 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__" ---@type string
--- @field out_root string -- --out-root value (default "build/gen") --- @field out_root string -- --out-root value (default "build/gen")
--- @field project_root string -- PS1 repository root (derived from metadata by default) --- @field project_root string -- PS1 repository root (derived from metadata by default)
--- @field verbose boolean -- If true, log diagnostic info --- @field verbose boolean -- If true, log diagnostic info
--- @field flags PassFlags|nil -- Per-pass stash; copied onto PassCtx.flags
--- @alias FlagHandler fun(args: ParsedArgs, argv: string[]|nil, arg_idx: integer|nil): integer|nil
--- @class PassModule
--- @field run fun(ctx: PassCtx): PassResult
--- @class Ps1MetaMod
--- @field PASSES table<string, PassDescriptor>
--- @field PASS_KIND_STOP_ON_ERROR table<string, boolean>
--- @field parse_args fun(argv: string[]): ParsedArgs
--- @field build_ctx fun(args: ParsedArgs): PassCtx
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- PASSES Table -- PASSES Table
@@ -185,7 +104,7 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__" ---@type string
-- A row without a `groups` entry is dependency-only: it runs only when a transitive dep requests it, -- A row without a `groups` entry is dependency-only: it runs only when a transitive dep requests it,
-- but it remains directly requestable through its explicit CLI flag (e.g. --atoms-source-map, --scan-source). -- but it remains directly requestable through its explicit CLI flag (e.g. --atoms-source-map, --scan-source).
local PASSES = { ---@type table<string, PassDescriptor> local PASSES = {
["scan-source"] = { ["scan-source"] = {
module = "passes.scan_source", module = "passes.scan_source",
kind = "shared", deps = {}, kind = "shared", deps = {},
@@ -199,12 +118,6 @@ local PASSES = { ---@type table<string, PassDescriptor>
kind = "header-output", kind = "header-output",
deps = {"scan-source", "word-counts"}, deps = {"scan-source", "word-counts"},
}, },
auto_reg = {
module = "passes.auto_reg",
kind = "header-output",
deps = {"components"},
groups = { "pre-link" },
},
["emission-model"] = { ["emission-model"] = {
module = "passes.emission_model", module = "passes.emission_model",
kind = "validation", kind = "validation",
@@ -223,7 +136,8 @@ local PASSES = { ---@type table<string, PassDescriptor>
}, },
["static-analysis"] = { ["static-analysis"] = {
module = "passes.static_analysis", module = "passes.static_analysis",
-- "diagnostic" — every `error`/`warning` finding is written to the report file. -- "diagnostic" — every `error`/`warning` finding is written to the report file;
-- The orchestrator does NOT exit non-zero on these findings (see PASS_KIND_STOP_ON_ERROR).
-- Report severity is independent from process exit policy. -- Report severity is independent from process exit policy.
kind = "diagnostic", kind = "diagnostic",
deps = {"scan-source", "word-counts", "components", "emission-model"}, deps = {"scan-source", "word-counts", "components", "emission-model"},
@@ -255,10 +169,10 @@ local PASSES = { ---@type table<string, PassDescriptor>
--- @param group_name string -- Build-phase group ("pre-link" | "post-link") --- @param group_name string -- Build-phase group ("pre-link" | "post-link")
--- @return string[] -- Sorted root pass names belonging to that group --- @return string[] -- Sorted root pass names belonging to that group
local function roots_for_group(group_name) local function roots_for_group(group_name)
local names = {} ---@type string[] local names = {}
for name, pass in pairs(PASSES) do ---@type string, PassDescriptor for name, pass in pairs(PASSES) do
if pass.groups then if pass.groups then
for _, g in ipairs(pass.groups) do ---@type integer, string for _, g in ipairs(pass.groups) do
if g == group_name then if g == group_name then
names[#names + 1] = name names[#names + 1] = name
break break
@@ -275,25 +189,27 @@ end
--- cannot silently fall through to pre-link (or any other default) and dispatch nothing. --- cannot silently fall through to pre-link (or any other default) and dispatch nothing.
--- @param args ParsedArgs --- @param args ParsedArgs
--- @param group_name string --- @param group_name string
--- @return nil
local function request_roots_for_group(args, group_name) local function request_roots_for_group(args, group_name)
local roots = roots_for_group(group_name) ---@type string[] local roots = roots_for_group(group_name)
if #roots == 0 then if #roots == 0 then
error(string.format("ps1_meta: build-phase group %q has zero roots in PASSES; check PASSES rows for a `groups = { %q }` field" error(string.format("ps1_meta: build-phase group %q has zero roots in PASSES; check PASSES rows for a `groups = { %q }` field"
, group_name, group_name)) , group_name, group_name))
end end
for _, name in ipairs(roots) do ---@type integer, string for _, name in ipairs(roots) do
args.requested_set[#args.requested_set + 1] = name args.requested_set[#args.requested_set + 1] = name
end end
end end
-- Pass-kind taxonomy: findings always print. No pass kind stops the build. -- Pass-kind taxonomy: Which kinds stop the build on errors?
--
-- Report severity is independent from process exit policy. -- Report severity is independent from process exit policy.
-- Adding a new pass kind requires listing it here explicitly; an unknown kind must not silently fall back to "true". -- A "diagnostic" pass still writes every `error`/`warning` finding into its report file,
local PASS_KIND_STOP_ON_ERROR = { ---@type table<string, boolean> -- bag: pass kind -> stop-on-error -- but `report_validation_errors` returns early for non-stopping kinds, so nothing is printed to stderr and the orchestrator does not exit non-zero.
-- Adding a new pass kind requires listing it here explicitly; An unknown kind must not silently fall back to "true".
local PASS_KIND_STOP_ON_ERROR = {
["shared"] = false, ["shared"] = false,
["header-output"] = false, ["header-output"] = true,
["validation"] = false, ["validation"] = true,
["diagnostic"] = false, ["diagnostic"] = false,
["report"] = false, ["report"] = false,
} }
@@ -302,7 +218,7 @@ local PASS_KIND_STOP_ON_ERROR = { ---@type table<string, boolean> -- bag: pass
-- Per-pass flags (e.g. --word-counts); phase flags (--pre-link, --post-link, --all) are within FLAG_HANDLERS because they own side effects or invoke group-derivation logic. -- Per-pass flags (e.g. --word-counts); phase flags (--pre-link, --post-link, --all) are within FLAG_HANDLERS because they own side effects or invoke group-derivation logic.
-- dwarf-injection is *also* a per-pass opt-in flag, but its selection + opt-in state are both owned by the explicit FLAG_HANDLERS entry below -- dwarf-injection is *also* a per-pass opt-in flag, but its selection + opt-in state are both owned by the explicit FLAG_HANDLERS entry below
-- (it sets args.flags.dwarf_injection and appends "dwarf-injection" to requested_set), so it is intentionally absent from this table. -- (it sets args.flags.dwarf_injection and appends "dwarf-injection" to requested_set), so it is intentionally absent from this table.
local PASS_FLAG_TO_NAME = { ---@type table<string, string> -- bag: CLI flag -> pass name or ALL_PASSES_SENTINEL local PASS_FLAG_TO_NAME = {
["--word-counts"] = "word-counts", ["--word-counts"] = "word-counts",
["--components"] = "components", ["--components"] = "components",
["--validate"] = "annotation", ["--validate"] = "annotation",
@@ -317,26 +233,24 @@ local PASS_FLAG_TO_NAME = { ---@type table<string, string> -- bag: CLI flag ->
--- Append every pass name to args.requested_set. --- Append every pass name to args.requested_set.
--- Names are derived from PASSES (no parallel name list); used by --all and by any caller that wants the full closure. --- Names are derived from PASSES (no parallel name list); used by --all and by any caller that wants the full closure.
--- @param args ParsedArgs --- @param args ParsedArgs
--- @return nil
local function request_all_passes(args) local function request_all_passes(args)
local names = {} ---@type string[] local names = {}
for name in pairs(PASSES) do names[#names + 1] = name end ---@type string for name in pairs(PASSES) do names[#names + 1] = name end
table.sort(names) table.sort(names)
for _, n in ipairs(names) do ---@type integer, string for _, n in ipairs(names) do
args.requested_set[#args.requested_set + 1] = n args.requested_set[#args.requested_set + 1] = n
end end
end end
-- Per-flag handlers. Each handler takes (args, argv, arg_idx) and returns the new arg_idx (so multi-arg flags like --source FILE advance it). -- Per-flag handlers. Each handler takes (args, argv, arg_idx) and returns the new arg_idx (so multi-arg flags like --source FILE advance it).
-- Returning nil + os.exit() handles termination flags (--help). -- Returning nil + os.exit() handles termination flags (--help).
local FLAG_HANDLERS = {} ---@type table<string, FlagHandler> local FLAG_HANDLERS = {}
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- CLI parsing -- CLI parsing
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- Print the CLI usage to stdout and exit 0. --- Print the CLI usage to stdout and exit 0.
--- @return nil
local function print_help() local function print_help()
io.write([[ io.write([[
ps1_meta.lua - Tape-atom metaprogram orchestrator ps1_meta.lua - Tape-atom metaprogram orchestrator
@@ -375,7 +289,8 @@ COMMON_FLAGS:
--help Show this help and exit --help Show this help and exit
EXIT CODES: EXIT CODES:
0 Ran. Findings print on stderr and in the report; they do not fail the process. 0 All requested passes succeeded
1 Validation errors found
2 Metaprogram internal error 2 Metaprogram internal error
EXAMPLES: EXAMPLES:
@@ -385,7 +300,7 @@ EXAMPLES:
]]) ]])
end end
local FLAG_VALUE_NAMES = { ---@type table<string, string> -- bag: flag -> value metavar local FLAG_VALUE_NAMES = {
["--source"] = "FILE", ["--source"] = "FILE",
["--unity-root"] = "FILE", ["--unity-root"] = "FILE",
["--metadata"] = "PATH", ["--metadata"] = "PATH",
@@ -394,14 +309,9 @@ local FLAG_VALUE_NAMES = { ---@type table<string, string> -- bag: flag -> value
["--elf"] = "PATH", ["--elf"] = "PATH",
} }
--- @param argv string[]
--- @param arg_idx integer
--- @param flag string
--- @return string
--- @return integer
local function require_flag_value(argv, arg_idx, flag) local function require_flag_value(argv, arg_idx, flag)
local value = argv[arg_idx + 1] ---@type string|nil local value = argv[arg_idx + 1]
local next_known = type(value) == "string" ---@type boolean local next_known = type(value) == "string"
and (FLAG_HANDLERS[value] ~= nil or PASS_FLAG_TO_NAME[value] ~= nil) and (FLAG_HANDLERS[value] ~= nil or PASS_FLAG_TO_NAME[value] ~= nil)
if value == nil or next_known then if value == nil or next_known then
io.stderr:write("ps1_meta: " .. flag .. " requires " .. FLAG_VALUE_NAMES[flag] .. "\n") io.stderr:write("ps1_meta: " .. flag .. " requires " .. FLAG_VALUE_NAMES[flag] .. "\n")
@@ -415,81 +325,49 @@ end
-- Populated AFTER print_help so the --help handler can reference it as an upvalue (Lua resolves locals at closure-call time, -- Populated AFTER print_help so the --help handler can reference it as an upvalue (Lua resolves locals at closure-call time,
-- but if the closure is defined before the local, it falls back to _G). -- but if the closure is defined before the local, it falls back to _G).
--- @param args ParsedArgs
--- @return nil
FLAG_HANDLERS["--help"] = function(args) print_help(); os.exit(0) end FLAG_HANDLERS["--help"] = function(args) print_help(); os.exit(0) end
--- @param args ParsedArgs
--- @return nil
FLAG_HANDLERS["--verbose"] = function(args) args.verbose = true end FLAG_HANDLERS["--verbose"] = function(args) args.verbose = true end
--- @param args ParsedArgs
--- @param argv string[]
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--source"] = function(args, argv, arg_idx) FLAG_HANDLERS["--source"] = function(args, argv, arg_idx)
local value, value_idx = require_flag_value(argv, arg_idx, "--source") ---@type string, integer local value, value_idx = require_flag_value(argv, arg_idx, "--source")
args.sources[#args.sources + 1] = value args.sources[#args.sources + 1] = value
return value_idx return value_idx
end end
--- @param args ParsedArgs
--- @param argv string[]
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--unity-root"] = function(args, argv, arg_idx) FLAG_HANDLERS["--unity-root"] = function(args, argv, arg_idx)
local value, value_idx = require_flag_value(argv, arg_idx, "--unity-root") ---@type string, integer local value, value_idx = require_flag_value(argv, arg_idx, "--unity-root")
args.unity_root = value args.unity_root = value
return value_idx return value_idx
end end
--- @param args ParsedArgs
--- @param argv string[]
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx) FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx)
local value, value_idx = require_flag_value(argv, arg_idx, "--metadata") ---@type string, integer local value, value_idx = require_flag_value(argv, arg_idx, "--metadata")
args.metadata = value args.metadata = value
return value_idx return value_idx
end end
--- @param args ParsedArgs
--- @param argv string[]
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx) FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx)
local value, value_idx = require_flag_value(argv, arg_idx, "--out-root") ---@type string, integer local value, value_idx = require_flag_value(argv, arg_idx, "--out-root")
args.out_root = value args.out_root = value
return value_idx return value_idx
end end
--- @param args ParsedArgs
--- @param argv string[]
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx) FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx)
local value, value_idx = require_flag_value(argv, arg_idx, "--project-root") ---@type string, integer local value, value_idx = require_flag_value(argv, arg_idx, "--project-root")
args.project_root = value args.project_root = value
return value_idx return value_idx
end end
-- Per-pass stash flags. Read by `passes/atoms_source_map.lua` to opt into the post-link gdb-runtime emission. -- Per-pass stash flags. Read by `passes/atoms_source_map.lua` to opt into the post-link gdb-runtime emission.
-- Same shape as the existing per-flag handlers. mutates `args.flags` (which propagates into `ctx.flags`). -- Same shape as the existing per-flag handlers. mutates `args.flags` (which propagates into `ctx.flags`).
--- @param args ParsedArgs
--- @return nil
FLAG_HANDLERS["--gdb-runtime"] = function(args) FLAG_HANDLERS["--gdb-runtime"] = function(args)
args.flags = args.flags or {} args.flags = args.flags or {}
args.flags.gdb_runtime = true args.flags.gdb_runtime = true
end end
--- @param args ParsedArgs
--- @param argv string[]
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx) FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx)
local value, value_idx = require_flag_value(argv, arg_idx, "--elf") ---@type string, integer local value, value_idx = require_flag_value(argv, arg_idx, "--elf")
args.flags = args.flags or {} args.flags = args.flags or {}
args.flags.elf_path = value args.flags.elf_path = value
return value_idx return value_idx
end end
-- Enable DWARF injection (default OFF). Opts in to the post-link pass and sets the flag in one shot. -- Enable DWARF injection (default OFF). Opts in to the post-link pass and sets the flag in one shot.
-- The explicit handler below owns both selection and opt-in state, so --dwarf-injection is intentionally absent from PASS_FLAG_TO_NAME. -- The explicit handler below owns both selection and opt-in state, so --dwarf-injection is intentionally absent from PASS_FLAG_TO_NAME.
--- @param args ParsedArgs
--- @return nil
FLAG_HANDLERS["--dwarf-injection"] = function(args) FLAG_HANDLERS["--dwarf-injection"] = function(args)
args.flags = args.flags or {} args.flags = args.flags or {}
args.flags.dwarf_injection = true args.flags.dwarf_injection = true
@@ -497,16 +375,12 @@ FLAG_HANDLERS["--dwarf-injection"] = function(args)
end end
-- Build-phase flags: --pre-link and --post-link request the roots of their declared groups (see roots_for_group). -- Build-phase flags: --pre-link and --post-link request the roots of their declared groups (see roots_for_group).
-- topo_sort closes transitive deps from those roots; dispatch_passes runs every pass in the resolved closure without phase-filtering. -- topo_sort closes transitive deps from those roots; dispatch_passes runs every pass in the resolved closure without phase-filtering.
--- @param args ParsedArgs
--- @return nil
FLAG_HANDLERS["--pre-link"] = function(args) FLAG_HANDLERS["--pre-link"] = function(args)
request_roots_for_group(args, "pre-link") request_roots_for_group(args, "pre-link")
end end
-- Batch post-link phase: gdb-runtime + dwarf-injection in one luajit cold start. -- Batch post-link phase: gdb-runtime + dwarf-injection in one luajit cold start.
-- Sets the same opt-in flags as --gdb-runtime + --dwarf-injection and selects the post-link build-phase group. -- Sets the same opt-in flags as --gdb-runtime + --dwarf-injection and selects the post-link build-phase group.
-- elf is required; parse_args enforces it after all flags are parsed. -- elf is required; parse_args enforces it after all flags are parsed.
--- @param args ParsedArgs
--- @return nil
FLAG_HANDLERS["--post-link"] = function(args) FLAG_HANDLERS["--post-link"] = function(args)
args.flags = args.flags or {} args.flags = args.flags or {}
args.flags.gdb_runtime = true args.flags.gdb_runtime = true
@@ -517,11 +391,8 @@ end
-- `--dwarf-injection` also emits atom-local debug data. -- `--dwarf-injection` also emits atom-local debug data.
-- Pass-flag handler. Reads the closed-set table, expands --all, appends to requested_set. -- Pass-flag handler. Reads the closed-set table, expands --all, appends to requested_set.
--- @param args ParsedArgs
--- @param a string
--- @return nil
FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a) FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a)
local name = PASS_FLAG_TO_NAME[a] ---@type string|nil local name = PASS_FLAG_TO_NAME[a]
if name == ALL_PASSES_SENTINEL then if name == ALL_PASSES_SENTINEL then
request_all_passes(args) request_all_passes(args)
return return
@@ -533,7 +404,7 @@ end
--- @param argv string[] --- @param argv string[]
--- @return ParsedArgs --- @return ParsedArgs
local function parse_args(argv) local function parse_args(argv)
local args = { ---@type ParsedArgs local args = {
requested_set = {}, requested_set = {},
sources = {}, sources = {},
unity_root = nil, unity_root = nil,
@@ -543,10 +414,10 @@ local function parse_args(argv)
verbose = false, verbose = false,
} }
local pos = 1 ---@type integer local pos = 1
while pos <= #argv do while pos <= #argv do
local a = argv[pos] ---@type string local a = argv[pos]
local handler = FLAG_HANDLERS[a] ---@type FlagHandler|nil local handler = FLAG_HANDLERS[a]
if handler then if handler then
pos = handler(args, argv, pos) or pos pos = handler(args, argv, pos) or pos
elseif PASS_FLAG_TO_NAME[a] then elseif PASS_FLAG_TO_NAME[a] then
@@ -571,14 +442,14 @@ local function parse_args(argv)
-- `<repo>/code/duffle/word_count.metadata.h` is the canonical metadata location. -- `<repo>/code/duffle/word_count.metadata.h` is the canonical metadata location.
-- `project_root` names `<repo>`; the resolver derives `<project_root>/code` separately. -- `project_root` names `<repo>`; the resolver derives `<project_root>/code` separately.
if not args.project_root then if not args.project_root then
local metadata_dir = duffle.dirname(duffle.normalize_path(args.metadata)) ---@type string local metadata_dir = duffle.dirname(duffle.normalize_path(args.metadata))
local code_root = duffle.dirname(metadata_dir) ---@type string local code_root = duffle.dirname(metadata_dir)
args.project_root = duffle.dirname(code_root) args.project_root = duffle.dirname(code_root)
else else
args.project_root = duffle.normalize_path(args.project_root) args.project_root = duffle.normalize_path(args.project_root)
end end
local has_unity = type(args.unity_root) == "string" and args.unity_root ~= "" ---@type boolean local has_unity = type(args.unity_root) == "string" and args.unity_root ~= ""
if has_unity and #args.sources > 0 then if has_unity and #args.sources > 0 then
io.stderr:write("ps1_meta: --unity-root FILE and --source FILE are mutually exclusive\n") io.stderr:write("ps1_meta: --unity-root FILE and --source FILE are mutually exclusive\n")
os.exit(EXIT_INTERNAL_ERROR) os.exit(EXIT_INTERNAL_ERROR)
@@ -591,10 +462,10 @@ local function parse_args(argv)
-- Post-link opt-ins (--gdb-runtime, --dwarf-injection) write output that depends on the linked ELF. -- Post-link opt-ins (--gdb-runtime, --dwarf-injection) write output that depends on the linked ELF.
-- Without --elf the metaprogram can't satisfy those requests, so refuse loud and early. -- Without --elf the metaprogram can't satisfy those requests, so refuse loud and early.
-- This covers the explicit --post-link batch, --dwarf-injection by itself, and --gdb-runtime by itself. -- This covers the explicit --post-link batch, --dwarf-injection by itself, and --gdb-runtime by itself.
local flags = args.flags or {} ---@type PassFlags local flags = args.flags or {}
local elf_path = flags.elf_path ---@type string|nil local elf_path = flags.elf_path
local has_elf = type(elf_path) == "string" and #elf_path > 0 ---@type boolean local has_elf = type(elf_path) == "string" and #elf_path > 0
local post_links = flags.gdb_runtime or flags.dwarf_injection ---@type boolean local post_links = flags.gdb_runtime or flags.dwarf_injection
if post_links and not has_elf then if post_links and not has_elf then
io.stderr:write("ps1_meta: --elf PATH is required for post-link output\n") io.stderr:write("ps1_meta: --elf PATH is required for post-link output\n")
os.exit(EXIT_INTERNAL_ERROR) os.exit(EXIT_INTERNAL_ERROR)
@@ -613,9 +484,9 @@ end
--- @param args ParsedArgs --- @param args ParsedArgs
--- @return PassCtx --- @return PassCtx
local function build_ctx(args) local function build_ctx(args)
local normalized_project_root = duffle.normalize_path(args.project_root) ---@type string local normalized_project_root = duffle.normalize_path(args.project_root)
local project_root = normalized_project_root ---@type string local project_root = normalized_project_root
local project_root_is_absolute = normalized_project_root:match("^%a:/") ---@type boolean local project_root_is_absolute = normalized_project_root:match("^%a:/")
or normalized_project_root:sub(1, 2) == "//" or normalized_project_root:sub(1, 2) == "//"
or normalized_project_root:sub(1, 1) == "/" or normalized_project_root:sub(1, 1) == "/"
if not project_root_is_absolute then if not project_root_is_absolute then
@@ -626,9 +497,9 @@ local function build_ctx(args)
-- Do not route POSIX/UNC/drive-absolute paths through to_absolute_path. -- Do not route POSIX/UNC/drive-absolute paths through to_absolute_path.
duffle.canonical_path_key(project_root) duffle.canonical_path_key(project_root)
end end
local resolution ---@type Corpus local resolution
if args.unity_root then if args.unity_root then
local ok_resolve, resolved = pcall(duffle.resolve_source_corpus, { ---@type boolean, Corpus|string local ok_resolve, resolved = pcall(duffle.resolve_source_corpus, {
unity_root = args.unity_root, unity_root = args.unity_root,
project_root = project_root, project_root = project_root,
}) })
@@ -638,18 +509,59 @@ local function build_ctx(args)
end end
resolution = resolved resolution = resolved
else else
local ok_exact, exact = pcall(duffle.resolve_exact_sources, { ---@type boolean, Corpus|string local source_order = {}
sources = args.sources, local sources_by_path = {}
project_root = project_root, local resolver = {
}) resolved = {},
if not ok_exact then skipped = {},
io.stderr:write("ps1_meta: cannot resolve --source: " .. tostring(exact) .. "\n") shadowed = {},
}
for _, input_path in ipairs(args.sources) do
local path = duffle.normalize_path(input_path)
local key_ok, key_or_error = pcall(duffle.canonical_path_key, path)
if not key_ok then
error("ps1_meta: invalid --source " .. input_path .. ": " .. tostring(key_or_error), 0)
end
local file = io.open(path, "r")
if not file then
io.stderr:write("ps1_meta: cannot open --source " .. input_path .. "\n")
os.exit(EXIT_INTERNAL_ERROR) os.exit(EXIT_INTERNAL_ERROR)
end end
resolution = exact local text = file:read("*a")
file:close()
local source = {
path = path,
text = text,
dir = duffle.dirname(path),
basename = duffle.basename_no_ext(path),
}
source_order[#source_order + 1] = source
local key = key_or_error
if not sources_by_path[key] then sources_by_path[key] = source end
resolver.resolved[#resolver.resolved + 1] = {
include_path = path,
include_text = nil,
root_source = nil,
root_line = nil,
candidate_a = path,
candidate_b = nil,
selected_path = path,
disposition = "exact",
}
end
resolution = {
unity_root = nil,
project_root = project_root,
code_root = duffle.normalize_path(project_root .. "/code"),
source_order = source_order,
sources_by_path = sources_by_path,
sources_by_dir = duffle.group_sources_by_dir(source_order),
resolver = resolver,
}
end end
local corpus = { ---@type Corpus local corpus = {
unity_root = resolution.unity_root, unity_root = resolution.unity_root,
project_root = resolution.project_root, project_root = resolution.project_root,
code_root = resolution.code_root, code_root = resolution.code_root,
@@ -666,12 +578,11 @@ local function build_ctx(args)
atom_phases = {}, atom_phases = {},
word_counts = {}, word_counts = {},
components = {}, components = {},
atom_bundles = {}, component_body_index = {},
tape_emits = {},
collisions = {}, collisions = {},
resolver = resolution.resolver, resolver = resolution.resolver,
} }
local ctx = { ---@type PassCtx local ctx = {
metadata_path = args.metadata, metadata_path = args.metadata,
shared = { corpus = corpus }, shared = { corpus = corpus },
out_root = args.out_root, out_root = args.out_root,
@@ -700,15 +611,15 @@ end
--- Keeping these blocks local makes the topological sort self-contained. --- Keeping these blocks local makes the topological sort self-contained.
local function topo_sort(passes, requested_set) local function topo_sort(passes, requested_set)
-- Dependency closure: include every pass transitively required by `requested_set`. -- Dependency closure: include every pass transitively required by `requested_set`.
local needed = {} ---@type table<string, boolean> -- bag: pass name -> needed local needed = {}
for _, name in ipairs(requested_set) do needed[name] = true end ---@type integer, string for _, name in ipairs(requested_set) do needed[name] = true end
local changed = true ---@type boolean local changed = true
while changed do while changed do
changed = false changed = false
for name, _ in pairs(needed) do ---@type string, boolean for name, _ in pairs(needed) do
local pass = passes[name] ---@type PassDescriptor local pass = passes[name]
if not pass then error("unknown pass '" .. name .. "' requested") end if not pass then error("unknown pass '" .. name .. "' requested") end
for _, dep in ipairs(pass.deps) do ---@type integer, string for _, dep in ipairs(pass.deps) do
if not needed[dep] then if not needed[dep] then
needed[dep] = true needed[dep] = true
changed = true changed = true
@@ -718,10 +629,10 @@ local function topo_sort(passes, requested_set)
end end
-- In-degree calculation: count each needed pass's needed dependencies. -- In-degree calculation: count each needed pass's needed dependencies.
local in_degree = {} ---@type table<string, integer> -- bag: pass name -> in-degree local in_degree = {}
for name, _ in pairs(needed) do in_degree[name] = 0 end ---@type string, boolean for name, _ in pairs(needed) do in_degree[name] = 0 end
for name, _ in pairs(needed) do ---@type string, boolean for name, _ in pairs(needed) do
for _, dep in ipairs(passes[name].deps) do ---@type integer, string for _, dep in ipairs(passes[name].deps) do
if needed[dep] then if needed[dep] then
in_degree[name] = in_degree[name] + 1 in_degree[name] = in_degree[name] + 1
end end
@@ -729,21 +640,21 @@ local function topo_sort(passes, requested_set)
end end
-- Ready-queue seeding: add zero-in-degree passes in deterministic order. -- Ready-queue seeding: add zero-in-degree passes in deterministic order.
local ready = {} ---@type string[] local ready = {}
for name, deg in pairs(in_degree) do ---@type string, integer for name, deg in pairs(in_degree) do
if deg == 0 then ready[#ready + 1] = name end if deg == 0 then ready[#ready + 1] = name end
end end
table.sort(ready) table.sort(ready)
-- Ready-queue drain: decrement dependents when each pass is emitted. -- Ready-queue drain: decrement dependents when each pass is emitted.
-- Newly-zero-degree passes are inserted back into the ready queue (kept sorted). -- Newly-zero-degree passes are inserted back into the ready queue (kept sorted).
local order = {} ---@type string[] local order = {}
while #ready > 0 do while #ready > 0 do
local just_finished = table.remove(ready, 1) ---@type string local just_finished = table.remove(ready, 1)
order[#order + 1] = just_finished order[#order + 1] = just_finished
for name, _ in pairs(needed) do ---@type string, boolean for name, _ in pairs(needed) do
if name ~= just_finished then if name ~= just_finished then
for _, dep in ipairs(passes[name].deps) do ---@type integer, string for _, dep in ipairs(passes[name].deps) do
if dep == just_finished then if dep == just_finished then
in_degree[name] = in_degree[name] - 1 in_degree[name] = in_degree[name] - 1
if in_degree[name] == 0 then if in_degree[name] == 0 then
@@ -759,10 +670,10 @@ local function topo_sort(passes, requested_set)
-- Cycle detection: if `order` doesn't include all needed passes, some are stuck with in_degree > 0 -- Cycle detection: if `order` doesn't include all needed passes, some are stuck with in_degree > 0
-- (the cycle closed on itself before Kahn could process them). -- (the cycle closed on itself before Kahn could process them).
-- Without this check, a fully-closed cycle (e.g. A -> B -> A) would silently return an empty order list, leaving the orchestrator to dispatch nothing. -- Without this check, a fully-closed cycle (e.g. A -> B -> A) would silently return an empty order list, leaving the orchestrator to dispatch nothing.
local needed_count = 0 ---@type integer local needed_count = 0
for _ in pairs(needed) do needed_count = needed_count + 1 end ---@type string -- count hash entries; Lua's #t doesn't work for _ in pairs(needed) do needed_count = needed_count + 1 end -- count hash entries; Lua's #t doesn't work
if #order ~= needed_count then if #order ~= needed_count then
for name, deg in pairs(in_degree) do ---@type string, integer for name, deg in pairs(in_degree) do
if deg > 0 then if deg > 0 then
error("dependency cycle detected involving pass '" .. name .. "'") error("dependency cycle detected involving pass '" .. name .. "'")
end end
@@ -776,19 +687,19 @@ end
-- Main Orchestrator -- Main Orchestrator
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- (internal) Write every pass error to stderr. --- (internal) If the pass's kind is in PASS_KIND_STOP_ON_ERROR and it reported errors, write each error to stderr.
--- Returns true only when the pass kind still stops the build. --- Returns true if any validation errors were reported.
--- @param pass_name string --- @param pass_name string
--- @param pass PassDescriptor --- @param pass PassDescriptor
--- @param result PassResult --- @param result PassResult
--- @return boolean --- @return boolean
local function report_validation_errors(pass_name, pass, result) local function report_validation_errors(pass_name, pass, result)
local has_errors = result.errors and #result.errors > 0 ---@type boolean local has_errors = result.errors and #result.errors > 0
if not has_errors then return false end if not (has_errors and PASS_KIND_STOP_ON_ERROR[pass.kind]) then return false end
for _, e in ipairs(result.errors) do ---@type integer, Finding for _, e in ipairs(result.errors) do
io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or "")) io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or ""))
end end
return PASS_KIND_STOP_ON_ERROR[pass.kind] == true return true
end end
--- (internal) Run each pass in `order` in topological sequence. --- (internal) Run each pass in `order` in topological sequence.
@@ -796,11 +707,11 @@ end
--- @param order string[] --- @param order string[]
--- @return boolean -- true if any validation errors were reported --- @return boolean -- true if any validation errors were reported
local function dispatch_passes(ctx, order) local function dispatch_passes(ctx, order)
local had_errors = false ---@type boolean local had_errors = false
for _, pass_name in ipairs(order) do ---@type integer, string for _, pass_name in ipairs(order) do
local pass = PASSES[pass_name] ---@type PassDescriptor local pass = PASSES[pass_name]
local mod = require(pass.module) ---@type PassModule local mod = require(pass.module)
local result = mod.run(ctx) ---@type PassResult local result = mod.run(ctx)
if report_validation_errors(pass_name, pass, result) then if report_validation_errors(pass_name, pass, result) then
had_errors = true had_errors = true
end end
@@ -810,16 +721,16 @@ end
--- Main entry point. Runs the requested passes in dep-topological order. --- Main entry point. Runs the requested passes in dep-topological order.
--- @param argv string[] --- @param argv string[]
--- @return nil
local function main(argv) local function main(argv)
local ok, err = pcall(function() ---@type boolean, string|nil local ok, err = pcall(function()
local args = parse_args(argv) ---@type ParsedArgs local args = parse_args(argv)
local ctx = build_ctx(args) ---@type PassCtx local ctx = build_ctx(args)
local requested = args.requested_set ---@type string[] local requested = args.requested_set
local closed = topo_sort(PASSES, requested) ---@type string[] local closed = topo_sort(PASSES, requested)
dispatch_passes(ctx, closed) local had_errors = dispatch_passes(ctx, closed)
if had_errors then os.exit(EXIT_VALIDATION_ERRORS) end
end) end)
if not ok then if not ok then
@@ -833,7 +744,7 @@ end
-- Module export for in-process consumers (tests that dofile this script). -- Module export for in-process consumers (tests that dofile this script).
-- The conditional `main(...)` call below only fires when this file is invoked as the entry script (arg[0] ends in "ps1_meta.lua"); -- The conditional `main(...)` call below only fires when this file is invoked as the entry script (arg[0] ends in "ps1_meta.lua");
-- in dofile() mode (test's arg[0] does not match), main() is skipped and the chunk returns `_M` to the caller. -- in dofile() mode (test's arg[0] does not match), main() is skipped and the chunk returns `_M` to the caller.
local _M = { ---@type Ps1MetaMod local _M = {
PASSES = PASSES, PASSES = PASSES,
PASS_KIND_STOP_ON_ERROR = PASS_KIND_STOP_ON_ERROR, PASS_KIND_STOP_ON_ERROR = PASS_KIND_STOP_ON_ERROR,
parse_args = parse_args, parse_args = parse_args,
+82
View File
@@ -0,0 +1,82 @@
# scripts/reload.ps1
#
# PCSX-Redux Lua helper reload client.
#
# Modes:
# elf - Request a full ELF reload. Requires -ElfPath.
# patch - Request a single-word RAM patch. Requires -Address and -Word.
#
# -RequestOnly prints the URI and exits before any network I/O.
# -Quiet suppresses the compact-JSON printout on the real path.
[CmdletBinding()]
param(
[ValidateSet('elf', 'patch')][string]$Mode = 'elf',
[string]$Target = 'hello_camera',
[string]$ElfPath = '',
[string]$Address = '',
[string]$Word = '',
[int]$Port = 8080,
[switch]$RequestOnly,
[switch]$Quiet
)
# mode-specific argument guards
switch ($Mode) {
'patch' {
if ([string]::IsNullOrEmpty($Address) -or [string]::IsNullOrEmpty($Word)) {
Write-Error "patch mode requires both -Address and -Word"
exit 1
}
}
'elf' {
if ([string]::IsNullOrEmpty($ElfPath)) {
Write-Error "elf mode requires -ElfPath"
exit 1
}
}
}
# Build the URL-encoded query string.
$queryParts = New-Object System.Collections.Generic.List[string]
[void]$queryParts.Add("mode=$([uri]::EscapeDataString($Mode))")
[void]$queryParts.Add("target=$([uri]::EscapeDataString($Target))")
switch ($Mode) {
'elf' {
[void]$queryParts.Add("path=$([uri]::EscapeDataString($ElfPath))")
}
'patch' {
[void]$queryParts.Add("addr=$([uri]::EscapeDataString($Address))")
[void]$queryParts.Add("hex=$([uri]::EscapeDataString($Word))")
}
}
$uri = "http://localhost:$Port/api/v1/lua/reload?$($queryParts -join '&')"
# RequestOnly path: emit URI and return before any network I/O.
if ($RequestOnly) {
Write-Output $uri
return
}
# Real request path: POST, decode body if it is a byte array, parse JSON.
$response = Invoke-WebRequest -Method Post -Uri $uri
if ($response.Content -is [byte[]]) {
$text = [System.Text.Encoding]::UTF8.GetString([byte[]]$response.Content)
}
else {
$text = [string]$response.Content
}
$obj = $text | ConvertFrom-Json
if (-not $Quiet) {
$obj | ConvertTo-Json -Compress | Write-Output
}
if (-not $obj.ok) {
$errCode = if ($obj.error) { [string]$obj.error } else { 'unknown' }
throw "Reload failed: $errCode"
}
-118
View File
@@ -14,21 +14,16 @@ $url_armips = 'https://github.com/Kingcom/armips.git'
$url_pcsx_redux = 'https://github.com/grumpycoders/pcsx-redux.git' $url_pcsx_redux = 'https://github.com/grumpycoders/pcsx-redux.git'
$url_psyq_iwyu = 'https://github.com/johnbaumann/psyq_include_what_you_use.git' $url_psyq_iwyu = 'https://github.com/johnbaumann/psyq_include_what_you_use.git'
$url_lpeg = 'https://github.com/roberto-ieru/LPeg.git' $url_lpeg = 'https://github.com/roberto-ieru/LPeg.git'
# $url_mkpsxiso = 'https://github.com/Lameguy64/mkpsxiso.git'
$url_mkpsxiso_win64 = 'https://github.com/Lameguy64/mkpsxiso/releases/download/v2.30/mkpsxiso-2.30-win64.zip'
$path_armips = join-path $path_toolchain 'armips' $path_armips = join-path $path_toolchain 'armips'
$path_pcsx_redux = join-path $path_toolchain 'pcsx-redux' $path_pcsx_redux = join-path $path_toolchain 'pcsx-redux'
$path_psyq_iwyu = join-path $path_toolchain 'psyq_iwyu' $path_psyq_iwyu = join-path $path_toolchain 'psyq_iwyu'
$path_lpeg = join-path $path_toolchain 'lpeg' $path_lpeg = join-path $path_toolchain 'lpeg'
$path_mkpsxiso = join-path $path_toolchain 'mkpsxiso'
clone-gitrepo $path_armips $url_armips clone-gitrepo $path_armips $url_armips
clone-gitrepo $path_lpeg $url_lpeg clone-gitrepo $path_lpeg $url_lpeg
clone-gitrepo $path_pcsx_redux $url_pcsx_redux clone-gitrepo $path_pcsx_redux $url_pcsx_redux
clone-gitrepo $path_psyq_iwyu $url_psyq_iwyu clone-gitrepo $path_psyq_iwyu $url_psyq_iwyu
# clone-gitrepo $path_mkpsxiso $url_mkpsxiso
$path_armips_build = join-path $path_armips 'build' $path_armips_build = join-path $path_armips 'build'
verify-path $path_armips_build verify-path $path_armips_build
@@ -61,110 +56,6 @@ if (-not $msbuild_exe) {
} }
$path_pcsx_sln = join-path $path_pcsx_redux 'vsprojects\pcsx-redux.sln' $path_pcsx_sln = join-path $path_pcsx_redux 'vsprojects\pcsx-redux.sln'
# ════════════════════════════════════════════════════════════════════════════
# NuGet restore — required before MSBuild.
# pcsx-redux's .vcxproj files use the legacy packages.config style with
# hardcoded `<Import Project="..\packages\{id}.{ver}\...">` directives.
# MSBuild's `/t:Restore` won't fetch missing packages here (the local
# packages\ dir is checked but no package-source lookup happens), and
# `dotnet restore` errors on packages.config projects, so we walk every
# packages.config, parse out the <package id version/> entries, and pull
# any missing .nupkg directly from api.nuget.org's flat container.
# ════════════════════════════════════════════════════════════════════════════
$path_pcsx_packages = join-path $path_pcsx_redux 'vsprojects\packages'
$nuget_flat_container = 'https://api.nuget.org/v3-flatcontainer'
# Collect required (id, version) pairs from every packages.config.
$required_packages = @{}
Get-ChildItem -Path (join-path $path_pcsx_redux 'vsprojects') -Filter 'packages.config' -Recurse -ErrorAction SilentlyContinue |
ForEach-Object {
[xml]$xml = Get-Content -LiteralPath $_.FullName -Raw
foreach ($pkg in $xml.packages.package) {
$key = '{0}|{1}' -f $pkg.id, $pkg.version
$required_packages[$key] = @{ id = $pkg.id; version = $pkg.version }
}
}
# Ensure the packages root exists.
if (-not (Test-Path -LiteralPath $path_pcsx_packages)) {
New-Item -ItemType Directory -Path $path_pcsx_packages -Force | Out-Null
}
# Download anything missing.
# Skip the package entirely if its dir already has any contents (the legacy packages.config style means the targets file location varies per package
# — `luajit.native` puts it at build/native/, `glfw` puts it elsewhere — so we can't probe a specific path; just check whether the dir is non-empty).
Add-Type -AssemblyName System.IO.Compression.FileSystem
foreach ($pkg in $required_packages.Values) {
$pkgDir = Join-Path $path_pcsx_packages ('{0}.{1}' -f $pkg.id, $pkg.version)
if ((Test-Path -LiteralPath $pkgDir) -and `
(@(Get-ChildItem -LiteralPath $pkgDir -Recurse -ErrorAction SilentlyContinue).Count -gt 0)) {
continue
}
$url = '{0}/{1}/{2}/{1}.{2}.nupkg' -f $nuget_flat_container, $pkg.id, $pkg.version
$nupkg = Join-Path $pkgDir ('{0}.{1}.nupkg' -f $pkg.id, $pkg.version)
New-Item -ItemType Directory -Path $pkgDir -Force | Out-Null
Write-Host "Fetching NuGet package: $($pkg.id) $($pkg.version)"
try {
Invoke-WebRequest -Uri $url -OutFile $nupkg -UseBasicParsing -ErrorAction Stop
[System.IO.Compression.ZipFile]::ExtractToDirectory($nupkg, $pkgDir)
Remove-Item -LiteralPath $nupkg -Force
} catch {
$msg = $_.Exception.Message
if ($msg -match '404') {
Write-Host " Not on nuget.org (vendored?) — skipping $url"
} else {
Write-Warning "Failed to fetch $url$msg"
}
if (Test-Path -LiteralPath $nupkg) { Remove-Item -LiteralPath $nupkg -Force }
}
}
# ════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
# isoffi.lua size guard — `core.vcxproj` #includes src/core/isoffi.lua into luaiso.cc via the `-- lualoader, R"EOF(...)EOF"` trick.
# The raw string literal between R"EOF(-- and -- )EOF" must stay under ~16,379 bytes or MSVC (19.44) fails with C2026 (its actual raw-string limit is 16,384, minus 5 bytes for the `-- lualoader, ` prefix).
# If the upstream file grows past that, trim it: remove license header, trailing whitespace, blank separators, inline comments, and shrink 4-space indent to 2-space.
# Idempotent — only writes when the raw string exceeds the limit.
# ════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
$path_isoffi = join-path $path_pcsx_redux 'src\core\isoffi.lua'
if (Test-Path -LiteralPath $path_isoffi) {
$content = Get-Content -LiteralPath $path_isoffi -Raw -Encoding utf8
$startMarker = $content.IndexOf('R"EOF(--')
$endMarker = $content.IndexOf('-- )EOF"')
$literalLen = if ($startMarker -ge 0 -and $endMarker -gt $startMarker) { $endMarker - ($startMarker + 8) } else { -1 }
# Effective MSVC raw-string limit for the lualoader prefix is 16379 bytes.
if ($literalLen -gt 16379) {
Write-Host "isoffi.lua raw string is $literalLen bytes (>16379); trimming for MSVC C2026 limit."
$lines = $content -split "`n"
$markerIdx = -1
for ($i = 0; $i -lt $lines.Length; $i++) {
if ($lines[$i] -match '^-- \)EOF"') { $markerIdx = $i; break }
}
$newLines = @()
for ($i = 0; $i -lt $lines.Length; $i++) {
$lineNum = $i + 1
$line = $lines[$i]
# Keep the first line and the EOF-marker line untouched.
if ($i -eq 0 -or $i -eq $markerIdx) { $newLines += $line; continue }
# Drop the GPL license header (lines 2-17).
if ($lineNum -ge 2 -and $lineNum -le 17) { continue }
# Drop blank separator lines.
if ($line -match '^\s*$') { continue }
# Drop trailing whitespace.
$line = $line -replace '\s+$', ''
# Drop inline comments (anything from `--` to end of line).
$line = $line -replace '\s*--.*$', ''
# Shrink 4-space indent to 2-space.
$line = $line -replace '^( )', ' '
if ($line -match '^\s*$') { continue }
$newLines += $line
}
($newLines -join "`n") | Out-File -LiteralPath $path_isoffi -Encoding utf8 -NoNewline
$newLen = ((Get-Content -LiteralPath $path_isoffi -Raw -Encoding utf8) -replace '.*R"EOF\(--', '' -replace '-- \)EOF".*', '').Length
Write-Host "isoffi.lua trimmed: $literalLen -> $newLen bytes of raw string content."
}
}
& $msbuild_exe $path_pcsx_sln /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=v143 /m /v:minimal & $msbuild_exe $path_pcsx_sln /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=v143 /m /v:minimal
# Locate luajit via scoop. `luajit.exe` is on PATH via scoop's shim; # Locate luajit via scoop. `luajit.exe` is on PATH via scoop's shim;
@@ -221,15 +112,6 @@ $lfs_dll_import = join-path $luajit_lib_dir 'libluajit-5.1.dll.a'
# ════════════════════════════════════════════════════════════════════════════ # ════════════════════════════════════════════════════════════════════════════
$path_openbios = join-path $path_pcsx_redux 'src\mips\openbios' $path_openbios = join-path $path_pcsx_redux 'src\mips\openbios'
# Wipe stale *.dep files across src\mips.
# These cache absolute paths to the GCC headers directory; if the toolchain was upgraded (e.g. v14.2.0 → v16.1.0)
# Make reads the stale paths and aborts with "no rule to make target .../stddef.h".
# `make clean` in openbios only clears its own dir — subdirs like common/crt0/, modplayer/, and shell/ keep their stale .dep files.
# Easier to just delete the lot before each build than to teach every Makefile about deepclean recursion.
Get-ChildItem -Path (join-path $path_pcsx_redux 'src\mips') -Recurse -Filter '*.dep' -ErrorAction SilentlyContinue |
ForEach-Object { Remove-Item -LiteralPath $_.FullName -Force }
push-location $path_openbios push-location $path_openbios
& make clean & make clean
& make & make