feat(font): implement writing lines

Create, destroy, clear, write runs, step through them and read the
line metrics, following KytyPS5. Layouts from SharpProspero.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Adrià Franch 2026-10-06 00:38:19 +02:00
parent 964dd3c919
commit 83442e1e40
7 changed files with 280 additions and 30 deletions

View File

@ -21,4 +21,13 @@ if(BUILD_TESTING)
set_tests_properties(guest_font guest_system_font PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceFont>")
endif()
add_test_executable(guest_font_writing_line_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontWritingLine.cpp)
target_include_directories(guest_font_writing_line_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_font_writing_line_tests PRIVATE libSceFont libc)
configure_windows_unwind(guest_font_writing_line_tests)
add_test(NAME guest_font_writing_line COMMAND guest_font_writing_line_tests)
if(WIN32)
set_tests_properties(guest_font_writing_line PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceFont>")
endif()
endif()

View File

@ -10,6 +10,7 @@ function(add_sce_font_library target)
${fontDir}/src/SystemFont.cpp
${fontDir}/src/Text.cpp
${fontDir}/src/Unimplemented.cpp
${fontDir}/src/WritingLine.cpp
)
target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(${target} PRIVATE freetype libc)

View File

@ -388,6 +388,20 @@ struct FontWritingMetrics {
FontWritingExtent Extent;
};
struct FontWritingLineStep {
float x;
float y;
float advanceX;
float advanceY;
float spacingProgress;
void* writingOrderer;
struct {
float x;
float y;
} Adjusting;
FontWritingMetrics Metrics;
};
struct FontHorizontalLayout {
float baselineOffset;
float lineAdvance;
@ -428,5 +442,7 @@ static_assert(sizeof(FontWritingProfile) == sizeof(std::uint32_t));
static_assert(sizeof(FontWritingStep) == 0x48);
static_assert(sizeof(FontWritingLetterStep) == 0x40);
static_assert(sizeof(FontWritingMetrics) == 0x18);
static_assert(sizeof(FontWritingLineStep) == 0x40);
static_assert(offsetof(FontWritingLineStep, writingOrderer) == 0x18);
#endif

View File

@ -42,11 +42,6 @@ int APS5_VABI sceFontCreateWords() {
return 0;
}
int APS5_VABI sceFontCreateWritingLine() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceFontDefineAttribute() {
NotImplemented_nid_no_patch(__func__);
return 0;
@ -67,11 +62,6 @@ int APS5_VABI sceFontDestroyWords() {
return 0;
}
int APS5_VABI sceFontDestroyWritingLine() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceFontDettachDeviceCacheBuffer() {
NotImplemented_nid_no_patch(__func__);
return 0;
@ -432,31 +422,11 @@ int APS5_VABI sceFontWordsFindWordCharacters() {
return 0;
}
int APS5_VABI sceFontWritingLineClear() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceFontWritingLineGetOrderingSpace() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceFontWritingLineGetRenderMetrics() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceFontWritingLineRefersRenderStep() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceFontWritingLineWritesOrder() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceFontGraphicsDrawupFillTextureImageObject(void) {
NotImplemented_nid_no_patch(__func__);
return 0;

View File

@ -0,0 +1,129 @@
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <stdexcept>
#include <string>
#include <unordered_set>
#include <vector>
#include "prx/libSceFont/include/FontTypes.hpp"
namespace {
constexpr std::int32_t WRITING_FORM_HORIZONTAL = 0x10;
constexpr std::int32_t WRITING_FORM_HORIZONTAL_LTR = 0x12;
constexpr std::uint16_t CREATE_WRITING_LINE_DETAIL_ID = 0x0FD5;
struct CreateWritingLineDetail {
std::uint16_t detailId;
};
struct WritingLine {
std::vector<FontWritingLineStep> steps;
std::size_t cursor = 0;
FontWritingMetrics metrics{};
};
std::mutex linesMutex;
std::unordered_set<WritingLine*> lines;
[[noreturn]] void Unsupported(const char* function, const std::string& what) {
throw std::runtime_error(std::string(function) + ": " + what);
}
WritingLine* GetLine(const char* function, void* writingLine) {
auto* line = static_cast<WritingLine*>(writingLine);
std::lock_guard lock(linesMutex);
if (!lines.contains(line)) Unsupported(function, "unknown writing line handle (error code not verified)");
return line;
}
}
#pragma GCC visibility push(default)
extern "C" {
int APS5_VABI sceFontCreateWritingLine(const FontMemory* memory, std::int32_t writingForm, const void* writingLineDetail, void** pWritingLine) {
(void)memory;
if (!pWritingLine) Unsupported(__func__, "null writing line pointer (error code not verified)");
if (writingForm != WRITING_FORM_HORIZONTAL && writingForm != WRITING_FORM_HORIZONTAL_LTR)
Unsupported(__func__, "writing form " + std::to_string(writingForm) + " is not modelled");
if (writingLineDetail && static_cast<const CreateWritingLineDetail*>(writingLineDetail)->detailId != CREATE_WRITING_LINE_DETAIL_ID)
Unsupported(__func__, "unknown writing line detail id");
auto* line = new WritingLine{};
{
std::lock_guard lock(linesMutex);
lines.insert(line);
}
*pWritingLine = line;
return SCE_FONT_OK;
}
int APS5_VABI sceFontDestroyWritingLine(void** pWritingLine) {
if (!pWritingLine || !*pWritingLine) return SCE_FONT_OK;
auto* line = GetLine(__func__, *pWritingLine);
{
std::lock_guard lock(linesMutex);
lines.erase(line);
}
delete line;
*pWritingLine = nullptr;
return SCE_FONT_OK;
}
int APS5_VABI sceFontWritingLineClear(void* writingLine) {
auto* line = GetLine(__func__, writingLine);
line->steps.clear();
line->cursor = 0;
line->metrics = {};
return SCE_FONT_OK;
}
int APS5_VABI sceFontWritingLineWritesOrder(void* writingLine, std::uint64_t writingAttribute, const FontWritingMetrics* writingMetrics, void* writingOrderer) {
auto* line = GetLine(__func__, writingLine);
if (writingAttribute != 0) Unsupported(__func__, "writing attribute " + std::to_string(writingAttribute) + " is not modelled");
if (!writingMetrics) Unsupported(__func__, "null writing metrics are not modelled");
const FontWritingMetrics& order = *writingMetrics;
const float pen = line->metrics.advanceX;
FontWritingLineStep step{};
step.x = pen;
step.advanceX = order.advanceX;
step.advanceY = order.advanceY;
step.writingOrderer = writingOrderer;
step.Metrics = order;
const FontWritingExtent extent{order.Extent.top, order.Extent.bottom, pen + order.Extent.left, pen + order.Extent.right};
if (line->steps.empty()) {
line->metrics.Extent = extent;
} else {
line->metrics.Extent.top = std::min(line->metrics.Extent.top, extent.top);
line->metrics.Extent.bottom = std::max(line->metrics.Extent.bottom, extent.bottom);
line->metrics.Extent.left = std::min(line->metrics.Extent.left, extent.left);
line->metrics.Extent.right = std::max(line->metrics.Extent.right, extent.right);
}
line->metrics.advanceX += order.advanceX;
line->metrics.advanceY += order.advanceY;
line->steps.push_back(step);
line->cursor = 0;
return SCE_FONT_OK;
}
const FontWritingLineStep* APS5_VABI sceFontWritingLineRefersRenderStep(void* writingLine) {
if (!writingLine) return nullptr;
auto* line = GetLine(__func__, writingLine);
if (line->cursor >= line->steps.size()) return nullptr;
return &line->steps[line->cursor++];
}
int APS5_VABI sceFontWritingLineGetRenderMetrics(void* writingLine, FontWritingMetrics* writingMetrics) {
auto* line = GetLine(__func__, writingLine);
if (!writingMetrics) Unsupported(__func__, "null metrics pointer (error code not verified)");
if (line->steps.empty()) Unsupported(__func__, "metrics of an empty line are not modelled");
*writingMetrics = line->metrics;
return SCE_FONT_OK;
}
}
#pragma GCC visibility pop

View File

@ -0,0 +1,123 @@
#include "prx/libSceFont/include/FontTypes.hpp"
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <exception>
extern "C" {
int APS5_VABI sceFontCreateWritingLine(const FontMemory*, std::int32_t, const void*, void**);
int APS5_VABI sceFontDestroyWritingLine(void**);
int APS5_VABI sceFontWritingLineClear(void*);
int APS5_VABI sceFontWritingLineWritesOrder(void*, std::uint64_t, const FontWritingMetrics*, void*);
const FontWritingLineStep* APS5_VABI sceFontWritingLineRefersRenderStep(void*);
int APS5_VABI sceFontWritingLineGetRenderMetrics(void*, FontWritingMetrics*);
}
static void Check(bool value, int line) {
if (!value) {
std::fprintf(stderr, "Font writing line check failed at line %d\n", line);
std::abort();
}
}
#define Require(value) Check((value), __LINE__)
template <typename F>
static bool Throws(F f) {
try {
f();
} catch (const std::exception&) {
return true;
}
return false;
}
struct WritingLineDetail {
std::uint16_t detailId;
std::uint16_t reserved[3];
void* reservedPointers[3];
};
static bool Same(const FontWritingMetrics& a, const FontWritingMetrics& b) {
return a.advanceX == b.advanceX && a.advanceY == b.advanceY && a.Extent.top == b.Extent.top &&
a.Extent.bottom == b.Extent.bottom && a.Extent.left == b.Extent.left && a.Extent.right == b.Extent.right;
}
static bool StepIs(const FontWritingLineStep* step, float x, const FontWritingMetrics& run, void* orderer) {
return step && step->x == x && step->y == 0.0f && step->advanceX == run.advanceX && step->advanceY == run.advanceY &&
step->spacingProgress == 0.0f && step->writingOrderer == orderer && step->Adjusting.x == 0.0f &&
step->Adjusting.y == 0.0f && Same(step->Metrics, run);
}
int main() {
FontMemory memory{};
void* line = nullptr;
Require(Throws([&] { sceFontCreateWritingLine(&memory, 0x10, nullptr, nullptr); }));
Require(Throws([&] { sceFontCreateWritingLine(&memory, 0x11, nullptr, &line); }) && line == nullptr);
Require(Throws([&] { sceFontCreateWritingLine(&memory, 0, nullptr, &line); }) && line == nullptr);
WritingLineDetail detail{0x0FD4, {}, {}};
Require(Throws([&] { sceFontCreateWritingLine(&memory, 0x10, &detail, &line); }) && line == nullptr);
detail.detailId = 0x0FD5;
Require(sceFontCreateWritingLine(&memory, 0x10, &detail, &line) == SCE_FONT_OK && line != nullptr);
void* ltr = nullptr;
Require(sceFontCreateWritingLine(&memory, 0x12, nullptr, &ltr) == SCE_FONT_OK && ltr != nullptr && ltr != line);
FontWritingMetrics metrics{};
Require(sceFontWritingLineRefersRenderStep(line) == nullptr);
Require(Throws([&] { sceFontWritingLineGetRenderMetrics(line, &metrics); }));
int ordererA = 0;
int ordererB = 0;
const FontWritingMetrics runA{10.0f, 0.0f, {-8.0f, 2.0f, 1.0f, 9.0f}};
const FontWritingMetrics runB{6.0f, 0.0f, {-12.0f, 3.0f, -1.0f, 5.0f}};
Require(sceFontWritingLineWritesOrder(line, 0, &runA, &ordererA) == SCE_FONT_OK);
Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK && Same(metrics, runA));
Require(sceFontWritingLineWritesOrder(line, 0, &runB, &ordererB) == SCE_FONT_OK);
Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK);
Require(Same(metrics, {16.0f, 0.0f, {-12.0f, 3.0f, 1.0f, 15.0f}}));
Require(StepIs(sceFontWritingLineRefersRenderStep(line), 0.0f, runA, &ordererA));
Require(StepIs(sceFontWritingLineRefersRenderStep(line), 10.0f, runB, &ordererB));
Require(sceFontWritingLineRefersRenderStep(line) == nullptr);
Require(sceFontWritingLineRefersRenderStep(line) == nullptr);
const FontWritingMetrics runC{4.0f, 0.0f, {-4.0f, 1.0f, 0.0f, 20.0f}};
Require(sceFontWritingLineWritesOrder(line, 0, &runC, nullptr) == SCE_FONT_OK);
Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK);
Require(Same(metrics, {20.0f, 0.0f, {-12.0f, 3.0f, 1.0f, 36.0f}}));
Require(StepIs(sceFontWritingLineRefersRenderStep(line), 0.0f, runA, &ordererA));
Require(StepIs(sceFontWritingLineRefersRenderStep(line), 10.0f, runB, &ordererB));
Require(StepIs(sceFontWritingLineRefersRenderStep(line), 16.0f, runC, nullptr));
Require(sceFontWritingLineRefersRenderStep(line) == nullptr);
Require(Throws([&] { sceFontWritingLineWritesOrder(line, 1, &runA, nullptr); }));
Require(Throws([&] { sceFontWritingLineWritesOrder(line, 0, nullptr, nullptr); }));
Require(Throws([&] { sceFontWritingLineGetRenderMetrics(line, nullptr); }));
Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK && metrics.advanceX == 20.0f);
Require(sceFontWritingLineClear(line) == SCE_FONT_OK);
Require(sceFontWritingLineRefersRenderStep(line) == nullptr);
Require(Throws([&] { sceFontWritingLineGetRenderMetrics(line, &metrics); }));
Require(sceFontWritingLineWritesOrder(line, 0, &runB, &ordererB) == SCE_FONT_OK);
Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK && Same(metrics, runB));
Require(StepIs(sceFontWritingLineRefersRenderStep(line), 0.0f, runB, &ordererB));
Require(sceFontWritingLineWritesOrder(ltr, 0, &runC, nullptr) == SCE_FONT_OK);
Require(sceFontWritingLineGetRenderMetrics(ltr, &metrics) == SCE_FONT_OK && Same(metrics, runC));
int notALine = 0;
Require(sceFontWritingLineRefersRenderStep(nullptr) == nullptr);
Require(Throws([&] { sceFontWritingLineClear(nullptr); }));
Require(Throws([&] { sceFontWritingLineClear(&notALine); }));
Require(Throws([&] { sceFontWritingLineRefersRenderStep(&notALine); }));
void* bogus = &notALine;
Require(Throws([&] { sceFontDestroyWritingLine(&bogus); }) && bogus == &notALine);
void* destroyed = line;
Require(sceFontDestroyWritingLine(&line) == SCE_FONT_OK && line == nullptr);
Require(sceFontDestroyWritingLine(&line) == SCE_FONT_OK && line == nullptr);
Require(sceFontDestroyWritingLine(nullptr) == SCE_FONT_OK);
Require(Throws([&] { sceFontWritingLineClear(destroyed); }));
Require(Throws([&] { sceFontWritingLineWritesOrder(destroyed, 0, &runA, nullptr); }));
Require(sceFontDestroyWritingLine(&ltr) == SCE_FONT_OK && ltr == nullptr);
std::puts("font writing line: ok");
}

