Files
PS5_RetroArch/src/display.hpp
T
Mihawk 1ee7eb38fd The RGUI menu is on the console's screen, and the fault was a struct size
bash tools/run-title.sh --watch 20 runs the title unattended with no fatal signal,
and the console's owner watched RetroArch's RGUI menu on the television during it.
The title's own trace from that run is committed as
evidence/ppsa-99169-rgui-menu-on-screen/:

  ps5_init: own table ident=ps5 poke_interface=4015d0 set_viewport=401590 wrap_type_to_enum=0
  ps5_get_poke_interface entered
  ps5_set_texture_frame: rgb32=0 320x240 frame=present have=1 (was 0)
  ps5_frame 1: menu 320x240 pitch=640 present=1
  display: flip 1200 of buffer 1, status=0 marker=1

Two independent faults were behind the black screen.

The first was one line of the build. src/ was compiled with no -DHAVE_* flags at
all while the frontend archive is compiled with fifty, so the two sides disagreed
about RetroArch's video_driver_t: video_ps5 was 136 bytes where the frontend read
144, and every member after overlay_interface was read one slot late. poke_interface
came back NULL, the frontend calls it only when it is not NULL, and RGUI's hand-over
was therefore dropped in silence on every frame while the driver presented 1500
frames of its own probe. tools/build-title.sh now passes tools/retroarch-flags.sh's
own list to the title's sources and writes the configured tree's config.h where
RetroArch's headers look for it. tests/test_frontend.py compares
sizeof(video_driver_t), compiled with the frontend's defines, against the size of
video_ps5 in the object the title links, and checks the member at the frontend's
poke_interface offset against the table's relocations; built without the defines it
fails with "video_ps5 is 136 bytes in the title's object and video_driver_t is 144
bytes in the frontend", which was verified by doing it.

The second was sceAgcInit(8), called before the display opened, left over from the
round that submitted flips through an AGC command buffer.
../PS5_Vulkan/src/demo_renderer.cpp, whose output has been seen on this console,
contains no sceAgc call. Removing it produced the first pixels this port ever put on
the screen.

The lead this round started from is measured false. Holding the frame inside
present() instead of returning into RetroArch's runloop was expected to be the fix;
with the probe's frame held for 8 seconds the bands appeared immediately, before the
hold began, and were still on screen twelve seconds after present() had returned into
the runloop, with the display's flip status at marker=1 for all sixteen samples. The
runloop does not take a presented frame back. The two candidate causes went in
together, so what the run separates is the runloop; the AGC call is what remains with
no other candidate.

present() is a real driver now: flush the back buffer, flip to that buffer's
registered index, wait a vblank, alternate buffers. The probe paint, the readback and
the hold are gone. The bands remain underneath as the instrument that tells a black
screen from a menu that is not drawing.

gates: bash tools/verify.sh -> PASS (format unit build integration evidence).
reproduce: bash tools/run-title.sh --watch 20, then read /app0/trace.txt.
2026-09-18 17:38:58 -04:00

102 lines
3.2 KiB
C++

/*
* PS5 RetroArch - the console's display, owned by this project.
*
* Copyright (C) 2026 Mihawk
* SPDX-License-Identifier: GPL-3.0-or-later
*
* The layer a RetroArch video driver sits on: VideoOut, the direct memory the GPU
* scans out, two registered buffers, present-and-wait. Nothing here is
* RetroArch-specific, so it can be tested on its own.
*
* The frame is tiled. A caller does not write pixels directly: it uses `write` and
* `clear`, which address the tiled layout. That keeps the one thing that is easy
* to get wrong in one place.
*
* Reference: docs/REFERENCE.md, "The display".
*/
#pragma once
#include <cstdint>
#include <cstddef>
namespace ps5::display
{
/** What a caller draws through: an opaque frame and its size. */
struct Surface
{
void *base = nullptr;
unsigned width = 0;
unsigned height = 0;
};
class Display final
{
public:
Display() = default;
~Display();
Display(const Display &) = delete;
Display &operator=(const Display &) = delete;
/** Opens the display and prepares two buffers at the console's frame size. */
bool open(unsigned width, unsigned height) noexcept;
void close() noexcept;
[[nodiscard]] bool is_open() const noexcept
{
return handle_ >= 0;
}
[[nodiscard]] unsigned width() const noexcept
{
return width_;
}
[[nodiscard]] unsigned height() const noexcept
{
return height_;
}
/** The buffer the caller may write into now. */
[[nodiscard]] Surface back_surface() const noexcept;
/** One pixel, through the tiled layout. Out-of-range writes are ignored. */
static void write(Surface surface, unsigned x, unsigned y, std::uint32_t colour) noexcept;
/** The byte offset of one pixel, so a caller can read back what it wrote. */
[[nodiscard]] static std::size_t offset_of(unsigned x, unsigned y) noexcept;
/** Fills the whole frame. */
static void clear(Surface surface, std::uint32_t colour) noexcept;
/** Presents the back buffer, waits for the display to take it, and makes the
* other buffer the back one. */
bool present() noexcept;
/** The last failure, for one log line; never null. */
[[nodiscard]] const char *last_error() const noexcept
{
return error_;
}
private:
int handle_ = -1;
void *mapped_ = nullptr;
void *frames_[2] = {nullptr, nullptr};
std::uint64_t flip_status_[16] = {0};
/* The physical address the direct-memory allocation returned, kept so a log
* line can say which 16 MiB the registered buffers live in. Nothing else here
* needs it. */
long long physical_ = 0;
/* Flips submitted, for the trace line that would otherwise be the only way to
* know whether the display is being driven at all. */
unsigned long long flips_ = 0;
int registered_[2] = {-1, -1};
/* The buffer the next frame is drawn into. The two alternate: after a flip to
* the back buffer is shown, that one is on the screen and the other becomes the
* one this driver may write. */
int back_ = 0;
unsigned width_ = 0;
unsigned height_ = 0;
const char *error_ = "display not opened";
};
} // namespace ps5::display