4 Commits
Author SHA1 Message Date
Emil 93946c1d24 Refresh Lua Release evidence after verifier review
Native and manual checks / native (ubuntu-24.04) (push) Failing after 34s
Native and manual checks / manual (push) Successful in 29s
Windows editor and software Vulkan / windows-graphics (push) Canceled after 0s
Native and manual checks / native (windows-2025) (push) Canceled after 0s
2026-09-24 02:35:14 +03:00
Emil 7c777eba7c Harden Lua export verification against stale or incomplete evidence 2026-09-24 02:35:14 +03:00
Emil af836785d3 Qualify Windows UTF-8 test entry point 2026-09-24 02:34:12 +03:00
Emil ba8bf45dbc Add repeatable synthetic Editor input latency probe 2026-09-24 02:30:41 +03:00
13 changed files with 1084 additions and 935 deletions
+3
View File
@@ -47,5 +47,8 @@ if(TARGET faset_ui AND TARGET faset_editor_session AND TARGET faset_scene_view)
set_tests_properties(editor_ui_gizmos PROPERTIES LABELS "gpu")
add_test(NAME editor_ui_authoring COMMAND faset_editor_ui_tests)
set_tests_properties(editor_ui_authoring PROPERTIES LABELS "gpu")
add_executable(faset_editor_ui_latency ${PROJECT_SOURCE_DIR}/tests/editor_ui_latency.cpp)
target_link_libraries(faset_editor_ui_latency PRIVATE faset_editor_ui)
target_compile_definitions(faset_editor_ui_latency PRIVATE FASET_TEST_ENGINE="${PROJECT_SOURCE_DIR}")
endif()
endif()
+1 -1
View File
@@ -57,7 +57,7 @@ were installed. This proves linkage and CPU validation, not graphics compatibili
## September 24 Release follow-up
The [Release execution record](lua-release-2026-09-24/README.md) closes the
Linux offscreen export gap above. At source commit `846f1f4`, a Release Editor
Linux offscreen export gap above. At source commit `4331857`, a Release Editor
exported the checked-in Lua-only sample into a disposable Unicode-path project.
The package was relocated, the source project path was hidden, and its Player
validated and rendered 120 frames on an NVIDIA GeForce RTX 2080 Ti with driver
@@ -1,7 +1,7 @@
# Lua-only Release game: relocated execution
This is a local Linux result for source revision
`846f1f459d61ee0f878ab9ad116ac0e75d74d588` on 2026-09-24. The
`433185717e1f324acb6eaf186818589d11697453` on 2026-09-24. The
[machine-readable report](report.json) records the exact source input hashes,
Release package, Editor binary hash, command arguments and elapsed times. The
Editor itself was built with the `linux-release` preset. The exported Player
@@ -25,6 +25,10 @@ resource/timing definitions; the [validate](lua-validate.json),
results. The [PNG preview](lua.png) is a lossless conversion of the checked
PPM.
The verifier also confirmed a clean Git source tree, rejected undeclared
package files and unexpected C++ script files, classified the selected device
as physical, and found the sample Lua `on_start` log in the relocated run.
To repeat this check from a built checkout, choose a **new, empty** output
directory:
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
{
"asset_generations": {},
"build_fingerprint": "55f8ff15cb5e4f241fcf7ee436a52a5a1eed36f2031f12bf44070276d796927e",
"build_fingerprint": "b745490f85f05987d849f6bb6f4d06953883f4b8e10c5551fadf164c7eec5857",
"configuration": "Release",
"executable": "faset_player",
"files": [
@@ -136,7 +136,7 @@
},
{
"path": "faset_player",
"sha256": "c9f626134e0742eb015da1c02ca2587471f30261042830631f55307b1ec4ac71",
"sha256": "99d7b8286a9151ec4eff9a9899e862d987048b031e1e40ca1938646afbde37bd",
"size": 6329192
},
{
@@ -166,7 +166,7 @@
}
],
"format": "faset.export",
"generation": "38d65abf-1c6d-4e6e-850a-5a4916e36b16",
"generation": "aa31bf16-984e-4854-b46f-15655bd6cc67",
"lua_enabled": true,
"lua_fingerprint": "a9261789bb1e0ca38bd40b08b0454165cd51454cba3ad4725dd357812ced6e35",
"platform": "linux",
File diff suppressed because it is too large Load Diff
@@ -1,16 +1,16 @@
{
"arguments": [
"/tmp/faset-playable-exports-yz70irb6/Faset Café 世界/lua/faset_player",
"/tmp/faset-playable-exports-8wzkrc_u/Faset Café 世界/lua/faset_player",
"--headless",
"--frames",
"120",
"--capture",
"/tmp/faset-playable-exports-yz70irb6/Faset Café 世界/lua/verification.ppm",
"/tmp/faset-playable-exports-8wzkrc_u/Faset Café 世界/lua/verification.ppm",
"--profile",
"/tmp/faset-playable-exports-yz70irb6/Faset Café 世界/lua/profile.json"
"/tmp/faset-playable-exports-8wzkrc_u/Faset Café 世界/lua/profile.json"
],
"cwd": "/tmp/faset-playable-exports-yz70irb6/empty-working-directory",
"seconds": 0.6078518449794501,
"cwd": "/tmp/faset-playable-exports-8wzkrc_u/empty-working-directory",
"seconds": 0.6288121230027173,
"exit_code": 0,
"timed_out": false,
"stdout": "{\"device\":\"NVIDIA GeForce RTX 2080 Ti\",\"dimension\":2,\"effective_visibility_mode\":\"direct\",\"frames\":120,\"gpu_visibility_active\":false,\"ticks\":120,\"validation_errors\":0,\"visibility_mode\":\"direct\"}\n",
@@ -1,10 +1,10 @@
{
"arguments": [
"/tmp/faset-playable-exports-yz70irb6/Faset Café 世界/lua/faset_player",
"/tmp/faset-playable-exports-8wzkrc_u/Faset Café 世界/lua/faset_player",
"--validate"
],
"cwd": "/tmp/faset-playable-exports-yz70irb6/empty-working-directory",
"seconds": 0.0062960259965620935,
"cwd": "/tmp/faset-playable-exports-8wzkrc_u/empty-working-directory",
"seconds": 0.007359832990914583,
"exit_code": 0,
"timed_out": false,
"stdout": "{\"dimension\":2,\"validated\":true}\n",
@@ -1,14 +1,17 @@
{
"format": "faset.playable-export-verification",
"version": 1,
"started_utc": "2026-09-23T23:20:41.969369+00:00",
"started_utc": "2026-09-23T23:30:12.719805+00:00",
"platform": "linux",
"engine": "/home/emil/Desktop/.worktrees/Faset_Engine-p1-lua-export",
"editor": "/home/emil/Desktop/.worktrees/Faset_Engine-p1-lua-export/build/linux-release/faset_editor",
"standalone_root": "/tmp/faset-playable-exports-yz70irb6",
"engine_revision": "846f1f459d61ee0f878ab9ad116ac0e75d74d588",
"standalone_root": "/tmp/faset-playable-exports-8wzkrc_u",
"engine_revision": "433185717e1f324acb6eaf186818589d11697453",
"engine_dirty": false,
"engine_git_status": [],
"engine_diff_sha256": null,
"editor_sha256": "03ac75942f5f1d29e9d6fedb21ecffc6c74f4299401dbe84f01ab1b4e9c176a0",
"project_workspace": "/tmp/faset-playable-projects-6xvbjr4b",
"project_workspace": "/tmp/faset-playable-projects-cuhq4iwr",
"vulkaninfo_available": true,
"driver_override": null,
"frames_per_game": 120,
@@ -44,14 +47,14 @@
"sha256": "7e4c0781c1837208b4499a9991ffb702143edf1f933ae9c7e3d2543fd2577747"
}
],
"generation": "38d65abf-1c6d-4e6e-850a-5a4916e36b16",
"generation": "aa31bf16-984e-4854-b46f-15655bd6cc67",
"configuration": "Release",
"standalone_directory": "/tmp/faset-playable-exports-yz70irb6/Faset Café 世界/lua",
"standalone_directory": "/tmp/faset-playable-exports-8wzkrc_u/Faset Café 世界/lua",
"executable": "faset_player",
"package_file_count": 32,
"asset_generations": {},
"device": "NVIDIA GeForce RTX 2080 Ti",
"validation_enabled": true,
"device_class": "physical",
"driver": {
"apiVersion": "1.4.329",
"driverVersion": "595.84.0.0",
@@ -60,56 +63,57 @@
"driverName": "NVIDIA",
"driverInfo": "595.84"
},
"validation_enabled": true,
"validation_errors": 0,
"completed_frames": 120,
"summary_ms": {
"gpu": {
"max": 0.585024,
"min": 0.558816,
"p50": 0.560416,
"p95": 0.577312,
"max": 0.584992,
"min": 0.551712,
"p50": 0.5608,
"p95": 0.574592,
"samples": 120
},
"render_call": {
"max": 4.487375,
"min": 1.483498,
"p50": 1.856402,
"p95": 2.145527,
"max": 4.928888,
"min": 1.544123,
"p50": 1.919881,
"p95": 2.423702,
"samples": 120
},
"renderer_cpu": {
"max": 4.483287,
"min": 1.480312,
"p50": 1.851913,
"p95": 2.141489,
"max": 4.923196,
"min": 1.540666,
"p50": 1.915753,
"p95": 2.420426,
"samples": 120
},
"renderer_readback_cpu": {
"max": 2.631884,
"min": 0.383573,
"p50": 0.508409,
"p95": 0.57264,
"max": 2.837363,
"min": 0.395756,
"p50": 0.504853,
"p95": 0.641169,
"samples": 120
},
"simulation": {
"max": 0.21704,
"max": 0.210056,
"min": 0.084289,
"p50": 0.122371,
"p95": 0.17531,
"p50": 0.122261,
"p95": 0.171062,
"samples": 120
},
"snapshot": {
"max": 0.113824,
"min": 0.046728,
"p50": 0.067627,
"p95": 0.102965,
"max": 0.254931,
"min": 0.046518,
"p50": 0.065754,
"p95": 0.087906,
"samples": 120
},
"wall": {
"max": 4.747145,
"min": 1.658538,
"p50": 2.05683,
"p95": 2.394717,
"max": 5.322149,
"min": 1.702973,
"p50": 2.142903,
"p95": 2.634219,
"samples": 120
}
},
@@ -126,5 +130,5 @@
"requested_projects": [
"lua"
],
"finished_utc": "2026-09-23T23:22:48.305164+00:00"
"finished_utc": "2026-09-23T23:32:18.151669+00:00"
}
+1 -1
View File
@@ -239,7 +239,7 @@ int test_main(int argc, char** argv) {
}
#ifdef _WIN32
int wmain(int argc, wchar_t** argv) {
return run_utf8_main(argc, argv, test_main);
return faset::run_utf8_main(argc, argv, test_main);
}
#else
int main(int argc, char** argv) {
+70
View File
@@ -0,0 +1,70 @@
#include <chrono>
#include <cmath>
#include <faset/core/io.hpp>
#include <faset/editor/editor_ui.hpp>
#include <iostream>
#include <stdexcept>
int main(int argc, char** argv) {
using namespace faset;
using Clock = std::chrono::steady_clock;
if (argc != 2) {
std::cerr << "Usage: faset_editor_ui_latency OUTPUT.json\n";
return 2;
}
const auto root = std::filesystem::temp_directory_path() /
path_from_utf8("faset-ui-latency-" + new_id());
try {
std::filesystem::create_directories(root);
atomic_write_json(root / "project.faset.json",
{{"format", "faset.project"},
{"version", 1},
{"name", "UI latency fixture"},
{"dimension", 3}});
editor::Session session({root, path_from_utf8(FASET_TEST_ENGINE), root});
render::Renderer renderer({1280, 800, "Editor input latency", true, true});
editor::EditorUI ui(session, renderer,
path_from_utf8(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf",
path_from_utf8(FASET_TEST_ENGINE) / "assets/ui/dark.json");
ui.frame({});
Json samples = Json::array();
constexpr int warmup = 10, measured = 100;
for (int index = 0; index < warmup + measured; ++index) {
render::Event event;
event.type = render::Event::Type::KeyDown;
event.key = index % 2 == 0 ? "E" : "W";
const auto started = Clock::now();
ui.frame({event});
renderer.render(ui.snapshot());
const auto elapsed = std::chrono::duration<double, std::milli>(Clock::now() - started)
.count();
const auto target = index % 2 == 0 ? "gizmo-Rotate" : "gizmo-Move";
if (!ui.widgets().find(target)->selected ||
renderer.stats().validation_errors != 0 || !std::isfinite(elapsed) ||
elapsed <= 0)
throw std::runtime_error("Synthetic input was not visible in a valid frame");
if (index >= warmup)
samples.push_back({{"iteration", index - warmup + 1},
{"milliseconds", elapsed},
{"gizmo", event.key == "E" ? "Rotate" : "Move"}});
}
atomic_write_json(path_from_utf8(argv[1]),
{{"format", "faset.editor-ui-latency"},
{"version", 1},
{"warmup_frames", warmup},
{"measured_frames", measured},
{"presentation_mode", "offscreen"},
{"method", "KeyDown gizmo toggle through EditorUI::frame and "
"offscreen Vulkan render completion; no window present"},
{"device", renderer.stats().device},
{"validation_enabled", renderer.stats().validation_enabled},
{"validation_errors", renderer.stats().validation_errors},
{"samples", samples}});
std::filesystem::remove_all(root);
std::cout << "Measured " << measured << " synthetic Editor input frames\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << "\nFixture: " << path_to_utf8(root) << '\n';
return 1;
}
}
+33 -4
View File
@@ -5,8 +5,10 @@ import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
from verify_playable_exports import (project_matrix, project_workspace, verify_capture,
verify_lua_export, vulkan_device_details)
from verify_playable_exports import (device_class, project_matrix, project_workspace,
sha256, verify_capture, verify_lua_export,
verify_lua_execution, verify_package,
vulkan_device_details)
class ExportProjectMatrixTests(unittest.TestCase):
@@ -42,10 +44,12 @@ class ExportProjectMatrixTests(unittest.TestCase):
verify_lua_export(root, manifest)
with self.assertRaisesRegex(RuntimeError, "license"):
verify_lua_export(root, {**manifest, "files": manifest["files"][:1]})
(root / "Scripts/Gameplay.cpp").write_text("// stray\n", encoding="utf-8")
(root / "Scripts/GameplayExtra.cpp").write_text("// stray\n", encoding="utf-8")
manifest["files"].append({"path": "Scripts/GameplayExtra.cpp"})
with self.assertRaisesRegex(RuntimeError, "C\\+\\+"):
verify_lua_export(root, manifest)
(root / "Scripts/Gameplay.cpp").unlink()
(root / "Scripts/GameplayExtra.cpp").unlink()
manifest["files"].pop()
(root / "Notices/lua/LICENSE.txt").unlink()
with self.assertRaisesRegex(RuntimeError, "license"):
verify_lua_export(root, manifest)
@@ -70,6 +74,31 @@ class ExportProjectMatrixTests(unittest.TestCase):
{"deviceName": "Discrete GPU", "driverVersion": "123",
"driverName": "Vendor", "driverInfo": "1.2.3"})
self.assertEqual(vulkan_device_details(summary, "Unknown"), {})
self.assertEqual(device_class("SwiftShader Device", {}), "software")
self.assertEqual(device_class("NVIDIA GeForce RTX 2080 Ti", {}), "physical")
self.assertEqual(device_class("Mystery Device", {}), "unknown")
def test_package_rejects_undeclared_file(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "Scripts").mkdir()
(root / "Scripts/player.lua").write_text("return {}\n", encoding="utf-8")
file = root / "Scripts/player.lua"
manifest = {"format": "faset.export", "version": 1,
"configuration": "Release", "files": [
{"path": "Scripts/player.lua", "size": file.stat().st_size,
"sha256": sha256(file)}]}
import json
(root / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
verify_package(root)
(root / "Scripts/GameplayExtra.cpp").write_text("// stray\n", encoding="utf-8")
with self.assertRaisesRegex(RuntimeError, "undeclared"):
verify_package(root)
def test_lua_execution_requires_on_start_marker(self):
verify_lua_execution("Lua player ready: A/D move, Space jump, E reset\n")
with self.assertRaisesRegex(RuntimeError, "on_start"):
verify_lua_execution("")
if __name__ == "__main__":
+47 -8
View File
@@ -53,6 +53,27 @@ def vulkan_device_details(summary: str, device: str) -> dict:
return {}
def device_class(device: str, driver: dict) -> str:
identity = " ".join((device, driver.get("driverName", ""), driver.get("driverInfo", ""))).lower()
if any(name in identity for name in ("swiftshader", "llvmpipe", "lavapipe", "softpipe")):
return "software"
if any(name in identity for name in ("nvidia", "geforce", "radeon", "amd", "intel", "arc")):
return "physical"
return "unknown"
def git_identity(engine: Path) -> dict:
def git(*arguments: str) -> str:
return subprocess.run(["git", "-C", str(engine), *arguments], check=True,
capture_output=True, text=True).stdout
revision = git("rev-parse", "HEAD").strip()
status = git("status", "--porcelain", "--untracked-files=normal")
diff = git("diff", "--binary", "HEAD")
return {"revision": revision, "dirty": bool(status),
"status_lines": status.splitlines(),
"diff_sha256": hashlib.sha256(diff.encode("utf-8")).hexdigest() if diff else None}
def run(arguments: list[str | Path], cwd: Path, log: Path, timeout: int = 1800) -> dict:
command = [str(argument) for argument in arguments]
print(f"{log.stem}: {subprocess.list2cmdline(command)}", flush=True)
@@ -105,6 +126,11 @@ def verify_package(directory: Path) -> dict:
require(path.is_file() and not path.is_symlink(), f"Missing packaged file: {path}")
require(path.stat().st_size == entry["size"] and sha256(path) == entry["sha256"],
f"Packaged file checksum mismatch: {path}")
declared = [entry["path"] for entry in manifest["files"]]
actual = {path.relative_to(directory).as_posix() for path in directory.rglob("*")
if path.is_file() or path.is_symlink()}
require(len(declared) == len(set(declared)) and actual == set(declared) | {"manifest.json"},
"Package has undeclared, duplicate or missing files")
return manifest
@@ -153,13 +179,20 @@ def verify_lua_export(directory: Path, manifest: dict) -> None:
require("Notices/lua/LICENSE.txt" in {entry["path"] for entry in manifest["files"]} and
(directory / "Notices/lua/LICENSE.txt").is_file(),
"Lua export is missing its license notice")
require(not (directory / "Scripts/Gameplay.cpp").exists() and
not (directory / "Scripts/Gameplay.hpp").exists(),
require(not any(path.startswith("Scripts/") and
path.lower().endswith((".cpp", ".cc", ".cxx", ".hpp", ".hh", ".h"))
for path in (entry["path"] for entry in manifest["files"])),
"Lua-only export unexpectedly contains C++ gameplay source")
require(not (directory / ".luarc.json").exists(),
require(not any(path.endswith(".luarc.json") for path in
(entry["path"] for entry in manifest["files"])),
"Lua-only export includes Editor language-server configuration")
def verify_lua_execution(stderr: str) -> None:
require("Lua player ready: A/D move, Space jump, E reset" in stderr,
"Lua gameplay on_start did not execute in the relocated Player")
def main() -> int:
# Keep captured CI logs portable even when the Windows console uses a legacy code page.
sys.stdout.reconfigure(encoding="utf-8")
@@ -189,8 +222,7 @@ def main() -> int:
evidence, projects = output / "evidence", workspace / "Faset Café 世界"
evidence.mkdir()
projects.mkdir()
revision = subprocess.run(["git", "-C", str(engine), "rev-parse", "HEAD"],
capture_output=True, text=True, check=True).stdout.strip()
source_identity = git_identity(engine)
vulkan_summary = ""
if shutil.which("vulkaninfo"):
try:
@@ -208,7 +240,11 @@ def main() -> int:
report = {"format": "faset.playable-export-verification", "version": 1,
"started_utc": datetime.now(timezone.utc).isoformat(), "platform": sys.platform,
"engine": str(engine), "editor": str(editor), "standalone_root": str(standalone),
"engine_revision": revision, "editor_sha256": sha256(editor),
"engine_revision": source_identity["revision"],
"engine_dirty": source_identity["dirty"],
"engine_git_status": source_identity["status_lines"],
"engine_diff_sha256": source_identity["diff_sha256"],
"editor_sha256": sha256(editor),
"project_workspace": str(workspace),
"vulkaninfo_available": bool(vulkan_summary),
"driver_override": os.environ.get("VK_DRIVER_FILES") or os.environ.get("VK_ICD_FILENAMES"),
@@ -272,6 +308,8 @@ def main() -> int:
capture, profile = relocated / "verification.ppm", relocated / "profile.json"
rendered = run([player, "--headless", "--frames", "120", "--capture", capture,
"--profile", profile], working, evidence / f"{name}-run.json", 300)
if item["language"] == "lua":
verify_lua_execution(rendered["stderr"])
summary = json.loads(rendered["stdout"].strip().splitlines()[-1])
require(summary["frames"] == 120 and summary["dimension"] == dimension and
summary["validation_errors"] == 0, "Standalone GPU run reported an error")
@@ -282,8 +320,9 @@ def main() -> int:
len(measured["samples"]) == 120, "Invalid or incomplete frame profile")
require(all(frame["gpu_allocated_bytes"] > 0 for frame in measured["samples"]),
"Frame profile lacks Vulkan allocation measurements")
item.update({"device": measured["device"], "validation_enabled": measured["validation_enabled"],
"driver": vulkan_device_details(vulkan_summary, measured["device"]),
driver = vulkan_device_details(vulkan_summary, measured["device"])
item.update({"device": measured["device"], "device_class": device_class(measured["device"], driver),
"driver": driver, "validation_enabled": measured["validation_enabled"],
"validation_errors": 0, "completed_frames": 120,
"summary_ms": measured["summary_ms"],
"capture": verify_capture(capture, language=item["language"]),