Files
PS5_RetroArch/tests/test_genesis_plus_gx_video.py
T
Mihawk 3aad94459d Withdraw the red/blue compensations and relink against the migrated driver
Build the frontend against ../PS5_Vulkan 8b311e3 and remove the changes that
existed only because that driver could not sample the frontend's own byte order.

Driver review (8b311e3, "Migrate the driver to ps5-opengl 0.3.0's compiler fork,
and close the console SIGFPE at its owner"): driver/ps5vk_image.c is not touched,
VK_FORMAT_B8G8R8A8_UNORM still carries SAMPLED_IMAGE_BIT with the 8_8_8_8_UNORM
word and the ZYXW selectors, and the format audit's counts are unchanged (179
required, 58 reported, 0 missing, 55 conditional, split 0/0/0/0/0), so the fetch
order this port depends on survived the migration. What did change there is the
compiler tree (ps5-opengl 0.3.0's opengnm-psbc, metadata v14), the compute
dispatch words, a null shader module in ps5vk_compile_stage that faulted every
meta clear/blit/resolve, and a driver-owned 32 MiB compile stack. The console
SIGFPE of round 15 was the probe's own unfilled std::array row, not ACO:
symbolization had missed the PIE load base.

Withdrawn: 0014's format substitution (its usage mask stays), 0075's core-frame
RGBA upload and 0077's menu colour and cached-frame ownership.
tools/apply-port-patches.py grew an 0083 group and a WITHDRAWN tuple that
exchanges each named insertion for the upstream text it replaced, so a tree that
already carries the insertion and a fresh one converge - checked by patching a
copy of vendor/retroarch's gfx/drivers/vulkan.c and the withdrawn build/ra-conf
and comparing (identical). src/core_frame_ps5.cpp no longer converts channels,
four tests assert the core's own bytes instead of the conversion's output, two new
tests pin the mechanism, and the patch-count pin moved 179 -> 185. Kept: 0027/0055
(the 16-bit RGUI frame, still converted by hand into R8G8B8A8 because a B4G4R4A4
view with a B/R swizzle is refused), 0015 (upstream's own VK_REMAP_TO_TEXFMT) and
0081 (upstream's TODO: supports_rgba follows the order a caller asked for).

Command and result:
  PS5_VULKAN_DIR=$PWD/build/frozen-driver \
  PS5_VULKAN_ICD=$PWD/build/frozen-driver/build/driver/ps5/libvulkan.so.1 \
  bash tools/verify.sh
  -> format PASS (1s), unit PASS (12s, 75 tests), build PASS (247s),
     integration PASS (10s), evidence PASS (0s),
     verify: PASS (format unit build integration evidence)

Artifact: dist/PPSA99169/eboot.bin, 34,703,515 bytes, sha256 82534b86..., title
identity f9e4ea6c..., two consecutive runs byte-identical. The four driver
archives were frozen into build/frozen-driver/ before the build and re-hashed
unchanged after it, and the identity was recomputed from tools/build-title.sh's
own input list against those hashes and matched, so the eboot names the driver it
carries. Record: evidence/driver-1.0-native-byte-order/.

Four runs preceded the green one: unit (three new markers did not appear in their
own replacements and the count pin was stale), build (the frozen tree's .deps/work
symlink was one level short), a run interrupted when 8b311e3 landed, and evidence
(the new step directory had no capture/expectation yet). Logs:
build/verify-native-byte-order-first-run.log, -second-run.log,
-third-run-killed.log, -gates-4-of-5.log.

Not verified on the console: the colours of this build are the owner's check, and
the withdrawn path is the one that was accepted before. The command is recorded in
docs/ACTIVE.md.
2026-09-20 20:39:18 -04:00

111 lines
5.7 KiB
Python

"""Exercise the core's RGB565 packing, cropped frames and upload byte order."""
import ctypes
import hashlib
from pathlib import Path
import subprocess
import tarfile
import tempfile
import unittest
ROOT = Path(__file__).resolve().parent.parent
ARCHIVE = ROOT / '.deps/downloads/genesis-plus-gx-c2838c7dc4236fc2fe94e5dbd08b41486067918e.tar.gz'
class GenesisVideo(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.tmp = tempfile.TemporaryDirectory()
directory = Path(cls.tmp.name)
source = directory / 'adapter.c'
source.write_text('#include "' + str(ROOT / 'tooling/genesis-plus-gx/ps5-video.h') + '"\n'
'bool convert(uint32_t *d, size_t n, const uint16_t *s, '
'size_t pixels, size_t p, size_t offset, unsigned w, unsigned h) '
'{ return ps5_genesis_frame(d, n, s, pixels, p, offset, w, h); }\n')
obj = directory / 'adapter.o'
subprocess.run(['cc', '-std=c99', '-fPIC', '-Wall', '-Wextra', '-Werror',
'-c', str(source), '-o', str(obj)], check=True)
lib = directory / 'video.so'
subprocess.run(['c++', '-shared', '-fPIC', '-Wall', '-Wextra', '-Werror',
str(obj), '-o', str(lib)], check=True)
cls.api = ctypes.CDLL(str(lib))
cls.convert = cls.api.convert
cls.convert.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p,
ctypes.c_size_t, ctypes.c_size_t, ctypes.c_size_t,
ctypes.c_uint, ctypes.c_uint]
cls.convert.restype = ctypes.c_bool
@classmethod
def tearDownClass(cls):
cls.tmp.cleanup()
def test_all_colours_from_upstream_packing_to_gpu(self):
if not ARCHIVE.exists():
self.skipTest('build Genesis Plus GX first to fetch the pinned source')
self.assertEqual(hashlib.sha256(ARCHIVE.read_bytes()).hexdigest(),
'7ba2eab9d6dae71bb42e8208573300ad3475a4263e860178c4d19c36d85fc92b')
with tarfile.open(ARCHIVE) as tar:
member = next(m for m in tar.getmembers() if m.name.endswith('/core/vdp_render.h'))
renderer = tar.extractfile(member).read().decode().replace('\r\n', '\n')
packing = renderer.split('#elif defined(USE_16BPP_RENDERING)\n')[1].split('\n')[0]
directory = Path(self.tmp.name)
source = directory / 'packing.c'
source.write_text('#include <stdint.h>\n' + packing + '\n'
'void pack(uint16_t *p) { for (unsigned r=0; r<32; ++r) '
'for (unsigned g=0; g<64; ++g) for (unsigned b=0; b<32; ++b) '
'*p++ = PIXEL(r,g,b); }\n')
lib = directory / 'packing.so'
subprocess.run(['cc', '-shared', '-fPIC', '-Wall', '-Werror', str(source),
'-o', str(lib)], check=True)
api = ctypes.CDLL(str(lib))
api.pack.argtypes = [ctypes.c_void_p]
src = (ctypes.c_uint16 * 65536)()
api.pack(src)
original = bytes(src)
core = (ctypes.c_uint32 * 65536)()
self.assertTrue(self.convert(core, 65536, src, 65536, 512, 0, 256, 256))
# These are the bytes an upload copies into a B8G8R8A8 image, which is
# libretro's little-endian XRGB8888 layout: blue, green, red, unused.
pixels = (ctypes.c_uint8 * (65536 * 4)).from_buffer(core)
for pixel in range(65536):
r, g, b = pixel >> 11, (pixel >> 5) & 63, pixel & 31
self.assertEqual(list(pixels[pixel * 4:pixel * 4 + 4]),
[b * 8 + b // 4, g * 4 + g // 16, r * 8 + r // 4, 0])
self.assertEqual(bytes(src), original)
def test_cropping_pitch_cached_frames_and_geometry_transitions(self):
pixels = 720 * 576
data = (ctypes.c_uint16 * pixels)()
out = (ctypes.c_uint32 * (pixels + 1))()
# Reuse buffers over resolution changes, including cropped SMS/NTSC views.
for width, height, offset in [(320, 224, 0), (160, 144, 0), (248, 192, 16),
(256, 240, 0), (320, 480, 0), (600, 448, 40),
(720, 576, 0), (256, 224, 0)]:
ctypes.memset(data, 0, ctypes.sizeof(data))
start = offset // 2
for y in range(height):
data[start + y * 720] = 0xf800
data[start + y * 720 + width - 1] = 0x001f
original = bytes(data)
out[width * height] = 0xdeadbeef
for _ in range(2):
self.assertTrue(self.convert(out, width * height, data, pixels,
1440, offset, width, height))
self.assertEqual(bytes(data), original)
for y in range(height):
self.assertEqual(out[y * width], 0xff0000)
self.assertEqual(out[y * width + width - 1], 0x0000ff)
self.assertEqual(out[width * height], 0xdeadbeef)
def test_invalid_viewports_do_not_write(self):
src = (ctypes.c_uint16 * 1440)()
out = (ctypes.c_uint32 * 1440)(*[0xdeadbeef] * 1440)
before = bytes(out)
for capacity, pixels, pitch, offset, width, height in [
(1, 1440, 1440, 0, 2, 1), (1440, 1440, 1441, 0, 1, 1),
(1440, 1440, 1440, 1, 1, 1), (1440, 1440, 1440, 2880, 1, 1),
(1440, 1440, 1440, 16, 720, 1), (1440, 1440, 1440, 0, 1, 3),
(1440, 1440, 0, 0, 1, 1), (1440, 1440, 1440, 0, 0, 1),
(1440, 1440, 1440, 0, 1, 0), (1440, 0, 1440, 0, 1, 1)]:
self.assertFalse(self.convert(out, capacity, src, pixels, pitch, offset, width, height))
self.assertEqual(bytes(out), before)