Skip to content

ODROID-M1 dedicated target: hardkernel BSP kernel + Mali-G52 blob + VU8M overlays - #1

Open
Pyohwan wants to merge 396 commits into
masterfrom
odroid-m1-bsp-kernel
Open

ODROID-M1 dedicated target: hardkernel BSP kernel + Mali-G52 blob + VU8M overlays#1
Pyohwan wants to merge 396 commits into
masterfrom
odroid-m1-bsp-kernel

Conversation

@Pyohwan

@Pyohwan Pyohwan commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a new dedicated buildroot target rk3568-odroidm1 for ODROID-M1, separate from the shared rk3568 target (mainline-kernel/Panfrost, shared by 11 handheld boards — incompatible with our downstream BSP kernel, left untouched).

PR against my own fork, for documentation/review only — not intended for upstream batocera-linux.

Status

Boots end-to-end, EmulationStation renders via GBM/KMSDRM, gameplay verified across multiple systems (NDS, PSP/PPSSPP, Genesis) with noticeably better performance than vanilla Batocera on the same board. Storage pooling, gamepad input, WiFi, and the single-active-display (HDMI ↔ VU8M) toggle are all verified working on real hardware.

What this is

  • Kernel: hardkernel BSP fork (Pyohwan/linux, branch fix-hdmi-dsi-dual-livelock).
  • Kernel config: matches the real device's actual running config (Mali driver generation, DSI/VOP2 panel config).
  • DTB: in-tree rockchip/rk3568-odroid-m1; i2c0/i2c1/spi0 statically merged. display_vu8m ships as a standalone .dtbo, applied at boot time by boot.cmd reading /boot/config.ini's overlays= key (see "Key decisions" below - not extlinux.conf, which this board doesn't ship at all).
  • GPU: mali-g52-odroidm1 package wrapping hardkernel's official Mali blob — only its GBM/DRM EGL platform is used (the blob's Wayland-client EGL platform doesn't work, see below).
  • Rendering: no compositor — BR2_PACKAGE_BATOCERA_WAYLAND_LABWC off, EmulationStation renders directly via KMSDRM (same architecture the rk3326 target uses).
  • U-Boot: u-boot-rockchip.bin (idbloader + u-boot.itb combined into one image, correct offsets baked in by U-Boot's own build) is a from-source mainline 2026.04 build (odroid-m1-rk3568_defconfig — see package/batocera/boot/uboot-odroid-m1/BUILD-NOTES.md for exact provenance), replacing a prebuilt blob of unknown origin end to end - there's no separate untouched Hardkernel SPL stage, the whole combined blob is ours.
  • Board identifier: bsp-odroidm1 (not rk3568-*, avoids an upstream post-image-script.sh filename-collision quirk for subtargets whose name starts with the target family name).

Key decisions

  • GBM/KMSDRM direct rendering instead of Wayland/LabWC. The vendor Mali blob's Wayland-client EGL platform is broken (eglInitialize() fails after a successful eglGetPlatformDisplayEXT(), confirmed on two blob versions); its GBM/DRM EGL platform works fine, so the compositor is skipped entirely.
  • Single active display (HDMI ↔ VU8M) via /boot/config.ini's overlays= key, not DT-baking or DPMS blanking. boot.cmd loads config.ini with U-Boot's ini command (the same mechanism Hardkernel's own official Ubuntu image uses, confirmed by dumping and inspecting its real boot.scr) and fdt applys whatever .dtbo names are listed - empty by default (HDMI-only), set to display_vu8m to enable the panel. One line edit plus a reboot, no image rebuild, and it's read identically by every boot path (SPI-NOR Petitboot via kexec, skip_spiboot direct U-Boot, or this project's own U-Boot if its GPT entry is re-added - see the boot-mechanism decision below). This replaced an earlier extlinux.conf-based design (two static LABELs toggled by DEFAULT) that only covered one of those boot paths; extlinux.conf isn't shipped at all anymore; a syslinux.cfg fallback still exists only for Petitboot builds with a syslinux parser instead of the kexec one, kept manually in sync with config.ini's default. The unwanted DSI panel is never probed by the kernel at all (rather than probed then hidden), so there's nothing left to fight with batocera-switch-screen-checker's hotplug detection. Verified both directions on hardware, including VU8M as the sole active display with HDMI physically disconnected.
  • VU8M rotation key fixed. display.rotate.<connector> uses a different connector-identifier format depending on the consumer: emulationstation-standalone canonicalizes it to batocera-resolution listOutputs's own form (1 - DSI on this board), while the boot splash uses batocera-drminfo's raw name (DSI). The sysconfig default previously used the wrong (splash-style) format for the ES-side key, so rotation silently never applied. Fixed to the correct key and verified live (90° is the correct direction).
  • Discoverable by the on-board SPI-NOR Petitboot via boot.scr, self-boot and Petitboot are mutually exclusive by design (not simultaneous). Petitboot parses boot.ini/boot.scr/kboot.conf/grub.cfg and explicitly not extlinux.conf, so a boot.scr is generated from boot.cmd at build time (with mkimage, rather than committing a binary that can drift out of sync with its source) and placed at the boot partition's root. Two non-obvious constraints, both found by reading upstream Petitboot's own source and confirmed against pb-discover.log on hardware: the load lines must NOT carry a :<part> device suffix (with one, Petitboot mis-resolves the partition index to p1 and every resource stays permanently unresolved), and the boot partition must be GPT entry ODROID-M1 dedicated target: hardkernel BSP kernel + Mali-G52 blob + VU8M overlays #1. The real conflict is with a separate requirement: the SPL locates the main-stage U-Boot by GPT entry name ("uboot") - with that entry present, the boot ROM always runs this card's own bootloader and the SPI-NOR/Petitboot is never reached at all, no matter what's on the card (confirmed live, and matches how the Petitboot author himself describes it on the ODROID forum, t=44346: a storage device carrying its own bootloader outranks Petitboot). So genimage.cfg deliberately leaves the "uboot" GPT entry unregistered (in-partition-table = "no") - this is what makes Petitboot reachable and the boot partition land on GPT entry ODROID-M1 dedicated target: hardkernel BSP kernel + Mali-G52 blob + VU8M overlays #1 by declaration order, at the cost of the card no longer self-booting by default. fw_setenv skip_spiboot true in the Petitboot shell makes the SPI-NOR's own U-Boot distro-boot straight off this card's boot.scr instead of showing the menu (false restores it) - same script either way, so VU8M and everything else behaves identically. Re-adding the "uboot" entry to genimage.cfg is all it takes to go back to direct self-boot, at the cost of Petitboot no longer being reachable. All three paths (Petitboot menu, skip_spiboot, and direct self-boot with the entry re-added) read the same config.ini-driven VU8M toggle identically - verified on hardware.
  • Bug fixes required by the LabWC removal: libbrotlidec.so.1 runtime dependency kept explicit (curl still needs it, nothing else pulls it in anymore); FUSE module now loaded before batocera-storage-manager runs; NVMe multi-partition ROM merge rescoped per-partition instead of per-disk (same fix applied to ignore.drives); a startup race between D-Bus and PipeWire/WirePlumber fixed for Bluetooth audio; RK809 DAC volume fixed to account for the driver clamping to 252/255, not 100%.
  • emulationstation-standalone's second-output lookup switched to batocera-settings-get-master (shared file, applies to every board): the non-master lookup only checks /userdata/system/batocera.conf directly, so an unset user override read back as empty and auto-picked a second output anyway - confirmed live, DSI-1 kept getting auto-selected and DPMS'd back on every boot despite the sysconfig default. Flagged by review as worth noting since it's a shared-file behavior change; any other board with a global.videooutput2 sysconfig default would see the same fix (a strict improvement over the previous silently-broken lookup, not board-specific).

Known issues (root-caused, not yet fixed)

  • Bluetooth: can't hold a gamepad (HID) and an A2DP speaker connected simultaneously — chip-level RTL8822BU limitation (A/B/C tested against Intel built-in / RTL8821CU / RTL8822BU), not fixable in software. Planning to switch to Hardkernel's RTL8821CU dongle (WiFi Module 5BK).
  • Mono speaker plays quieter than expected (no longer silent, just low volume).
  • ES screen sharpness perception vs. vanilla — unresolved (resolution confirmed identical).
  • Standalone melonDS is non-functional post-pivot (no compositor; both xcb and eglfs Qt platforms fail) — NDS testing in this PR used a libretro core instead.

Notable fixes bundled in (packaging)

  • mali-g52-odroidm1: pkgconfig, missing headers, a symbol alias, /etc/ld.so.preload entry, current mesa3d-headers, and (2nd review round) the sibling mali-G52 package's DMA-BUF/format-modifier feature selects that this variant was missing.
  • buildroot submodule bumped (836eafc95d26b3) for the mesa3d-headers version fix this board's mesa3d-headers dependency needs (26.1.4 → 26.1.5).
  • Fixed an inaccurate comment in configs/batocera-rk3568-odroidm1.board claiming none of the shared rk3568/patches ffmpeg/mpv patches apply here - actually 4 of 5 are byte-identical copies kept in sync by hand, only the HEVC one genuinely doesn't apply. Added patches-source.txt provenance files.
  • rocknix-joypad / TRX / moonlight-qt / SDL3_X11: excluded for this target only, no other board affected.
  • xone: extended its existing older-kernel workaround to also cover BR2_KERNEL_HEADERS_6_1.
  • dosbox-x: fixed a duplicate --disable-avcodec flag in DOSBOX_X_CONF_OPTS (the real one already exists unconditionally upstream/on the shared rk3568 target for the ffmpeg 8.1.2 API breakage; this branch had accidentally added a second, redundant copy).

Test plan

  • Full buildroot build completes clean, image generated with checksums
  • Boots on real hardware through kernel/init/all services
  • EmulationStation renders correctly via GBM/KMSDRM, no crash-restart loop
  • Gameplay verified across multiple systems (NDS, PSP/PPSSPP, Genesis), noticeably better performance than vanilla Batocera on the same board
  • Storage pooling (NVMe multi-partition ROMs merge) working
  • WiFi (NEXT-1300WBT/RTL8822BU via rtw88) working
  • Gamepad Bluetooth HID working
  • Bluetooth A2DP audio pairs and plays when connected alone
  • u-boot-rockchip.bin replaced with a mainline 2026.04 build from this board's real upstream defconfig; validates and boots the new FIT correctly
  • VU8M toggled via /boot/config.ini's overlays= key — verified both directions from a clean reboot, including VU8M as the sole active display with HDMI physically disconnected
  • VU8M rotation verified live (correct key + direction)
  • Runtime-edited config.ini (via SSH, no image rebuild) read correctly by U-Boot's ini command
  • Audio follows active display (HDMI sink vs. internal speaker) based on live DRM state; Bluetooth still takes priority when connected
  • ES frame pacing / perceived FPS variance fixed via system.cpu.governor=interactive
  • Gamepad + Bluetooth audio simultaneously — still fails once both are actively connected (chip-level RTL8822BU limitation, see "Known issues")
  • Simultaneous dual display (HDMI+VU8M at once) — not supported; single-output-at-a-time via config covers the actual use case
  • Listed automatically as "batocera.linux" in the SPI-NOR Petitboot menu (default state - "uboot" GPT entry unregistered), and boots from there through kexec into our own 6.1 kernel and on to EmulationStation, VU8M toggle honoured via config.ini same as any other path
  • fw_setenv skip_spiboot true in the Petitboot shell distro-boots straight off this card's boot.scr instead, same config.ini-driven behavior

🤖 Generated with Claude Code

https://claude.ai/code/session_f66fcf92-a0dc-4745-94a0-4fa46afc8b55
https://claude.ai/code/session_011nACLgqFd8ijkjYT67HXrv
https://claude.ai/code/session_66b26045-42fa-4151-b4bb-4a185e068a06

@Pyohwan
Pyohwan marked this pull request as ready for review July 25, 2026 09:28
Pyohwan added a commit that referenced this pull request Jul 25, 2026
…ion order)

The SD card now shows up as "batocera.linux" in the on-board SPI-NOR
Petitboot menu on its own, and booting that entry kexecs into our real
6.1 kernel through to EmulationStation - verified end to end on hardware.

Three things were needed, each of which had been broken by an earlier
commit in this branch that judged them dead from our own boot chain's
point of view only:

- boot.scr had to exist at all. Petitboot's parser handles boot.ini /
  boot.scr / kboot.conf / grub.cfg and explicitly not extlinux.conf (per
  the ODROID-M1 Petitboot author on the forum), so removing boot.cmd as
  "dead code" silently removed the only thing Petitboot could recognise.
  It is now generated from boot.cmd at build time with mkimage instead of
  being a committed binary, so the two can't drift apart again.

- The load commands must not carry a ":<part>" device suffix. Petitboot
  only defers resolution to another device when the resource string
  contains a colon (upstream create_devpath_resource()); with
  "${devnum}:${partition}" it substituted a device name but got the
  partition index wrong and every resource stayed unresolved forever
  (pb-discover.log: "resource depends on device /dev/mmcblk1p1" x3, then
  "boot option mmcblk1p2#batocera.linux is unresolved"). Without the
  colon it resolves against the partition being scanned, which is right.
  This matches Armbian's own Petitboot-compatible boot.scr.

- The boot partition has to be #1, so U-Boot no longer takes a GPT table
  slot. That GPT entry was added earlier for the vendor wiki's sake and
  UART capture had already shown SPL locates the FIT purely by fixed
  sector offset, so it bought nothing - and cost the partition ordering
  Petitboot effectively depends on. Layout is back to vanilla Batocera's.

Nothing here changes direct SD boot: bootstd still picks extlinux.conf
first (confirmed by UART), so the VU8M DEFAULT-label toggle keeps working
as before. Booting via Petitboot always uses the base DTB (kexec has no
FDTOVERLAYS equivalent), i.e. HDMI-only - documented in boot.cmd.
Pyohwan added a commit that referenced this pull request Jul 25, 2026
… need

SPL needs U-Boot in a GPT entry literally named "uboot" (locates it by
label, per Hardkernel's wiki - not "SPL doesn't care about the partition
table", which an earlier commit on this branch mistakenly concluded from
seeing "Trying fit image at 0x4000 sector" without checking what happens
if the named entry is absent). Petitboot's device scan only finds a
bootable OS on partition #1 (confirmed live: with the boot partition at
#2, entries are parsed correctly - "boot option ... is unresolved" is
gone - but Petitboot still lists nothing at all).

Both are satisfied simultaneously by declaring the boot partition first
in genimage.cfg: genimage numbers GPT entries by declaration order, not
by on-disk offset (confirmed by reading genimage's own overlap-checking
code, which only rejects actual overlaps). So the boot partition becomes
GPT #1 for Petitboot while "uboot" keeps its own named entry (now #3) at
its original 8M offset for the SPL. Physical layout on the card is
unchanged either way.

Verified end to end on hardware in this exact image: SPL finds u-boot.itb
fine at GPT #3 ("Trying fit image at 0x4000 sector" -> all atf/fdt checks
OK), U-Boot's bootstd sees the boot partition as "part 1" and boots
extlinux.conf's "batocera-vu8m" label correctly (VU8M active, correctly
rotated), and Petitboot lists and boots "batocera.linux" from the same
card. All three requirements hold in the same image at last.
Pyohwan added a commit that referenced this pull request Jul 26, 2026
…ion order)