View File

@ -39,6 +39,7 @@ Throughout the project, every function at every stage either **does exactly what
- [AMPR markers](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded
- [sceFontGetRenderScaledKerning](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - arguments, the cleared output and the bound renderer check taken from `cellFontGetRenderScaledKerning` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellFont.cpp) (PS3 predecessor of the library), not confirmed on a title. The kerning is the `sceFontGetKerning` one at the render scale
- [sceFontGetPixelResolution](../../core/libs/prx/libSceFont/src/Library.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title. The value is the one the library's driver reports (64 for FreeType); the argument checks and the cleared output follow `sceFontGetLibrary`
- [sceFontCreateWritingLine, sceFontWritingLineWritesOrder](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - signatures and the `SceFontWritingLineStep`/`SceFontCreateWritingLineDetail` layouts (detail id `0x0FD5`) taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFontStructs.cs), not confirmed on a title; the meaning of the 64-bit writing attribute is unknown
- [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits
- [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted
- [SET_PREDICATION](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw
@ -141,3 +142,4 @@ Throughout the project, every function at every stage either **does exactly what
- [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached
- [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped
- [sceFontOpenFontSet](../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) opens only the primary file of a system font set. Characters the console takes from the set's other files (Hangul, Thai and Arabic in Japanese and Chinese sets, Arabic in European sets) are reported as unsupported glyphs. The console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans CJK) is loaded instead. It is not the system font: its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for.
- [Font writing lines](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - `sceFontCreateWritingLine`, `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics` and `sceFontWritingLineClear` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libFont.cpp): runs are placed left to right, each at the line's advance so far, with no spacing or adjustment. Only horizontal and left-to-right lines, writing attribute 0 and non-null run metrics are modelled; other values, the metrics of an empty line and unknown or null handles throw, since no reference gives their result or error code. `sceFontWritingLineGetOrderingSpace` still throws (KytyPS5's values are placeholders). Lines are allocated on the host, not in the font memory