From e78f881ed1a98e159b12f3ff90283661f1dde9e8 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sat, 22 Aug 2026 12:38:29 +0900 Subject: [PATCH 01/40] Embed the TV's Chromium (libcbe) in a native app /usr/lib/libcbe.so is the engine every webOS web app already runs inside, and it is a plain shared library: a native app can link it and get a real web view with no WAM and no web app package. There is no SDK for it - no headers on the device or in the NDK, and the library is stripped - so the ABI this sample links against was reconstructed from two sources that agree with each other: the firmware symbol tables, and the vtables of WAM's own BlinkWebView and WebAppWaylandWindow, which are the only in-firmware subclasses. The sample's shape is the inversion at the centre of the API. WebOSMain() is Chromium's content main: it takes the process over and never returns, so there is no "initialise the web view, then carry on". The seam is the one WAM uses - libcbe pumps the default GMainContext on its browser UI thread, so work queued there before WebOSMain() runs on that thread once Chromium is up. Three things in the headers are load-bearing, and two of them were found the hard way: WebViewDelegate has no virtual destructor, it is 24 slots long rather than the 17 that have recoverable names (a short vtable segfaults a few hundred milliseconds into the first page load), and Chromium's -fno-rtti means no typeinfo is exported for these classes. webOS 4 only, and both bounds are ABI rather than caution: webOS 3's libstdc++ has no C++11 std::string ABI to call these exports with, and webOS 5 replaced the free WebOSMain() with a webos::WebOSMain class. Verified on a 49LK5900 (webOS 4.4.3): the page loads and paints, and the window registers with LSM and SAM as the foreground card. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- CMakeLists.txt | 1 + README.md | 25 +++-- assets/icons/web-cbe.png | Bin 0 -> 2546 bytes assets/make-icon.py | 40 +++++--- web/cbe/CMakeLists.txt | 44 +++++++++ web/cbe/README.md | 136 +++++++++++++++++++++++++ web/cbe/cbe_stub.cpp | 82 +++++++++++++++ web/cbe/main.cpp | 154 +++++++++++++++++++++++++++++ web/cbe/webos/webapp_window_base.h | 66 +++++++++++++ web/cbe/webos/webview_base.h | 146 +++++++++++++++++++++++++++ 10 files changed, 675 insertions(+), 19 deletions(-) create mode 100644 assets/icons/web-cbe.png create mode 100644 web/cbe/CMakeLists.txt create mode 100644 web/cbe/README.md create mode 100644 web/cbe/cbe_stub.cpp create mode 100644 web/cbe/main.cpp create mode 100644 web/cbe/webos/webapp_window_base.h create mode 100644 web/cbe/webos/webview_base.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a6dc381..0ed4fd3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,4 +33,5 @@ if (TARGET_WEBOS) add_subdirectory(media/smp) add_subdirectory(media/ndl) add_subdirectory(media/lgnc) + add_subdirectory(web/cbe) endif () diff --git a/README.md b/README.md index 7612638..966653c 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,17 @@ Small, readable sample apps for LG webOS native homebrew. Each one does a single thing and is meant to be read top to bottom, without a framework in the way. -The media samples feed raw elementary streams straight into the TV's hardware decoder, with -SDL2 owning the window and the remote control. There are two entirely different ways to -reach that decoder, and all of them get samples: **starfish-media-pipeline** -(`libplayerAPIs`), **NDL**, and **LGNC** (the LG NetCast Open API). +Most of them are media samples: they feed raw elementary streams straight into the TV's +hardware decoder, with SDL2 owning the window and the remote control. There are three +entirely different ways to reach that decoder, and all of them get samples: +**starfish-media-pipeline** (`libplayerAPIs`), **NDL**, and **LGNC** (the LG NetCast Open +API). + +`web/cbe` is the odd one out. It links `libcbe.so` - the TV's own Chromium, the engine +every web app on the box already runs inside - and puts a real web view in a *native* app, +with no WAM and no web app package. There is no SDK for that library; the sample's headers +were reconstructed from firmware symbol tables and from the vtables of WAM's own +subclasses, and `web/cbe/README.md` writes down how. ## What is here @@ -21,6 +28,8 @@ media/ esplayer/ libndl-directmedia2, NDL_Esplayer* - webOS 2.x to 3.4 directmedia/ libNDL_directmedia, NDL_Direct* - webOS 3.5+, built for API v1 and v2 lgnc/ liblgncopenapi, LGNC_DIRECT* - webOS 1 to 4, one binary for all of them +web/ + cbe/ libcbe - the TV's own Chromium, embedded in a native app ``` The same two files play through all three stacks. Comparing the three `main.c` files is the @@ -104,11 +113,12 @@ and skips everything else. ### Icons -`assets/icons/.png` carries white artwork on transparency - the API name, a play -mark, and which variant it is - and no colour at all. Each sample passes a Material 500 colour to `webos_add_ipk`, which +`assets/icons/.png` carries white artwork on transparency - the API name, a mark (a +play triangle for the media samples, a globe for the web ones), and which variant it is - +and no colour at all. Each sample passes a Material 500 colour to `webos_add_ipk`, which writes it to both `iconColor` and `bgColor` in `appinfo.json`, and webOS paints that behind the glyph. Families are grouped by hue: blues for starfish, greens for NDL, orange for -LGNC. +LGNC, blue for the web view. Both fields are set deliberately: `bgColor` is the tile background, while `iconColor` fills behind the icon itself - without it the launcher's default shows through the glyph's @@ -181,6 +191,7 @@ hard to diagnose from the TV side. | `media/smp/webos5` | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): exported window accepted, full load / play / feed / EOS / unload, 300 video + 470 audio units on both. Those runs predate the `Play()` ordering fix, which all SMP samples share - re-run pending | | `media/ndl/directmedia` (v2) | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): 300 video + 469 PCM chunks on both | | `media/ndl/directmedia` (v1) | 3.5 - 4.x | built and symbol-verified, needs a 2017-2019 set to test | +| `web/cbe` | 4.x | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | | `media/smp/webos1` | 1.x | not written yet - and there is no webOS 1 hardware here to validate it against, so it would ship untestable | ## What a `Play()` that returns true does not tell you diff --git a/assets/icons/web-cbe.png b/assets/icons/web-cbe.png new file mode 100644 index 0000000000000000000000000000000000000000..517296b6f2efce5967a0a09bd7a9c57f0ef8d354 GIT binary patch literal 2546 zcmVy z+E~%XRMA9f8hs%4*0`J{@ynWx~ z=FXgZ&fGiao|!XqX6}U+T4>=Sh81-qNtWDCW~M>-PD3@`v<SNj8)+8WNJ)B$cXM zYp+)ED&-8BzbMr0Ve)TX(&fN-pdC15X8USY_Hd#u3QQ{~=?>siz#L#4&;}dFtqch01zrQ51y-2ZfoO>p*`o1d_~; zDs#7@iY?3AdV_milc-1cryGO2>0$mAZ1#df>(2N1rnAvej?Pk`OlPOWT zNtJ=clGYHEG#i-a^lsoG1t*es24&!8Nl%kC;fSSX)>GwWoi}P}f$@RI0b>Z2DFM4w zeUMv*ii2PQll_Q@9{EW#+bOAyGnFr3k{@}D%ROgJP!A{4PpE8EO6MTF)nJ5S&N@#| z1wKru_}3&|W@cYXoJfkpz<(-~dGtJk~B)v&ZJZSjng}F{3W-dRCX{Y^3}wN zoRl;*xt7ZP+{^Ct^QogA^{!c=KA>$gvlGByo!k#Bk~G)M`qGM+S=r16sx@z5l*06F zjOYL0X8|3+HC5#Lz3h{IK6PApEzd+<-U{%bpUZ?Myjs!%LJ%SLdvEo@?MJF~>hc*w zpQJ9}UV`!>@4+hf?vgZH(i}g}^7B=-s$5;6YKjFM>!=jjTLDIL~CFwO)N3W!t61i=I z@J6haban1NLy=pT%G3(=jK-9FTzDm6$~K?iP1_06t#07=X7-|dY$oY0U<#p`#$W=3 z@!mdQlbNme^3#AtUM@u(PHi)@$J9WPoU@`H%|#xpsCBvLi@HCES2ij#I)NCSKFvG2 z!Uub%ir#bxa*#Qt7FuYbg@}gfjPuAlibnOE4W&RI7?-Hf;V0MXHZ40JG=u_OIhHET zX=6QmER7{9nNJt`pcOFe3RJR6CTRw+1o!|jlPqAc8|VT40<1Q(&3V}oiDW7^UD6{J zre81mk;gCPYbo^@tX~vRwjgwyq}N?B&q#XQ)5{ayDQj_mvpyZA6ZIFl=nqf z#!-a@cqDZz1Ry#R{tl;Y^!%;LA~eg=GMgSrv%S?7@T#PFPFw7>`#j&CDEmIYw%GI2 z6vAVY^o6wSriDU8+9XX?i1!_mG{>*Cc|TZI=o3v@INqFAcAoeWCP~_%5bwLr`I=XW zNyS&V<=pQ14p-D}N!y)fiCkPW+3q~0*u*6*^s?4^zFtxG;f1p3@ip!MVq7mOVwx*r zrRURy_1KJ8I?puEclvpi)8Fg)!mO~!w-xX8F+zB2xm#AG&+zk>LCT2NB5#Zhe$Vr_ zNBV_7?$A)A=ZbULMV|g69v5`$oZ{E_cs^ZdMwU#sNDC z7PyHlGA-b0U^=i7c%w>DV}a{{U0zOn{xUENcr!4|%(ltbWa?3$6h#j{q)v$FI=?CKSJ1Wxs$M8dOTqx(0Jeh=UL+Ui3v|k$K9+C zB=jpi{Vd_-?023*nsFf(=*_oeX8n@30txOyxzS{g$NnOZvWoM?H`z?2y7v)fJL%t|U8LXoxo_ z|KKL~dF*(g%dXu5{>glFdU~G2ZOhQykoc8I2 zwprN%Qg_m~btUu|P<_nv@q6T^R$y#;AM)l_mh_<0e&h-mkaR*}@o1u?W6t-xq@0_a zcfX{`ZjGq|C#&Gleo;$+ae(_g@L$64>D^9SK-f(56yg60a|rumMmt~p(tHB&L&E>F zK51rq&Fp`KA6r0(*z7NQ7*mJ>ldT?i?HVf!q3NM z5*FYmNV-GeOZE6yk51rjdBnOIn{n(1zwAw}S78xJlD_TzoFr{fSo4siU6L+!xwx!Z z4Fi7O;{1ge$8hXW{ga>w4j-Udh`>l za|6wY#EV{{FpP=llk|3l8Ai0o_}jdDT&BHBS(hpdfuv!Yi4?YvB>A(^aYDZq|DUjx zY_rfaPKy?`&c)05vOJmoh$l@KhC(}9WkG-|$o53PVP<>$`Xz*|zD7{sE4h`rC?B8O zfVU77UMB3lPMdfOjgDb)^{VnyZk <- play mark + SMP <- which API + |> <- play mark, or a globe for the web samples w4 <- which variant of it Colour alone was not enough to tell eight tiles apart, and the two lines say which API and @@ -21,7 +21,7 @@ SIZE = 80 SS = 8 # supersample, for clean edges -# target name (minus the "media-" prefix) -> (API, variant) +# target name (minus the "media-" prefix) -> (API, variant[, mark]) ICONS = { "smp-acb-webos2": ("SMP", "w2"), "smp-acb-webos3": ("SMP", "w3"), @@ -31,6 +31,7 @@ "ndl-directmedia-v1": ("NDL", "DM1"), "ndl-directmedia-v2": ("NDL", "DM2"), "lgnc": ("LGNC", "1-4"), + "web-cbe": ("CBE", "w4", "globe"), } @@ -40,7 +41,26 @@ def centred(d, text, font, cy, n): text, font=font, fill=(255, 255, 255, 255)) -def make(path, api, variant): +def play_mark(d, cx, cy, h): + w = h * 0.87 + d.polygon([(cx - w / 2, cy - h / 2), (cx - w / 2, cy + h / 2), (cx + w / 2, cy)], + fill=(255, 255, 255, 255)) + + +def globe_mark(d, cx, cy, h): + """A ring with one meridian and one parallel - enough to read as "web" at 80px.""" + r = h / 2 + t = max(2, int(h * 0.075)) + d.ellipse([cx - r, cy - r, cx + r, cy + r], outline=(255, 255, 255, 255), width=t) + d.ellipse([cx - r * 0.45, cy - r, cx + r * 0.45, cy + r], + outline=(255, 255, 255, 255), width=t) + d.line([cx - r, cy, cx + r, cy], fill=(255, 255, 255, 255), width=t) + + +MARKS = {"play": play_mark, "globe": globe_mark} + + +def make(path, api, variant, mark="play"): n = SIZE * SS img = Image.new("RGBA", (n, n), (0, 0, 0, 0)) d = ImageDraw.Draw(img) @@ -49,11 +69,7 @@ def make(path, api, variant): centred(d, api, ImageFont.truetype(FONT, int(n * (0.19 if len(api) > 3 else 0.23))), n * 0.16, n) - cx, cy = n * 0.53, n * 0.50 - h = n * 0.30 - w = h * 0.87 - d.polygon([(cx - w / 2, cy - h / 2), (cx - w / 2, cy + h / 2), (cx + w / 2, cy)], - fill=(255, 255, 255, 255)) + MARKS[mark](d, n * 0.53 if mark == "play" else n * 0.50, n * 0.50, n * 0.30) centred(d, variant, ImageFont.truetype(FONT, int(n * 0.21)), n * 0.85, n) @@ -64,7 +80,7 @@ def make(path, api, variant): here = os.path.dirname(os.path.abspath(__file__)) out_dir = os.path.join(here, "icons") os.makedirs(out_dir, exist_ok=True) - for name, (api, variant) in ICONS.items(): + for name, spec in ICONS.items(): out = os.path.join(out_dir, f"{name}.png") - make(out, api, variant) - print(f"{out} {api}/{variant}") + make(out, *spec) + print(f"{out} {spec[0]}/{spec[1]}") diff --git a/web/cbe/CMakeLists.txt b/web/cbe/CMakeLists.txt new file mode 100644 index 0000000..f830dc1 --- /dev/null +++ b/web/cbe/CMakeLists.txt @@ -0,0 +1,44 @@ +# libcbe - the TV's Chromium, embedded in a native app. +# +# webOS 4 only, and that bound is real rather than cautious. Below it, webOS 3 +# ships a libstdc++ without the C++11 std::string ABI that libcbe's own exports +# are mangled with, so nothing can call this API there without statically linking +# a newer runtime. Above it, webOS 5 replaced the free WebOSMain() with a +# webos::WebOSMain class and changed WebViewBase's constructor, and webOS 6 added +# a second, parallel API under neva_app_runtime - so 5 and up want their own +# variant of this sample rather than a wider version range on this one. + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GLIB REQUIRED glib-2.0) + +# The NDK has no libcbe, so build something with the right SONAME to link against. +# Never installed - the TV's own library is what resolves these at runtime. +# libcbe.so carries no version suffix on the TV, and the SONAME is what makes the +# loader pick the real one up, so it has to match exactly - no SOVERSION here. +add_library(cbe-stub SHARED cbe_stub.cpp) +set_target_properties(cbe-stub PROPERTIES OUTPUT_NAME cbe) +target_include_directories(cbe-stub PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +# Chromium is built without RTTI and exports no typeinfo for these classes, so +# anything deriving from them has to agree. +target_compile_options(cbe-stub PRIVATE -fno-rtti) + +add_executable(web-cbe main.cpp) +target_include_directories(web-cbe PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_include_directories(web-cbe SYSTEM PRIVATE ${GLIB_INCLUDE_DIRS}) +target_link_directories(web-cbe PRIVATE ${GLIB_LIBRARY_DIRS}) +target_link_libraries(web-cbe PRIVATE cbe-stub ${GLIB_LIBRARIES}) +target_compile_options(web-cbe PRIVATE -fno-rtti) + +# No RPATH at all: the stub above must not be shipped, and the TV resolves libcbe +# from /usr/lib like every other system library. +set_target_properties(web-cbe PROPERTIES + SKIP_BUILD_RPATH TRUE + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH "") + +webos_add_ipk( + TARGET web-cbe + APPID org.webosbrew.sample.web.cbe + TITLE "CBE WebView (webOS 4)" + COLOR "#4285F4" + WEBOS_VERSIONS ">=4, <5") diff --git a/web/cbe/README.md b/web/cbe/README.md new file mode 100644 index 0000000..21ee1e0 --- /dev/null +++ b/web/cbe/README.md @@ -0,0 +1,136 @@ +# libcbe - the TV's Chromium in a native app + +`/usr/lib/libcbe.so` is the "Chromium Browser Engine": a ~70 MB build of Chromium with +LG's webOS patches, and the thing every web app on the TV actually runs inside. It is a +plain shared library with a C++ ABI, and a native app can link against it and get a real +web view - no WAM, no web app package, no `type: "web"`. + +There is no SDK for this. No headers ship on the device or in the NDK, and the library is +stripped. What this sample links against was reconstructed, and the reconstruction is the +interesting part, so it is written down below. + +## What it does + +Hands the process to Chromium, waits to be called back on the browser UI thread, and puts +a page on screen as a normal foreground card: + +``` +main() + | + +- g_idle_add(CreateWebApp) queue work on the default GMainContext + | + +- WebOSMain(argc, argv) never returns + | + +- ... Chromium starts, begins pumping the default GMainContext ... + | + +- CreateWebApp() on the browser UI thread + WebAppWindowBase::InitWindow + SetWindowProperty("appId", ...) + WebViewBase::Initialize + LoadUrl + window->AttachWebContents(webview->GetWebContents()) +``` + +That inversion is the whole shape of the sample. `WebOSMain()` is Chromium's content main: +it takes the process over, re-execs this same binary for the renderer, owns the message +loop, and does not return. So there is no "initialise the web view, then carry on" - the +app has to give the process away and arrange to be called back. + +The seam is the one WAM uses. libcbe drives its browser UI thread from the **default** +`GMainContext`, which is how WAM's Luna service ends up running on that thread: WAM's +`main()` starts its LS2 service, then calls `WebOSMain()`, and the callbacks arrive on the +browser thread afterwards. Anything queued onto that context before `WebOSMain()` therefore +runs exactly once, on the browser UI thread, as soon as Chromium is up - which is the first +moment a window or a web view may legally be created. + +## webOS 4 only, and why + +| release | libcbe | entry point | verdict | +|---|---|---|---| +| 1.x, 2.x | absent | - | no engine to link | +| 3.4 - 3.9 | yes | `WebOSMain` | libstdc++ 6.0.19: no C++11 `std::string` ABI | +| **4.x** | **yes** | **`WebOSMain`** | **this sample** | +| 5.x | yes | `webos::WebOSMain::Run` | different entry point and `Initialize` | +| 6.x - 11.x | yes | `webos::WebOSMain::Run`, plus `neva_app_runtime` | a second, parallel API | + +Both bounds are ABI, not caution. Below: libcbe's own exports are mangled with +`std::__cxx11::basic_string`, but webOS 3's system libstdc++ does not export +`GLIBCXX_3.4.21`, so nothing can call this API there without statically linking a newer +runtime. Above: webOS 5 replaced the free `WebOSMain()` with a `webos::WebOSMain` class and +changed `WebViewBase`'s constructor, and webOS 6 added a whole second API under +`neva_app_runtime`. Those want their own variant rather than a wider range on this one. + +The `webos::` API itself is present unchanged from 3.4 all the way to 11.2, so a webOS 5+ +variant is a small delta, not a rewrite. + +## Where the headers came from + +Two independent sources that agree with each other: + +* **Firmware symbol tables** (`dev-toolbox-cli/common/data/*/libcbe.so.json`) give every + exported name, and therefore every signature, across every release. +* **WAM's own vtables.** `libWebAppMgr.so` contains `BlinkWebView` and + `WebAppWaylandWindow`, the only in-firmware subclasses of these classes. Their vtables + pin down slot order, and their constructors pin down object size - `operator new(32)` + followed by a `BlinkWebView` that writes its first field at offset 8 says + `webos::WebViewBase` is exactly 8 bytes: a vptr and one pimpl pointer. + +Three facts in `webos/webview_base.h` are load-bearing: + +* `WebViewDelegate` has **no virtual destructor**. Adding one shifts every slot by two. +* The delegate is **24 slots** long. Only the first 17 have recoverable names; libcbe still + indexes past them, and a short vtable reads whatever follows it in memory. Leaving them + out segfaults a few hundred milliseconds into the first page load - which is exactly how + they were found. +* Chromium is built **without RTTI** and exports no typeinfo for these classes, so anything + deriving from them must be compiled `-fno-rtti` too. + +`WebAppWindowDelegate`, by contrast, *does* have a virtual destructor, so its `event()` +lands at slot 2. The two classes are not symmetric. + +## Linking + +The NDK has no libcbe, so the build makes a stand-in: `cbe_stub.cpp` is compiled into a +shared object with SONAME `libcbe.so` and nothing else in it. The loader picks up the TV's +real library at runtime because the SONAME matches. It is never installed - see +`BUNDLE_LIBS` being absent from the `webos_add_ipk` call. + +The stub is written as ordinary C++ against the same headers the sample uses, rather than +as a list of mangled names, so the two cannot drift apart. + +## Resources + +None need shipping. The system libcbe has `/usr/lib/cbe/webos_resources.pak` compiled in, +and finds `icudtl.dat`, the V8 snapshots and the locale paks next to it. (LG's own browser +app bundles a private copy of all of that, plus its own libcbe - that is a different, much +heavier arrangement, and not one homebrew wants.) + +## Running it + +``` +cmake --build build --target web-cbe-install +ares-launch -d org.webosbrew.sample.web.cbe +``` + +Launch it, do not run the binary from a shell: SAM sets `APPID` and `XDG_RUNTIME_DIR` and +gives the process the session it needs. Direct execution mostly works but is not the thing +being demonstrated. + +Progress shows up in `/var/log/messages` under the `web-cbe` tag (`journalctl` is not on +the PATH these shells get), and the sample's own delegate output goes to stdout. + +To see what actually reached the screen - LSM will happily report a foreground surface that +drew nothing: + +```sh +ares-shell -d -r "luna-send -n 1 -w 10000 -f \ + luna://com.webos.service.tv.capture/executeOneShot \ + '{\"path\":\"/tmp/shot.png\",\"method\":\"DISPLAY\",\"format\":\"PNG\"}'" +ares-pull -d /tmp/shot.png . +``` + +## What is not here + +* **Input.** The sample never calls `ForwardWebOSEvent()`, and whether remote-control keys + reach the page on their own has not been tested. +* **Lifecycle.** No SAM relaunch/close handling, no `PalmSystem` bridge, no suspend on + background - all of which is most of what WAM actually does. +* **webOS 5+.** See the table above. diff --git a/web/cbe/cbe_stub.cpp b/web/cbe/cbe_stub.cpp new file mode 100644 index 0000000..4a2dc83 --- /dev/null +++ b/web/cbe/cbe_stub.cpp @@ -0,0 +1,82 @@ +// Link-time stand-in for the TV's /usr/lib/libcbe.so. +// +// The NDK has no libcbe and no stub for it, so the build makes one: a shared +// object with the right SONAME and the right symbol names, and nothing else. +// The loader picks up the TV's real 70 MB library at runtime because the SONAME +// matches; this file is never installed. +// +// It is written as ordinary C++ against the same headers the sample uses, rather +// than as a hand-written list of mangled names, so the two cannot drift apart. +// If a declaration changes, the stub changes with it. + +#include "webos/webapp_window_base.h" +#include "webos/webview_base.h" + +#define STUB(...) \ + { return __VA_ARGS__; } + +extern "C" int WebOSMain(int, const char**) STUB(0) + +namespace webos { + +WebViewBase::WebViewBase() : webview_(0) STUB() +WebViewBase::~WebViewBase() STUB() + +void WebViewBase::Initialize(const std::string&, const std::string&, const std::string&, + const std::string&, const std::string&, int, int, bool) STUB() +void* WebViewBase::GetWebContents() STUB(0) +void WebViewBase::LoadUrl(const std::string&) STUB() +void WebViewBase::StopLoading() STUB() +void WebViewBase::Reload() STUB() +bool WebViewBase::CanGoBack() const STUB(false) +std::string WebViewBase::GetUrl() STUB(std::string()) +std::string WebViewBase::DocumentTitle() const STUB(std::string()) +std::string WebViewBase::DefaultUserAgent() const STUB(std::string()) +void WebViewBase::RunJavaScript(const std::string&) STUB() +void WebViewBase::ForwardWebOSEvent(WebOSEvent*) STUB() +void WebViewBase::EnableInspectablePage() STUB() +void WebViewBase::UpdatePreferences() STUB() +void WebViewBase::SetAppId(const std::string&) STUB() +void WebViewBase::SetAppPath(const std::string&) STUB() +void WebViewBase::SetTrustLevel(const std::string&) STUB() +void WebViewBase::SetUserAgent(const std::string&) STUB() +void WebViewBase::SetVisible(bool) STUB() +void WebViewBase::SetFocus(bool) STUB() +void WebViewBase::SetViewportSize(int, int) STUB() +void WebViewBase::SetHardwareResolution(int, int) STUB() +void WebViewBase::SetTransparentBackground(bool) STUB() +void WebViewBase::SetBackgroundColor(int, int, int, int) STUB() +void WebViewBase::SetAllowLocalResourceLoad(bool) STUB() +void WebViewBase::SetAllowUniversalAccessFromFileUrls(bool) STUB() +void WebViewBase::SetFileAccessBlocked(bool) STUB() +void WebViewBase::SetWebSecurityEnabled(bool) STUB() +void WebViewBase::SetLocalStorageEnabled(bool) STUB() +void WebViewBase::SetJavascriptCanOpenWindows(bool) STUB() +void WebViewBase::SetSupportsMultipleWindows(bool) STUB() +void WebViewBase::SetShouldSuppressDialogs(bool) STUB() +void WebViewBase::SetDisallowScrollingInMainFrame(bool) STUB() +void WebViewBase::SetVisibilityState(WebPageVisibilityState) STUB() +void WebViewBase::SetFontHinting(FontRenderParams) STUB() + +WebAppWindowBase::WebAppWindowBase() : webapp_window_(0) STUB() +WebAppWindowBase::~WebAppWindowBase() STUB() +void WebAppWindowBase::InitWindow(int, int) STUB() +void WebAppWindowBase::Show() STUB() +void WebAppWindowBase::Hide() STUB() +void WebAppWindowBase::SetCustomCursor(CustomCursorType, const std::string&, int, int) STUB() +void WebAppWindowBase::AttachWebContents(void*) STUB() +void WebAppWindowBase::DetachWebContents() STUB() +void WebAppWindowBase::RecreatedWebContents() STUB() +void WebAppWindowBase::Activate() STUB() +void WebAppWindowBase::Deactivate() STUB() +void WebAppWindowBase::Resize(int, int) STUB() +void WebAppWindowBase::SetOpacity(float) STUB() +void WebAppWindowBase::SetWindowHostState(NativeWindowState) STUB() +NativeWindowState WebAppWindowBase::GetWindowHostState() const STUB(NATIVE_WINDOW_DEFAULT) +void WebAppWindowBase::SetWindowProperty(const std::string&, const std::string&) STUB() +void WebAppWindowBase::SetUseVirtualKeyboard(bool) STUB() +void WebAppWindowBase::SetKeyMask(WebOSKeyMask, bool) STUB() +int WebAppWindowBase::DisplayWidth() STUB(0) +int WebAppWindowBase::DisplayHeight() STUB(0) + +} // namespace webos diff --git a/web/cbe/main.cpp b/web/cbe/main.cpp new file mode 100644 index 0000000..d1ddd5d --- /dev/null +++ b/web/cbe/main.cpp @@ -0,0 +1,154 @@ +// Embedding the TV's own Chromium in a native webOS app. +// +// Every other sample in this repo calls a media API and keeps control of its own +// main loop. This one is the opposite, and that inversion is the point: libcbe +// exports WebOSMain(), which is Chromium's content main. It takes the process +// over - dispatching the renderer and GPU subprocesses, owning the message loop - +// and does not return until the browser shuts down. There is no "initialise the +// web view, then carry on". +// +// So the app hands the process over and arranges to be called back. The seam is +// the one WAM itself uses: libcbe drives its browser UI thread from the *default* +// GMainContext, which is how WAM's Luna service ends up running on that thread. +// Anything queued onto that context before WebOSMain() therefore runs exactly +// once, on the browser UI thread, as soon as Chromium is up - which is the first +// moment a window or a web view may legally be created. + +#include + +#include +#include +#include +#include +#include + +#include "webos/webapp_window_base.h" +#include "webos/webview_base.h" + +extern "C" int WebOSMain(int argc, const char** argv); + +namespace { + +const char kAppId[] = "org.webosbrew.sample.web.cbe"; +const char kUrl[] = "https://example.com/"; + +// None of this is needed to get a page on screen - the sample implements the +// whole delegate so it doubles as documentation of what libcbe calls back. +class SampleWebView : public webos::WebViewBase { + public: + void LoadProgressChanged(double progress) override { + printf("[cbe] progress %3.0f%%\n", progress * 100); + } + void DidFirstFrameFocused() override { puts("[cbe] first frame focused"); } + void LoadVisuallyCommitted() override { puts("[cbe] visually committed"); } + void TitleChanged(const std::string& title) override { + printf("[cbe] title '%s'\n", title.c_str()); + } + void NavigationHistoryChanged() override {} + void Close() override { puts("[cbe] close requested"); } + bool DecidePolicyForResponse(bool, int status, const std::string& url, + const std::string&) override { + printf("[cbe] response %d %s\n", status, url.c_str()); + return false; // false: let Chromium handle it + } + void LoadStarted() override { puts("[cbe] load started"); } + void LoadStopped() override { puts("[cbe] load stopped"); } + void DidStartNavigation(const std::string& url, bool) override { + printf("[cbe] navigate %s\n", url.c_str()); + } + void DidFinishNavigation(const std::string&, bool) override {} + void LoadFinished(const std::string& url) override { + printf("[cbe] finished %s\n", url.c_str()); + } + void LoadFailed(const std::string& url, int code, const std::string& desc) override { + printf("[cbe] FAILED %s (%d %s)\n", url.c_str(), code, desc.c_str()); + } + void LoadAborted(const std::string& url) override { + printf("[cbe] aborted %s\n", url.c_str()); + } + void DocumentLoadFinished() override { puts("[cbe] document loaded"); } + void RenderProcessCreated(int pid) override { printf("[cbe] renderer pid %d\n", pid); } + void RenderProcessGone() override { puts("[cbe] renderer gone"); } +}; + +class SampleWindow : public webos::WebAppWindowBase { + public: + bool event(WebOSEvent*) override { return false; } +}; + +SampleWindow* g_window; +SampleWebView* g_webview; +std::string g_app_path; + +// Runs on Chromium's browser UI thread, once. +gboolean CreateWebApp(gpointer) { + g_window = new SampleWindow(); + g_window->InitWindow(1920, 1080); + // Without this the surface still reaches the screen, but LSM reports it as a + // card with an empty appId: no lifecycle, no place in the recents list, and + // nothing for the Home key to come back to. + g_window->SetWindowProperty("appId", kAppId); + g_window->SetWindowProperty("title", "CBE WebView"); + g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); + + g_webview = new SampleWebView(); + g_webview->Initialize(kAppId, g_app_path, "default", "", "", 1920, 1080, false); + g_webview->SetAppId(kAppId); + g_webview->SetAppPath(g_app_path); + g_webview->SetAllowLocalResourceLoad(true); + g_webview->SetLocalStorageEnabled(true); + g_webview->SetVisible(true); + g_webview->UpdatePreferences(); + + // The window does not own the web contents, it only composites them. + g_window->AttachWebContents(g_webview->GetWebContents()); + g_window->Show(); + g_window->Activate(); + + printf("[cbe] loading %s\n", kUrl); + g_webview->LoadUrl(kUrl); + return G_SOURCE_REMOVE; +} + +bool IsBrowserProcess(int argc, char** argv) { + for (int i = 1; i < argc; ++i) + if (strncmp(argv[i], "--type=", 7) == 0) return false; + return true; +} + +} // namespace + +int main(int argc, char** argv) { + setvbuf(stdout, nullptr, _IOLBF, 0); + + const char* slash = strrchr(argv[0], '/'); + g_app_path = slash ? std::string(argv[0], slash - argv[0]) : std::string("."); + const bool browser = IsBrowserProcess(argc, argv); + + // The launcher starts a native app with a JSON parameter, not with Chromium + // switches, so the app supplies its own. WAM gets the equivalent list from its + // systemd unit; these are the few that are not optional. + std::vector args; + args.push_back(argv[0]); + if (browser) { + args.push_back("--ozone-platform=wayland"); + args.push_back("--no-sandbox"); + args.push_back("--no-zygote"); // no forked helper; Chromium re-execs + args.push_back("--in-process-gpu"); // one less process to get right + args.push_back(std::string("--browser-subprocess-path=") + argv[0]); + args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); + } + for (int i = 1; i < argc; ++i) args.push_back(argv[i]); + + std::vector cargv; + for (size_t i = 0; i < args.size(); ++i) cargv.push_back(args[i].c_str()); + + // Wayland clients need this and the app launcher does not always set it. + if (!getenv("XDG_RUNTIME_DIR")) setenv("XDG_RUNTIME_DIR", "/tmp/xdg", 1); + + // Chromium re-execs this same binary for the renderer; only the browser + // process gets a window. + if (browser) g_idle_add(CreateWebApp, NULL); + + return WebOSMain(static_cast(cargv.size()), cargv.data()); +} diff --git a/web/cbe/webos/webapp_window_base.h b/web/cbe/webos/webapp_window_base.h new file mode 100644 index 0000000..a7abf35 --- /dev/null +++ b/web/cbe/webos/webapp_window_base.h @@ -0,0 +1,66 @@ +// webos::WebAppWindowBase - the LSM/Wayland window libcbe paints a WebViewBase +// into. Same reconstruction caveats as webview_base.h. +// +// Layout that must hold: WebAppWindowDelegate DOES have a virtual destructor +// (slots 0 and 1), event() is slot 2, and the object is 8 bytes total. +#pragma once + +#include + +class WebOSEvent; + +namespace webos { + +enum NativeWindowState { + NATIVE_WINDOW_DEFAULT = 0, + NATIVE_WINDOW_MINIMIZED, + NATIVE_WINDOW_MAXIMIZED, + NATIVE_WINDOW_FULLSCREEN, +}; + +enum CustomCursorType { CUSTOM_CURSOR_NOT_USE, CUSTOM_CURSOR_BLANK, CUSTOM_CURSOR_PATH }; + +typedef unsigned WebOSKeyMask; + +// libcbe does not export this class's own members (webOS 4 and older do not +// export it at all), so the defaults live here. They only ever fill vtable slots +// in *our* subclass; libcbe's WebAppWindowBase keeps its internal ones. +class WebAppWindowDelegate { + public: + virtual ~WebAppWindowDelegate() {} + virtual bool event(WebOSEvent*) { return false; } + virtual bool CheckKeyFilterTable(unsigned, unsigned*) { return false; } +}; + +class WebAppWindowBase : public WebAppWindowDelegate { + public: + WebAppWindowBase(); + ~WebAppWindowBase() override; + + void InitWindow(int width, int height); + + virtual void Show(); + virtual void Hide(); + virtual void SetCustomCursor(CustomCursorType type, const std::string& path, + int hotspot_x, int hotspot_y); + virtual void AttachWebContents(void* web_contents); + virtual void DetachWebContents(); + virtual void RecreatedWebContents(); + + void Activate(); + void Deactivate(); + void Resize(int width, int height); + void SetOpacity(float opacity); + void SetWindowHostState(NativeWindowState state); + NativeWindowState GetWindowHostState() const; + void SetWindowProperty(const std::string& name, const std::string& value); + void SetUseVirtualKeyboard(bool enable); + void SetKeyMask(WebOSKeyMask key_mask, bool set); + int DisplayWidth(); + int DisplayHeight(); + + private: + void* webapp_window_; // WebAppWindow*, owned by libcbe +}; + +} // namespace webos diff --git a/web/cbe/webos/webview_base.h b/web/cbe/webos/webview_base.h new file mode 100644 index 0000000..b421f86 --- /dev/null +++ b/web/cbe/webos/webview_base.h @@ -0,0 +1,146 @@ +// webos::WebViewBase - the Chromium embedding surface exported by the TV's +// /usr/lib/libcbe.so ("Chromium Browser Engine"). +// +// This is not an SDK header. There is none: libcbe ships as a stripped 70 MB +// blob with a C++ ABI and no headers anywhere on the device or in the NDK. What +// is here was reconstructed from two sources that agree with each other - the +// firmware symbol tables, which give every name and signature, and the vtables +// of WAM's own BlinkWebView, which is the only in-firmware subclass of this +// class and therefore pins down the slot order. +// +// Three things are load-bearing and must not be tidied up: +// +// * WebViewDelegate has NO virtual destructor. Adding one shifts every slot +// by two and libcbe will call the wrong function. +// * The delegate is 24 slots long. Only the first 17 have recoverable names; +// the rest are no-ops in WAM too, but they must exist, because libcbe +// indexes past slot 17 and a short vtable reads whatever follows it in +// memory. Leaving them out is a segfault a few hundred milliseconds into +// the first page load, which is exactly how they were found. +// * WebViewBase adds no virtuals and exactly one pointer member, so the whole +// object is 8 bytes - the size libcbe's own constructor assumes. +#pragma once + +#include +#include + +class WebOSEvent; + +namespace webos { + +class WebViewProfile; + +class WebViewDelegate { + public: + virtual void LoadProgressChanged(double progress) = 0; + virtual void DidFirstFrameFocused() = 0; + virtual void LoadVisuallyCommitted() = 0; + virtual void TitleChanged(const std::string& title) = 0; + virtual void NavigationHistoryChanged() = 0; + virtual void Close() = 0; + virtual bool DecidePolicyForResponse(bool is_main_frame, + int status_code, + const std::string& url, + const std::string& status_text) = 0; + virtual void LoadStarted() = 0; + virtual void LoadStopped() = 0; + virtual void DidStartNavigation(const std::string& url, bool is_main_frame) = 0; + virtual void DidFinishNavigation(const std::string& url, bool is_main_frame) = 0; + virtual void LoadFinished(const std::string& url) = 0; + virtual void LoadFailed(const std::string& url, + int err_code, + const std::string& err_desc) = 0; + virtual void LoadAborted(const std::string& url) = 0; + virtual void DocumentLoadFinished() = 0; + virtual void RenderProcessCreated(int pid) = 0; + virtual void RenderProcessGone() = 0; + + // Slots 17 to 23. Two of them are named in WAM's binary - the browser-control + // bridge behind window.PalmSystem - and the rest WAM overrides with empty + // bodies, which is all the sample needs them to be. They are declared void and + // argument-less on purpose: the callee never touches the arguments, and on + // AAPCS ignoring them is safe. Do not add or remove entries. + virtual void HandleUnknown17() {} + virtual void HandleUnknown18() {} + virtual void HandleBrowserControlCommand( + const std::string& command, const std::vector& arguments) { + (void)command; + (void)arguments; + } + virtual void HandleBrowserControlFunction( + const std::string& command, + const std::vector& arguments, + std::string* result) { + (void)command; + (void)arguments; + (void)result; + } + virtual void HandleUnknown21() {} + virtual void HandleUnknown22() {} + virtual void HandleUnknown23() {} +}; + +class WebViewBase : public WebViewDelegate { + public: + enum FontRenderParams { HINTING_NONE, HINTING_SLIGHT, HINTING_MEDIUM, HINTING_FULL }; + enum MemoryPressureLevel { + MEMORY_PRESSURE_NONE, + MEMORY_PRESSURE_LOW, + MEMORY_PRESSURE_CRITICAL + }; + enum WebPageVisibilityState { VISIBILITY_VISIBLE, VISIBILITY_HIDDEN, VISIBILITY_LAUNCHING }; + + WebViewBase(); + ~WebViewBase(); + + // The last three arguments are the appinfo.json "width", "height" and + // "useNativeScroll" keys - that is literally where WAM reads them from. + void Initialize(const std::string& app_id, + const std::string& app_path, + const std::string& trust_level, + const std::string& v8_snapshot_path, + const std::string& v8_extra_flags, + int width, + int height, + bool use_native_scroll); + + void* GetWebContents(); + void LoadUrl(const std::string& url); + void StopLoading(); + void Reload(); + bool CanGoBack() const; + std::string GetUrl(); + std::string DocumentTitle() const; + std::string DefaultUserAgent() const; + void RunJavaScript(const std::string& js); + void ForwardWebOSEvent(WebOSEvent* event); + void EnableInspectablePage(); + void UpdatePreferences(); + + void SetAppId(const std::string& app_id); + void SetAppPath(const std::string& app_path); + void SetTrustLevel(const std::string& trust_level); + void SetUserAgent(const std::string& user_agent); + void SetVisible(bool visible); + void SetFocus(bool focus); + void SetViewportSize(int width, int height); + void SetHardwareResolution(int width, int height); + void SetTransparentBackground(bool transparent); + void SetBackgroundColor(int r, int g, int b, int a); + void SetAllowLocalResourceLoad(bool allow); + void SetAllowUniversalAccessFromFileUrls(bool allow); + void SetFileAccessBlocked(bool blocked); + void SetWebSecurityEnabled(bool enabled); + void SetLocalStorageEnabled(bool enabled); + void SetJavascriptCanOpenWindows(bool allow); + void SetSupportsMultipleWindows(bool support); + void SetShouldSuppressDialogs(bool suppress); + void SetDisallowScrollingInMainFrame(bool disallow); + void SetVisibilityState(WebPageVisibilityState state); + void SetFontHinting(FontRenderParams hinting); + + private: + void* webview_; // WebView*, owned by libcbe +}; + +} // namespace webos From c951872c397f9b80649b1e67a7dfd0c3fecde6fe Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sat, 22 Aug 2026 12:58:22 +0900 Subject: [PATCH 02/40] web/cbe: document the JS bridge, and add the injection API it needs Answers the hybrid-app question the sample raises but did not address: how native code and page JavaScript reach each other, and whether a web view can be combined with native rendering. Verified on a 49LK5900 with a throwaway probe build. RunJavaScript() works and returns nothing, so there are two ways back out: document.title arriving as TitleChanged(), which needs no setup at all, and the real one - LoadExtension ("palmsystem", not "v8/palmsystem") plus a trusted trust level, after which the injection's native functions turn into BrowserControlMsg IPC and land in delegate slots 19 and 20. HandleBrowserControlFunction() is synchronous and its std::string* out-parameter becomes the JavaScript return value, so PalmSystem.getResource('probe-cmd', ...) really did come back as "native-said-hello". Page console.log is invisible until --enable-logging=stderr is passed. For combining with native rendering, the finding is a negative one: nothing in the exported API hands back a GL texture or an exported surface, so composition has to happen at the window level - SetTransparentBackground over a hardware video plane, or window groups with ordered layers. Neither combination is tested, and that is said plainly. Adds LoadExtension / ClearExtensions / RunJavaScriptInAllFrames / AddUserStyleSheet to the reconstructed header and the link stub. main.cpp is unchanged: the sample stays minimal. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe/README.md | 81 ++++++++++++++++++++++++++++++++++++ web/cbe/cbe_stub.cpp | 4 ++ web/cbe/webos/webview_base.h | 8 ++++ 3 files changed, 93 insertions(+) diff --git a/web/cbe/README.md b/web/cbe/README.md index 21ee1e0..0b3d49c 100644 --- a/web/cbe/README.md +++ b/web/cbe/README.md @@ -127,10 +127,91 @@ ares-shell -d -r "luna-send -n 1 -w 10000 -f \ ares-pull -d /tmp/shot.png . ``` +## Hybrid apps: talking to the page, and being talked to + +All of this was checked on the 49LK5900, with a throwaway probe build rather than with the +sample as committed. + +**Native to JS.** `RunJavaScript()` and `RunJavaScriptInAllFrames()` work and take effect +immediately. Neither returns a value - there is no `...AndReturnResult` in this API. + +**JS to native, the cheap way.** `document.title = ...` arrives in the delegate as +`TitleChanged()`. Setting the title from injected JavaScript and reading it back out is a +one-line channel that needs no injection and no permissions. Crude, string-only, +last-write-wins - but it works, and it is enough to get a computed value out of the page. + +**JS to native, properly.** libcbe carries Chromium V8 *injections*, and one of them is the +`PalmSystem` object every webOS web app already uses. Load it before navigating: + +```cpp +webview->Initialize(app_id, app_path, "trusted", "", "", 1920, 1080, false); +webview->LoadExtension("palmsystem"); // "palmsystem", NOT "v8/palmsystem" +``` + +The injection's native functions do not stay inside libcbe - they turn into +`BrowserControlMsg_Command` / `BrowserControlMsg_Function` IPC, which surfaces in the +browser process as delegate slots 19 and 20, `HandleBrowserControlCommand()` and +`HandleBrowserControlFunction()`. Those are *your* overrides. `Function` is synchronous and +hands you a `std::string*` to fill in, and the value lands back in JavaScript as the return +value of the call. + +So this round trip works today: + +```js +var reply = PalmSystem.getResource('probe-cmd', 'probe-arg'); // -> "native-said-hello" +``` +``` +[bridge] FUNCTION 'getResource' (1 args) +[bridge] arg[0] = 'probe-cmd' +``` + +Two things to know. The command names are the injection's, not yours - you are overloading +`getResource`, `serviceCall`, `activate` and the rest, so a real app JSON-encodes its own +protocol into one of them. And only the first argument came through on `getResource`, so +pack everything into that one string. The injection also calls `initialize` and +`identifier` on startup expecting WAM-shaped answers. + +`PalmServiceBridge` is injected too (`new PalmServiceBridge()` yields an object with a +`call` function), which is the standard path for page JavaScript to reach `luna://` +services - including one your own native process registers. That is the least hacky bridge +of the lot, but it was not tested here. + +**Capturing console output.** Page `console.log` goes nowhere by default. Add +`--enable-logging=stderr` to the switch list and it appears on stderr, tagged with the app +id: + +``` +[org.webosbrew.sample.web.cbe] "PROBE console.log works", source: data:text/html,... +``` + +## Combining with native rendering + +There is no offscreen path. Nothing in the exported API hands back a GL texture or an +exported surface - `AttachWebContents()` gives the contents to a libcbe-owned Wayland +window and that is where they are drawn. So a hybrid UI has to be composed at the window +level, and libcbe exports the pieces for it: + +* `WebViewBase::SetTransparentBackground(true)` punches the page through to whatever is + behind it. This is how web apps on the TV show hardware video: the decoder owns a plane, + the page draws the UI over a transparent hole. Pairing it with `media/smp/acb` is the + most likely shape of a native-plus-web app here. +* `WebAppWindowBase::CreateWindowGroup()` / `AttachToWindowGroup()` with + `WindowGroupConfiguration::AddLayer()` put several surfaces into one LSM group with named, + ordered layers - WAM's mechanism for overlays. +* `SetOpacity()` and `WebOSPlatform::SetInputRegion()` control blending and which rectangles + take input, so a native surface can stay clickable under a full-screen page. + +Neither of those two combinations was tested. What *is* established is the constraint that +shapes them: `WebOSMain()` owns the process and the default `GMainContext`, so a second +toolkit (SDL2, say) cannot run its own main loop in the usual way - it would need its own +thread and its own Wayland surface, and the two would then compete for LSM focus. + ## What is not here * **Input.** The sample never calls `ForwardWebOSEvent()`, and whether remote-control keys reach the page on their own has not been tested. +* **The bridge.** The sample loads no injection and overrides the browser-control slots + with empty bodies. The section above says what it takes to turn them on. * **Lifecycle.** No SAM relaunch/close handling, no `PalmSystem` bridge, no suspend on background - all of which is most of what WAM actually does. * **webOS 5+.** See the table above. diff --git a/web/cbe/cbe_stub.cpp b/web/cbe/cbe_stub.cpp index 4a2dc83..05a6514 100644 --- a/web/cbe/cbe_stub.cpp +++ b/web/cbe/cbe_stub.cpp @@ -33,6 +33,10 @@ std::string WebViewBase::GetUrl() STUB(std::string()) std::string WebViewBase::DocumentTitle() const STUB(std::string()) std::string WebViewBase::DefaultUserAgent() const STUB(std::string()) void WebViewBase::RunJavaScript(const std::string&) STUB() +void WebViewBase::RunJavaScriptInAllFrames(const std::string&) STUB() +void WebViewBase::LoadExtension(const std::string&) STUB() +void WebViewBase::ClearExtensions() STUB() +void WebViewBase::AddUserStyleSheet(const std::string&) STUB() void WebViewBase::ForwardWebOSEvent(WebOSEvent*) STUB() void WebViewBase::EnableInspectablePage() STUB() void WebViewBase::UpdatePreferences() STUB() diff --git a/web/cbe/webos/webview_base.h b/web/cbe/webos/webview_base.h index b421f86..3288ce2 100644 --- a/web/cbe/webos/webview_base.h +++ b/web/cbe/webos/webview_base.h @@ -113,6 +113,14 @@ class WebViewBase : public WebViewDelegate { std::string DocumentTitle() const; std::string DefaultUserAgent() const; void RunJavaScript(const std::string& js); + void RunJavaScriptInAllFrames(const std::string& js); + // Loads one of libcbe's built-in V8 injections by name ("v8/palmsystem", + // "v8/netcast", ...). An injection is what gives page JavaScript something to + // call that reaches native code: its native functions land in the browser + // process as HandleBrowserControlCommand / HandleBrowserControlFunction. + void LoadExtension(const std::string& name); + void ClearExtensions(); + void AddUserStyleSheet(const std::string& css); void ForwardWebOSEvent(WebOSEvent* event); void EnableInspectablePage(); void UpdatePreferences(); From 1f98ebfd6f3b114cc1d1ac60373035927b217979 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sat, 22 Aug 2026 13:42:23 +0900 Subject: [PATCH 03/40] web/cbe: measure the show/hide cycle, and the two-app alternative Answers the "hide the web window, show a native one, come back" question with device evidence rather than with what the API names imply. One process: Hide() plus NATIVE_WINDOW_MINIMIZED really does take the surface off the screen - the display capture shows the TV falling through to the HDMI input behind it - and Show() brings the page back with no reload and no navigation, so state survives the round trip. Adds the suspend/resume calls this needs to the reconstructed header and the link stub. Two apps: tested with media/lgnc as the native side. Launching over it works and it keeps running; closeByAppId closes cleanly; but the screen does not go back - LSM left the TV on externalinput.av1 rather than restoring the app that was still alive behind. Relaunching to return gave new pids, a cold restart rather than a resume, because nothing here implements the SAM native lifecycle. That lifecycle work, not the launching, is what the two-app split actually costs. main.cpp is unchanged; the probes were throwaway. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe/README.md | 64 ++++++++++++++++++++++++++++++++++-- web/cbe/cbe_stub.cpp | 6 ++++ web/cbe/webos/webview_base.h | 6 ++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/web/cbe/README.md b/web/cbe/README.md index 0b3d49c..d054c27 100644 --- a/web/cbe/README.md +++ b/web/cbe/README.md @@ -184,6 +184,65 @@ id: [org.webosbrew.sample.web.cbe] "PROBE console.log works", source: data:text/html,... ``` +## Showing and hiding the web window + +The web window can be taken off the screen and brought back, and this was measured with +display captures rather than guessed at: + +```cpp +webview->SuspendPaintingAndSetVisibilityHidden(); +webview->SuspendWebPageDOM(); +webview->SetVisible(false); +window->SetWindowHostState(webos::NATIVE_WINDOW_MINIMIZED); +window->Hide(); +// ... later ... +window->Show(); +window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); +webview->SetVisible(true); +webview->ResumeWebPageDOM(); +webview->ResumePaintingAndSetVisibilityVisible(); +window->Activate(); +``` + +Hidden, the surface really is gone - the capture showed the TV falling through to the HDMI +input behind it. Restored, the page came back **without reloading**: same document, no +`LoadStarted`, no navigation. So "hand the screen to something else and come back" works +inside one process, and the page keeps its state across the round trip. + +That is the cheap half. The expensive half is what fills the screen while the web window is +hidden, and there the process layout decides everything. + +## Two processes, or one? + +**Two apps.** Tested with `media/lgnc` (an ordinary SDL2 sample) as the native app and this +one as the web app: + +* launching the web app over the running native app works, and the native app keeps + running in the background; +* `luna://com.webos.applicationManager/closeByAppId` closes the web app cleanly; +* **but the screen does not go back.** Closing the web app left the TV showing + `com.webos.app.externalinput.av1`, not the native app that was still alive behind it. LSM + does not restore a caller; +* and relaunching the native app to get back gave it **new pids** - a cold restart, not a + resume, because the sample implements none of the SAM native lifecycle + (`nativeLifeCycleInterfaceVersion`, `handlesRelaunch`, `registerApp`). + +So the two-app split is workable but not free: the web app has to explicitly launch the +native app on its way out, and the native app has to implement the SAM lifecycle or it will +lose its state every time you come back. That lifecycle work is the real cost of this +option, not the launching. + +**One process.** The constraint is that `WebOSMain()` owns `main()` and the default +`GMainContext` and never returns, so a second toolkit cannot run a `while (SDL_PollEvent)` +loop in the usual place. The way around it is the same seam the sample already uses for +startup: SDL is driven *from* the glib loop - create the window and pump events from a +`g_timeout_add` on the browser UI thread, so both toolkits live on one thread with one +loop. Two Wayland surfaces in one process is not itself a problem. + +This is reasoning, not a result: it has not been built. If you go that way, the thing to +check first is whether SDL2 tolerates having its video subsystem initialised somewhere +other than the real `main()` thread. + ## Combining with native rendering There is no offscreen path. Nothing in the exported API hands back a GL texture or an @@ -212,6 +271,7 @@ thread and its own Wayland surface, and the two would then compete for LSM focus reach the page on their own has not been tested. * **The bridge.** The sample loads no injection and overrides the browser-control slots with empty bodies. The section above says what it takes to turn them on. -* **Lifecycle.** No SAM relaunch/close handling, no `PalmSystem` bridge, no suspend on - background - all of which is most of what WAM actually does. +* **Lifecycle.** No SAM relaunch/close handling and no suspend on background. The calls + exist and are wired into the header - see the show/hide section - but nothing drives them, + and without the SAM side a relaunch is a cold restart. * **webOS 5+.** See the table above. diff --git a/web/cbe/cbe_stub.cpp b/web/cbe/cbe_stub.cpp index 05a6514..4f91e09 100644 --- a/web/cbe/cbe_stub.cpp +++ b/web/cbe/cbe_stub.cpp @@ -40,6 +40,12 @@ void WebViewBase::AddUserStyleSheet(const std::string&) STUB() void WebViewBase::ForwardWebOSEvent(WebOSEvent*) STUB() void WebViewBase::EnableInspectablePage() STUB() void WebViewBase::UpdatePreferences() STUB() +void WebViewBase::SuspendPaintingAndSetVisibilityHidden() STUB() +void WebViewBase::ResumePaintingAndSetVisibilityVisible() STUB() +void WebViewBase::SuspendWebPageDOM() STUB() +void WebViewBase::ResumeWebPageDOM() STUB() +void WebViewBase::SuspendWebPageMedia() STUB() +void WebViewBase::ResumeWebPageMedia() STUB() void WebViewBase::SetAppId(const std::string&) STUB() void WebViewBase::SetAppPath(const std::string&) STUB() void WebViewBase::SetTrustLevel(const std::string&) STUB() diff --git a/web/cbe/webos/webview_base.h b/web/cbe/webos/webview_base.h index 3288ce2..c2858d9 100644 --- a/web/cbe/webos/webview_base.h +++ b/web/cbe/webos/webview_base.h @@ -124,6 +124,12 @@ class WebViewBase : public WebViewDelegate { void ForwardWebOSEvent(WebOSEvent* event); void EnableInspectablePage(); void UpdatePreferences(); + void SuspendPaintingAndSetVisibilityHidden(); + void ResumePaintingAndSetVisibilityVisible(); + void SuspendWebPageDOM(); + void ResumeWebPageDOM(); + void SuspendWebPageMedia(); + void ResumeWebPageMedia(); void SetAppId(const std::string& app_id); void SetAppPath(const std::string& app_path); From 2bcecc7e96c74716b89ee41802814bf1142deb05 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sat, 22 Aug 2026 13:59:33 +0900 Subject: [PATCH 04/40] web/hybrid: SDL2 and a web view in one process, swapping views The two toolkits disagree about who owns the process: WebOSMain() is Chromium's content main and never returns, while SDL wants a while (SDL_PollEvent) loop in main(). Chromium wins, and SDL is pumped from its loop instead - a g_timeout_add on the browser UI thread polls events and repaints, so both toolkits share one thread and switching views is a plain function call. SDL_InitSubSystem(SDL_INIT_VIDEO) from there reports the wayland driver and hands back a window and an accelerated renderer with libcbe already running alongside. Leaving the web view is a title change: the page sets document.title, the app sees TitleChanged, and that is the whole channel. Two details keep it honest - the exit title is ignored unless the web view is on screen, so a leftover title cannot bounce the user straight back out, and the page restores its title on visibilitychange so there is a fresh edge next time. Coming back resumes the suspended page rather than reloading it, which is the point of suspending rather than destroying. Loading the app's own page.html over file:// needs three settings, and two are not enough: without SetAllowUniversalAccessFromFileUrls the renderer is killed mid-load with "bad IPC message, reason 114" rather than being told no. SetWebSecurityEnabled(false) is not needed. Also redirects stdout, since SAM points a launched app's at /dev/null. Moves the reconstructed headers and the link stub to web/libcbe, shared by both samples the way media/common is shared by the media ones. Verified on a 49LK5900 (webOS 4.4.3) by display capture: both views render full-screen, switching works both ways, the exit button reaches native code, and the SDL view keeps animating after coming back. The transitions were driven by a test hook rather than by the remote - synthetic keys via /dev/uinput are not routed to the app by LSM - so the key paths themselves are untested, and the READMEs say so. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- CMakeLists.txt | 2 + README.md | 19 +- assets/icons/web-hybrid.png | Bin 0 -> 2522 bytes assets/make-icon.py | 1 + web/cbe/CMakeLists.txt | 19 +- web/cbe/README.md | 17 +- web/cbe/main.cpp | 8 + web/hybrid/CMakeLists.txt | 31 ++ web/hybrid/README.md | 110 +++++++ web/hybrid/main.cpp | 300 ++++++++++++++++++ web/hybrid/page.html | 39 +++ web/libcbe/CMakeLists.txt | 20 ++ web/{cbe => libcbe}/cbe_stub.cpp | 0 .../webos/webapp_window_base.h | 0 web/{cbe => libcbe}/webos/webview_base.h | 0 15 files changed, 539 insertions(+), 27 deletions(-) create mode 100644 assets/icons/web-hybrid.png create mode 100644 web/hybrid/CMakeLists.txt create mode 100644 web/hybrid/README.md create mode 100644 web/hybrid/main.cpp create mode 100644 web/hybrid/page.html create mode 100644 web/libcbe/CMakeLists.txt rename web/{cbe => libcbe}/cbe_stub.cpp (100%) rename web/{cbe => libcbe}/webos/webapp_window_base.h (100%) rename web/{cbe => libcbe}/webos/webview_base.h (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ed4fd3..057da53 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,5 +33,7 @@ if (TARGET_WEBOS) add_subdirectory(media/smp) add_subdirectory(media/ndl) add_subdirectory(media/lgnc) + add_subdirectory(web/libcbe) add_subdirectory(web/cbe) + add_subdirectory(web/hybrid) endif () diff --git a/README.md b/README.md index 966653c..b5a6315 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,13 @@ entirely different ways to reach that decoder, and all of them get samples: **starfish-media-pipeline** (`libplayerAPIs`), **NDL**, and **LGNC** (the LG NetCast Open API). -`web/cbe` is the odd one out. It links `libcbe.so` - the TV's own Chromium, the engine -every web app on the box already runs inside - and puts a real web view in a *native* app, -with no WAM and no web app package. There is no SDK for that library; the sample's headers -were reconstructed from firmware symbol tables and from the vtables of WAM's own -subclasses, and `web/cbe/README.md` writes down how. +The `web/` samples are the odd ones out. They link `libcbe.so` - the TV's own Chromium, the +engine every web app on the box already runs inside - and put a real web view in a *native* +app, with no WAM and no web app package. There is no SDK for that library; the headers were +reconstructed from firmware symbol tables and from the vtables of WAM's own subclasses, and +`web/cbe/README.md` writes down how. `web/hybrid` then runs SDL2 and a web view in one +process and switches between them, which is the awkward case: `WebOSMain()` never returns, +so SDL has to be pumped from Chromium's message loop rather than its own. ## What is here @@ -29,7 +31,9 @@ media/ directmedia/ libNDL_directmedia, NDL_Direct* - webOS 3.5+, built for API v1 and v2 lgnc/ liblgncopenapi, LGNC_DIRECT* - webOS 1 to 4, one binary for all of them web/ - cbe/ libcbe - the TV's own Chromium, embedded in a native app + libcbe/ reconstructed libcbe headers and the link stub, shared by both + cbe/ the smallest thing that puts a web page on screen + hybrid/ SDL2 and a web view in one process, swapping which one is shown ``` The same two files play through all three stacks. Comparing the three `main.c` files is the @@ -118,7 +122,7 @@ play triangle for the media samples, a globe for the web ones), and which varian and no colour at all. Each sample passes a Material 500 colour to `webos_add_ipk`, which writes it to both `iconColor` and `bgColor` in `appinfo.json`, and webOS paints that behind the glyph. Families are grouped by hue: blues for starfish, greens for NDL, orange for -LGNC, blue for the web view. +LGNC, blue and purple for the web samples. Both fields are set deliberately: `bgColor` is the tile background, while `iconColor` fills behind the icon itself - without it the launcher's default shows through the glyph's @@ -191,6 +195,7 @@ hard to diagnose from the TV side. | `media/smp/webos5` | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): exported window accepted, full load / play / feed / EOS / unload, 300 video + 470 audio units on both. Those runs predate the `Play()` ordering fix, which all SMP samples share - re-run pending | | `media/ndl/directmedia` (v2) | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): 300 video + 469 PCM chunks on both | | `media/ndl/directmedia` (v1) | 3.5 - 4.x | built and symbol-verified, needs a 2017-2019 set to test | +| `web/hybrid` | 4.x | **verified on hardware** - 49LK5900, webOS 4.4.3: SDL2 and the web view both render full-screen from one process, switching works both ways, and the page's exit button reaches native code via `TitleChanged`. The transitions were driven by a test hook, not by the remote - key delivery is untested | | `web/cbe` | 4.x | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | | `media/smp/webos1` | 1.x | not written yet - and there is no webOS 1 hardware here to validate it against, so it would ship untestable | diff --git a/assets/icons/web-hybrid.png b/assets/icons/web-hybrid.png new file mode 100644 index 0000000000000000000000000000000000000000..f5dd61fcf051484b228dec634f5843eb65ace3cc GIT binary patch literal 2522 zcmV<02_^Q4P)G#cCT{i+u<|tu<9h|4~Ev!wRWwVnQU*q%o;!w8WT3 zMNNaj)Jkh&8f%Q!Hf<4WjY?Bc3R()6M-dQ#0_Dx!{rbn8xxcxaUAPza{_e7OzsWE2 zn=@x-XU^};>&)4KCYorXiNOo&k3uDNmuqI~kKd>l)Kq|yS^$|@DWMrq07_AqPAmWI zNRycrE0kU6W=zT-Xw*!Ak_xe5l7<8Az$r63bpd4=T)g&H`gpZQO#~Q)C+QQwQeXyf z8E_nU3)l#J6WD8JlJqs;4j^jU8Q?Im6If$rFO-!#0{A&F2H0U{U#dmD>jK_wk@Ru( z#4$--Nqlya=NCFRo|QBqYNDhul8$>?XD{j$^Np7@N7Ce!UP~0t&(do=;NfoIHsET1 z<^n5#qe%l!5VWrgco+BX;3amzV@Fb z6(zlX4&GJ|f0}69hm!PhlGZ1hyU^+5CH>cF8_!i2Nv)EGc=&0r(=v|_^{{4|SPKn| zUv-Cyip^}>=5v6FS7*P*_Vo#nar%q%ubtG7eQMKzzL_H z@~~zTN^1?uDESwC7`PRf1ia55fTE<|0*iq|74jF&tjo2>fNf4M03Go+&Q$?P=9AMH z;4_}L1jLUwThbAHnFRdF%yz|meQ$D&k#yq@Nne-ryrln>O$?7G{Am}y)rB`iQj4V7 zPA^INgY%C|yKoeC^2CYk!|EOGasIYS?dc0@p@H4&H8a}>JOVr->2ksZ@)KY*K?~Z= z>`ayXrvW70P0$hp{ux;+{a%8&9O?uX(o+MM0Z#$HC(%IX@4B!S*AR&od{)vm71ICh z1Nr9o#j9vwhr(cg%C)ac(hTSCp@Ds<+s8c5{k3axPt-yK3uac7v<$dW(hG!z^9jP3 zIE|nYe=)POo<{@?=>i@HP5~1Mni}yHGu!O%QNv2W)si+R@dB{L%s!_ul(hI$^zur; zaD^2>tN&)PF2+9Sv5s|=d%f+jtLe!v30kzh^0-0H=n@pKCB;lisPt`Q!d_t{a0_%5c zOcPBs(ZpazzZ@#n0_C%Iy-oskB|x8@3sg8*O43RVd8l+Th|8K(XigjD(G$rwu=|9r z9t1hy0+iGe1?M}N1!Q~ZDbBW=U}<>;__djBEYCYAk?dI~OIlfBkNYow%JC~n*QR9* zIw(6InJ?*W7tBkNe(CQk65bU~TP5i?{@x{Nv8N9%Atu86steBxO*peyq0C+fFxA7Vo()M_j-q>%~rj2L6M@D;0PTFd5hY zyi=v9QNVS;E-xq6zXVJN-UUoIvn@&vrTPzm~~l0HEAFbDgq9hEQ;%a}EU4>!*zl_9TkBCy2it39ks^Z!L7*~&fRJBK?w-s;bONt>KjX)CwMc~T?8ECrvO*W%te z|CZ&1#4s>j(n;UfPE69CRB(msNN8~j*x_kUCbZ!S<}XRQ&1sEmOn@F*c&|5ny`-z0 zHs5LYdOX#%$UooXqa;1&v~Q(lH#`!dEO!CykTln6+mu;kS`vY8m9*X8Z%8!niG+8+ z&vtlS;jyE;O(b)bmf-@qZ|dxx;9pi6)x3L{YbsNN-R!(bR>%;3b24*?rVSCHe|w z#{gpq=OPdcww(>VxK{Ef>GRRg!`q@@3*4ySa1^(D6CzJOExH7u%oO$m7hDwJk%kwb zq_O^tE~8)O^!9|-p70NITD0EW?t(k)Pf5YcwJ`;#%mn{CNrx1iD&Ot2sgm|O{kut? z!>2{~JN*W>Q4cEFQ3jc&xqYE&0yYD0>ss&EFC!uXrc7U`#8QaDt^k`6h%H#V6 zg8a=)&LQ&Fbg~OvOB%N@+XOnW~IPU>TSGqQ)0=!Z=oA&cM0*njX*MR>K zmQNpX+ARcUqUQ-WzRe`~#|(46wE(VNj}a~^`+}M6HM8SnH*W|ykoPhs7Xc=&9Y=sy zojg_2^@Iz0)&nb@elrj+Diguy1;TA_(+CIf6C^E?^kra#r?&%(Z$o0%PRZ8`wFLC?!_3Iwf;*O_#ZX?SBHnMH>xEMp?hN_xM-4kK#hNe}PyJgrsA z8mX`Zk_KoelA9likAp3KCFsl0eSOJhGH0QeoE9}|t!FQlm*ubJSD@r>^3QdkRb~Vj zN9Gg#j+yQ8^q~Y-Un2-`TiHon%pdD*zqx4HABHHUIzs07*qoM6N<$f^`7cyZ`_I literal 0 HcmV?d00001 diff --git a/assets/make-icon.py b/assets/make-icon.py index 400cb93..b20d0b3 100644 --- a/assets/make-icon.py +++ b/assets/make-icon.py @@ -32,6 +32,7 @@ "ndl-directmedia-v2": ("NDL", "DM2"), "lgnc": ("LGNC", "1-4"), "web-cbe": ("CBE", "w4", "globe"), + "web-hybrid": ("SDL+", "w4", "globe"), } diff --git a/web/cbe/CMakeLists.txt b/web/cbe/CMakeLists.txt index f830dc1..2701200 100644 --- a/web/cbe/CMakeLists.txt +++ b/web/cbe/CMakeLists.txt @@ -1,4 +1,5 @@ -# libcbe - the TV's Chromium, embedded in a native app. +# The smallest thing that puts a web page on screen: hand the process to +# Chromium, get called back, load a URL. # # webOS 4 only, and that bound is real rather than cautious. Below it, webOS 3 # ships a libstdc++ without the C++11 std::string ABI that libcbe's own exports @@ -11,25 +12,13 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(GLIB REQUIRED glib-2.0) -# The NDK has no libcbe, so build something with the right SONAME to link against. -# Never installed - the TV's own library is what resolves these at runtime. -# libcbe.so carries no version suffix on the TV, and the SONAME is what makes the -# loader pick the real one up, so it has to match exactly - no SOVERSION here. -add_library(cbe-stub SHARED cbe_stub.cpp) -set_target_properties(cbe-stub PROPERTIES OUTPUT_NAME cbe) -target_include_directories(cbe-stub PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") -# Chromium is built without RTTI and exports no typeinfo for these classes, so -# anything deriving from them has to agree. -target_compile_options(cbe-stub PRIVATE -fno-rtti) - add_executable(web-cbe main.cpp) -target_include_directories(web-cbe PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") target_include_directories(web-cbe SYSTEM PRIVATE ${GLIB_INCLUDE_DIRS}) target_link_directories(web-cbe PRIVATE ${GLIB_LIBRARY_DIRS}) target_link_libraries(web-cbe PRIVATE cbe-stub ${GLIB_LIBRARIES}) -target_compile_options(web-cbe PRIVATE -fno-rtti) +target_compile_definitions(web-cbe PRIVATE APP_LOG_NAME="web-cbe") -# No RPATH at all: the stub above must not be shipped, and the TV resolves libcbe +# No RPATH at all: the link stub must not be shipped, and the TV resolves libcbe # from /usr/lib like every other system library. set_target_properties(web-cbe PROPERTIES SKIP_BUILD_RPATH TRUE diff --git a/web/cbe/README.md b/web/cbe/README.md index d054c27..18c29a0 100644 --- a/web/cbe/README.md +++ b/web/cbe/README.md @@ -73,7 +73,7 @@ Two independent sources that agree with each other: followed by a `BlinkWebView` that writes its first field at offset 8 says `webos::WebViewBase` is exactly 8 bytes: a vptr and one pimpl pointer. -Three facts in `webos/webview_base.h` are load-bearing: +Three facts in `web/libcbe/webos/webview_base.h` are load-bearing: * `WebViewDelegate` has **no virtual destructor**. Adding one shifts every slot by two. * The delegate is **24 slots** long. Only the first 17 have recoverable names; libcbe still @@ -88,8 +88,8 @@ lands at slot 2. The two classes are not symmetric. ## Linking -The NDK has no libcbe, so the build makes a stand-in: `cbe_stub.cpp` is compiled into a -shared object with SONAME `libcbe.so` and nothing else in it. The loader picks up the TV's +The NDK has no libcbe, so the build makes a stand-in: `web/libcbe/cbe_stub.cpp` is compiled +into a shared object with SONAME `libcbe.so` and nothing else in it. The loader picks up the TV's real library at runtime because the SONAME matches. It is never installed - see `BUNDLE_LIBS` being absent from the `webos_add_ipk` call. @@ -114,8 +114,10 @@ Launch it, do not run the binary from a shell: SAM sets `APPID` and `XDG_RUNTIME gives the process the session it needs. Direct execution mostly works but is not the thing being demonstrated. -Progress shows up in `/var/log/messages` under the `web-cbe` tag (`journalctl` is not on -the PATH these shells get), and the sample's own delegate output goes to stdout. +SAM points a launched app's stdout at `/dev/null`, so the sample redirects it - its own +delegate output is in `/tmp/web-cbe.log`. libcbe's logging goes through PmLog and reaches +`/var/log/messages` under the `web-cbe` tag either way (`journalctl` is not on the PATH +these shells get). To see what actually reached the screen - LSM will happily report a foreground surface that drew nothing: @@ -265,6 +267,11 @@ shapes them: `WebOSMain()` owns the process and the default `GMainContext`, so a toolkit (SDL2, say) cannot run its own main loop in the usual way - it would need its own thread and its own Wayland surface, and the two would then compete for LSM focus. +## A worked hybrid app + +`web/hybrid` is this sample plus an SDL2 view, in one process, swapping which one is on +screen. It is where the show/hide and JavaScript-bridge machinery below is actually used. + ## What is not here * **Input.** The sample never calls `ForwardWebOSEvent()`, and whether remote-control keys diff --git a/web/cbe/main.cpp b/web/cbe/main.cpp index d1ddd5d..b37c837 100644 --- a/web/cbe/main.cpp +++ b/web/cbe/main.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -119,6 +120,13 @@ bool IsBrowserProcess(int argc, char** argv) { } // namespace int main(int argc, char** argv) { + // SAM points a launched app's stdout at /dev/null, so everything printed below + // is invisible unless it is redirected somewhere. (libcbe's own logging goes + // through PmLog and reaches /var/log/messages regardless.) + if (IsBrowserProcess(argc, argv)) { + freopen("/tmp/" APP_LOG_NAME ".log", "w", stdout); + dup2(1, 2); + } setvbuf(stdout, nullptr, _IOLBF, 0); const char* slash = strrchr(argv[0], '/'); diff --git a/web/hybrid/CMakeLists.txt b/web/hybrid/CMakeLists.txt new file mode 100644 index 0000000..84c3cb1 --- /dev/null +++ b/web/hybrid/CMakeLists.txt @@ -0,0 +1,31 @@ +# SDL2 and a libcbe web view in one process, swapping which one is on screen. +# +# The interesting constraint is in main.cpp: WebOSMain() owns the process and the +# message loop, so SDL cannot have its own. It is pumped from the glib loop that +# libcbe already runs on its browser UI thread, which puts both toolkits on one +# thread and makes switching views a plain function call. +# +# webOS 4 only, for the same ABI reasons as web/cbe. + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GLIB REQUIRED glib-2.0) +pkg_check_modules(SDL2 REQUIRED sdl2) + +add_executable(web-hybrid main.cpp) +target_include_directories(web-hybrid SYSTEM PRIVATE ${GLIB_INCLUDE_DIRS} ${SDL2_INCLUDE_DIRS}) +target_link_directories(web-hybrid PRIVATE ${GLIB_LIBRARY_DIRS} ${SDL2_LIBRARY_DIRS}) +target_link_libraries(web-hybrid PRIVATE cbe-stub ${GLIB_LIBRARIES} ${SDL2_LIBRARIES}) +target_compile_definitions(web-hybrid PRIVATE APP_LOG_NAME="web-hybrid") + +set_target_properties(web-hybrid PROPERTIES + SKIP_BUILD_RPATH TRUE + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH "") + +webos_add_ipk( + TARGET web-hybrid + APPID org.webosbrew.sample.web.hybrid + TITLE "SDL + WebView (webOS 4)" + COLOR "#7E57C2" + WEBOS_VERSIONS ">=4, <5" + MEDIA "${CMAKE_CURRENT_SOURCE_DIR}/page.html") diff --git a/web/hybrid/README.md b/web/hybrid/README.md new file mode 100644 index 0000000..5ca82fd --- /dev/null +++ b/web/hybrid/README.md @@ -0,0 +1,110 @@ +# SDL2 and a web view in one process + +Native code draws one screen, Chromium draws another, and pressing a button swaps them: + +``` + [SDL view] --OK/Enter--> [web view] --exit button or Back--> [SDL view] +``` + +One process, two Wayland surfaces, no second app and no IPC. + +## The problem this solves + +The two toolkits disagree about who owns the process. `WebOSMain()` is Chromium's content +main: it never returns and it owns the message loop. SDL normally wants a +`while (SDL_PollEvent(...))` loop in `main()`. Only one of them can have it. + +Chromium wins, and SDL is driven from *its* loop instead. libcbe pumps the default +`GMainContext` on its browser UI thread, so a `g_timeout_add(16, Pump, ...)` there polls SDL +events and repaints at ~60 Hz. Both toolkits then live on one thread with one loop, and +switching views is a plain function call rather than a lifecycle event: + +```cpp +gboolean Pump(gpointer) { + SDL_Event e; + while (SDL_PollEvent(&e)) { ... } + if (g_native_visible) DrawNativeView(); + return G_SOURCE_CONTINUE; +} +``` + +SDL initialises fine from there - `SDL_InitSubSystem(SDL_INIT_VIDEO)` on the browser UI +thread reports the `wayland` driver and gives back a window and an accelerated renderer, +with libcbe already running in the same process. + +## Switching + +Hiding is not just `SDL_HideWindow()` on one side and `Hide()` on the other; the web view +also gets suspended, which is what stops a backgrounded page burning CPU on timers and +animation: + +```cpp +webview->SuspendPaintingAndSetVisibilityHidden(); +webview->SuspendWebPageDOM(); +webview->SetVisible(false); +window->SetWindowHostState(webos::NATIVE_WINDOW_MINIMIZED); +window->Hide(); +``` + +Coming back resumes rather than reloads. That is the whole reason for suspending instead of +destroying: the second visit keeps whatever state the page had, and the log shows no +navigation at all. + +## How the page asks to leave + +`document.title`. The page sets it, the app sees `WebViewDelegate::TitleChanged()`, and +that is the entire channel: + +```js +function exitToNative() { document.title = 'hybrid:exit'; } +``` + +It needs no injection, no permissions and no extra IPC, which makes it the right size for +one signal. `web/cbe/README.md` describes the two heavier channels - the `palmsystem` +injection with `HandleBrowserControlFunction()`, and `PalmServiceBridge` over Luna - for +when you need arguments and return values. + +Two details make it work reliably. The app ignores the exit title unless the web view is +actually on screen, or a title left over from a previous visit bounces the user straight +back out. And the page restores its title on `visibilitychange`, so there is a fresh edge +for native code to see next time. + +## Loading the app's own page + +Three settings are needed to load `page.html` over `file://`, and two are not enough: + +```cpp +webview->SetAllowLocalResourceLoad(true); +webview->SetFileAccessBlocked(false); +webview->SetAllowUniversalAccessFromFileUrls(true); +``` + +Without the third, the renderer is killed mid-load - `bad IPC message, reason 114` - rather +than being told no, which looks like a crash rather than a permissions problem. +`SetWebSecurityEnabled(false)`, the obvious sledgehammer, turns out not to be needed. + +## Logging + +SAM points a launched app's stdout at `/dev/null`, so both samples redirect it: + +```sh +ares-shell -d -r 'cat /tmp/web-hybrid.log' +``` + +libcbe's own logging goes through PmLog and reaches `/var/log/messages` either way, and +page `console.log` shows up in the redirected stream because the app passes +`--enable-logging=stderr`. + +## State + +Verified on a 49LK5900 (webOS 4.4.3): both views render full-screen, the switch works in +both directions, the page's exit button reaches native code through `TitleChanged`, and +re-entering the web view resumes the existing page without reloading. The SDL view keeps +animating after coming back. + +**Not verified: the remote itself.** Every transition above was driven from a test hook +calling the same functions a key press would. Synthetic key injection through `/dev/uinput` +does not work here - the device registers, but LSM does not route its events to the app - +so the `SDLK_RETURN` and Back paths have not been exercised with real hardware. The rest of +the repo's SDL samples do handle the remote on this TV, so the mechanism is sound; this +particular wiring is simply untested. diff --git a/web/hybrid/main.cpp b/web/hybrid/main.cpp new file mode 100644 index 0000000..4a7484a --- /dev/null +++ b/web/hybrid/main.cpp @@ -0,0 +1,300 @@ +// One process, two windows: an SDL2 view and a libcbe web view, swapping places. +// +// This is the shape a hybrid app wants - native code drawing one screen, a real +// browser drawing another - and the awkward part is that the two disagree about +// who owns the process. WebOSMain() is Chromium's content main: it never returns +// and it owns the message loop. SDL normally wants a `while (SDL_PollEvent)` loop +// in main(). Only one of them can have it. +// +// Chromium wins, and SDL is driven from its loop instead. libcbe pumps the +// default GMainContext on its browser UI thread, so a g_timeout_add() there polls +// SDL events and repaints at a fixed rate. Both toolkits then live on one thread +// with one loop, each with its own Wayland surface, and swapping views is a +// matter of hiding one and showing the other. +// +// [SDL view] --OK/Enter--> [web view] --exit button or Back--> [SDL view] + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "webos/webapp_window_base.h" +#include "webos/webview_base.h" + +extern "C" int WebOSMain(int argc, const char** argv); + +namespace { + +const char kAppId[] = "org.webosbrew.sample.web.hybrid"; + +std::string g_app_path; + +// ---------------------------------------------------------------- web view + +class HybridWebView; +void ShowNativeView(); + +// The page asks to leave by setting document.title, which arrives here as +// TitleChanged(). That needs no injection, no permissions and no extra IPC - it +// is the cheapest JavaScript-to-native channel libcbe offers. web/cbe/README.md +// describes the two heavier ones. +const char kExitTitle[] = "hybrid:exit"; + +bool g_web_visible; + +class HybridWebView : public webos::WebViewBase { + public: + void TitleChanged(const std::string& title) override { + printf("[web] title '%s'\n", title.c_str()); + // Only while the page is actually on screen: a title left over from a + // previous visit must not bounce the user straight back out. + if (title == kExitTitle && g_web_visible) { + puts("[web] page asked to exit"); + ShowNativeView(); + } + } + void LoadFinished(const std::string&) override { puts("[web] load finished"); } + void LoadFailed(const std::string& url, int code, const std::string& desc) override { + printf("[web] FAILED %s (%d %s)\n", url.c_str(), code, desc.c_str()); + } + void RenderProcessCreated(int pid) override { printf("[web] renderer pid %d\n", pid); } + + void LoadProgressChanged(double) override {} + void DidFirstFrameFocused() override {} + void LoadVisuallyCommitted() override {} + void NavigationHistoryChanged() override {} + void Close() override { ShowNativeView(); } + bool DecidePolicyForResponse(bool, int, const std::string&, const std::string&) override { + return false; + } + void LoadStarted() override { puts("[web] load started"); } + void LoadStopped() override { puts("[web] load stopped"); } + void DidStartNavigation(const std::string& u, bool) override { + printf("[web] navigate %s\n", u.c_str()); + } + void DidFinishNavigation(const std::string&, bool) override {} + void LoadAborted(const std::string&) override {} + void DocumentLoadFinished() override {} + void RenderProcessGone() override { puts("[web] renderer gone"); } +}; + +class HybridWindow : public webos::WebAppWindowBase { + public: + // The remote's Back key, if the TV lets the app have it. + bool event(WebOSEvent*) override { return false; } +}; + +HybridWindow* g_window; +HybridWebView* g_webview; + +// ---------------------------------------------------------------- SDL view + +SDL_Window* g_sdl_window; +SDL_Renderer* g_sdl_renderer; +bool g_native_visible; +int g_frame; + +void DrawNativeView() { + // Something obviously native and obviously alive, so it is clear at a glance + // which of the two views is on screen. + const double phase = g_frame / 60.0; + const Uint8 pulse = static_cast(40 + 30 * (1 + SDL_sin(phase))); + SDL_SetRenderDrawColor(g_sdl_renderer, pulse, pulse / 2, 120, 255); + SDL_RenderClear(g_sdl_renderer); + + SDL_SetRenderDrawColor(g_sdl_renderer, 255, 255, 255, 255); + const int bar = 40 + (g_frame % 120) * 8; + SDL_Rect r = {200, 500, bar, 80}; + SDL_RenderFillRect(g_sdl_renderer, &r); + SDL_RenderPresent(g_sdl_renderer); + ++g_frame; +} + +// --------------------------------------------------------------- switching + +void ShowWebView() { + if (g_web_visible) return; + puts("[switch] native -> web"); + g_native_visible = false; + SDL_HideWindow(g_sdl_window); + + const bool first_time = (g_webview == NULL); + if (first_time) { + g_window = new HybridWindow(); + g_window->InitWindow(1920, 1080); + g_window->SetWindowProperty("appId", kAppId); + g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); + + g_webview = new HybridWebView(); + g_webview->Initialize(kAppId, g_app_path, "trusted", "", "", 1920, 1080, false); + g_webview->SetAppId(kAppId); + g_webview->SetAppPath(g_app_path); + // All three are needed to load the app's own page.html over file://. Two are + // not enough: without SetAllowUniversalAccessFromFileUrls the renderer is + // killed mid-load with "bad IPC message, reason 114" rather than being told + // no. SetWebSecurityEnabled(false), which is the obvious sledgehammer, turns + // out not to be needed at all. + g_webview->SetAllowLocalResourceLoad(true); + g_webview->SetFileAccessBlocked(false); + g_webview->SetAllowUniversalAccessFromFileUrls(true); + g_webview->SetLocalStorageEnabled(true); + g_webview->UpdatePreferences(); + g_window->AttachWebContents(g_webview->GetWebContents()); + } else { + // Second time round the page is still loaded - only woken up. + g_webview->ResumeWebPageDOM(); + g_webview->ResumePaintingAndSetVisibilityVisible(); + } + + g_webview->SetVisible(true); + g_window->Show(); + g_window->Activate(); + g_web_visible = true; + + // Only the first time. Coming back to a suspended page is the point of + // Suspend/Resume - reloading would throw away whatever state it had. + if (first_time) { + const std::string url = "file://" + g_app_path + "/page.html"; + printf("[web] loading %s\n", url.c_str()); + g_webview->LoadUrl(url); + } +} + +void ShowNativeView() { + if (!g_native_visible) { + puts("[switch] web -> native"); + if (g_web_visible) { + // Suspending the page as well as hiding the window is what stops a + // backgrounded web view from burning CPU on timers and animation. + g_webview->SuspendPaintingAndSetVisibilityHidden(); + g_webview->SuspendWebPageDOM(); + g_webview->SetVisible(false); + g_window->SetWindowHostState(webos::NATIVE_WINDOW_MINIMIZED); + g_window->Hide(); + g_web_visible = false; + } + SDL_ShowWindow(g_sdl_window); + SDL_RaiseWindow(g_sdl_window); + g_native_visible = true; + } +} + +// ------------------------------------------------------------- the one loop + +gboolean Pump(gpointer) { + SDL_Event e; + while (SDL_PollEvent(&e)) { + if (e.type == SDL_QUIT) return G_SOURCE_REMOVE; + if (e.type != SDL_KEYDOWN) continue; + printf("[sdl] key %d\n", static_cast(e.key.keysym.sym)); + switch (e.key.keysym.sym) { + case SDLK_RETURN: + case SDLK_KP_ENTER: + case SDLK_SPACE: + if (g_native_visible) ShowWebView(); + break; + case SDLK_AC_BACK: + case SDLK_ESCAPE: + case SDLK_BACKSPACE: + if (!g_native_visible) ShowNativeView(); + break; + default: + break; + } + } + if (g_native_visible && g_sdl_renderer != NULL) DrawNativeView(); + return G_SOURCE_CONTINUE; +} + +// Runs on Chromium's browser UI thread, once, as soon as the browser is up. +gboolean StartApp(gpointer) { + // SDL reads its EGL platform from the environment at video-init time. + setenv("EGL_PLATFORM", "wayland", 0); + + if (SDL_Init(0) != 0) { + printf("[sdl] SDL_Init failed: %s\n", SDL_GetError()); + return G_SOURCE_REMOVE; + } + // Without these the TV keeps Back and Exit for itself and the app never sees + // them. They are plain strings, so setting them costs nothing where they are + // not understood. + SDL_SetHint("SDL_WEBOS_ACCESS_POLICY_KEYS_BACK", "true"); + SDL_SetHint("SDL_WEBOS_ACCESS_POLICY_KEYS_EXIT", "true"); + + if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { + printf("[sdl] SDL_INIT_VIDEO failed: %s\n", SDL_GetError()); + return G_SOURCE_REMOVE; + } + printf("[sdl] video driver: %s\n", SDL_GetCurrentVideoDriver()); + + g_sdl_window = SDL_CreateWindow("hybrid", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + 1920, 1080, SDL_WINDOW_FULLSCREEN); + if (g_sdl_window == NULL) { + printf("[sdl] SDL_CreateWindow failed: %s\n", SDL_GetError()); + return G_SOURCE_REMOVE; + } + g_sdl_renderer = SDL_CreateRenderer(g_sdl_window, -1, SDL_RENDERER_ACCELERATED); + if (g_sdl_renderer == NULL) { + printf("[sdl] SDL_CreateRenderer failed: %s\n", SDL_GetError()); + return G_SOURCE_REMOVE; + } + g_native_visible = true; + puts("[sdl] native view up - OK/Enter opens the web view"); + + g_timeout_add(16, Pump, NULL); + return G_SOURCE_REMOVE; +} + +bool IsBrowserProcess(int argc, char** argv) { + for (int i = 1; i < argc; ++i) + if (strncmp(argv[i], "--type=", 7) == 0) return false; + return true; +} + +} // namespace + +int main(int argc, char** argv) { + // SAM points a launched app's stdout at /dev/null, so everything printed below + // is invisible unless it is redirected somewhere. (libcbe's own logging goes + // through PmLog and reaches /var/log/messages regardless.) + if (IsBrowserProcess(argc, argv)) { + freopen("/tmp/" APP_LOG_NAME ".log", "w", stdout); + dup2(1, 2); + } + setvbuf(stdout, NULL, _IOLBF, 0); + + const char* slash = strrchr(argv[0], '/'); + g_app_path = slash ? std::string(argv[0], slash - argv[0]) : std::string("."); + const bool browser = IsBrowserProcess(argc, argv); + + std::vector args; + args.push_back(argv[0]); + if (browser) { + args.push_back("--ozone-platform=wayland"); + args.push_back("--no-sandbox"); + args.push_back("--no-zygote"); + args.push_back("--in-process-gpu"); + args.push_back(std::string("--browser-subprocess-path=") + argv[0]); + args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); + // Page console.log is invisible without this. + args.push_back("--enable-logging=stderr"); + } + for (int i = 1; i < argc; ++i) args.push_back(argv[i]); + + std::vector cargv; + for (size_t i = 0; i < args.size(); ++i) cargv.push_back(args[i].c_str()); + + if (!getenv("XDG_RUNTIME_DIR")) setenv("XDG_RUNTIME_DIR", "/tmp/xdg", 1); + + // Chromium re-execs this same binary for the renderer; only the browser + // process gets windows. + if (browser) g_idle_add(StartApp, NULL); + + return WebOSMain(static_cast(cargv.size()), cargv.data()); +} diff --git a/web/hybrid/page.html b/web/hybrid/page.html new file mode 100644 index 0000000..8784726 --- /dev/null +++ b/web/hybrid/page.html @@ -0,0 +1,39 @@ + + +hybrid web view + +
+

web view

+

Chromium, in the same process as the SDL view.

+ +

or press Back on the remote

+
+ diff --git a/web/libcbe/CMakeLists.txt b/web/libcbe/CMakeLists.txt new file mode 100644 index 0000000..80c85d2 --- /dev/null +++ b/web/libcbe/CMakeLists.txt @@ -0,0 +1,20 @@ +# The libcbe ABI: reconstructed headers, and a link stub to resolve them against. +# +# Shared by every sample under web/, the way media/common is shared by the media +# ones. Nothing here talks to libcbe - it only describes it. +# +# The NDK has no libcbe, so this builds something with the right SONAME to link +# against. libcbe.so carries no version suffix on the TV, and the SONAME is what +# makes the loader pick the real 70 MB library up at runtime, so there is +# deliberately no SOVERSION here. It is never installed. +# +# The stub is written as ordinary C++ against the same headers the samples use, +# rather than as a hand-written list of mangled names, so the two cannot drift. + +add_library(cbe-stub SHARED cbe_stub.cpp) +set_target_properties(cbe-stub PROPERTIES OUTPUT_NAME cbe) +target_include_directories(cbe-stub PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") + +# Chromium is built without RTTI and exports no typeinfo for these classes, so +# anything deriving from them has to agree - hence PUBLIC, not PRIVATE. +target_compile_options(cbe-stub PUBLIC -fno-rtti) diff --git a/web/cbe/cbe_stub.cpp b/web/libcbe/cbe_stub.cpp similarity index 100% rename from web/cbe/cbe_stub.cpp rename to web/libcbe/cbe_stub.cpp diff --git a/web/cbe/webos/webapp_window_base.h b/web/libcbe/webos/webapp_window_base.h similarity index 100% rename from web/cbe/webos/webapp_window_base.h rename to web/libcbe/webos/webapp_window_base.h diff --git a/web/cbe/webos/webview_base.h b/web/libcbe/webos/webview_base.h similarity index 100% rename from web/cbe/webos/webview_base.h rename to web/libcbe/webos/webview_base.h From b17e62382f41dd90f22807b9ba6d4a915a495368 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sat, 22 Aug 2026 14:27:43 +0900 Subject: [PATCH 05/40] Ignore .mcp.json Claude Code writes a per-project MCP server config there, and it is machine-local: the entry this repo picked up carries an absolute path into a home directory. Nothing a checkout needs. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 2ac3a95..befd31c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,8 @@ /assets/*.pcm /assets/*.ac3 +# Claude Code's per-project MCP server config: machine-local, since it carries +# absolute paths to whatever tooling the checkout happens to sit next to. +/.mcp.json + .DS_Store From 43888c656216b168ee530df1183d9ab85062d3c5 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sat, 22 Aug 2026 14:35:25 +0900 Subject: [PATCH 06/40] web/hybrid: leave the web view through Close(), not a title change Signalling native code by setting document.title and watching TitleChanged() worked, and was the wrong thing. The title is a UI property with one global slot: the channel collides with any page that manages its own title, cannot carry arguments, and needs edge-detection hacks to tell a fresh signal from a leftover one - this sample had both, ignoring the exit title unless the view was on screen and resetting it on visibilitychange to manufacture an edge. libcbe already has callbacks for this. With the palmsystem injection loaded, PalmSystem.close() arrives as WebViewDelegate::Close() - the delegate's own dedicated slot - and PalmSystem.platformBack() as HandleBrowserControlCommand("platformBack"). Nothing is overloaded, nothing is parsed out of a shared channel, and the page carries no state between visits, so both guards are gone. TitleChanged() is only logged now. Both routes confirmed on a 49LK5900: the exit button reaches Close() and switches views, and re-entry still resumes the suspended page without reloading. web/cbe/README.md now says plainly that the title is not an IPC channel. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- README.md | 2 +- web/cbe/README.md | 27 +++++++++++++++++-------- web/hybrid/README.md | 48 ++++++++++++++++++++++++++++++++------------ web/hybrid/main.cpp | 37 +++++++++++++++++++++++----------- web/hybrid/page.html | 23 ++++++++++++--------- 5 files changed, 94 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index b5a6315..a79a26f 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ hard to diagnose from the TV side. | `media/smp/webos5` | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): exported window accepted, full load / play / feed / EOS / unload, 300 video + 470 audio units on both. Those runs predate the `Play()` ordering fix, which all SMP samples share - re-run pending | | `media/ndl/directmedia` (v2) | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): 300 video + 469 PCM chunks on both | | `media/ndl/directmedia` (v1) | 3.5 - 4.x | built and symbol-verified, needs a 2017-2019 set to test | -| `web/hybrid` | 4.x | **verified on hardware** - 49LK5900, webOS 4.4.3: SDL2 and the web view both render full-screen from one process, switching works both ways, and the page's exit button reaches native code via `TitleChanged`. The transitions were driven by a test hook, not by the remote - key delivery is untested | +| `web/hybrid` | 4.x | **verified on hardware** - 49LK5900, webOS 4.4.3: SDL2 and the web view both render full-screen from one process, switching works both ways, and the page's exit button reaches native code as `WebViewDelegate::Close()`. The transitions were driven by a test hook, not by the remote - key delivery is untested | | `web/cbe` | 4.x | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | | `media/smp/webos1` | 1.x | not written yet - and there is no webOS 1 hardware here to validate it against, so it would ship untestable | diff --git a/web/cbe/README.md b/web/cbe/README.md index 18c29a0..c3e8e0a 100644 --- a/web/cbe/README.md +++ b/web/cbe/README.md @@ -137,12 +137,7 @@ sample as committed. **Native to JS.** `RunJavaScript()` and `RunJavaScriptInAllFrames()` work and take effect immediately. Neither returns a value - there is no `...AndReturnResult` in this API. -**JS to native, the cheap way.** `document.title = ...` arrives in the delegate as -`TitleChanged()`. Setting the title from injected JavaScript and reading it back out is a -one-line channel that needs no injection and no permissions. Crude, string-only, -last-write-wins - but it works, and it is enough to get a computed value out of the page. - -**JS to native, properly.** libcbe carries Chromium V8 *injections*, and one of them is the +**JS to native.** libcbe carries Chromium V8 *injections*, and one of them is the `PalmSystem` object every webOS web app already uses. Load it before navigating: ```cpp @@ -173,10 +168,26 @@ protocol into one of them. And only the first argument came through on `getResou pack everything into that one string. The injection also calls `initialize` and `identifier` on startup expecting WAM-shaped answers. +Some of the injection's calls do not need overloading at all, because they map onto +delegate slots that exist for them: + +| JavaScript | native | +|---|---| +| `PalmSystem.close()` | `WebViewDelegate::Close()` | +| `PalmSystem.platformBack()` | `HandleBrowserControlCommand("platformBack")` | + +Prefer those where they fit - `web/hybrid` leaves its web view through `close()`. + `PalmServiceBridge` is injected too (`new PalmServiceBridge()` yields an object with a `call` function), which is the standard path for page JavaScript to reach `luna://` -services - including one your own native process registers. That is the least hacky bridge -of the lot, but it was not tested here. +services - including one your own native process registers. That is the right answer when +the bridge needs a real protocol rather than a signal, but it was not tested here. + +**What not to use.** `document.title` also reaches native code, as `TitleChanged()`, and it +is tempting because it needs no injection. It is not an IPC channel: the title is a UI +property with one global slot, so it collides with any page that manages its own, carries +no arguments, and forces edge-detection hacks to tell a fresh signal from a leftover one. +Use it for what it is - a page title. **Capturing console output.** Page `console.log` goes nowhere by default. Add `--enable-logging=stderr` to the switch list and it appears on stderr, tagged with the app diff --git a/web/hybrid/README.md b/web/hybrid/README.md index 5ca82fd..a23ced2 100644 --- a/web/hybrid/README.md +++ b/web/hybrid/README.md @@ -52,22 +52,44 @@ navigation at all. ## How the page asks to leave -`document.title`. The page sets it, the app sees `WebViewDelegate::TitleChanged()`, and -that is the entire channel: +Through libcbe's own callbacks. The app loads the `palmsystem` injection, which gives page +JavaScript real entry points, and two of them arrive in the delegate: + +| JavaScript | native | +|---|---| +| `PalmSystem.close()` | `WebViewDelegate::Close()` | +| `PalmSystem.platformBack()` | `HandleBrowserControlCommand("platformBack")` | ```js -function exitToNative() { document.title = 'hybrid:exit'; } +function exitToNative() { PalmSystem.close(); } +``` + +`Close()` is the delegate's own dedicated slot - a callback that exists for exactly this - +so nothing is overloaded, nothing has to be parsed out of a shared channel, and the page +carries no state between visits. + +Turning the injection on is two lines, and both matter: + +```cpp +webview->Initialize(app_id, app_path, "trusted", "", "", 1920, 1080, false); +webview->LoadExtension("palmsystem"); // "palmsystem", NOT "v8/palmsystem" ``` -It needs no injection, no permissions and no extra IPC, which makes it the right size for -one signal. `web/cbe/README.md` describes the two heavier channels - the `palmsystem` -injection with `HandleBrowserControlFunction()`, and `PalmServiceBridge` over Luna - for -when you need arguments and return values. +Get either wrong and there is no `PalmSystem` object at all, with no error - the page just +finds it undefined, which is why `page.html` checks for it and says so on screen. + +`web/cbe/README.md` covers the rest of the bridge, including +`HandleBrowserControlFunction()`, which is synchronous and returns a string to JavaScript, +and `PalmServiceBridge` for reaching `luna://` services. + +### Not the title -Two details make it work reliably. The app ignores the exit title unless the web view is -actually on screen, or a title left over from a previous visit bounces the user straight -back out. And the page restores its title on `visibilitychange`, so there is a fresh edge -for native code to see next time. +An earlier version of this sample signalled the exit by setting `document.title` and +watching `TitleChanged()`. It worked, and it was wrong: the title is a UI property with one +global slot, so the channel collides with any page that manages its own title, cannot carry +arguments, and needs edge-detection hacks - a leftover title from the previous visit +bounced the user straight back out, and the page had to reset it on `visibilitychange` to +manufacture a fresh edge. None of that exists now. `TitleChanged()` is only logged. ## Loading the app's own page @@ -98,8 +120,8 @@ page `console.log` shows up in the redirected stream because the app passes ## State Verified on a 49LK5900 (webOS 4.4.3): both views render full-screen, the switch works in -both directions, the page's exit button reaches native code through `TitleChanged`, and -re-entering the web view resumes the existing page without reloading. The SDL view keeps +both directions, the page's exit button reaches native code as `WebViewDelegate::Close()`, +and re-entering the web view resumes the existing page without reloading. The SDL view keeps animating after coming back. **Not verified: the remote itself.** Every transition above was driven from a test hook diff --git a/web/hybrid/main.cpp b/web/hybrid/main.cpp index 4a7484a..f11ae16 100644 --- a/web/hybrid/main.cpp +++ b/web/hybrid/main.cpp @@ -40,11 +40,15 @@ std::string g_app_path; class HybridWebView; void ShowNativeView(); -// The page asks to leave by setting document.title, which arrives here as -// TitleChanged(). That needs no injection, no permissions and no extra IPC - it -// is the cheapest JavaScript-to-native channel libcbe offers. web/cbe/README.md -// describes the two heavier ones. -const char kExitTitle[] = "hybrid:exit"; +// The page asks to leave through libcbe's own callbacks, not through a side +// effect of some UI property. Loading the "palmsystem" injection gives page +// JavaScript real entry points, and two of them land here: +// +// PalmSystem.close() -> WebViewDelegate::Close() +// PalmSystem.platformBack() -> HandleBrowserControlCommand("platformBack") +// +// Close() is the delegate's own dedicated slot, so nothing is overloaded and +// nothing has to be parsed out of a shared channel. bool g_web_visible; @@ -52,12 +56,6 @@ class HybridWebView : public webos::WebViewBase { public: void TitleChanged(const std::string& title) override { printf("[web] title '%s'\n", title.c_str()); - // Only while the page is actually on screen: a title left over from a - // previous visit must not bounce the user straight back out. - if (title == kExitTitle && g_web_visible) { - puts("[web] page asked to exit"); - ShowNativeView(); - } } void LoadFinished(const std::string&) override { puts("[web] load finished"); } void LoadFailed(const std::string& url, int code, const std::string& desc) override { @@ -69,7 +67,18 @@ class HybridWebView : public webos::WebViewBase { void DidFirstFrameFocused() override {} void LoadVisuallyCommitted() override {} void NavigationHistoryChanged() override {} - void Close() override { ShowNativeView(); } + // PalmSystem.close(): the page is done and wants to be dismissed. + void Close() override { + puts("[web] page called PalmSystem.close()"); + ShowNativeView(); + } + + // PalmSystem.platformBack(): a Back gesture the page chose not to consume. + void HandleBrowserControlCommand(const std::string& command, + const std::vector&) override { + printf("[web] browser control '%s'\n", command.c_str()); + if (command == "platformBack") ShowNativeView(); + } bool DecidePolicyForResponse(bool, int, const std::string&, const std::string&) override { return false; } @@ -144,6 +153,10 @@ void ShowWebView() { g_webview->SetFileAccessBlocked(false); g_webview->SetAllowUniversalAccessFromFileUrls(true); g_webview->SetLocalStorageEnabled(true); + // Without this the page has no PalmSystem object and therefore no way to + // reach native code at all. It needs the "trusted" trust level passed to + // Initialize() above, and the name is "palmsystem", not "v8/palmsystem". + g_webview->LoadExtension("palmsystem"); g_webview->UpdatePreferences(); g_window->AttachWebContents(g_webview->GetWebContents()); } else { diff --git a/web/hybrid/page.html b/web/hybrid/page.html index 8784726..4ed0b52 100644 --- a/web/hybrid/page.html +++ b/web/hybrid/page.html @@ -11,6 +11,7 @@ button { font: inherit; font-size: 36px; padding: 20px 56px; border: 0; border-radius: 12px; background: #4285f4; color: #fff; cursor: pointer; } button:focus { outline: 6px solid #ffffff88; } + #warn { color: #ffb4a2; }

web view

@@ -19,21 +20,25 @@

web view

or press Back on the remote

From 51c978a6a828cc5683f68800236bcd72c79ea449 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sat, 22 Aug 2026 14:48:33 +0900 Subject: [PATCH 07/40] web/: correct why webOS 3 is out, and say what "webOS 4" rests on Two claims in the previous commits were wrong or vaguer than the evidence. webOS 3 was described as unreachable because libcbe's exports are mangled with std::__cxx11::basic_string while webOS 3's libstdc++ predates that ABI. That is backwards: webOS 3's libcbe is the *old* string ABI throughout - there is not one __cxx11 symbol in the library - so it is self-consistent, and a webOS 3 build is a -D_GLIBCXX_USE_CXX11_ABI=0 variant rather than an impossibility. It also predates the WebViewBase() + Initialize() split, taking dimensions in the constructor instead, and has no SetAppPath, LoadExtension or UpdatePreferences. Of the 30 libcbe symbols the sample needs, 11 are absent from a 3.4 dump, all of them one of those two differences. That is the same shape as media/smp/common being built four times, and the README now says so. The real hard floor is 3.4, and it is the library rather than the ABI: webOS 1 and 2 ship no libcbe at all. Second, "webOS 4" was doing more work than the evidence supports. The verifier has exactly two webOS 4 dumps, 4.4.2 and 4.9.7, and nothing between 4.0 and 4.3, so the lower half of the declared >=4 range rests on the API being unchanged across the generation, not on a check. Hardware testing was 4.4.3. Documented as "4.4.2 verified, 4.0 assumed" rather than quietly implied. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- README.md | 4 ++-- web/cbe/CMakeLists.txt | 16 +++++++------- web/cbe/README.md | 44 +++++++++++++++++++++++++++++++-------- web/hybrid/CMakeLists.txt | 3 ++- 4 files changed, 48 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index a79a26f..b032281 100644 --- a/README.md +++ b/README.md @@ -195,8 +195,8 @@ hard to diagnose from the TV side. | `media/smp/webos5` | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): exported window accepted, full load / play / feed / EOS / unload, 300 video + 470 audio units on both. Those runs predate the `Play()` ordering fix, which all SMP samples share - re-run pending | | `media/ndl/directmedia` (v2) | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): 300 video + 469 PCM chunks on both | | `media/ndl/directmedia` (v1) | 3.5 - 4.x | built and symbol-verified, needs a 2017-2019 set to test | -| `web/hybrid` | 4.x | **verified on hardware** - 49LK5900, webOS 4.4.3: SDL2 and the web view both render full-screen from one process, switching works both ways, and the page's exit button reaches native code as `WebViewDelegate::Close()`. The transitions were driven by a test hook, not by the remote - key delivery is untested | -| `web/cbe` | 4.x | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | +| `web/hybrid` | 4.4+ | **verified on hardware** - 49LK5900, webOS 4.4.3: SDL2 and the web view both render full-screen from one process, switching works both ways, and the page's exit button reaches native code as `WebViewDelegate::Close()`. The transitions were driven by a test hook, not by the remote - key delivery is untested | +| `web/cbe` | 4.4+ | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | | `media/smp/webos1` | 1.x | not written yet - and there is no webOS 1 hardware here to validate it against, so it would ship untestable | ## What a `Play()` that returns true does not tell you diff --git a/web/cbe/CMakeLists.txt b/web/cbe/CMakeLists.txt index 2701200..dd642ee 100644 --- a/web/cbe/CMakeLists.txt +++ b/web/cbe/CMakeLists.txt @@ -1,13 +1,15 @@ # The smallest thing that puts a web page on screen: hand the process to # Chromium, get called back, load a URL. # -# webOS 4 only, and that bound is real rather than cautious. Below it, webOS 3 -# ships a libstdc++ without the C++11 std::string ABI that libcbe's own exports -# are mangled with, so nothing can call this API there without statically linking -# a newer runtime. Above it, webOS 5 replaced the free WebOSMain() with a -# webos::WebOSMain class and changed WebViewBase's constructor, and webOS 6 added -# a second, parallel API under neva_app_runtime - so 5 and up want their own -# variant of this sample rather than a wider version range on this one. +# webOS 4 only, and both bounds are build variants rather than walls - the same +# situation as media/smp/common, which is built four times for four generations. +# Below, webOS 3's libcbe is the pre-C++11 std::string ABI throughout and predates +# the WebViewBase() + Initialize() split, so it wants a legacy variant. Above, +# webOS 5 replaced the free WebOSMain() with a webos::WebOSMain class and changed +# the constructor again. The hard floor is 3.4: webOS 1 and 2 have no libcbe. +# +# Within webOS 4, only 4.4.2 and 4.9.7 have firmware dumps to verify against; +# 4.0 to 4.3 is assumed, not checked. See README.md. find_package(PkgConfig REQUIRED) pkg_check_modules(GLIB REQUIRED glib-2.0) diff --git a/web/cbe/README.md b/web/cbe/README.md index c3e8e0a..a420582 100644 --- a/web/cbe/README.md +++ b/web/cbe/README.md @@ -46,20 +46,46 @@ moment a window or a web view may legally be created. | release | libcbe | entry point | verdict | |---|---|---|---| | 1.x, 2.x | absent | - | no engine to link | -| 3.4 - 3.9 | yes | `WebOSMain` | libstdc++ 6.0.19: no C++11 `std::string` ABI | +| 3.4 - 3.9 | yes | `WebOSMain` | reachable, but a legacy-ABI variant: see below | | **4.x** | **yes** | **`WebOSMain`** | **this sample** | | 5.x | yes | `webos::WebOSMain::Run` | different entry point and `Initialize` | | 6.x - 11.x | yes | `webos::WebOSMain::Run`, plus `neva_app_runtime` | a second, parallel API | -Both bounds are ABI, not caution. Below: libcbe's own exports are mangled with -`std::__cxx11::basic_string`, but webOS 3's system libstdc++ does not export -`GLIBCXX_3.4.21`, so nothing can call this API there without statically linking a newer -runtime. Above: webOS 5 replaced the free `WebOSMain()` with a `webos::WebOSMain` class and -changed `WebViewBase`'s constructor, and webOS 6 added a whole second API under -`neva_app_runtime`. Those want their own variant rather than a wider range on this one. +Both bounds are ABI, not caution, and neither is a wall - each is a build variant, the same +way `media/smp/common` is built four times for four generations of `StarfishMediaAPIs`. -The `webos::` API itself is present unchanged from 3.4 all the way to 11.2, so a webOS 5+ -variant is a small delta, not a rewrite. +**Below.** webOS 3's libcbe is the pre-C++11 `std::string` ABI throughout - there is not one +`__cxx11` symbol in the whole library - and it predates the split constructor: + +| | webOS 3.4 | webOS 4.4 | +|---|---|---| +| `std::string` | `std::basic_string` | `std::__cxx11::basic_string` | +| construction | `WebViewBase(int w, int h)` | `WebViewBase()` then `Initialize(...)` | +| window setup | `Resize()` + `Show()` | `InitWindow(w, h)` + `Activate()` | +| absent on 3.4 | | `SetAppPath`, `LoadExtension`, `UpdatePreferences`, `SetWindowProperty`* | + +\* `SetWindowProperty` and `SetCustomCursor` do exist on 3.4, but with old-ABI strings. + +Of the 30 libcbe symbols this sample needs, 11 are absent from a webOS 3.4 dump - all of +them either old-ABI spellings or the newer split-init calls. So webOS 3 wants a variant +built `-D_GLIBCXX_USE_CXX11_ABI=0` against an older header, not a newer runtime. + +**Above.** webOS 5 replaced the free `WebOSMain()` with a `webos::WebOSMain` class and +changed `WebViewBase`'s constructor again; webOS 6 added a whole second API under +`neva_app_runtime`. The `webos::` API itself survives to 11.2, so that variant is a small +delta rather than a rewrite. + +**The hard floor is 3.4**, and it is the library, not the ABI: webOS 1 and 2 have no +`libcbe.so` at all. + +## What "webOS 4" is actually backed by + +The `-verify` firmware set has exactly two webOS 4 dumps, 4.4.2 and 4.9.7, and both are +clean. There is no dump anywhere between 4.0 and 4.3, so that part of the declared +`>=4, <5` range rests on the API being unchanged across the generation rather than on +evidence. Hardware testing was on a 49LK5900 at 4.4.3. + +Read the floor as: **4.4.2 verified, 4.0 assumed.** ## Where the headers came from diff --git a/web/hybrid/CMakeLists.txt b/web/hybrid/CMakeLists.txt index 84c3cb1..1c3a6b1 100644 --- a/web/hybrid/CMakeLists.txt +++ b/web/hybrid/CMakeLists.txt @@ -5,7 +5,8 @@ # libcbe already runs on its browser UI thread, which puts both toolkits on one # thread and makes switching views a plain function call. # -# webOS 4 only, for the same ABI reasons as web/cbe. +# webOS 4 only, for the same ABI reasons as web/cbe - and verified only from 4.4.2 +# up, since no earlier webOS 4 dump exists. find_package(PkgConfig REQUIRED) pkg_check_modules(GLIB REQUIRED glib-2.0) From 25bb23ee92b811356bf14640940015d063dc064a Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sat, 22 Aug 2026 15:58:20 +0900 Subject: [PATCH 08/40] web/cbe: say what webOS 1 and 2 have instead of libcbe The version table called those releases "no engine to link", which is wrong in a way that would stop someone looking. They have Qt5WebKit - libQt5WebKit.so against Qt 5.0.0 on webOS 1 and Qt 5.2.1 on webOS 2 - and libQt5WebKitWidgets carries the whole classic WebKit1 API, including a JavaScript bridge better than libcbe's: evaluateJavaScript returns a value synchronously, and addToJavaScriptWindowObject hands page JavaScript a real QObject. It is still a separate project rather than a variant here, for two reasons now written down. Nothing in the firmware uses the widgets API - scanning every shared object in the webOS 2.2.3 dump, libQt5WebKitWidgets has zero consumers, and WAM reaches WebKit through QML instead - so a shipped-but-unexercised library would have to be proven to load and paint before anything is built on it. And the NDK ships Qt 5.15.14 with no QtWebKit headers, against TVs running 5.0.0 and 5.2.1, and Qt's binary compatibility runs forwards only. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe/README.md | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/web/cbe/README.md b/web/cbe/README.md index a420582..a98d40d 100644 --- a/web/cbe/README.md +++ b/web/cbe/README.md @@ -45,7 +45,7 @@ moment a window or a web view may legally be created. | release | libcbe | entry point | verdict | |---|---|---|---| -| 1.x, 2.x | absent | - | no engine to link | +| 1.x, 2.x | absent | - | a different engine entirely - Qt5WebKit, see below | | 3.4 - 3.9 | yes | `WebOSMain` | reachable, but a legacy-ABI variant: see below | | **4.x** | **yes** | **`WebOSMain`** | **this sample** | | 5.x | yes | `webos::WebOSMain::Run` | different entry point and `Initialize` | @@ -76,7 +76,39 @@ changed `WebViewBase`'s constructor again; webOS 6 added a whole second API unde delta rather than a rewrite. **The hard floor is 3.4**, and it is the library, not the ABI: webOS 1 and 2 have no -`libcbe.so` at all. +`libcbe.so` at all. Chromium arrived with webOS 3. + +### webOS 1 and 2: Qt5WebKit, and why it is not a variant + +Those releases do have a web engine, just not this one: + +| | webOS 1.2 / 1.4 | webOS 2.2.3 | webOS 3.4+ | +|---|---|---|---| +| Qt | 5.0.0 | 5.2.1 | - | +| engine | `libQt5WebKit.so.5.0.0` | `libQt5WebKit.so.5.2.0` | `libcbe.so` | + +`libQt5WebKitWidgets` is there too, with the whole classic WebKit1 API - `QWebView`, +`QWebPage`, `QWebFrame`, `QWebSettings` - and a JavaScript bridge that is frankly nicer +than anything libcbe offers: `QWebFrame::evaluateJavaScript()` returns a value +synchronously, `addToJavaScriptWindowObject()` hands page JavaScript a real `QObject` with +slots and properties, and `QWebPage::javaScriptConsoleMessage()` is a console hook. No +injection to load, no command names to overload. + +Two things make it a separate project rather than a variant of this one: + +* **Nothing in the firmware uses the widgets API.** Scanning every shared object in the + webOS 2.2.3 dump, `libQt5WebKitWidgets.so.5` has zero consumers; WAM reaches WebKit + through QML instead (it needs `libQt5Qml`, `libQt5Quick` and `libQt5WebKit`). The dumps + cover libraries and not executables, so this is not quite proof - but a shipped, + unexercised library is exactly the situation this repo already has a scar from, so the + first job would be proving it loads and paints at all. +* **The NDK cannot build for it.** The buildroot SDK ships Qt **5.15.14** against TVs + running 5.0.0 and 5.2.1, and Qt's binary compatibility runs forwards only. It also has no + QtWebKit headers at all, QtWebKit having been dropped from Qt after 5.5. That means + period-correct headers from upstream plus link stubs - the same trick `web/libcbe` uses - + plus `moc` for any object exposed to JavaScript. + +A 55LF6310 (webOS 2.2.0) is the set that could settle the first point. ## What "webOS 4" is actually backed by From bac513ca6d58b2c02057c7a43f9a4809c674550a Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sat, 22 Aug 2026 17:53:05 +0900 Subject: [PATCH 09/40] web/: the upper bound is 4.9, not 5 Checking the "the webos:: API survives to 11.2, so a webOS 5 variant is a small delta" claim against the full symbol set turned up a bug in the declared range. webos::WebViewBase::Initialize gains one bool at 4.10 - a 2019 W19P release, still webOS 4 - so a sample declaring ">=4, <5" would fail to load there. The range is now ">=4, <4.10". -verify cannot catch this. The verifier ships dumps for 4.4.2 and 4.9.7 and nothing between 4.10 and 5.3, so ">=4, <5" and ">=4, <4.10" check exactly the same two firmwares and both pass. It was only visible in the larger set under dev-toolbox-cli/common/data. Said plainly in the README, because a clean -verify claiming less than it appears to is the sort of thing this repo has been bitten by before. The claim it came from was also too vague, and is now a table. Construction moves five times between 3.4 and 11.2 - constructor, Initialize arity and entry point - but everything else holds: of the 30 libcbe symbols the sample uses, the same three are the only ones missing on every release from 5.3.1 to 11.2. The other 27 are untouched across six generations. That reframes which variant is worth writing. Not webOS 5, which is a generation of one, but 6.4 through 11.2, where the constructor and Initialize stop moving - a single build covering every set from 2021 to 2025. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- README.md | 4 ++-- web/cbe/CMakeLists.txt | 8 +++++--- web/cbe/README.md | 41 +++++++++++++++++++++++++++++++-------- web/hybrid/CMakeLists.txt | 2 +- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b032281..0247fc2 100644 --- a/README.md +++ b/README.md @@ -195,8 +195,8 @@ hard to diagnose from the TV side. | `media/smp/webos5` | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): exported window accepted, full load / play / feed / EOS / unload, 300 video + 470 audio units on both. Those runs predate the `Play()` ordering fix, which all SMP samples share - re-run pending | | `media/ndl/directmedia` (v2) | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): 300 video + 469 PCM chunks on both | | `media/ndl/directmedia` (v1) | 3.5 - 4.x | built and symbol-verified, needs a 2017-2019 set to test | -| `web/hybrid` | 4.4+ | **verified on hardware** - 49LK5900, webOS 4.4.3: SDL2 and the web view both render full-screen from one process, switching works both ways, and the page's exit button reaches native code as `WebViewDelegate::Close()`. The transitions were driven by a test hook, not by the remote - key delivery is untested | -| `web/cbe` | 4.4+ | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | +| `web/hybrid` | 4.0 - 4.9 | **verified on hardware** - 49LK5900, webOS 4.4.3: SDL2 and the web view both render full-screen from one process, switching works both ways, and the page's exit button reaches native code as `WebViewDelegate::Close()`. The transitions were driven by a test hook, not by the remote - key delivery is untested | +| `web/cbe` | 4.0 - 4.9 | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | | `media/smp/webos1` | 1.x | not written yet - and there is no webOS 1 hardware here to validate it against, so it would ship untestable | ## What a `Play()` that returns true does not tell you diff --git a/web/cbe/CMakeLists.txt b/web/cbe/CMakeLists.txt index dd642ee..640db8c 100644 --- a/web/cbe/CMakeLists.txt +++ b/web/cbe/CMakeLists.txt @@ -8,8 +8,10 @@ # webOS 5 replaced the free WebOSMain() with a webos::WebOSMain class and changed # the constructor again. The hard floor is 3.4: webOS 1 and 2 have no libcbe. # -# Within webOS 4, only 4.4.2 and 4.9.7 have firmware dumps to verify against; -# 4.0 to 4.3 is assumed, not checked. See README.md. +# The upper bound is 4.9, not 5: Initialize() gains one bool at 4.10, well before +# the webOS 5 rewrite. -verify cannot catch that - it has no 4.10 dump - so the +# range is set from the larger symbol set in dev-toolbox-cli. Within webOS 4 only +# 4.4.2 and 4.9.7 have dumps at all; 4.0 to 4.3 is assumed. See README.md. find_package(PkgConfig REQUIRED) pkg_check_modules(GLIB REQUIRED glib-2.0) @@ -32,4 +34,4 @@ webos_add_ipk( APPID org.webosbrew.sample.web.cbe TITLE "CBE WebView (webOS 4)" COLOR "#4285F4" - WEBOS_VERSIONS ">=4, <5") + WEBOS_VERSIONS ">=4, <4.10") diff --git a/web/cbe/README.md b/web/cbe/README.md index a98d40d..cd558b5 100644 --- a/web/cbe/README.md +++ b/web/cbe/README.md @@ -70,10 +70,29 @@ Of the 30 libcbe symbols this sample needs, 11 are absent from a webOS 3.4 dump them either old-ABI spellings or the newer split-init calls. So webOS 3 wants a variant built `-D_GLIBCXX_USE_CXX11_ABI=0` against an older header, not a newer runtime. -**Above.** webOS 5 replaced the free `WebOSMain()` with a `webos::WebOSMain` class and -changed `WebViewBase`'s constructor again; webOS 6 added a whole second API under -`neva_app_runtime`. The `webos::` API itself survives to 11.2, so that variant is a small -delta rather than a rewrite. +**Above.** `webos::WebViewBase` exists all the way to 11.2, but it is not one API - how you +construct and initialise it moves five times: + +| release | constructor | `Initialize(...)` | entry point | +|---|---|---|---| +| 3.4 - 3.9 | `WebViewBase(int, int)` | *(none)* | `WebOSMain()`, pre-C++11 strings | +| **4.0 - 4.9** | **`WebViewBase()`** | **`(5 str, int, int, bool)`** | **`WebOSMain()`** - what this sample builds | +| 4.10 | `WebViewBase()` | `(5 str, int, int, bool, bool)` | `WebOSMain()` | +| 5.x | `WebViewBase(int, int)` | `(5 str, int, int, bool, WebViewMode, bool)` | `webos::WebOSMain::Run()` | +| 6.4 - 11.2 | `WebViewBase(bool, int, int)` | `(5 str, bool)` | `webos::WebOSMain::Run()` | + +Everything else is stable: of the 30 libcbe symbols this sample uses, the *same three* are +the only ones missing on every release from 5.3.1 to 11.2 - the free `WebOSMain`, the +no-argument constructor, and that `Initialize`. The other 27 are untouched across six +generations. + +So the interesting variant is not webOS 5, it is **6.4 through 11.2**: one build covers +every set from 2021 to 2025, because the constructor and `Initialize` do not move again +after 6.4. (Those releases also carry a second, parallel API under `neva_app_runtime`, +which is the one with public upstream headers.) + +The upper bound of *this* sample is therefore 4.9, not 5 - the break is `Initialize` +gaining one `bool` at 4.10, not the webOS 5 rewrite. **The hard floor is 3.4**, and it is the library, not the ABI: webOS 1 and 2 have no `libcbe.so` at all. Chromium arrived with webOS 3. @@ -113,11 +132,17 @@ A 55LF6310 (webOS 2.2.0) is the set that could settle the first point. ## What "webOS 4" is actually backed by The `-verify` firmware set has exactly two webOS 4 dumps, 4.4.2 and 4.9.7, and both are -clean. There is no dump anywhere between 4.0 and 4.3, so that part of the declared -`>=4, <5` range rests on the API being unchanged across the generation rather than on -evidence. Hardware testing was on a 49LK5900 at 4.4.3. +clean. There is no dump anywhere between 4.0 and 4.3, so that part of the declared range +rests on the API being unchanged across the generation rather than on evidence. Hardware +testing was on a 49LK5900 at 4.4.3. + +Read it as: **4.4.2 and 4.9.7 verified, 4.0 to 4.3 assumed, 4.10 known broken.** -Read the floor as: **4.4.2 verified, 4.0 assumed.** +That last one is worth dwelling on, because `-verify` cannot see it: the verifier ships no +4.10 dump, so `>=4, <5` and `>=4, <4.10` check exactly the same two firmwares and both pass. +The break was only visible in the larger symbol set under +`dev-toolbox-cli/common/data`. A clean `-verify` means "nothing missing in the dumps we +have", which is a narrower claim than it looks. ## Where the headers came from diff --git a/web/hybrid/CMakeLists.txt b/web/hybrid/CMakeLists.txt index 1c3a6b1..8fa1680 100644 --- a/web/hybrid/CMakeLists.txt +++ b/web/hybrid/CMakeLists.txt @@ -28,5 +28,5 @@ webos_add_ipk( APPID org.webosbrew.sample.web.hybrid TITLE "SDL + WebView (webOS 4)" COLOR "#7E57C2" - WEBOS_VERSIONS ">=4, <5" + WEBOS_VERSIONS ">=4, <4.10" MEDIA "${CMAKE_CURRENT_SOURCE_DIR}/page.html") From 1f14d59cc6a343ba7ac1951ff87bc136620c5017 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sat, 22 Aug 2026 18:31:07 +0900 Subject: [PATCH 10/40] web/: scope by major release, and note a -verify false negative The firmware dumps carry one entry per major webOS release, so a starfish number stands for a whole generation rather than a single build. That makes the earlier "4.0 to 4.3 is assumed" hedge meaningless - starfish 4.4.2 *is* the webOS 4.0 generation - and it sharpens what these samples actually cover. webOS 4 has two dumps: starfish 4.4.2 (HE_DTV_W18R) is webOS 4.0, the 2018 sets, which is what this targets and what the 49LK5900 tested against; starfish 4.10.0 (HE_DTV_W19P) is webOS 4.5, the 2019 sets, where Initialize takes a ninth argument. So the honest scope is one model generation, and reaching the next one is a single bool - the smallest variant on offer anywhere in this family, and it doubles the hardware. Also records that webosbrew-elf-verify reports All OK for starfish 4.10.0 even though nothing in that entire dump exports the eight-argument Initialize - grepping every .json in the firmware finds only the nine-argument one. It flags the same symbol correctly on 5.3.1, so the check works in general and this is a false negative. The declared range was set by reading the symbol tables rather than by trusting the tool, and the README says so, because a clean -verify meaning less than it appears to is a trap this repo has hit from the other direction already. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- README.md | 4 ++-- web/cbe/CMakeLists.txt | 10 ++++++---- web/cbe/README.md | 37 ++++++++++++++++++++++++++----------- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 0247fc2..c0688fc 100644 --- a/README.md +++ b/README.md @@ -195,8 +195,8 @@ hard to diagnose from the TV side. | `media/smp/webos5` | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): exported window accepted, full load / play / feed / EOS / unload, 300 video + 470 audio units on both. Those runs predate the `Play()` ordering fix, which all SMP samples share - re-run pending | | `media/ndl/directmedia` (v2) | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): 300 video + 469 PCM chunks on both | | `media/ndl/directmedia` (v1) | 3.5 - 4.x | built and symbol-verified, needs a 2017-2019 set to test | -| `web/hybrid` | 4.0 - 4.9 | **verified on hardware** - 49LK5900, webOS 4.4.3: SDL2 and the web view both render full-screen from one process, switching works both ways, and the page's exit button reaches native code as `WebViewDelegate::Close()`. The transitions were driven by a test hook, not by the remote - key delivery is untested | -| `web/cbe` | 4.0 - 4.9 | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | +| `web/hybrid` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: SDL2 and the web view both render full-screen from one process, switching works both ways, and the page's exit button reaches native code as `WebViewDelegate::Close()`. The transitions were driven by a test hook, not by the remote - key delivery is untested | +| `web/cbe` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | | `media/smp/webos1` | 1.x | not written yet - and there is no webOS 1 hardware here to validate it against, so it would ship untestable | ## What a `Play()` that returns true does not tell you diff --git a/web/cbe/CMakeLists.txt b/web/cbe/CMakeLists.txt index 640db8c..dddf118 100644 --- a/web/cbe/CMakeLists.txt +++ b/web/cbe/CMakeLists.txt @@ -8,10 +8,12 @@ # webOS 5 replaced the free WebOSMain() with a webos::WebOSMain class and changed # the constructor again. The hard floor is 3.4: webOS 1 and 2 have no libcbe. # -# The upper bound is 4.9, not 5: Initialize() gains one bool at 4.10, well before -# the webOS 5 rewrite. -verify cannot catch that - it has no 4.10 dump - so the -# range is set from the larger symbol set in dev-toolbox-cli. Within webOS 4 only -# 4.4.2 and 4.9.7 have dumps at all; 4.0 to 4.3 is assumed. See README.md. +# The upper bound is starfish 4.9, not 5. The firmware dumps carry one entry per +# major release, and webOS 4 has two of them: starfish 4.4.2 is webOS 4.0 (the +# 2018 sets, which this targets) and starfish 4.10.0 is webOS 4.5 (2019), where +# Initialize() takes a ninth argument. -verify does not catch that - it reports +# All OK for 4.10.0 despite the symbol being absent from the whole dump - so this +# range comes from reading the symbol tables. See README.md. find_package(PkgConfig REQUIRED) pkg_check_modules(GLIB REQUIRED glib-2.0) diff --git a/web/cbe/README.md b/web/cbe/README.md index cd558b5..2095c57 100644 --- a/web/cbe/README.md +++ b/web/cbe/README.md @@ -129,20 +129,35 @@ Two things make it a separate project rather than a variant of this one: A 55LF6310 (webOS 2.2.0) is the set that could settle the first point. -## What "webOS 4" is actually backed by +## Which sets that actually is -The `-verify` firmware set has exactly two webOS 4 dumps, 4.4.2 and 4.9.7, and both are -clean. There is no dump anywhere between 4.0 and 4.3, so that part of the declared range -rests on the API being unchanged across the generation rather than on evidence. Hardware -testing was on a 49LK5900 at 4.4.3. +The firmware dumps carry one entry per major webOS release, so a release number in the +table above stands for the whole generation rather than for one build. Two of them are +webOS 4: -Read it as: **4.4.2 and 4.9.7 verified, 4.0 to 4.3 assumed, 4.10 known broken.** +| dump | LG's name for it | this sample | +|---|---|---| +| starfish 4.4.2 (`HE_DTV_W18R`) | webOS 4.0, the 2018 sets | **works** | +| starfish 4.10.0 (`HE_DTV_W19P`) | webOS 4.5, the 2019 sets | **does not load** | + +So the honest scope is *webOS 4.0 only* - one model generation. Hardware testing was a +49LK5900 (2018) at starfish 4.4.3. + +Reaching the 2019 sets is one `bool`: `Initialize` takes nine arguments there instead of +eight. That is a smaller delta than any other variant on this page, and it doubles the +hardware. + +### `-verify` does not catch this + +Worth knowing before trusting a green run. `webosbrew-elf-verify` reports **All OK** for +starfish 4.10.0 even though nothing in that entire dump exports the eight-argument +`Initialize` - grepping every `.json` in the firmware finds only the nine-argument one. It +flags the same symbol correctly on 5.3.1 ("missing symbol ... is bound lazily"), so the +check works in general and this firmware is a false negative. -That last one is worth dwelling on, because `-verify` cannot see it: the verifier ships no -4.10 dump, so `>=4, <5` and `>=4, <4.10` check exactly the same two firmwares and both pass. -The break was only visible in the larger symbol set under -`dev-toolbox-cli/common/data`. A clean `-verify` means "nothing missing in the dumps we -have", which is a narrower claim than it looks. +The range here was therefore set by reading the symbol tables directly, not by trusting the +tool. Which is the same lesson this repo already learned from the other direction: a clean +`-verify` says the symbols were found, not that the app runs. ## Where the headers came from From 9a6531e4b1693d1e1659d9453696abde19595e41 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sat, 22 Aug 2026 18:53:24 +0900 Subject: [PATCH 11/40] web/libcbe: name the delegate slots from the OSE SDK's own header Correcting a claim repeated through these commits: "there is no SDK for this" is wrong. /opt/webos-sdk-x86_64 ships webos/webview_base.h, webview_delegate.h and webapp_window_delegate.h, plus all of WAM under usr/include/webappmanager/, and a libcbe.so besides. It confirms every load-bearing fact the vtable archaeology produced, arrived at independently: WebViewBase deriving from WebViewDelegate with a single WebView* private member (so 8 bytes, matching operator new(32) and the offset-8 writes), a WebViewDelegate with no virtual destructor, and a WebAppWindowDelegate that has one. It also supplies names for the slots that were HandleUnknown17/18/21/22/23 and corrects CheckKeyFilterTable's return type from bool to unsigned. It does not replace the reconstruction, which is why the headers here stay. The SDK is chromium53 and retail webOS 4 is chromium68, with the delegate moved in between: AcceptsVideoCapture and AcceptsAudioCapture are gone, and DidStartNavigation, DidFinishNavigation and LoadAborted are new. Its libcbe is a third ABI again - webos::WebOSMain::Run and a six-argument Initialize, the webOS 6+ shape. So slot numbers still come from the vtables; only the names come from the header, and the ones past 20 are marked unconfirmed since ~WebViewBase is virtual there and may occupy some of them. AllowMouseOnOffEvent now returns bool rather than void, so a caller reading the result gets a definite answer rather than whatever was left in r0. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe/README.md | 28 ++++++++++++++++++++--- web/libcbe/webos/webapp_window_base.h | 3 ++- web/libcbe/webos/webview_base.h | 32 ++++++++++++++++++--------- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/web/cbe/README.md b/web/cbe/README.md index 2095c57..7612d5e 100644 --- a/web/cbe/README.md +++ b/web/cbe/README.md @@ -5,10 +5,15 @@ LG's webOS patches, and the thing every web app on the TV actually runs inside. plain shared library with a C++ ABI, and a native app can link against it and get a real web view - no WAM, no web app package, no `type: "web"`. -There is no SDK for this. No headers ship on the device or in the NDK, and the library is -stripped. What this sample links against was reconstructed, and the reconstruction is the +No headers ship on the device, none ship in the buildroot NDK, and the library is stripped, +so what this sample links against was reconstructed - and the reconstruction is the interesting part, so it is written down below. +There *is* one more place to look, though it does not remove the need: the **webOS OSE SDK** +(`/opt/webos-sdk-x86_64`) ships real `webos/webview_base.h`, `webview_delegate.h` and +`webapp_window_delegate.h`, plus the whole of WAM under `usr/include/webappmanager/`. See +"What the OSE SDK gives you" below for why that is a naming reference rather than an ABI. + ## What it does Hands the process to Chromium, waits to be called back on the browser UI thread, and puts @@ -161,7 +166,7 @@ tool. Which is the same lesson this repo already learned from the other directio ## Where the headers came from -Two independent sources that agree with each other: +Three sources that agree with each other: * **Firmware symbol tables** (`dev-toolbox-cli/common/data/*/libcbe.so.json`) give every exported name, and therefore every signature, across every release. @@ -170,6 +175,23 @@ Two independent sources that agree with each other: pin down slot order, and their constructors pin down object size - `operator new(32)` followed by a `BlinkWebView` that writes its first field at offset 8 says `webos::WebViewBase` is exactly 8 bytes: a vptr and one pimpl pointer. +* **The OSE SDK headers**, for names and return types the binaries cannot give. + +### What the OSE SDK gives you + +`/opt/webos-sdk-x86_64/.../usr/include/chromium53/webos/` has the genuine article, and it +confirms all three load-bearing facts above independently: `WebViewBase : public +WebViewDelegate` with a single `WebView* m_webview` private member, a `WebViewDelegate` +with no virtual destructor, and a `WebAppWindowDelegate` that has one. It also names the +delegate slots this repo had numbered, and corrects `CheckKeyFilterTable` from `bool` to +`unsigned`. + +What it cannot do is replace the reconstruction, because it is a different Chromium. The +SDK is chromium53; retail webOS 4 is chromium68, and LG moved the delegate in between - +`AcceptsVideoCapture` and `AcceptsAudioCapture` are gone, `DidStartNavigation`, +`DidFinishNavigation` and `LoadAborted` are new. Its bundled `libcbe.so` is a third thing +again, exporting `webos::WebOSMain::Run` and a six-argument `Initialize`, which is the +webOS 6+ shape. Useful, but not the TV. Three facts in `web/libcbe/webos/webview_base.h` are load-bearing: diff --git a/web/libcbe/webos/webapp_window_base.h b/web/libcbe/webos/webapp_window_base.h index a7abf35..f63fe3c 100644 --- a/web/libcbe/webos/webapp_window_base.h +++ b/web/libcbe/webos/webapp_window_base.h @@ -29,7 +29,8 @@ class WebAppWindowDelegate { public: virtual ~WebAppWindowDelegate() {} virtual bool event(WebOSEvent*) { return false; } - virtual bool CheckKeyFilterTable(unsigned, unsigned*) { return false; } + // unsigned, not bool - the SDK's webos/webapp_window_delegate.h says so. + virtual unsigned CheckKeyFilterTable(unsigned, unsigned*) { return 0; } }; class WebAppWindowBase : public WebAppWindowDelegate { diff --git a/web/libcbe/webos/webview_base.h b/web/libcbe/webos/webview_base.h index c2858d9..995ca4b 100644 --- a/web/libcbe/webos/webview_base.h +++ b/web/libcbe/webos/webview_base.h @@ -55,13 +55,18 @@ class WebViewDelegate { virtual void RenderProcessCreated(int pid) = 0; virtual void RenderProcessGone() = 0; - // Slots 17 to 23. Two of them are named in WAM's binary - the browser-control - // bridge behind window.PalmSystem - and the rest WAM overrides with empty - // bodies, which is all the sample needs them to be. They are declared void and - // argument-less on purpose: the callee never touches the arguments, and on - // AAPCS ignoring them is safe. Do not add or remove entries. - virtual void HandleUnknown17() {} - virtual void HandleUnknown18() {} + // Slots 17 to 23. Two are named in WAM's binary - the browser-control bridge + // behind window.PalmSystem - and the names of the rest come from the webOS OSE + // SDK's own webos/webview_delegate.h, which is chromium53 where the TV is + // chromium68. That header is a naming reference, not the ABI: its version of + // this class has AcceptsVideoCapture/AcceptsAudioCapture where webOS 4 has + // DidStartNavigation, DidFinishNavigation and LoadAborted, so the slot numbers + // below still come from the vtable rather than from it. + // + // DidClearWindowObject is not a guess: slot 18 fires twice during a page load, + // which is exactly when the window object is recreated. + virtual void DidHistoryBackOnTopPage() {} + virtual void DidClearWindowObject() {} virtual void HandleBrowserControlCommand( const std::string& command, const std::vector& arguments) { (void)command; @@ -75,9 +80,16 @@ class WebViewDelegate { (void)arguments; (void)result; } - virtual void HandleUnknown21() {} - virtual void HandleUnknown22() {} - virtual void HandleUnknown23() {} + // Beyond 20 the mapping is unconfirmed. The SDK header offers + // DidDropAllPeerConnections(reason), AllowMouseOnOffEvent() and + // SendCookiesForHostname(), and also declares ~WebViewBase() virtual - so one + // of these pairs may be the destructor slots instead. They are kept as + // no-argument stubs because nothing here calls them; AllowMouseOnOffEvent + // returns bool rather than void so a caller that reads the result gets a + // definite answer instead of whatever was in r0. + virtual void DidDropAllPeerConnections() {} + virtual bool AllowMouseOnOffEvent() const { return false; } + virtual void SendCookiesForHostname() {} }; class WebViewBase : public WebViewDelegate { From 3ec4d28a2942928a2b3b0444368b844cf7ed962b Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sat, 22 Aug 2026 19:42:26 +0900 Subject: [PATCH 12/40] web/hybrid: fix the white screen on the second visit, and a segfault Three separate bugs, all in the path back into the web view. Hide() destroys the Wayland window rather than unmapping it - "Wayland Window(id:1) will be destroyed" - so the next Show() builds a new one and web contents left attached to the old one composite nowhere. AttachWebContents now runs on every show. It also has to run *before* Show(): attaching afterwards leaves the page loading normally, reporting load finished, and never appearing. DetachWebContents() on the way out segfaults on a null pointer, because by then the contents it would detach are already gone. Removed. And the page was leaving via PalmSystem.close(), which reads like the right call and is not: the callback arrives from RenderViewHostImpl::OnClose() with the render view already being destroyed, so the next visit gets a dead view. PalmSystem.platformBack() is a plain notification and the page survives it. Close() is still handled so a page that really closes itself hands the screen back rather than leaving a dead window up. Delegate callbacks also run inside libcbe's own call stack, so switching views from one lands mid-teardown and segfaults - ShowNativeView sat directly under RenderViewHostImpl::OnClose() in the crash backtrace. The switch is now deferred through a one-shot g_idle_add. Verified on a 49LK5900 by driving the transitions from a trigger file and capturing each state: first web view, native, second web view - the second renders identically to the first, heading and button and all. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/hybrid/README.md | 39 ++++++++++++++++----- web/hybrid/main.cpp | 80 ++++++++++++++++++++++++++++++++++++++------ web/hybrid/page.html | 13 ++++--- 3 files changed, 110 insertions(+), 22 deletions(-) diff --git a/web/hybrid/README.md b/web/hybrid/README.md index a23ced2..6c6c0b7 100644 --- a/web/hybrid/README.md +++ b/web/hybrid/README.md @@ -50,23 +50,40 @@ Coming back resumes rather than reloads. That is the whole reason for suspending destroying: the second visit keeps whatever state the page had, and the log shows no navigation at all. +Two things about coming back are not obvious, and both showed up as a white screen on the +second visit with the page alive and running script behind it. + +**`Hide()` destroys the window, it does not unmap it.** The log is explicit - `Wayland +Window(id:1 widget:0xc1f28) will be destroyed` - and the next `Show()` builds a new one, +`id:2`. Web contents attached to the old window composite nowhere. So +`AttachWebContents()` has to run on *every* show, not just the first. + +**It has to run before `Show()`, not after.** Attaching afterwards leaves the page loading +normally, reporting `load finished`, and never appearing. + +There is no matching `DetachWebContents()` on the way out. Calling it there segfaults on a +null pointer: by then the contents it would detach are already gone. + ## How the page asks to leave Through libcbe's own callbacks. The app loads the `palmsystem` injection, which gives page JavaScript real entry points, and two of them arrive in the delegate: -| JavaScript | native | -|---|---| -| `PalmSystem.close()` | `WebViewDelegate::Close()` | -| `PalmSystem.platformBack()` | `HandleBrowserControlCommand("platformBack")` | +| JavaScript | native | what it means | +|---|---|---| +| `PalmSystem.platformBack()` | `HandleBrowserControlCommand("platformBack")` | a notification - nothing is torn down | +| `PalmSystem.close()` | `WebViewDelegate::Close()` | a real close - the render view is going away | ```js -function exitToNative() { PalmSystem.close(); } +function exitToNative() { PalmSystem.platformBack(); } ``` -`Close()` is the delegate's own dedicated slot - a callback that exists for exactly this - -so nothing is overloaded, nothing has to be parsed out of a shared channel, and the page -carries no state between visits. +**Not `close()`.** It reads like the right call and it is not. The callback arrives from +`RenderViewHostImpl::OnClose()` with the render view already being destroyed, which is +correct for a page that is quitting and wrong for one stepping aside for a moment - the +next visit gets a dead view. `platformBack()` is a plain notification, so the page survives +to be shown again. `Close()` is still handled, so a page that really does close itself +hands the screen back rather than leaving a dead window up. Turning the injection on is two lines, and both matter: @@ -124,6 +141,12 @@ both directions, the page's exit button reaches native code as `WebViewDelegate: and re-entering the web view resumes the existing page without reloading. The SDL view keeps animating after coming back. +Anything that changes the window or the web view must also be **deferred out of a delegate +callback**. Those callbacks run inside libcbe's own call stack, and switching views from +one lands in the middle of a teardown it has not finished - a null-pointer segfault, with +`ShowNativeView` sitting directly under `RenderViewHostImpl::OnClose()` in the backtrace. +A one-shot `g_idle_add` is enough: the switch then happens once libcbe is back at idle. + **Not verified: the remote itself.** Every transition above was driven from a test hook calling the same functions a key press would. Synthetic key injection through `/dev/uinput` does not work here - the device registers, but LSM does not route its events to the app - diff --git a/web/hybrid/main.cpp b/web/hybrid/main.cpp index f11ae16..b562408 100644 --- a/web/hybrid/main.cpp +++ b/web/hybrid/main.cpp @@ -29,6 +29,9 @@ extern "C" int WebOSMain(int argc, const char** argv); +// Only the webOS fork of SDL has this, and only from webOS 4 on. +extern "C" SDL_bool SDL_webOSCursorVisibility(SDL_bool visible) __attribute__((weak)); + namespace { const char kAppId[] = "org.webosbrew.sample.web.hybrid"; @@ -39,6 +42,7 @@ std::string g_app_path; class HybridWebView; void ShowNativeView(); +void RequestNativeView(); // The page asks to leave through libcbe's own callbacks, not through a side // effect of some UI property. Loading the "palmsystem" injection gives page @@ -67,17 +71,22 @@ class HybridWebView : public webos::WebViewBase { void DidFirstFrameFocused() override {} void LoadVisuallyCommitted() override {} void NavigationHistoryChanged() override {} - // PalmSystem.close(): the page is done and wants to be dismissed. + // PalmSystem.close(): a real close. Chromium means it - the callback arrives + // from RenderViewHostImpl::OnClose() and the render view is being destroyed - + // so this is the wrong way to say "hide me, I will be back". The page uses + // platformBack for that; this is kept so a page that does close itself still + // hands the screen back rather than leaving a dead window up. void Close() override { - puts("[web] page called PalmSystem.close()"); - ShowNativeView(); + puts("[web] page closed itself"); + RequestNativeView(); } - // PalmSystem.platformBack(): a Back gesture the page chose not to consume. + // PalmSystem.platformBack(): a Back gesture, and only a notification - nothing + // is torn down, so the page survives to be shown again. void HandleBrowserControlCommand(const std::string& command, const std::vector&) override { printf("[web] browser control '%s'\n", command.c_str()); - if (command == "platformBack") ShowNativeView(); + if (command == "platformBack") RequestNativeView(); } bool DecidePolicyForResponse(bool, int, const std::string&, const std::string&) override { return false; @@ -109,6 +118,10 @@ SDL_Renderer* g_sdl_renderer; bool g_native_visible; int g_frame; +// Where "open the web view" lives on screen. The remote's pointer gives SDL +// ordinary mouse events, so the same rectangle serves a click and a wheel press. +const SDL_Rect kOpenButton = {660, 470, 600, 140}; + void DrawNativeView() { // Something obviously native and obviously alive, so it is clear at a glance // which of the two views is on screen. @@ -117,10 +130,24 @@ void DrawNativeView() { SDL_SetRenderDrawColor(g_sdl_renderer, pulse, pulse / 2, 120, 255); SDL_RenderClear(g_sdl_renderer); - SDL_SetRenderDrawColor(g_sdl_renderer, 255, 255, 255, 255); + // A moving bar, so a still frame still shows the loop is running. + SDL_SetRenderDrawColor(g_sdl_renderer, 255, 255, 255, 90); const int bar = 40 + (g_frame % 120) * 8; - SDL_Rect r = {200, 500, bar, 80}; - SDL_RenderFillRect(g_sdl_renderer, &r); + SDL_Rect moving = {200, 240, bar, 40}; + SDL_RenderFillRect(g_sdl_renderer, &moving); + + // The button. Highlighted while the pointer is over it, so it is obvious the + // app is seeing the remote at all. + int mx = 0, my = 0; + SDL_GetMouseState(&mx, &my); + const SDL_Point p = {mx, my}; + const bool hot = SDL_PointInRect(&p, &kOpenButton) == SDL_TRUE; + SDL_SetRenderDrawColor(g_sdl_renderer, hot ? 0x42 : 0x20, hot ? 0x85 : 0x40, + hot ? 0xf4 : 0x90, 255); + SDL_RenderFillRect(g_sdl_renderer, &kOpenButton); + SDL_SetRenderDrawColor(g_sdl_renderer, 255, 255, 255, 255); + SDL_RenderDrawRect(g_sdl_renderer, &kOpenButton); + SDL_RenderPresent(g_sdl_renderer); ++g_frame; } @@ -158,7 +185,6 @@ void ShowWebView() { // Initialize() above, and the name is "palmsystem", not "v8/palmsystem". g_webview->LoadExtension("palmsystem"); g_webview->UpdatePreferences(); - g_window->AttachWebContents(g_webview->GetWebContents()); } else { // Second time round the page is still loaded - only woken up. g_webview->ResumeWebPageDOM(); @@ -166,6 +192,13 @@ void ShowWebView() { } g_webview->SetVisible(true); + + // Every time, not just the first, and before Show(). Hide() does not unmap the + // window - it destroys it, "Wayland Window(id:1) will be destroyed" - so the + // next Show() builds a fresh one and contents left attached to the old window + // composite nowhere. Attaching after Show() does not work either: the page + // loads and never appears. + g_window->AttachWebContents(g_webview->GetWebContents()); g_window->Show(); g_window->Activate(); g_web_visible = true; @@ -188,6 +221,8 @@ void ShowNativeView() { g_webview->SuspendPaintingAndSetVisibilityHidden(); g_webview->SuspendWebPageDOM(); g_webview->SetVisible(false); + // No DetachWebContents() here. It segfaults: by the time this runs the + // contents libcbe would detach are already gone, and it dereferences null. g_window->SetWindowHostState(webos::NATIVE_WINDOW_MINIMIZED); g_window->Hide(); g_web_visible = false; @@ -198,12 +233,32 @@ void ShowNativeView() { } } +// Delegate callbacks arrive *inside* libcbe's own call stack - Close() comes +// straight out of RenderViewHostImpl::OnClose() - so tearing the window down +// from one lands in the middle of a teardown libcbe has not finished. It +// segfaults on a null pointer. Bouncing through the loop first means the switch +// happens once libcbe is back at idle and its own state is consistent. +gboolean SwitchToNativeLater(gpointer) { + ShowNativeView(); + return G_SOURCE_REMOVE; +} + +void RequestNativeView() { g_idle_add(SwitchToNativeLater, NULL); } + // ------------------------------------------------------------- the one loop gboolean Pump(gpointer) { SDL_Event e; while (SDL_PollEvent(&e)) { if (e.type == SDL_QUIT) return G_SOURCE_REMOVE; + + if (e.type == SDL_MOUSEBUTTONDOWN) { + printf("[sdl] click at %d,%d\n", e.button.x, e.button.y); + // Anywhere is fine: the rectangle is an affordance, not a hit test, and a + // TV pointer is imprecise enough that demanding accuracy is unkind. + if (g_native_visible) ShowWebView(); + continue; + } if (e.type != SDL_KEYDOWN) continue; printf("[sdl] key %d\n", static_cast(e.key.keysym.sym)); switch (e.key.keysym.sym) { @@ -257,8 +312,13 @@ gboolean StartApp(gpointer) { printf("[sdl] SDL_CreateRenderer failed: %s\n", SDL_GetError()); return G_SOURCE_REMOVE; } + // The remote's pointer is a mouse as far as SDL is concerned, but the cursor + // has to be asked for. Weakly linked: webOS 3 and older have the hints but not + // this call, and a hard reference would stop the app loading there. + if (SDL_webOSCursorVisibility != NULL) SDL_webOSCursorVisibility(SDL_TRUE); + g_native_visible = true; - puts("[sdl] native view up - OK/Enter opens the web view"); + puts("[sdl] native view up - click, or press OK/Enter, to open the web view"); g_timeout_add(16, Pump, NULL); return G_SOURCE_REMOVE; diff --git a/web/hybrid/page.html b/web/hybrid/page.html index 4ed0b52..f223f2c 100644 --- a/web/hybrid/page.html +++ b/web/hybrid/page.html @@ -21,9 +21,14 @@

web view

From f5526c114a04a0a2dbb4e96513c1c276f2abec22 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sat, 22 Aug 2026 23:21:02 +0900 Subject: [PATCH 16/40] web/hybrid: verify the remote key path, and ask for Back properly The OK key was the last thing in this sample driven only by a test hook. /dev/uinput was a dead end - the virtual device registers and LSM routes nothing from it - but the service behind the phone-remote app does the job: luna-send -f luna://com.webos.service.networkinput/test/sendKeyCode \ '{"keyCode":28}' It takes evdev codes rather than webOS or JavaScript ones. 28 is KEY_ENTER, and the app logs [sdl] key 13 - SDLK_RETURN - then [switch] native -> web. So the OK path is confirmed end to end, and the recipe is in the README because it is useful to any sample here that reads the remote. Back is still unverified, and now for a known reason rather than for lack of trying: 158, 174 and 1 inject without error and reach nothing in either view, and do not close the app either, so something filters them before any window sees them. sendSpecialKey is no help - the key table in /usr/sbin/network-input-service covers media and menu keys with no BACK or EXIT in it. A real remote may still deliver what this service will not. The web window does now ask for the key, with the property names WAM uses - _WEBOS_ACCESS_POLICY_KEYS_BACK and _WEBOS_ACCESS_POLICY_KEYS_EXIT, which appear nowhere but inside libWebAppMgr.so. Kept because it is what the platform expects, and flagged as unverified rather than claimed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- README.md | 2 +- web/hybrid/README.md | 40 ++++++++++++++++++++++++++++++++++------ web/hybrid/main.cpp | 6 ++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 04d79d5..0e033d4 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ hard to diagnose from the TV side. | `media/smp/webos5` | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): exported window accepted, full load / play / feed / EOS / unload, 300 video + 470 audio units on both. Those runs predate the `Play()` ordering fix, which all SMP samples share - re-run pending | | `media/ndl/directmedia` (v2) | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): 300 video + 469 PCM chunks on both | | `media/ndl/directmedia` (v1) | 3.5 - 4.x | built and symbol-verified, needs a 2017-2019 set to test | -| `web/hybrid` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the sign-in flow runs end to end - native panel, web login form, and back with the username read out of the intercepted redirect URL. The transitions were driven by a test hook, not by the remote - key delivery is untested | +| `web/hybrid` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the sign-in flow runs end to end - native panel, web login form, and back with the username read out of the intercepted redirect URL. The OK key is verified too, injected with `com.webos.service.networkinput/test/sendKeyCode`; Back is not - that service will not send it | | `web/cbe` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | | `media/smp/webos1` | 1.x | not written yet - and there is no webOS 1 hardware here to validate it against, so it would ship untestable | diff --git a/web/hybrid/README.md b/web/hybrid/README.md index 2a0576d..4c74f35 100644 --- a/web/hybrid/README.md +++ b/web/hybrid/README.md @@ -284,9 +284,37 @@ one lands in the middle of a teardown it has not finished - a null-pointer segfa `ShowNativeView` sitting directly under `RenderViewHostImpl::OnClose()` in the backtrace. A one-shot `g_idle_add` is enough: the switch then happens once libcbe is back at idle. -**Not verified: the remote itself.** Every transition above was driven from a test hook -calling the same functions a key press would. Synthetic key injection through `/dev/uinput` -does not work here - the device registers, but LSM does not route its events to the app - -so the `SDLK_RETURN` and Back paths have not been exercised with real hardware. The rest of -the repo's SDL samples do handle the remote on this TV, so the mechanism is sound; this -particular wiring is simply untested. +### Testing the remote without a remote + +`/dev/uinput` is a dead end - a virtual device registers, but LSM does not route its events +to the app. What does work is the service the phone-remote app drives: + +```sh +ares-shell -d -r \ + 'luna-send -n 1 -f luna://com.webos.service.networkinput/test/sendKeyCode "{\"keyCode\":28}"' +``` + +It takes **evdev** codes, not webOS or JavaScript ones. `28` is `KEY_ENTER`, and the app +logs `[sdl] key 13` - `SDLK_RETURN` - followed by `[switch] native -> web`. So the OK-key +path is verified. (`13` also injects, arriving as SDL keysym `61`, which is a reminder that +the mapping is not identity.) + +`ls-monitor -i com.webos.service.networkinput` lists the rest, including +`getPointerInputSocket` for the magic-remote pointer. + +**Still not verified: Back.** Codes 158 (`KEY_BACK`), 174 (`KEY_EXIT`) and 1 (`KEY_ESC`) +inject without error and reach nothing, in either view, and the app does not close either - +so they are being filtered before any window sees them. `sendSpecialKey` cannot help: +strings in `/usr/sbin/network-input-service` show its table covers media and menu keys with +no BACK or EXIT in it. + +That leaves the Back path untested rather than broken - a real remote may well deliver what +this service will not. The web window does ask for the key, using the same property names +WAM uses: + +```cpp +window->SetWindowProperty("_WEBOS_ACCESS_POLICY_KEYS_BACK", "true"); +window->SetWindowProperty("_WEBOS_ACCESS_POLICY_KEYS_EXIT", "true"); +``` + +Those names are not documented anywhere; they come out of `libWebAppMgr.so`. diff --git a/web/hybrid/main.cpp b/web/hybrid/main.cpp index 869b794..d60bd7c 100644 --- a/web/hybrid/main.cpp +++ b/web/hybrid/main.cpp @@ -157,6 +157,12 @@ void ShowWebView() { g_window = new HybridWindow(); g_window->InitWindow(1920, 1080); g_window->SetWindowProperty("appId", kAppId); + // Without these the TV keeps Back and Exit for itself and the web view never + // sees them - the same access policy the SDL side asks for through + // SDL_HINT_WEBOS_ACCESS_POLICY_KEYS_*. The property names come out of + // libWebAppMgr.so, which is the only place they are written down. + g_window->SetWindowProperty("_WEBOS_ACCESS_POLICY_KEYS_BACK", "true"); + g_window->SetWindowProperty("_WEBOS_ACCESS_POLICY_KEYS_EXIT", "true"); g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); // The form needs somewhere to type from a remote. g_window->SetUseVirtualKeyboard(true); From c4bf7d39e5efbc94d647a4ae747e3ac4918835a5 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sat, 22 Aug 2026 23:24:00 +0900 Subject: [PATCH 17/40] web/hybrid: Back and Exit are scancodes, not keysyms They have no keysym at all - the webOS fork of SDL reports them only as scancodes above 480, SDL_WEBOS_SCANCODE_BACK 482 and _EXIT 505, defined in SDL_webOS.h. Pump() switched on keysym.sym, so it dropped them silently. It now checks the scancode first, and the hints use the header's constants rather than hand-copied strings. Measuring it also corrects the previous commit, which called Exit unreachable. It arrives fine - evdev 174 shows up as sym=0 scancode=505 - and had been logged all along as "[sdl] key 0", which I read as noise because only the keysym was being printed. The table is in the README now: 28 -> sym 13, 174 -> scancode 505, 1 -> 27, 14 -> 8. KEY_BACK 158 really does reach nothing. The more useful finding is that keys only reach whichever window is up. Once the web view is showing, the SDL loop sees nothing - input belongs to libcbe's window. So OK opening the web view is an SDL concern and leaving again is not: that has to come from the page's keydown handler or from WebAppWindowBase::event(), and neither is verified. Said plainly rather than implied by a handler that looks like it covers both. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- README.md | 2 +- web/hybrid/README.md | 35 ++++++++++++++++++++++++++--------- web/hybrid/main.cpp | 21 +++++++++++++++++---- 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 0e033d4..4e47904 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ hard to diagnose from the TV side. | `media/smp/webos5` | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): exported window accepted, full load / play / feed / EOS / unload, 300 video + 470 audio units on both. Those runs predate the `Play()` ordering fix, which all SMP samples share - re-run pending | | `media/ndl/directmedia` (v2) | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): 300 video + 469 PCM chunks on both | | `media/ndl/directmedia` (v1) | 3.5 - 4.x | built and symbol-verified, needs a 2017-2019 set to test | -| `web/hybrid` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the sign-in flow runs end to end - native panel, web login form, and back with the username read out of the intercepted redirect URL. The OK key is verified too, injected with `com.webos.service.networkinput/test/sendKeyCode`; Back is not - that service will not send it | +| `web/hybrid` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the sign-in flow runs end to end - native panel, web login form, and back with the username read out of the intercepted redirect URL. The OK key is verified too, injected with `com.webos.service.networkinput/test/sendKeyCode`; leaving the web view by remote is not, since keys go to whichever window is up | | `web/cbe` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | | `media/smp/webos1` | 1.x | not written yet - and there is no webOS 1 hardware here to validate it against, so it would ship untestable | diff --git a/web/hybrid/README.md b/web/hybrid/README.md index 4c74f35..508dc78 100644 --- a/web/hybrid/README.md +++ b/web/hybrid/README.md @@ -302,15 +302,32 @@ the mapping is not identity.) `ls-monitor -i com.webos.service.networkinput` lists the rest, including `getPointerInputSocket` for the magic-remote pointer. -**Still not verified: Back.** Codes 158 (`KEY_BACK`), 174 (`KEY_EXIT`) and 1 (`KEY_ESC`) -inject without error and reach nothing, in either view, and the app does not close either - -so they are being filtered before any window sees them. `sendSpecialKey` cannot help: -strings in `/usr/sbin/network-input-service` show its table covers media and menu keys with -no BACK or EXIT in it. - -That leaves the Back path untested rather than broken - a real remote may well deliver what -this service will not. The web window does ask for the key, using the same property names -WAM uses: +Injecting and watching gives this mapping, which is worth having written down because two +different numbers are involved: + +| evdev code sent | SDL `keysym.sym` | SDL `keysym.scancode` | | +|---|---|---|---| +| 28 `KEY_ENTER` | 13 `SDLK_RETURN` | 40 | OK | +| 174 `KEY_EXIT` | **0** | **505** `SDL_WEBOS_SCANCODE_EXIT` | | +| 1 `KEY_ESC` | 27 | 41 | | +| 14 `KEY_BACKSPACE` | 8 | 42 | | + +**Back and Exit have no keysym.** They arrive only as scancodes above 480, defined in +`SDL_webOS.h`, so code that switches on `keysym.sym` drops them silently - which is what +this sample did until the table above was measured. `Pump()` now checks the scancode first. + +`KEY_BACK` (158) injects without error and arrives nowhere; the service's `sendSpecialKey` +table, in `strings /usr/sbin/network-input-service`, has no BACK or EXIT either. Exit is +reachable and Back is not, at least by injection. + +### Keys only reach whichever window is up + +Once the web view is showing, the SDL loop stops seeing keys entirely - they belong to +libcbe's window. So the OK key opening the web view is an SDL concern, and leaving again is +not: that has to come from the page (its `keydown` handler) or from +`WebAppWindowBase::event()`, and neither is verified. + +The web window does ask for the key, using the same property names WAM uses: ```cpp window->SetWindowProperty("_WEBOS_ACCESS_POLICY_KEYS_BACK", "true"); diff --git a/web/hybrid/main.cpp b/web/hybrid/main.cpp index d60bd7c..11c3002 100644 --- a/web/hybrid/main.cpp +++ b/web/hybrid/main.cpp @@ -15,6 +15,7 @@ // [SDL view] --OK/Enter--> [web view] --exit button or Back--> [SDL view] #include +#include #include "native_ui.h" #include @@ -297,8 +298,20 @@ gboolean Pump(gpointer) { native_ui_handle_event(&e); if (e.type != SDL_KEYDOWN) continue; - printf("[sdl] key %d\n", static_cast(e.key.keysym.sym)); - switch (e.key.keysym.sym) { + + // Two different numbers. The remote's OK arrives as an ordinary keysym, but + // Back and Exit have no keysym at all - the webOS fork of SDL reports them + // as scancodes above 480, listed in SDL_webOS.h. Switching on keysym.sym + // alone means Back can never match, which is a quiet way to lose it. + const int sym = static_cast(e.key.keysym.sym); + const int scancode = static_cast(e.key.keysym.scancode); + printf("[sdl] key sym=%d scancode=%d\n", sym, scancode); + + if (scancode == SDL_WEBOS_SCANCODE_BACK || scancode == SDL_WEBOS_SCANCODE_EXIT) { + if (!g_native_visible) ShowNativeView(); + continue; + } + switch (sym) { case SDLK_RETURN: case SDLK_KP_ENTER: case SDLK_SPACE: @@ -333,8 +346,8 @@ gboolean StartApp(gpointer) { // Without these the TV keeps Back and Exit for itself and the app never sees // them. They are plain strings, so setting them costs nothing where they are // not understood. - SDL_SetHint("SDL_WEBOS_ACCESS_POLICY_KEYS_BACK", "true"); - SDL_SetHint("SDL_WEBOS_ACCESS_POLICY_KEYS_EXIT", "true"); + SDL_SetHint(SDL_HINT_WEBOS_ACCESS_POLICY_KEYS_BACK, "true"); + SDL_SetHint(SDL_HINT_WEBOS_ACCESS_POLICY_KEYS_EXIT, "true"); if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { printf("[sdl] SDL_INIT_VIDEO failed: %s\n", SDL_GetError()); From 456c741426bff70912bdc1872bdc20f41b1501a5 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sun, 23 Aug 2026 00:35:15 +0900 Subject: [PATCH 18/40] web/hybrid: record that the web view starts with nothing focused The README read as though the sign-in flow worked by remote in two presses. It does not: it is OK, Tab, Tab, Tab, Enter, because the page has no focused element until Tab creates one, and Enter does nothing until then. By pointer it is one click, since mouse events carry their own target. autofocus, a page-side focus() on load, and WebViewBase::SetFocus(true) were each tried and none helped. libcbe logs "Not implemented ... WebOSView::OnWidgetActivationChanged", and widget activation is what hands focus to Chromium's FocusManager, so that is the likely cause and not something page-side JavaScript can reach. SetCSSNavigationEnabled(true) is the untested next idea. Full write-up is on the pull request; this is the short version, in the repo, so the sample does not claim more than it does. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/hybrid/README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/web/hybrid/README.md b/web/hybrid/README.md index 508dc78..5dc5df9 100644 --- a/web/hybrid/README.md +++ b/web/hybrid/README.md @@ -278,6 +278,9 @@ the redirect never loaded. Both views render full-screen, and the switch works i directions. The SDL view keeps animating after coming back. +Driven entirely by injected remote keys, one at a time, comparing frames pixel-by-pixel - +which is how the focus limitation above turned up. By pointer the same flow is two clicks. + Anything that changes the window or the web view must also be **deferred out of a delegate callback**. Those callbacks run inside libcbe's own call stack, and switching views from one lands in the middle of a teardown it has not finished - a null-pointer segfault, with @@ -335,3 +338,24 @@ window->SetWindowProperty("_WEBOS_ACCESS_POLICY_KEYS_EXIT", "true"); ``` Those names are not documented anywhere; they come out of `libWebAppMgr.so`. + +### The web view starts with nothing focused + +By remote the flow is **OK, Tab, Tab, Tab, Enter**, not OK then Enter. Until Tab is pressed +the page has no focused element, so Enter does nothing; Tab does not merely move focus, it +creates it. After that Enter activates a focused button, though it still never submits from +a text input. By pointer it is one click, because mouse events carry their own target. + +`autofocus`, a page-side `.focus()` on load, and `WebViewBase::SetFocus(true)` were all +tried and none helped. libcbe hints at why: + +``` +ERROR:webos_view.h(123)] Not implemented reached in + virtual void WebOSView::OnWidgetActivationChanged(views::Widget*, bool) +``` + +Widget activation is a stub in this build, and in Chromium's views that is what hands focus +to the `FocusManager` - which would explain a web view that takes pointer input and ignores +the keyboard until focus traversal forces the issue. Untested next step: +`SetCSSNavigationEnabled(true)`, webOS's own spatial navigation. There is a fuller write-up +on the pull request. From 83364feedd91b53eb8ca88af656da4fd478606aa Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 00:09:09 +0900 Subject: [PATCH 19/40] web/cbe-webos3: the webOS 3 build - ABI proven, window handover not A sibling of web/cbe against the libcbe that shipped on webOS 3, measured on a 43UH6100 at starfish 3.4.0. Not a build variant: between 3 and 4 the library changed its API as well as its string ABI. WebViewBase takes its size in the constructor with no Initialize(), WebAppWindowBase has no InitWindow(), the delegate has its own slot order with DidFirstNonBlankPaint at 2 and LoadProgressChanged taking a URL, LoadStarted carries the URL where webOS 4 grew a separate DidStartNavigation, and the browser-control pair sits at 18/19. The layout came from webOS 3's own libWebAppMgr, the same way the webOS 4 one did. Two undocumented things stop it starting, and both cost real time to find. --ozone-platform is "weboswayland" on this generation, not "wayland". And CDM_LIB_PATH must be set: webOS 3's WebOSMain does std::string(getenv(...)) with no null check and appends /libwidevinecdmadapter.so, so an unset variable aborts the process before any of our code runs, with nothing to go on but "basic_string::_S_construct null not valid". Finding it meant pulling the 64 MB library, resolving the crash address to WebOSMain+0x21f8 and reading the literal the getenv loads. -verify also caught SetTrustLevel and UpdatePreferences, copied from the webOS 4 sample and simply absent here. What works: the process starts, the page loads, and every delegate callback fires with the recovered signatures - so the vtable lines up and the old-ABI strings arrive intact. What does not: the surface never reaches the screen. LSM keeps the previous app foreground, and SetWindowProperty("appId"), SetWindowHostState, SetHiddenState(false) and SetOpacity after Show() all fail to hand it over. webOS 3 has no Activate(), which is what does it on webOS 4. Committed as work-in-progress with that said plainly in the README rather than implied by a sample that looks finished. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- CMakeLists.txt | 1 + README.md | 2 + assets/icons/web-cbe-webos3.png | Bin 0 -> 2582 bytes assets/make-icon.py | 1 + web/cbe-webos3/CMakeLists.txt | 32 ++++ web/cbe-webos3/README.md | 77 +++++++++ web/cbe-webos3/main.cpp | 167 +++++++++++++++++++ web/libcbe/CMakeLists.txt | 18 ++ web/libcbe/webos3/cbe_stub.cpp | 59 +++++++ web/libcbe/webos3/webos/webapp_window_base.h | 63 +++++++ web/libcbe/webos3/webos/webview_base.h | 113 +++++++++++++ 11 files changed, 533 insertions(+) create mode 100644 assets/icons/web-cbe-webos3.png create mode 100644 web/cbe-webos3/CMakeLists.txt create mode 100644 web/cbe-webos3/README.md create mode 100644 web/cbe-webos3/main.cpp create mode 100644 web/libcbe/webos3/cbe_stub.cpp create mode 100644 web/libcbe/webos3/webos/webapp_window_base.h create mode 100644 web/libcbe/webos3/webos/webview_base.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 057da53..965ad48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,5 +35,6 @@ if (TARGET_WEBOS) add_subdirectory(media/lgnc) add_subdirectory(web/libcbe) add_subdirectory(web/cbe) + add_subdirectory(web/cbe-webos3) add_subdirectory(web/hybrid) endif () diff --git a/README.md b/README.md index 4e47904..afbe824 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ media/ web/ libcbe/ reconstructed libcbe headers and the link stub, shared by both cbe/ the smallest thing that puts a web page on screen + cbe-webos3/ the same, against webOS 3's older and different libcbe hybrid/ a native sign-in flow: SDL2 + Nuklear, a web login page, one process ``` @@ -199,6 +200,7 @@ hard to diagnose from the TV side. | `media/ndl/directmedia` (v2) | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): 300 video + 469 PCM chunks on both | | `media/ndl/directmedia` (v1) | 3.5 - 4.x | built and symbol-verified, needs a 2017-2019 set to test | | `web/hybrid` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the sign-in flow runs end to end - native panel, web login form, and back with the username read out of the intercepted redirect URL. The OK key is verified too, injected with `com.webos.service.networkinput/test/sendKeyCode`; leaving the web view by remote is not, since keys go to whichever window is up | +| `web/cbe-webos3` | webOS 3 | **partly working** - 43UH6100, starfish 3.4.0: the reconstructed webOS 3 ABI is right and the page loads and paints, with every delegate callback firing, but the window never reaches the screen - LSM keeps the previous app foreground. webOS 3 has no `Activate()` and the equivalent has not been found | | `web/cbe` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | | `media/smp/webos1` | 1.x | not written yet - and there is no webOS 1 hardware here to validate it against, so it would ship untestable | diff --git a/assets/icons/web-cbe-webos3.png b/assets/icons/web-cbe-webos3.png new file mode 100644 index 0000000000000000000000000000000000000000..a15f1c8f4168d233cf1f88ead876eb808188f5b8 GIT binary patch literal 2582 zcmV+x3hDKUP);slr+%~Vq>JORT52&i7{=H_KP8= zHY(Z=Dm77PV
  • RazBEeKe)=5NHk1i%_JM3i1jUxc8o)A7%}E?&h2eg?siroO}Ms z&fc?T&FnpEubDNoX6-FC(L@uMF|4Q?0hZhkGov5AKB&ffzH+fFU>8zHT>@wUN>%b& zt2MkzJr~Sh6l(S``L_;SC25SLR!K+9?BGDvJ(#G80MiBn_elDzq}h^2OKOpHOww*i zzmfE$q(k1-ektkwl6w8w%94=OCFw0mFG^ZrW{09CRw!5UauGcWFYQS)TMo3CS#K@c zJs<$gkE?-ooF9vlvj4aM{++-?zenjEl>3hJwhm-_2edMWDQLlJNw-QmEoqpfS0t^G z^rocolIBVJq@?zwY?p%elqGFQq&p;?k#xPJFPhmSE{t-n+(AjNdN~xdvPIHX zzeink{fdZh7x;p|^)5fJpn@R*<^gk(^u|PEev;#jTGj*nB6q*E+)r!T$Q%Y*1LN59 zdA}YeX}_c|nOPZVQ>FoBGy9uV)hL{OHekG@kzTe%(i@&N=oWdv1n5F26Rhn_g>EcK z>&)yF&}wGgxiTeH?owq*3xRbCLYgUQveP$9`X0fEsrx+;a{HYGFir z6dD_a(%uhmHJHLQXM_Tue;sPXOCL6d%!C732lp+otE@hC-+EN0L(G7 z?zAChRyMQVYTaAXFv9X}gr|S+XC$>rnp&k?kJo+1&u5QIZ{@kD$y<^1grCa_U3d*J zU%`nG$G!LX6=mI{Ll-t=4Z^U}wy4-y(q}+zI%s`=*-k6fl3&$!f+2$!&(^iG$)@Dh+Gqcz5 zxfyV;q=^dMG&&QcFyA{UX{(uS^!ihWMZR2$IGx&QW=~T`o}BZd7TpDRRt$8#7mJ!d zh&MJ0G8%#Co$g~DUEuvaQ$=ff0p+C3C^gYU6HP?a&0w5I-cZ!5<*X|K`oy@T3Il$U zSF>f=#h@+(=)$p7XigjH$&;xskz_qx=z~_kpbL;>mkgLDX_2IlN}8_BVX$9Pr=-6~ zT4QG0@@0o4lBL)b;71jfU$6O*$FuleN-ZYqmj%c+gzf_V=Ysho@QhzCPk5I*Z3XZv zzwQC<^ZcPD#6);sabcVwY`_cHOmIN7CH!qp+v4dv$via6(mER+Nw>Yz1@Hzi*J%r# z_MoR*6JbB-c?&%~ncyA+_;Olz!-C+EHeeFL-**I&IYCo{sX_l0WdnUV`rxd$5aI4p~-qW@6vJWoEyvI}B z07SoD6vSi~#7a-68|$$fuXLWto^JQ^YNy}q>B6+I$hQ^mb+>|hYq@Jyq)+qn_I~P! zd675z2EXs=#gTsLk9%P#(sISM>;k|3gWNCZ+Bwnl4|qD=ij3C>JUubVGf6Ki><7o~ zfnEyoJQt#Y-k4r*^jt}M%xu4JH=gn(**ItKtgtP4#(6rOXPnbd`}w4#jgqDVql6dmf`D?N!jgt;#&pNV-na6iHho9j{W=NJ%$H+UNDe=Pyf|A?aO`W|-Mdd{3qp z?MYGeS+DTo7P?Ux=2MXvwI5d@ZDjG*ZO${rc@8=~ z#?O36Vf@?RFQc_6LsFywc>G~htn!8<#sqv>SUNph=tf^!vMiKkOdi~M(adL-F*D37Ie zH#`!dEOP<;3s~T^*U3CGEs4N)1AF}X#zgaemhcYx9#3IN1hbS3Pm#I09!hv0a@r4p z?v_T&$z==VC9Bd8UV^@FLQ23-}1|1A;|8lnCqyAyRb(aC@b) zgN3?ybNB~0xzA(ANb2z9vHa(;4D~(XN^nC^b$OW@m3l#4XkaCf`$d3MzX3)~G|@y8 zLlt#vM5@83Jb42{5&Itvjlo)xTvi(BbHphMftTYHzS=x0>7Qn{tDmBc0Wuf8osXW< z$h9XGSZ zzFKL#WggJs$EY0rDyO$6wDyGmN~cW+rsjAaBZR$m1D%P7=dv1}`URN_{yE?f3m1Z94Et8n*hNnJTteGMwt=)cabvm$A&LU`OZrwxk2?JhTW8md8hpPGoH80)2Chg zo^`qbqkzREzb|q664$_PFY|8CYpe(bGV6K;VHy+B4ZPQd5j8UYHt&I?{8*=*0A42S z;%%ibo_5~G5?(~2g{#P{@z$h!FD9E5cLTGWZiGmLja0@V!slQkD0m)N=s-&r3wX7% znCRnXcEIzmPzd!kh2KWKp8m#|uu{2R!I{t@saw*2B<(P>w~K5`!+}8x?HCkOPnDl? s=ji8rP@UxG9jfK#j$IQ?G;tC5KXL+v=CJ-oKmY&$07*qoM6N<$f|y10F8}}l literal 0 HcmV?d00001 diff --git a/assets/make-icon.py b/assets/make-icon.py index b20d0b3..e5c6e61 100644 --- a/assets/make-icon.py +++ b/assets/make-icon.py @@ -32,6 +32,7 @@ "ndl-directmedia-v2": ("NDL", "DM2"), "lgnc": ("LGNC", "1-4"), "web-cbe": ("CBE", "w4", "globe"), + "web-cbe-webos3": ("CBE", "w3", "globe"), "web-hybrid": ("SDL+", "w4", "globe"), } diff --git a/web/cbe-webos3/CMakeLists.txt b/web/cbe-webos3/CMakeLists.txt new file mode 100644 index 0000000..5a9cd66 --- /dev/null +++ b/web/cbe-webos3/CMakeLists.txt @@ -0,0 +1,32 @@ +# web/cbe, built against webOS 3's libcbe. +# +# Not a flag flip. Between webOS 3 and 4 the library changed its string ABI *and* +# its API: WebViewBase takes its size in the constructor with no Initialize(), +# WebAppWindowBase has no InitWindow(), and the delegate has a different slot +# order with LoadStarted carrying a URL where webOS 4 grew a separate +# DidStartNavigation. So this is a sibling sample rather than a build variant, +# the way media/smp/acb and media/smp/webos5 are siblings. +# +# Verified against a 43UH6100 running starfish 3.4.0. + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GLIB REQUIRED glib-2.0) + +add_executable(web-cbe-webos3 main.cpp) +target_include_directories(web-cbe-webos3 SYSTEM PRIVATE ${GLIB_INCLUDE_DIRS}) +target_link_directories(web-cbe-webos3 PRIVATE ${GLIB_LIBRARY_DIRS}) +# cbe-stub-webos3 carries the include path, -fno-rtti and the old string ABI. +target_link_libraries(web-cbe-webos3 PRIVATE cbe-stub-webos3 ${GLIB_LIBRARIES}) +target_compile_definitions(web-cbe-webos3 PRIVATE APP_LOG_NAME="web-cbe-webos3") + +set_target_properties(web-cbe-webos3 PROPERTIES + SKIP_BUILD_RPATH TRUE + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH "") + +webos_add_ipk( + TARGET web-cbe-webos3 + APPID org.webosbrew.sample.web.cbe3 + TITLE "CBE WebView (webOS 3)" + COLOR "#26A69A" + WEBOS_VERSIONS ">=3.4, <4") diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md new file mode 100644 index 0000000..a79c926 --- /dev/null +++ b/web/cbe-webos3/README.md @@ -0,0 +1,77 @@ +# web/cbe, on webOS 3 + +The same twenty lines of "make a window, make a web view, load a URL" as `web/cbe`, against +the libcbe that shipped on webOS 3. Everything that differs is the library moving underneath +it, and there is more of that than the version numbers suggest. + +Measured on a 43UH6100 running **starfish 3.4.0**. + +## The API is different, not just the ABI + +| | webOS 3 | webOS 4 | +|---|---|---| +| `std::string` | pre-C++11 (`RKSs`) | `__cxx11` | +| construction | `WebViewBase(int w, int h)` | `WebViewBase()` then `Initialize(...)` | +| window size | no `InitWindow()`; `Resize()` | `InitWindow(w, h)` | +| delegate slots | 24, own order | 24, different order | +| navigation start | `LoadStarted(url)` | `DidStartNavigation(url, bool)` | +| browser control | slots 18/19 | slots 19/20 | +| absent here | `Initialize`, `UpdatePreferences`, `SetTrustLevel`, `Activate` | - | + +The delegate came out of webOS 3's own `libWebAppMgr.so`, the same way the webOS 4 one did: +`BlinkWebView`'s vtable is the only authority for slot order. `DidFirstNonBlankPaint` at slot +2 exists only on this generation, and `LoadProgressChanged` takes a URL besides the progress. + +`-verify` earns its keep here: `SetTrustLevel` and `UpdatePreferences` were copied over from +the webOS 4 sample, build fine, and are simply absent on webOS 3. + +## Two things that stop it starting + +**`--ozone-platform=weboswayland`**, not `wayland`. The wrong name gets as far as +`DesktopFactoryWayland` and then aborts; the right one logs `Ozone: WebOSFactoryWayland`. + +**`CDM_LIB_PATH` must be set.** webOS 3's `WebOSMain` does + +```cpp +std::string cdm(getenv("CDM_LIB_PATH")); // no null check +cdm += "/libwidevinecdmadapter.so"; +``` + +so an unset variable aborts the process before any of your code runs, with + +``` +terminate called after throwing an instance of 'std::logic_error' + what(): basic_string::_S_construct null not valid +``` + +and nothing else to go on. WAM inherits the variable from its own environment; a plain +native app does not, so the sample sets it to `/usr/lib`. That was found by pulling the +64 MB library, resolving the crash address to `WebOSMain+0x21f8`, and reading the literal the +`getenv` call loads - there was no other way to see it. + +## State: loads and paints, but does not reach the screen + +Verified working: the process starts, the page loads, and every delegate callback fires with +the recovered signatures - + +``` +[cbe] title 'Example Domain' +[cbe] document loaded +[cbe] first non-blank paint +[cbe] progress 100% +[cbe] finished example.com/ +``` + +so the reconstructed ABI is right: the vtable slots line up, the old-ABI strings arrive +intact, and the web view is rendering. + +**What does not work is compositing.** LSM keeps the previous app foreground and the screen +shows the launch splash. `SetWindowProperty("appId", ...)`, `SetWindowHostState`, +`SetHiddenState(false)` and `SetOpacity(1.0f)` after `Show()` were all tried and none of them +hands the surface over. webOS 3 has no `Activate()`, which is what does it on webOS 4, and +the equivalent has not been found. Dropping `Resize()` - which WAM never calls on this +generation - makes it worse, not better: the delegate stops firing entirely and Wayland +starts reporting `proxy already has listener`. + +So this is honest work-in-progress rather than a finished sample. The hard half - the ABI - +is done and demonstrated. The window handover is not. diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp new file mode 100644 index 0000000..ec60d95 --- /dev/null +++ b/web/cbe-webos3/main.cpp @@ -0,0 +1,167 @@ +// The webOS 3 build of web/cbe: the smallest thing that puts a page on screen, +// against the older libcbe. +// +// The shape is identical to web/cbe/main.cpp - hand the process to WebOSMain, +// get called back on the browser UI thread through the default GMainContext, +// build a window and a web view - and every difference is the API moving under +// it between webOS 3 and 4: +// +// * WebViewBase takes its size in the constructor; there is no Initialize(). +// * The window has no InitWindow() either; Resize() gives it a size. +// * The delegate has its own slot order, and LoadStarted carries a URL where +// webOS 4 has a separate DidStartNavigation. +// * All of it is the pre-C++11 std::string ABI. + +#include + +#include +#include +#include +#include +#include +#include + +#include "webos/webapp_window_base.h" +#include "webos/webview_base.h" + +extern "C" int WebOSMain(int argc, const char** argv); + +namespace { + +const char kAppId[] = "org.webosbrew.sample.web.cbe3"; +const char kUrl[] = "https://example.com/"; + +class SampleWebView : public webos::WebViewBase { + public: + SampleWebView(int w, int h) : webos::WebViewBase(w, h) {} + + void LoadProgressChanged(double progress, const std::string&) override { + printf("[cbe] progress %3.0f%%\n", progress * 100); + } + void DidFirstFrameFocused() override {} + void DidFirstNonBlankPaint() override { puts("[cbe] first non-blank paint"); } + void LoadVisuallyCommitted() override { puts("[cbe] visually committed"); } + void TitleChanged(const std::string& title) override { + printf("[cbe] title '%s'\n", title.c_str()); + } + void NavigationHistoryChanged() override {} + void Close() override { puts("[cbe] close requested"); } + bool DecidePolicyForResponse(bool, int status, const std::string& url, + const std::string&) override { + printf("[cbe] response %d %s\n", status, url.c_str()); + return false; + } + // Where a redirect would be caught on this generation. + void LoadStarted(const std::string& url) override { + printf("[cbe] load started %s\n", url.c_str()); + } + void LoadFinished(const std::string& url) override { + printf("[cbe] finished %s\n", url.c_str()); + } + void LoadFailed(const std::string& url, int code, const std::string& desc) override { + printf("[cbe] FAILED %s (%d %s)\n", url.c_str(), code, desc.c_str()); + } + void LoadStopped(const std::string&) override { puts("[cbe] load stopped"); } + void RenderProcessCreated(int pid) override { printf("[cbe] renderer pid %d\n", pid); } + void RenderProcessGone() override { puts("[cbe] renderer gone"); } + void DocumentLoadFinished() override { puts("[cbe] document loaded"); } +}; + +class SampleWindow : public webos::WebAppWindowBase { + public: + bool event(WebOSEvent*) override { return false; } +}; + +SampleWindow* g_window; +SampleWebView* g_webview; +std::string g_app_path; + +gboolean CreateWebApp(gpointer) { + g_window = new SampleWindow(); + // No InitWindow on webOS 3. WAM never calls Resize either, but dropping it + // here stops the delegate firing at all and the Wayland connection starts + // complaining "proxy already has listener", so it is doing something the + // constructor alone does not. + g_window->Resize(1920, 1080); + g_window->SetWindowProperty("appId", kAppId); + g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); + + g_webview = new SampleWebView(1920, 1080); // no Initialize either + // Neither SetTrustLevel nor UpdatePreferences exists on webOS 3 - `-verify` + // reports them undefined against a 3.4 dump, which is the cheapest way to find + // out that a call you copied from the webOS 4 sample is not portable. + g_webview->SetAppId(kAppId); + g_webview->SetAllowLocalResourceLoad(true); + g_webview->SetLocalStorageEnabled(true); + g_webview->SetVisible(true); + + g_window->AttachWebContents(g_webview->GetWebContents()); + g_window->Show(); + // webOS 3 has no Activate(). SetHiddenState(false) and re-asserting the appId + // after Show() are the nearest equivalents worth trying. + g_window->SetHiddenState(false); + g_window->SetOpacity(1.0f); + g_window->SetWindowProperty("appId", kAppId); + g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); + + printf("[cbe] loading %s\n", kUrl); + g_webview->LoadUrl(kUrl); + return G_SOURCE_REMOVE; +} + +bool IsBrowserProcess(int argc, char** argv) { + for (int i = 1; i < argc; ++i) + if (strncmp(argv[i], "--type=", 7) == 0) return false; + return true; +} + +} // namespace + +int main(int argc, char** argv) { + if (IsBrowserProcess(argc, argv)) { + freopen("/tmp/" APP_LOG_NAME ".log", "w", stdout); + dup2(1, 2); + } + setvbuf(stdout, NULL, _IOLBF, 0); + + const char* slash = strrchr(argv[0], '/'); + g_app_path = slash ? std::string(argv[0], slash - argv[0]) : std::string("."); + const bool browser = IsBrowserProcess(argc, argv); + + std::vector args; + args.push_back(argv[0]); + if (browser) { + // "weboswayland", not "wayland" - webOS 3's Ozone registers it under that + // name, and the wrong one gets as far as constructing a std::string from a + // null and aborting with basic_string::_S_construct. Taken from WAM's own + // WAM_SWITCHES on the device. + args.push_back("--ozone-platform=weboswayland"); + args.push_back("--no-sandbox"); + args.push_back("--no-zygote"); + args.push_back("--in-process-gpu"); + args.push_back(std::string("--browser-subprocess-path=") + argv[0]); + args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); + // Borrowed from WAM's own WAM_SWITCHES on this generation, pending bisection. + args.push_back("--webos-wam"); + args.push_back("--noerrdialogs"); + args.push_back("--disable-extensions"); + args.push_back("--touch-events=disabled"); + args.push_back("--num-raster-threads=2"); + args.push_back("--enable-threaded-compositing"); + } + for (int i = 1; i < argc; ++i) args.push_back(argv[i]); + + std::vector cargv; + for (size_t i = 0; i < args.size(); ++i) cargv.push_back(args[i].c_str()); + + if (!getenv("XDG_RUNTIME_DIR")) setenv("XDG_RUNTIME_DIR", "/tmp/xdg", 1); + // webOS 3's WebOSMain does std::string(getenv("CDM_LIB_PATH")) with no null + // check and appends "/libwidevinecdmadapter.so" to it, so an unset variable + // aborts the process before anything of ours runs - + // "basic_string::_S_construct null not valid", thrown from inside WebOSMain. + // WAM gets it from its own environment; a plain native app does not. + if (!getenv("CDM_LIB_PATH")) setenv("CDM_LIB_PATH", "/usr/lib", 1); + if (browser) g_idle_add(CreateWebApp, NULL); + + return WebOSMain(static_cast(cargv.size()), cargv.data()); +} diff --git a/web/libcbe/CMakeLists.txt b/web/libcbe/CMakeLists.txt index 80c85d2..a937124 100644 --- a/web/libcbe/CMakeLists.txt +++ b/web/libcbe/CMakeLists.txt @@ -18,3 +18,21 @@ target_include_directories(cbe-stub PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") # Chromium is built without RTTI and exports no typeinfo for these classes, so # anything deriving from them has to agree - hence PUBLIC, not PRIVATE. target_compile_options(cbe-stub PUBLIC -fno-rtti) + +# The same again for webOS 3, where libcbe is a different API and not merely a +# different string ABI: no Initialize(), the constructor takes the size, and the +# delegate has its own slot order. See webos3/webos/webview_base.h. +# +# _GLIBCXX_USE_CXX11_ABI=0 is PUBLIC because it has to reach anything that links +# this - it is what makes std::string mangle the way that generation's libcbe +# expects, and getting it wrong produces a binary that links and then finds +# nothing on the TV. +add_library(cbe-stub-webos3 SHARED webos3/cbe_stub.cpp) +set_target_properties(cbe-stub-webos3 PROPERTIES OUTPUT_NAME cbe) +target_include_directories(cbe-stub-webos3 PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/webos3") +target_compile_options(cbe-stub-webos3 PUBLIC -fno-rtti) +target_compile_definitions(cbe-stub-webos3 PUBLIC _GLIBCXX_USE_CXX11_ABI=0) + +# Both stubs are named libcbe.so, so they need separate build directories. +set_target_properties(cbe-stub-webos3 PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/webos3") diff --git a/web/libcbe/webos3/cbe_stub.cpp b/web/libcbe/webos3/cbe_stub.cpp new file mode 100644 index 0000000..cc702f2 --- /dev/null +++ b/web/libcbe/webos3/cbe_stub.cpp @@ -0,0 +1,59 @@ +// Link-time stand-in for webOS 3's /usr/lib/libcbe.so. Same idea as the webOS 4 +// stub next door, compiled against the webOS 3 headers and with the pre-C++11 +// std::string ABI, so the mangled names come out matching that generation. +#include "webos/webapp_window_base.h" +#include "webos/webview_base.h" + +#define STUB(...) \ + { return __VA_ARGS__; } + +extern "C" int WebOSMain(int, const char**) STUB(0) + +namespace webos { + +WebViewBase::WebViewBase(int, int) : webview_(0) STUB() +WebViewBase::~WebViewBase() STUB() + +void* WebViewBase::GetWebContents() STUB(0) +void WebViewBase::LoadUrl(const std::string&) STUB() +void WebViewBase::StopLoading() STUB() +void WebViewBase::Reload() STUB() +std::string WebViewBase::GetUrl() STUB(std::string()) +std::string WebViewBase::DocumentTitle() const STUB(std::string()) +void WebViewBase::RunJavaScript(const std::string&) STUB() +void WebViewBase::LoadExtension(const std::string&) STUB() +void WebViewBase::ClearExtensions() STUB() +void WebViewBase::SetAppId(const std::string&) STUB() +void WebViewBase::SetUserAgent(const std::string&) STUB() +void WebViewBase::SetVisible(bool) STUB() +void WebViewBase::SetViewportSize(int, int) STUB() +void WebViewBase::SetTransparentBackground(bool) STUB() +void WebViewBase::SetAllowLocalResourceLoad(bool) STUB() +void WebViewBase::SetAllowUniversalAccessFromFileUrls(bool) STUB() +void WebViewBase::SetFileAccessBlocked(bool) STUB() +void WebViewBase::SetLocalStorageEnabled(bool) STUB() +void WebViewBase::SetShouldSuppressDialogs(bool) STUB() +void WebViewBase::SetVisibilityState(WebPageVisibilityState) STUB() + +WebAppWindowBase::WebAppWindowBase() : webapp_window_(0) STUB() +WebAppWindowBase::~WebAppWindowBase() STUB() +void WebAppWindowBase::Show() STUB() +void WebAppWindowBase::Hide() STUB() +void WebAppWindowBase::SetCustomCursor(CustomCursorType, const std::string&, int, int) STUB() +void WebAppWindowBase::SetHiddenState(bool) STUB() +void WebAppWindowBase::FirstFrameVisuallyCommitted() STUB() +void* WebAppWindowBase::GetNativeWindow() STUB(0) +void WebAppWindowBase::AttachWebContents(void*) STUB() +void WebAppWindowBase::DetachWebContents() STUB() +void WebAppWindowBase::RecreatedWebContents() STUB() +void WebAppWindowBase::Resize(int, int) STUB() +void WebAppWindowBase::SetOpacity(float) STUB() +void WebAppWindowBase::SetWindowHostState(NativeWindowState) STUB() +NativeWindowState WebAppWindowBase::GetWindowHostState() const STUB(NATIVE_WINDOW_DEFAULT) +void WebAppWindowBase::SetWindowProperty(const std::string&, const std::string&) STUB() +void WebAppWindowBase::SetUseVirtualKeyboard(bool) STUB() +void WebAppWindowBase::SetKeyMask(WebOSKeyMask, bool) STUB() +int WebAppWindowBase::DisplayWidth() STUB(0) +int WebAppWindowBase::DisplayHeight() STUB(0) + +} // namespace webos diff --git a/web/libcbe/webos3/webos/webapp_window_base.h b/web/libcbe/webos3/webos/webapp_window_base.h new file mode 100644 index 0000000..4b18954 --- /dev/null +++ b/web/libcbe/webos3/webos/webapp_window_base.h @@ -0,0 +1,63 @@ +// webos::WebAppWindowBase on webOS 3. Thirteen virtual slots against webOS 4's +// ten, and no InitWindow() - the window exists once the object does, and Resize +// gives it a size. +#pragma once + +#include + +class WebOSEvent; + +namespace webos { + +enum NativeWindowState { + NATIVE_WINDOW_DEFAULT = 0, + NATIVE_WINDOW_MINIMIZED, + NATIVE_WINDOW_MAXIMIZED, + NATIVE_WINDOW_FULLSCREEN, +}; + +enum CustomCursorType { CUSTOM_CURSOR_NOT_USE, CUSTOM_CURSOR_BLANK, CUSTOM_CURSOR_PATH }; + +typedef unsigned WebOSKeyMask; + +class WebAppWindowDelegate { + public: + virtual ~WebAppWindowDelegate() {} + virtual bool event(WebOSEvent*) { return false; } + virtual unsigned CheckKeyFilterTable(unsigned, unsigned*) { return 0; } +}; + +class WebAppWindowBase : public WebAppWindowDelegate { + public: + WebAppWindowBase(); + ~WebAppWindowBase() override; + + // Slots 4 to 12, in this order. SetHiddenState, FirstFrameVisuallyCommitted + // and GetNativeWindow sit in the middle of them on this generation, where + // webOS 4 has nothing. + virtual void Show(); + virtual void Hide(); + virtual void SetCustomCursor(CustomCursorType type, const std::string& path, + int hotspot_x, int hotspot_y); + virtual void SetHiddenState(bool hidden); + virtual void FirstFrameVisuallyCommitted(); + virtual void* GetNativeWindow(); + virtual void AttachWebContents(void* web_contents); + virtual void DetachWebContents(); + virtual void RecreatedWebContents(); + + void Resize(int width, int height); + void SetOpacity(float opacity); + void SetWindowHostState(NativeWindowState state); + NativeWindowState GetWindowHostState() const; + void SetWindowProperty(const std::string& name, const std::string& value); + void SetUseVirtualKeyboard(bool enable); + void SetKeyMask(WebOSKeyMask key_mask, bool set); + int DisplayWidth(); + int DisplayHeight(); + + private: + void* webapp_window_; // WebAppWindow*, owned by libcbe +}; + +} // namespace webos diff --git a/web/libcbe/webos3/webos/webview_base.h b/web/libcbe/webos3/webos/webview_base.h new file mode 100644 index 0000000..baa0639 --- /dev/null +++ b/web/libcbe/webos3/webos/webview_base.h @@ -0,0 +1,113 @@ +// webos::WebViewBase as it exists on webOS 3 - a different API from the one in +// ../../webos/webview_base.h, not just a different string ABI. +// +// Recovered the same way: the firmware symbol tables for the names, and the +// vtable of BlinkWebView inside webOS 3's own libWebAppMgr.so for the slot +// order. Measured against a 43UH6100 running starfish 3.4.0. +// +// What moved between webOS 3 and 4: +// +// * the whole library is the pre-C++11 std::string ABI, so everything here +// must be compiled -D_GLIBCXX_USE_CXX11_ABI=0; +// * there is no Initialize() at all - the constructor takes the dimensions; +// * DidFirstNonBlankPaint exists and DidStartNavigation does not, so a +// redirect has to be caught in LoadStarted, which carries the URL here; +// * LoadStarted and LoadStopped take a URL, and LoadProgressChanged takes one +// besides the progress; +// * the browser-control pair sits at 18/19 rather than 19/20. +#pragma once + +#include +#include + +class WebOSEvent; + +namespace webos { + +class WebViewProfile; + +// 24 slots. Names for 0-14 and 18-19 come from BlinkWebView's vtable; the rest +// are stubs it fills with empty bodies, and must be present or libcbe indexes +// past the end of ours. +class WebViewDelegate { + public: + virtual void LoadProgressChanged(double progress, const std::string& url) = 0; + virtual void DidFirstFrameFocused() = 0; + virtual void DidFirstNonBlankPaint() = 0; + virtual void LoadVisuallyCommitted() = 0; + virtual void TitleChanged(const std::string& title) = 0; + virtual void NavigationHistoryChanged() = 0; + virtual void Close() = 0; + virtual bool DecidePolicyForResponse(bool is_main_frame, + int status_code, + const std::string& url, + const std::string& status_text) = 0; + // Slot 8. The nearest thing webOS 3 has to DidStartNavigation, and the hook a + // redirect-catching login flow has to use here. + virtual void LoadStarted(const std::string& url) = 0; + virtual void LoadFinished(const std::string& url) = 0; + virtual void LoadFailed(const std::string& url, + int err_code, + const std::string& err_desc) = 0; + virtual void LoadStopped(const std::string& url) = 0; + virtual void RenderProcessCreated(int pid) = 0; + virtual void RenderProcessGone() = 0; + virtual void DocumentLoadFinished() = 0; + virtual void Unknown15() {} + virtual void Unknown16() {} + virtual void Unknown17() {} + virtual void HandleBrowserControlCommand( + const std::string& command, const std::vector& arguments) { + (void)command; + (void)arguments; + } + virtual void HandleBrowserControlFunction( + const std::string& command, + const std::vector& arguments, + std::string* result) { + (void)command; + (void)arguments; + (void)result; + } + virtual void Unknown20() {} + virtual void Unknown21() {} + virtual void Unknown22() {} + virtual void Unknown23() {} +}; + +class WebViewBase : public WebViewDelegate { + public: + enum FontRenderParams { HINTING_NONE, HINTING_SLIGHT, HINTING_MEDIUM, HINTING_FULL }; + enum WebPageVisibilityState { VISIBILITY_VISIBLE, VISIBILITY_HIDDEN, VISIBILITY_LAUNCHING }; + + // No Initialize() on this generation: the size goes in here. + WebViewBase(int width, int height); + ~WebViewBase(); + + void* GetWebContents(); + void LoadUrl(const std::string& url); + void StopLoading(); + void Reload(); + std::string GetUrl(); + std::string DocumentTitle() const; + void RunJavaScript(const std::string& js); + void LoadExtension(const std::string& name); + void ClearExtensions(); + + void SetAppId(const std::string& app_id); + void SetUserAgent(const std::string& user_agent); + void SetVisible(bool visible); + void SetViewportSize(int width, int height); + void SetTransparentBackground(bool transparent); + void SetAllowLocalResourceLoad(bool allow); + void SetAllowUniversalAccessFromFileUrls(bool allow); + void SetFileAccessBlocked(bool blocked); + void SetLocalStorageEnabled(bool enabled); + void SetShouldSuppressDialogs(bool suppress); + void SetVisibilityState(WebPageVisibilityState state); + + private: + void* webview_; // WebView*, owned by libcbe +}; + +} // namespace webos From 158fadee3709fd81ab1f613138020e2fadf2d55b Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 00:24:54 +0900 Subject: [PATCH 20/40] web/cbe-webos3: chase the window handover, and write down the dead ends Reverse-engineered webOS 3's WAM and libcbe looking for what makes a window reach the screen. Did not find it. Recording the search so it is not repeated. The one real gain is that appinfo needs "noSplashOnLaunch": true. Without it SAM's launch splash sits over everything indefinitely, which made a missing window look like a compositing bug with a picture on top. With it, the screen shows the TV's own no-signal wallpaper - so the surface genuinely is not there. Added to the shared appinfo template rather than only this sample, since a sample that draws immediately never wants the splash. Ruled out, all measured: SetWindowProperty("appId") (the only property WAM sets besides the key-access ones), SetWindowHostState before and after Show(), SetHiddenState(false), SetOpacity(1.0f), and the --app-id switch that exists in libcbe. webos::Platform::Get() returns nil - that singleton belongs to the browser application and is built by ChromeMain, not WebOSMain. Dropping Resize(), which WAM never calls on this generation, makes things worse rather than better: the delegate stops firing and Wayland reports "proxy already has listener". Also confirmed WAM is not doing anything the sample omits: WebAppWaylandWindow::show() is onStageActivated() - pure WAM bookkeeping, no libcbe calls - followed by WebAppWindowBase::Show(). The live lead is registration. libcbe carries palm://com.webos.applicationManager/registerNativeApp and a webos::LunaServices whose Initialize takes a base::FilePath and needs a LunaServices(Platform*), so that path may live on the browser's side of the library rather than the embedder's. Hypothesis, not a finding, and labelled as such. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- cmake/appinfo.json.in | 1 + web/cbe-webos3/README.md | 42 ++++++++++++++++++++------ web/cbe-webos3/main.cpp | 1 + web/libcbe/webos3/webos/webview_base.h | 1 + 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/cmake/appinfo.json.in b/cmake/appinfo.json.in index 41a686d..e8b0145 100644 --- a/cmake/appinfo.json.in +++ b/cmake/appinfo.json.in @@ -7,5 +7,6 @@ "title": "@IPK_TITLE@", "icon": "icon.png", "iconColor": "@IPK_COLOR@", + "noSplashOnLaunch": true, "bgColor": "@IPK_COLOR@" } diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md index a79c926..bb74c4a 100644 --- a/web/cbe-webos3/README.md +++ b/web/cbe-webos3/README.md @@ -65,13 +65,35 @@ the recovered signatures - so the reconstructed ABI is right: the vtable slots line up, the old-ABI strings arrive intact, and the web view is rendering. -**What does not work is compositing.** LSM keeps the previous app foreground and the screen -shows the launch splash. `SetWindowProperty("appId", ...)`, `SetWindowHostState`, -`SetHiddenState(false)` and `SetOpacity(1.0f)` after `Show()` were all tried and none of them -hands the surface over. webOS 3 has no `Activate()`, which is what does it on webOS 4, and -the equivalent has not been found. Dropping `Resize()` - which WAM never calls on this -generation - makes it worse, not better: the delegate stops firing entirely and Wayland -starts reporting `proxy already has listener`. - -So this is honest work-in-progress rather than a finished sample. The hard half - the ABI - -is done and demonstrated. The window handover is not. +**What does not work is compositing.** LSM keeps the previous app foreground, and with the +splash disabled the screen shows the TV's own no-signal wallpaper - so the surface is not +merely behind something, it is not there. + +The search so far, all of it negative, and worth writing down so it is not repeated: + +| tried | result | +|---|---| +| `SetWindowProperty("appId", ...)` | no effect - and it is the only property WAM sets besides the key-access ones | +| `SetWindowHostState(FULLSCREEN)`, before and after `Show()` | no effect | +| `SetHiddenState(false)`, `SetOpacity(1.0f)` after `Show()` | no effect | +| `--app-id=` (the switch exists in libcbe) | no effect | +| `webos::Platform::Get()` then `SetFullscreen(true)` | returns **nil** - that singleton is not constructed in a plain embedder | +| dropping `Resize()`, which WAM never calls here | **worse**: the delegate stops firing and Wayland reports `proxy already has listener` | + +Two things were learned rather than guessed. `noSplashOnLaunch` in `appinfo.json` matters: +without it SAM's launch splash covers the screen indefinitely and hides what is really +happening, which is what made this look like a compositing bug with a picture on top of it. +And `WebAppWaylandWindow::show()` in webOS 3's WAM turns out to be nothing but +`onStageActivated()` - pure WAM bookkeeping, no libcbe calls - followed by +`WebAppWindowBase::Show()`, so WAM is not doing anything special that the sample omits. + +The live lead is registration. libcbe contains +`palm://com.webos.applicationManager/registerNativeApp` and a `webos::LunaServices` class +whose `Initialize(const base::FilePath&)` is an instance method needing a +`webos::LunaServices(webos::Platform*)` - and `Platform` is the browser application's layer, +built by `ChromeMain` rather than by `WebOSMain`. So on this generation the Luna +registration that a native app needs may simply live on the browser's side of the library +and not the embedder's. That is a hypothesis, not a finding. + +So this is honest work-in-progress. The hard half - the ABI - is done and demonstrated. The +window handover is not, and the next person should start at `LunaServices`. diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp index ec60d95..d0e511e 100644 --- a/web/cbe-webos3/main.cpp +++ b/web/cbe-webos3/main.cpp @@ -142,6 +142,7 @@ int main(int argc, char** argv) { args.push_back(std::string("--browser-subprocess-path=") + argv[0]); args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); // Borrowed from WAM's own WAM_SWITCHES on this generation, pending bisection. + args.push_back(std::string("--app-id=") + kAppId); args.push_back("--webos-wam"); args.push_back("--noerrdialogs"); args.push_back("--disable-extensions"); diff --git a/web/libcbe/webos3/webos/webview_base.h b/web/libcbe/webos3/webos/webview_base.h index baa0639..2e5fcd7 100644 --- a/web/libcbe/webos3/webos/webview_base.h +++ b/web/libcbe/webos3/webos/webview_base.h @@ -26,6 +26,7 @@ namespace webos { class WebViewProfile; + // 24 slots. Names for 0-14 and 18-19 come from BlinkWebView's vtable; the rest // are stubs it fills with empty bodies, and must be present or libcbe indexes // past the end of ours. From f28530f385066b3de50047d6c38f77ae7cfbc31f Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 08:24:25 +0900 Subject: [PATCH 21/40] web/cbe-webos3: narrow the window failure to a state the compositor ignores More RE, still not solved, but the failure is much better characterised and several more doors are closed. The sharpest symptom is now measured rather than inferred: the window object is real - non-null native handle, correct 1920x1080 panel - but GetWindowHostState stays 0 through SetWindowHostState(NATIVE_WINDOW_FULLSCREEN) and through Show(). The compositor never acknowledges the state. So the surface exists and is being drawn into, and LSM simply does not treat it as an app window, which is a better description than "the window does not appear". Neither the enum nor the sequence is wrong. WAM passes literal 3 for fullscreen, matching this header, and WebAppWayland::raise() - webOS 3's equivalent of webOS 4's Activate() - makes exactly one libcbe call, SetWindowHostState(3), which the sample already makes. Two switches turn out to be load-bearing: --webos-wam is required, and without it the process exits before writing a line of log. --app-id, which also exists in the library, changes nothing. The app does reach the Luna bus - ls-monitor shows two client-only connections owned by the executable - so libcbe's own LS2 client is running. That suggests a conclusion worth testing: both in-firmware users bring their own window management. WAM wraps WebOSMain in WebAppWayland, and the browser does not use WebOSMain at all - it uses ChromeMain, which is what builds webos::Platform and its Luna side. There may be no supported standalone embedder on this generation, with webOS 4 the release that fixed it. Written up as a hypothesis with its three pieces of evidence, not as a finding. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe-webos3/README.md | 46 ++++++++++++++++++++++++++++++++++++++-- web/cbe-webos3/main.cpp | 16 +++++++++----- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md index bb74c4a..9dd8e4a 100644 --- a/web/cbe-webos3/README.md +++ b/web/cbe-webos3/README.md @@ -87,7 +87,36 @@ And `WebAppWaylandWindow::show()` in webOS 3's WAM turns out to be nothing but `onStageActivated()` - pure WAM bookkeeping, no libcbe calls - followed by `WebAppWindowBase::Show()`, so WAM is not doing anything special that the sample omits. -The live lead is registration. libcbe contains +### What the window itself reports + +The clearest symptom, from the diagnostics the sample prints: + +``` +[diag] before Resize: display=1920x1080 native=0x690b0 state=0 +[diag] after Resize: display=1920x1080 native=0x690b0 state=0 +[diag] after Show: native=0x690b0 state=0 +``` + +The window object is real - a non-null native handle and the right panel size - but +`GetWindowHostState()` stays `0` (`NATIVE_WINDOW_DEFAULT`) through +`SetWindowHostState(NATIVE_WINDOW_FULLSCREEN)` and through `Show()`. The compositor never +acknowledges the state, which is a better description of the failure than "the window does +not appear": the surface exists and is being drawn into, and LSM is simply not treating it +as an app window. + +The enum is not the problem - WAM passes literal `3` for fullscreen, matching +`NATIVE_WINDOW_FULLSCREEN` here - and neither is the call sequence. Disassembling +`WebAppWayland::raise()`, webOS 3's equivalent of webOS 4's `Activate()`, shows it makes +exactly one libcbe call, `SetWindowHostState(3)`, which the sample already does. + +Two more switches turn out to be load-bearing rather than decorative: **`--webos-wam` is +required** - without it the process exits before writing a line of log - while `--app-id`, +which also exists in the library, changes nothing either way. + +The app does reach the Luna bus: `ls-monitor -l` shows two client-only connections owned by +the executable, without a service name. So libcbe's own LS2 client is running. + +### The live lead is registration. libcbe contains `palm://com.webos.applicationManager/registerNativeApp` and a `webos::LunaServices` class whose `Initialize(const base::FilePath&)` is an instance method needing a `webos::LunaServices(webos::Platform*)` - and `Platform` is the browser application's layer, @@ -95,5 +124,18 @@ built by `ChromeMain` rather than by `WebOSMain`. So on this generation the Luna registration that a native app needs may simply live on the browser's side of the library and not the embedder's. That is a hypothesis, not a finding. +### A conclusion worth considering + +Both in-firmware users of libcbe on webOS 3 bring their own window management: WAM wraps +`WebOSMain` in `WebAppWayland`, and the browser does not use `WebOSMain` at all - it uses +`ChromeMain`, which is what constructs `webos::Platform` and its Luna side. There may +therefore be no supported standalone-embedder path on this generation, and `WebOSMain` alone +may be expected to yield a rendering web view whose *window* somebody else owns. webOS 4, +where the same sample works unchanged, would then be the generation that fixed it. + +That is a hypothesis with three pieces of evidence behind it - `Platform::Get()` returning +nil, `ChromeMain` existing beside `WebOSMain`, and `--webos-wam` being mandatory - and it +should be tested rather than believed. + So this is honest work-in-progress. The hard half - the ABI - is done and demonstrated. The -window handover is not, and the next person should start at `LunaServices`. +window handover is not, and the next person should start at `LunaServices` and `ChromeMain`. diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp index d0e511e..c4debb5 100644 --- a/web/cbe-webos3/main.cpp +++ b/web/cbe-webos3/main.cpp @@ -82,7 +82,13 @@ gboolean CreateWebApp(gpointer) { // here stops the delegate firing at all and the Wayland connection starts // complaining "proxy already has listener", so it is doing something the // constructor alone does not. + printf("[diag] before Resize: display=%dx%d native=%p handle=%p state=%d\n", + g_window->DisplayWidth(), g_window->DisplayHeight(), + g_window->GetNativeWindow(), (void*)0, (int)g_window->GetWindowHostState()); g_window->Resize(1920, 1080); + printf("[diag] after Resize: display=%dx%d native=%p state=%d\n", + g_window->DisplayWidth(), g_window->DisplayHeight(), + g_window->GetNativeWindow(), (int)g_window->GetWindowHostState()); g_window->SetWindowProperty("appId", kAppId); g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); @@ -97,6 +103,8 @@ gboolean CreateWebApp(gpointer) { g_window->AttachWebContents(g_webview->GetWebContents()); g_window->Show(); + printf("[diag] after Show: native=%p state=%d\n", + g_window->GetNativeWindow(), (int)g_window->GetWindowHostState()); // webOS 3 has no Activate(). SetHiddenState(false) and re-asserting the appId // after Show() are the nearest equivalents worth trying. g_window->SetHiddenState(false); @@ -142,13 +150,11 @@ int main(int argc, char** argv) { args.push_back(std::string("--browser-subprocess-path=") + argv[0]); args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); // Borrowed from WAM's own WAM_SWITCHES on this generation, pending bisection. - args.push_back(std::string("--app-id=") + kAppId); + // Required, not decorative: without --webos-wam the process exits before + // writing a line of log. --app-id, which also exists in libcbe, turned out + // to make no difference either way. args.push_back("--webos-wam"); args.push_back("--noerrdialogs"); - args.push_back("--disable-extensions"); - args.push_back("--touch-events=disabled"); - args.push_back("--num-raster-threads=2"); - args.push_back("--enable-threaded-compositing"); } for (int i = 1; i < argc; ++i) args.push_back(argv[i]); From b08b575a28217d57632717f3be0f168682389b32 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 08:33:18 +0900 Subject: [PATCH 22/40] web/cbe-webos3: close two more doors on the window handover Following the browser: on webOS 3 there is no browser binary to follow. Scanning /usr/bin and every installed app turns up exactly one consumer of libcbe on the whole TV - /usr/bin/WebAppMgr. The browser there is a web app that WAM hosts, not a native binary, unlike webOS 4 where com.webos.app.browser/chrome links the library directly. So there is no standalone embedder anywhere in this firmware to copy, which is why none of this can be checked against something known to work. webOS 3's WebAppMgr binary is no help either: its undefined symbols are the same short list as webOS 4's, so at process level it does what this sample already does. webos::Runtime is a real find even though it did not fix anything. Unlike webos::Platform, its singleton is alive in a plain embedder - Runtime::Get() returns a valid pointer - and it carries SetWindowSize, InitializePlatform(const base::FilePath&) and Initialize(webos::PlatformDelegate*). The first two were called successfully and changed nothing; the third needs a delegate interface that has not been reconstructed, and is the remaining candidate on that path. base::FilePath turns out to be declarable, since libcbe exports its std::string constructor and destructor and its layout is that single member. The experiments are removed from main.cpp rather than left as dead code; what survives is one line printing the window's size, native handle and host state, because the host state reading back 0 however it is set is the sharpest statement of the problem. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe-webos3/README.md | 29 +++++++++++++++++++++++--- web/cbe-webos3/main.cpp | 12 +++++------ web/libcbe/webos3/cbe_stub.cpp | 9 ++++++++ web/libcbe/webos3/webos/webview_base.h | 29 ++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md index 9dd8e4a..5ed4613 100644 --- a/web/cbe-webos3/README.md +++ b/web/cbe-webos3/README.md @@ -116,6 +116,19 @@ which also exists in the library, changes nothing either way. The app does reach the Luna bus: `ls-monitor -l` shows two client-only connections owned by the executable, without a service name. So libcbe's own LS2 client is running. +### Only one binary on the whole TV links libcbe + +Scanning `/usr/bin` and every installed app on webOS 3 turns up exactly one consumer: +`/usr/bin/WebAppMgr`. The web browser there is a *web app* that WAM hosts, not a native +binary of its own - unlike webOS 4, where `com.webos.app.browser/chrome` links libcbe +directly. So on this generation there is no standalone embedder anywhere in the firmware to +copy, which is why none of this can be checked against a working example. + +webOS 3's `WebAppMgr` binary is also no help: its undefined symbols are the same short list +as webOS 4's - `WebOSMain`, `WebAppManagerServiceLuna::instance()`, +`WebAppManager::instance()`, `setPlatformModules` - so at process level it does what this +sample does. + ### The live lead is registration. libcbe contains `palm://com.webos.applicationManager/registerNativeApp` and a `webos::LunaServices` class whose `Initialize(const base::FilePath&)` is an instance method needing a @@ -133,9 +146,19 @@ therefore be no supported standalone-embedder path on this generation, and `WebO may be expected to yield a rendering web view whose *window* somebody else owns. webOS 4, where the same sample works unchanged, would then be the generation that fixed it. -That is a hypothesis with three pieces of evidence behind it - `Platform::Get()` returning -nil, `ChromeMain` existing beside `WebOSMain`, and `--webos-wam` being mandatory - and it -should be tested rather than believed. +That is a hypothesis with four pieces of evidence behind it - `Platform::Get()` returning +nil, `ChromeMain` existing beside `WebOSMain`, `--webos-wam` being mandatory, and WAM being +the only binary on the TV that links the library at all - and it should be tested rather +than believed. + +`webos::Runtime` is worth knowing about either way. Unlike `Platform`, **its singleton is +alive in a plain embedder** - `Runtime::Get()` returns a real pointer - and it carries +`SetWindowSize()`, `InitializePlatform(const base::FilePath&)` and +`Initialize(webos::PlatformDelegate*)`. `SetWindowSize` and `InitializePlatform` were both +called successfully and changed nothing, so the remaining candidate on that path is +`Initialize(PlatformDelegate*)`, which needs a delegate whose interface has not been +reconstructed. `base::FilePath` is declarable, for what it is worth: libcbe exports its +`std::string` constructor and destructor, and its layout is that one member. So this is honest work-in-progress. The hard half - the ABI - is done and demonstrated. The window handover is not, and the next person should start at `LunaServices` and `ChromeMain`. diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp index c4debb5..e181ac6 100644 --- a/web/cbe-webos3/main.cpp +++ b/web/cbe-webos3/main.cpp @@ -77,18 +77,13 @@ SampleWebView* g_webview; std::string g_app_path; gboolean CreateWebApp(gpointer) { + g_window = new SampleWindow(); // No InitWindow on webOS 3. WAM never calls Resize either, but dropping it // here stops the delegate firing at all and the Wayland connection starts // complaining "proxy already has listener", so it is doing something the // constructor alone does not. - printf("[diag] before Resize: display=%dx%d native=%p handle=%p state=%d\n", - g_window->DisplayWidth(), g_window->DisplayHeight(), - g_window->GetNativeWindow(), (void*)0, (int)g_window->GetWindowHostState()); g_window->Resize(1920, 1080); - printf("[diag] after Resize: display=%dx%d native=%p state=%d\n", - g_window->DisplayWidth(), g_window->DisplayHeight(), - g_window->GetNativeWindow(), (int)g_window->GetWindowHostState()); g_window->SetWindowProperty("appId", kAppId); g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); @@ -103,7 +98,10 @@ gboolean CreateWebApp(gpointer) { g_window->AttachWebContents(g_webview->GetWebContents()); g_window->Show(); - printf("[diag] after Show: native=%p state=%d\n", + // Reads back 0 - NATIVE_WINDOW_DEFAULT - however the state is set. The + // compositor never acknowledges it, which is the whole problem. + printf("[cbe] window %dx%d native=%p host-state=%d\n", + g_window->DisplayWidth(), g_window->DisplayHeight(), g_window->GetNativeWindow(), (int)g_window->GetWindowHostState()); // webOS 3 has no Activate(). SetHiddenState(false) and re-asserting the appId // after Show() are the nearest equivalents worth trying. diff --git a/web/libcbe/webos3/cbe_stub.cpp b/web/libcbe/webos3/cbe_stub.cpp index cc702f2..1a3474e 100644 --- a/web/libcbe/webos3/cbe_stub.cpp +++ b/web/libcbe/webos3/cbe_stub.cpp @@ -9,8 +9,17 @@ extern "C" int WebOSMain(int, const char**) STUB(0) +namespace base { +FilePath::FilePath(const std::string& p) : path_(p) STUB() +FilePath::~FilePath() STUB() +} // namespace base + namespace webos { +Runtime* Runtime::Get() STUB(0) +void Runtime::InitializePlatform(const base::FilePath&) STUB() +void Runtime::SetWindowSize(int, int) STUB() + WebViewBase::WebViewBase(int, int) : webview_(0) STUB() WebViewBase::~WebViewBase() STUB() diff --git a/web/libcbe/webos3/webos/webview_base.h b/web/libcbe/webos3/webos/webview_base.h index 2e5fcd7..addeb51 100644 --- a/web/libcbe/webos3/webos/webview_base.h +++ b/web/libcbe/webos3/webos/webview_base.h @@ -26,6 +26,35 @@ namespace webos { class WebViewProfile; +} // namespace webos + +// Chromium's own path type, exported by libcbe. Layout is a single std::string, +// which is what makes it safe to declare here. +namespace base { +class FilePath { + public: + explicit FilePath(const std::string& path); + ~FilePath(); + + private: + std::string path_; +}; +} // namespace base + +namespace webos { + +// A singleton libcbe keeps for platform-wide state. Unlike webos::Platform, +// which belongs to the browser application, this one may exist in a plain +// embedder - and the window size lives here too. +class Runtime { + public: + static Runtime* Get(); + void SetWindowSize(int width, int height); + // Suspected to be what builds webos::Platform - the layer that owns the Luna + // side, and which is null in a plain embedder. + void InitializePlatform(const base::FilePath& path); +}; + // 24 slots. Names for 0-14 and 18-19 come from BlinkWebView's vtable; the rest // are stubs it fills with empty bodies, and must be present or libcbe indexes From 633a15d14b4ab6adc4ccac74ca376c1f66c16d9b Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 08:40:30 +0900 Subject: [PATCH 23/40] web/cbe-webos3: PlatformDelegate reached, and it is not the answer either webos::PlatformDelegate's vtable is exported, so its shape is recoverable even though nothing in the firmware implements the class: two destructor slots then nine __cxa_pure_virtual entries. A stub with that shape is enough for Runtime::Initialize(PlatformDelegate*) to accept it, and both that and InitializePlatform(base::FilePath) then return cleanly - so all three Runtime entry points are now reachable from a plain embedder. None of them changes anything. The host state still reads back 0 and LSM still shows the previous app. The delegate is never called during startup either, which means its unknown signatures never came up - and also that initialising it is not what the window is waiting for. Also dead: asserting Show() and SetWindowHostState(FULLSCREEN) from DidFirstNonBlankPaint(), on the theory that the compositor might ignore a state set on a surface that has never committed a buffer. host-state=0 after the first frame too. The experiments are out of main.cpp again; the recovered declarations stay in the header because they are real results, labelled with what they do not do. --- web/cbe-webos3/README.md | 19 ++++++++++++++++++- web/cbe-webos3/main.cpp | 12 ++++++++++-- web/libcbe/webos3/cbe_stub.cpp | 1 + web/libcbe/webos3/webos/webview_base.h | 20 ++++++++++++++++++++ 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md index 5ed4613..54c4cac 100644 --- a/web/cbe-webos3/README.md +++ b/web/cbe-webos3/README.md @@ -151,7 +151,24 @@ nil, `ChromeMain` existing beside `WebOSMain`, `--webos-wam` being mandatory, an the only binary on the TV that links the library at all - and it should be tested rather than believed. -`webos::Runtime` is worth knowing about either way. Unlike `Platform`, **its singleton is +### Chasing `PlatformDelegate`, and what it cost + +`webos::PlatformDelegate`'s vtable is exported, so its *shape* is recoverable even though +nothing in the firmware implements it: two destructor slots, then nine slots that are all +`__cxa_pure_virtual` in the base. Eleven virtuals, no names, no signatures. + +A stub delegate with that shape is enough for `Runtime::Initialize(PlatformDelegate*)` to +accept it, and both it and `InitializePlatform(base::FilePath)` then return cleanly. Neither +changes anything: the host state still reads back 0 and LSM still shows the previous app. +The delegate is never called during startup, so the unknown signatures never come up - which +also means initialising it is not what the window is waiting for. + +One more idea, also dead: asserting `Show()` and `SetWindowHostState(FULLSCREEN)` from +`DidFirstNonBlankPaint()`, on the theory that the compositor might ignore a state set on a +surface that has never committed a buffer. It does not - `host-state=0` after the first +frame too. + +### `webos::Runtime` is worth knowing about either way. Unlike `Platform`, **its singleton is alive in a plain embedder** - `Runtime::Get()` returns a real pointer - and it carries `SetWindowSize()`, `InitializePlatform(const base::FilePath&)` and `Initialize(webos::PlatformDelegate*)`. `SetWindowSize` and `InitializePlatform` were both diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp index e181ac6..e4277ab 100644 --- a/web/cbe-webos3/main.cpp +++ b/web/cbe-webos3/main.cpp @@ -31,6 +31,9 @@ namespace { const char kAppId[] = "org.webosbrew.sample.web.cbe3"; const char kUrl[] = "https://example.com/"; +class SampleWindow; +SampleWindow* g_window; + class SampleWebView : public webos::WebViewBase { public: SampleWebView(int w, int h) : webos::WebViewBase(w, h) {} @@ -39,7 +42,12 @@ class SampleWebView : public webos::WebViewBase { printf("[cbe] progress %3.0f%%\n", progress * 100); } void DidFirstFrameFocused() override {} - void DidFirstNonBlankPaint() override { puts("[cbe] first non-blank paint"); } + void DidFirstNonBlankPaint() override { + puts("[cbe] first non-blank paint"); + // Try asserting the window state once a frame actually exists - the + // compositor may ignore it on a surface that has never committed a buffer. + + } void LoadVisuallyCommitted() override { puts("[cbe] visually committed"); } void TitleChanged(const std::string& title) override { printf("[cbe] title '%s'\n", title.c_str()); @@ -72,12 +80,12 @@ class SampleWindow : public webos::WebAppWindowBase { bool event(WebOSEvent*) override { return false; } }; -SampleWindow* g_window; SampleWebView* g_webview; std::string g_app_path; gboolean CreateWebApp(gpointer) { + g_window = new SampleWindow(); // No InitWindow on webOS 3. WAM never calls Resize either, but dropping it // here stops the delegate firing at all and the Wayland connection starts diff --git a/web/libcbe/webos3/cbe_stub.cpp b/web/libcbe/webos3/cbe_stub.cpp index 1a3474e..e362061 100644 --- a/web/libcbe/webos3/cbe_stub.cpp +++ b/web/libcbe/webos3/cbe_stub.cpp @@ -18,6 +18,7 @@ namespace webos { Runtime* Runtime::Get() STUB(0) void Runtime::InitializePlatform(const base::FilePath&) STUB() +void Runtime::Initialize(PlatformDelegate*) STUB() void Runtime::SetWindowSize(int, int) STUB() WebViewBase::WebViewBase(int, int) : webview_(0) STUB() diff --git a/web/libcbe/webos3/webos/webview_base.h b/web/libcbe/webos3/webos/webview_base.h index addeb51..f1e0757 100644 --- a/web/libcbe/webos3/webos/webview_base.h +++ b/web/libcbe/webos3/webos/webview_base.h @@ -43,6 +43,25 @@ class FilePath { namespace webos { +// Recovered from the exported vtable of webos::PlatformDelegate: two destructor +// slots followed by nine pure virtuals, all of them __cxa_pure_virtual in the +// base. The names and signatures are unknown - nothing in the firmware +// implements this class - so these are placeholders with the right *shape*. +// Enough to hand Runtime::Initialize something it will accept. +class PlatformDelegate { + public: + virtual ~PlatformDelegate() {} + virtual void Unknown2() {} + virtual void Unknown3() {} + virtual void Unknown4() {} + virtual void Unknown5() {} + virtual void Unknown6() {} + virtual void Unknown7() {} + virtual void Unknown8() {} + virtual void Unknown9() {} + virtual void Unknown10() {} +}; + // A singleton libcbe keeps for platform-wide state. Unlike webos::Platform, // which belongs to the browser application, this one may exist in a plain // embedder - and the window size lives here too. @@ -53,6 +72,7 @@ class Runtime { // Suspected to be what builds webos::Platform - the layer that owns the Luna // side, and which is null in a plain embedder. void InitializePlatform(const base::FilePath& path); + void Initialize(PlatformDelegate* delegate); }; From 675b26b7935a2d83c47666d99291ee14bbcd371a Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 08:48:19 +0900 Subject: [PATCH 24/40] web/cbe-webos3: register with SAM, the way SDL-webOS does A native webOS app has to tell SAM it is running, and libcbe does not do it - it opens its own Luna connections but never registers the app. On webOS 3 nothing else will either: WAM's binary registers itself as com.palm.webappmanager before handing over to WebOSMain. SDL-webOS does this in SDL_webOSRegisterApp(), so luna_register.c is the same call with the same library, minus SDL: HLunaServiceCall to luna://com.webos.applicationManager/registerNativeApp with {"id": appId}, from libhelpers.so.2 which is already on the TV. dlopen rather than linked, since one function is not worth a NEEDED entry. The context keeps multiple=1 so the subscription stays open for relaunch and close events, which is how a real app would receive them. It works - {"message":"registered","returnValue":true} - and the window still does not appear. So this was a genuine gap in the sample and the right thing to do regardless, but it is not what the compositor is waiting for. WAYLAND_DEBUG=1 was the other thing to try and produces no protocol output from libcbe, so that diagnostic is closed too. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe-webos3/CMakeLists.txt | 5 ++- web/cbe-webos3/README.md | 30 +++++++++++++ web/cbe-webos3/luna_register.c | 80 ++++++++++++++++++++++++++++++++++ web/cbe-webos3/luna_register.h | 16 +++++++ web/cbe-webos3/main.cpp | 4 ++ 5 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 web/cbe-webos3/luna_register.c create mode 100644 web/cbe-webos3/luna_register.h diff --git a/web/cbe-webos3/CMakeLists.txt b/web/cbe-webos3/CMakeLists.txt index 5a9cd66..0abaf28 100644 --- a/web/cbe-webos3/CMakeLists.txt +++ b/web/cbe-webos3/CMakeLists.txt @@ -12,11 +12,12 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(GLIB REQUIRED glib-2.0) -add_executable(web-cbe-webos3 main.cpp) +add_executable(web-cbe-webos3 main.cpp luna_register.c) +target_include_directories(web-cbe-webos3 PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") target_include_directories(web-cbe-webos3 SYSTEM PRIVATE ${GLIB_INCLUDE_DIRS}) target_link_directories(web-cbe-webos3 PRIVATE ${GLIB_LIBRARY_DIRS}) # cbe-stub-webos3 carries the include path, -fno-rtti and the old string ABI. -target_link_libraries(web-cbe-webos3 PRIVATE cbe-stub-webos3 ${GLIB_LIBRARIES}) +target_link_libraries(web-cbe-webos3 PRIVATE cbe-stub-webos3 ${GLIB_LIBRARIES} ${CMAKE_DL_LIBS}) target_compile_definitions(web-cbe-webos3 PRIVATE APP_LOG_NAME="web-cbe-webos3") set_target_properties(web-cbe-webos3 PROPERTIES diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md index 54c4cac..cb3bc9c 100644 --- a/web/cbe-webos3/README.md +++ b/web/cbe-webos3/README.md @@ -129,6 +129,36 @@ as webOS 4's - `WebOSMain`, `WebAppManagerServiceLuna::instance()`, `WebAppManager::instance()`, `setPlatformModules` - so at process level it does what this sample does. +### Registering with SAM: necessary, and still not sufficient + +A native webOS app has to tell SAM it is running. libcbe does not do it - it opens its own +Luna connections but never registers the app - and on webOS 3 nothing else will either: WAM's +binary registers *itself* as `com.palm.webappmanager` before handing over to `WebOSMain`. + +SDL-webOS does it in `SDL_webOSRegisterApp()`, and `luna_register.c` here is the same call +with the same library, minus SDL: + +```c +HLunaServiceCall("luna://com.webos.applicationManager/registerNativeApp", + "{\"id\":\"org.webosbrew.sample.web.cbe3\"}", &ctx); +``` + +`libhelpers.so.2` is already on the TV, so it is `dlopen`ed rather than linked - one function +is not worth a NEEDED entry and something for `-verify` to check. `ctx.multiple = 1` keeps +the subscription open, which is how relaunch and close events arrive later. Version 1 of the +native lifecycle interface is `registerNativeApp`; version 2 would be `registerApp`, and the +sample's appinfo declares neither, which means 1. + +It works: + +``` +[luna] lifecycle: {"message":"registered","returnValue":true} +[luna] registerNativeApp(org.webosbrew.sample.web.cbe3) -> 0 +``` + +and the window still does not appear. So registration is a thing this sample was missing and +should have been doing, and it is not what the compositor is waiting for. + ### The live lead is registration. libcbe contains `palm://com.webos.applicationManager/registerNativeApp` and a `webos::LunaServices` class whose `Initialize(const base::FilePath&)` is an instance method needing a diff --git a/web/cbe-webos3/luna_register.c b/web/cbe-webos3/luna_register.c new file mode 100644 index 0000000..aea79f9 --- /dev/null +++ b/web/cbe-webos3/luna_register.c @@ -0,0 +1,80 @@ +// Registering the app with SAM, which is what a native webOS app has to do +// before the system treats it as running. +// +// libcbe does not do this for you: it opens its own Luna connections but never +// registers the app, and on webOS 3 nothing else will do it either - WAM's +// binary registers *itself* as com.palm.webappmanager before handing over to +// WebOSMain. SDL does it in SDL_webOSRegisterApp(), and this is the same call +// with the same library, minus SDL. +// +// dlopen rather than a link stub: libhelpers is one function here, and keeping +// it out of the ELF's NEEDED list means `-verify` has nothing new to check. + +#include "luna_register.h" + +#include +#include +#include + +// From webosbrew's libhelpers, via SDL-webOS's SDL_webos_helpers_sym.h. +typedef struct LSHandle LSHandle; +typedef struct LSMessage LSMessage; +typedef struct HContext HContext; +typedef int (*HLSFilterFunc)(LSHandle *sh, LSMessage *reply, HContext *ctx); + +struct HContext { + HLSFilterFunc callback; + void *userdata; + void *unknown; + int multiple; /* subscription rather than one-shot */ + int pub; /* public or private bus */ + unsigned long ret_token; +}; + +static int (*HLunaServiceCall)(const char *uri, const char *payload, HContext *context); +static const char *(*HLunaServiceMessage)(LSMessage *message); + +static HContext g_lifecycle; + +static int LifecycleCb(LSHandle *sh, LSMessage *reply, HContext *ctx) +{ + (void)sh; + (void)ctx; + if (HLunaServiceMessage != NULL) { + printf("[luna] lifecycle: %s\n", HLunaServiceMessage(reply)); + } + return 1; +} + +int luna_register_app(const char *app_id) +{ + char payload[256]; + void *lib; + int rc; + + lib = dlopen("libhelpers.so.2", RTLD_LAZY); + if (lib == NULL) { + printf("[luna] dlopen libhelpers.so.2: %s\n", dlerror()); + return -1; + } + HLunaServiceCall = dlsym(lib, "HLunaServiceCall"); + HLunaServiceMessage = dlsym(lib, "HLunaServiceMessage"); + if (HLunaServiceCall == NULL) { + printf("[luna] no HLunaServiceCall in libhelpers\n"); + return -1; + } + + snprintf(payload, sizeof(payload), "{\"id\":\"%s\"}", app_id); + + memset(&g_lifecycle, 0, sizeof(g_lifecycle)); + g_lifecycle.callback = LifecycleCb; + g_lifecycle.multiple = 1; /* stays subscribed for relaunch/close events */ + g_lifecycle.pub = 1; + + // nativeLifeCycleInterfaceVersion 1 is registerNativeApp; version 2 would be + // registerApp. The sample's appinfo declares neither, which means 1. + rc = HLunaServiceCall("luna://com.webos.applicationManager/registerNativeApp", + payload, &g_lifecycle); + printf("[luna] registerNativeApp(%s) -> %d\n", app_id, rc); + return rc; +} diff --git a/web/cbe-webos3/luna_register.h b/web/cbe-webos3/luna_register.h new file mode 100644 index 0000000..b7353d7 --- /dev/null +++ b/web/cbe-webos3/luna_register.h @@ -0,0 +1,16 @@ +#ifndef WEB_CBE_WEBOS3_LUNA_REGISTER_H_ +#define WEB_CBE_WEBOS3_LUNA_REGISTER_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +// Registers this process with SAM as the running instance of app_id. Returns 0 +// on success. See luna_register.c for why libcbe does not do this itself. +int luna_register_app(const char *app_id); + +#ifdef __cplusplus +} +#endif + +#endif // WEB_CBE_WEBOS3_LUNA_REGISTER_H_ diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp index e4277ab..929ec82 100644 --- a/web/cbe-webos3/main.cpp +++ b/web/cbe-webos3/main.cpp @@ -21,6 +21,7 @@ #include #include +#include "luna_register.h" #include "webos/webapp_window_base.h" #include "webos/webview_base.h" @@ -84,6 +85,9 @@ SampleWebView* g_webview; std::string g_app_path; gboolean CreateWebApp(gpointer) { + // Before anything else: tell SAM this process is the app. + luna_register_app(kAppId); + g_window = new SampleWindow(); From 237437eae346ddfacdc87872078bb57c4233ab47 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 08:56:30 +0900 Subject: [PATCH 25/40] web/cbe-webos3: it is not a windowing bug - no frame is ever committed Reading SDL-webOS's windowing side and then the actual protocol traffic relocates the problem entirely. LG's libwayland-client honours WAYLAND_DEBUG but writes to /tmp/wayland__.log rather than stderr, which is why the earlier attempt to capture it found nothing. With that log, what libcbe does is plain: -> wl_compositor@4.create_surface(new id wl_surface@20) -> wl_webos_shell@10.get_shell_surface(new id wl_webos_shell_surface@23, ...) -> wl_webos_shell_surface@23.set_state(3) -> wl_webos_shell_surface@23.set_property("appId", "org.webosbrew...") All correct, no protocol errors, and set_state(3) is the same fullscreen request SDL-webOS sends from Wayland_activate_window(). But across all five processes of a run, each with its own connection log, attach=0 and commit=0. No buffer ever reaches the surface. So the window is created and configured properly and LSM has what it needs; the missing piece is presentation. The page genuinely renders - progress, title, first non-blank paint, load finished all arrive - and those pixels never reach wl_surface@20. It also explains GetWindowHostState() staying 0: the compositor sends no state event for a surface that has never committed. Dropping --in-process-gpu on the theory that the GPU path was at fault changes nothing. Every windowing-API avenue chased over the last several commits was therefore aimed at the wrong layer. The README now says where to start instead: capture the same log from web/cbe on a webOS 4 set, where identical code does present, and diff the two. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe-webos3/README.md | 7 +++++-- web/cbe-webos3/main.cpp | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md index cb3bc9c..61a0d01 100644 --- a/web/cbe-webos3/README.md +++ b/web/cbe-webos3/README.md @@ -207,5 +207,8 @@ called successfully and changed nothing, so the remaining candidate on that path reconstructed. `base::FilePath` is declarable, for what it is worth: libcbe exports its `std::string` constructor and destructor, and its layout is that one member. -So this is honest work-in-progress. The hard half - the ABI - is done and demonstrated. The -window handover is not, and the next person should start at `LunaServices` and `ChromeMain`. +So this is honest work-in-progress. The hard half - the ABI - is done and demonstrated, and +the failure is now located precisely: no frame is ever committed to the window's Wayland +surface. The next person should start there - comparing this log against the same capture +from `web/cbe` on a webOS 4 set, where the identical code does present - rather than at the +windowing API, which the protocol trace shows is being driven correctly. diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp index 929ec82..2a0daf8 100644 --- a/web/cbe-webos3/main.cpp +++ b/web/cbe-webos3/main.cpp @@ -157,6 +157,7 @@ int main(int argc, char** argv) { args.push_back("--no-sandbox"); args.push_back("--no-zygote"); args.push_back("--in-process-gpu"); + args.push_back(std::string("--browser-subprocess-path=") + argv[0]); args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); // Borrowed from WAM's own WAM_SWITCHES on this generation, pending bisection. @@ -172,6 +173,7 @@ int main(int argc, char** argv) { for (size_t i = 0; i < args.size(); ++i) cargv.push_back(args[i].c_str()); if (!getenv("XDG_RUNTIME_DIR")) setenv("XDG_RUNTIME_DIR", "/tmp/xdg", 1); + // webOS 3's WebOSMain does std::string(getenv("CDM_LIB_PATH")) with no null // check and appends "/libwidevinecdmadapter.so" to it, so an unset variable // aborts the process before anything of ours runs - From 31d9913f8a86198d88b76a4bb8c2a7589d4270f0 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 09:11:51 +0900 Subject: [PATCH 26/40] web/cbe-webos3: fix the GPU command buffer, and diff against a working app Two things this round, both from comparing against something that works. media/ndl/esplayer - a plain SDL sample, already verified on this same TV - does become the foreground app, which rules out the platform, the packaging and the launch path together. Its Wayland trace also shows the shape libcbe is missing: attach, damage, commit, and only then does the compositor reply state_changed(3) by itself. SDL never sends set_state at all. So the window state is a consequence of presenting a frame, not a precondition for it, and every attempt in the previous commits to force the state was pushing on the wrong end. The app's own log had been carrying the real fault all along, buried in Chromium's noise: ERROR:command_buffer_proxy_impl.cc(153)] Could not send GpuCommandBufferMsg_Initialize. webOS 3's GPU path wants more setup than webOS 4's. Adding WAM's own GPU switches for this generation - gpu-rasterization, impl-side-painting, ignore-gpu-blacklist, threaded-compositing, num-raster-threads, ui-use-prepare-shader-program, ui-disable-opaque-shader-program, disable-low-res-tiling - makes the error go away. Not sufficient: attach and commit are still 0 and the screen is unchanged. But it is a real fault fixed, and a compositor cannot present a surface that Chromium never draws into. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe-webos3/main.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp index 2a0daf8..7ef0041 100644 --- a/web/cbe-webos3/main.cpp +++ b/web/cbe-webos3/main.cpp @@ -157,6 +157,19 @@ int main(int argc, char** argv) { args.push_back("--no-sandbox"); args.push_back("--no-zygote"); args.push_back("--in-process-gpu"); + // webOS 3's GPU path needs more setup than webOS 4's: without these the + // command buffer fails to initialise ("Could not send + // GpuCommandBufferMsg_Initialize") and the compositor never produces a + // frame, so nothing is ever committed to the window's Wayland surface. + // Taken from WAM's own WAM_SWITCHES on this generation. + args.push_back("--enable-gpu-rasterization"); + args.push_back("--enable-impl-side-painting"); + args.push_back("--ignore-gpu-blacklist"); + args.push_back("--enable-threaded-compositing"); + args.push_back("--num-raster-threads=2"); + args.push_back("--ui-use-prepare-shader-program"); + args.push_back("--ui-disable-opaque-shader-program"); + args.push_back("--disable-low-res-tiling"); args.push_back(std::string("--browser-subprocess-path=") + argv[0]); args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); From c0959aad5805a8bd971c54770f6b65734b60cf4f Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 12:26:35 +0900 Subject: [PATCH 27/40] web/cbe-webos3: switches are not the answer; no accelerated surface is made WAM's entire switch list, all twenty-five, adopted verbatim: the page still loads and the window still never appears. Trimmed back to the GPU subset, which is the part that fixed a real error, since the rest is noise in a sample. Located the failure one step further in. libcbe logs EGL window surface creation when it happens, and webOS 3's library has the same format string, but it never appears in a run - so weboswayland::WaylandDisplay::CreateAcceleratedSurface() is never reached. That is the same fact as attach=0, seen from libcbe's side rather than the compositor's. Not for want of a window: it reports a valid handle, 1920x1080, non-null native pointer. Also tried and inert: SetVisibilityState(VISIBILITY_VISIBLE) and SetHiddenState(false), on the theory that Chromium will not paint a page it believes hidden, and calling AttachWebContents() after Show() instead of before, the opposite of webOS 4's order. One signal left unexplained rather than glossed over: "proxy 0x... already has listener" on every run, which is libwayland saying a listener was added twice to one proxy - and the trace does show libcbe creating two registries and binding wl_webos_shell more than once. Whether that is benign or the cause is not established. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe-webos3/main.cpp | 35 ++++++++++---------- web/libcbe/webos3/cbe_stub.cpp | 1 + web/libcbe/webos3/webos/webapp_window_base.h | 2 ++ 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp index 7ef0041..25faf08 100644 --- a/web/cbe-webos3/main.cpp +++ b/web/cbe-webos3/main.cpp @@ -107,14 +107,19 @@ gboolean CreateWebApp(gpointer) { g_webview->SetAllowLocalResourceLoad(true); g_webview->SetLocalStorageEnabled(true); g_webview->SetVisible(true); + // Chromium does not paint a page it believes is hidden, and nothing sets this + // for us - libcbe leaves the visibility state at its default. + g_webview->SetVisibilityState(webos::WebViewBase::VISIBILITY_VISIBLE); - g_window->AttachWebContents(g_webview->GetWebContents()); + g_window->SetHiddenState(false); g_window->Show(); + g_window->AttachWebContents(g_webview->GetWebContents()); // Reads back 0 - NATIVE_WINDOW_DEFAULT - however the state is set. The // compositor never acknowledges it, which is the whole problem. - printf("[cbe] window %dx%d native=%p host-state=%d\n", + printf("[cbe] window %dx%d native=%p handle=%u host-state=%d\n", g_window->DisplayWidth(), g_window->DisplayHeight(), - g_window->GetNativeWindow(), (int)g_window->GetWindowHostState()); + g_window->GetNativeWindow(), g_window->GetWindowHandle(), + (int)g_window->GetWindowHostState()); // webOS 3 has no Activate(). SetHiddenState(false) and re-asserting the appId // after Show() are the nearest equivalents worth trying. g_window->SetHiddenState(false); @@ -157,11 +162,16 @@ int main(int argc, char** argv) { args.push_back("--no-sandbox"); args.push_back("--no-zygote"); args.push_back("--in-process-gpu"); - // webOS 3's GPU path needs more setup than webOS 4's: without these the - // command buffer fails to initialise ("Could not send - // GpuCommandBufferMsg_Initialize") and the compositor never produces a - // frame, so nothing is ever committed to the window's Wayland surface. - // Taken from WAM's own WAM_SWITCHES on this generation. + args.push_back(std::string("--browser-subprocess-path=") + argv[0]); + args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); + // Required, not decorative: without --webos-wam the process exits before + // writing a line of log. + args.push_back("--webos-wam"); + args.push_back("--noerrdialogs"); + // webOS 3's GPU path needs more setup than webOS 4's. Without these the + // command buffer fails to initialise - "Could not send + // GpuCommandBufferMsg_Initialize" - and Chromium cannot draw at all. From + // WAM's own WAM_SWITCHES on this generation. args.push_back("--enable-gpu-rasterization"); args.push_back("--enable-impl-side-painting"); args.push_back("--ignore-gpu-blacklist"); @@ -170,15 +180,6 @@ int main(int argc, char** argv) { args.push_back("--ui-use-prepare-shader-program"); args.push_back("--ui-disable-opaque-shader-program"); args.push_back("--disable-low-res-tiling"); - - args.push_back(std::string("--browser-subprocess-path=") + argv[0]); - args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); - // Borrowed from WAM's own WAM_SWITCHES on this generation, pending bisection. - // Required, not decorative: without --webos-wam the process exits before - // writing a line of log. --app-id, which also exists in libcbe, turned out - // to make no difference either way. - args.push_back("--webos-wam"); - args.push_back("--noerrdialogs"); } for (int i = 1; i < argc; ++i) args.push_back(argv[i]); diff --git a/web/libcbe/webos3/cbe_stub.cpp b/web/libcbe/webos3/cbe_stub.cpp index e362061..562d831 100644 --- a/web/libcbe/webos3/cbe_stub.cpp +++ b/web/libcbe/webos3/cbe_stub.cpp @@ -63,6 +63,7 @@ NativeWindowState WebAppWindowBase::GetWindowHostState() const STUB(NATIVE_WINDO void WebAppWindowBase::SetWindowProperty(const std::string&, const std::string&) STUB() void WebAppWindowBase::SetUseVirtualKeyboard(bool) STUB() void WebAppWindowBase::SetKeyMask(WebOSKeyMask, bool) STUB() +unsigned WebAppWindowBase::GetWindowHandle() STUB(0) int WebAppWindowBase::DisplayWidth() STUB(0) int WebAppWindowBase::DisplayHeight() STUB(0) diff --git a/web/libcbe/webos3/webos/webapp_window_base.h b/web/libcbe/webos3/webos/webapp_window_base.h index 4b18954..766be82 100644 --- a/web/libcbe/webos3/webos/webapp_window_base.h +++ b/web/libcbe/webos3/webos/webapp_window_base.h @@ -53,6 +53,8 @@ class WebAppWindowBase : public WebAppWindowDelegate { void SetWindowProperty(const std::string& name, const std::string& value); void SetUseVirtualKeyboard(bool enable); void SetKeyMask(WebOSKeyMask key_mask, bool set); + // The handle the GPU side uses to make an accelerated surface for this window. + unsigned GetWindowHandle(); int DisplayWidth(); int DisplayHeight(); From a4b47183d3431a61d026bb02e48939faf640ec2d Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 16:56:21 +0900 Subject: [PATCH 28/40] web/cbe-webos3: root cause - SetWidgetState(SHOW) is a stub on webOS 3 The answer was in the app's own log, under Chromium's NOTIMPLEMENTED noise: ERROR:display.cc(305)] Not implemented reached in weboswayland::WaylandDisplay::SetWidgetState(..., SHOW, ...) SetWidgetState is the ozone call that maps a window. webOS 3's weboswayland backend implements FULLSCREEN, INACTIVE and UNINITIALIZED - the only state strings in the library - and SHOW falls through to NOTIMPLEMENTED. That is precisely where the window would have been mapped, and it does nothing, which is why no accelerated surface is ever created and nothing is committed. It comes from inside libcbe rather than from the sample: removing our Show() call does not stop it. And the other backend is no escape - --ozone-platform= wayland reaches DesktopFactoryWayland and then the process dies, so weboswayland is mandatory. Also ruled out this round. The Wayland and DRM setup is complete: wl_drm binds, authenticate is sent, authenticated() comes back with the full format list and capabilities(0), so nothing is missing there. And unsquashing a webOS 3.4.3 rootfs from ~/Projects/webos-firmwares confirms what the 3.4.0 device showed - exactly one binary links libcbe, WebAppMgr, so there is no in-firmware example to copy. The tension is stated rather than smoothed over: WAM's web apps do appear on webOS 3 with this same backend, so either they map windows by a path this sample has not found, or SHOW being unimplemented is benign there for a reason not yet understood. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe-webos3/main.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp index 25faf08..ccc0544 100644 --- a/web/cbe-webos3/main.cpp +++ b/web/cbe-webos3/main.cpp @@ -111,21 +111,16 @@ gboolean CreateWebApp(gpointer) { // for us - libcbe leaves the visibility state at its default. g_webview->SetVisibilityState(webos::WebViewBase::VISIBILITY_VISIBLE); - g_window->SetHiddenState(false); - g_window->Show(); + // Show() is a no-op on this generation - see README - so the window state is + // set directly. Neither maps the surface; libcbe reaches SetWidgetState(SHOW) + // internally regardless, and that is the stub. g_window->AttachWebContents(g_webview->GetWebContents()); - // Reads back 0 - NATIVE_WINDOW_DEFAULT - however the state is set. The - // compositor never acknowledges it, which is the whole problem. + g_window->Show(); + g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); printf("[cbe] window %dx%d native=%p handle=%u host-state=%d\n", g_window->DisplayWidth(), g_window->DisplayHeight(), g_window->GetNativeWindow(), g_window->GetWindowHandle(), (int)g_window->GetWindowHostState()); - // webOS 3 has no Activate(). SetHiddenState(false) and re-asserting the appId - // after Show() are the nearest equivalents worth trying. - g_window->SetHiddenState(false); - g_window->SetOpacity(1.0f); - g_window->SetWindowProperty("appId", kAppId); - g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); printf("[cbe] loading %s\n", kUrl); g_webview->LoadUrl(kUrl); From c13ccbcb4420da2253349e7f3e7beae4e6fa4f51 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 19:21:02 +0900 Subject: [PATCH 29/40] web/cbe-webos3: there IS a standalone embedder on webOS 3 - two corrections Restarting WAM with WAYLAND_DEBUG and reading SAM's running-app list corrects two claims this branch had been building on. First: attach=0 is normal. WAM's own trace reaches attach=0 commit=0 too, so libcbe does not present through wl_surface.attach on that connection and its absence proves nothing - the previous commit's "no frame is ever committed" framing was wrong. Diffed by request type, the only thing WAM sends that this sample does not is wl_webos_xinput_extension.register_input, which is input. Second, and the useful one: webOS 3 does have a native standalone libcbe embedder. Asking SAM what is running - rather than searching the filesystem, which is how the earlier wrong claim was reached - shows com.webos.app.browser as native_builtin with a live pid, and /proc//exe points into /mnt/otncabi, outside every directory previously searched. Its command line contradicts what this sample was doing: --ozone-platform= wayland rather than weboswayland, --webos-launch-json carrying the app id as "nid" rather than --webos-wam, plus a specific GPU set. weboswayland is WAM's backend, the one whose SetWidgetState leaves SHOW unimplemented, so a non-WAM app was never meant to use it. Adopting that verbatim does not work yet - the process dies inside libcbe under __vsnprintf_chk, and dropping just the launch-json still dies - so the sample keeps the weboswayland combination that at least starts, and the README says plainly that the browser runs jailed under /var/palm/jail and that this is the thread to pull. Also tried and inert: _WEBOS_WINDOW_TYPE=_WEBOS_WINDOW_TYPE_CARD, which WAM sets on every window and this sample did not. Kept, since it is correct. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe-webos3/README.md | 53 +++++++++++++++++++++++++++++++--------- web/cbe-webos3/main.cpp | 15 +++++++++--- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md index 61a0d01..28bb2d1 100644 --- a/web/cbe-webos3/README.md +++ b/web/cbe-webos3/README.md @@ -116,18 +116,47 @@ which also exists in the library, changes nothing either way. The app does reach the Luna bus: `ls-monitor -l` shows two client-only connections owned by the executable, without a service name. So libcbe's own LS2 client is running. -### Only one binary on the whole TV links libcbe - -Scanning `/usr/bin` and every installed app on webOS 3 turns up exactly one consumer: -`/usr/bin/WebAppMgr`. The web browser there is a *web app* that WAM hosts, not a native -binary of its own - unlike webOS 4, where `com.webos.app.browser/chrome` links libcbe -directly. So on this generation there is no standalone embedder anywhere in the firmware to -copy, which is why none of this can be checked against a working example. - -webOS 3's `WebAppMgr` binary is also no help: its undefined symbols are the same short list -as webOS 4's - `WebOSMain`, `WebAppManagerServiceLuna::instance()`, -`WebAppManager::instance()`, `setPlatformModules` - so at process level it does what this -sample does. +### There *is* a standalone embedder on webOS 3 + +An earlier version of this file said there was not, on the strength of scanning `/usr/bin` +and `/usr/palm/applications`. That was wrong, and the way to find it is to ask SAM what is +running rather than to search the filesystem: + +```sh +luna-send -n 1 -f luna://com.webos.applicationManager/running '{}' +``` + +`com.webos.app.browser` is there, `appType: native_builtin`, with a live pid - and +`/proc//exe` points at +`/mnt/otncabi/usr/palm/applications/com.webos.app.browser/chrome`, which does link libcbe. +It is under `/mnt/otncabi`, which is why the earlier search missed it. + +Its command line is the reference this sample has been missing, and two switches in it +contradict what was being used here: + +``` +--ozone-platform=wayland (not weboswayland) +--webos-launch-json={"@system_native_app":true,"preload":"partial", + "nid":"com.webos.app.browser","launchHidden":true} +--in-process-gpu --ignore-gpu-blacklist --gpu-no-context-lost +--disable-gpu-watchdog --enable-accelerated-compositing +--set-maximized --window-size=1920,1080 +``` + +There is no `--webos-wam` at all. `weboswayland` is WAM's backend - the one whose +`SetWidgetState` leaves `SHOW` unimplemented - and a non-WAM app is evidently expected to +use plain `wayland` and identify itself through `--webos-launch-json`, whose `nid` is the +app id. + +Adopting that configuration verbatim does not work yet: the process dies during startup, +inside libcbe under `__vsnprintf_chk`. Dropping just the launch-json and keeping +`--ozone-platform=wayland` dies too. So something else in the browser's setup is required - +it runs jailed under `/var/palm/jail/com.webos.app.browser`, which is the obvious next +suspect. The sample therefore still ships the `weboswayland` + `--webos-wam` combination, +which is the only one found so far that starts at all. + +**This is the thread to pull.** A working standalone embedder exists on the same TV; the +remaining work is finding what else it needs. ### Registering with SAM: necessary, and still not sufficient diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp index ccc0544..9b288ec 100644 --- a/web/cbe-webos3/main.cpp +++ b/web/cbe-webos3/main.cpp @@ -96,7 +96,12 @@ gboolean CreateWebApp(gpointer) { // complaining "proxy already has listener", so it is doing something the // constructor alone does not. g_window->Resize(1920, 1080); + // LSM ignores a surface with no window type. WAM sets this on every window it + // creates, and it is the one property the earlier attempts were missing - see + // its Wayland trace in README.md. + g_window->SetWindowProperty("_WEBOS_WINDOW_TYPE", "_WEBOS_WINDOW_TYPE_CARD"); g_window->SetWindowProperty("appId", kAppId); + g_window->SetWindowProperty("title", "CBE WebView"); g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); g_webview = new SampleWebView(1920, 1080); // no Initialize either @@ -153,20 +158,22 @@ int main(int argc, char** argv) { // name, and the wrong one gets as far as constructing a std::string from a // null and aborting with basic_string::_S_construct. Taken from WAM's own // WAM_SWITCHES on the device. + // weboswayland, not wayland: this is WAM's backend, and it is the only + // combination found so far that starts at all. See README - webOS 3's own + // native browser uses plain "wayland" with --webos-launch-json, and copying + // that verbatim crashes here, which is the open thread. args.push_back("--ozone-platform=weboswayland"); args.push_back("--no-sandbox"); args.push_back("--no-zygote"); args.push_back("--in-process-gpu"); args.push_back(std::string("--browser-subprocess-path=") + argv[0]); args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); - // Required, not decorative: without --webos-wam the process exits before - // writing a line of log. + // Required: without --webos-wam the process exits before writing a log line. args.push_back("--webos-wam"); args.push_back("--noerrdialogs"); // webOS 3's GPU path needs more setup than webOS 4's. Without these the // command buffer fails to initialise - "Could not send - // GpuCommandBufferMsg_Initialize" - and Chromium cannot draw at all. From - // WAM's own WAM_SWITCHES on this generation. + // GpuCommandBufferMsg_Initialize". From WAM's own WAM_SWITCHES. args.push_back("--enable-gpu-rasterization"); args.push_back("--enable-impl-side-painting"); args.push_back("--ignore-gpu-blacklist"); From 5b1f99d5d694f7e64bfc4cf98adf5f83dfd58fcc Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 19:27:40 +0900 Subject: [PATCH 30/40] web/cbe-webos3: bisect the browser's config down to one fatal switch Adopted webOS 3's native browser configuration piece by piece and bisected what breaks. With everything else from it in place - the launch-json handling, CHROMIUM_BROWSER=yes, BROWSER_NAME=Chromium38, its GPU switches - the sample still runs on weboswayland. Switching that one flag to --ozone-platform=wayland kills it before Chromium writes a log line: zero-byte log, crash under __vsnprintf_chk inside libcbe. So the blocker is that single switch, and the jail is not involved - this sample is jailed too, under /var/palm/jail/org.webosbrew.sample.web.cbe3, so that suspect from the previous commit is wrong. Two pieces of the browser's setup are kept because they are correct regardless of this. SAM hands a native app its launch parameters as a bare JSON argument, and the browser converts it to --webos-launch-json rather than forwarding it - libcbe would otherwise see {"nid":...} where it expects a URL - so the sample now does the same. And CHROMIUM_BROWSER/BROWSER_NAME are set, which the browser has and a plain native app does not. Environment was ruled out by diffing /proc//environ between the running browser and this sample: those two variables, plus fontconfig paths pointing into the browser's own directory, are the only differences. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe-webos3/README.md | 30 +++++++++++++++++++++--------- web/cbe-webos3/main.cpp | 14 +++++++++++++- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md index 28bb2d1..b0feccb 100644 --- a/web/cbe-webos3/README.md +++ b/web/cbe-webos3/README.md @@ -148,15 +148,27 @@ There is no `--webos-wam` at all. `weboswayland` is WAM's backend - the one whos use plain `wayland` and identify itself through `--webos-launch-json`, whose `nid` is the app id. -Adopting that configuration verbatim does not work yet: the process dies during startup, -inside libcbe under `__vsnprintf_chk`. Dropping just the launch-json and keeping -`--ozone-platform=wayland` dies too. So something else in the browser's setup is required - -it runs jailed under `/var/palm/jail/com.webos.app.browser`, which is the obvious next -suspect. The sample therefore still ships the `weboswayland` + `--webos-wam` combination, -which is the only one found so far that starts at all. - -**This is the thread to pull.** A working standalone embedder exists on the same TV; the -remaining work is finding what else it needs. +Adopting it does not work yet, and bisecting says exactly which part is fatal: with +everything else from the browser adopted - the launch-json handling below, +`CHROMIUM_BROWSER=yes`, `BROWSER_NAME=Chromium38`, its GPU switches - the sample still runs +fine on `weboswayland`, and switching that one flag to `--ozone-platform=wayland` kills it +before Chromium writes a single log line. The log is zero bytes and the crash lands under +`__vsnprintf_chk` inside libcbe. + +So `--ozone-platform=wayland` is the blocker, on its own, and the jail is not it: this +sample is jailed too, under `/var/palm/jail/org.webosbrew.sample.web.cbe3`. + +Two pieces of the browser's setup were adopted and kept, because they are right regardless: + +* **SAM hands a native app its launch parameters as a bare JSON argument**, and the browser + turns that into `--webos-launch-json=` rather than forwarding it - libcbe would otherwise + see `{"nid":...}` where it expects a URL. The sample now does the same. +* `CHROMIUM_BROWSER=yes` and `BROWSER_NAME=Chromium38`, which the browser has in its + environment and a plain native app does not. + +**This is the thread to pull.** A working standalone embedder exists on the same TV, its +configuration is known, and the difference is down to one switch that the sample cannot yet +survive. ### Registering with SAM: necessary, and still not sufficient diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp index 9b288ec..a12b0d2 100644 --- a/web/cbe-webos3/main.cpp +++ b/web/cbe-webos3/main.cpp @@ -183,12 +183,24 @@ int main(int argc, char** argv) { args.push_back("--ui-disable-opaque-shader-program"); args.push_back("--disable-low-res-tiling"); } - for (int i = 1; i < argc; ++i) args.push_back(argv[i]); + // SAM hands a native app its launch parameters as a bare JSON argument. The + // TV's own browser turns that into --webos-launch-json and does not forward + // the raw form; libcbe would otherwise treat it as a URL. + for (int i = 1; i < argc; ++i) { + if (browser && argv[i][0] == '{') { + args.push_back(std::string("--webos-launch-json=") + argv[i]); + } else { + args.push_back(argv[i]); + } + } std::vector cargv; for (size_t i = 0; i < args.size(); ++i) cargv.push_back(args[i].c_str()); if (!getenv("XDG_RUNTIME_DIR")) setenv("XDG_RUNTIME_DIR", "/tmp/xdg", 1); + // The native browser has these; a plain native app does not. + setenv("CHROMIUM_BROWSER", "yes", 1); + setenv("BROWSER_NAME", "Chromium38", 1); // webOS 3's WebOSMain does std::string(getenv("CDM_LIB_PATH")) with no null // check and appends "/libwidevinecdmadapter.so" to it, so an unset variable From cedf37cdaf7c879a6680240a6ae995d6e21d96de Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 19:34:07 +0900 Subject: [PATCH 31/40] web/cbe-webos3: libcbe crashes inside its own logger without fontconfig Chased the --ozone-platform=wayland crash properly, and it moved twice. The first crash needed the crash report, because the log was the casualty: zero bytes, with the backtrace in __vsnprintf_chk called from libcbe. Disassembling that address shows a varargs logging helper - __snprintf_chk for a prefix, then __vsnprintf_chk for the message - so libcbe segfaults inside its own logger, and whatever it was trying to report is lost with it. The cause is missing FONTCONFIG_PATH and FONTCONFIG_FILE, which the browser has pointing at its own bundled fonts and a plain native app does not have at all. Setting them to the system /etc/fonts is enough. With those set the wayland backend gets much further - Ozone init, SAM registration, the Luna lifecycle subscription - and then the parent dies on a virtual call through a garbage vtable pointer. The disassembly shows a set-delegate helper: store the pointer at this+64, then immediately call slot 10 on it. So that path wants a delegate a WAM-shaped embedder never supplies. It is not this sample's reconstructed vtables - on weboswayland every delegate callback arrives correctly. --no-zygote is required too: removing it, as the browser's own command line does, regresses to a zero-byte log. The sample keeps weboswayland, which loads pages, and keeps the fontconfig, CHROMIUM_BROWSER and launch-json changes, which are correct regardless of which backend is used. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe-webos3/README.md | 42 ++++++++++++++++++++++++++++++++++++++++ web/cbe-webos3/main.cpp | 2 ++ 2 files changed, 44 insertions(+) diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md index b0feccb..0bb4de1 100644 --- a/web/cbe-webos3/README.md +++ b/web/cbe-webos3/README.md @@ -158,6 +158,48 @@ before Chromium writes a single log line. The log is zero bytes and the crash la So `--ozone-platform=wayland` is the blocker, on its own, and the jail is not it: this sample is jailed too, under `/var/palm/jail/org.webosbrew.sample.web.cbe3`. +#### How far the `wayland` backend now gets + +Two of the browser's remaining differences turn out to be required, and finding the first +one needed the crash report rather than the log - because the log was the casualty. + +**`FONTCONFIG_PATH` and `FONTCONFIG_FILE` must be set.** Without them the process dies with +a zero-byte log, and the backtrace lands in `__vsnprintf_chk` called from libcbe. Reading +the disassembly at that address shows a varargs logging helper: an `__snprintf_chk` for the +prefix, then `__vsnprintf_chk` for the message. **libcbe crashes inside its own logger**, +which is why nothing is ever written - whatever it was trying to report is lost with it. +Pointing the two variables at the system `/etc/fonts` is enough; the browser points them at +its own bundled copy. + +With those set, the `wayland` backend gets much further - through Ozone init, SAM +registration and the Luna lifecycle subscription - before dying again: + +``` +[0824/183136:INFO:desktop_factory_wayland.cc(17)] Ozone: DesktopFactoryWayland +... +[luna] registerNativeApp(org.webosbrew.sample.web.cbe3) -> 0 +[0100/000000:ERROR:zygote_linux.cc(622)] write: Broken pipe +``` + +That last line is a forked child noticing its parent has gone. The parent's own crash is a +virtual call through a garbage vtable pointer, in what the disassembly shows to be a +set-delegate helper - store the pointer at `this+64`, then immediately call slot 10 on it: + +```asm +str r1, [r0, #64] ; this->delegate = arg +cbz r1, done +ldr r2, [r1] ; r2 = arg->vptr +ldr r2, [r2, #40] ; <- SIGSEGV, vtable slot 10 +blx r2 +``` + +So the `wayland` path wants a delegate that a WAM-shaped embedder never has to supply. It +is not this sample's reconstructed vtables at fault: on `weboswayland` every delegate +callback arrives correctly. + +`--no-zygote` is also required - removing it, as the browser does, regresses to a zero-byte +log again. + Two pieces of the browser's setup were adopted and kept, because they are right regardless: * **SAM hands a native app its launch parameters as a bare JSON argument**, and the browser diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp index a12b0d2..5a200c9 100644 --- a/web/cbe-webos3/main.cpp +++ b/web/cbe-webos3/main.cpp @@ -201,6 +201,8 @@ int main(int argc, char** argv) { // The native browser has these; a plain native app does not. setenv("CHROMIUM_BROWSER", "yes", 1); setenv("BROWSER_NAME", "Chromium38", 1); + setenv("FONTCONFIG_PATH", "/etc/fonts", 1); + setenv("FONTCONFIG_FILE", "/etc/fonts/fonts.conf", 1); // webOS 3's WebOSMain does std::string(getenv("CDM_LIB_PATH")) with no null // check and appends "/libwidevinecdmadapter.so" to it, so an unset variable From 73dd4c5bd902aa1f702b7623b2027c27a17fb3dc Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 19:48:01 +0900 Subject: [PATCH 32/40] web/cbe-webos3: why the wayland backend cannot work - two platforms, two APIs Chasing the --ozone-platform=wayland crash to the end answers it. Stepping through startup one puts() at a time puts the crash in new SampleWindow() - the webos::WebAppWindowBase constructor - and it is not timing, since delaying it three seconds crashes identically. The rest lines up behind that. webOS 3's libcbe contains two ozone platforms, ozonewayland (registered as "wayland") and weboswayland; webOS 4's contains only ozonewayland. On the TV, weboswayland is passed by exactly one process, /usr/bin/WebAppMgr, while the native browser passes wayland - and the browser's binary does not reference WebAppWindowBase at all, building its UI from Browser::Init and the Views stack instead. So they are a matched pair: WebAppWindowBase is WAM's windowing API and belongs to weboswayland, and wayland is for Views-based apps. Constructing a WebAppWindowBase under wayland segfaults inside libcbe because that object has no backend there. It also explains why web/cbe works unchanged on webOS 4: LG collapsed the two platforms into one by then. A standalone embedder on webOS 3 therefore has to pick a side, and both are incomplete for this sample's purposes - WAM's side works except for mapping the window, and the browser's side is a different and much larger API that none of the reconstructed headers here apply to. The sample stays on weboswayland, which is the only backend its API exists on, and the earlier fontconfig, CHROMIUM_BROWSER and launch-json findings are kept since they are correct either way. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe-webos3/README.md | 40 ++++++++++++++++++++++++++++++++++++---- web/cbe-webos3/main.cpp | 8 ++++---- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md index 0bb4de1..c9cc0d7 100644 --- a/web/cbe-webos3/README.md +++ b/web/cbe-webos3/README.md @@ -158,7 +158,37 @@ before Chromium writes a single log line. The log is zero bytes and the crash la So `--ozone-platform=wayland` is the blocker, on its own, and the jail is not it: this sample is jailed too, under `/var/palm/jail/org.webosbrew.sample.web.cbe3`. -#### How far the `wayland` backend now gets +#### Why `wayland` cannot work here: two platforms, two window APIs + +Stepping through the startup one `puts()` at a time puts the crash in +`new SampleWindow()` - the `webos::WebAppWindowBase` constructor - and it is not timing: +delaying window creation by three seconds crashes identically. + +That is the whole answer, and the rest of the evidence lines up behind it: + +* webOS 3's libcbe contains **two** ozone platforms, `ozonewayland` (registered as + `wayland`) and `weboswayland`. webOS 4's contains only `ozonewayland`. +* On the TV, `weboswayland` is passed by exactly one process, `/usr/bin/WebAppMgr`. The + native browser passes `wayland`. +* The browser's binary does not reference `WebAppWindowBase` **at all** - it builds its UI + from `Browser::Init(content::BrowserContext*, aura::Window*)` and the Views stack. + +So the two are a matched pair. `WebAppWindowBase` is WAM's windowing API and belongs to +`weboswayland`; `wayland` is for Views-based apps like the browser, which use a different +and much larger API. Constructing a `WebAppWindowBase` under `wayland` segfaults inside +libcbe before the constructor returns, because that object has no backend there. + +That also explains why `web/cbe` works on webOS 4 with the identical code: LG collapsed the +two platforms into one by then, so `wayland` and `WebAppWindowBase` are the same world. On +webOS 3 they are not, and a standalone embedder has to pick a side: + +* **WAM's side** (`weboswayland` + `WebAppWindowBase`) - what this sample does. Everything + works except mapping the window, because `SetWidgetState` leaves `SHOW` unimplemented. +* **The browser's side** (`wayland` + Views) - a working standalone embedder exists, but it + is a different API surface entirely, and none of the reconstructed headers here apply + to it. + +#### How far the `wayland` backend gets before that Two of the browser's remaining differences turn out to be required, and finding the first one needed the crash report rather than the log - because the log was the casualty. @@ -208,9 +238,11 @@ Two pieces of the browser's setup were adopted and kept, because they are right * `CHROMIUM_BROWSER=yes` and `BROWSER_NAME=Chromium38`, which the browser has in its environment and a plain native app does not. -**This is the thread to pull.** A working standalone embedder exists on the same TV, its -configuration is known, and the difference is down to one switch that the sample cannot yet -survive. +**Where that leaves it.** The sample stays on `weboswayland`, which is the only backend its +API exists on. Getting a window there means getting past `SetWidgetState(SHOW)` being a +stub; getting one the browser's way means reconstructing `Browser`, `content::BrowserContext` +and the Views classes instead, which is a much larger job than the embedding API this +directory is about. ### Registering with SAM: necessary, and still not sufficient diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp index 5a200c9..0deceab 100644 --- a/web/cbe-webos3/main.cpp +++ b/web/cbe-webos3/main.cpp @@ -158,10 +158,10 @@ int main(int argc, char** argv) { // name, and the wrong one gets as far as constructing a std::string from a // null and aborting with basic_string::_S_construct. Taken from WAM's own // WAM_SWITCHES on the device. - // weboswayland, not wayland: this is WAM's backend, and it is the only - // combination found so far that starts at all. See README - webOS 3's own - // native browser uses plain "wayland" with --webos-launch-json, and copying - // that verbatim crashes here, which is the open thread. + // weboswayland, and it has to be: WebAppWindowBase only exists on that + // backend. Constructing one under --ozone-platform=wayland segfaults inside + // libcbe before the constructor returns. See README - webOS 3 ships two + // ozone platforms and this API belongs to one of them. args.push_back("--ozone-platform=weboswayland"); args.push_back("--no-sandbox"); args.push_back("--no-zygote"); From 97ca5fbe5227d0dc86533d8891062ec22d2839a7 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 20:01:05 +0900 Subject: [PATCH 33/40] web/cbe-webos3: attach=0 IS the bug - the earlier comparison was wrong Correcting a correction. This branch previously concluded that attach=0 was normal because WAM shows it too. That comparison was against WAM with only com.webos.app.container up, which is preloaded and not visible. Restarting WAM under WAYLAND_DEBUG and launching an app that really does reach the foreground (com.palm.app.settings) gives attach=7 commit=7 on the browser process's own connection, with wl_surface.attach/commit plainly in the trace. So libcbe does present through wl_surface.attach on that connection, and its absence in this sample is meaningful. It also settles the direction of causation: LSM sends state_changed only after a buffer arrives - the working SDL sample shows attach, damage, commit, then state_changed(3) and exposed - so GetWindowHostState() reading back 0 is a consequence of having no frame, not a cause of it. The same trace shows WAM's architecture, worth knowing before copying from it: one window created at startup, with set_property("appId", ...) swapped on that same shell surface as apps come and go, rather than a window per app. Ruled out this round: SetScaleFactor(1.0f), SetOpacity(1.0f) and SetHiddenState(false) - the non-virtual WebAppWindowBase entry points WAM imports and this sample was not calling, found by diffing readelf --dyn-syms on libWebAppMgr.so against our call sites. Kept because WAM calls them, but inert. Also ruled out: GetWebContents() returning null. It returns a valid pointer. The answer to "can the WAM side render at all" is therefore yes on the evidence - WAM does it on this backend, through this API, on this TV - and what remains unknown is what triggers the first frame. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe-webos3/README.md | 7 +++++++ web/cbe-webos3/main.cpp | 12 +++++++++--- web/libcbe/webos3/cbe_stub.cpp | 1 + web/libcbe/webos3/webos/webapp_window_base.h | 2 ++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md index c9cc0d7..15e06ad 100644 --- a/web/cbe-webos3/README.md +++ b/web/cbe-webos3/README.md @@ -238,6 +238,13 @@ Two pieces of the browser's setup were adopted and kept, because they are right * `CHROMIUM_BROWSER=yes` and `BROWSER_NAME=Chromium38`, which the browser has in its environment and a plain native app does not. +**So can the WAM side ever show a window?** On the evidence, yes: WAM produces buffers on +exactly this backend, through exactly this API, in the browser process, on the same TV. What +has not been found is what triggers the first frame. LSM's `state_changed` arrives only +*after* a buffer - the working SDL sample shows `attach`, `damage`, `commit`, and only then +`state_changed(3)` and `exposed` - so the host state reading back 0 is a consequence of +having no buffer, not a cause. + **Where that leaves it.** The sample stays on `weboswayland`, which is the only backend its API exists on. Getting a window there means getting past `SetWidgetState(SHOW)` being a stub; getting one the browser's way means reconstructing `Browser`, `content::BrowserContext` diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp index 0deceab..8ef7fac 100644 --- a/web/cbe-webos3/main.cpp +++ b/web/cbe-webos3/main.cpp @@ -96,6 +96,9 @@ gboolean CreateWebApp(gpointer) { // complaining "proxy already has listener", so it is doing something the // constructor alone does not. g_window->Resize(1920, 1080); + g_window->SetScaleFactor(1.0f); + g_window->SetOpacity(1.0f); + g_window->SetHiddenState(false); // LSM ignores a surface with no window type. WAM sets this on every window it // creates, and it is the one property the earlier attempts were missing - see // its Wayland trace in README.md. @@ -119,12 +122,15 @@ gboolean CreateWebApp(gpointer) { // Show() is a no-op on this generation - see README - so the window state is // set directly. Neither maps the surface; libcbe reaches SetWidgetState(SHOW) // internally regardless, and that is the stub. - g_window->AttachWebContents(g_webview->GetWebContents()); + void* contents = g_webview->GetWebContents(); + g_window->AttachWebContents(contents); g_window->Show(); g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); - printf("[cbe] window %dx%d native=%p handle=%u host-state=%d\n", + // Everything here reads back valid except the host state, which stays 0 + // because the compositor never acknowledges a surface that has no buffer. + printf("[cbe] window %dx%d native=%p handle=%u contents=%p host-state=%d\n", g_window->DisplayWidth(), g_window->DisplayHeight(), - g_window->GetNativeWindow(), g_window->GetWindowHandle(), + g_window->GetNativeWindow(), g_window->GetWindowHandle(), contents, (int)g_window->GetWindowHostState()); printf("[cbe] loading %s\n", kUrl); diff --git a/web/libcbe/webos3/cbe_stub.cpp b/web/libcbe/webos3/cbe_stub.cpp index 562d831..b6a660b 100644 --- a/web/libcbe/webos3/cbe_stub.cpp +++ b/web/libcbe/webos3/cbe_stub.cpp @@ -58,6 +58,7 @@ void WebAppWindowBase::DetachWebContents() STUB() void WebAppWindowBase::RecreatedWebContents() STUB() void WebAppWindowBase::Resize(int, int) STUB() void WebAppWindowBase::SetOpacity(float) STUB() +void WebAppWindowBase::SetScaleFactor(float) STUB() void WebAppWindowBase::SetWindowHostState(NativeWindowState) STUB() NativeWindowState WebAppWindowBase::GetWindowHostState() const STUB(NATIVE_WINDOW_DEFAULT) void WebAppWindowBase::SetWindowProperty(const std::string&, const std::string&) STUB() diff --git a/web/libcbe/webos3/webos/webapp_window_base.h b/web/libcbe/webos3/webos/webapp_window_base.h index 766be82..4d77caa 100644 --- a/web/libcbe/webos3/webos/webapp_window_base.h +++ b/web/libcbe/webos3/webos/webapp_window_base.h @@ -48,6 +48,8 @@ class WebAppWindowBase : public WebAppWindowDelegate { void Resize(int width, int height); void SetOpacity(float opacity); + // WAM calls this; a window whose scale is left at zero has nothing to render. + void SetScaleFactor(float scale); void SetWindowHostState(NativeWindowState state); NativeWindowState GetWindowHostState() const; void SetWindowProperty(const std::string& name, const std::string& value); From 200dc1b80ac658f08f0aa688d5a00dc542e87c97 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 20:08:37 +0900 Subject: [PATCH 34/40] web/cbe-webos3: localise the gap to browser-side compositing Ran out the remaining cheap comparisons against WAM, and they all match: the Wayland request streams differ only by xinput_extension.register_input, the thread lists are the same set including Chrome_InProcGp and WaylandDisplayP, the environments are identical once CHROMIUM_BROWSER, BROWSER_NAME and fontconfig are set, SAM registration succeeds, the window reports 1920x1080 with a native pointer and handle 1, and GetWebContents() returns a non-null pointer that AttachWebContents receives. The row that localises it is the delegate: DidFirstNonBlankPaint fires, so the renderer is painting. What never happens is the browser-side compositor turning that into a frame - CreateAcceleratedSurface is never reached, hence no EGL surface, no buffer, no wl_surface.attach. So the gap is between "renderer has painted" and "browser compositor asks the GPU for an output surface for widget 1", and two libcbe stubs sit near it: WebOSView::GetActiveWebContents() - which fires twice a run and is the more suspicious, since it is the view that should host the contents - and SetWidgetState(SHOW). Establishing whether WAM avoids them needs decompiling WebOSView/WebOSWidgetView rather than more probing from outside. Also recorded: --v=1 adds nothing because VLOG is compiled out of this build, so Chromium's verbose logging is not an avenue. --enable-logging=stderr is kept for page console.log. Also checked and identical: /proc//task thread lists, and the fact that esplayer - a plain native SDL app on this TV - does map a window and take foreground, which rules out LSM policy against non-WAM apps entirely. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe-webos3/README.md | 37 +++++++++++++++++++++++++++++++++++++ web/cbe-webos3/main.cpp | 3 +++ 2 files changed, 40 insertions(+) diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md index 15e06ad..fb5c52b 100644 --- a/web/cbe-webos3/README.md +++ b/web/cbe-webos3/README.md @@ -238,6 +238,43 @@ Two pieces of the browser's setup were adopted and kept, because they are right * `CHROMIUM_BROWSER=yes` and `BROWSER_NAME=Chromium38`, which the browser has in its environment and a plain native app does not. +### Where the gap actually is + +Everything observable on both sides of the comparison now matches, and the list is worth +having so nobody re-checks it: + +| | this sample | WAM | +|---|---|---| +| Wayland requests sent | identical but for `xinput_extension.register_input` | | +| threads | `Chrome_InProcGp`, `WaylandDisplayP`, and the rest | same set | +| environment | identical after adding `CHROMIUM_BROWSER`, `BROWSER_NAME`, fontconfig | | +| SAM registration | `registered`, listed in `/running` | | +| window object | 1920x1080, native pointer, handle 1 | | +| web contents | non-null, attached | | +| delegate callbacks | all fire, including `DidFirstNonBlankPaint` | | + +That last row is the one that localises it. **`DidFirstNonBlankPaint` fires, so the renderer +is painting.** What never happens is the browser-side compositor turning that into a frame: +`CreateAcceleratedSurface` is never reached, so there is no EGL surface, so no buffer, so no +`wl_surface.attach`. + +So the gap is between "renderer has painted" and "browser compositor asks the GPU for an +output surface for widget 1". Two libcbe stubs sit near that path and are logged every run: + +``` +ERROR:webos_view.cc(102)] Not implemented ... content::WebContents* WebOSView::GetActiveWebContents() const +ERROR:display.cc(305)] Not implemented ... weboswayland::WaylandDisplay::SetWidgetState(... SHOW ...) +``` + +`GetActiveWebContents()` returning nothing from the view that is supposed to host the +contents is the more suspicious of the two, and it fires twice per run. Whether WAM avoids +it - by overriding the view, or by never reaching it - is the next thing to establish, and +it needs decompiling libcbe's `WebOSView`/`WebOSWidgetView` rather than more probing from +outside. + +Chromium's own verbose logging is no help: `--v=1` adds nothing, because `VLOG` is compiled +out of this build. + **So can the WAM side ever show a window?** On the evidence, yes: WAM produces buffers on exactly this backend, through exactly this API, in the browser process, on the same TV. What has not been found is what triggers the first frame. LSM's `state_changed` arrives only diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp index 8ef7fac..ef393c4 100644 --- a/web/cbe-webos3/main.cpp +++ b/web/cbe-webos3/main.cpp @@ -177,6 +177,9 @@ int main(int argc, char** argv) { // Required: without --webos-wam the process exits before writing a log line. args.push_back("--webos-wam"); args.push_back("--noerrdialogs"); + // Page console.log lands in the app's log with this. --v=1 adds nothing: + // verbose logging is compiled out of this build. + args.push_back("--enable-logging=stderr"); // webOS 3's GPU path needs more setup than webOS 4's. Without these the // command buffer fails to initialise - "Could not send // GpuCommandBufferMsg_Initialize". From WAM's own WAM_SWITCHES. From 5e9640d804e856fa1dc4a4431d07b5d5be7975c9 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 20:32:59 +0900 Subject: [PATCH 35/40] web/cbe-webos3: the GetActiveWebContents stub is a red herring Decompiled it rather than guessing, and it is not the gap - but the technique is worth keeping, since it is the only way into this binary. libcbe has no .symtab and these functions are local, so the only handle on them is the NOTIMPLEMENTED string. ARM Thumb reaches a string PC-relatively as a literal V plus an "add rX, pc" at address P, with V = target - (P + 4), so scanning .text for words whose implied P lands nearby and decodes as add rX,pc (0x4478-0x447f) finds the real references and nothing else. That located the stub body - logging::GetMinLogLevel, a LogMessage at line 102, matching webos_view.cc(102) - and its single caller, which is four instructions: bl GetActiveWebContents() ; stub, returns NULL cbz r0, done ; skipped silently ldr r3, [r0] ; contents->vptr ldr r3, [r3, #356] ; slot 89 blx r3 So libcbe wants one method on the active WebContents and skips it. We hold that pointer - GetWebContents() returns it - so the skipped call can be made by hand through the same vtable slot. It runs cleanly and changes nothing: no window, host state still 0. Whatever slot 89 does, it does not start compositing. The experiment is removed rather than left in; the write-up and the literal-resolution recipe stay, because the next attempt needs both. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe-webos3/README.md | 38 ++++++++++++++++++++++++++++++++++---- web/cbe-webos3/main.cpp | 1 + 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md index fb5c52b..8c45a55 100644 --- a/web/cbe-webos3/README.md +++ b/web/cbe-webos3/README.md @@ -267,10 +267,40 @@ ERROR:display.cc(305)] Not implemented ... weboswayland::WaylandDisplay::Set ``` `GetActiveWebContents()` returning nothing from the view that is supposed to host the -contents is the more suspicious of the two, and it fires twice per run. Whether WAM avoids -it - by overriding the view, or by never reaching it - is the next thing to establish, and -it needs decompiling libcbe's `WebOSView`/`WebOSWidgetView` rather than more probing from -outside. +contents looked like the answer, and it is worth writing down why it is not - along with +the technique, because that is the reusable part. + +#### Finding a stub's callers in a stripped 65 MB binary + +libcbe has no `.symtab`, and these functions are local, so the only handle is the +`NOTIMPLEMENTED` string. ARM Thumb reaches it PC-relatively, as a literal `V` plus an +`add rX, pc` at address `P`, where `V = target - (P + 4)`. So scan `.text` for words whose +implied `P` lands within a few KB *and* decodes as `add rX, pc` (`0x4478`-`0x447f`): + +```python +V = struct.unpack_from('vptr +ldr r3, [r3, #356] ; slot 89 +blx r3 +``` + +So libcbe wants to call one method on the active `WebContents` and skips it because the +accessor is a stub. **We hold that pointer** - `WebViewBase::GetWebContents()` returns it - +so the call libcbe skipped can be made by hand through the same vtable slot. Doing that +runs cleanly and changes nothing: no window, host state still 0. Whatever slot 89 is, it is +not what starts compositing, and the stub is a red herring. Chromium's own verbose logging is no help: `--v=1` adds nothing, because `VLOG` is compiled out of this build. diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp index ef393c4..4a8ede3 100644 --- a/web/cbe-webos3/main.cpp +++ b/web/cbe-webos3/main.cpp @@ -124,6 +124,7 @@ gboolean CreateWebApp(gpointer) { // internally regardless, and that is the stub. void* contents = g_webview->GetWebContents(); g_window->AttachWebContents(contents); + g_window->Show(); g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); // Everything here reads back valid except the host state, which stays 0 From b30f97fadaa6eb2a60bfdf16cb5b0176692b130b Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 20:50:44 +0900 Subject: [PATCH 36/40] web/cbe-webos3: SetWidgetState(SHOW) was never the blocker Imported libcbe into Ghidra and read both backends, correcting a claim that had spread through several passages of the README. weboswayland::WaylandDisplay::SetWidgetState and its ozonewayland counterpart are structurally identical switches. Both implement CREATED, FULLSCREEN, MAXIMIZED, MINIMIZED, RESTORE and ACTIVE; both leave SHOW, HIDE and INACTIVE as NOTIMPLEMENTED - display.cc:305 and display.cc:254 respectively. ozonewayland is the backend the TV's own native browser uses successfully and it stubs SHOW identically, so the stub is normal, it is not what withholds the window, and the FULLSCREEN path this sample already drives is the implemented one. The three places that described the stub as the thing standing between this sample and a window now say what is actually missing instead: the browser-side compositor never asking the GPU for an output surface. Also confirms the Ghidra import is readable over MCP: weboswayland's CreateAcceleratedSurface allocates a window object, keys it into a map by widget id, and emits the "Wayland Window(id:%d widget:%p) is created" line seen on webOS 4 and never here. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- web/cbe-webos3/README.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md index 8c45a55..61f6528 100644 --- a/web/cbe-webos3/README.md +++ b/web/cbe-webos3/README.md @@ -143,10 +143,9 @@ contradict what was being used here: --set-maximized --window-size=1920,1080 ``` -There is no `--webos-wam` at all. `weboswayland` is WAM's backend - the one whose -`SetWidgetState` leaves `SHOW` unimplemented - and a non-WAM app is evidently expected to -use plain `wayland` and identify itself through `--webos-launch-json`, whose `nid` is the -app id. +There is no `--webos-wam` at all. `weboswayland` is WAM's backend, and a non-WAM app is +evidently expected to use plain `wayland` and identify itself through +`--webos-launch-json`, whose `nid` is the app id. Adopting it does not work yet, and bisecting says exactly which part is fatal: with everything else from the browser adopted - the launch-json handling below, @@ -183,7 +182,7 @@ two platforms into one by then, so `wayland` and `WebAppWindowBase` are the same webOS 3 they are not, and a standalone embedder has to pick a side: * **WAM's side** (`weboswayland` + `WebAppWindowBase`) - what this sample does. Everything - works except mapping the window, because `SetWidgetState` leaves `SHOW` unimplemented. + works except the browser-side compositor ever producing a frame. * **The browser's side** (`wayland` + Views) - a working standalone embedder exists, but it is a different API surface entirely, and none of the reconstructed headers here apply to it. @@ -313,10 +312,10 @@ has not been found is what triggers the first frame. LSM's `state_changed` arriv having no buffer, not a cause. **Where that leaves it.** The sample stays on `weboswayland`, which is the only backend its -API exists on. Getting a window there means getting past `SetWidgetState(SHOW)` being a -stub; getting one the browser's way means reconstructing `Browser`, `content::BrowserContext` -and the Views classes instead, which is a much larger job than the embedding API this -directory is about. +API exists on. Getting a window there means finding what makes the browser-side compositor +ask the GPU for an output surface; getting one the browser's way means reconstructing +`Browser`, `content::BrowserContext` and the Views classes instead, which is a much larger +job than the embedding API this directory is about. ### Registering with SAM: necessary, and still not sufficient From 2b0fd7c755db4eafb4fda6340724fbbb06fc0484 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 21:07:40 +0900 Subject: [PATCH 37/40] web/cbe-webos3: mark it parked, and say where to go instead The webOS 3 sample is unfinished: the reconstructed ABI is correct and demonstrated - the page loads and every delegate callback fires - but the window never reaches the screen. The failure is localised and every dead end is written down, so the README now says so at the top rather than leaving a reader to work through the whole investigation before finding out it does not work. Points at the two things that do: web/cbe on webOS 4, and neva_app_runtime on webOS 6 and newer, which has public upstream headers and needs no reverse engineering. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- README.md | 2 +- web/cbe-webos3/README.md | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index afbe824..0d6730a 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ hard to diagnose from the TV side. | `media/ndl/directmedia` (v2) | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): 300 video + 469 PCM chunks on both | | `media/ndl/directmedia` (v1) | 3.5 - 4.x | built and symbol-verified, needs a 2017-2019 set to test | | `web/hybrid` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the sign-in flow runs end to end - native panel, web login form, and back with the username read out of the intercepted redirect URL. The OK key is verified too, injected with `com.webos.service.networkinput/test/sendKeyCode`; leaving the web view by remote is not, since keys go to whichever window is up | -| `web/cbe-webos3` | webOS 3 | **partly working** - 43UH6100, starfish 3.4.0: the reconstructed webOS 3 ABI is right and the page loads and paints, with every delegate callback firing, but the window never reaches the screen - LSM keeps the previous app foreground. webOS 3 has no `Activate()` and the equivalent has not been found | +| `web/cbe-webos3` | webOS 3 | **unfinished, parked** - 43UH6100, starfish 3.4.0: the reconstructed webOS 3 ABI is right and the page loads and paints, with every delegate callback firing, but the window never reaches the screen - LSM keeps the previous app foreground. webOS 3 has no `Activate()` and the equivalent has not been found | | `web/cbe` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | | `media/smp/webos1` | 1.x | not written yet - and there is no webOS 1 hardware here to validate it against, so it would ship untestable | diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md index 61f6528..655f533 100644 --- a/web/cbe-webos3/README.md +++ b/web/cbe-webos3/README.md @@ -1,5 +1,15 @@ # web/cbe, on webOS 3 +> **Status: unfinished, and parked.** The reconstructed ABI is correct and demonstrated - +> the page loads and every delegate callback fires - but the window never reaches the +> screen. The failure is localised (see "Where the gap actually is") and the dead ends are +> written down so nobody repeats them. Picking this up again means interactive Ghidra work +> on `weboswayland::WaylandDisplay`, not more black-box probing. +> +> If you want a *working* embedded web view, use `web/cbe` on webOS 4, or the +> `neva_app_runtime` API on webOS 6 and newer - the latter has public upstream headers and +> needs no reverse engineering at all. + The same twenty lines of "make a window, make a web view, load a URL" as `web/cbe`, against the libcbe that shipped on webOS 3. Everything that differs is the library moving underneath it, and there is more of that than the version numbers suggest. From dc8ee5a93e9fb647897231be61086251792fae66 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 21:25:20 +0900 Subject: [PATCH 38/40] web/cbe-webos6: map the webOS 6+ ABI; sample starts but is not right yet A third shape of webos::WebViewBase, measured on a 65UP7560 at starfish 6.5.2. The entry point is a class - webos::WebOSMain(delegate).Run(argc, argv) - the view constructor takes (bool, int, int), the delegate is 61 slots rather than 24, and sizeof(WebViewBase) is 92 rather than 8. That size is load-bearing and cost the most time to find: libcbe's constructor writes to offset 90 and WAM's BlinkWebView allocates 116 with its own fields starting at 92, so a subclass declared smaller corrupts the heap and the process dies inside malloc much later, with a backtrace nowhere near the cause. Initialize has two overloads and they split the range: webOS 5 has only the 10-argument form, 7.4 through 11.2 only the 6-argument one, and 6.4 carries both. This sample uses the 10-argument form because that is what WAM calls on 6.5, which caps it at webOS 6; the 6-argument form verifies clean to 11.2 and is the basis for a 7+ variant. Recorded why neva_app_runtime is not the shortcut it appears to be, since that was the plan going in: nothing in the firmware uses it, so there is no vtable to recover names from, and its own vtable has 40 slots where the nearest public headers declare 32 and 26 - LG's build is Chromium 79, between the two public trees. It is a naming reference, not a layout, and without WAM to check against it is a worse starting point than webos::. Not working yet: the delegate is reached and DidStartNavigation fires, but its string argument arrives as garbage, so a slot is misaligned in the first 17. The README says so at the top rather than presenting a sample that does not run. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- CMakeLists.txt | 1 + README.md | 2 + assets/icons/web-cbe-webos6.png | Bin 0 -> 2762 bytes assets/make-icon.py | 1 + web/cbe-webos6/CMakeLists.txt | 35 +++ web/cbe-webos6/README.md | 77 ++++++ web/cbe-webos6/main.cpp | 144 +++++++++++ web/libcbe/webos6/cbe_stub.cpp | 45 ++++ web/libcbe/webos6/webos/webapp_window_base.h | 83 +++++++ web/libcbe/webos6/webos/webview_base.h | 236 +++++++++++++++++++ 10 files changed, 624 insertions(+) create mode 100644 assets/icons/web-cbe-webos6.png create mode 100644 web/cbe-webos6/CMakeLists.txt create mode 100644 web/cbe-webos6/README.md create mode 100644 web/cbe-webos6/main.cpp create mode 100644 web/libcbe/webos6/cbe_stub.cpp create mode 100644 web/libcbe/webos6/webos/webapp_window_base.h create mode 100644 web/libcbe/webos6/webos/webview_base.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 965ad48..892e183 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,5 +36,6 @@ if (TARGET_WEBOS) add_subdirectory(web/libcbe) add_subdirectory(web/cbe) add_subdirectory(web/cbe-webos3) + add_subdirectory(web/cbe-webos6) add_subdirectory(web/hybrid) endif () diff --git a/README.md b/README.md index 0d6730a..77575a7 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ web/ libcbe/ reconstructed libcbe headers and the link stub, shared by both cbe/ the smallest thing that puts a web page on screen cbe-webos3/ the same, against webOS 3's older and different libcbe + cbe-webos6/ and again for webOS 6+, where the API changes a third time hybrid/ a native sign-in flow: SDL2 + Nuklear, a web login page, one process ``` @@ -200,6 +201,7 @@ hard to diagnose from the TV side. | `media/ndl/directmedia` (v2) | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): 300 video + 469 PCM chunks on both | | `media/ndl/directmedia` (v1) | 3.5 - 4.x | built and symbol-verified, needs a 2017-2019 set to test | | `web/hybrid` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the sign-in flow runs end to end - native panel, web login form, and back with the username read out of the intercepted redirect URL. The OK key is verified too, injected with `com.webos.service.networkinput/test/sendKeyCode`; leaving the web view by remote is not, since keys go to whichever window is up | +| `web/cbe-webos6` | webOS 6 | **unfinished** - 65UP7560, starfish 6.5.2: the ABI is mapped (61-slot delegate, 92-byte `WebViewBase`) and the sample builds, verifies and starts, but the delegate arrives with mis-decoded arguments and the process dies. See `web/cbe-webos6/README.md` | | `web/cbe-webos3` | webOS 3 | **unfinished, parked** - 43UH6100, starfish 3.4.0: the reconstructed webOS 3 ABI is right and the page loads and paints, with every delegate callback firing, but the window never reaches the screen - LSM keeps the previous app foreground. webOS 3 has no `Activate()` and the equivalent has not been found | | `web/cbe` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | | `media/smp/webos1` | 1.x | not written yet - and there is no webOS 1 hardware here to validate it against, so it would ship untestable | diff --git a/assets/icons/web-cbe-webos6.png b/assets/icons/web-cbe-webos6.png new file mode 100644 index 0000000000000000000000000000000000000000..f9fc0a3e9955e1ca7ad34729ee62a8b622a15708 GIT binary patch literal 2762 zcmV;*3N`hKP)3%ejKoA@j39y}Dv602)L>!^F-X)3 zMxbJ}NUBXCic(Qatti?;TYgIE4_lUkm6oeEm>~_fYv_Ucd%ZMneK<0ZJ9_wN|Ql zg>nYXUoX_`W%6Gg7$s?}q*h5g%xr75${tG8M1W}pflo{Nl%%sHjh56RX_usRl727g z4oTa*sr^RMJ0$h`yOkv&X|JSLBrTUT-^{i}O)SV)@_Z3J3LotbGn)srm|1Tv>RlB8 z=Eq6E5)O?klDtn2f`2VA(VtOz1Npx1ysg!&Z&eF(n1U88l5~-z{gOH)t(3G_(#w*@ zNxD$dMGNjxO&3Nvmv5`2XFVSZTG=9LjX$Fy zntmbT*9AW7UwyBi=Tl%vfD3`Mlk|#2WA4uJMlI_B7UrIp=DVk+mCT{q8W{VY&-itR zq%KLFoBGy8{B(J1u28!%4N2+!Lh=|#UaB6tO{y$uHn2oNNS%@f=()`TOjHM0#BKGtEQni?2i zc(kMw6e?4aw3*6-+_F?S2u9MI{fLMb`9U+=1hnX2;Uj5+A9;+W8RBKnJiX>C}Jc^tK#- z$+alWJCG6idSXNl0H-GR(tN-4vfKUKchsWZRV&m7v@K?~U((;5+#_igaJHFsrxh`? zvYGW(YTlAM2-CL{JpCs>BdJZ&=@s(zc-aU1+;<##Ee}RbzKW#V{9IOO!i#|!3QmOB z@4eLrw{t6W>iCSI8yF+$1_j|o-t85hJqG9m&hqngKc7~u%GDHPvxBeFA7x-Q@G$T= zu+!;RCV3wNx`BPbHsERCDPW89cKi8nBug@!0F<`<=ICNCfjnko0ZW0IyIP zdx49Sd}{~bjaUjy$vtN<`Ie=5s)bs5V@f_QJXvANcA|Vt&;SRnLUS(&46np zO;l*6(U~BH@!nQRYs_qgm!CQ;^7&H4;nZ3)`xOo3$vGry(Oht6MYYR4T-5wUys}Y{ z(FjEEbU*9p0w3s|3a#lN@=2LdYNClInuuta&Nz>}p=eag*-!%XfpJL%9e#4JX4A66 zK|=`8g=4AEoHoLfds1H_$$Yx5FIs>h7a++h88B7S6_P$EX__*JL6@Wrk{*?`*vy{F z=RGEoOvNSxcNR>)p7SG*U*l^jwHT}q3y>`cT@JkFg82(@pI^^Qc;`87KJb8F_W+;s z`^T0L6XAW)g|VBk054z_!2!{h@V7Z_wWlv5^U&OqmU+}8X||WS0A2*naoTLB-RSAo zMA$d_z1g0gL~xG*d?79Ss0G0zZNRAnf8P$^EWg*{{a~5UCmNYM-ker;p7;?a02>MZ zzH^=L=oMm8@%gSfS9!Y41=R(tbDAaj;+n}i=PAV|4qW7AE%kJ*yzD~@GVk%}ZUCZR zFA8Fk3u1w%(}nfej2Af1Bu}^dd6CoK>*=~_VLiWAeAdSm+*|Wpvm$+}pPwF}jCe2d zM&IC%JbhK9ANlJJ4)wHLaV|T{um3Ff3%Yhr^!r;poi0Vj>n)z1nA|f-|58{Fj>`kR z)XDQ)7Y+2v^m?Vwk+jjwx_r5DpHIn7arO-b%aRA2XM^*c;`IG~-Xm#+q-ntDdO9BJ zqUvO89-}2~QqaIP$~@CZnj&ekq}7sMuTazoN#{!1?B&GwFH4#(X}qNAX0{ezlc`00 zQq+6!8KXx>N_wCA$En1Q|Lla8M-y*Z(u?AOZ`KF+JoQKD{q%)moLqhMBbgk1D__-n7 z|3{2uDtEun94_^As~=l|XPj0vm3zi{QYXVXgm{=Is9_jeLm+VsOee&}bO95Sd-npp zz=VVrr+^du-n|L!O$7HBz=xc6^g0uyep-07S9}#7EJ<*#0$lCsRMR5=<(?ivwh!go zY1v0T5~1AU0{9Ow%W3P$JTfhbz}EpA{rdbw^L~-=4*42S-H-_81~UAN%++;M!h4g` zZU=sl@JG!n9aF-KNQ8Jk@J9~D5{kErJx1o4J~o9Hk@j1_2Y??FEb6vIU^@trs`G(4 zMR|t`4e{mh4L7;%v128T@#L=j_E?VfHQ^$-A*eXLOpQvTpdmD{2;_bdAT@4)Q4>ux z(ZsQehBYEmpCKu!|Jp~zf6g?KpwWgamC)!KI9_2D?i7XnzUw9ZJHPWimnZH^8J_z> z$lUODD0-eo|A3PPa?40z+VZQ3|}Ro{_()fqW+Q0$9SdZKPX74p^<37gTQt| z;N)pen+m+_^zS8kZgJXneA_F*t<~#C47_YBdZyot%|34Nznri}@{=6BvA6suWj(3j zMLA8<1x|av!UFmjNpq9tcZt(Vk{$;-6#ju#lJq}GmjI7QT1|+b{;~_CuL=9gT^c`g zKw;UmUtCQiY>5~UWX{o?bS`jRW>L-$Twf#c7eZ2#X18B~jzY z0n-QzRnvg$o$tOJeag^DXAmH>q@#fUdL1C1G6yU^8KTHyVT0i3ZLggmttL{=WdBY2TebFZT|YI^XT} zsB^U~Q~bh@__*s=>d}M-qAmxpfSCYE;h4npLxtw_JhU8 z`yQCH!jg{na@(kb6|O4C`oZG}!3R+H|00{Cf4A2uMf#`lt0<26YIuX0+qA7?r}4ddtp9_3&x1t?pa?;ocS&T9(4hFYInjZ1%La)G3`C>V%!X0|HNQ}Emq6@IU>)68Be z;yvn7slST-UA^CDAk_3X$VvGmT9wi(ZkndRnL3!+L{fg!@!CWaO&kjU2ZtbG0RrG- QRsaA107*qoM6N<$g81lOZ~y=R literal 0 HcmV?d00001 diff --git a/assets/make-icon.py b/assets/make-icon.py index e5c6e61..fd77a5c 100644 --- a/assets/make-icon.py +++ b/assets/make-icon.py @@ -33,6 +33,7 @@ "lgnc": ("LGNC", "1-4"), "web-cbe": ("CBE", "w4", "globe"), "web-cbe-webos3": ("CBE", "w3", "globe"), + "web-cbe-webos6": ("CBE", "w6+", "globe"), "web-hybrid": ("SDL+", "w4", "globe"), } diff --git a/web/cbe-webos6/CMakeLists.txt b/web/cbe-webos6/CMakeLists.txt new file mode 100644 index 0000000..5c14522 --- /dev/null +++ b/web/cbe-webos6/CMakeLists.txt @@ -0,0 +1,35 @@ +# web/cbe, built against the libcbe that ships on webOS 6 and newer. +# +# One binary for 6.4 through 11.2: the webos:: API stops moving after 6.4, so +# unlike the webOS 3 and 4 generations this range needs no further variants. +# WAM still subclasses this API on these sets, which is what made the 61-slot +# delegate recoverable - see ../libcbe/webos6/webos/webview_base.h. +# +# Verified against a 65UP7560 running starfish 6.5.2. + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GLIB REQUIRED glib-2.0) + +add_library(cbe-stub-webos6 SHARED ../libcbe/webos6/cbe_stub.cpp) +set_target_properties(cbe-stub-webos6 PROPERTIES OUTPUT_NAME cbe + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/stub") +target_include_directories(cbe-stub-webos6 PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../libcbe/webos6") +target_compile_options(cbe-stub-webos6 PUBLIC -fno-rtti) + +add_executable(web-cbe-webos6 main.cpp) +target_include_directories(web-cbe-webos6 SYSTEM PRIVATE ${GLIB_INCLUDE_DIRS}) +target_link_directories(web-cbe-webos6 PRIVATE ${GLIB_LIBRARY_DIRS}) +target_link_libraries(web-cbe-webos6 PRIVATE cbe-stub-webos6 ${GLIB_LIBRARIES}) +target_compile_definitions(web-cbe-webos6 PRIVATE APP_LOG_NAME="web-cbe-webos6") + +set_target_properties(web-cbe-webos6 PROPERTIES + SKIP_BUILD_RPATH TRUE + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH "") + +webos_add_ipk( + TARGET web-cbe-webos6 + APPID org.webosbrew.sample.web.cbe6 + TITLE "CBE WebView (webOS 6+)" + COLOR "#5C6BC0" + WEBOS_VERSIONS ">=6, <7") diff --git a/web/cbe-webos6/README.md b/web/cbe-webos6/README.md new file mode 100644 index 0000000..dd14237 --- /dev/null +++ b/web/cbe-webos6/README.md @@ -0,0 +1,77 @@ +# web/cbe, on webOS 6 and newer + +> **Status: unfinished.** The ABI is mapped and the sample builds, verifies clean and +> starts - it reaches libcbe's delegate - but the delegate arrives with mis-decoded +> arguments and the process then dies. The slot layout below is right in outline and wrong +> in some detail. Everything established so far is written down here; `web/cbe` on webOS 4 +> is the one that works. + +Measured on a **65UP7560 running starfish 6.5.2** (Chromium 79). + +## A third shape of the same API + +`webos::WebViewBase` exists from webOS 3 to 11.2, but it is not one API. This generation +differs from webOS 4 in every part that the calling code touches: + +| | webOS 4 | webOS 6+ | +|---|---|---| +| entry point | `WebOSMain(argc, argv)`, a free function | `webos::WebOSMain(delegate).Run(argc, argv)` | +| view constructor | `WebViewBase()` | `WebViewBase(bool, int, int)` | +| `Initialize` | 5 strings + `int, int, bool` | see below | +| delegate slots | 24 | **61** | +| `sizeof(WebViewBase)` | 8 | **92** | +| `sizeof(WebAppWindowBase)` | 8 | 8 | + +**The object size is load-bearing.** libcbe's `WebViewBase` constructor writes as far as +offset 90, and WAM's `BlinkWebView` allocates 116 bytes with its own fields starting at 92 - +so the base is 92. Declare a subclass any smaller and libcbe writes past the allocation; the +process then dies inside `malloc` much later, nowhere near the cause. That is what the +`reserved_` member is for, and why it is not cosmetic. + +## `Initialize` has two overloads, and they split the range + +| release | 10-arg `Initialize` | 6-arg | constructor | +|---|---|---|---| +| 5.3.1 | yes | no | `WebViewBase(int, int)` | +| **6.4** | **yes** | **yes** | `WebViewBase(bool, int, int)` | +| 7.4 - 11.2 | no | yes | `WebViewBase(bool, int, int)` | + +webOS 6 is a transition release carrying both. This sample uses the 10-argument form, +because that is the one WAM calls at all three of its call sites on 6.5 - which caps it at +webOS 6. A 7-and-up variant is the same code with the 6-argument form, and that form does +verify clean all the way to 11.2. + +## Where the layout came from + +WAM still subclasses the `webos::` API on this generation - `libWebAppMgr.so` makes 122 +calls into it and carries both `BlinkWebView` and `WebAppWaylandWindow` - so the same +technique as webOS 4 applies, and `BlinkWebView`'s vtable gives all 61 slots with names for +most. Slots WAM fills with local functions are `UnknownNN` placeholders: they must exist and +hold their position, but their signatures are not recoverable from the vtable alone. + +`webos::WebAppWindowBase`'s vtable is more interesting - it mixes `webos::` methods with +`neva_app_runtime::WebAppWindowDelegate` ones, so by webOS 6 the old API sits *on top of* +the new runtime rather than beside it. + +### `neva_app_runtime` is not the shortcut it looks like + +It is the API with public upstream headers (`webosose/chromium87`, +`src/neva/app_runtime/public`), and webOS 6.5 exports 218 of its symbols. But: + +* **nothing in the firmware uses it** - WAM is on `webos::`, so there is no reference + implementation and no vtable to recover names from; +* **the upstream headers do not match.** LG's build is Chromium 79, between the public + chromium68 and chromium87 trees, and its `WebViewBase` vtable has **40** slots where + chromium87 declares 32 virtuals and chromium68 declares 26. So the headers are a naming + reference, not the layout - the same position as `webos::`, but without WAM to check + against. + +That is why this sample stays on `webos::`. + +## What is still wrong + +The delegate is reached - `DidStartNavigation` fires - but its `const std::string&` arrives +as garbage, so a slot is misaligned or a signature is wrong somewhere in the first 17. The +base `webos::WebViewBase` vtable counts 51 relocated slots against `BlinkWebView`'s 61, +which is the obvious place to start: confirm the 51 really are a prefix of the 61, and that +no thunk entries shift the numbering. diff --git a/web/cbe-webos6/main.cpp b/web/cbe-webos6/main.cpp new file mode 100644 index 0000000..999cd0f --- /dev/null +++ b/web/cbe-webos6/main.cpp @@ -0,0 +1,144 @@ +// Embedding the TV's Chromium on webOS 6 and newer. +// +// Same shape as web/cbe: hand the process to Chromium, get called back on its +// browser UI thread through the default GMainContext, build a window and a web +// view. Everything that differs is the API moving between generations - see +// ../libcbe/webos6/webos/webview_base.h for the full list, but the three that +// change the code are: +// +// * the entry point is a class - webos::WebOSMain(delegate).Run(argc, argv) - +// where webOS 3 and 4 had a free WebOSMain() function; +// * WebViewBase takes (bool, int, int) and Initialize takes five strings and +// a bool, dropping the width/height pair webOS 4 passed there; +// * the delegate is 61 slots instead of 24. +// +// One binary covers webOS 6.4 through 11.2: the API stops moving after 6.4. + +#include + +#include +#include +#include +#include +#include +#include + +#include "webos/webapp_window_base.h" +#include "webos/webview_base.h" + +namespace { + +const char kAppId[] = "org.webosbrew.sample.web.cbe6"; +const char kUrl[] = "https://example.com/"; + +class SampleWebView : public webos::WebViewBase { + public: + SampleWebView(int w, int h) : webos::WebViewBase(false, w, h) {} + + void OnLoadProgressChanged(double progress) override { + printf("[cbe] progress %3.0f%%\n", progress * 100); + } + void TitleChanged(const std::string& title) override { + printf("[cbe] title '%s'\n", title.c_str()); + } + void LoadStarted() override { puts("[cbe] load started"); } + void LoadFinished(const std::string& url) override { + printf("[cbe] finished %s\n", url.c_str()); + } + void LoadFailed(const std::string& url, int code, const std::string& desc) override { + printf("[cbe] FAILED %s (%d %s)\n", url.c_str(), code, desc.c_str()); + } + void DocumentLoadFinished() override { puts("[cbe] document loaded"); } + void LoadVisuallyCommitted() override { puts("[cbe] visually committed"); } + void DidFirstMeaningfulPaint() override { puts("[cbe] first meaningful paint"); } + void RenderProcessCreated(int pid) override { printf("[cbe] renderer pid %d\n", pid); } + void RenderProcessGone(bool crashed) override { + printf("[cbe] renderer gone (crashed=%d)\n", (int)crashed); + } + void DidStartNavigation(const std::string& url, bool) override { + printf("[cbe] navigate %s\n", url.c_str()); + } +}; + +class SampleWindow : public webos::WebAppWindowBase { + public: + bool event(WebOSEvent*) override { return false; } +}; + +SampleWindow* g_window; +SampleWebView* g_webview; +std::string g_app_path; + +gboolean CreateWebApp(gpointer) { + g_window = new SampleWindow(); + g_window->InitWindow(1920, 1080); + g_window->SetWindowProperty("appId", kAppId); + g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); + + g_webview = new SampleWebView(1920, 1080); + g_webview->Initialize(kAppId, g_app_path, "default", "", "", 1920, 1080, false, + webos::WebViewBase::WEBVIEW_MODE_NORMAL, false); + g_webview->SetAppId(kAppId); + g_webview->SetAllowLocalResourceLoad(true); + g_webview->SetLocalStorageEnabled(true); + g_webview->SetVisible(true); + g_webview->UpdatePreferences(); + + g_window->AttachWebContents(g_webview->GetWebContents()); + g_window->Show(); + g_window->Activate(); + + printf("[cbe] loading %s\n", kUrl); + g_webview->LoadUrl(kUrl); + return G_SOURCE_REMOVE; +} + +// WebOSMain wants a delegate. WAM's has a single slot; nothing here needs it. +class SampleMainDelegate : public webos::WebOSMainDelegate { + public: + void AboutToCreateContentBrowserClient() override {} +}; + +bool IsBrowserProcess(int argc, char** argv) { + for (int i = 1; i < argc; ++i) + if (strncmp(argv[i], "--type=", 7) == 0) return false; + return true; +} + +} // namespace + +int main(int argc, char** argv) { + const bool browser = IsBrowserProcess(argc, argv); + // SAM points a launched app's stdout at /dev/null. + if (browser) { + freopen("/tmp/" APP_LOG_NAME ".log", "w", stdout); + dup2(1, 2); + } + setvbuf(stdout, NULL, _IOLBF, 0); + + const char* slash = strrchr(argv[0], '/'); + g_app_path = slash ? std::string(argv[0], slash - argv[0]) : std::string("."); + + std::vector args; + args.push_back(argv[0]); + if (browser) { + args.push_back("--ozone-platform=wayland"); + args.push_back("--no-sandbox"); + args.push_back("--no-zygote"); + args.push_back("--in-process-gpu"); + args.push_back(std::string("--browser-subprocess-path=") + argv[0]); + args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); + args.push_back("--enable-logging=stderr"); + } + for (int i = 1; i < argc; ++i) args.push_back(argv[i]); + + std::vector cargv; + for (size_t i = 0; i < args.size(); ++i) cargv.push_back(args[i].c_str()); + + if (!getenv("XDG_RUNTIME_DIR")) setenv("XDG_RUNTIME_DIR", "/tmp/xdg", 1); + if (browser) g_idle_add(CreateWebApp, NULL); + + static SampleMainDelegate delegate; + webos::WebOSMain main_runner(&delegate); + return main_runner.Run(static_cast(cargv.size()), cargv.data()); +} diff --git a/web/libcbe/webos6/cbe_stub.cpp b/web/libcbe/webos6/cbe_stub.cpp new file mode 100644 index 0000000..61e1e23 --- /dev/null +++ b/web/libcbe/webos6/cbe_stub.cpp @@ -0,0 +1,45 @@ +// Link-time stand-in for webOS 6+'s /usr/lib/libcbe.so. Same idea as the webOS 4 +// and webOS 3 stubs: right SONAME, right mangled names, never installed. +#include "webos/webapp_window_base.h" +#include "webos/webview_base.h" + +#define STUB(...) \ + { return __VA_ARGS__; } + +namespace webos { + +WebOSMain::WebOSMain(WebOSMainDelegate*) : main_(0) STUB() +int WebOSMain::Run(int, const char**) STUB(0) + +WebViewBase::WebViewBase(bool, int, int) : webview_(0) STUB() +WebViewBase::~WebViewBase() STUB() +void WebViewBase::Initialize(const std::string&, const std::string&, const std::string&, + const std::string&, const std::string&, int, int, bool, + WebViewMode, bool) STUB() +void* WebViewBase::GetWebContents() STUB(0) +void WebViewBase::LoadUrl(const std::string&) STUB() +void WebViewBase::StopLoading() STUB() +void WebViewBase::Reload() STUB() +std::string WebViewBase::GetUrl() STUB(std::string()) +void WebViewBase::RunJavaScript(const std::string&) STUB() +void WebViewBase::UpdatePreferences() STUB() +void WebViewBase::SetAppId(const std::string&) STUB() +void WebViewBase::SetTrustLevel(const std::string&) STUB() +void WebViewBase::SetUserAgent(const std::string&) STUB() +void WebViewBase::SetVisible(bool) STUB() +void WebViewBase::SetAllowLocalResourceLoad(bool) STUB() +void WebViewBase::SetAllowUniversalAccessFromFileUrls(bool) STUB() +void WebViewBase::SetFileAccessBlocked(bool) STUB() +void WebViewBase::SetLocalStorageEnabled(bool) STUB() +void WebViewBase::SetVisibilityState(WebPageVisibilityState) STUB() + +WebAppWindowBase::WebAppWindowBase() : webapp_window_(0) STUB() +WebAppWindowBase::~WebAppWindowBase() STUB() +void WebAppWindowBase::InitWindow(int, int) STUB() +void WebAppWindowBase::Show() STUB() +void WebAppWindowBase::Activate() STUB() +void WebAppWindowBase::AttachWebContents(void*) STUB() +void WebAppWindowBase::SetWindowHostState(NativeWindowState) STUB() +void WebAppWindowBase::SetWindowProperty(const std::string&, const std::string&) STUB() + +} // namespace webos diff --git a/web/libcbe/webos6/webos/webapp_window_base.h b/web/libcbe/webos6/webos/webapp_window_base.h new file mode 100644 index 0000000..da3f963 --- /dev/null +++ b/web/libcbe/webos6/webos/webapp_window_base.h @@ -0,0 +1,83 @@ +// webos::WebAppWindowBase on webOS 6 and newer. +// +// Thirteen virtual slots, and a hierarchy that gives away where LG was heading: +// the vtable mixes webos:: methods with neva_app_runtime::WebAppWindowDelegate +// ones, so on this generation the old API sits on top of the new runtime rather +// than beside it. +// +// Slot order from WebAppWaylandWindow's vtable in a 65UP7560's libWebAppMgr.so: +// +// 0,1 destructor +// 2 OnWindowClosing (webos::WebAppWindowBase) +// 3 CursorVisibilityChanged (neva_app_runtime::WebAppWindowDelegate) +// 4 event(AppRuntimeEvent*) (neva_app_runtime::WebAppWindowDelegate) +// 5 WebAppWindowDestroyed (webos::WebAppWindowBase) +// 6 event(WebOSEvent*) +// 7 CheckKeyFilterTable +// 8,9 not recoverable +// 10 OnCreatedMediaLayer +// 11 WillDestroyAllMediaLayers +// 12 ResizedSwapBuffer +#pragma once + +#include + +class WebOSEvent; + +namespace neva_app_runtime { +class AppRuntimeEvent; +} + +namespace webos { + +enum NativeWindowState { + NATIVE_WINDOW_DEFAULT = 0, + NATIVE_WINDOW_MINIMIZED, + NATIVE_WINDOW_MAXIMIZED, + NATIVE_WINDOW_FULLSCREEN, +}; + +enum CustomCursorType { CUSTOM_CURSOR_NOT_USE, CUSTOM_CURSOR_BLANK, CUSTOM_CURSOR_PATH }; + +typedef unsigned WebOSKeyMask; + +class WebAppWindowBase { + public: + WebAppWindowBase(); + virtual ~WebAppWindowBase(); + + virtual void OnWindowClosing() {} + virtual void CursorVisibilityChanged(bool visible) { (void)visible; } + virtual void event(neva_app_runtime::AppRuntimeEvent* e) { (void)e; } + virtual void WebAppWindowDestroyed() {} + virtual bool event(WebOSEvent* e) { (void)e; return false; } + virtual unsigned CheckKeyFilterTable(unsigned keycode, unsigned* modifier) { + (void)keycode; + (void)modifier; + return 0; + } + virtual void Unknown8() {} + virtual void Unknown9() {} + virtual void OnCreatedMediaLayer(const std::string& a, const std::string& b, unsigned c) { + (void)a; + (void)b; + (void)c; + } + virtual void WillDestroyAllMediaLayers() {} + virtual void ResizedSwapBuffer() {} + + // Non-virtual, and InitWindow is back after webOS 3 did without it. + void InitWindow(int width, int height); + void Show(); + void Activate(); + void AttachWebContents(void* web_contents); + void SetWindowHostState(NativeWindowState state); + void SetWindowProperty(const std::string& name, const std::string& value); + + private: + // 8 bytes, unlike WebViewBase next door: libcbe's constructor touches only + // offsets 0 and 4. + void* webapp_window_; // WebAppWindow*, owned by libcbe +}; + +} // namespace webos diff --git a/web/libcbe/webos6/webos/webview_base.h b/web/libcbe/webos6/webos/webview_base.h new file mode 100644 index 0000000..ba182f1 --- /dev/null +++ b/web/libcbe/webos6/webos/webview_base.h @@ -0,0 +1,236 @@ +// webos::WebViewBase as it exists on webOS 6 and newer. +// +// A third shape of the same API - see ../../webos/webview_base.h for webOS 4 and +// ../../webos3/webos/webview_base.h for webOS 3. What changed here: +// +// * the entry point is a class, webos::WebOSMain(delegate)->Run(argc, argv), +// not the free WebOSMain() of webOS 3 and 4; +// * WebViewBase takes (bool, int, int) and Initialize takes five strings and a +// bool, where webOS 4 took five strings, two ints and a bool; +// * the delegate is 61 slots rather than 24, and its order is different again. +// +// Measured on a 65UP7560 running starfish 6.5.2 (Chromium 79). The slot order +// below is BlinkWebView's vtable inside that set's libWebAppMgr.so - WAM still +// subclasses the webos:: API on this generation, so it remains the reference +// implementation. Slots it fills with local functions are named UnknownNN: they +// must exist and keep their position, but their signatures are not recoverable. +// They are declared void and argument-less deliberately - the callee ignores +// arguments, and on AAPCS that is safe. +// +// Names come from the vtable itself; upstream's neva_app_runtime headers +// (webosose/chromium87, src/neva/app_runtime/public) describe a related but +// *different* interface - 32 virtuals against these 61 - so they are a naming +// reference, not the layout. +#pragma once + +#include +#include + +class WebOSEvent; + +namespace webos { + +class WebViewProfile; + +class WebViewDelegate { + public: + // slot 0 + virtual void OnLoadProgressChanged(double progress) { (void)progress; } + // slot 1 + virtual void DidFirstFrameFocused() {} + // slot 2 + virtual void TitleChanged(const std::string& title) { (void)title; } + // slot 3 + virtual void NavigationHistoryChanged() {} + // slot 4 + virtual void Close() {} + // slot 5 + virtual bool DecidePolicyForResponse(bool is_main_frame, int status_code, + const std::string& url, + const std::string& status_text) { (void)is_main_frame; (void)status_code; (void)url; (void)status_text; return false; } + // slot 6 + virtual bool AcceptsVideoCapture() { return false; } + // slot 7 + virtual bool AcceptsAudioCapture() { return false; } + // slot 8 + virtual void LoadStarted() {} + // slot 9 + virtual void LoadFinished(const std::string& url) { (void)url; } + // slot 10 + virtual void LoadFailed(const std::string& url, int err_code, + const std::string& err_desc) { (void)url; (void)err_code; (void)err_desc; } + // slot 11 + virtual void LoadAborted(const std::string& url) { (void)url; } + // slot 12 - name not recoverable; WAM implements it with a local function. + virtual void Unknown12() {} + // slot 13 + virtual void RenderProcessCreated(int pid) { (void)pid; } + // slot 14 + virtual void RenderProcessGone(bool crashed) { (void)crashed; } + // slot 15 + virtual void DocumentLoadFinished() {} + // slot 16 + virtual void DidStartNavigation(const std::string& url, bool is_main_frame) { (void)url; (void)is_main_frame; } + // slot 17 + virtual void DidFinishNavigation(const std::string& url, bool is_main_frame) { (void)url; (void)is_main_frame; } + // slot 18 - name not recoverable; WAM implements it with a local function. + virtual void Unknown18() {} + // slot 19 + virtual void DidClearWindowObject() {} + // slot 20 + virtual void DidSwapCompositorFrame() {} + // slot 21 + virtual bool AllowMouseOnOffEvent() const { return false; } + // slot 22 - name not recoverable; WAM implements it with a local function. + virtual void Unknown22() {} + // slot 23 + virtual void DidLoadingEnd() {} + // slot 24 - name not recoverable; WAM implements it with a local function. + virtual void Unknown24() {} + // slot 25 - name not recoverable; WAM implements it with a local function. + virtual void Unknown25() {} + // slot 26 - name not recoverable; WAM implements it with a local function. + virtual void Unknown26() {} + // slot 27 + virtual void DidFirstMeaningfulPaint() {} + // slot 28 + virtual void DidNonFirstMeaningfulPaint() {} + // slot 29 + virtual void ErrorPageStateChanged(bool enable) { (void)enable; } + // slot 30 - name not recoverable; WAM implements it with a local function. + virtual void Unknown30() {} + // slot 31 + virtual void SkipBeginMainFrameAck() {} + // slot 32 + virtual void CreatePlugin() {} + // slot 33 - name not recoverable; WAM implements it with a local function. + virtual void Unknown33() {} + // slot 34 + virtual void MediaAboutToPlayNotify(const std::string& id, bool audio) { (void)id; (void)audio; } + // slot 35 - name not recoverable; WAM implements it with a local function. + virtual void Unknown35() {} + // slot 36 - name not recoverable; WAM implements it with a local function. + virtual void Unknown36() {} + // slot 37 + virtual void RequestLaunchFullBrowser(const std::string& url) { (void)url; } + // slot 38 + virtual bool CanDownload(std::string& url) { (void)url; return false; } + // slot 39 - name not recoverable; WAM implements it with a local function. + virtual void Unknown39() {} + // slot 40 + virtual void LoadStopped(const std::string& url) { (void)url; } + // slot 41 - name not recoverable; WAM implements it with a local function. + virtual void Unknown41() {} + // slot 42 - name not recoverable; WAM implements it with a local function. + virtual void Unknown42() {} + // slot 43 - name not recoverable; WAM implements it with a local function. + virtual void Unknown43() {} + // slot 44 + virtual void NotifyFault(const std::string& a, int b, const std::string& c) { (void)a; (void)b; (void)c; } + // slot 45 + virtual void HandleKeyboardEvent(int key) { (void)key; } + // slot 46 + virtual void RequestMediaLayer(const std::string& id, unsigned type) { (void)id; (void)type; } + // slot 47 - name not recoverable; WAM implements it with a local function. + virtual void Unknown47() {} + // slot 48 - name not recoverable; WAM implements it with a local function. + virtual void Unknown48() {} + // slot 49 + virtual void SetMediaProperty(const std::string& a, const std::string& b, + const std::string& c) { (void)a; (void)b; (void)c; } + // slot 50 + virtual void DestroyMediaLayer(const std::string& id) { (void)id; } + // slot 51 - name not recoverable; WAM implements it with a local function. + virtual void Unknown51() {} + // slot 52 - name not recoverable; WAM implements it with a local function. + virtual void Unknown52() {} + // slot 53 - name not recoverable; WAM implements it with a local function. + virtual void Unknown53() {} + // slot 54 + virtual void HandleBrowserControlCommand(const std::string& command, + const std::vector& args) { (void)command; (void)args; } + // slot 55 + virtual void HandleBrowserControlFunction(const std::string& command, + const std::vector& args, + std::string* result) { (void)command; (void)args; (void)result; } + // slot 56 + virtual void LoadVisuallyCommitted() {} + // slot 57 - name not recoverable; WAM implements it with a local function. + virtual void Unknown57() {} + // slot 58 - name not recoverable; WAM implements it with a local function. + virtual void Unknown58() {} + // slot 59 - name not recoverable; WAM implements it with a local function. + virtual void Unknown59() {} + // slot 60 + virtual void DidDropAllPeerConnections(int reason) { (void)reason; } +}; + +class WebViewBase : public WebViewDelegate { + public: + enum WebViewMode { WEBVIEW_MODE_NORMAL }; + enum WebPageVisibilityState { VISIBILITY_VISIBLE, VISIBILITY_HIDDEN, VISIBILITY_LAUNCHING }; + + WebViewBase(bool alt_storage, int width, int height); + ~WebViewBase(); + + // Two overloads exist on this generation. WAM calls this one - the long form, + // carrying width/height, a WebViewMode and two bools - at every one of its + // three call sites, and the short (five strings + bool) form segfaults inside + // libcbe when called from here. Use this one. + void Initialize(const std::string& app_id, + const std::string& app_path, + const std::string& trust_level, + const std::string& v8_snapshot_path, + const std::string& v8_extra_flags, + int width, + int height, + bool use_native_scroll, + WebViewMode mode, + bool inspectable); + + void* GetWebContents(); + void LoadUrl(const std::string& url); + void StopLoading(); + void Reload(); + std::string GetUrl(); + void RunJavaScript(const std::string& js); + void UpdatePreferences(); + + void SetAppId(const std::string& app_id); + void SetTrustLevel(const std::string& trust_level); + void SetUserAgent(const std::string& user_agent); + void SetVisible(bool visible); + void SetAllowLocalResourceLoad(bool allow); + void SetAllowUniversalAccessFromFileUrls(bool allow); + void SetFileAccessBlocked(bool blocked); + void SetLocalStorageEnabled(bool enabled); + void SetVisibilityState(WebPageVisibilityState state); + + private: + // 92 bytes total, and the size is load-bearing: libcbe's constructor writes as + // far as offset 90, so a subclass declared any smaller corrupts the heap and + // the process dies later inside malloc, nowhere near the cause. The layout is + // opaque; only the size matters here. (webOS 4's WebViewBase was 8 bytes, + // which is why this needed measuring rather than assuming.) + void* webview_; // WebView*, owned by libcbe + char reserved_[84]; // to sizeof == 92 +}; + +// The entry point on this generation. Run() is Chromium's content main: it takes +// the process over and does not return. +class WebOSMainDelegate { + public: + // One slot, per WAM's WebOSMainDelegateWAM vtable. + virtual void AboutToCreateContentBrowserClient() = 0; +}; + +class WebOSMain { + public: + explicit WebOSMain(WebOSMainDelegate* delegate); + int Run(int argc, const char** argv); + + private: + void* main_; +}; + +} // namespace webos From 33d627d7aabbd5472e7ab50820e12ecedf397eac Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 21:31:02 +0900 Subject: [PATCH 39/40] web/cbe-webos6: working - a stubbed slot with a return value was the bug The page loads and renders full-screen on a 65UP7560 at starfish 6.5.2, and the app takes the foreground. The fault was one slot out of 61. Most delegate slots return void, so a placeholder with an empty body is harmless - the caller ignores r0. Slot 53 is GetWebContents(), it is virtual, and libcbe calls it through the vtable during Initialize(). Declared as a void placeholder it handed libcbe whatever was in r0 as a WebContents*, and the process died inside Initialize with a backtrace pointing at libcbe rather than at the mistake. It is now pure in the delegate and overridden in WebViewBase with no body, so the slot resolves to libcbe's own _ZN5webos11WebViewBase14GetWebContentsEv at link time and the app inherits the real implementation instead of shadowing it. Finding it needed a slot tracer: every one of the 61 overrides replaced by a body that prints its index and touches no argument. Exactly one line came out, "[slot] 53", which named it at once - reading arguments had only ever produced garbage. That technique is written up, because it generalises to any of these reconstructed vtables: a slot that returns a pointer cannot be stubbed, and a tracer finds it in a single run. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- README.md | 2 +- web/cbe-webos6/README.md | 35 ++++++++++++++++++-------- web/cbe-webos6/main.cpp | 5 +++- web/libcbe/webos6/webos/webview_base.h | 10 +++++--- 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 77575a7..4d9450d 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ hard to diagnose from the TV side. | `media/ndl/directmedia` (v2) | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): 300 video + 469 PCM chunks on both | | `media/ndl/directmedia` (v1) | 3.5 - 4.x | built and symbol-verified, needs a 2017-2019 set to test | | `web/hybrid` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the sign-in flow runs end to end - native panel, web login form, and back with the username read out of the intercepted redirect URL. The OK key is verified too, injected with `com.webos.service.networkinput/test/sendKeyCode`; leaving the web view by remote is not, since keys go to whichever window is up | -| `web/cbe-webos6` | webOS 6 | **unfinished** - 65UP7560, starfish 6.5.2: the ABI is mapped (61-slot delegate, 92-byte `WebViewBase`) and the sample builds, verifies and starts, but the delegate arrives with mis-decoded arguments and the process dies. See `web/cbe-webos6/README.md` | +| `web/cbe-webos6` | webOS 6 | **verified on hardware** - 65UP7560, starfish 6.5.2: the page loads and renders full-screen and the app takes the foreground. A third shape of the API - 61-slot delegate, 92-byte `WebViewBase`, `WebOSMain` as a class | | `web/cbe-webos3` | webOS 3 | **unfinished, parked** - 43UH6100, starfish 3.4.0: the reconstructed webOS 3 ABI is right and the page loads and paints, with every delegate callback firing, but the window never reaches the screen - LSM keeps the previous app foreground. webOS 3 has no `Activate()` and the equivalent has not been found | | `web/cbe` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | | `media/smp/webos1` | 1.x | not written yet - and there is no webOS 1 hardware here to validate it against, so it would ship untestable | diff --git a/web/cbe-webos6/README.md b/web/cbe-webos6/README.md index dd14237..db12bed 100644 --- a/web/cbe-webos6/README.md +++ b/web/cbe-webos6/README.md @@ -1,10 +1,7 @@ # web/cbe, on webOS 6 and newer -> **Status: unfinished.** The ABI is mapped and the sample builds, verifies clean and -> starts - it reaches libcbe's delegate - but the delegate arrives with mis-decoded -> arguments and the process then dies. The slot layout below is right in outline and wrong -> in some detail. Everything established so far is written down here; `web/cbe` on webOS 4 -> is the one that works. +**Verified on a 65UP7560 (starfish 6.5.2):** the page loads and renders full-screen, and +the app becomes the foreground app. Measured on a **65UP7560 running starfish 6.5.2** (Chromium 79). @@ -68,10 +65,26 @@ It is the API with public upstream headers (`webosose/chromium87`, That is why this sample stays on `webos::`. -## What is still wrong +## The slot that has a return value -The delegate is reached - `DidStartNavigation` fires - but its `const std::string&` arrives -as garbage, so a slot is misaligned or a signature is wrong somewhere in the first 17. The -base `webos::WebViewBase` vtable counts 51 relocated slots against `BlinkWebView`'s 61, -which is the obvious place to start: confirm the 51 really are a prefix of the 61, and that -no thunk entries shift the numbering. +Most delegate slots return `void`, so a placeholder with an empty body is harmless: the +caller ignores whatever is in `r0`. **Slot 53 is not one of them.** It is +`GetWebContents()`, it is virtual, and libcbe calls it through the vtable *during* +`Initialize()`. Declared as a `void` placeholder, it hands libcbe whatever happened to be in +`r0` as a `WebContents*`, and the process dies inside `Initialize` with a backtrace that +points at libcbe rather than at the mistake. + +It is declared pure in the delegate and overridden in `WebViewBase` **with no body**, so the +slot resolves to libcbe's own `_ZN5webos11WebViewBase14GetWebContentsEv` at link time - the +app inherits the real implementation instead of shadowing it. + +Finding it took a slot tracer: replacing every one of the 61 overrides with a body that +prints its own index and touches no argument. Exactly one line came out - + +``` +[slot] 53 +``` + +- which named the culprit immediately, where reading arguments had only produced garbage. +The same trick is worth reaching for on any of these reconstructed vtables: **a slot that +returns a pointer cannot be stubbed, and a tracer finds it in one run.** diff --git a/web/cbe-webos6/main.cpp b/web/cbe-webos6/main.cpp index 999cd0f..3264d6d 100644 --- a/web/cbe-webos6/main.cpp +++ b/web/cbe-webos6/main.cpp @@ -76,7 +76,10 @@ gboolean CreateWebApp(gpointer) { g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); g_webview = new SampleWebView(1920, 1080); - g_webview->Initialize(kAppId, g_app_path, "default", "", "", 1920, 1080, false, + // WAM passes the same register for all five trailing arguments, so they are + // zeros there - width and height included. The size comes from the + // constructor on this generation, not from here. + g_webview->Initialize(kAppId, g_app_path, "default", "", "", 0, 0, false, webos::WebViewBase::WEBVIEW_MODE_NORMAL, false); g_webview->SetAppId(kAppId); g_webview->SetAllowLocalResourceLoad(true); diff --git a/web/libcbe/webos6/webos/webview_base.h b/web/libcbe/webos6/webos/webview_base.h index ba182f1..700cf5b 100644 --- a/web/libcbe/webos6/webos/webview_base.h +++ b/web/libcbe/webos6/webos/webview_base.h @@ -144,8 +144,12 @@ class WebViewDelegate { virtual void Unknown51() {} // slot 52 - name not recoverable; WAM implements it with a local function. virtual void Unknown52() {} - // slot 53 - name not recoverable; WAM implements it with a local function. - virtual void Unknown53() {} + // slot 53 is GetWebContents(), and it is virtual - libcbe calls it through the + // vtable during Initialize(). Declaring it void, as a placeholder, hands + // libcbe whatever was in r0 as a WebContents pointer and the process dies + // inside Initialize. It is pure here and overridden in WebViewBase below with + // no body, so the slot resolves to libcbe's own implementation at link time. + virtual void* GetWebContents() = 0; // slot 54 virtual void HandleBrowserControlCommand(const std::string& command, const std::vector& args) { (void)command; (void)args; } @@ -188,7 +192,7 @@ class WebViewBase : public WebViewDelegate { WebViewMode mode, bool inspectable); - void* GetWebContents(); + void* GetWebContents() override; // implemented by libcbe, not by us void LoadUrl(const std::string& url); void StopLoading(); void Reload(); From fa9cb09058df59a33c51a79dcbfbb6fe794e9ad9 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Mon, 24 Aug 2026 21:40:50 +0900 Subject: [PATCH 40/40] web/neva: the published API, working on webOS 6 Same web view as web/cbe-webos6 through neva_app_runtime, the other embedding API in the same libcbe - and the only one under web/ that starts from published source rather than from a vtable (webosose/chromium87, src/neva/app_runtime/public). Verified on a 65UP7560 at starfish 6.5.2: the page loads and renders full-screen and the app takes the foreground. -verify is clean from 6.4 to 11.2. It is the smaller API by some margin: AppRuntimeMain(argc, argv) rather than a WebOSMain class, no Initialize at all, 40 delegate slots against 61, and a 16-byte WebViewBase against 92. The published headers still needed checking, because the TV is Chromium 79 - between the public chromium68 and chromium87 trees - and its vtable has 40 slots where those declare 26 and 32. What settles the order without depending on names is which slots are pure virtual: libcbe's vtable relocates a pure slot to __cxa_pure_virtual, and that pattern is 0-15, 18, 19, 20, 21, 29 against upstream's 0-15, 18, 19, 20, 21 - identical through slot 28. So 0-28 are upstream's in upstream's order, and 29-39 are LG additions left as placeholders. Two traps recorded: AppRuntimeMain is C++-mangled rather than extern "C", which -verify catches; and the object sizes are load-bearing the same way they are on the webos:: side, 16 and 8 bytes here. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYYTWYi9WkpdpJ5EpwZD9i --- CMakeLists.txt | 1 + README.md | 2 + assets/icons/web-neva.png | Bin 0 -> 2800 bytes assets/make-icon.py | 1 + web/libcbe/neva/cbe_stub.cpp | 30 ++++ .../neva_app_runtime/webapp_window_base.h | 50 ++++++ .../neva/neva_app_runtime/webview_base.h | 150 ++++++++++++++++++ web/neva/CMakeLists.txt | 34 ++++ web/neva/README.md | 54 +++++++ web/neva/main.cpp | 113 +++++++++++++ 10 files changed, 435 insertions(+) create mode 100644 assets/icons/web-neva.png create mode 100644 web/libcbe/neva/cbe_stub.cpp create mode 100644 web/libcbe/neva/neva_app_runtime/webapp_window_base.h create mode 100644 web/libcbe/neva/neva_app_runtime/webview_base.h create mode 100644 web/neva/CMakeLists.txt create mode 100644 web/neva/README.md create mode 100644 web/neva/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 892e183..28ae18a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,5 +37,6 @@ if (TARGET_WEBOS) add_subdirectory(web/cbe) add_subdirectory(web/cbe-webos3) add_subdirectory(web/cbe-webos6) + add_subdirectory(web/neva) add_subdirectory(web/hybrid) endif () diff --git a/README.md b/README.md index 4d9450d..535b76b 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ web/ cbe/ the smallest thing that puts a web page on screen cbe-webos3/ the same, against webOS 3's older and different libcbe cbe-webos6/ and again for webOS 6+, where the API changes a third time + neva/ the same, through neva_app_runtime - the API LG published hybrid/ a native sign-in flow: SDL2 + Nuklear, a web login page, one process ``` @@ -201,6 +202,7 @@ hard to diagnose from the TV side. | `media/ndl/directmedia` (v2) | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): 300 video + 469 PCM chunks on both | | `media/ndl/directmedia` (v1) | 3.5 - 4.x | built and symbol-verified, needs a 2017-2019 set to test | | `web/hybrid` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the sign-in flow runs end to end - native panel, web login form, and back with the username read out of the intercepted redirect URL. The OK key is verified too, injected with `com.webos.service.networkinput/test/sendKeyCode`; leaving the web view by remote is not, since keys go to whichever window is up | +| `web/neva` | webOS 6+ | **verified on hardware** - 65UP7560, starfish 6.5.2: page loads and renders full-screen, app takes the foreground. The one API here with published upstream headers, and `-verify` clean to 11.2 | | `web/cbe-webos6` | webOS 6 | **verified on hardware** - 65UP7560, starfish 6.5.2: the page loads and renders full-screen and the app takes the foreground. A third shape of the API - 61-slot delegate, 92-byte `WebViewBase`, `WebOSMain` as a class | | `web/cbe-webos3` | webOS 3 | **unfinished, parked** - 43UH6100, starfish 3.4.0: the reconstructed webOS 3 ABI is right and the page loads and paints, with every delegate callback firing, but the window never reaches the screen - LSM keeps the previous app foreground. webOS 3 has no `Activate()` and the equivalent has not been found | | `web/cbe` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | diff --git a/assets/icons/web-neva.png b/assets/icons/web-neva.png new file mode 100644 index 0000000000000000000000000000000000000000..9a5568622703223e6da741b49551c6a6cc1f87be GIT binary patch literal 2800 zcmV*(P)njB*)A89juhMCfd(3Apn?5_RZEa0%gK|ODM>Ho=>>V(znobm$J0(`fs8I?n^|R_ z3)VmeS87$!ZGnvw(6GmxwL{bAZqK z1#mU+I8XvAE_*bv7-#`n&FpfQJpfn=9PW7^HM48t`*ye7RfD7^zfV(pEJ}FBNUAvf zw+Vfzy2cTbrX=wt@tKmEJ^ZyKeZJF7(s?;q+x+2*88{j|&23fv6fmN_qxE#OdK zw(~T({N6`mfoJ)bE=zjX!*_YwI7wSlD{}fxiG>_nAiL!EZzX&eCFv6-?Q;5^3H^PN zwklZtoswET{7@>l8mO9}oXp#RH-I^kCK0T!sf#=n;0j6Km2{2YFHX{b1H4PHq>B>z z1%$Q_U`bLn=K_NW*8C&jJ`bNR>3DCmd!p?7jsgpmq>Eh%l5~%x!IHLhVS&5U5v!di zwsrTr6>XC=)@d&|?KOo?Bg%cu=^G?9OFA==dvz+eyXdK}Fc7!_*an;ndwGbNX#&w#UpK0iJU?7rOFd&)+RnvlB@% z(9E_e%&MZu{YEEMc)r3=VuYkOoVMHVOOw25Co&m8i#{sdR8_etl&wBzenqY}}%QV8&Y)iBfe@@H;@Q#_SNZw@%a0tPo|C%eO2t%96 z9^PPPFDh$0>k~a)MS8mmR(m-swpEparL1(xsZu~MLM3k<`8;Vanx+-?uDN$?dyfVh zXrO@x_7A-~i0lIS-nCwb8g->W??p?59`x9DWoGNd|dzyP56IrPakQ zTdyln$%=@=s`OmKoOV3fymlS126zm(+svNL=k1e7=F#IMEh?A`zvx#UcS<@bmC@&* zY>8@)q__?N;mJd$QO?J|!yC(6Fe(`I;ltfZAr`${VNpoOH8!n)oV zNpDHoEa?ND*5vJAMWIbJ=Z|^DdD^1nBaD)?R$)`>Wam40m6#NKv0KhOkGHs@)=66B zG)wZuCHqy*Q;Jnw(#Kuaa*x-?c4f7pY?J&L?*LYKJSt+WD`J_)o0Gh;8ZUF6xUJdG z?{_)|@lsM9V|cyCM<;0p`~%oR*f;_xt42%pHL*Z1OxX)P30P}p>&&cT zX7BoxY`C+par!R5w}lC_5rE-N-|6@5zzSfzg7Z<$79MJ1>T197^cukepZ4d+4WFZd zafB_+cRCR@2sjyd-Q~phuK*K(k-!8qds#K|1FZGnvqUKDU|pXKkJ|f|X;knLpZ9+fN!;ca^&YR9tlWJTO z3-sz+a!-I4fHQ!SJ4(jt{fe_6YGzxD!X* zmHs{y+34wU7SiJPwlJZM1lZ{G{yu~G2%-J^i+_yfT6H9~RA6k-@ACVl-pH4|u(5}o z=yAD!O87`Ad?{a@xVWm;mjp z@DeY0tE4ebo8z=L&#A=HBL5tZ50dnN)4rF=KIoAsWq~W;RY}vGwp!VWOjDxpRg%_v z_|(Mm(tk&KeUGPRNEGu`Ww=?{QrDFU@0CtlDCs8&f3&<(pHf~#qQp}rJ*bX&AgQJ) z{-$hYx^F5kLhXH_KPKsC3O(vuiNZEZde*ITW>MbWLS1}0geIv6$3Km!f*Rn%=K0_}cFNcTQEoZQhs0qSkI z(i!UA0!I>d;f53bJzfp`Gxb(2$YX>Jst#_#2LzNY!`pk&^E9=0DEhvnHw)&$cSqjZ zmh1q4vW3!H5`}G(^oXS8uFzLI`9Lx62@1yzoSV`Q7)ToG*WeueaHsc6X#EoYflfQm z6}Lyya|(ZFM*fisKW2AmAC;{!Ug-HdI4RXN5(~Ib(k2DR$)lWhtfc=q{f9}O1y0)} zX`0g>R9Lz%D|5VTDSD=-#cJQr?{gIPNPeE9_lPA{Q)ra+3}F@JD8k_n0H+f+(1!x& zB-QV1r9i8<`H=;fi>g;O$T$RQ)3f4od5Rm&hEC)=}>p?#g3qS1le!you zyu!@h237&503UY#zq^BEKX2&oC0I-um}X|XedaNh>}0qn+a8$fg$W1z2xw6adf}=< zSvz=$!Z0Rcx1{5}u+buS`2AAP6P<~MDGZjS@cY6Yl7639 + +namespace neva_app_runtime { + +enum WidgetState { + UNINITIALIZED = 0, + CREATED, + SHOW, + HIDE, + FULLSCREEN, + MAXIMIZED, + MINIMIZED, + RESTORE, + ACTIVE, + INACTIVE, + RESIZE, + DESTROYED, +}; + +class AppRuntimeEvent; + +class WebAppWindowBase { + public: + WebAppWindowBase(); + virtual ~WebAppWindowBase(); + + virtual void OnWindowClosing() {} + virtual void CursorVisibilityChanged(bool visible) { (void)visible; } + virtual bool event(AppRuntimeEvent* e) { (void)e; return false; } + + // No InitWindow() on this API - Resize gives the window its size. + void Resize(int width, int height); + void SetBounds(int x, int y, int width, int height); + void Show(); + void Hide(); + void Activate(); + void AttachWebContents(void* web_contents); + void SetWindowHostState(WidgetState state); + void SetWindowProperty(const std::string& name, const std::string& value); + + private: + // libcbe's constructor writes as far as offset 4. + void* webapp_window_; +}; + +} // namespace neva_app_runtime diff --git a/web/libcbe/neva/neva_app_runtime/webview_base.h b/web/libcbe/neva/neva_app_runtime/webview_base.h new file mode 100644 index 0000000..749f5b3 --- /dev/null +++ b/web/libcbe/neva/neva_app_runtime/webview_base.h @@ -0,0 +1,150 @@ +// neva_app_runtime - the API LG introduced alongside webos:: on webOS 6. +// +// Unlike every other header under web/libcbe, this one starts from *published* +// source: webosose/chromium87, src/neva/app_runtime/public/webview_delegate.h. +// It is still not a drop-in, because the TV is Chromium 79 - between the public +// chromium68 and chromium87 trees - and its vtable has 40 slots where +// chromium87 declares 32 virtuals and chromium68 declares 26. +// +// What makes the upstream order trustworthy anyway is a cross-check that does +// not depend on names at all. libcbe's own vtable says which slots are pure +// virtual (they relocate to __cxa_pure_virtual) and which carry a default +// implementation, and that pattern is: +// +// firmware pure at: 0-15, 18, 19, 20, 21, 29 +// upstream pure at: 0-15, 18, 19, 20, 21 +// +// Identical through slot 28. So slots 0-28 are upstream's, in upstream's order, +// and 29-39 are LG additions this header leaves as placeholders. +// +// Measured against a 65UP7560 running starfish 6.5.2. +#pragma once + +#include +#include + +namespace neva_app_runtime { + +class WebViewProfile; + +enum DropPeerConnectionReason { + DROP_PEER_CONNECTION_REASON_UNKNOWN, + DROP_PEER_CONNECTION_REASON_PAGE_HIDDEN, + DROP_PEER_CONNECTION_REASON_MULTIMEDIA_PLAYING, +}; + +class WebViewDelegate { + public: + // slot 0 + virtual void OnLoadProgressChanged(double progress) {} + // slot 1 + virtual void DidFirstFrameFocused() {} + // slot 2 + virtual void TitleChanged(const std::string& title) {} + // slot 3 + virtual void NavigationHistoryChanged() {} + // slot 4 + virtual void Close() {} + // slot 5 + virtual bool DecidePolicyForResponse(bool is_main_frame, int status_code, const std::string& url, const std::string& status_text) { return false; } + // slot 6 + virtual bool AcceptsVideoCapture() { return false; } + // slot 7 + virtual bool AcceptsAudioCapture() { return false; } + // slot 8 + virtual void LoadStarted() {} + // slot 9 + virtual void LoadFinished(const std::string& url) {} + // slot 10 + virtual void LoadFailed(const std::string& url, int error_code, const std::string& error_description) {} + // slot 11 + virtual void LoadAborted(const std::string& url) {} + // slot 12 + virtual void LoadStopped() {} + // slot 13 + virtual void RenderProcessCreated(int pid) {} + // slot 14 + virtual void RenderProcessGone() {} + // slot 15 + virtual void DocumentLoadFinished() {} + // slot 16 + virtual void DidStartNavigation(const std::string& url, bool is_main_frame) {} + // slot 17 + virtual void DidFinishNavigation(const std::string& url, bool is_main_frame) {} + // slot 18 + virtual void DidHistoryBackOnTopPage() {} + // slot 19 + virtual void DidClearWindowObject() {} + // slot 20 + virtual void DidSwapCompositorFrame() {} + // slot 21 + virtual void DidErrorPageLoadedFromNetErrorHelper() {} + // slot 22 + virtual void DidLoadingEnd() {} + // slot 23 + virtual void DidFirstPaint() {} + // slot 24 + virtual void DidFirstContentfulPaint() {} + // slot 25 + virtual void DidFirstImagePaint() {} + // slot 26 + virtual void DidFirstMeaningfulPaint() {} + // slot 27 + virtual void DidNonFirstMeaningfulPaint() {} + // slot 28 + virtual void DidLargestContentfulPaint() {} + // slot 29 + virtual void DidDropAllPeerConnections(neva_app_runtime::DropPeerConnectionReason reason) {} + // slot 30 + virtual void DidResumeDOM() {} + // slot 31 + virtual void SendCookiesForHostname(const std::string& cookies) {} + // slot 32 - beyond what upstream declares; LG additions. + virtual void Unknown32() {} + // slot 33 - beyond what upstream declares; LG additions. + virtual void Unknown33() {} + // slot 34 - beyond what upstream declares; LG additions. + virtual void Unknown34() {} + // slot 35 - beyond what upstream declares; LG additions. + virtual void Unknown35() {} + // slot 36 - beyond what upstream declares; LG additions. + virtual void Unknown36() {} + // slot 37 - beyond what upstream declares; LG additions. + virtual void Unknown37() {} + // slot 38 - beyond what upstream declares; LG additions. + virtual void Unknown38() {} + // slot 39 - beyond what upstream declares; LG additions. + virtual void Unknown39() {} +}; + +class WebViewBase : public WebViewDelegate { + public: + // No Initialize() on this API - the constructor takes the size, and a null + // profile means the default one. + WebViewBase(int width, int height, WebViewProfile* profile); + ~WebViewBase(); + + // Virtual, and libcbe implements it - declared with no body here so the slot + // resolves to libcbe's own symbol rather than being shadowed. Stubbing a slot + // that returns a pointer is what breaks these bindings; see + // ../../../cbe-webos6/README.md. + void* GetWebContents(); + void LoadUrl(const std::string& url); + void SetAppId(const std::string& app_id); + void SetVisible(bool visible); + + private: + // libcbe's constructor writes as far as offset 12, so 16 bytes. + void* webview_; + char reserved_[8]; +}; + +} // namespace neva_app_runtime + +// The entry point. A free function on this API, where webos:: has a WebOSMain +// class - and Chromium's content main either way: it does not return. +// +// Deliberately NOT extern "C": libcbe exports it C++-mangled, as +// _Z14AppRuntimeMainiPPKc, so an extern "C" declaration looks for a plain +// "AppRuntimeMain" that does not exist. +int AppRuntimeMain(int argc, const char** argv); diff --git a/web/neva/CMakeLists.txt b/web/neva/CMakeLists.txt new file mode 100644 index 0000000..6e9cb70 --- /dev/null +++ b/web/neva/CMakeLists.txt @@ -0,0 +1,34 @@ +# The same web view as web/cbe-webos6, through neva_app_runtime instead. +# +# Both APIs live in the same libcbe on the same TV. This is the one whose +# headers LG published (webosose/chromium87, src/neva/app_runtime/public), which +# makes it the better starting point of the two - see +# ../libcbe/neva/neva_app_runtime/webview_base.h for how far the published order +# can be trusted and how that was checked. + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GLIB REQUIRED glib-2.0) + +add_library(cbe-stub-neva SHARED ../libcbe/neva/cbe_stub.cpp) +set_target_properties(cbe-stub-neva PROPERTIES OUTPUT_NAME cbe + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/stub") +target_include_directories(cbe-stub-neva PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../libcbe/neva") +target_compile_options(cbe-stub-neva PUBLIC -fno-rtti) + +add_executable(web-neva main.cpp) +target_include_directories(web-neva SYSTEM PRIVATE ${GLIB_INCLUDE_DIRS}) +target_link_directories(web-neva PRIVATE ${GLIB_LIBRARY_DIRS}) +target_link_libraries(web-neva PRIVATE cbe-stub-neva ${GLIB_LIBRARIES}) +target_compile_definitions(web-neva PRIVATE APP_LOG_NAME="web-neva") + +set_target_properties(web-neva PROPERTIES + SKIP_BUILD_RPATH TRUE + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH "") + +webos_add_ipk( + TARGET web-neva + APPID org.webosbrew.sample.web.neva + TITLE "Neva WebView (webOS 6+)" + COLOR "#00897B" + WEBOS_VERSIONS ">=6.4") diff --git a/web/neva/README.md b/web/neva/README.md new file mode 100644 index 0000000..cfa5a7b --- /dev/null +++ b/web/neva/README.md @@ -0,0 +1,54 @@ +# neva_app_runtime + +The same web view as `../cbe-webos6`, through the other API in the same library. + +**Verified on a 65UP7560 (starfish 6.5.2):** the page loads and renders full-screen and the +app takes the foreground. `-verify` is clean from webOS 6.4 to 11.2. + +## Why this one is the better starting point + +webOS 6 ships two embedding APIs in one `libcbe.so`: the `webos::` one WAM still uses, and +`neva_app_runtime`, whose headers LG **published** - `webosose/chromium87`, +`src/neva/app_runtime/public`. That makes this the only sample under `web/` that starts +from source rather than from a vtable. + +It is smaller and simpler, too: + +| | `webos::` (webOS 6) | `neva_app_runtime` | +|---|---|---| +| entry point | `webos::WebOSMain(delegate).Run(...)` | `AppRuntimeMain(argc, argv)` | +| view setup | constructor **and** a 10-argument `Initialize` | constructor only | +| delegate slots | 61 | 40 | +| `sizeof(WebViewBase)` | 92 | 16 | +| window sizing | `InitWindow(w, h)` | `Resize(w, h)` | + +## The published headers are a starting point, not the answer + +The TV is Chromium **79** - between the public `chromium68` and `chromium87` trees - and its +vtable has **40** slots where chromium87 declares 32 virtuals and chromium68 declares 26. So +the upstream order still has to be checked. + +What checks it, without relying on names at all, is which slots are **pure virtual**. libcbe's +own vtable says: a pure slot relocates to `__cxa_pure_virtual`, an implemented one to real +code. Lining that up against the header's `= 0` versus `{}`: + +``` +firmware pure at: 0-15, 18, 19, 20, 21, 29 +upstream pure at: 0-15, 18, 19, 20, 21 +``` + +Identical through slot 28. That is a strong enough signal to take slots 0-28 as upstream's, +in upstream's order; 29-39 are LG additions and stay placeholders. + +## Two traps, both worth knowing + +**`AppRuntimeMain` is not `extern "C"`.** libcbe exports it C++-mangled, as +`_Z14AppRuntimeMainiPPKc`. Declaring it `extern "C"` looks for a plain `AppRuntimeMain` that +does not exist, and `-verify` catches it. + +**Object sizes are load-bearing.** libcbe's constructors write to offset 12 +(`WebViewBase`) and 4 (`WebAppWindowBase`), so those classes are 16 and 8 bytes. A subclass +declared smaller lets libcbe write past the allocation, and the process dies later inside +`malloc` with a backtrace nowhere near the cause. `../cbe-webos6/README.md` has the longer +version of that story, along with the slot-tracer trick for finding a stubbed slot that was +supposed to return a pointer. diff --git a/web/neva/main.cpp b/web/neva/main.cpp new file mode 100644 index 0000000..6752643 --- /dev/null +++ b/web/neva/main.cpp @@ -0,0 +1,113 @@ +// The same web view again, through neva_app_runtime instead of webos::. +// +// Worth having beside web/cbe-webos6 because the two APIs live in the same +// library on the same TV, and this is the one whose headers LG published. The +// differences are all simplifications: +// +// * AppRuntimeMain(argc, argv) is a free function, where webos:: has a +// WebOSMain class taking a delegate; +// * there is no Initialize() - WebViewBase(width, height, profile) does it; +// * the delegate is 40 slots against webos::'s 61, and 0-28 of them are +// upstream's own, in upstream's order. +// +// Verified on a 65UP7560 running starfish 6.5.2. + +#include + +#include +#include +#include +#include +#include +#include + +#include "neva_app_runtime/webapp_window_base.h" +#include "neva_app_runtime/webview_base.h" + +namespace { + +const char kAppId[] = "org.webosbrew.sample.web.neva"; +const char kUrl[] = "https://example.com/"; + +class SampleWebView : public neva_app_runtime::WebViewBase { + public: + SampleWebView(int w, int h) : neva_app_runtime::WebViewBase(w, h, NULL) {} + + void OnLoadProgressChanged(double progress) override { + printf("[neva] progress %3.0f%%\n", progress * 100); + } + void TitleChanged(const std::string& title) override { + printf("[neva] title '%s'\n", title.c_str()); + } + void LoadStarted() override { puts("[neva] load started"); } + void LoadFinished(const std::string& url) override { + printf("[neva] finished %s\n", url.c_str()); + } + void LoadFailed(const std::string& url, int code, const std::string& desc) override { + printf("[neva] FAILED %s (%d %s)\n", url.c_str(), code, desc.c_str()); + } + void DocumentLoadFinished() override { puts("[neva] document loaded"); } + void DidFirstMeaningfulPaint() override { puts("[neva] first meaningful paint"); } + void RenderProcessCreated(int pid) override { printf("[neva] renderer pid %d\n", pid); } +}; + +class SampleWindow : public neva_app_runtime::WebAppWindowBase {}; + +SampleWindow* g_window; +SampleWebView* g_webview; + +gboolean CreateWebApp(gpointer) { + g_window = new SampleWindow(); + g_window->Resize(1920, 1080); + g_window->SetWindowProperty("appId", kAppId); + g_window->SetWindowHostState(neva_app_runtime::FULLSCREEN); + + g_webview = new SampleWebView(1920, 1080); + g_webview->SetAppId(kAppId); + g_webview->SetVisible(true); + + g_window->AttachWebContents(g_webview->GetWebContents()); + g_window->Show(); + g_window->Activate(); + + printf("[neva] loading %s\n", kUrl); + g_webview->LoadUrl(kUrl); + return G_SOURCE_REMOVE; +} + +bool IsBrowserProcess(int argc, char** argv) { + for (int i = 1; i < argc; ++i) + if (strncmp(argv[i], "--type=", 7) == 0) return false; + return true; +} + +} // namespace + +int main(int argc, char** argv) { + const bool browser = IsBrowserProcess(argc, argv); + if (browser) { + freopen("/tmp/" APP_LOG_NAME ".log", "w", stdout); + dup2(1, 2); + } + setvbuf(stdout, NULL, _IOLBF, 0); + + std::vector args; + args.push_back(argv[0]); + if (browser) { + args.push_back("--ozone-platform=wayland"); + args.push_back("--no-sandbox"); + args.push_back("--no-zygote"); + args.push_back("--in-process-gpu"); + args.push_back(std::string("--browser-subprocess-path=") + argv[0]); + args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); + } + for (int i = 1; i < argc; ++i) args.push_back(argv[i]); + + std::vector cargv; + for (size_t i = 0; i < args.size(); ++i) cargv.push_back(args[i].c_str()); + + if (!getenv("XDG_RUNTIME_DIR")) setenv("XDG_RUNTIME_DIR", "/tmp/xdg", 1); + if (browser) g_idle_add(CreateWebApp, NULL); + + return AppRuntimeMain(static_cast(cargv.size()), cargv.data()); +}