The SD card now shows up as "batocera.linux" in the on-board SPI-NOR
Petitboot menu on its own, and booting that entry kexecs into our real
6.1 kernel through to EmulationStation - verified end to end on hardware.

Three things were needed, each of which had been broken by an earlier
commit in this branch that judged them dead from our own boot chain's
point of view only:

- boot.scr had to exist at all. Petitboot's parser handles boot.ini /
  boot.scr / kboot.conf / grub.cfg and explicitly not extlinux.conf (per
  the ODROID-M1 Petitboot author on the forum), so removing boot.cmd as
  "dead code" silently removed the only thing Petitboot could recognise.
  It is now generated from boot.cmd at build time with mkimage instead of
  being a committed binary, so the two can't drift apart again.

- The load commands must not carry a ":<part>" device suffix. Petitboot
  only defers resolution to another device when the resource string
  contains a colon (upstream create_devpath_resource()); with
  "${devnum}:${partition}" it substituted a device name but got the
  partition index wrong and every resource stayed unresolved forever
  (pb-discover.log: "resource depends on device /dev/mmcblk1p1" x3, then
  "boot option mmcblk1p2#batocera.linux is unresolved"). Without the
  colon it resolves against the partition being scanned, which is right.
  This matches Armbian's own Petitboot-compatible boot.scr.

