257 lines
11 KiB
Python
257 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Validate the offline Phase-1.0P VideoOut submit analysis."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
from typing import Any
|
|
|
|
|
|
PHASE = "PHASE_1_0P_VIDEOOUT_SUBMIT_OFFLINE_ANALYSIS"
|
|
STATUS = "VIDEOOUT_SUBMIT_FAILURE_SITE_PROVEN_ROOT_CAUSE_UNRESOLVED"
|
|
ARTIFACT_SHA256 = "c99a0856309a357ad2667d89b4924e4063ad214cae09c8a419457b0732f583cd"
|
|
TRACE_SHA256 = "3d0b8811ae11f5cac2c2d331e252e1789a10588cab0e6045828a6b6af0fe1eb6"
|
|
RA_SOURCE = "12cf1d783c41eb303987e49a5a920805a59ef7a4"
|
|
RA_RUNNER = "606909706f91d7213751c245081333f56c2cce89"
|
|
SDK_COMMIT = "d2e2e585740362976a39fdd5ccf390f199a7bc37"
|
|
SDL_COMMIT = "0baf4ac49382b537ba449901b5b6d0d189bb1fbb"
|
|
AUTHORIZATION_FIELDS = (
|
|
"ps5_connection_authorized", "device_transfer_authorized",
|
|
"device_execution_authorized", "result_receive_authorized",
|
|
"installation_authorized", "autoload_authorized",
|
|
"device_write_authorized", "automatic_retry",
|
|
)
|
|
|
|
|
|
def load_json(path: Path) -> dict[str, Any]:
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
if not isinstance(value, dict):
|
|
raise ValueError("analysis manifest is not an object")
|
|
return value
|
|
|
|
|
|
def all_false(record: dict[str, Any], fields: tuple[str, ...]) -> bool:
|
|
return all(record.get(field) is False for field in fields)
|
|
|
|
|
|
def bound_inputs_are_exact(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("artifact_name") == "retroarch_ps5_write_diag.elf"
|
|
and record.get("artifact_size") == 1845208
|
|
and record.get("artifact_sha256") == ARTIFACT_SHA256
|
|
and record.get("trace_size") == 18031
|
|
and record.get("trace_sha256") == TRACE_SHA256
|
|
and record.get("linked_sdl_source_size") == 19642
|
|
and record.get("linked_sdl_source_sha256")
|
|
== "9949a280fed40241746788a8c001280455bc629e0566a88f82f16a8634d43025"
|
|
and record.get("linker_map_size") == 637728
|
|
and record.get("linker_map_sha256")
|
|
== "6768ffc7267b9b362c3b953571e5dfeed1e70004a3e19f41ef500c3f26272719"
|
|
)
|
|
|
|
|
|
def exact_call_is_bounded(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("symbol") == "sceVideoOutSubmitFlip"
|
|
and record.get("tuple") == "(handle,0,1,0)"
|
|
and record.get("function_address") == "0xfe560"
|
|
and record.get("call_address") == "0xfe7bc"
|
|
and record.get("got_address") == "0x1779b0"
|
|
and record.get("registers")
|
|
== {"edi": "handle", "esi": 0, "edx": 1, "ecx": 0}
|
|
and record.get("runtime_handle") == "0x4e100100"
|
|
and record.get("runtime_return") == -1
|
|
and record.get("saved_errno") == 0
|
|
and record.get("wait_called_after_failure") is False
|
|
)
|
|
|
|
|
|
def abi_claims_fail_closed(record: dict[str, Any]) -> bool:
|
|
required_unproven = (
|
|
"flip_mode_semantics", "frame_id_semantics", "buffer_zero_validity",
|
|
"buffer_attribute_semantics", "flip_master_or_active_app_requirement",
|
|
"return_code_semantics",
|
|
)
|
|
return (
|
|
record.get("sdk_exports_symbols_only") is True
|
|
and record.get("sdk_public_videoout_headers_present") is False
|
|
and record.get("sdl_fork_is_only_local_public_prototype_source") is True
|
|
and record.get("submit_signature_independently_corroborated") is False
|
|
and all(record.get(field) == "UNPROVEN" for field in required_unproven)
|
|
and record.get("errno_is_useful_for_observed_failure") is False
|
|
)
|
|
|
|
|
|
def terminal_order_is_not_promoted(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("observed") == ["D07", "D12", "D04"]
|
|
and record.get("source_deterministic") is True
|
|
and record.get("network_reordering_required") is False
|
|
and record.get("d12_meaning") == "FAILURE_SHUTDOWN_REQUEST"
|
|
and record.get("d12_proves_cleanup_complete") is False
|
|
and record.get("current_trace_valid_terminal_classification") is False
|
|
and record.get("host_parser_relaxation_allowed") is False
|
|
and record.get("future_protocol_requires_new_magic") is True
|
|
and record.get("future_terminal_after_d04_and_cleanup_evidence") is True
|
|
)
|
|
|
|
|
|
def decision_is_fail_closed(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("classification") == STATUS
|
|
and record.get("parameter_experiment_allowed") is False
|
|
and record.get("new_videoout_export_call_allowed") is False
|
|
and record.get("host_parser_promotion_allowed") is False
|
|
and record.get("phase10q_target_build_allowed") is False
|
|
and record.get("phase10q_device_action_allowed") is False
|
|
and record.get("required_next_evidence")
|
|
== "ACCEPTABLE_PUBLIC_VIDEOOUT_ABI_AND_RUNTIME_STATE_CONTRACT"
|
|
)
|
|
|
|
|
|
def git(root: Path, *args: str) -> str:
|
|
result = subprocess.run(
|
|
["git", *args], cwd=root, capture_output=True, text=True, check=False)
|
|
if result.returncode:
|
|
raise RuntimeError(result.stderr.strip() or "git failed")
|
|
return result.stdout
|
|
|
|
|
|
def validate_repository(root: Path, retroarch_root: Path, sdk_root: Path,
|
|
sdl_root: Path) -> list[str]:
|
|
errors: list[str] = []
|
|
record = load_json(
|
|
root / "manifests/retroarch/phase-1.0p-videoout-submit-analysis.json")
|
|
if record.get("phase") != PHASE or record.get("status") != STATUS:
|
|
errors.append("phase/status mismatch")
|
|
commits = record.get("source_commits", {})
|
|
expected = {
|
|
"chimera_gfx_phase10o": "af283ef93fae907e6c9c376df24d053ada3f7318",
|
|
"chimera_retroarch_artifact": RA_SOURCE,
|
|
"chimera_retroarch_runner": RA_RUNNER,
|
|
"ps5_payload_sdk_v0_41": SDK_COMMIT,
|
|
"public_ps5_sdl": SDL_COMMIT,
|
|
}
|
|
if commits != expected:
|
|
errors.append("source commit binding mismatch")
|
|
if not bound_inputs_are_exact(record.get("bound_inputs", {})):
|
|
errors.append("artifact, trace, source or map binding mismatch")
|
|
actions = record.get("offline_actions", {})
|
|
if not all(actions.get(field) is True for field in (
|
|
"source_inspected", "linker_map_inspected", "disassembly_inspected",
|
|
"dynamic_symbols_and_relocations_inspected",
|
|
)) or not all(actions.get(field) is False for field in (
|
|
"target_source_changed", "target_artifact_created", "ps5_connected",
|
|
"device_transfer_performed", "target_execution_performed",
|
|
"result_received_from_device",
|
|
)):
|
|
errors.append("offline-only action boundary mismatch")
|
|
if not all_false(record.get("current_authorizations", {}), AUTHORIZATION_FIELDS):
|
|
errors.append("authorization remains active")
|
|
if not exact_call_is_bounded(record.get("exact_call", {})):
|
|
errors.append("exact submit call is not bounded")
|
|
registration = record.get("registration", {})
|
|
if not (
|
|
registration.get("function_address") == "0xfdc20"
|
|
and registration.get("runtime_return") == 0
|
|
and registration.get("buffer_index_used") == 0
|
|
and registration.get("opaque_layout_semantics") == "UNPROVEN"
|
|
):
|
|
errors.append("registration evidence was broadened")
|
|
if not abi_claims_fail_closed(record.get("abi_evidence", {})):
|
|
errors.append("ABI uncertainty was promoted")
|
|
candidates = record.get("candidate_matrix", {})
|
|
if not (
|
|
candidates.get("diagnostic_normal_first_index_mismatch")
|
|
== "REJECTED_CURRENT_CAUSE"
|
|
and candidates.get("buffer_registration_failure")
|
|
== "REJECTED_BY_OBSERVED_RETURN_ZERO"
|
|
and candidates.get("errno_specific_failure_identity") == "ABSENT"
|
|
and all(candidates.get(field) == "UNPROVEN" for field in (
|
|
"wrong_submit_tuple", "wrong_opaque_buffer_contract",
|
|
"missing_flip_master_or_app_state", "visible_flip",
|
|
"complete_cleanup", "safe_exit",
|
|
))
|
|
):
|
|
errors.append("candidate matrix overclaims a root cause")
|
|
if not terminal_order_is_not_promoted(record.get("terminal_ordering", {})):
|
|
errors.append("terminal ordering is promoted or incomplete")
|
|
if not decision_is_fail_closed(record.get("decision", {})):
|
|
errors.append("decision is not fail-closed")
|
|
tests = record.get("tests", {})
|
|
if not (
|
|
tests.get("chimera_gfx_ctest") == "60_OF_60_PASS"
|
|
and tests.get("phase10p_guardrails") == 20
|
|
and tests.get("hardware_claim_from_host_test") is False
|
|
):
|
|
errors.append("test evidence mismatch")
|
|
tracked = git(root, "ls-files").splitlines()
|
|
if any(path.lower().endswith((".elf", ".self", ".sprx", ".pkg", ".map"))
|
|
for path in tracked):
|
|
errors.append("target artifact or linker map is tracked")
|
|
try:
|
|
if git(retroarch_root, "rev-parse", "HEAD").strip() != RA_RUNNER:
|
|
errors.append("RetroArch HEAD mismatch")
|
|
patch = git(retroarch_root, "show", f"{RA_SOURCE}:pkg/ps5/sdl2-ps5-early-diag.patch")
|
|
diag = git(retroarch_root, "show", f"{RA_SOURCE}:pkg/ps5/chimera_ps5_diag.c")
|
|
gfx = git(retroarch_root, "show", f"{RA_SOURCE}:gfx/drivers/sdl2_gfx.c")
|
|
for token in (
|
|
"CHIMERA_PS5_FIRST_FRAME_INDEX 0u",
|
|
"sceVideoOutSubmitFlip(",
|
|
"submit_errno = submit_result != 0 ? errno : 0",
|
|
"chimera_ps5_smoke_video_error(",
|
|
):
|
|
if token not in patch:
|
|
errors.append(f"artifact source token missing: {token}")
|
|
if "stage == CHIMERA_PS5_DIAG_D12" not in diag:
|
|
errors.append("D12 terminal source missing")
|
|
if "chimera_ps5_early_diag_sdl_result(sdl_init_result)" not in gfx:
|
|
errors.append("D04 caller source missing")
|
|
if git(sdk_root, "rev-parse", "HEAD").strip() != SDK_COMMIT:
|
|
errors.append("SDK commit mismatch")
|
|
if git(sdl_root, "rev-parse", "HEAD").strip() != SDL_COMMIT:
|
|
errors.append("SDL commit mismatch")
|
|
if git(sdk_root, "status", "--porcelain"):
|
|
errors.append("SDK tree is dirty")
|
|
if git(sdl_root, "status", "--porcelain"):
|
|
errors.append("SDL tree is dirty")
|
|
stubs = (sdk_root / "sce_stubs/libSceVideoOut.c").read_text(encoding="utf-8")
|
|
header = (sdl_root / "src/video/ps5/SDL_ps5video.h").read_text(encoding="utf-8")
|
|
source = (sdl_root / "src/video/ps5/SDL_ps5video.c").read_text(encoding="utf-8")
|
|
if "sceVideoOutSubmitFlip" not in stubs:
|
|
errors.append("SDK submit export missing")
|
|
if "int sceVideoOutSubmitFlip(int, int, uint32_t, int64_t);" not in header:
|
|
errors.append("SDL submit prototype missing")
|
|
if "static uint32_t frame_id = 0;" not in source or \
|
|
"sceVideoOutSubmitFlip(device_data->handle, idx, 1, frame_id)" not in source:
|
|
errors.append("public SDL first-frame source mismatch")
|
|
except (OSError, RuntimeError) as error:
|
|
errors.append(str(error))
|
|
return errors
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", type=Path, required=True)
|
|
parser.add_argument("--retroarch-root", type=Path, required=True)
|
|
parser.add_argument("--sdk-root", type=Path, required=True)
|
|
parser.add_argument("--sdl-root", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
errors = validate_repository(
|
|
args.root.resolve(), args.retroarch_root.resolve(),
|
|
args.sdk_root.resolve(), args.sdl_root.resolve())
|
|
if errors:
|
|
for error in errors:
|
|
print(f"ERROR: {error}")
|
|
return 1
|
|
print("Phase-1.0P offline VideoOut analysis validation passed")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|