mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
[CI] validate Magma cached object lifetime scenarios with Vulkan layers
This commit is contained in:
@@ -1509,6 +1509,11 @@ jobs:
|
||||
python3 scripts/ci/magma_runahead_checks.py validation --build-dir build-split \
|
||||
--out "${RUNNER_TEMP}/magma-runahead-syncval" --execute
|
||||
|
||||
- name: Magma cached views and framebuffers pass Vulkan synchronization validation
|
||||
run: |
|
||||
python3 scripts/ci/magma_cache_checks.py --build-dir build-split \
|
||||
--out "${RUNNER_TEMP}/magma-cache-syncval" --execute
|
||||
|
||||
- name: Upload Magma run-ahead control and validation evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -1517,6 +1522,7 @@ jobs:
|
||||
path: |
|
||||
${{ runner.temp }}/magma-runahead-negative
|
||||
${{ runner.temp }}/magma-runahead-syncval
|
||||
${{ runner.temp }}/magma-cache-syncval
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Magma P7 native mip format debt stays a named refusal
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run exactly seven CTest-discovered Magma cache cases under Vulkan sync validation.
|
||||
|
||||
Preparation is inert: --execute is required before discovery, log deletion or tests.
|
||||
Run only while other host GPU lanes that share these private logs are idle.
|
||||
This script does not build, modify source, or operate a device.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
CASES = (
|
||||
"SamplerSwizzleAtoBtoAKeepsEachQueuedPixel",
|
||||
"SamplerRespecifyKeepsOldAndNewNativeImages",
|
||||
"SamplerViewsDistinguishMipLayerAndViewType",
|
||||
"FramebuffersAtoBtoAKeepDistinctAttachments",
|
||||
"FramebufferReattachmentDistinguishesMipAndLayer",
|
||||
"FramebufferSrgbPolicyAtoBtoAUsesDistinctFormats",
|
||||
"FramebufferRespecifyRebindsTheNewNativeImage",
|
||||
)
|
||||
PREFIX = "DirectVulkan.Split.Caches."
|
||||
REQUIRED = {PREFIX + "MagmaWireCacheScenario." + name for name in CASES}
|
||||
SETTINGS = {
|
||||
"VK_INSTANCE_LAYERS": "VK_LAYER_KHRONOS_validation",
|
||||
"VK_LAYER_ENABLES": "VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION_EXT",
|
||||
"VK_LOADER_DEBUG": "layer",
|
||||
}
|
||||
REQUIRED_ENV = {
|
||||
"MOBILEGL_BACKEND_TYPE": "DirectVulkan",
|
||||
"MOBILEGL_TRANSPORT": "inproc",
|
||||
"MOBILEGL_IPC_ROLE_SPLIT_STATE": "1",
|
||||
"MOBILEGL_IPC_STRICT_ERRORS": "1",
|
||||
"MOBILEGL_IPC_RUN_AHEAD": "1",
|
||||
"MOBILEGL_IPC_PRESENT_CREDIT": "3",
|
||||
"MGITEST_MAGMA_CACHES_LANE": "1",
|
||||
"MGITEST_SPLIT_LANE": "1",
|
||||
"MOBILEGL_ITEST_REQUIRE_GPU": "1",
|
||||
}
|
||||
|
||||
|
||||
def sha256(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def write_json(path, value):
|
||||
path.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def load_checker(source):
|
||||
path = source / "scripts/ci/magma_runahead_checks.py"
|
||||
spec = importlib.util.spec_from_file_location("magma_shared_checker", path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"cannot import repository checker: {path}")
|
||||
checker = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(checker)
|
||||
return checker, path
|
||||
|
||||
|
||||
def discover(build, out, checker):
|
||||
result = subprocess.run(
|
||||
["ctest", "--test-dir", str(build), "-L", "^integration-magma-caches$",
|
||||
"--show-only=json-v1"], capture_output=True, text=True, timeout=60)
|
||||
(out / "discovery.json").write_text(result.stdout, encoding="utf-8")
|
||||
(out / "discovery.stderr.txt").write_text(result.stderr, encoding="utf-8")
|
||||
if result.returncode:
|
||||
raise RuntimeError(f"CTest discovery returned {result.returncode}")
|
||||
rows = json.loads(result.stdout)["tests"]
|
||||
tests = {test["name"]: test for test in rows}
|
||||
if len(rows) != 7 or len(tests) != 7 or set(tests) != REQUIRED:
|
||||
raise RuntimeError(f"exact-seven discovery mismatch: missing={sorted(REQUIRED-set(tests))}, "
|
||||
f"extra={sorted(set(tests)-REQUIRED)}, rows={len(rows)}")
|
||||
executables = {}
|
||||
private_logs = set()
|
||||
for name, test in tests.items():
|
||||
command, declared, cwd, _ = checker.recipe(test)
|
||||
wrong = {key: declared.get(key) for key, expected in REQUIRED_ENV.items()
|
||||
if declared.get(key) != expected}
|
||||
if wrong:
|
||||
raise RuntimeError(f"{name}: required strict cache environment changed: {wrong}")
|
||||
private = Path(declared["MOBILEGL_LOG_FILE_PATH"])
|
||||
private = (cwd / private).resolve() if not private.is_absolute() else private.resolve()
|
||||
if not private.is_relative_to(build) or private in private_logs:
|
||||
raise RuntimeError(f"private library log is not unique and inside the build: {private}")
|
||||
private_logs.add(private)
|
||||
executable = Path(command[0]).resolve(strict=True)
|
||||
executables[str(executable)] = sha256(executable)
|
||||
if len(executables) != 1:
|
||||
raise RuntimeError(f"cache cases do not share one executable: {list(executables)}")
|
||||
return tests, executables
|
||||
|
||||
|
||||
def inspect_exact_case(directory, expected):
|
||||
# The repository's inspect() intentionally admits only its run-ahead cases.
|
||||
# Preserve the same no-skip/execution checks with this lane's exact identity.
|
||||
document = ET.parse(directory / "gtest.xml").getroot()
|
||||
cases = document.findall(".//testcase")
|
||||
if len(cases) != 1:
|
||||
raise RuntimeError(f"expected exactly one testcase, found {len(cases)}")
|
||||
case = cases[0]
|
||||
actual = f"{case.get('classname')}.{case.get('name')}"
|
||||
if actual != expected.removeprefix(PREFIX):
|
||||
raise RuntimeError(f"executed testcase {actual!r} does not match {expected!r}")
|
||||
return case
|
||||
|
||||
|
||||
def validate(tests, build, out, checker, executable_hashes):
|
||||
outcomes = []
|
||||
junit = ET.Element("testsuite", name="MagmaWireCacheSynchronizationValidation")
|
||||
for name in sorted(REQUIRED):
|
||||
directory = out / name
|
||||
result = {"ctest_name": name, "verdict": "FAILED_CHECK"}
|
||||
copied = None
|
||||
try:
|
||||
# Shared code uses the discovered argv/ENVIRONMENT/WORKING_DIRECTORY,
|
||||
# applies overrides AFTER CTest's values, and saves executable SHA,
|
||||
# effective recipe, raw XML, merged stdout/stderr, and private log.
|
||||
result.update(checker.run_case(tests[name], build, directory, SETTINGS))
|
||||
stdout = (directory / "stdout.txt").read_text(encoding="utf-8", errors="replace")
|
||||
private = (directory / "private.log").read_text(encoding="utf-8", errors="replace")
|
||||
validation, application = checker.split_diagnostics(stdout, private)
|
||||
(directory / "validation-errors.txt").write_text("\n".join(validation), encoding="utf-8")
|
||||
(directory / "application-errors.txt").write_text("\n".join(application), encoding="utf-8")
|
||||
result.update(validation_error_lines=len(validation), application_error_lines=len(application),
|
||||
layer_inserted=bool(checker.LAYER.search(stdout + "\n" + private)))
|
||||
# Application ERROR diagnostics (including the existing stencil caps
|
||||
# messages) are retained separately by the repository classifier.
|
||||
# Embedded validation/VUID/SYNC-HAZARD and loader ERROR remain fatal.
|
||||
case = inspect_exact_case(directory, name)
|
||||
copied = copy.deepcopy(case)
|
||||
copied.set("name", name)
|
||||
junit.append(copied)
|
||||
skipped = (case.find("skipped") is not None or case.get("status") != "run" or
|
||||
case.get("result") != "completed")
|
||||
result["testcase_completed_without_skip"] = not skipped
|
||||
if skipped:
|
||||
raise RuntimeError("selected cache testcase skipped or did not complete")
|
||||
if result["returncode"] or case.find("failure") is not None or case.find("error") is not None:
|
||||
raise RuntimeError("cache testcase did not pass with process exit zero")
|
||||
if not result["private_log_present"]:
|
||||
raise RuntimeError("actual private library log is missing")
|
||||
if not result["layer_inserted"]:
|
||||
raise RuntimeError("loader output does not prove Khronos validation-layer insertion")
|
||||
if validation:
|
||||
raise RuntimeError("VUID / synchronization hazard / validation or loader error found")
|
||||
recipe = json.loads((directory / "recipe.json").read_text(encoding="utf-8"))
|
||||
executable = str(Path(recipe["command"][0]).resolve(strict=True))
|
||||
if recipe["executable_sha256"] != executable_hashes[executable]:
|
||||
raise RuntimeError("test executable changed since discovery")
|
||||
result["verdict"] = "GREEN"
|
||||
except (RuntimeError, OSError, ValueError, KeyError, ET.ParseError) as exc:
|
||||
result.update(verdict="FAILED_CHECK", error=str(exc))
|
||||
if copied is None:
|
||||
copied = ET.SubElement(junit, "testcase", name=name)
|
||||
ET.SubElement(copied, "failure", message=str(exc))
|
||||
outcomes.append(result)
|
||||
junit.set("tests", str(len(outcomes)))
|
||||
junit.set("failures", str(sum(row["verdict"] != "GREEN" for row in outcomes)))
|
||||
junit.set("skipped", str(sum(case.find("skipped") is not None for case in junit)))
|
||||
write_json(out / "summary.json", outcomes)
|
||||
ET.ElementTree(junit).write(out / "aggregate-gtest.xml", encoding="utf-8", xml_declaration=True)
|
||||
print(f"{name}: process_rc={result.get('returncode')} verdict={result['verdict']}", flush=True)
|
||||
unchanged = all(sha256(Path(path)) == digest for path, digest in executable_hashes.items())
|
||||
complete = len(outcomes) == 7 and {row["ctest_name"] for row in outcomes} == REQUIRED
|
||||
green = complete and unchanged and all(row["verdict"] == "GREEN" for row in outcomes)
|
||||
write_json(out / "totals.json", {
|
||||
"verdict": "GREEN" if green else "FAILED_CHECK", "complete_exact_seven": complete,
|
||||
"passed": sum(row["verdict"] == "GREEN" for row in outcomes),
|
||||
"failed": sum(row["verdict"] != "GREEN" for row in outcomes),
|
||||
"layer_inserted_cases": sum(row.get("layer_inserted", False) for row in outcomes),
|
||||
"validation_error_lines": sum(row.get("validation_error_lines", 0) for row in outcomes),
|
||||
"application_error_lines": sum(row.get("application_error_lines", 0) for row in outcomes),
|
||||
"executables_unchanged_after_all_cases": unchanged,
|
||||
})
|
||||
return 0 if green else 1
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--source-root", type=Path, default=Path(__file__).resolve().parents[2])
|
||||
parser.add_argument("--build-dir", type=Path, help="defaults to source-root/build-split")
|
||||
parser.add_argument("--out", type=Path, required=True, help="new evidence directory; existing paths are refused")
|
||||
parser.add_argument("--execute", action="store_true", help="run serially after other GPU lanes are idle")
|
||||
args = parser.parse_args()
|
||||
if not args.execute:
|
||||
print("Prepared only. Add --execute after other host GPU lanes are idle; nothing was discovered or run.")
|
||||
return 0
|
||||
source = args.source_root.resolve(strict=True)
|
||||
build = (args.build_dir or source / "build-split").resolve(strict=True)
|
||||
out = args.out.resolve()
|
||||
out.mkdir(parents=True, exist_ok=False)
|
||||
try:
|
||||
checker, checker_path = load_checker(source)
|
||||
tests, executable_hashes = discover(build, out, checker)
|
||||
head = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD"], text=True).strip()
|
||||
write_json(out / "runner-identity.json", {
|
||||
"source_root": str(source), "source_head_at_start": head, "build_dir": str(build),
|
||||
"shared_checker": str(checker_path), "shared_checker_sha256": sha256(checker_path),
|
||||
"runner": str(Path(__file__).resolve()), "runner_sha256": sha256(Path(__file__)),
|
||||
"executable_sha256_by_path": executable_hashes, "validation_overrides": SETTINGS,
|
||||
"required_cases": sorted(REQUIRED),
|
||||
})
|
||||
return validate(tests, build, out, checker, executable_hashes)
|
||||
except (RuntimeError, OSError, ValueError, KeyError, ET.ParseError, subprocess.TimeoutExpired,
|
||||
subprocess.CalledProcessError) as exc:
|
||||
(out / "runner-error.txt").write_text(str(exc), encoding="utf-8")
|
||||
print(f"FAILED: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user