- The boot partition has to be #1, so U-Boot no longer takes a GPT table
  slot. That GPT entry was added earlier for the vendor wiki's sake and
  UART capture had already shown SPL locates the FIT purely by fixed
  sector offset, so it bought nothing - and cost the partition ordering
  Petitboot effectively depends on. Layout is back to vanilla Batocera's.

Nothing here changes direct SD boot: bootstd still picks extlinux.conf
first (confirmed by UART), so the VU8M DEFAULT-label toggle keeps working
as before. Booting via Petitboot always uses the base DTB (kexec has no
FDTOVERLAYS equivalent), i.e. HDMI-only - documented in boot.cmd.
Pyohwan added a commit that referenced this pull request Jul 26, 2026
… need

SPL needs U-Boot in a GPT entry literally named "uboot" (locates it by
label, per Hardkernel's wiki - not "SPL doesn't care about the partition
table", which an earlier commit on this branch mistakenly concluded from
seeing "Trying fit image at 0x4000 sector" without checking what happens
if the named entry is absent). Petitboot's device scan only finds a
bootable OS on partition #1 (confirmed live: with the boot partition at
#2, entries are parsed correctly - "boot option ... is unresolved" is
gone - but Petitboot still lists nothing at all).

Both are satisfied simultaneously by declaring the boot partition first
in genimage.cfg: genimage numbers GPT entries by declaration order, not
by on-disk offset (confirmed by reading genimage's own overlap-checking
code, which only rejects actual overlaps). So the boot partition becomes
GPT #1 for Petitboot while "uboot" keeps its own named entry (now #3) at
its original 8M offset for the SPL. Physical layout on the card is
unchanged either way.

