Qualify native language sessions and derived file lifecycle

This commit is contained in:
Matthew Stanley
2026-10-20 22:35:19 -07:00
parent 7b28f12b02
commit 74c4057746
11 changed files with 211 additions and 54 deletions
+55
View File
@@ -0,0 +1,55 @@
# Native text and donor assets
`runtime/include/native_text.h` provides the reusable text side of a native
translation mod. It complements the existing draw-hook translation support:
adapters can encode plain Unicode back into original guest text and font data,
then let the guest renderer consume it.
Catalog schema 1 keeps a stable `id`, readable `original`, editable
`translation`, and lowercase `source_sha256` for each entry. The digest identifies
the exact original native record. An empty translation means native fallback.
The adapter validates source digests against its title's records before making
changes. Duplicate IDs, malformed Unicode and missing digests are errors.
`NativeGlyphMap` maps Unicode scalars to native glyph indices.
`native_text_encode` wraps words and emits adapter-supplied line, page and final
control bits. Unsupported characters and words exceeding the native width fail;
they never silently become spaces. A title owns its dimensions, style bits,
source record inventory, buffer capacity and alignment requirements.
`native_font_compose` copies a cell in the original indexed 4-bpp font to an
unused cell and composes an acute accent, tilde, diaeresis or inverted mark.
Native palette indices, base shape and baseline are retained. The adapter can
omit top rows of the copied shape to remove i's dot before composing í. Original
cells remain unchanged, and occupied destinations are refused. Other native
font layouts need their own explicit adapter; there is no host-font fallback.
For asset-backed games, an activation callback can use
`psx_mod_replace_disc_file` to mount a derived file without modifying the owner
disc. The runtime copies its bytes into an immutable Mode-2 Form-1 extent and
updates both endian halves of its ISO directory record. Host file readers and
guest CD reads resolve the same final file. Multiple replacements of the same
file layer in activation order. Private native LBA/size tables remain the
title adapter's responsibility. Use only verified committed resources and
trusted adapter code to derive these bytes; paths, clocks and host fonts must
not affect their content.
The `psx-mode2` verified resource format retains 2336 bytes per raw Mode-2
sector, including XA subheaders and audio/video payloads. Its identity differs
from the existing cooked `psx-disc` format. Cooked ISO images cannot satisfy this
contract. `ModMode2Disc` performs bounded ISO9660 lookup and ordinary file reads
against this immutable snapshot; raw streams mount through
`psx_mod_append_disc_extent` and the original CD decoder.
Register a session-reset callback for title-owned preparation caches. Activation
reconstructs them from the committed plan; reset retires stale data even when
the next session disables the feature. Save-state identity includes verified
catalog/donor content through the ordinary mod plan. Adapters still need title
playback, visual and audio qualification, plus any relevant co-op or netplay
qualification.
Mega Man X4 is the first asset adapter. Its source keeps archive and message
format knowledge outside the framework. Focused coverage is in
`native_text_test`, `mod_media_test` and `mod_runtime_test`, including malformed
directory records, cross-sector reads, layered replacement, native directory
parity, immutable sources, session cleanup and edited catalog checksums.
+10 -49
View File
@@ -12,58 +12,19 @@
#include <sys/stat.h>
#endif
namespace PSXRecompV4 {
// A failed identity is never a cache hit. Change time protects restored mtimes.
// A failed identity is never a cache hit; callers must verify content instead.
inline bool host_file_identity(const std::filesystem::path& path, std::string& identity) {
identity.clear();
std::ostringstream out;
#if defined(_WIN32)
HANDLE file = CreateFileW(path.c_str(), FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
if (file == INVALID_HANDLE_VALUE) return false;
struct HandleScope { HANDLE value; ~HandleScope() { CloseHandle(value); } } handle{file};
WCHAR filesystem_name[32]{};
// NTFS/ReFS expose change time and native file identity. For a remote
// handle also require SMB3+, rather than assuming every NAS is unsupported.
FILE_REMOTE_PROTOCOL_INFO remote{};
const bool is_remote = GetFileInformationByHandleEx(file, FileRemoteProtocolInfo,
&remote, sizeof remote) != FALSE;
const DWORD protocol_error = is_remote ? ERROR_SUCCESS : GetLastError();
bool local_handle = false;
if (!is_remote) {
// A failed protocol query proves nothing about locality. Resolve the
// handle itself: mapped drives become UNC paths here and cannot take
// the local fallback when a remote provider omits protocol metadata.
WCHAR final_path[32768]{};
const DWORD length = GetFinalPathNameByHandleW(file, final_path, 32768, FILE_NAME_NORMALIZED);
if (length >= 7 && length < 32768 && final_path[0] == L'\\' &&
final_path[1] == L'\\' && final_path[2] == L'?' && final_path[3] == L'\\' &&
final_path[5] == L':' && final_path[6] == L'\\') {
WCHAR drive_root[] = {final_path[4], L':', L'\\', L'\0'};
const UINT drive_type = GetDriveTypeW(drive_root);
local_handle = drive_type == DRIVE_FIXED || drive_type == DRIVE_REMOVABLE;
}
}
const bool reliable_protocol = is_remote
? remote.Protocol == 0x00020000 && remote.ProtocolMajorVersion >= 3 // WNNC_NET_LANMAN
: local_handle && (protocol_error == ERROR_INVALID_PARAMETER || protocol_error == ERROR_NOT_SUPPORTED);
const bool reliable_fs = reliable_protocol &&
GetVolumeInformationByHandleW(file, nullptr, 0, nullptr, nullptr, nullptr,
filesystem_name, 32) &&
(wcscmp(filesystem_name, L"NTFS") == 0 || wcscmp(filesystem_name, L"ReFS") == 0);
BY_HANDLE_FILE_INFORMATION info{};
FILE_BASIC_INFO basic{};
const bool ok = reliable_fs && GetFileInformationByHandle(file, &info) &&
GetFileInformationByHandleEx(file, FileBasicInfo, &basic, sizeof basic) &&
!(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
(info.nFileIndexHigh != 0 || info.nFileIndexLow != 0) &&
info.dwVolumeSerialNumber != 0 && basic.ChangeTime.QuadPart != 0 &&
basic.LastWriteTime.QuadPart != 0;
if (!ok) return false;
out << "win1:" << info.dwVolumeSerialNumber << ':' << info.nFileIndexHigh << ':'
<< info.nFileIndexLow << ':' << info.nFileSizeHigh << ':' << info.nFileSizeLow
<< ':' << basic.LastWriteTime.QuadPart << ':' << basic.ChangeTime.QuadPart;
// NTFS change/write timestamps can collide across immediate rewrites,
// including same-size writes with a restored mtime. A file USN alone also
// cannot qualify persistent receipts: it may be zero or reset with its
// journal, and the query is unavailable over SMB3. Until a journal-bound
// change identity is qualified, retain the portable full-verification path.
(void)path;
return false;
#else
std::ostringstream out;
struct stat info{};
if (stat(path.c_str(), &info) || !S_ISREG(info.st_mode) || info.st_ino == 0) return false;
out << "posix1:" << info.st_dev << ':' << info.st_ino << ':' << info.st_size;
@@ -73,9 +34,9 @@ inline bool host_file_identity(const std::filesystem::path& path, std::string& i
#else
out << ':' << info.st_mtim.tv_sec << ':' << info.st_mtim.tv_nsec
<< ':' << info.st_ctim.tv_sec << ':' << info.st_ctim.tv_nsec;
#endif
#endif
identity = out.str();
return true;
#endif
}
}
+2
View File
@@ -581,6 +581,8 @@ private:
bool mod_register_builtin_resolver(const std::string& id, ModBuiltinResolver resolver);
void mod_clear_builtin_resolvers_for_tests();
bool mod_register_activation_plugin(const std::string& id, void (*callback)(void));
bool mod_register_session_reset_plugin(const std::string& id, void (*callback)(void));
void mod_reset_plugin_sessions();
bool mod_register_vblank_plugin(const std::string& id, void (*callback)(void));
bool mod_register_savestate_plugin(const std::string& id, void (*callback)(void));
void mod_invoke_savestate_plugin(const std::string& id);
+4
View File
@@ -25,6 +25,10 @@ typedef int (*PSXModFunctionFilterCallback)(struct CPUState* cpu,
*/
int psx_mod_register_activation_plugin(const char* id,
PSXModActivationCallback callback);
/* Host-only cleanup before a new plan, including when it omits this plugin.
* Runs with emulation stopped and no current resource context. */
int psx_mod_register_session_reset_plugin(const char* id,
PSXModActivationCallback callback);
int psx_mod_register_vblank_plugin(const char* id,
PSXModVBlankCallback callback);
/* Runs after successful guest-state restore, before the restored PC resumes.
+4 -1
View File
@@ -43,6 +43,9 @@ struct NativeFontAtlas {
std::vector<uint8_t> packed;
};
enum class NativeGlyphMark { Acute, Tilde, Diaeresis, Inverted };
// omit_top_rows is an adapter-provided crop of the copied glyph, for example
// to remove i's dot before composing í. The source atlas cell is preserved.
bool native_font_compose(NativeFontAtlas& atlas, uint16_t base, uint16_t destination,
NativeGlyphMark mark, std::string& error);
NativeGlyphMark mark, std::string& error,
uint32_t omit_top_rows=0);
} // namespace PSXRecompV4
+18
View File
@@ -43,6 +43,7 @@ std::map<std::string, MediaPreparer>& media_preparers() {
struct RegisteredPlugin {
void (*savestate)(void) = nullptr;
PSXModActivationCallback activation = nullptr;
PSXModActivationCallback session_reset = nullptr;
PSXModVBlankCallback vblank = nullptr;
/* Generated-function entry hooks, keyed by guest address. One id may
* observe several functions. */
@@ -1491,6 +1492,18 @@ bool mod_register_activation_plugin(const std::string& id, void (*callback)(void
return true;
}
bool mod_register_session_reset_plugin(const std::string& id, void (*callback)(void)) {
if (!valid_id(id) || !callback) return false;
RegisteredPlugin& plugin = registered_plugins()[id];
if (plugin.session_reset) return false;
plugin.session_reset = callback;
return true;
}
void mod_reset_plugin_sessions() {
for (const auto& entry : registered_plugins())
if (entry.second.session_reset) entry.second.session_reset();
}
bool mod_register_vblank_plugin(const std::string& id, void (*callback)(void)) {
if (!valid_id(id) || !callback) return false;
RegisteredPlugin& plugin = registered_plugins()[id];
@@ -4199,6 +4212,11 @@ extern "C" int psx_mod_register_activation_plugin(
PSXRecompV4::mod_register_activation_plugin(id, callback) ? 1 : 0;
}
extern "C" int psx_mod_register_session_reset_plugin(
const char* id, PSXModActivationCallback callback) {
return id && PSXRecompV4::mod_register_session_reset_plugin(id, callback) ? 1 : 0;
}
extern "C" int psx_mod_register_savestate_plugin(
const char* id, PSXModActivationCallback callback) {
return id &&
+6 -2
View File
@@ -1578,6 +1578,7 @@ bool mod_runtime_initialize(const std::filesystem::path& root,
}
bool mod_runtime_clear_for_netplay(std::string* error) {
mod_reset_plugin_sessions();
psx_mod_netplay_set_active(0);
(void)psx_mod_netplay_set_options_identity("");
state().ticket_ready = false;
@@ -2112,8 +2113,10 @@ bool mod_runtime_commit(const std::filesystem::path& disc_path, std::string* err
if (!s.provider_preparing) s.ticket_ready = false;
clear_function_entry_hooks();
s.disc_extents.clear();
s.derived_disc_patches.clear();
s.derived_disc_storage.clear(); s.audio_tracks.clear(); s.extent_start = 0;
s.derived_disc_patches.clear();
s.derived_disc_storage.clear();
s.audio_tracks.clear();
s.extent_start = 0;
s.main_applied = false;
s.error.clear();
psx_mod_counter_add("startup.plan_reuse", 1);
@@ -2873,6 +2876,7 @@ bool mod_runtime_read_disc_file_sectors(const std::string& path, uint32_t max_by
extern "C" void mod_runtime_activate_plugins(void) {
using namespace PSXRecompV4;
mod_reset_plugin_sessions();
HostLaunchTimingScope timing(HOST_LAUNCH_PLUGIN_ACTIVATION);
RuntimeMods& s = state();
s.ticket_ready = false;
+10 -2
View File
@@ -109,14 +109,14 @@ bool native_text_encode(const std::string& text,const NativeGlyphMap& glyphs,
}
bool native_font_compose(NativeFontAtlas& a,uint16_t base,uint16_t dest,
NativeGlyphMark mark,std::string& error) {
NativeGlyphMark mark,std::string& error,uint32_t omit_top_rows) {
error.clear();
if(!a.width || !a.height || !a.cell_width || !a.cell_height ||
a.width%a.cell_width || a.height%a.cell_height || a.width%2 ||
uint64_t(a.width)*a.height!=uint64_t(a.packed.size())*2 ||
a.cell_width>64 || a.cell_height>64) {error="Invalid native font atlas";return false;}
const uint32_t cols=a.width/a.cell_width,cells=cols*(a.height/a.cell_height);
if(base>=cells || dest>=cells || base==dest) {error="Glyph cell outside native atlas";return false;}
if(base>=cells || dest>=cells || base==dest || omit_top_rows>=a.cell_height) {error="Glyph cell outside native atlas";return false;}
auto get=[&](unsigned id,unsigned x,unsigned y) {
const size_t p=((id/cols)*a.cell_height+y)*a.width+(id%cols)*a.cell_width+x;
return uint8_t((a.packed[p/2]>>((p&1)*4))&15);
@@ -129,6 +129,14 @@ bool native_font_compose(NativeFontAtlas& a,uint16_t base,uint16_t dest,
const auto v=get(base,x,y);cell[y*a.cell_width+x]=v;
if(v) {++colors[v];top=std::min(top,y);bottom=std::max(bottom,y);}
}
// Adapter-owned metrics can omit the dot of i before adding an accent.
// Only the copied destination shape changes; source pixels stay intact.
if(omit_top_rows) {
std::fill_n(cell.begin(),omit_top_rows*a.cell_width,0);
top=a.cell_height;bottom=0;
for(unsigned y=omit_top_rows;y<a.cell_height;++y)for(unsigned x=0;x<a.cell_width;++x)
if(cell[y*a.cell_width+x]) {top=std::min(top,y);bottom=std::max(bottom,y);}
}
if(top==a.cell_height) {error="Source font glyph is blank";return false;}
const auto original=cell;
if(mark==NativeGlyphMark::Inverted) std::reverse(cell.begin(),cell.end());
+24
View File
@@ -128,6 +128,30 @@ int main(int argc, char** argv) {
write(disc, iso);
check(load_mod_media(disc, "psx-disc", iso.size(), hash(iso), bytes, &error) && *bytes == iso,
"mixed-mode volume size may extend beyond data track");
// Bounded immutable ISO9660 lookup, including a file crossing sectors.
std::vector<uint8_t> mode2_files(24*2336);
auto dual=[&](size_t at,uint32_t value) {
for(unsigned i=0;i<4;++i) {mode2_files[at+i]=uint8_t(value>>(i*8));mode2_files[at+7-i]=uint8_t(value>>(i*8));}
};
const size_t volume=16*2336+8,dir=20*2336+8;
mode2_files[volume]=1;std::copy_n("CD001",5,mode2_files.begin()+volume+1);mode2_files[volume+6]=1;
dual(volume+158,20);dual(volume+166,2048);
mode2_files[dir]=42;dual(dir+2,21);dual(dir+10,3000);mode2_files[dir+32]=8;
std::copy_n("TEXT.BIN",8,mode2_files.begin()+dir+33);
for(unsigned i=0;i<3000;++i)mode2_files[(21+i/2048)*2336+8+i%2048]=uint8_t(i*7);
ModMode2Disc native(mode2_files.data(),mode2_files.size());
uint32_t found_lba=0,found_size=0;std::vector<uint8_t> found;
check(native.find("text.bin",found_lba,found_size) && found_lba==21 && found_size==3000 &&
native.read("TEXT.BIN",found) && found[2049]==uint8_t(2049*7),"Mode-2 ISO file lookup and cross-sector read");
check(!native.find("../TEXT.BIN",found_lba,found_size),"Mode-2 lookup rejects parent traversal");
mode2_files[dir]=1;
check(!native.find("TEXT.BIN",found_lba,found_size),"short directory record rejected before name-length access");
mode2_files[dir]=42;mode2_files[dir+9]^=1;
check(!native.find("TEXT.BIN",found_lba,found_size),"directory dual-endian fields must agree");
mode2_files[dir+9]^=1;dual(dir+2,24);
check(!native.find("TEXT.BIN",found_lba,found_size),"donor file extent cannot escape snapshot");
dual(dir+2,21);mode2_files[21*2336+2]=0x20;
check(!native.read("TEXT.BIN",found) && found.empty(),"ordinary file reads cannot consume Form-2 XA payload");
for (const char* file : {"rom.z64", "disc.iso", "disc.bin", "disc.cue", "audio.bin"})
fs::remove(root / file);
fs::remove(root);
+73
View File
@@ -301,6 +301,22 @@ static void test_media_plugin(void) {
}
static int derived_reset_calls;
static uint32_t derived_first, derived_second;
static void test_derived_reset(void) { ++derived_reset_calls; }
static void test_derived_plugin(void) {
uint32_t size=0;
if(!psx_mod_read_disc_file("S0/LEVEL.NSF",nullptr,0,&size) || size!=3000) ++failures;
const uint8_t* catalog=nullptr;uint64_t length=0;
if(!psx_mod_current_resource_bytes("catalog",&catalog,&length) || length!=3 || catalog[0]!='e') ++failures;
std::vector<uint8_t> first(5000,0x36),second(7000,0x79);
if(!psx_mod_replace_disc_file("S0/LEVEL.NSF",first.data(),uint32_t(first.size()),&derived_first)) ++failures;
if(!psx_mod_replace_disc_file("S0/LEVEL.NSF",second.data(),uint32_t(second.size()),&derived_second)) ++failures;
uint32_t invalid=99;
if(psx_mod_replace_disc_file("S0",first.data(),uint32_t(first.size()),&invalid) || invalid) ++failures;
if(psx_mod_replace_disc_file("S0/MISSING",first.data(),uint32_t(first.size()),&invalid) || invalid) ++failures;
}
extern "C" {
void* iso_open(const char*);
void iso_close(void*);
@@ -1199,6 +1215,63 @@ int main(int argc, char** argv) {
check(PSXRecompV4::mod_runtime_commit(raw_path,&error),"reader mount raw disc");
check(psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),(uint32_t)result.size(),&bytes) &&
std::equal(result.begin(),result.end(),iso.begin()+22*2048),"raw and ISO reads identical");
/* Derived native files layer without changing owner media. The host and
* emulated drive must agree on the final ISO directory and file bytes. */
{
const auto derived_root=root/"derived-native-file";
const std::vector<uint8_t> bundled_catalog{'e','s','!'};
write_bytes(derived_root/"packages/derived.test/1.0.0/catalog/es.json",bundled_catalog);
write_text(derived_root/"packages/derived.test/1.0.0/manifest.toml",
"format_version = 9\nid = \"derived.test\"\nversion = \"1.0.0\"\nname = \"Derived\"\n"
"[[target]]\ngame_id = \"READER\"\n[[feature]]\nid = \"active\"\nname = \"Active\"\ndefault_enabled = true\n"
"[[resource]]\nfeature = \"active\"\nid = \"catalog\"\nlabel = \"Catalog\"\nformat = \"file\"\nrequired = true\n"
"default = \"catalog/es.json\"\nsize = 3\nsha256 = \""+sha256_hex(bundled_catalog)+"\"\n"
"[[plugin]]\nfeature = \"active\"\nid = \"derived.test.plugin\"\n");
check(psx_mod_register_activation_plugin("derived.test.plugin",test_derived_plugin),"register derived file preparation");
check(psx_mod_register_session_reset_plugin("derived.test.plugin",test_derived_reset),"register session retirement");
check(PSXRecompV4::mod_runtime_initialize(derived_root,"READER",0,{},&error) &&
PSXRecompV4::mod_runtime_commit(raw_path,&error),"verified bundled catalog commits");
const auto resets=derived_reset_calls;
mod_runtime_activate_plugins();
check(derived_reset_calls==resets+1,"retire previous plugin session before preparing assets");
check(derived_first==24 && derived_second==27,"layered native files append stable extents");
std::vector<uint8_t> derived(7000);
check(psx_mod_read_disc_file("S0/LEVEL.NSF",derived.data(),uint32_t(derived.size()),&bytes) &&
bytes==7000 && std::all_of(derived.begin(),derived.end(),[](auto b){return b==0x79;}),"host sees final derived file before drive activation");
uint32_t refused=99;
check(!psx_mod_replace_disc_file("S0/LEVEL.NSF",derived.data(),7000,&refused) && !refused,"replacement is activation only");
mod_runtime_enable_disc_patches();
void* derived_mount=iso_open(raw_path.string().c_str());
std::array<uint8_t,2352> derived_sector{};
check(derived_mount && iso_sector_count(derived_mount)==31,"drive includes both derived layers");
check(iso_read_sector(derived_mount,21,derived_sector.data(),derived_sector.size()) &&
derived_sector[2]==27 && derived_sector[9]==27 && derived_sector[10]==0x58 && derived_sector[11]==0x1b &&
derived_sector[16]==0x1b && derived_sector[17]==0x58,"guest ISO record has final little and big endian extent and size");
check(iso_read_raw_sector(derived_mount,27,derived_sector.data(),derived_sector.size()) &&
derived_sector[15]==2 && derived_sector[18]==8 && derived_sector[22]==8 && derived_sector[24]==0x79,
"guest reads native Mode-2 Form-1 derived data");
check(iso_read_sector(derived_mount,30,derived_sector.data(),derived_sector.size()) &&
derived_sector[855]==0x79 && derived_sector[856]==0,"derived final sector is padded");
mod_runtime_activate_plugins();
check(derived_first==24 && derived_second==27 && iso_sector_count(derived_mount)==31,"reactivation retires old derived state");
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error),"clear derived native files");
check(iso_sector_count(derived_mount)==24 && psx_mod_read_disc_file("S0/LEVEL.NSF",nullptr,0,&bytes) && bytes==3000,
"clearing restores original host and guest extents");
iso_close(derived_mount);PSXRecompV4::mod_runtime_end_netplay();
std::ifstream unchanged(raw_path,std::ios::binary);
check(std::vector<uint8_t>(std::istreambuf_iterator<char>(unchanged),{})==raw_iso,"owner disc remains byte identical");
// scan() migrates legacy packages/ into installed/. Mutate the active
// payload, rather than recreating the retired migration source.
write_bytes(derived_root/"installed/derived.test/1.0.0/catalog/es.json",{'b','a','d'});
write_text(derived_root/"state.toml","format_version = 2\n[[feature]]\npackage_id = \"derived.test\"\nid = \"active\"\nenabled = true\n");
PSXRecompV4::ModPackageManager selected_derived;selected_derived.set_root(derived_root);
check(selected_derived.scan(&error) && selected_derived.load_state(&error) &&
selected_derived.feature_enabled("derived.test","active"),"tampered catalog feature remains selected for verification");
const auto tampered=selected_derived.resolve("READER",{},{});
check(!tampered.ok,"tampered bundled catalog fails resolution");
check(PSXRecompV4::mod_runtime_initialize(derived_root,"READER",0,{},&error),"reload selected bundled catalog");
check(!PSXRecompV4::mod_runtime_commit(raw_path,&error),"modified bundled catalog rejected by SHA-256");
}
/* Verified bytes are scoped to the committed feature, and do not depend
* on reopening an owner file after launch. No launcher is needed here. */
const auto media_root = root / "verified-media";
+5
View File
@@ -39,6 +39,11 @@ int main() {
check(atlas.packed[4*16+10]==0x33 && atlas.packed[5*16+10]==0x23,"base glyph palette and outline copied exactly");
const auto composed=atlas.packed;
check(!native_font_compose(atlas,0,1,NativeGlyphMark::Tilde,error) && atlas.packed==composed,"occupied glyph cannot be overwritten");
NativeFontAtlas dotted;dotted.width=32;dotted.height=16;dotted.packed.resize(256);
dotted.packed[2*16+2]=0x33;dotted.packed[7*16+2]=0x23;
check(native_font_compose(dotted,0,1,NativeGlyphMark::Acute,error,4),"native i dot can be omitted before accenting");
check(dotted.packed[2*16+2]==0x33 && dotted.packed[2*16+10]==0 && dotted.packed[7*16+10]==0x23,
"dot removal preserves source and destination stem");
std::cout<<(failures?"FAIL":"PASS")<<": native translation catalog, Unicode, layout and font composition\n";
return failures?1:0;
}