Skip to content

Commit 1ede308

Browse files
committed
Move Code OSS browser compatibility into WebScene
1 parent d7459d1 commit 1ede308

16 files changed

Lines changed: 1090 additions & 16 deletions

‎.github/workflows/CI.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ jobs:
3737
run: python -m unittest discover -s eng/graphics/tests -v
3838
- name: Graphics reference workload and trace checks
3939
run: node --test tests/GraphicsCompatibility/reference-tests.mjs
40+
- name: Runtime browser compatibility checks
41+
run: node --test tests/RuntimeCompatibility/*.test.mjs
4042
- name: Build Release
4143
run: dotnet build WebScene.sln -c Release --no-restore
4244
- name: Portable Core contracts

‎.github/workflows/native-linux.yml‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,17 @@ jobs:
2828
timeout-minutes: 30
2929
steps:
3030
- uses: actions/checkout@v4
31+
- uses: actions/cache@v4
32+
with:
33+
path: ~/.cache/webscene/llvm-22.1.1
34+
key: linux-cxx-llvm-22.1.1-efc4d945744f951d-v2
3135
- name: Install pinned build tools
3236
run: |
3337
sudo apt-get update
34-
sudo apt-get install -y clang-18 clang-tools-18 ninja-build
38+
sudo apt-get install -y ninja-build
3539
python3 -m pip install 'cmake==3.31.6'
40+
python3 eng/sdk/install-linux-llvm.py "$HOME/.cache/webscene/llvm-22.1.1"
41+
echo "WEBSCENE_LLVM_ROOT=$HOME/.cache/webscene/llvm-22.1.1" >> "$GITHUB_ENV"
3642
- uses: actions/cache@v4
3743
with:
3844
path: |
@@ -43,17 +49,18 @@ jobs:
4349
shell: bash
4450
run: |
4551
set -euo pipefail
46-
cmake -G Ninja -S src/WebScene.NativeWeb -B artifacts/native-linux -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++-18 -DWEBSCENE_NATIVE_WEB_GPU=OFF
52+
cmake -G Ninja -S src/WebScene.NativeWeb -B artifacts/native-linux -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="$PWD/src/WebScene.Sdk/cmake/WebSceneToolchain.cmake" -DWEBSCENE_NATIVE_WEB_GPU=OFF
4753
cmake --build artifacts/native-linux --parallel 2 2>&1 | tee artifacts/native-linux-build.log
4854
ctest --test-dir artifacts/native-linux --output-on-failure --output-junit native-linux-tests.xml 2>&1 | tee artifacts/native-linux-tests.log
4955
- name: Build independent native-only Linux SDK without optional graphics
5056
shell: bash
5157
run: |
5258
set -euo pipefail
53-
cmake -G Ninja -S src/WebScene.Sdk -B artifacts/native-sdk -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang-18 -DCMAKE_CXX_COMPILER=clang++-18 -DWEBSCENE_SDK_ENABLE_WEBGPU=OFF -DCMAKE_INSTALL_PREFIX="$PWD/artifacts/sdk"
59+
cmake -G Ninja -S src/WebScene.Sdk -B artifacts/native-sdk -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="$PWD/src/WebScene.Sdk/cmake/WebSceneToolchain.cmake" -DWEBSCENE_SDK_WEBGPU=OFF -DCMAKE_INSTALL_PREFIX="$PWD/artifacts/sdk"
5460
cmake --build artifacts/native-sdk --parallel 2 2>&1 | tee artifacts/native-sdk-build.log
5561
cmake --install artifacts/native-sdk
56-
cmake -G Ninja -S tests/SDKLinux -B artifacts/native-sdk-consumer -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++-18 -DCMAKE_PREFIX_PATH="$PWD/artifacts/sdk" -DWEBSCENE_TEST_WEBGPU=OFF
62+
python3 eng/sdk/stage-linux-runtime.py --llvm "$WEBSCENE_LLVM_ROOT" --sdk artifacts/sdk
63+
cmake -G Ninja -S tests/SDKLinux -B artifacts/native-sdk-consumer -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="$PWD/artifacts/sdk/lib/cmake/WebScene/WebSceneToolchain.cmake" -DWEBSCENE_LLVM_ROOT="$WEBSCENE_LLVM_ROOT" -DCMAKE_PREFIX_PATH="$PWD/artifacts/sdk" -DWEBSCENE_TEST_WEBGPU=OFF
5764
cmake --build artifacts/native-sdk-consumer --parallel 2 2>&1 | tee artifacts/native-sdk-consumer-build.log
5865
ctest --test-dir artifacts/native-sdk-consumer --output-on-failure --output-junit installed-sdk-tests.xml 2>&1 | tee artifacts/native-sdk-consumer-tests.log
5966
tar -czf artifacts/native-linux-sdk-no-gpu.tar.gz -C artifacts sdk

‎docs/code-oss-compatibility.md‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Code OSS native runtime compatibility
2+
3+
Status: development integration. Code OSS 1.137.0 reaches farther through the
4+
native AppScene/WebScene stack with three runtime-owned compatibility slices,
5+
but the complete workbench/editor smoke is not yet qualified. This work does
6+
not add or depend on Electron, CEF, WKWebView, or a browser process.
7+
8+
The reference consumer is
9+
[`SceneTech/vscode-demo`](https://github.com/SceneTech/vscode-demo). It starts
10+
the upstream Code OSS web server as an owned local Node process and presents its
11+
web workbench through an AppScene window backed by WebScene's V8, DOM, CSS,
12+
layout, and scene engine.
13+
14+
## Compatibility slices moved into WebScene
15+
16+
The runtime now installs these adapters in top-level and iframe realms after
17+
the underlying WebScene Blob, DOM, stylesheet, event, and timing objects exist:
18+
19+
- a bounded retained User Timing timeline, including `getEntries`,
20+
`getEntriesByType`, `getEntriesByName`, marks, measures, and clears;
21+
- asynchronous `FileReader` reads over WebScene's byte-preserving Blob;
22+
- top-level stylesheet insertion, deletion, live rule access, and style-rule
23+
declaration mutation through the owning native style element.
24+
25+
These are transitional WebScene implementations with focused regression tests.
26+
They remove application-owned global patches while preserving the native
27+
renderer as the destination of stylesheet mutations. Product-independent
28+
native implementations and their complete acceptance boundaries are tracked in
29+
[Performance Timeline #72](https://github.com/SceneTech/WebScene/issues/72),
30+
[FileReader #73](https://github.com/SceneTech/WebScene/issues/73), and
31+
[stylesheet CSSOM #74](https://github.com/SceneTech/WebScene/issues/74).
32+
33+
## Authentication boundary
34+
35+
The resource response ABI still cannot transport general HTTP status/headers
36+
or `Set-Cookie`, and the engine cookie jar is not browser scoped. The reference
37+
consumer therefore still needs a narrowly validated loopback token bridge for
38+
its WebSocket authentication. The durable response/cookie design and security
39+
tests are tracked in
40+
[resource ABI and cookies #75](https://github.com/SceneTech/WebScene/issues/75).
41+
The token-specific bridge must remain outside WebScene until that general
42+
contract exists.
43+
44+
## Open PR interaction
45+
46+
- WebScene #70 changes CSS compilation/cache representation. CSSOM publication
47+
must invalidate and reuse that cache without storing document cascade state.
48+
- WebScene #71 and AppScene #63 add build-time V8 code caches. Code OSS still
49+
requires full V8/JIT and original JavaScript sources; code caching is an
50+
optional startup optimization after runtime compatibility passes.
51+
- Neither PR provides the APIs above, so this branch remains independently
52+
reviewable and can be rebased after either change lands.
53+
54+
## Qualification contract
55+
56+
A Code OSS package is qualified only when the native application renders the
57+
real workbench, opens an upstream editor model, types a marker, observes that
58+
marker in native rendered lines, and verifies upstream undo. Fetching the page,
59+
starting Node, evaluating JavaScript, or rendering a test-owned element is not
60+
sufficient. Retain runtime diagnostics and a native screenshot with every
61+
failed smoke run.
62+
63+
The current adapters do not certify extension webviews, transferable
64+
MessagePorts, persistent IndexedDB storage, complete HTTP/fetch behavior,
65+
accessibility, IME, drag and drop, or all extension APIs.
66+
67+
## Quality and performance gates
68+
69+
`tests/RuntimeCompatibility` is part of the three-platform CI matrix. It checks
70+
byte-exact FileReader results and event order, bounded timeline semantics,
71+
native stylesheet publication, malformed input, abort/failure paths,
72+
idempotence, and isolation between reused V8 contexts. The performance budgets
73+
are deliberately much larger than current measurements so they reject severe
74+
regressions without turning temporary hosted-runner load into a flaky result:
75+
76+
| Operation | CI budget | macOS ARM64 | Ubuntu ARM64 VM | Windows 11 ARM64 VM |
77+
| --- | ---: | ---: | ---: | ---: |
78+
| Read an 8 MiB Blob | 3,000 ms | 9 ms | 528 ms | 33 ms |
79+
| Retain 50,000 marks | 1,500 ms | 54 ms | 239 ms | 75 ms |
80+
| Publish 450 stylesheet mutations | 2,000 ms | 19 ms | 71 ms | 23 ms |
81+
82+
Measurements were recorded on 2026-09-15 with Node 25.1.0 on macOS, Node
83+
18.19.1 in Ubuntu 24.04, and Node 24.19.0 in Windows 11. The VM runs used the
84+
same uncommitted source through read-only host shares. Native V8 engine tests on
85+
macOS, Linux, and Windows remain required because these JavaScript fixtures do
86+
not exercise V8 handle and context lifetimes.

‎experiments/WebScene.NativeEngine.Probe/CMakeLists.txt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,8 +731,11 @@ if(WEBSCENE_NATIVE_ENGINE_ENABLE_V8)
731731
ixwebsocket)
732732
add_test(NAME webscene_native_engine_tests COMMAND webscene_native_engine_tests)
733733
add_test(NAME webscene_native_idle_v8_platform COMMAND webscene_native_engine_tests)
734+
add_test(NAME webscene_native_select_popup_context COMMAND webscene_native_engine_tests)
734735
set_tests_properties(webscene_native_idle_v8_platform PROPERTIES
735736
ENVIRONMENT "WEBSCENE_NATIVE_ENGINE_TEST_FILTER=idle-v8-platform")
737+
set_tests_properties(webscene_native_select_popup_context PROPERTIES
738+
ENVIRONMENT "WEBSCENE_NATIVE_ENGINE_TEST_FILTER=select-popup-context")
736739
set_tests_properties(webscene_native_engine_tests PROPERTIES
737740
ENVIRONMENT
738741
"WEBSCENE_V8_DETAILED_MEMORY_METRICS=1;WEBSCENE_INTEROP_STRESS=1")
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
#pragma once
2+
#include <string_view>
3+
4+
namespace webscene_native {
5+
inline constexpr std::string_view fileReaderCompatibilityScript = R"JS(
6+
// SCENETECH_FILE_READER_COMPATIBILITY_V1
7+
(() => {
8+
if (typeof globalThis.FileReader === 'function') return;
9+
if (typeof globalThis.Blob !== 'function' || typeof globalThis.Blob.prototype.arrayBuffer !== 'function')
10+
throw new Error('Native Blob.arrayBuffer is required for FileReader');
11+
const schedule = callback => globalThis.setTimeout(callback, 0);
12+
const failure = (message, name) => new DOMException(message, name);
13+
const bytesToBinary = bytes => {
14+
let result = '';
15+
for (let offset = 0; offset < bytes.length; offset += 4096)
16+
result += String.fromCharCode(...bytes.subarray(offset, offset + 4096));
17+
return result;
18+
};
19+
const base64 = bytes => {
20+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
21+
let result = '';
22+
for (let i = 0; i < bytes.length; i += 3) {
23+
const value = (bytes[i] << 16) | ((bytes[i + 1] || 0) << 8) | (bytes[i + 2] || 0);
24+
result += alphabet[(value >>> 18) & 63] + alphabet[(value >>> 12) & 63]
25+
+ (i + 1 < bytes.length ? alphabet[(value >>> 6) & 63] : '=')
26+
+ (i + 2 < bytes.length ? alphabet[value & 63] : '=');
27+
}
28+
return result;
29+
};
30+
class NativeBlobFileReader extends EventTarget {
31+
constructor() {
32+
super();
33+
this._state = 0; this._result = null; this._error = null;
34+
this._operation = 0; this._handlers = new Map();
35+
this.onloadstart = this.onprogress = this.onload = this.onerror = this.onabort = this.onloadend = null;
36+
}
37+
get readyState() { return this._state; }
38+
get result() { return this._result; }
39+
get error() { return this._error; }
40+
// The SDK EventTarget also invokes ontype properties. Own dispatch avoids
41+
// depending on that extension or delivering FileReader.onload twice.
42+
addEventListener(type, listener, options = {}) {
43+
if (typeof listener !== 'function' && typeof listener?.handleEvent !== 'function') return;
44+
type = String(type);
45+
const capture = typeof options === 'boolean' ? options : Boolean(options?.capture);
46+
if (options?.signal?.aborted) return;
47+
const entries = this._handlers.get(type) || [];
48+
if (entries.some(entry => entry.listener === listener && entry.capture === capture)) return;
49+
const entry = { listener, capture, once: Boolean(options?.once), removeAbort: null };
50+
entries.push(entry); this._handlers.set(type, entries);
51+
if (options?.signal?.addEventListener) {
52+
const remove = () => this.removeEventListener(type, listener, capture);
53+
options.signal.addEventListener('abort', remove, { once: true });
54+
entry.removeAbort = () => options.signal.removeEventListener('abort', remove);
55+
}
56+
}
57+
removeEventListener(type, listener, options = {}) {
58+
const capture = typeof options === 'boolean' ? options : Boolean(options?.capture);
59+
const entries = this._handlers.get(String(type));
60+
const index = entries?.findIndex(entry => entry.listener === listener && entry.capture === capture) ?? -1;
61+
if (index >= 0) { entries[index].removeAbort?.(); entries.splice(index, 1); }
62+
}
63+
dispatchEvent(event) {
64+
if (!event || !event.type) throw new TypeError('dispatchEvent requires an Event');
65+
Object.defineProperties(event, {
66+
target: { value: this, configurable: true }, currentTarget: { value: this, configurable: true }
67+
});
68+
const invoke = listener => {
69+
try {
70+
if (typeof listener === 'function') listener.call(this, event);
71+
else listener?.handleEvent(event);
72+
} catch (error) { schedule(() => { throw error; }); }
73+
};
74+
invoke(this['on' + event.type]);
75+
for (const entry of [...(this._handlers.get(event.type) || [])]) {
76+
if (!this._handlers.get(event.type)?.includes(entry)) continue;
77+
if (entry.once) this.removeEventListener(event.type, entry.listener, entry.capture);
78+
invoke(entry.listener);
79+
}
80+
Object.defineProperty(event, 'currentTarget', { value: null, configurable: true });
81+
return !event.defaultPrevented;
82+
}
83+
_emit(type, loaded, total) {
84+
const event = new Event(type);
85+
Object.defineProperties(event, {
86+
lengthComputable: { value: true }, loaded: { value: loaded }, total: { value: total }
87+
});
88+
this.dispatchEvent(event);
89+
}
90+
_read(blob, format, encoding) {
91+
if (!(blob instanceof Blob)) throw new TypeError('FileReader requires a Blob');
92+
if (this._state === 1) throw failure('A read is already in progress', 'InvalidStateError');
93+
const operation = ++this._operation;
94+
this._state = 1; this._result = null; this._error = null;
95+
const active = () => operation === this._operation && this._state === 1;
96+
schedule(() => {
97+
if (!active()) return;
98+
this._emit('loadstart', 0, blob.size);
99+
if (!active()) return;
100+
Promise.resolve().then(() => blob.arrayBuffer()).then(buffer => {
101+
const bytes = new Uint8Array(buffer);
102+
let result = buffer;
103+
if (format === 'text') {
104+
let decoder;
105+
const charset = /(?:^|;)\s*charset\s*=\s*["']?([^;"'\s]+)/i.exec(blob.type);
106+
try { decoder = new TextDecoder(encoding || charset?.[1] || 'utf-8'); }
107+
catch { decoder = new TextDecoder('utf-8'); }
108+
result = decoder.decode(bytes);
109+
} else if (format === 'data') result = 'data:' + (blob.type || 'application/octet-stream') + ';base64,' + base64(bytes);
110+
else if (format === 'binary') result = bytesToBinary(bytes);
111+
schedule(() => {
112+
if (!active()) return;
113+
this._emit('progress', bytes.byteLength, blob.size);
114+
if (!active()) return;
115+
this._result = result; this._state = 2;
116+
this._emit('load', bytes.byteLength, blob.size);
117+
if (operation === this._operation && this._state === 2) this._emit('loadend', bytes.byteLength, blob.size);
118+
});
119+
}).catch(error => schedule(() => {
120+
if (!active()) return;
121+
this._error = error instanceof DOMException ? error : failure(String(error?.message || error), 'NotReadableError');
122+
this._state = 2; this._result = null;
123+
this._emit('error', 0, blob.size);
124+
if (operation === this._operation && this._state === 2) this._emit('loadend', 0, blob.size);
125+
}));
126+
});
127+
}
128+
readAsArrayBuffer(blob) { this._read(blob, 'array'); }
129+
readAsText(blob, encoding) { this._read(blob, 'text', encoding); }
130+
readAsDataURL(blob) { this._read(blob, 'data'); }
131+
readAsBinaryString(blob) { this._read(blob, 'binary'); }
132+
abort() {
133+
this._result = null;
134+
if (this._state !== 1) return;
135+
const operation = ++this._operation;
136+
this._state = 2; this._error = null;
137+
this._emit('abort', 0, 0);
138+
if (operation === this._operation && this._state === 2) this._emit('loadend', 0, 0);
139+
}
140+
}
141+
for (const [name, value] of Object.entries({ EMPTY: 0, LOADING: 1, DONE: 2 })) {
142+
Object.defineProperty(NativeBlobFileReader, name, { value, enumerable: true });
143+
Object.defineProperty(NativeBlobFileReader.prototype, name, { value, enumerable: true });
144+
}
145+
Object.defineProperty(NativeBlobFileReader.prototype, Symbol.toStringTag, { value: 'FileReader' });
146+
Object.defineProperty(globalThis, 'FileReader', { value: NativeBlobFileReader, configurable: true, writable: true });
147+
})();
148+
)JS";
149+
}

0 commit comments

Comments
 (0)