Verified end to end on hardware in this exact image: SPL finds u-boot.itb
fine at GPT #3 ("Trying fit image at 0x4000 sector" -> all atf/fdt checks
OK), U-Boot's bootstd sees the boot partition as "part 1" and boots
extlinux.conf's "batocera-vu8m" label correctly (VU8M active, correctly
rotated), and Petitboot lists and boots "batocera.linux" from the same
card. All three requirements hold in the same image at last.
@Pyohwan
Pyohwan force-pushed the odroid-m1-bsp-kernel branch from af639ea to e1efa96 Compare July 26, 2026 08:21
Pyohwan added a commit that referenced this pull request Jul 27, 2026
…ion order)

The SD card now shows up as "batocera.linux" in the on-board SPI-NOR
Petitboot menu on its own, and booting that entry kexecs into our real
6.1 kernel through to EmulationStation - verified end to end on hardware.

Three things were needed, each of which had been broken by an earlier
commit in this branch that judged them dead from our own boot chain's
point of view only:

- boot.scr had to exist at all. Petitboot's parser handles boot.ini /
  boot.scr / kboot.conf / grub.cfg and explicitly not extlinux.conf (per
  the ODROID-M1 Petitboot author on the forum), so removing boot.cmd as
  "dead code" silently removed the only thing Petitboot could recognise.
  It is now generated from boot.cmd at build time with mkimage instead of
  being a committed binary, so the two can't drift apart again.

- The load commands must not carry a ":<part>" device suffix. Petitboot
  only defers resolution to another device when the resource string
  contains a colon (upstream create_devpath_resource()); with
  "${devnum}:${partition}" it substituted a device name but got the
  partition index wrong and every resource stayed unresolved forever
  (pb-discover.log: "resource depends on device /dev/mmcblk1p1" x3, then
  "boot option mmcblk1p2#batocera.linux is unresolved"). Without the
  colon it resolves against the partition being scanned, which is right.
  This matches Armbian's own Petitboot-compatible boot.scr.

- The boot partition has to be #1, so U-Boot no longer takes a GPT table
  slot. That GPT entry was added earlier for the vendor wiki's sake and
  UART capture had already shown SPL locates the FIT purely by fixed
  sector offset, so it bought nothing - and cost the partition ordering
  Petitboot effectively depends on. Layout is back to vanilla Batocera's.

Nothing here changes direct SD boot: bootstd still picks extlinux.conf
first (confirmed by UART), so the VU8M DEFAULT-label toggle keeps working
as before. Booting via Petitboot always uses the base DTB (kexec has no
FDTOVERLAYS equivalent), i.e. HDMI-only - documented in boot.cmd.
Pyohwan added a commit that referenced this pull request Jul 27, 2026
… need

SPL needs U-Boot in a GPT entry literally named "uboot" (locates it by
label, per Hardkernel's wiki - not "SPL doesn't care about the partition
table", which an earlier commit on this branch mistakenly concluded from
seeing "Trying fit image at 0x4000 sector" without checking what happens
if the named entry is absent). Petitboot's device scan only finds a
bootable OS on partition #1 (confirmed live: with the boot partition at
#2, entries are parsed correctly - "boot option ... is unresolved" is
gone - but Petitboot still lists nothing at all).

Both are satisfied simultaneously by declaring the boot partition first
in genimage.cfg: genimage numbers GPT entries by declaration order, not
by on-disk offset (confirmed by reading genimage's own overlap-checking
code, which only rejects actual overlaps). So the boot partition becomes
GPT #1 for Petitboot while "uboot" keeps its own named entry (now #3) at
its original 8M offset for the SPL. Physical layout on the card is
unchanged either way.

Verified end to end on hardware in this exact image: SPL finds u-boot.itb
fine at GPT #3 ("Trying fit image at 0x4000 sector" -> all atf/fdt checks
OK), U-Boot's bootstd sees the boot partition as "part 1" and boots
extlinux.conf's "batocera-vu8m" label correctly (VU8M active, correctly
rotated), and Petitboot lists and boots "batocera.linux" from the same
card. All three requirements hold in the same image at last.
@Pyohwan
Pyohwan force-pushed the odroid-m1-bsp-kernel branch from 1d68de7 to d9476a7 Compare July 27, 2026 12:32
Pyohwan and others added 21 commits August 4, 2026 23:37
…t.cfg

Adds a named inputConfig entry for "SHAKS S6b 52e1 Win-Mac" (hotkey=select,
matching batocera's own convention) so this pad works out of the box on a
fresh reflash instead of requiring the user to redo "CONFIGURE INPUT" every
time /userdata gets wiped. Mapping copied verbatim from the live-configured
/userdata/system/configs/emulationstation/es_input.cfg on the device.
BIOS lives only on the SD card today (plain directory on mmcblk1p2, never
unioned with anything) - every reflash wipes it entirely. This bit us
2026-08-04: a prior reflash's BIOS restore only copied back 2 of 26 files
from the manual NVMe backup, silently breaking neogeo (missing neogeo.zip
-> FBNeo couldn't find sp-s3.sp1) and leaving several other systems' BIOS
missing too (PSX, Saturn, GBA/GB, PC Engine CD, ...).

New dedicated init script (kept separate from batocera-storage-manager,
which hardcodes a single roms-only pool throughout and would be riskier to
extend given BIOS is a launch-time dependency for every emulator) mirrors
the same category.search=newest mergerfs union pattern already used for
roms: .bios_base (SD, mostly empty after first boot) + /media/ROMs/bios
(NVMe, where the actual content now lives) merged onto /userdata/bios.

On first run, migrates whatever is currently sitting directly in
/userdata/bios onto the NVMe branch (not .bios_base) - the SD-card side
needs to end up empty, otherwise the content would just get wiped by the
next reflash exactly as before, defeating the point.

Live-tested on device: mount succeeds, neogeo.zip (and everything else)
readable through the merged path, migration correctly landed content on
NVMe (df shows /userdata/bios backed by the 510G pool, not the 53G SD
card), idempotent on re-run (no stacked mounts).
mergerfs earns its complexity when two branches both hold real, coexisting
content that needs merging (roms' .roms_base factory-demo content
alongside the real NVMe library). BIOS has no such split by design - the
SD-card side is meant to stay empty and NVMe holds everything - so a FUSE
union with category.search/category.create policy was solving a problem
that doesn't exist here. A plain `mount --bind` does the same job (NVMe's
/media/ROMs/bios transparently appears at /userdata/bios, any future write
lands directly on NVMe) with no FUSE process and no policy to get wrong.

Live-tested on device: unmounted the previous mergerfs union, redeployed,
confirmed `mount` shows a straight bind of nvme0n1p3 rather than a fuse.*
entry, neogeo.zip still readable through the bind path, idempotent on
re-run (no stacked mounts).
…/fbneo)

pcsx_rearmed is CPU-bound (dynarec + GTE) like FBNeo, not GPU-bound like
N64 - live tests showed the same "interactive" governor lagging under
load: Beatmania ~44fps, Soul Blade mostly 59fps with occasional drops to
44fps. Same fix as n64.powermode/fbneo.powermode: pin CPU to "performance"
during gameplay only via the existing powermode_launch_hooks.sh mechanism.
configgen's global default (configgen-defaults.yml) is hud_support: true
with no n64-specific override anywhere upstream, so every rk3568 board
inherits mangohud wrapping mupen64plus by default. The Mali blob doesn't
implement whatever EGL extension function mangohud's inject_egl hooks
need, so SDL_CreateWindow fails outright inside mupen64plus - audio plays,
video never comes up (confirmed on ODROID-M1 with both g13p0 and g24p0
blobs). This was previously worked around live via SSH (n64.hud_support=0
in /userdata/system/batocera.conf) but never actually persisted anywhere,
so it silently regressed on every SD-card reflash since - same class of
mistake as S19fbneokorean's missing +x bit.

Fixed properly this time in configgen-defaults-rk3568.yml (a real
per-architecture defaults file already used for the same kind of fix -
see the adjacent psx hud_support:false entry, "duckstation doesn't like
it on n2+" - not a live/SSH-only workaround this time), so it applies to
any rk3568 board, not just this one.

Live-tested via batocera-settings-set n64.hud_support false for immediate
verification without waiting on a rebuild.
User-confirmed live via per-game ADVANCED GAME OPTIONS override on New
Super Mario Bros. (Korea) - noticeably less stutter with CPU pinned to
performance during gameplay. Same underlying cause as n64/fbneo/psx
(CPU-bound emulation, "interactive" governor lags behind load), made
system-wide since it's not specific to that one game.
…ode menu instead

n64/fbneo/psx/nds.powermode=highperformance were all measured and
confirmed live, but a system-specific override always wins over ES's
own Game Settings > Power Mode (global) - user wants a single global
knob instead of needing to touch source for every system that benefits.
Measurements kept in Joplin in case per-system defaults are wanted again.
…cera-linux#16176)

`gosu` resets `HOME` from the first `/etc/passwd` entry matching the
target UID. Ubuntu images include an "ubuntu" user at UID 1000, and
`useradd -o` adds another entry rather than replacing it, so explicitly
restore `HOME` after `gosu` resolves the target user.
Qt defaulted to xcb and aborted (Aborted signal 6) - this board has neither
X11 nor a working Wayland-client EGL path (vendor blob bug, see board
defconfig). ENABLE_WAYLAND=ON here was wrong since it was written before
that bug was documented. eglfs (direct KMS/GBM, confirmed working) is
correct, needs libinput for its KMS input backend.

Live-tested: window/game boot now works end to end (DRM mode switch to
1920x1080 confirmed, no GPU faults). Does NOT fix performance - melonDS's
own 3D renderer still logs "Failed to create OpenGL context, falling back
to Software Renderer" and self-persists that fallback to melonDS.toml on
every run, because it wants desktop GL like the old libretro-melonds core
did and this blob only has GLES. Software-only 3D is too slow to play on
this CPU. Whether melonDS ever gets GPU accel here now depends entirely on
the planned Panfrost A/B test.
Generator assumed every target has Wayland or X11/XWayland and defaulted
to xcb otherwise - boards with neither (headless KMS/DRM direct, like
odroid-m1) got Qt aborting immediately with no platform plugin available.
Same failure class just fixed for standalone melonDS.
…ate cores

Both were excluded by upstream select conditions that don't reflect real
technical requirements for this board:
- kronos: gated to x86 only, but its own .mk already supports several
  ARM+GLES boards (odroid-xu4/n2, rockpro64, odroid-c4) via FORCE_GLES=1 -
  added an RK3568 branch reusing the odroid-c4 mapping. Untested on real
  hardware, yabasanshiro stays the default fallback.
- libretro-melonds-ds: gated behind BR2_PACKAGE_BATOCERA_XWAYLAND, which is
  off on this board for an unrelated reason (Wayland-client EGL is broken
  on this Mali blob, see board defconfig comment) - melonds-ds's actual
  deps (cmake/libpcap/libstdc++) don't need XWayland at all. The already-
  built libretro-melonds is confirmed dead on this blob (wants desktop GL,
  board only has GLES); melonds-ds has a GL-free Software render mode
  worth trying.

Neither confirmed working yet - next build should surface whether they
even compile for this target, then need real hardware testing.
libretro-kronos unconditionally depends on full mesa3d, which buildroot
refuses to combine with mesa3d-headers (our vendor blob's header source).
Needs kronos.mk patched before this can be retried - keeping melonds-ds
(no such conflict) enabled for tonight's build.
qt6wayland is no longer needed now that ENABLE_WAYLAND=OFF (melonds.mk);
libinput is needed for the eglfs platform plugin's KMS backend and was
already added to MELONDS_DEPENDENCIES but never select'd here, which
buildroot's Kconfig consistency check rejects outright.
…re real system folders for merge prompts

Two related fixes, both confirmed live on the 4-partition NVMe drive that
exposed them:

1. REQUEST_MERGE sent the *disk's* unique id (get_unique_id, serial/WWN-
   based) for its 'ignore forever' button, not the partition's. A drive
   with several partitions - two leftover EmuELEC install partitions
   alongside the real ROMs partition - meant permanently ignoring either
   junk partition silently also blocked the real ROMs partition's merge
   on every subsequent boot, including fresh SD reflashes where the
   'already configured, keep it' guard (added earlier for the reboot
   case) doesn't apply yet since mergerfs.roms hasn't been written at
   all. Added get_partition_unique_id (filesystem UUID/PARTUUID-based)
   and send that in the notification instead - no ES-side change needed,
   Window.cpp just echoes back whatever id it was given via
   ignoreDevicePermanently(). REQUEST_FORMAT (whole-disk, no partition
   table yet) is correctly left disk-scoped.

2. The merge-candidate check was just "does a roms/ directory exist",
   so leftover install partitions with their own unrelated roms/ folder
   (in this case, EmuELEC's own roms layout) got prompted too. Added
   is_valid_roms_dir: requires at least one subdirectory matching a real
   batocera system folder, checked against the on-device skeleton
   (/usr/share/batocera/datainit/roms/<system>) rather than a hardcoded
   list.

Live-verified on hardware: p1/p2 (EmuELEC leftovers) correctly rejected
and stay individually ignored; p3 (real ROMs partition) no longer
inherits their ignore status.
… with no compositor/X11, disable mangohud on rk3568 flycast systems

SDL2 auto-probes video (wayland->x11->...) and audio (pipewire needs
XDG_RUNTIME_DIR) drivers with no fallback for compositor-less KMS/DRM-only
boards, so standalone flycast failed outright on ODROID-M1 ("Can't load
EGL/GL library on window creation", audio silently disabled). Same
generic fix pattern as duckstationGenerator's earlier eglfs fallback
(cfc6624), applied to flycast's SDL_VIDEODRIVER/SDL_AUDIODRIVER/
XDG_RUNTIME_DIR instead of Qt's QT_QPA_PLATFORM.

Also hit the same mangohud+Mali-blob EGL crash already fixed for n64
(b1f1989) once the driver detection was fixed - extended that
configgen-defaults-rk3568.yml hud_support:false pattern to dreamcast/
naomi/atomiswave/systemsp (all share the flycast core).

Live-verified on-device via scp'd configgen + batocera-settings-set
before committing: video renders and audio plays with no mangohud crash.
- due to boot & i/o failures
move gameforce ace back to the 6.1 kernel
Pyohwan and others added 30 commits August 26, 2026 23:12
Follow-up to d05f5c6. User feedback: before we actually cut a tagged
release, keep batocera.version identical to upstream's own dev-build
identifier ("44-dev-<hash> <date>") rather than rebranding every
untagged build as "batocera44-odroidm1-dev-<hash>". The missing commit
hash that started this whole investigation was purely the git-worktree
docker mount bug (fixed in 520d806) - upstream's format was never
broken, our build environment just couldn't populate it. Our own
release-tag format (batocera44-odroidm1-vX.Y.Z) still kicks in
correctly once HEAD is exactly on one of those tags - that part is
unchanged, since that's a genuine, deliberate identifier for an actual
release, not a rebrand of ordinary dev builds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei
Live-verified auto core/API selection against the benchmarked table
(project_odroid_retro.md) game-by-game. Genesis/FBNeo/Saturn/PSX all
matched. N64 didn't: it launched glide64mk2 instead of rice.

Root cause: ES's "auto" choice isn't determined by es_systems.yml's core
listing order - it's computed by comparing each (emulator, core) pair
against this board's configgen-defaults emulator/core keys (see
python-src/batocera-es-system's registry.py, get_systems_metadata). The
earlier "auto order" fix (mentioned in project_odroid_retro.md) reordered
es_systems.yml's n64 core list, which has zero effect on that comparison
- the actual default (mupen64plus + glide64mk2) was never overridden
here, so it fell through to the shared configgen-defaults.yml value the
whole time.

Auditing the same mechanism for the other benchmarked standalone-emulator
systems found two more real mismatches (not yet live-tested, but the
same class of bug, confirmed via source inspection):

- gamecube/wii: emulator was "libretro" (copied verbatim from the shared
  configgen-defaults-rk3568.yml), which picks libretro-dolphin - not the
  standalone dolphin (emulator: dolphin) this board actually benchmarks
  and needs for the Wayland-only OpenGL fix. dolphinGenerator.py already
  defaults to Vulkan unless gfxbackend=OGL is set, so no extra option
  needed for the API side.
- dreamcast: no override at all, falling through to the shared file's
  libretro-flycast default instead of standalone flycast. Also added
  flycast_renderer=4 (forces Vulkan) since flycastGenerator.py's own
  default is OpenGL, unlike dolphin/ppsspp which already default to
  Vulkan.

psp needed no change: ppssppConfig.py already defaults to
"3 (VULKAN)" when gfxbackend is unset, matching the benchmark
recommendation as-is.

Not yet live-verified on hardware (n64 was; gamecube/wii/dreamcast
weren't reached yet in the ongoing verification pass) - flagged in
project_odroid_retro.md for the next round of testing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei
Continuation of the auto core/API verification pass (n64/gamecube/wii/
dreamcast fixed earlier). PS2's auto default was set to pcsx2, which is
the core we already determined is structurally broken on this board's
Mali blob (DATA_INVALID_FAULT GPU faults - see project_odroid_retro.md's
"Mali G52 blob 구조적 한계" section). Confirmed live: auto silently
launched pcsx2, dmesg showed "GPU fault 0x5b from job slot 0", and the
game visibly hung.

Switched the default to aethersx2 (the actually-recommended, working
choice). pcsx2 stays installed/selectable in ES as a fallback, just no
longer the silent default that most users would hit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei
…doring

Follow-up to 4dc8fe5 (PS2 auto default fix). Live-testing that fix
surfaced a real launch failure: "libXrandr.so.2: cannot open shared
object file" - a leftover gap from AetherSX2's prebuilt-binary NEEDED
list that nothing had exercised before, since PS2's auto default had
been silently pointing at pcsx2 this whole time.

Initially vendored libXrandr.so.2 + its libXext/libXrender/libX11
transitive closure from Debian arm64 packages (same technique already
used for the GL/GLX/curl/openssl shims in aethersx2/libs/), but per
feedback: unlike those, there's no ABI/missing-feature reason libX11's
family can't just be built normally via buildroot's own xlib_libX11/
libXext/libXrender/libXrandr packages - so build them for real instead.

Reverted the vendored copies, added a select chain to aethersx2's
Config.in instead (same flattening pattern as melonds/Config.in - these
xlib_* Config.in entries all live inside "if BR2_PACKAGE_XORG7", so a
select landing on one of them doesn't reliably cascade to that symbol's
own further selects). Confirmed live: libXrandr/libXext/libXrender/libX11
all built successfully via buildroot and installed into the standard
/usr/lib, which aethersx2Generator.py's LD_LIBRARY_PATH already covers -
no changes needed there.

No performance impact: aethersx2Generator.py forces QT_QPA_PLATFORM=
wayland, so the Qt xcb platform plugin path that actually pulls
libXrandr in at runtime (display mode/refresh-rate queries) is never
taken - same "NEEDED-pass only" story as the GL/GLX shims that already
exist in aethersx2/libs/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei
Convenience fix requested during VU8M setup testing: enabling VU8M
(overlays=display_vu8m) already requires editing config.ini, which is
the one file on the PC-visible FAT32 boot partition. batocera.conf (the
"correct" place for display.rotate) lives on the userdata partition
(ext4, not readable from a plain Windows/Mac file browser), and
batocera-boot.conf - while technically fine to hand-edit here (nothing
regenerates it wholesale; batocera-config only ever amends specific
keys like sharedevice/rootshadowpassword via targeted sed, never
display.rotate) - is a second file to open for no real reason when
config.ini is sitting right there.

config.ini is checked first, ahead of the existing batocera-boot.conf
path, so setting VU8M up (enable + rotate) is now a single-file edit.
Verified live: "Rotation from config.ini: [1]" logged, correct
800x1280 crop and -o 1 fbv rotation applied.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei
…rd README

batocera-backglass-window.cpp's font candidate lists (both header and
description) only ever contained DejaVu Sans / Liberation Sans - both
Latin-only, so any Korean text (game titles, backglass metadata)
rendered as missing-glyph boxes on the VU8M backglass screen. This is a
narrow gap in an otherwise Korean-font-aware system: ES itself already
ships a bundled Korean fallback font (PyeojinGothic-Medium, compiled
into the ES binary as a resource), and this board already has Noto
Sans KR + NanumGothic installed as real font files (via the vanilla
upstream nanum-font package, unrelated to our fork) - backglass's own
hardcoded candidate lists just never got updated to include either.
Prepended NotoSansKR-VF.ttf (checked first) to both candidate lists -
it's a variable font with full Latin+CJK coverage in one file, so one
font renders both scripts consistently (SDL_ttf can't font-switch
mid-string anyway). Falls back to the existing DejaVu/Liberation chain
if Noto isn't present. Compiled and deployed live; full visual
confirmation still pending (testing got sidetracked investigating an
unrelated HDMI+VU8M simultaneous-connection issue - see below).

Also adds README-odroidm1.md: repo-root, Korean, covers what's added
over vanilla Batocera, VU8M/HDMI display setup (including the config.ini
single-file rotation trick from the commit below), and the full
per-system performance/auto-core table. Iterated heavily with the user
this session - still being refined, not final.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei
…anism

Live testing on-device found that S65values4boot already syncs
display.rotate/display.rotate.<connector> from batocera.conf into
batocera-boot.conf on every shutdown/reboot (vanilla mechanism) - the
config.ini duplication added in 58fcc13 was unnecessary, since the
boot splash's existing batocera-boot.conf fallback already picks up
the correct value with no extra config needed.

Also fixes the real bug behind this session's dual-display report: VU8M
rotation is controlled by global.videooutput2/display.rotate2.<connector>
in batocera.conf (set via the ES MULTISCREENS menu, or directly with
batocera-settings-set), not by anything in batocera-boot.conf or
config.ini - those only ever affect the primary output. Verified live:
global.videooutput2=DSI-1 + display.rotate2.DSI-1=3 (270 degrees) gives
correct portrait orientation on VU8M.

README updated to describe the actual mechanism instead of the
config.ini workaround.
…README

270 degrees was this particular panel's measured correct orientation,
not a general fact about VU8M - actual mounting can vary per unit.
- Remove the standalone "boot partition file editing" section - stale
  instructions, redundant with each subsection already saying where its
  own file lives.
- Shrink the HDMI+VU8M "not recommended together" caveat down to the
  one hardware fact that's still true (DSI has no hotplug detection) -
  today's live testing showed no actual splash-rotation problem once
  global.videooutput2 is set correctly, so the earlier elaborate warning
  overstated a non-issue.
- Rename "화면 회전" to "화면 회전값 설정" and drop the
  global.videooutput2 example from it (that's the previous section's
  concern, not rotation) - keep only rotation-specific keys/commands here.
- Move the SHARE-partition-init caveat to the top of the section (it's
  a precondition for everything below, not an afterthought).
- Split the reboot out of the settings command block with a one-line
  explanation: EmulationStation only reads/applies videooutput2 and
  rotation at its own startup, so a plain batocera-settings-set has no
  effect until the next boot.
Both the primary and backglass screens have an option literally
labeled "SCREEN ROTATION", which is confusing without their full menu
paths (confirmed against GuiMenu.cpp): SYSTEM SETTINGS > HARDWARE for
the main screen, vs. SYSTEM SETTINGS > HARDWARE > MULTISCREENS >
BACKGLASS / INFORMATION SCREEN for VU8M.
Same upstream snapshot merged into the C track (odroid-m1-g29p1,
commit cbeaa11) on 2026-08-17, applied here to odroid-m1-bsp-kernel
so PR #4's diff reflects genuine A-vs-C track differences instead of
upstream drift. Merged with --allow-unrelated-histories (upstream's
2026-08-04 history squash has no shared ancestor with this fork).

243 conflicted files. 228 were files this branch never customized -
resolved by taking origin's version wholesale (git checkout --theirs).
The other 14 (this board's own S02resize/audio-config/power-mode/
Ssplash-mpv/es_input.cfg/emulationstation-standalone/storage-manager/
melonDS/libretro-core-info/libretro-kronos/batocera.conf fixes) were
reconciled via git merge-file: base = the file's content immediately
before this branch's own customizing commit, ours = this branch's
current version, theirs = origin's new version - applies this branch's
own diff cleanly on top of upstream's latest instead of blindly
preferring either side. All 14 merged with zero remaining conflict
markers except melonds.mk, whose 2 conflicting hunks (X11/libinput
dependency gate, ENABLE_WAYLAND rationale comment) are pure odroid-m1
board-specific additions absent from both base and origin - resolved
by keeping this branch's version.

buildroot submodule conflict resolved by keeping this branch's own pin
(fork/odroidm1-mpv-gbm-fix) - already only 1 trivial, unrelated commit
(webkitgtk build fix) behind origin/master, unlike C's buildroot fork
which needed real reconciliation.

Also fixed 6 files that lost their executable bit during manual
reconciliation (git show | cp does not preserve file mode).
…the ES version string

ES's version label is literally "BATOCERA.LINUX ES V" + BATOCERA_SYSTEM_VERSION,
so using the git tag (batocera44-odroidm1-vX.Y.Z) verbatim doubled up into
"...ES Vbatocera44-odroidm1-v1.0.0". Strip the tag down to "44-odroidm1-X.Y.Z"
for the version string while keeping the tag itself unchanged (still reads
well in GitHub's release list) - final ES label becomes
"BATOCERA.LINUX ES V44-odroidm1-1.0.0 <build date>", matching the shape of
upstream's own "V44-dev-<hash>".
…s ambiguous merge-base

Both odroid-m1-bsp-kernel and odroid-m1-g29p1 independently merged the
same upstream squash commit (e38b00f) with --allow-unrelated-histories,
which left git (and GitHub's PR diff) with two candidate merge-bases and
no single common ancestor - PR #4's diff stayed at ~390-540 files instead
of shrinking to the genuine A-vs-C track difference.

This merge changes no C-track content (tree is identical to the previous
commit) - it only records odroid-m1-bsp-kernel's current tip as an
ancestor of this branch, giving the two branches one unambiguous shared
commit going forward so PR #4's diff resolves to the real difference.
- Add the missing fbneo_korean_libretro.info symlink (present in the A
  track, missing here) - fbneo_korean is the default FBNeo core on this
  board and README-odroidm1.md advertises it, but without this symlink
  RetroArch can't read the core's name/extensions/options metadata.
- Remove the board fsoverlay copy of batocera-upgrade - confirmed
  byte-identical to the shared package/batocera/core/batocera-scripts
  copy (the local-var/$? fix already landed in both), so the fsoverlay
  copy does nothing now except silently re-shadow the shared file on
  the next upstream sync.
- Revert the erroneous "except ValueError, ImportError: is Python 2
  syntax" fix from 52523fc - it's valid PEP 758 syntax (unparenthesized
  multi-exception except clauses), accepted starting Python 3.14, which
  this build already targets (pyproject.toml target-version = "py314",
  buildroot PYTHON3_VERSION 3.14.5). Confirmed via `ruff format
  --target-version py314`, which actively strips the parentheses back
  out - the original upstream code was correct.
- Remove aethersx2Generator.py's unused imports (Mapping, SystemConfig,
  Guns, Input, DeviceInfoMapping, Resolution) - the 6 ruff-check
  failures that were breaking CI on this branch only, since this file
  never ran through CI before (per 1st/2nd review rounds). Auto-fixed
  with `ruff check --fix`.

All four verified with a local ruff 0.16.5 install (check + format
--diff), matching this repo's pyproject.toml configuration.
Zansword was an incorrect name attached to the credit in an earlier
session - the correct name/casing is DsNo. Also describe the patch more
precisely: it's a CRC-tolerance fix (accepts Korean-translated ROMs that
fail the standard CRC32 check), not a text/UI translation.
README-odroidm1.md -> README-odroidm1.ko.md (unchanged content, just
renamed), plus a new README-odroidm1.en.md English translation. Both
files cross-link to each other at the top. Using ISO 639-1 "ko"/"en"
language codes rather than "kr" (that's the ISO 3166-1 country code for
South Korea, not a language code).
… v1.0.0

g29p1 Vulkan blob + Wayland for ODROID-M1 (C안) — full system matrix complete, release prep
…parate odroid-m1 branch

odroid-m1-bsp-kernel is the actual ongoing release branch (where PR #4
just merged the g29p1/Wayland work) - keeping a separate odroid-m1
branch purely to host the updates/ manifest tree adds an extra branch
to remember to update on every release with no real benefit over just
committing it here directly.

Also copies the current updates/bsp-odroidm1/stable/last/batocera.version
manifest (content identical to the released batocera44-odroidm1-v1.0.0
image's own batocera.version) so the new URL resolves immediately -
switching the pointer without this would 404 for anyone building from
this branch before the next real release.

The old odroid-m1 branch is left as-is (not deleted) since the
already-shipped v1.0.0 image's batocera.conf still points there - only
affects builds from this branch going forward.
odroidm1: point updates.url at odroid-m1-bsp-kernel instead of the separate odroid-m1 branch
… fallback

The earlier fix (e8ea822) prepended NotoSansKR-VF.ttf to the font
candidate lists, picked once at startup by "first file that exists" -
this fixed Korean text but would regress any other script that font
covers only partially: NotoSansKR-VF.ttf has just 66 Cyrillic codepoints
against DejaVu Sans's full 256, so Cyrillic game titles would start
rendering as missing-glyph boxes to fix Korean ones (caught before this
ever shipped, via a question about whether the same gap affects other
languages).

SDL2_ttf has no per-glyph fallback chain (that's an SDL3_ttf-only
feature), so a single fixed font picked once renders every string for the
rest of the session regardless of what that string contains.

Replaced with: keep DejaVu/Liberation as the primary choice exactly as
upstream has it, and open NotoSansKR-VF.ttf as a fallback-only font. A new
pickFontForText() decodes each string to Unicode codepoints and checks
TTF_GlyphIsProvided32() against the primary font; only if the primary is
missing a glyph the string actually needs does it fall back to the
Korean/CJK font for that string. Checked per string (game title, system
name, description), not globally - a Cyrillic title still uses DejaVu
(unaffected) while a Korean title uses the fallback.

The new decode/coverage-check logic (utf8ToCodepoints + pickFontForText)
was verified with a standalone host-side unit test against ASCII, Korean,
Cyrillic, and mixed-script strings, all resolving to the expected font.
Not yet rebuilt and re-flashed to real hardware - pending before this (or
an equivalent patch against current upstream, which has since rewritten
this file with GIF-animation/Wayland support) goes to batocera-linux
upstream.
…overage-aware fallback

odroidm1: replace naive Korean font prepend with glyph-coverage-aware fallback
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants