Documentation index
NOOVEXOS 64 / TECHNICAL REFERENCE

Part 13: The web engine, JavaScript and cookies

Since v9.08 the browser is a real layout engine with a full JavaScript runtime. This part describes it: how it is built and linked into the kernel, how the kernel talks to it, the HTML parser and DOM, the CSS cascade, layout (block, inline, floats, positioning, tables, flexbox and grid), painting and fonts, the QuickJS bindings and the JavaScript prelude, networking and the cookie jar, the browser window around it, the qjs command, and how to test and extend it.

Paths are relative to the repository root. The engine lives in web/; the kernel side is apps/internet/webglue.c, apps/internet/browser.c and net/http/cookies.c.

File Lines Contents
include/nvx/apps/nwe_api.h 77 The only interface between kernel and engine: op codes, nwe_req, nwk_resp, the nwk_* callbacks
web/nwe.h 314 Engine types: node, style, box, frag, images, the global document G
web/krt.c 386 Freestanding C runtime for the engine and QuickJS: arena malloc, printf family, strtod, time, stubs
web/util.c 246 Pools, string buffers, UTF-8, URL resolution, merge sort
web/dom.c 659 Tag table, nodes and attributes, the HTML parser, entities, serializer
web/css.h, web/cssparse.c 66, 876 Selector structures and matching, style sheet parser, @media and friends, rule buckets
web/cssstyle.c 1803 Property values, the cascade, inheritance, var(), calc(), shorthands, the UA style sheet
web/layout.h, web/layout.c 29, 1539 Box tree, block and inline formatting, floats, positioning, hit testing
web/layout2.c 800 Intrinsic sizes, flexbox, grid, tables
web/paint.c 836 Painting, stacking, clipping, borders, shadows, gradients, form controls
web/svg.c 444 Inline and image SVG
web/font.c, web/fontdata.c 275, 31 (generated, about 850 KB of source) TrueType outline rasterizer and the embedded Liberation fonts
web/js.c 1274 QuickJS runtime, DOM bindings, timers, module loader
web/prelude.js (to web/prelude_gen.c) 895 The browser API written in JavaScript on top of the bindings
web/engine.c 1209 Images, scripts, navigation, forms, focus and editing, events, nwe_entry
apps/internet/webglue.c 289 Engine stack, HTTP client nwk_fetch, the nwk_* callbacks, console ring
net/http/cookies.c 454 Cookie jar and localStorage/sessionStorage, both saved to disk
apps/internet/browser.c 698 The Browser window: tabs, toolbar, about: pages, input routing, image decoding for the engine
third_party/quickjs/ QuickJS-ng, unmodified
tools/mkwebfont.py, tools/js2c.py, tools/webhost.c Font converter, prelude embedder, host test harness

build.sh has a [web] step that runs before the Doom step:

  1. tools/js2c.py web/prelude.js web/prelude_gen.c nwe_prelude turns the prelude into a C string.
  2. The QuickJS sources (quickjs.c, libregexp.c, libunicode.c, dtoa.c and friends) are compiled once into build/web/ and reused while they are unchanged.
  3. The double and float functions of musl's libm are compiled into libmk.a (the long double variants ending in l are skipped).
  4. The engine files are compiled with the WEBF flags: freestanding, -O2, SSE allowed, musl headers for the C library declarations.
  5. Everything is combined with ld -r into one object, and objcopy --keep-global-symbol=nwe_entry hides every other symbol. The result is build/web_all.o (about 2.4 MB). The step prints any symbol that is still undefined, so a missing runtime function shows up at build time.

Because only nwe_entry is exported, the engine cannot clash with kernel names (QuickJS defines free, malloc, printf and many others) and the kernel can only reach it through one door.

The kernel image grew from about 4.3 MB to about 6.7 MB. That needed a new memory layout (section 13.12).

13.2 The kernel interface (nwe_api.h)

The kernel calls long nwe_entry(int op, nwe_req* r). The ops are:

Op Value What it does
NWE_OP_INIT 1 One-time setup: arena, fonts, QuickJS runtime
NWE_OP_LOAD 2 Parse and load a document (url, data, len, viewport w/h; btn=1 means plain text)
NWE_OP_LAYOUT 3 Re-layout for a new viewport size
NWE_OP_PAINT 4 Paint into a framebuffer rectangle (fb, stride, fbw, fbh, x, y, w, h)
NWE_OP_SCROLL 5 Scroll by y pixels
NWE_OP_MOVE 6 Mouse moved (hover, cursor shape in cursor, link in href)
NWE_OP_DOWN, NWE_OP_UP 7, 8 Mouse button; a click is a down and an up
NWE_OP_KEY 9 Key press (key; arrows and editing keys are NWE_K_UP = 19 ... NWE_K_END = 26)
NWE_OP_TICK 10 Run due timers, animation frames, network callbacks, pending images
NWE_OP_FIND 11 Find in page
NWE_OP_EVAL 12 Evaluate a JavaScript expression; the result is in out
NWE_OP_INFO 13 Fill in title, scroll position and document height
NWE_OP_WHEEL 14 Mouse wheel at x,y by key pixels; scrolls the innermost scrollable box first

Every op returns the page title, scroll position, document height (doch), viewport height and a dirty flag. When script or a click wants to navigate, nav, navmethod, navbody and navblen are set and the kernel performs the request (the engine never fetches a top-level page itself).

The engine calls back into the kernel only through nwk_* functions: nwk_fetch, nwk_free, nwk_image (decode PNG, JPEG, WebP, GIF, BMP or ICO to ARGB), nwk_cookie_get/nwk_cookie_set, the nwk_store_* storage functions, nwk_log, nwk_status, nwk_alert, nwk_ms, nwk_epoch_ms, nwk_tz_min, nwk_pump (keeps the network stack running during long fetches) and nwk_alloc_arena.

The engine stack

web_call(op, r) in apps/internet/webglue.c runs every op on a separate 2 MiB stack through the assembly trampoline nwe_call_stack (arch/x86_64/user/stack_call.c). QuickJS and the recursive layout code need more stack than the kernel's boot stack can spare. A guard (g_in_engine) refuses re-entry: if a callback such as a fetch pumps the GUI and something tries to call the engine again, web_busy() is true and the browser draws "Loading..." instead.

Failure handling

nwe_entry sets a recovery point with __builtin_setjmp. abort(), an arena that is completely full, or an internal check calls nwe_fail, which jumps back and makes the op return -1. The browser then shows "This page stopped working (engine error)" and the next load starts from a clean arena. The rest of the system is not affected.

13.3 Memory

All engine and JavaScript memory comes from one arena that nwk_alloc_arena takes from the kernel heap at INIT. It tries 160 MiB, then 96, 64 and 32 MiB. web/krt.c implements malloc, free, realloc, calloc and malloc_usable_size on it: size classes from 16 bytes to 60,592 bytes with free lists, and a list for larger blocks (page-rounded, reused when a free block is at most twice the request). Before v9.09 requests between the largest class and 64 KiB were given the largest class and overflowed it.

The whole arena is reset on every page load, so nothing a page allocates can leak into the next one.

Within a page, three scratch pools are reset on every pass:

Pool (g_pool) Reset by Holds
1 each restyle (style_all) computed styles
2 each layout (layout_all) boxes, line fragments
3 each paint (paint_all) paint lists and temporary buffers

nfree ignores pointers that belong to a pool. Anything that must survive a pass (font and glyph caches, decoded images, parsed inline style declarations, rebuilt style sheets) is allocated between pool_suspend() and pool_resume().

QuickJS gets a memory limit of 60% of the arena (JS_SetMemoryLimit) and an 800 KiB stack limit. A page that runs out gets a JavaScript InternalError, not a crash.

13.4 HTML parsing and the DOM (web/dom.c)

The parser is a single-pass tokenizer with the tree-building rules that real pages rely on:

  • implied html, head and body elements, and implied end tags (p, li, dt/dd, option, table parts);
  • foster parenting of stray content in tables;
  • raw text for script, style, textarea and title;
  • named and numeric character references;
  • document.write during parsing (output goes into G.wbuf and is parsed in place).

Nodes (node in web/nwe.h) carry the tag, attributes, children, a pointer to the JavaScript wrapper (jsobj), form state (value, caret, checked, selidx), a decoded image and the scroll offset of scrollable boxes. parse_fragment (used by innerHTML, insertAdjacentHTML and outerHTML) parses into a holder element of the right context, so table fragments work. serialize and clone_node implement innerHTML/outerHTML reads and cloneNode.

13.5 CSS (web/cssparse.c, web/cssstyle.c)

Selectors and rules

  • Compound selectors with type, #id, .class and *; the combinators descendant, >, + and ~.
  • Attribute selectors: [a], =, ~=, |=, ^=, $=, *=, with the i flag.
  • Pseudo-classes: :hover, :focus, :focus-within, :active, :checked, :disabled, :enabled, :first-child, :last-child, :only-child, :nth-child(), :nth-last-child(), :nth-of-type(), :first-of-type, :last-of-type, :empty, :root, :link, :visited, :not(), :is(), :where(), :has(), and more. ::before and ::after generate content; ::marker and ::placeholder are styled.
  • At-rules: @media (width, height, orientation, prefers-color-scheme (light), and the usual and/not/, logic), @supports, @import, @layer, @container (treated as matching), @font-face and @keyframes (parsed and skipped).

Nested rules are flattened while parsing (parse_body and nest_combine in web/cssparse.c): each & is replaced by the parent selector, a child selector without & becomes a descendant of the parent, and nested @media, @supports, @layer and @container blocks apply to the parent selector.

Rules are put into buckets by the rightmost compound's id, class or tag, plus a universal bucket, so matching a node only looks at rules that can apply.

The cascade

compute() applies declarations in this order: the UA style sheet (css_ua_text), presentational attributes (width, bgcolor, align and so on), author rules by specificity and order, inline style, then !important. Inherited properties come from the parent's computed style. There are 177 properties in the PROPS X-macro in web/css.h; custom properties (--x, id P_CUSTOM = 1000) are stored per element and resolved by var() with fallbacks.

Lengths are stored as L {px, pct, lo, hi, k}, which can represent calc(), min(), max() and clamp() with mixed units; lres() resolves one against a reference size. Units: px, em, rem, %, vw, vh, vmin, vmax, pt, pc, in, cm, mm, ch, ex.

Values also cover colours (named, hex with 3/4/6/8 digits, rgb()/hsl()/hwb() in both syntaxes, oklch(), oklab(), lab(), lch(), color(srgb ...), light-dark() (light side), color-mix(), system colours such as Canvas and CanvasText, currentColor, transparent), linear, radial and conic gradients, multiple box shadows, text shadows, transform (translate, scale, rotate is ignored, matrix), opacity, filter (ignored), and the shorthands margin, padding, border*, background, font, flex, flex-flow, grid-template, grid-area, gap, inset, list-style, overflow, place-*.

13.6 Layout (web/layout.c, web/layout2.c)

layout_all builds a box tree from the styled DOM (with anonymous block and inline wrappers where needed) and lays it out:

Feature Notes
Block flow box-sizing, min/max sizes, auto margins, margin collapsing between siblings and through parents (first and last child), aspect-ratio
Inline flow text runs and inline boxes are broken into lines; white-space (normal, nowrap, pre, pre-wrap, pre-line), word-break/overflow-wrap, text-align including justify, vertical-align, line-height, letter-spacing, text-indent, text-transform; inline-block and replaced elements sit on the baseline
Floats left and right floats in a block formatting context, text flows around them, clear
Positioning relative (and transform translate) offsets after layout; absolute and fixed boxes are laid out after the main pass against their containing block; sticky with top sticks while its containing block is in view (applied at paint time)
Stacking z-index and positioned elements are painted in stacking order
Overflow hidden, scroll and auto clip; scrollable boxes keep their own scroll offset and take the wheel
Tables an occupancy grid with colspan and rowspan, automatic column widths from min/max content, border-collapse, border-spacing, captions
Flexbox the full algorithm: direction, wrap, grow/shrink/basis, order, justify-content, align-items/align-self/align-content, gap, auto margins
Grid grid-template-columns/rows with px, %, fr, auto, minmax(), repeat() including auto-fill/auto-fit; line placement, span, named areas, auto placement, gap, alignment
Text overflow text-overflow: ellipsis on overflow-hidden boxes, and -webkit-line-clamp (with display: -webkit-box) cuts the box after N lines and ends the last line with an ellipsis
Lists list-style-type (disc, circle, square, decimal, roman, alpha, none) and list-style-position
Forms input (text, password, checkbox, radio, range, color, number, search, email, submit, button, reset, file, hidden), textarea, select, button, progress, meter

intrinsic() gives min-content and max-content widths for shrink-to-fit boxes, table columns and flex bases. hit_test maps a point to the deepest node for mouse events.

The engine paints only what the window shows: layout produces the document height (doch), and the kernel scrolls the viewport.

13.7 Painting, fonts and images

paint_all walks the boxes in stacking order and draws backgrounds (colours, gradients, images with background-size, -position and -repeat), borders (solid, dashed, dotted, double, inset, outset; rounded corners with anti-aliasing via rr_cov), box shadows, text with decorations and shadows, list markers, form controls, the focus ring, the open select list, and find-in-page highlights. Clipping follows overflow and the viewport; fixed elements use the viewport clip. Boxes hidden with clip: rect(...) of zero area, clip-path: inset(50%) or a 1-px overflow-hidden box are skipped. -webkit-background-clip: text paints the text in the gradient's first colour instead of painting the background.

Fonts. tools/mkwebfont.py converts Liberation Sans, Serif and Mono (regular, bold, italic) into outline data in web/fontdata.c. Composite glyphs are decomposed, and glyphs are sorted by code point for binary search. Sans and Sans Bold also carry Greek, Cyrillic, Latin Extended-A and about 80 symbols taken from DejaVu Sans (check marks, arrows, stars, card suits, the hamburger menu, warning sign, gear, envelope and so on); other faces fall back to them, and a code point that no face has is drawn as ?. web/font.c rasterizes outlines with an accumulation buffer (exact area coverage, as in font-rs) and caches glyphs by (face, size in quarter pixels, code point). font-family maps generic and common names (Arial, Helvetica, system-ui, Georgia, Times, Courier, monospace ...) to the three families.

Images. <img>, <picture>/srcset (the best candidate for the viewport), CSS background images and data: URLs are fetched and decoded by the kernel (nwk_image) and scaled with bilinear filtering. SVG files and inline <svg> are rendered by web/svg.c: paths (all commands including arcs), basic shapes, use/symbol, viewBox, transforms, fill and stroke. loading="lazy" images load when they come near the viewport.

13.8 JavaScript (web/js.c, web/prelude.js)

The JavaScript engine is QuickJS-ng (ES2023: classes, modules, async/await, generators, Proxy, BigInt, typed arrays, regular expressions with Unicode). Each page gets a fresh runtime.

Two layers implement the browser API:

  1. C bindings in web/js.c. DOM nodes are objects of one native class that holds a pointer to the node. node_funcs, elem_funcs and doc_funcs provide tree access and mutation, attributes, textContent, innerHTML/outerHTML, value, checked, geometry (getBoundingClientRect, offset*, client*, scroll*), querySelector/querySelectorAll/matches/closest, getComputedStyle, focus, and form submission. Internal helpers sit on a hidden __n object.
  2. The prelude, about 900 lines of JavaScript that run before the page. It defines EventTarget, Event and its subclasses, the event dispatcher (capture, target and bubble phases, once, passive, stopPropagation, preventDefault, inline on... attributes), DOMTokenList (classList), dataset, the style object (backed by inline declarations), the HTML element constructors and prototypes (HTMLInputElement and about 60 others, set per tag with N.setProto), window, location, history (pushState, replaceState, popstate), navigator, screen, localStorage/sessionStorage, document.cookie, fetch with Response/Headers, XMLHttpRequest, URL/URLSearchParams, FormData, Blob, TextEncoder/TextDecoder, atob/btoa, MutationObserver, IntersectionObserver, ResizeObserver, customElements, matchMedia, requestAnimationFrame, queueMicrotask, structuredClone, performance.now, and console.

Editing. Elements with contenteditable (and plaintext-only) are focusable. A click places the caret at the clicked character (ce_place in web/engine.c), and each key goes to __ceKey in the prelude, which fires a cancelable beforeinput with inputType and data, edits the DOM (insert text, delete backward or forward and merge blocks, split the paragraph on Enter), moves the selection and fires input. Selection and Range are full objects (anchor, focus, direction, setBaseAndExtent, extend, modify, deleteContents, insertNode, getBoundingClientRect through the native caretRect). The engine draws the caret from the selection's focus (caret_pos in web/layout.c). MutationObserver is real: web/dom.c calls a hook for every child, attribute and text change while an observer exists (never during parsing), and records are delivered in a microtask. This is what editors like ProseMirror, which ChatGPT uses, rely on.

Streams. ReadableStream (readers, async iteration, pipeThrough, pipeTo, tee), WritableStream, TransformStream, TextDecoderStream and TextEncoderStream are implemented in the prelude. Response.body is a stream holding the downloaded body, and new Response(stream) reads a stream. Responses still arrive complete (section 13.15), so a streamed answer appears all at once.

Every element, the document and window have real on... properties for about 110 events (onclick, oninput, onload ...), and HTMLInputElement.prototype (and the textarea, select and option prototypes) own the value/checked accessors. Libraries test both: React checks 'oninput' in document and installs its value tracker from the prototype's value descriptor.

Scripts. Classic scripts run in document order; defer and type="module" scripts run after parsing; async scripts run when they arrive. Modules are loaded through a QuickJS module loader that fetches imported URLs. Scripts inserted by script (createElement('script')) run and fire load or error. DOMContentLoaded and load fire in the right order.

Timers and the event loop. setTimeout, setInterval and requestAnimationFrame are driven by NWE_OP_TICK, which the browser sends about every 20 ms while its window is open. Microtasks (promises) run after every script and every event.

Limits. A script that runs longer than 6 seconds without returning is interrupted (the interrupt handler compares nwk_ms() with a deadline that is armed before each entry into JavaScript). Uncaught errors go to the console (about:console) with a stack trace.

Redrawing. A DOM or style change only marks the document dirty (dirty_style, dirty_layout, dirty_paint); restyle and layout happen once, before the next paint or the next geometry query from script.

Tested with: React 19.2 (bundled todo app, state, effects, controlled inputs), jQuery 3.6 (DOM building, .css, .addClass, fadeIn), and the 290 KB Go language specification (laid out in about 57 ms on the host).

Binary data (v9.12): N.fetch(url, method, body, ctype, bin) returns the body as an ArrayBuffer when bin is true, and the body argument may be a string or any ArrayBuffer/typed array/DataView (read with jbytes in js.c). fetch always asks for bytes; Response keeps them in _u8 and decodes text only for text()/json(). XMLHttpRequest asks for bytes when responseType is arraybuffer or blob. Blob keeps either a string or bytes. TextDecoder (UTF-8) and TextEncoder call the natives utf8dec and utf8enc instead of looping in JavaScript.

13.9 Networking and the HTTP client (apps/internet/webglue.c)

nwk_fetch(url, method, ctype, body, blen, referer, resp) is the only HTTP client the engine uses:

  • HTTP/1.1 with Connection: close: plain HTTP over the kernel TCP stream (tcps_*), HTTPS over the kernel TLS 1.3 client (tls_get) with certificate checks;
  • up to 7 redirects (301, 302, 303, 307, 308; a 303, or a POST answered with 301/302, continues as GET);
  • chunked transfer and gzip content encoding (Accept-Encoding: gzip; a deflate body is also decoded);
  • Cookie headers from the jar and Set-Cookie back into it, on every request including subresources and fetch();
  • a Chrome-like User-Agent (WEB_UA, ends in Noovex/9.08) so sites send their normal pages;
  • Referer, Accept and Accept-Language: en-US headers.

Typed addresses without a scheme try HTTPS first and fall back to HTTP.

13.10 Cookies and web storage (net/http/cookies.c)

The jar holds up to 400 cookies (CK_MAX). For each cookie it keeps name, value (511 bytes), domain, path, expiry, creation time and the flags secure, HttpOnly, host-only and SameSite.

Rule Implementation
Domain Without Domain the cookie is host-only. Domain must domain-match the request host and contain a dot (or be localhost); a leading dot is ignored
Path Default path is the request path up to its last /; matching follows RFC 6265
Expiry Max-Age wins over Expires; Max-Age=0 or a date in the past deletes the cookie. Expired cookies are dropped when they are next looked at
Secure Only sent over HTTPS, and only set from HTTPS
HttpOnly Sent with requests but hidden from document.cookie (the forjs flag), and JavaScript cannot overwrite it
SameSite Parsed, stored and shown in about:cookies. There is no cross-site request context yet, so it does not change what is sent
Order Longer paths first, as browsers do

Persistence. Cookies with an expiry are saved to cookies.txt in the user's file system (fs_save_bin), at most every 3 seconds while something changed. Session cookies live until reboot. localStorage (keyed by origin; 600 entries in total) is saved to webstorage.txt the same way; sessionStorage entries are kept in memory only.

The old ck_* functions in net/http/http.c (used by the http command and the account code) are now wrappers around the same jar.

13.11 The Browser window (apps/internet/browser.c)

The window keeps the toolbar from earlier versions (back, reload, bookmark star, address bar, Go, Save, Home, Find, and a new Cookies button) and up to 6 tabs. Switching tabs reloads the page of that tab.

Page Contents
about:blank empty page
about:js a JavaScript demo page
about:cookies every stored cookie by site with its flags; buttons to clear everything (?clear) or remove one site (?del=)
about:console the last console messages and script errors
about:history, about:bookmarks history and bookmarks

Downloads (Content-Disposition: attachment or a file extension like .zip) go to Downloads, and a direct link to an image opens the image viewer. Network errors show an error page with the reason (DNS, connect, timeout, TLS).

Input. Mouse moves send MOVE (hover styles, link cursor, status-bar URL); a click sends DOWN and UP; the wheel goes to browser_wheel; keys are mapped by map_key (the kernel's arrow, Home, End, Page and Delete codes become NWE_K_*). browser_tick sends TICK from the main loop (graphics/ui/input.c) and asks for a redraw when the page changed.

Shell. browse <url> (or www <url>) opens the browser at a URL; in the live terminal it first switches to the desktop. desktop switches from the live terminal to the desktop without opening anything.

13.12 Memory layout change in v9.08

Note: v9.11 changed the layout again (15 MiB image, stack at 0x1080000, .bss at 0x1300000); section 2.7 has the current map. This section records why v9.08 had to move the stack.

The 6.7 MB kernel no longer fit below the boot stack at 0x700000. Nothing reported it: the stack simply overwrote the end of .rodata, and the boot integrity check failed ("kernel code was modified"). The layout is now:

Range Use
0x100000-0x900000 kernel image, at most 8 MiB (ASSERT(__file_end <= 0x00900000) in both linker scripts)
0x900000-0x980000 boot stack, top at 0x980000 (boot64.S, both entry paths)
0x980000-0xC00000 USB and disk buffers (xHCI from 0x990000)

The UEFI loader's KERNEL_MAX is now 0x900000-0x100000 (8 MiB), and its kernel-area table, the installer's memory test and the docs use the same boundaries. The ChaCha20-Poly1305 code used a fixed scratch buffer at 0x900000, which is now inside the stack area; it uses a heap buffer instead (aead_mac in lib/crypto/nvxcrypto.c).

13.13 The qjs command

qjs is QuickJS-ng's own command-line interpreter, built as a musl program (with std and os modules, gen/repl.c and gen/standalone.c), compressed and embedded like the other ports (section 6.6). Examples:

qjs -e "print(1+2, [1,2,3].map(x=>x*x).join(','))"
qjs script.js
qjs                      (interactive REPL)

It is separate from the browser's copy of QuickJS: it runs as a user process with the Linux system-call layer, while the browser's copy runs inside the kernel on the engine stack.

13.14 Testing the engine

Host harness. tools/webhost.c links the engine objects (built for Linux with -D_GNU_SOURCE, without krt.c) and renders a file to a PPM image:

webhost page.html out.ppm [width] [height] [maxheight] [javascript-to-eval]

It prints load and paint times, the title, the document height, every fetch and every console message. The optional last argument is evaluated after the page has loaded and its result is printed, which makes it easy to click buttons or read the DOM from a test. Link it with -lm -lz (zlib provides nwk_inflate for WOFF fonts). With WH_MS=N in the environment it keeps ticking the engine in real time for N milliseconds after loading, so timers, animations, transitions and the simulated media backend run before the picture is taken.

In QEMU. The test site used for v9.08 runs a small HTTP server on a tap device that answers every DNS name (so browse noovex.test works) and has pages for cookies (/cookie/set, /cookie/show), a form echo (/echo), a JSON API (/api/data) and a redirect. Checked in QEMU: rendering, fetch(), images, clicks, form POST, Back, server cookies (including HttpOnly hidden from script), about:cookies, a React app with real keyboard input, and jQuery.

13.15 Known gaps

  • No WebGL, OffscreenCanvas, Web Workers or WebAssembly. WebSockets work (section 13.33). Only one <iframe> per page shows its content (section 13.35). Canvas 2D (section 13.31) has no shadows, no real patterns, no toDataURL image data and rectangular clipping only. <video> has no subtitles, no encrypted media (EME) and plays at speed 1 only (section 13.17).
  • Web fonts with CFF outlines (.otf files that are not TrueType inside) are not used; only ligature substitution is taken from GSUB, and there is no kerning (GPOS).
  • 3D transforms are flattened to their 2D part, and transformed elements are hit-tested at their translated position only. Animations of layout properties relayout the page on every frame, which is slow on large pages.
  • Gradient text is drawn in one colour, not as a gradient.
  • Editing has no text selection by mouse drag, no clipboard and no undo; Shift and Ctrl combinations are not passed to pages.
  • One HTTP connection per request (no keep-alive, no HTTP/2), and requests are synchronous, so a slow server stalls the browser until it answers or times out. Fonts and stylesheets are also fetched synchronously when they are first needed.
  • Shadow DOM: styles are scoped by the host's tag name, so two different shadow roots on the same kind of element share their styles; :host-context(), slotchange and closed-mode retargeting are not implemented.
  • SameSite is not enforced, and there is no public-suffix list (a cookie for co.uk would be accepted).
  • Right-to-left text, complex scripts and emoji are not shaped.

13.16 Extending the engine

  • A new CSS property: add it to PROPS in web/css.h, parse it in apply() in web/cssstyle.c, add a field to style in web/nwe.h, and use it in layout or paint. If it is inherited, add it to the inheritance copy in compute().
  • A new DOM method: if it needs engine internals, add a native function to the right table in web/js.c; otherwise write it in web/prelude.js (it is rebuilt by build.sh).
  • A new kernel service for the engine: add an nwk_ function to nwe_api.h, implement it in apps/internet/webglue.c (and a stub in tools/webhost.c), and remember that it runs on the engine stack.
  • After any change, render a few pages with webhost before booting: it is much faster than QEMU and errors print to the terminal.

13.17 Video and audio in the browser

Every <video> or <audio> that has a source gets a media slot (section 6.13). The pieces:

Piece Where What it does
N.mediaOp(op, el, ...) web/js.c, native 36 0 create (URL or empty for kernel data, split flag, start ms, paused), 1 command, 2 status as [cur_ms, dur_ms, flags, w, h, buffered%], 3 append bytes to stream 0 or 1, 4 end of a stream, 5 restart at a time, 6 free, 7 stream length
registry web/wmedia.c up to 8 elements with their slot, the rectangle and clip they were last painted at, the last frame sequence, and the video size
painting wm_paint from paint.c draws the frame with object-fit: contain and black bars, a waiting shade, a big play button before the first play, and, with the controls attribute, a bar with play/pause, time and a seek line (an <audio controls> is a 300 x 54 bar). A size change sends SIZE so the decoder scales to the element
fast path NWE_OP_MEDIA, wm_fast when nothing else on the page is dirty, only elements with a new frame are redrawn into the window's part of SCENE; the browser copies that rectangle to the screen (browser_media_fast). Anything painted over a video is restored at the next full paint
tick wm_tick in NWE_OP_TICK asks for a repaint when a status flag changes, every second while controls are shown, and when the first frame arrives; when the video size becomes known it asks for a new layout, because replaced_size uses it as the intrinsic size
clicks wm_click in NWE_OP_UP a click on a video with controls calls __mediaUI: toggle play, or seek to the clicked point of the bar; it runs after the page's own click handlers and only if they did not cancel the event

The element API is in web/prelude.js (search for class TimeRanges). State lives in a hidden __ms object. load() picks src or the first playable <source>; a blob: URL is looked up in the table that URL.createObjectURL now keeps, so a MediaSource attaches and a Blob is handed over as kernel data; data: URLs are decoded; anything else is resolved and passed as a URL, which play downloads itself (no cookies are sent). One 100 ms interval polls every active element and turns status changes into events: durationchange, loadedmetadata, loadeddata, canplay, canplaythrough, playing, waiting, timeupdate (every 250 ms), seeked, ended, error, resize. A 700 ms scan picks up elements whose src was set as an attribute, and starts autoplay.

MediaSource: addSourceBuffer allows two buffers. The decoder starts when every buffer has received its init segment (or after a few appends); stream 0 is the first buffer. appendBuffer is asynchronous like in browsers (updatestart, update, updateend). Each append is scanned for timestamps so buffered is right: for MP4, mdhd timescale and trex default duration from the init segment, then tfdt and the trun sample durations of each moof; for WebM, TimecodeScale and Duration from Info, then each Cluster time code plus the largest block offset. A seek inside the buffered ranges restarts the decoder with all data kept; a seek outside drops everything but the init segments, and the page then appends segments for the new time. isTypeSupported and canPlayType accept MP4, WebM, Ogg, MP3, WAV and AAC with the codecs FFmpeg was built with, and refuse AV1, high-bit-depth VP9 profiles, AC-3/E-AC-3, more than 2 channels, and video larger than 1920 x 1088 or faster than 60 fps, so adaptive players choose a stream the decoder can handle.

Full screen: requestFullscreen gives the element a fixed, full-viewport style (the old style is saved), sets document.fullscreenElement and fires fullscreenchange; exitFullscreen restores it.

Testing: tools/webhost.c has a simulated media backend (a moving clock, a test pattern frame, and a log of every command and append), so the element API and MSE can be tested on the host with WH_MS=1500 webhost page.html out.ppm; WH_MS keeps ticking the engine in real time for that many milliseconds. In QEMU, a page with a progressive MP4 and a page that feeds fragmented MP4 video and audio through MediaSource both play to the end.

YouTube itself was not tested: the build machine cannot reach it, and it may need more (for example its own streaming protocol, signature code and checks that the browser is real). Reports of what the browser log shows on youtube.com help to close the gaps.

13.18 Transforms, animations and transitions

Transforms. transform is parsed into a list of functions (tfn: translate with lengths, scale, rotate, skew, matrix; matrix3d and rotate3d keep their 2D part) stored on the style, together with the translate, rotate and scale properties and transform-origin. tf_matrix(style, w, h, m) multiplies them into one 2D matrix x' = a x + c y + e, y' = b x + d y + f about the origin. Layout moves a transformed box by e, f (stored as box.btx/bty), so hit-testing and getBoundingClientRect follow translations. A transformed element is a stacking context. In paint_stack, an element whose matrix has a non-identity linear part (or that is animated) goes through paint_transformed: the bounds of its subtree are measured (sub_bounds, at most 1800 px around the origin and 1.6 million pixels), the subtree is painted twice into off-screen buffers, once over black and once over white, and the two results give colour and coverage for every pixel. That buffer is then drawn through the inverse matrix with bilinear filtering. No primitive needs to know about alpha. A video inside a transformed element is drawn but not registered for the fast frame path. A translation-only change (a sliding element) is painted with an offset and needs no buffer.

Animations and transitions live in web/anim.c.

Piece What it does
@keyframes parsed in cssparse.c (parse_keyframes; from, to, percentages, lists) into kfset records; cleared when the sheets are rebuilt
properties animation and its 8 longhands, transition and its 4 longhands, parsed into animspec/transspec arrays on the style (up to 8 animations and 12 transitions per element)
keyframe styles after each style pass anim_after_style links every element that names a known @keyframes to a run record (start time kept across style passes, keyed by element and name) and computes one style per keyframe with css_style_with (the element's style plus that keyframe's declarations)
transitions anim_before_style saves the current value of every transitioned property before a style pass; after it, a changed value starts a transition from the saved value (which may be the middle of a running transition) to the new one
values opacity, colours (color, background-color, border and outline colours, fill, stroke), lengths (width, height, min/max, top/right/bottom/left, margins, paddings, border widths, font-size, letter-spacing), border-radius, transforms (function by function when both lists have the same functions, otherwise as matrices) and visibility
timing linear, ease, ease-in, ease-out, ease-in-out, cubic-bezier() (solved with Newton steps), steps(), step-start, step-end; per-keyframe segments, delay, iteration-count (including infinite and fractions), direction, fill-mode, play-state
frames the engine tick calls anim_tick at most every 33 ms; it writes the current values straight into the computed styles, asks for a repaint, and for a relayout if a layout property is animating. It then dispatches the queued animation* and transition* events

13.19 Web fonts

@font-face rules are collected by fontface_add (web/webfont.c): family, weight or weight range, style, unicode-range (a face whose range covers Latin letters is preferred) and the src list, ranked woff2, woff, truetype, opentype. When face_for meets a family that has @font-face rules, wf_face picks the closest face and loads it the first time (eng_fetch_text, which also handles data: URLs); a URL that fails is not tried again on that page.

web/ttf.c turns the file into a dynamic face (up to 32 per page, numbered after the 9 built-in faces; nwe_face(face) returns the record for either kind):

Format Handling
TTF / TrueType OTF tables read in place
WOFF 1 every table inflated with the kernel's zlib (nwk_inflate)
WOFF 2 web/brotli.c decompresses the table stream (RFC 7932, with the static dictionary and transforms in web/brotli_tab.c, extracted from the reference library), then web/woff2.c rebuilds glyf/loca from the transformed streams (triplet-coded points, 255UInt16 counts, composite records, bounding-box bitmap) and hmtx if transformed, and writes a normal sfnt

cmap formats 4 and 12 give the code points; advances come from hmtx, metrics from head, hhea and OS/2. Glyph outlines are decoded from glyf only when a glyph is first drawn (ttf_outline), including composite glyphs with offsets and scales, into the same op list the built-in faces use, and cached. A missing glyph falls back to the built-in sans face. Ligatures: lookup type 4 (and type 7 extensions) of the liga, rlig and clig features become ligature records; each result glyph gets a private code point from U+F0000. ttf_shape replaces matching runs (longest match first) before text is measured and drawn, so a word such as "home" in an icon font becomes one icon.

Tests: the Brotli decoder was checked against the reference encoder on 800 random inputs (all qualities, windows 10 to 24, generic, text and font modes), and WOFF2 files made with fontTools from DejaVu Sans (6,253 glyphs) and Liberation Serif (2,602) rebuild to exactly the original outlines and advances.

13.20 Shadow DOM

node.shadow points from a host to its shadow root (a fragment) and node.host back. attachShadow (native 39) creates it; the JavaScript object gets the ShadowRoot prototype (host, mode, innerHTML, getElementById, activeElement, adoptedStyleSheets). Closed roots are hidden from shadowRoot.

Area Behaviour
rendering build_children uses the shadow root's children instead of the host's. A <slot> inside a shadow tree is replaced by the host's children whose slot attribute matches its name (the default slot takes the rest, including text), or by its own children when nothing is assigned
style the shadow tree inherits from the host; light-DOM children inherit from the slot they are shown in. Stylesheets inside a shadow root are collected with the host's tag name as their scope: they match only elements inside shadow roots of that tag, :host and :host(selector) match the host, ::slotted(selector) matches its assigned children. Page stylesheets do not reach into shadow trees (the browser's default sheet does)
events the event path continues from a shadow root to its host
DOM getRootNode({composed}), assignedNodes(), assignedElements(), document.adoptedStyleSheets, new CSSStyleSheet() with replaceSync/replace/insertRule

Insertions into a shadow tree count as document changes (in_composed_doc), so they restyle the page.

13.21 Style system speed

Change Effect
Stylesheet cache (parse_cached): parsed rules, keyframes and font faces are kept per sheet, keyed by an FNV hash of the text, base URL, media and shadow scope a rebuild re-uses unchanged sheets instead of parsing them again
Incremental index: when the sequence of sheets of the last rebuild is a prefix of the new one, only the new rules are added to the id/class/tag buckets adding a <style> element costs only its own rules
Ancestor filter: style_tree keeps counts of the tags, ids and classes of the current ancestors (4096 slots); each selector remembers up to 4 hashes of compounds that must be ancestors (descendant or child combinators) and is skipped at once if one is missing most tag-bucket rules with descendant selectors are rejected without walking up the tree; it is only used during a style pass
@media results are cached per rule for each style pass the media query string is no longer parsed for every element

On the host, a page with 18,000 rules that adds 60 <style> elements and forces a style pass after each went from 2,436 ms to 282 ms.

13.22 Crash containment

The engine runs on its own 2 MiB stack (g_estack, apps/internet/webglue.c). When a CPU exception happens in kernel mode, isr_body (arch/x86_64/interrupts/interrupts.c) first calls web_fault_redirect. If the browser is inside an engine call and the faulting stack pointer is in the engine stack (or just below it, for an overflow), the interrupt frame is changed so that iretq lands in web_fault_land on a safe part of that stack. web_fault_land calls nwe_fail, which unwinds to the setjmp in nwe_entry; the engine marks the page dead and returns -1.

web_call then sets g_web_failed. browser_tick sees it and shows "This page crashed" (browser_crashed); the next load resets the engine. NMI, double fault and machine check are never redirected, and after 64 redirected faults the normal error screen is shown again, so a broken engine cannot loop forever.

about:crash writes to a non-canonical address inside the engine to test this path.

13.23 YouTube player page

Since v9.15 yt_try is no longer called, so YouTube addresses load the real site. The description below is kept for the code that is still in apps/internet/browser.c.

YouTube's desktop site needs far more JavaScript and layout than the engine handles, so yt_try (apps/internet/browser.c) recognises watch addresses before any network request: youtube.com, www., m., music. and youtube-nocookie.com with /watch?v=, /shorts/, /embed/ or /live/, and youtu.be/ID. The video ID (letters, digits, - and _) is put in a built-in page.

The page's script posts to https://www.youtube.com/youtubei/v1/player with the clients ANDROID_VR, IOS, TVHTML5_SIMPLY_EMBEDDED_PLAYER and WEB_EMBEDDED_PLAYER in turn, and takes the first answer with streamingData. From formats (progressive streams that carry video and audio together) it picks the highest one up to 720p in MP4 or WebM, never AV1, sets it as the src of a <video>, and shows the title, channel, view count and the chosen quality. Playback goes through the media service and play with HTTP Range requests (section 13.17).

Limits: this could not be tested against youtube.com from the development machine; YouTube changes these clients often and may ask for sign-in or proof-of-origin tokens, and then the page shows the reason it was given. Adaptive-only videos (most above 720p), live streams and age-restricted videos do not play.

13.24 Limits raised for large web apps

Limit Value Where
Script time 15 s per call, plus 1 s per 40 KB of script source (at most 120 s more) arm, js_run_script (web/js.c)
Engine arena first of 768, 512, 384, 256, 160, 96, 64, 32 MiB that the heap can give while keeping 320 MiB free nwe_arena_init (web/krt.c), nwk_alloc_arena (apps/internet/webglue.c)
JavaScript heap 70 percent of the arena js_init
JavaScript stack 3 MiB JS_SetMaxStackSize
Engine stack 6 MiB ESTACK_SZ

Intl is defined at the start of web/prelude.js when the runtime has none. It follows English formatting rules for every locale.

13.25 Staying responsive while a page works

The engine still runs on the desktop thread. Instead of a second thread, the long-running places call back into the kernel:

Caller How often
QuickJS interrupt handler (interrupt_cb, web/js.c) on every check, rate-limited to one pump per 10 ms in nwk_busy
style_tree (web/cssstyle.c) and layout_blocks (web/layout.c) nwe_check_stop every 128 elements or blocks
net_poll (drivers/storage/ahci.c) and nd_poll (net/stack/tcpip.c) g_wait_pump once per millisecond of waiting

busy_pump (graphics/ui/desktop.c) polls USB mice, takes the PS/2 movement accumulated by the interrupt handler, moves the pointer with the same acceleration as the desktop and redraws only the pointer (present_cursor). Button presses are left for the main loop. It does nothing outside the desktop and is not re-entered.

If Esc is held while one engine call has been running for more than 1.5 seconds, nwk_busy returns 1. The interrupt handler then stops the running script and sets g_nwe_user_stop; in style or layout nwe_check_stop calls nwe_fail directly. nwe_entry unwinds, and the browser shows "This page stopped" (section 13.22).

Timers: js_tick runs due timers for at most 40 ms per tick. setTimeout and setInterval return 0 when 50,000 timers exist.

data: addresses are decoded in browser_go (percent-encoded or ;base64) and loaded as HTML.

13.26 Layout on demand for scripts

js_flush_n (web/js.c) runs eng_layout_if_dirty before getBoundingClientRect and the geometry getters (offset*, client*, scroll*). If the element is in the document but has no box yet (the page is still loading and has never been laid out), it marks the styles dirty first. A counter allows 30 forced layouts per 50 ms; after that the old values are returned until the window passes. Re-entry from inside style or layout is ignored.

Measured in the host harness on a test page: before, every element reported 0x0 during load; after, aspect-ratio, flex gap, clamp(), min(), display: contents, inset, :is(), :has(), grid repeat(3, 1fr), calc(), margin-inline: auto, padding-block and fit-content all reported the expected sizes and positions.

13.27 Bugs found against a modern landing page

A test page in the style of a current product landing page was rendered by the host harness (tools/webhost.c) and by Chromium at 1280x860, and the two images were compared.

Bug Fix
SVG <text> ignored svg_text (web/svg.c): the element's text (whitespace collapsed, up to 1023 bytes, nested elements included) is drawn with font_draw at the transformed x/y. The size is font-size times the square root of the transform's determinant. The face comes from css_face_for (web/cssstyle.c, formerly the static face_for).
color-mix() and alpha channels are mixed as premultiplied values and divided by the mixed alpha
comma inside min() in repeat() or minmax() top_comma finds the separating comma at nesting depth 0
min(100%, 420px) as a grid track when a length has a percentage and an upper bound from min(), the track uses the bound in pixels
empty getComputedStyle during load native 6 calls js_flush_n first

After the fixes the header, hero, buttons, SVG logo with its text, separator and two-column section matched the Chromium rendering in position and size. Still different: text-wrap: balance is ignored, so lines break greedily.

13.28 New tab page and probe fixes

resolve_area (web/cssstyle.c) runs at the end of compute: when the element has a grid-area name and its parent has grid-template-areas (kept in style.gta), the rows and columns where the name appears give grid-row-start/end and grid-column-start/end.

scrollHeight returns the larger of the content height plus padding and the box height minus borders.

The new tab page (g_page == 0 in apps/internet/browser.c) is drawn by the browser itself, not by the web engine. "I'm Feeling Lucky" opens a random Wikipedia article. Up to 16 clickable areas are kept per page.

13.29 Faster loading and form controls

js_flush_n keeps the time spent in forced layouts per second of wall time. It lays out only while that total is under 150 ms, or never when nothing is dirty. Engine-internal callers that need fresh boxes (scrollIntoView, focus navigation) call eng_layout_if_dirty themselves.

The host harness now takes WH_ACT, a list of actions separated by semicolons: c X Y (move, press and release), t TEXT (type characters), k CODE (one key). A form test page (text field, textarea, button, a div with role=button, checkbox with label, select, a link styled as a button, a button with a pointer-events: none child, a search field and a form) gave the expected values, clicks, input and change events and submit.

13.30 Load time limits

Limit Value Where
One script call, timer or event 6 s arm (web/js.c)
Extra for a large script 1 s per 200 KB, at most 8 s js_run_script
All page scripts during a load 8 s, counted from js_init; later scripts are skipped g_load_js_ms
Forced layouts for scripts 60 ms per second js_flush_n
Pointer pump during network waits once per 10 ms tick busy_pump (graphics/ui/desktop.c)

nwk_fetch (apps/internet/webglue.c) adds up the time and number of requests in g_fetch_ms and g_fetch_n. browser_go reports the time to fetch the page, the files fetched while the engine built it, and the engine's own time. The report also goes to the kernel log.

13.31 Canvas 2D (v10.10)

<canvas> has a real 2D context. The pixels live in an nimg hung on the element (n->img, non-premultiplied ARGB, created by cv_get in web/canvas.c at the size of the width/height attributes, 300x150 by default, at most 8192 on a side and 16 M pixels). paint_replaced draws it like an image, scaled to the CSS box.

Layer Contents
web/prelude.js CanvasRenderingContext2D with its state (fillStyle, strokeStyle, lineWidth, lineCap, lineJoin, miterLimit, globalAlpha, font, textAlign, textBaseline, the line dash, the transform and a clip rectangle) and save/restore; paths (moveTo, lineTo, closePath, rect, roundRect, arc, arcTo, ellipse, quadraticCurveTo, bezierCurveTo) flattened to device-space polygons; strokes turned into polygons (segment quads, miter, bevel and round joins, butt, square and round caps, dashes); fillRect, strokeRect, clearRect, fill, stroke, clip (bounding box), isPointInPath; fillText, strokeText, measureText; drawImage (3, 5 and 9 arguments, from <img> or another canvas); getImageData, putImageData, createImageData; createLinearGradient, createRadialGradient; translate, scale, rotate, transform, setTransform, getTransform, resetTransform, reset; Path2D (including SVG path strings with M, L, H, V, C, Q, Z) and ImageData. Setting width or height clears the canvas and resets the state.
web/js.c The natives cvInit, cvFill, cvText, cvImage, cvGet, cvPut, cvColor (CSS colour parsing through css_parse_color) and textWidth. They mark the page for repaint.
web/canvas.c cv_fill: polygons with anti-aliasing by signed-area accumulation (the method font.c uses for glyphs) over the bounding box only, solid colours or linear and radial gradients, source-over blending, or erasing for clearRect and destination-out. cv_text draws the glyphs with font_draw into a scratch buffer and uses it as coverage. cv_image copies with scaling and alpha. cv_getdata/cv_putdata convert to and from RGBA bytes.

Overlapping polygons of one fill are combined with the non-zero rule as a union (coverage is clamped at 1), so evenodd holes are not supported.

Tested with tools/webhost.c (rectangles, a stroked rectangle, a filled circle, a triangle with round joins, a linear and a radial gradient, a rotated rectangle, text, a dashed Bezier curve, clearRect, getImageData, drawImage from the same canvas, measureText, isPointInPath) and in QEMU, including an animation driven by requestAnimationFrame.

13.32 Grid layout in Rust (v10.10)

layout_grid (web/layout2.c) hands CSS Grid containers to Taffy, a Rust layout library (rust/nvxlayout, section 4.9 of Part 1). taffy_grid converts the container (column and row tracks after repeat() is expanded, the implicit row size, gaps, justify-content, align-content, justify-items, align-items, grid-auto-flow including dense) and each in-flow item (line placement and spans, explicit width and height, margins including auto, justify-self, align-self, replaced or not) into plain C structures and calls nvx_taffy_grid. Taffy runs the full track sizing algorithm and asks back through nvx_grid_measure for item sizes: min-content and max-content widths come from intrinsic, heights from laying the item out at the given width with lay. The results (position and size of every item) are applied with lay and shift_box. Absolutely positioned children are handled by the engine as before.

The container's min-height and max-height (in px) are passed too, so fr rows grow to fill a grid with only a minimum height (a holy-grail page with grid-template-rows: auto 1fr auto and min-height: 260px; the old C code left the middle row at its content height).

If Taffy returns an error or a size that is not a number, the old C grid code runs instead, and g_taffy_grid = 0 switches Taffy off. A Rust panic ends the page with "layout engine (Rust) stopped" (through nwe_fail), never the kernel.

Check: a 200px / 1fr / 2fr grid in a 620 px container with 10 px gaps gives the 1fr column 133.3 px; repeat(auto-fill, minmax(120px, 1fr)), spans, grid-column: 1 / -1, justify-items: center and justify-self: end render as in other browsers.

13.33 WebSockets (v10.11)

WebSocket is real (RFC 6455), for ws:// and wss://. The JavaScript class is in web/prelude.js; the protocol runs in the kernel in apps/internet/websock.c, reached through the natives wsOpen, wsSend, wsRecv, wsInfo and wsClose (web/js.c, magic 50-54) and the nwk_ws_* callbacks (nwe_api.h).

Step What happens
Open nwk_ws_open resolves the host, opens a Linux-style TCP socket (lxs_*), and for wss:// runs the TLS 1.3 handshake through the new session API (below). It sends the HTTP Upgrade request (with the page's cookies and Origin), checks the 101 reply and the Sec-WebSocket-Accept value (SHA-1 of the key plus the RFC GUID, Base64), and reads Sec-WebSocket-Protocol and any Set-Cookie.
Frames Client frames are masked (random 4-byte key, as the standard requires); server frames may be fragmented. Text (opcode 1), binary (2), ping (9, answered with pong), pong (10, ignored) and close (8) are handled. Messages up to 8 MiB; the receive buffer grows as needed.
Receive WebSocket polls wsRecv on a 30 ms timer in the prelude and fires message events (text as a string, binary as an ArrayBuffer or Blob by binaryType). Closing fires close with the code, reason and wasClean; a dropped connection gives code 1006 and an error event first.
Send send masks and frames the data; close(code, reason) sends a close frame and waits briefly for the peer's. ws/wss URL parsing, the ready-state machine, bufferedAmount (always 0) and the close-code checks follow the WHATWG API.
Reset A page load closes every socket (nwk_ws_close_all in reset_engine).

TLS session API. tls_get (section 9.9.2) handshook, sent one request and read the whole reply. WebSockets need to keep sending and receiving after the handshake, so lib/crypto/nvxtls.c now also exposes tls_connect_sess (handshake into a tls_sess holding the cipher, keys, IVs and record sequence numbers), tls_sess_write, tls_sess_read and tls_sess_wipe. tls_get is now a thin wrapper over them, so HTTPS fetching is unchanged.

Tested with tools/webhost.c plus a host WebSocket shim over real TCP against a Python ws:// echo server: connect, text echo, a second text message, a binary message echoed reversed, server ping answered, and a clean close (code 1000); and in QEMU against a local server reached as 10.0.2.2:8090.

Limits: no permessage-deflate compression, no subprotocol negotiation beyond echoing what the server picks, and wss:// uses the browser's TLS 1.3 client (section 9.9), so a server that needs a cipher or group it does not offer will not connect.

13.34 Popups and postMessage for sign-in (v10.11)

The browser still has one page per window, so it cannot open a real popup. window.open(url, name, features) with a features string (what "Sign in with Google" and similar use) now returns a detached window object that reports closed === true with working postMessage, location and close members, instead of navigating the current page away. Popup-based sign-in libraries read that as "popup blocked" and fall back to a full-page redirect, which the browser can follow; cookies set during the redirects are kept (section 13.10), so the redirect form of OAuth can complete. window.open(url, '_self') still navigates the current page. postMessage now structure-clones its data and carries the target origin. This was tested in webhost (a blocked-popup object with a working postMessage, and a delivered message); the full third-party sign-in flow needs a live identity provider and was not tested here.

13.35 Iframes (v10.12)

The engine keeps one document in global state, so a page and its iframe cannot share one engine. Instead the kernel links a second, complete copy of the engine for the iframe.

Build. build.sh makes web2_all.o from the same web_pre.o with objcopy --redefine-sym: nwe_entry becomes nwe2_entry and nwe_fail becomes nwe2_fail, and the frame copy's kernel callbacks are pointed at frame versions (nwk_alloc_arena2 caps its arena at 256 MiB, nwk_frame_post becomes nwk_frame_post_up, nwk_frame_recv becomes nwk_frame_recv_down, nwk_frame_is becomes nwk_frame_is2, and nwk_frame_paint, nwk_frame_reset and nwk_ws_close_all become no-ops so the frame cannot nest frames or close the page's WebSockets). Both copies keep every other symbol local, so they share nothing but the kernel. The kernel image grows by about 3.6 MB.

Step Where What happens
Ask paint_replaced (web/paint.c) When the page paints an <iframe> with a src (resolved against the page) or srcdoc, it calls nwk_frame_paint with the box and clip. The first visible iframe of a page claims the frame engine; other iframes keep the "Embedded page" placeholder.
Hidden frameScan (web/prelude.js), native frameOpen (magic 58), nwk_frame_open Every 40 ms the page looks for the first iframe[src], including ones that are hidden (display: none, 0x0) and so never painted, and asks the kernel to load it at 300x150. Invisible challenge iframes therefore load and run their scripts.
Load frame_tick (apps/internet/webglue.c, from browser_tick) Outside any engine call, the kernel fetches the URL with nwk_fetch (cookies included, the page's address as Referer) or takes the srcdoc text, and sends NWE_OP_LOAD to the frame engine at the iframe's size. A navigation inside the frame (a link, location) loads the new URL into the frame.
Paint nwk_frame_paint Each time the page paints, the frame engine paints into a buffer the size of the iframe, which is copied into the screen inside the page's clip. A size change sends NWE_OP_LAYOUT first.
Run frame_tick The frame engine gets NWE_OP_TICK with the page, so its timers, promises and animations run.
Input web_mouse (apps/internet/browser.c) A click inside the visible iframe goes to the frame engine (move, down, up) at frame coordinates instead of the page.
Messages web/prelude.js, natives framePost, frameRecv, isFrame (web/js.c, magic 55-57) iframe.contentWindow and, inside the frame, window.parent/window.top are window proxies with postMessage. Messages are serialized as JSON with the sender's origin and target origin, queued in the kernel (64 per direction, 256 KB each) and delivered by a 40 ms poll as message events with the right origin and source; a target origin that does not match the receiver drops the message.
Reset reset_engine A new page load forgets the frame and clears both queues.
Crash web_fault_land A CPU exception while the frame engine runs stops only the frame (nwe2_fail); the page stays.

Limits: one live iframe per page; no keyboard focus or wheel scrolling inside it; contentDocument is null (the documents live in different engines, as for a cross-origin frame); data sent with postMessage must survive JSON (no Blob, ArrayBuffer or MessagePort transfer); sandbox and allow attributes are not applied.

Why: challenge pages such as Cloudflare's run their check inside an iframe and report the result to the page with postMessage. This gives that check a place to run; whether a given service accepts the browser is decided by that service and was not tested here (the build machine cannot reach it).

13.36 Reloads, tabs and the toolbar (v10.13)

Bug: since v10.09 the second page load of a session (Reload, a new address, a tab switch, which reloads the tab's page) showed a white page. Cause: web_load_html (apps/internet/browser.c) filled the shared request block g_req and then called compose_present() to show "Loading" before sending NWE_OP_LOAD. Once a page was showing, that redraw ran web_paint, which cleared g_req and used it for NWE_OP_LAYOUT/NWE_OP_PAINT, so the engine received a load with no URL and length 0 and built an empty document. The first load of a session was fine because web_paint returns early while there is no page. With several tabs every switch hit this, so most tabs showed nothing.

Fix Where
compose_present() runs before the request is filled, not after. web_load_html
web_paint uses its own static request (preq) and the hover probe in browser_draw its own (mreq), so drawing can never clobber a request in flight. apps/internet/browser.c
The streaming TCP reader accepts a FIN only when it is in sequence (seq == ts_rcv); an early FIN after a missed segment is answered with a duplicate ACK so the server resends the data, instead of ending the response early. tcps_rd (net/http/http.c)

Toolbar (drawing only, all click areas keep their variables): a tab strip in a darker band with the active tab joined to a light toolbar; each tab has a coloured circle with the site's first letter, a title cut with ".." to fit, and a larger close cross; round back, reload (circular arrow) and star buttons; a rounded address field that, when not being edited, shows a lock (green for https, grey for http), the host in dark text and the rest of the address in grey, cut with ".." at the end instead of the start; "Search or enter address" when empty; pill buttons; a thin accent bar under the toolbar while the engine is busy.

Tested in QEMU: one page reloaded twice, then three tabs with different pages switched back and forth five times; every switch and reload showed the right page.

13.37 Web Crypto and structuredClone (v10.14)

crypto.subtle was an empty object, so pages that hash data (for example the PKCE step of an OAuth sign-in, which sends the SHA-256 of a random verifier) failed. It is now implemented in JavaScript in web/prelude.js:

Method Supported
digest SHA-256, SHA-1
importKey (raw) HMAC (SHA-256 or SHA-1), PBKDF2
sign, verify HMAC; verify compares in constant time
deriveBits PBKDF2 with HMAC-SHA-256 or HMAC-SHA-1
generateKey HMAC (random key from crypto.getRandomValues)
exportKey (raw) extractable keys
encrypt, decrypt, other algorithms reject with NotSupportedError

Results are Promises of ArrayBuffer, inputs are any BufferSource, and unknown algorithms reject with NotSupportedError as in browsers. CryptoKey keeps the key bytes in a non-enumerable property.

structuredClone used a JSON round trip, which turned Date into strings and dropped Map, Set, typed arrays and cycles. It now copies Date, RegExp, ArrayBuffer, typed arrays and DataView, Map, Set, Error, boxed primitives, arrays (holes kept) and plain objects, keeps shared and cyclic references, passes Blobs through, and throws DataCloneError for functions, symbols and DOM nodes.

Tested: SHA-256 ("abc" and 1000 bytes), SHA-1, HMAC-SHA-256 and PBKDF2-HMAC-SHA1 against Python's hashlib/hmac in QuickJS on the host, and SHA-256, HMAC and structuredClone on a page in the browser in QEMU.

JavaScript speed: building QuickJS with -O3 or -march=x86-64-v2 instead of -O2 gave no measurable gain on a host benchmark (recursion, object sort, string building with a regex, Map), and computed-goto dispatch is already on, so there was no quick compiler win. Real gains need an inline cache or a JIT (estimate 15-30 sessions for a JIT).

13.38 Framework compatibility, incremental restyle and stability (v10.15)

Method. Gmail, Google Docs, YouTube, GitHub and claude.ai cannot be reached from the build machine, so their public building blocks were installed from the machine's own package cache and run in the engine on the host (tools/webhost.c) next to Chromium (Playwright) as the reference: React 19 with Markdown (marked), DOMPurify, highlight.js, KaTeX, Jotai, Zod, TanStack Virtual and Headless UI (a chat app in the style of claude.ai); a GitHub-style repository page with Catalyst-style custom elements, relative-time, details-menu, clipboard-copy, a Turbo-style fetch-and-replace navigation and Primer-like CSS; a YouTube-style grid of 60 shadow-DOM components rendered with a Lit-style template engine (template element, importNode, TreeWalker), constructable stylesheets, ::part, ::slotted and lazy images through IntersectionObserver; a Docs-style contenteditable editor; D3 and Mermaid charts; and a 1,500-row inbox page with 3,000 CSS rules for speed. All of them now render close to Chromium and react to clicks and typing; differences left are font metrics and emoji glyphs (shown as "?"); SVG markers and emoji were added later in this release (section 13.39).

Area Before Now Where
Custom elements connectedCallback only ran at define time; constructors never ran on upgrade; elements created by innerHTML or the parser after define were never upgraded; no disconnectedCallback or attributeChangedCallback Full lifecycle: the parser and elem_new record every hyphenated element in g_ce_pend (native cePending, magic 59); upgrades run the real constructor (the HTMLElement constructor returns the element being upgraded), observedAttributes callbacks fire for existing attributes, then connectedCallback; insertions, removals and attribute changes go through the mutation hook (new kind 5 = attribute changed, fired after the change with the old value); innerHTML/outerHTML/insertAdjacentHTML and shadow-root innerHTML flush upgrades and connect/disconnect; template contents stay inert; name and constructor checks as in browsers; customElements.upgrade web/dom.c, web/js.c, web/prelude.js
Mutation events during load Off while any inline script ran during parsing (g_dom_parsing), so MutationObserver and custom elements missed everything a page did at load js_run_script clears the parsing flag while a script runs; the hook may nest (depth 8) web/js.c
isConnected False inside shadow trees Walks through shadow hosts web/js.c
Adopted stylesheets A hidden <style> child that frameworks removed on their first render Stored on the shadow root node (value, native setAdopted, magic 60) and parsed by collect with the host scope; replaceSync/insertRule re-apply to every root that adopted the sheet web/cssparse.c, web/js.c, web/prelude.js
SVG elements Only tags in the tag table were SVG; text, tspan, foreignObject, defs, gradients and markers made with createElementNS or parsed inside <svg> were plain HTML elements; getBBox and friends were missing NF_SVG node flag from createElementNS and the parser (children of foreignObject stay HTML); getBBox (shapes, paths with curves, text with anchor and baseline, groups with transforms), getComputedTextLength, getSubStringLength, getTotalLength, getPointAtLength, getCTM, getScreenCTM, createSVGPoint/Matrix/Rect/Transform, ownerSVGElement, transform.baseVal; getBoundingClientRect of SVG elements and of HTML inside foreignObject is estimated from geometry and text web/nwe.h, web/dom.c, web/js.c, web/prelude.js
SVG painting Text attributes were read only from the text element; <tspan> positions and em offsets ignored; <style> inside <svg> ignored; foreignObject not drawn font-size, text-anchor, weight, style, family and baseline inherit through groups; x/y/dx/dy per tspan with em units and anchored text chunks; <style> blocks inside <svg> are collected; foreignObject draws its text centred in its box web/svg.c, web/cssparse.c
SVG sizing A <svg> with a viewBox and no height was 150 px high (UA rule svg:not([height])); an SVG image without viewBox was rasterised at the wrong scale The 300x150 UA defaults apply only without a viewBox; with a viewBox and no size the SVG fills the available width and takes its height from the ratio; image rasterisation adds the implied viewBox web/cssstyle.c, web/layout.c, web/engine.c
Tables rows, cells and friends were missing (rows reflected the textarea attribute) rows, tBodies, tHead, tFoot, caption, create*/delete*, insertRow/deleteRow on tables and sections, cells, rowIndex, sectionRowIndex, insertCell/deleteCell, cellIndex, colSpan, rowSpan web/prelude.js
Rich text editing execCommand knew only plain text commands; no queryCommandState bold, italic, underline, strikeThrough, subscript, superscript (toggle, split and merge), formatBlock/heading, lists, createLink/unlink, removeFormat, foreColor, hiliteColor/backColor, fontName, fontSize, justify*, indent/outdent, insertHorizontalRule, insertImage, insertLineBreak, undo/redo (100 steps per editor); queryCommandState, queryCommandValue, queryCommandEnabled, queryCommandIndeterm web/prelude.js
innerText textContent with collapsed spaces Rendered-text rules: line breaks around blocks, two around paragraphs, <br>, tabs between cells, newlines between rows, hidden and display:none content skipped, pre kept web/js.c (itx_walk)
Closed <details> Author CSS such as display:flex showed its content Everything but the summary is forced to display:none while open is absent, as in browsers web/cssstyle.c

Incremental restyle. A class, attribute, hover, focus or text change used to restyle the whole document and lay it out again, and an id or class change even rebuilt the rule index (css_bump). Now eng_style_node(n) records up to 48 subtree roots (G.dirty_style = 2); style_partial (web/cssstyle.c) recomputes only those subtrees, with the ancestor filter primed from the root's ancestors, and writes each new style over the old style object in place, so boxes and text fragments keep valid pointers. lay_eq compares only the properties that affect layout; if none changed, layout is skipped and only a repaint happens (colours, backgrounds, borders' colours, shadows, outlines, radius, opacity changes that keep the same stacking, transforms, cursor). The root is widened to the parent when the style sheet has +/~ combinators (g_css_sib) and insertions restyle the parent when it has positional pseudo-classes (g_css_pos); :has() anywhere (g_css_has), a changed style sheet, a root inside a display:none subtree, or garbage beyond three times the last full restyle fall back to a full restyle. Hover, :active and focus changes restyle only the elements whose state changed (eng_chain_dirty, from the lowest common ancestor down). Typing in an input or textarea restyles only that control and lays out only for a textarea. The UA rule fieldset+p (a typo that forced sibling handling everywhere) was removed. Animations outside a partial root keep running (anim_after_style_part).

CSS rule lookup. Rules whose rightmost compound has only an attribute selector ([data-state=open], [aria-expanded]) were checked for every element; they are now bucketed by attribute name (battr).

Measured on the host (webhost, 1,500-row inbox with 3,000 rules): page load 187 ms to 118 ms; 20 class toggles each followed by a layout query 124 ms to 15 ms. In QEMU under TCG emulation (final build) the React chat app loads in 3.0 s and in 0.64 s on reload (code cache, section 13.39), the GitHub-style page in 0.8 s (2.2 s with the first v10.15 build, before the detached-tree and cache work) and the YouTube-style grid in 1.3 s.

Stability. A sweep over 260 real HTML documents found on the build machine (library documentation, man pages, NetSurf resources), 18 test pages and 557 randomly mutated pages under ASan and UBSan found and fixed:

Bug Effect Fix
add_text_pieces (web/layout.c): a carriage return in white-space: pre text matched no branch and the word scanner stopped without advancing Any page with a <pre> block and Windows line endings froze the browser (NetSurf licence page, the ncurses HOWTO: more than 2 minutes, now 90 ms) Skip the character when the word is empty
collect passed a missing href of a <link> to url_resolve Crash (NULL dereference) url_resolve treats NULL as empty
Huge font sizes (font-size: 1e30px) Integer overflow in font metrics and glyph code font-size clamped to 10,000 px in the cascade and 100,000 px in font_metrics

The second fuzz batch (260 pages) ran clean. webhost got WH_PRE (JavaScript evaluated before the timer loop) and a w X Y D wheel action; its readfile rejects unreadable paths.

Not done, with estimates: a JIT or inline caches for QuickJS (15-30 sessions; compiler flags gave nothing measurable, see 13.37); incremental layout (layout still runs for the whole page when a layout property changes, 3-5 sessions); SVG filters and gradients by url(); symbols outside the fonts and the emoji table still show as "?". The real Gmail, Docs, YouTube and claude.ai still need far more JavaScript speed and APIs (Service Workers, WebAssembly for some) and were not tested. Keyboard modifiers, IndexedDB, Workers and SVG markers were added later in this release (section 13.39).

13.39 Code cache, HTTP cache, IndexedDB, Workers and keys (v10.15)

Profile of the React chat app load: 78% of the time was QuickJS parsing and compiling the 900 KB bundle and 14% running it, so the speed work went where real browsers also put it.

Feature How it works Where
Bytecode cache Classic scripts of 8 KB or more are compiled with JS_EVAL_FLAG_COMPILE_ONLY, written with JS_WriteObject(JS_WRITE_OBJ_BYTECODE) into a kernel-side cache (nwk_code_put, 96 entries, 40 MiB, least recently used dropped first) keyed by a 64-bit FNV hash of the source text, its name and length, then run with JS_EvalFunction. On the next load of the same script nwk_code_get returns the bytecode and JS_ReadObject replaces parsing. The cache lives outside the per-page arena, so it survives reloads, tab switches and navigation; only the engine writes it, which is what makes JS_READ_OBJ_BYTECODE safe. Modules are not cached. Host numbers: React chat app 157 ms first load, 60-79 ms after; D3 + Mermaid page 745 ms, then 146-212 ms. The first load pays about 15-20 ms for writing. web/js.c, apps/internet/webglue.c, tools/webhost.c, include/nvx/apps/nwe_api.h
HTTP memory cache nwk_fetch keeps GET responses with status 200 (256 entries, 48 MiB, LRU) unless the response says no-store or sets a cookie. A response is reused while it is younger than its Cache-Control: max-age (capped at one day); scripts, style sheets, fonts and images without a lifetime are reused for 10 minutes. g_http_mode 1 (tab switch, Back) lets the first fetch, the page itself, use any cached copy younger than 10 minutes; mode 2 (Reload) bypasses the cache for the page and all its resources. The cookies page clears the cache too. apps/internet/webglue.c, apps/internet/browser.c
IndexedDB In memory, per origin, for the session: indexedDB.open with versioned upgradeneeded, blocked/versionchange, deleteDatabase, databases, cmp; object stores with in-line or out-of-line keys and key generators, add/put/get/getKey/getAll/getAllKeys/count/delete/clear; indexes with unique and multiEntry; cursors (openCursor, openKeyCursor, continue, advance, update, delete, all four directions); IDBKeyRange; transactions that complete after their last request, abort on an unhandled request error and roll back their stores; spec key ordering (number < date < string < binary < array); values copied with structuredClone. web/prelude.js
Web Workers Same-thread workers: the script is loaded from a URL, blob: or data: URL and run inside with (scope) so onmessage = ..., self, postMessage, close, importScripts (global eval), addEventListener and timers belong to the worker; messages are copied with structuredClone and delivered asynchronously both ways; terminate; load failures fire error. Module workers that use import report an error. Heavy work in a worker still blocks the page, because the engine has one JavaScript thread. web/prelude.js
Keyboard The Browser passes Ctrl, Shift, Alt and the Windows key as bits 24-27 of the key; Ctrl+letter arrives as the letter instead of a control code (which used to be read as Home, End and so on). Pages get ctrlKey, shiftKey, altKey, metaKey, getModifierState, physical code values (KeyK, Digit1, Space, Slash, ...), keyCode/which for shifted digits, and a keyup after every key. Ctrl and the Windows key suppress keypress and text insertion, so shortcuts reach the page. apps/internet/browser.c, web/engine.c, web/prelude.js
Emoji tools/mkemoji.py turns the CBDT bitmaps of Noto Color Emoji into assets/emoji32.bin (1,413 single-code-point emoji from the symbol and emoji ranges, 32x32, 128-colour palette PNGs, 1.44 MB; header NVEM, count, size, then sorted code point/offset/length entries). The kernel links it as a blob and serves nwk_emoji_has (binary search) and nwk_emoji (decode through nwk_image). When a code point has no glyph in the page fonts but is in the table, font_adv gives it 1.2 em and font_draw blits it scaled with bilinear filtering and the text colour's alpha; up to 96 decoded emoji are cached per page. Joined sequences (ZWJ, skin tones, flags) show as their parts. Tested in QEMU: the folder and file emoji of the GitHub-style page. tools/mkemoji.py, assets/emoji32.bin, apps/internet/webglue.c, web/font.c, build.sh
SVG markers marker-start/marker-end (attribute or style) on paths, lines and polylines draw the referenced <marker> at the end point with orient (auto, auto-start-reverse, angle), refX/refY, markerWidth/markerHeight, viewBox and markerUnits. web/svg.c

Tested against Chromium: IndexedDB (upgrade, stores, indexes, multiEntry, cursors in reverse, key ranges, rollback on abort, constraint error, array keys, cmp, version error, databases) and Workers (URL with importScripts, blob:, data:, failing URL) give the same results; key events for Ctrl+K, Shift+A, Alt+1, ! and Enter carry the same key, code, keyCode and modifier values.

13.40 WebAssembly, Service Workers and the Cache API (v10.16)

WebAssembly runs on wasm3, an interpreter vendored in third_party/wasm3 (MIT, LICENSE there), built into both web engine copies with the flags W3F in build.sh: no POSIX host, no thread-local storage (M3_THREAD_LOCAL empty, the kernel has no %fs segment), no typed function references, stack switching, snapshots or gas metering, and a native stack budget of 1.5 MiB below the entry point (the engine stack is 6 MiB and QuickJS keeps 3 MiB). The JavaScript API is in web/prelude.js on top of the natives in web/wasm.c.

Part How it works
Runtime One wasm3 environment and runtime per page, created on first use; the page arena reset frees it with the page. Every instance is loaded into it as a module named __iN.
Module The bytes are copied (wasm3 keeps pointers into them) and parsed; a parse error is a CompileError. imports, exports and customSections come from a small section reader in the prelude. Function bodies are validated when they are compiled: exported functions when the instance is made, the rest on first call.
Instance The import object is checked like Chromium (TypeError for a missing module object, LinkError for a wrong kind). Function imports become host functions (CompileRawFunction) that call the JavaScript value; imported memories, tables and globals are linked by renaming the import to the owning module and export, so wasm3's own linker shares the same object; plain numbers for immutable globals go through m3_LinkGlobal. The start function runs inside the constructor.
Calls Exported functions are JavaScript functions with the function index as name, the parameter count as length and the same object every time (table get included). Values convert like the spec: i32 numbers, i64 BigInt (a TypeError for numbers), f32 rounding, externref held in a per-page table, funcref as exported functions. JavaScript exceptions thrown in imports come out unchanged; traps are RuntimeError with Chromium's messages, stack overflow is RangeError.
Memory buffer is an ArrayBuffer over the linear memory itself (no copy). After every call into wasm, at the start of every import call and on grow, the engine compares the memory's address and size with the buffer and detaches the old buffer when they changed, so a view of grown memory is never left pointing at freed bytes.
Table, Global new builds a tiny module in memory that declares and exports the object, so it can be imported like an exported one. get, set, grow, length, value, valueOf and type.
Async compile, instantiate (bytes or module), compileStreaming and instantiateStreaming (any content type is accepted).
Long runs Every 16,384 loop iterations or branches the interpreter asks the engine whether to stop: Escape held in the Browser or more than 20 s in one call stops the code with a RuntimeError, and the desktop keeps drawing in between. Found by fuzzing: a module whose inner loop ran one iteration per outer iteration skipped the check when it was tied to the outer loop's back edge.

Service Workers run on the page thread like the Workers of section 13.39, in a scope object of their own (self, registration, clients, caches, skipWaiting, importScripts, the usual globals).

Part How it works
Registration navigator.serviceWorker.register(url, {scope}) with Chromium's checks (http or https, same origin, scope under the script's directory). The worker script runs, install waits for waitUntil promises, then activate; states installing, installed, activating, activated with statechange events and updatefound; ready resolves on activation. Registering a different script for the same scope updates it.
Persistence Registrations (scope, script URL, script hash) are kept in the origin's persistent web storage under a separate namespace (native storeGet(2, ...), stored as origin `sw
Fetch events fetch() from a controlled page dispatches fetch to the worker with a FetchEvent; respondWith answers it, otherwise the request goes to the network. The worker's own fetch goes straight to the network. Subresources the engine loads itself (img, script, CSS) and XMLHttpRequest are not intercepted.
Clients clients.claim sets the controller and fires controllerchange; matchAll, get, postMessage both ways (ExtendableMessageEvent with source).
Cache API caches.open/has/delete/keys/match and Cache.match/matchAll/add/addAll/put/delete/keys with ignoreSearch and ignoreMethod; only GET, http(s) and non-206 responses are stored. The caches live in memory for the page.

Other changes in this release: performance.now() and event time stamps use the high-resolution timer (0.1 ms steps instead of 10 ms); Date.now() advanced only once per second because k_realtime_ms in fs/vfs/dirtree.c read the CMOS clock on every call instead of keeping its base (now a static); Ctrl+A in the address bar selects the address instead of typing a, and other Ctrl or Windows-key combinations no longer insert letters there.

Tested: a WebAssembly test page (40 checks: imports and exports, i64, f32, traps, deep recursion, memory growth from JavaScript and from wasm with buffer detaching, tables, globals, errors, streaming) gives the same output as Chromium on the host and in QEMU except the detail text of one compile error; real WASI programs from the wasm3 test set (c-ray ray tracer, mal Lisp interpreter) give byte-identical output to Chromium on the host and in QEMU; CoreMark runs at 3,584 on the host (V8 with its optimizing compiler: 30,917). A Service Worker test page (register, ready, controller, fetch interception for GET and POST, Cache API, messages both ways, scope and 404 errors, registrations, unregister) gives the same output as Chromium, and a reload starts controlled, on the host and in QEMU. Mutation fuzzing of three modules under AddressSanitizer found the stop-check gap above and no memory errors.

Not done: a WebAssembly JIT (wasm3 is an interpreter, about a tenth of V8's speed on CoreMark), SIMD, threads and shared memory, tail calls and exception tags in the JavaScript API; Service Worker interception of engine-loaded subresources and navigations, background sync and push; Cache API storage across visits.

13.41 Freeze and crash fixes, popover and dialog (v10.17)

Prompted by the owner's report: clicking the Code button on a GitHub repository page froze NoovexOS for 60 s, with the freeze report pointing at strcmp.

Problem Fix
Elements inside a display:none subtree kept style pointers into the style pool after a full restyle reset it (only direct children were cleared). Showing or changing them later read freed memory: crashes, or loops over garbage. Found with a random DOM mutation tester that compares the layout before and after a forced full restyle. Whole hidden subtrees (and shadow roots) are cleared (unstyle_tree in web/dom.c, used by web/cssstyle.c), and every removed node is cleared too, so a detached element never carries a stale style.
The HTML parser gave an empty attribute (class="", value="", id="") the text of the previous quoted attribute value, because the reused text buffer was not reset to an empty string. Pages got wrong classes, ids and input values. Found by comparing 150 random selectors with Chromium. web/dom.c uses the empty string when no value text was read.
Selector matching with several descendant or sibling combinators retried every ancestor for every inner step, which grows very fast on deep pages with large style sheets. match_from in web/cssparse.c returns "fails for all ancestors" and stops retrying (the WebKit approach); results are unchanged.
querySelectorAll kept at most 1,024 pending nodes and silently skipped the rest of large pages. Growable work list.
getElementById walked the whole document every call. 64-entry cache keyed by root, id and a DOM generation counter that every insert, remove and attribute change bumps (g_dom_gen).
Long C loops in the engine never gave the desktop a turn, so a slow page froze the whole OS instead of stopping. nwe_check_stop (Escape, desktop pumping) also runs in querySelector/querySelectorAll and getElementById, and stops any engine call that runs longer than 45 s with "This page stopped".

Popover API: popover attribute (auto, manual, hint), showPopover, hidePopover, togglePopover (with force), beforetoggle/toggle events with oldState/newState, popovertarget/popovertargetaction buttons, light dismiss (click outside or Escape closes the top auto popover, nested popovers stay), :popover-open, and UA styles that hide closed popovers. <dialog>: showModal (modal styles, :modal, focus of [autofocus] or the first control, Escape fires cancel), close with returnValue and an asynchronous close event, and requestClose. The open state is stored in internal attributes (nvx-popover-open, nvx-modal).

Browser: Ctrl shortcuts use the Ctrl state recorded with each key (g_key_ctrl from the keyboard queue) instead of the live key state, so Ctrl+A in the address bar works even when the engine was busy; closing a background tab no longer reloads the current tab; Back addresses kept per tab are no longer cut to 255 characters; navigation and page URLs may be up to 4,095 characters (were 1,023).

Tested: 540 random mutation pages (with shadow roots, clones, hiding and re-showing, moves) give the same layout before and after a forced full restyle and are clean under AddressSanitizer; 150 random combinator selectors give the same counts as Chromium; a GitHub-style repository page with 4,000 rules, 400 rows and the Code menu (popover, tabs, copy button, portal) opens and closes without errors.

Not done: the exact loop on the real GitHub page could not be confirmed here (github.com is not reachable from the build machine); browser look and wider HTML/CSS/JS coverage got no further work in this release.

13.42 Mouse, keyboard, clipboard and container queries (v10.18)

Mouse. mouse_step in graphics/ui/input.c now calls browser_mouse_down, browser_mouse_held and browser_mouse_release, so the engine gets NWE_OP_DOWN, NWE_OP_MOVE (with the held buttons) and NWE_OP_UP instead of one click. The engine passes button, buttons and detail (click count) to JavaScript packed in one key value (0x40000000 | button | buttons << 4 | detail << 8), and __dispatch in web/prelude.js decodes it for MouseEvent and PointerEvent (pointerType "mouse", pressure). Pointer capture retargets moves and ups; getCoalescedEvents returns the event itself.

Gesture Result
Drag in a text field Selection from the press point (sela anchor plus caret), painted in light blue; selectionStart/selectionEnd follow.
Double-click / triple-click in a field Selects the word / the whole value.
Drag over page text __pageSel builds a DOM Selection from the text under the pointer (text_at), the highlight is painted by eng_set_highlight and paint.c (spaces between words included).
Drag a range slider input events while moving, change on release.
Drag a draggable=true element dragstart, dragenter/dragover/dragleave, drop with a DataTransfer, dragend.
Right-click mousedown/mouseup with button 2 and contextmenu; if the page calls preventDefault, the OS menu is not shown.
Middle button auxclick (the OS mouse driver does not report it yet).

Keyboard in fields: Shift with arrows, Home and End selects; Ctrl with arrows, Backspace and Delete works by words; typing replaces the selection; Up, Down, Home and End move by lines in a textarea; readonly fields ignore edits; long single-line fields scroll with the caret, and clicks account for that scroll. Ctrl+A, Ctrl+C, Ctrl+X and Ctrl+V use the OS clipboard (g_clip, 64 KB, shared with the editor) through nwk_clip_get/nwk_clip_set and fire copy, cut and paste events whose clipboardData the page can read or replace; outside fields they copy the page selection or select all page text. navigator.clipboard.readText/writeText and execCommand('copy') use the same clipboard. Tab and Shift+Tab move focus both ways; Space, Shift+Space, Page Up and Page Down scroll; Enter submits the form.

Keyboard layouts: drivers/input/ps2/ps2.c has override tables for Swedish, German, French, Spanish and UK on top of US. kbd_char returns Latin-1 characters (for example 0xE5 for a-ring), AltGr (right Alt, or Ctrl+Alt) gives the third level (@, {, [, \, currency signs), Caps Lock swaps the case of letters, and the keypad / and Enter are mapped. The browser and the desktop take the Latin-1 byte directly; the Terminal passes it to Linux programs as UTF-8. The layout is applied from /etc/nvx-setup-done at boot and changed with kbd_set_layout (Settings > Language & region > Change).

Container queries: @container blocks are parsed at top level and nested (g_parse_cq), rules keep their condition in rule.cq, and css_gather drops a rule whose condition fails for the element. The condition is checked against the nearest ancestor with container-type (and the given container-name) using its content box from the last layout; cqw, cqh, cqi, cqb, cqmin and cqmax use the same box. When a container's size changes, the engine restyles and lays out once more (css_cq_changed).

Sizing: aspect-ratio with a set height and automatic width gives width = height x ratio, also for flex row items; aspect-ratio: auto 4/3 is accepted; extra table width goes to columns without a fixed width first. A sizing page with vh, aspect-ratio, fit-content, min()/max()/clamp(), container queries and percentage heights matches Chromium except rows that depend on font metrics and min-width: fit-content.

New JavaScript: CompressionStream (stored deflate blocks with gzip and zlib framing) and DecompressionStream (real inflate through N.inflateRaw), scheduler.postTask/yield, TaskController, window.event, navigator.share, canShare, userActivation, connection, CloseWatcher, document.getAnimations, ToggleEvent, PromiseRejectionEvent, FormDataEvent, CompositionEvent and other event classes, and interface names such as TreeWalker, CSSStyleRule and the SVG element types for instanceof checks.

Tested: on the host, keyboard and mouse scripts for every row above, 150 DOM mutation seeds with no layout difference, 150 selectors equal to Chromium, the WebAssembly and Service Worker pages, seven app pages with no JavaScript errors and a sanitizer sweep over the page corpus; in QEMU, double-click, slider drag, page text selection, drag-and-drop, right-click, Shift+End, Ctrl+C, Ctrl+V, Shift+Tab and both / keys.

13.43 Hit testing, form control sizes, intrinsic keywords, middle click, text and file drag, key repeat (v10.19)

Prompted by the owner's photo: on Google's sign-in page the e-mail field could not be clicked and its label sat above the field.

Hit testing (hit_rec in web/layout.c): a box or text fragment whose style has pointer-events: none or visibility: hidden is never the target, but its children are still tested (they can turn pointer events back on). Before, such a hit went to the parent element, so a decoration layer over a field (outline and label layers in Material and Google fields) sent the click to the wrapper. Positioned children are tested in z-index order (highest first, later in the page first among equals), then in-flow children, then negative z-index children.

Percentage heights of inline-level boxes: measure_atom gets the definite content height of the block that lays out the line (g_icbh), so height: 100% on a field or inline block inside a sized container works.

Form controls: text fields are content-box with 2px/3px padding and a 1px border (the same outer size as Chromium's 1px/2px padding and 2px border), no fixed height; width = size x 0.58 em + 1.5 em, height = the normal line height; text areas are cols x 0.6 em + 15px wide and rows x line height high; buttons have 2px/7px padding. The old size, cols and rows width/height hints and the intrinsic aspect ratio of controls are gone.

Intrinsic keywords: intrinsic_content measures a box as if its width were auto; clamp_w uses it for min-width/max-width with min-content, max-content and fit-content, and flex items use it for the min/max main size. Font metrics follow Chromium: line height = round(ascent) + round(descent) + round(line gap), and one pixel moves from ascent to descent when the descent was rounded down. ch uses the width of "0" in the element's font, lh the element's line height.

Middle button: in_btn_raw counts presses in mouse_mpress; the desktop sends a press over the Browser to browser_mclick, which sends NWE_OP_DOWN/NWE_OP_UP with button 3. If the page does not cancel auxclick and the target is a link, the engine returns ret = 2 and the link opens in a new tab.

Text drag: a press inside the current page selection (__pageSel checks Range.comparePoint) or inside a field selection (__tdragArm from the engine) arms a drag instead of starting a new selection. After 5 px the polyfill fires dragstart with text/plain, then dragenter/dragover/drop; on an editable target that does not cancel drop the text is inserted at the drop point (N.caretAt, magic 66) with insertFromDrop input events, and removed from a source field (deleteByDrag).

File drag: dropping a file from the Files app on the Browser calls browser_drop_file, which sends the bytes with NWE_OP_DROPFILE (16). __fileDrop builds a File (type from the extension) in a DataTransfer (files, items, type Files); if the page cancels dragover it gets drop, otherwise an <input type=file> (or its label) under the pointer receives the file with input and change events.

Key repeat: ps2.c ignores the keyboard's own repeats and repeats the last held key from the timer interrupt (ps2_kbd_tick) after g_krep_delay ticks every g_krep_rate ticks (Slow 700/60 ms, Normal 500/30 ms, Fast 300/20 ms; Settings > Language & region > Key repeat, kept as krep= in /etc/nvx-setup-done). Modifiers, locks, Escape and hotkeys do not repeat. USB keyboards use the same timing. Repeated keys carry a flag through the keyboard queue (g_key_rep), so the browser sends keydown with repeat: true and no extra keyup.

addEventListener, removeEventListener and dispatchEvent called without an object use window.

Tested: sizing pages against Chromium (59 + 51 + 19 boxes; remaining differences are the default serif font for ch and 3 px on 16 px fields), 80 DOM mutation seeds, seven app pages without errors, host scripts for text and file drops; in QEMU a field under an overlay, held keys, middle click and a text drag.

13.44 Connection reuse, parallel fetching, persistent caches, page timing and more interfaces (v10.20)

Prompted by the owner's question why Google's sign-in page is slow. The analysis: the page needs several megabytes of scripts from several servers, and every request paid for a new connection and a new TLS handshake.

Connections (apps/internet/webglue.c): requests now go through the TCP code that Linux programs use (sys/linux/lxsock.c), which keeps many connections apart, retransmits and advertises a 64 KB window. A pool of 12 connections (pconn) is keyed by host, port and scheme; each HTTPS entry has its own tls_io buffer and tls_sess. Requests send Connection: keep-alive; http_recv reads exactly one response (Content-Length, chunked with chunk_done, or until close for old servers) so the connection can be used again. A reused connection that turns out to be closed is retried once on a fresh one. Idle connections are dropped after 50 s, when the server closed them, or when unexpected data arrived. A lost first SYN is resent after 250 ms (lxs_resyn). The e1000 and RTL8169 drivers keep 128 receive descriptors instead of 32, so a full 64 KB window from several servers is not dropped.

Parallel fetching: before the HTML is parsed, prefetch_scan in web/engine.c collects up to 32 <script src>, <link rel=stylesheet>, modulepreload and preload (script, style, font) addresses and calls nwk_prefetch. That sends up to 6 requests at a time (at most 4 per server) on separate connections, reads the answers in order and keeps them for 60 s (pf_store); the normal fetch for the same address then takes the stored answer (pf_take) and goes on to the cache as usual. Cookies set by those answers are applied at once.

Caches: compiled scripts (bytecode) and cacheable static files with an expiry time or a validator are written to webcache.bin (at most 28 MB, most recently used first, at most once a minute and at power off or restart, never uploaded to the cloud) and read back at the first page after boot; the file starts with the build time so bytecode from another build is ignored. Cached files now carry ETag and Last-Modified; an expired file is fetched with If-None-Match/If-Modified-Since and a 304 answer serves the stored copy. Clearing cookies and site data also clears this file.

Time limits: 30 s per script plus 1 s per 100 KB of source (up to 60 s more), 90 s of script time per page, and the engine's stop after 120 s now ignores time spent waiting for the network.

Timing: about:perf (show_perf in apps/internet/browser.c) shows the last page: network totals from nwk_netstat (requests, bytes, cache hits, parallel fetches, name lookups, connecting, handshakes, waiting for the first byte, downloading) and engine totals from nwe_perf, fetched with NWE_OP_PERF (scripts, compiling, styles, layout, painting), plus the time spent moving the pointer while the page was busy (busy_pump, measured well under 1%). The status bar shows "Done in ... s: network ..., scripts ..., layout ..." after each load.

Interfaces added in web/prelude.js:

Area What
Web Animations element.animate interpolates numbers with units, colours and transform functions between keyframes (array or object form, offsets, per-keyframe easing) with duration, delay, endDelay, iterations, direction, easing (keywords, cubic-bezier, steps) and fill; Animation with play, pause, reverse, finish, cancel, currentTime, playbackRate, finished, onfinish; KeyframeEffect, DocumentTimeline, getAnimations. Values are written to the inline style and restored afterwards.
Security trustedTypes with policies, TrustedHTML/TrustedScript/TrustedScriptURL (accepted wherever strings are).
Components attachInternals and ElementInternals (form value included in FormData, states, validity, ARIA reflection).
Sign-in navigator.credentials (get returns null, password and federated credentials can be created), PublicKeyCredential reporting no platform authenticator and no conditional mediation.
Media navigator.mediaDevices with no devices, MediaStream/MediaStreamTrack, canvas.captureStream; a Web Audio graph (AudioContext, OfflineAudioContext and the usual nodes and params) that keeps time but produces no sound; speechSynthesis with no voices.
Fonts and canvas FontFace (URL or binary sources, added as @font-face rules), document.fonts.add/delete/has, OffscreenCanvas backed by a hidden canvas.
Performance PerformanceMark, PerformanceMeasure with marks or options, the navigation entry, paint entries at load, PerformanceObserver with buffered, performance instanceof Performance.
Other URLPattern, cookieStore, ClipboardItem with navigator.clipboard.read/write, document.caretPositionFromPoint/caretRangeFromPoint (native N.textAt), window instanceof Window, elements with an id (and named forms, frames and images) as window properties through a named-properties object behind the global, Touch/TouchList, navigator.getGamepads, wakeLock, keyboard, bar objects, more element and SVG filter interface names, WebGL interface names (contexts are still not available), and the missing on... handler properties.

Scroll wheel: the PS/2 IntelliMouse wheel byte and the vmmouse wheel value count a turn towards the user as positive, while USB HID and the rest of NoovexOS count up as positive; both are now negated, so the QEMU pointer and PS/2 mice scroll the right way.

Not done: no JavaScript JIT; the QuickJS interpreter itself was profiled (a virtual-machine style benchmark is about 11 times slower than Node, large bundles are dominated by parsing) but not changed; streaming fetch bodies still arrive at the end; WebGL, WebRTC and real audio output are not available.

13.45 Faster AES-GCM, TCP window scaling, after-load timing (v10.21)

AES (lib/crypto/nvxtls.c): aes128_enc now uses one 256-entry table of the combined S-box and MixColumns step (TLS_TE0) with rotations for the other three columns and the plain S-box for the last round; the round keys stay in the old 176-byte format, so nvxwpa.c (WPA2 CCMP) is unchanged and faster too. GHASH uses a 16-entry table per key (gcm_tab_init, gcm_mul, the Shoup 4-bit method). Checked on the host against the old code on 300 random keys, nonces, additional data and lengths up to 20 KB, and against the standard all-zero test vector; 6.6 MB/s before, 97 MB/s after.

TCP (sys/linux/lxsock.c, net/stack/tcpip.c): outgoing SYNs carry the window-scale option (shift 2, g_tcp_syn_ws); if the SYN-ACK has one too, our advertised window is the free space of the 256 KB buffer shifted right by 2 and the peer's window is shifted left by its value. Listening sockets keep unscaled windows. lxs_mv copies 8 bytes at a time. The e1000 and RTL8169 drivers have 256 receive descriptors.

After-load timing: nwe_entry adds every engine call after the load (timer ticks, events, painting) to after_us, tick_us, event_us and longest_us; scripts that run after G.loaded count in after_scripts/after_js_us. When about:perf is opened from a page, perf_after reads those totals and the network counters since the load before the page is replaced.

Network waits: pc_wait calls nwk_busy, which moves the pointer, keeps the freeze watchdog fed and notices a held Esc (then the wait ends). Prefetching uses a 4 s limit (g_pc_tmo) and stops opening connections to a server whose connection or handshake failed in that round; the normal fetch then loads those files.

Engine: 16 MB engine stack (ESTACK_SZ), JS_SetMaxStackSize 8 MB in NoovexOS (3 MB in the host test program), JS_SetGCThreshold 16 MB.

13.46 TLS 1.2, P-256 and HelloRetryRequest, faster DOM bindings, feature tests, wheel events (v10.22)

TLS (lib/crypto/nvxtls.c): supported_groups lists x25519 and secp256r1; the first key share is still x25519 only. parse_serverhello fills a tls_sh (cipher, group, key, HelloRetryRequest flag, cookie) and requires supported_versions 0x0304. A HelloRetryRequest (the fixed SHA-256 random) replaces the transcript with message_hash (0xFE 00 00 20, hash of the first ClientHello), adds the HRR, and sends a change_cipher_spec plus a second ClientHello with the requested group and the cookie; a second HRR, an HRR that asks for the group already sent without a cookie, or a ServerHello for another group is refused. The P-256 code is x509_p256_ecdh in nvxx509.c (Jacobian points with the existing field code, a double-and-add-always loop with masked selection, peer point checked to be in range and on the curve, result wiped). The client change_cipher_spec is sent once. tls_sess keeps both application traffic secrets and the key length; tls_sess_read handles KeyUpdate (traffic upd) and answers update_requested with its own KeyUpdate. Session ID: tls_connect_sess used rnd[0..31] both as the X25519 private key and as the 32-byte legacy session ID, which is sent in the clear, so a passive observer could compute the shared secret. The session ID is now SHA-256 of the client random and the byte 0x53; check.py captures a ClientHello and checks that X25519 of the session ID is not the key share.

TLS 1.2 (tls12_handshake): the ClientHello also lists 0x0303 in supported_versions, the suites 0xC02B, 0xC02F, 0xCCA9 and 0xCCA8, extended_master_secret, an empty renegotiation_info and ec_point_formats. A ServerHello without supported_versions and with legacy version 0x0303 selects TLS 1.2; a server random ending in DOWNGRD 01 or 00 is refused. Only the ServerHello itself goes into the transcript; messages that follow it in the same record are handed on. Certificate (1.2 layout), ServerKeyExchange (named curve x25519 or secp256r1, signature over client random, server random and the parameters, checked with x509_leaf_sig12: 0x0401/0501/0601 PKCS#1, 0x0403/0503/0603 ECDSA, 0x0804-0806 PSS), CertificateRequest (answered with an empty Certificate) and ServerHelloDone are read from plain records. The premaster secret is the ECDH result; the master secret uses prf12 (P_SHA256) with "extended master secret" over the session hash when the server echoed the extension, else "master secret" over the randoms; the key block gives 16-byte AES or 32-byte ChaCha keys and 4-byte (GCM salt) or 12-byte IVs. Records (t12_write, t12_open): GCM uses the salt plus an explicit 8-byte sequence number in front of the ciphertext, ChaCha20-Poly1305 the IV XOR the sequence number; the additional data is sequence, type, version and length. Finished messages are 12 bytes of prf12 over the transcript hash. tls_sess.v12 switches tls_sess_write/tls_sess_read to these records; a HelloRequest is answered with a no_renegotiation warning; tls_get now reads through tls_sess_read.

Host tests in tools/tests/tls/x509h.c (ecdh and get modes, the last arguments are PEM files to trust) are run by check.py against the cryptography module and openssl s_server: TLS 1.3 with X25519, P-256 and an RSA certificate, TLS 1.2 with the four suites, and a proxy that changes the last byte of the ServerKeyExchange, which must be refused.

DOM speed: qsel in web/cssparse.c keeps 64 parsed selectors keyed by root and an FNV hash; results are reused while g_dom_gen is unchanged and the selector has no : (state pseudo-classes can change without a DOM change). lc, ieq and is_ws are inline in nwe.h; attr_get compares the first letter before calling ieq. Native 69 (clsOp) does contains/add/remove/toggle on a token attribute in C (duplicates removed, an unchanged toggle does not rewrite the attribute, -1/-2 for an empty or spaced token so the right DOMException is thrown). attr_set records a 256-bit hash set of on... attribute names; native 70 (onSeen) lets run() skip inlineHandler for event types no element has. kebab and dsKey results are cached in maps.

Sort (third_party/quickjs/quickjs.c): js_array_sort calls vs_natural_sort (runs, reversal of strictly descending runs, binary insertion up to 24 elements, bottom-up merging into a temporary array, a merge is skipped when the two runs are already in order); if memory runs out it falls back to rqsort. The compare function still breaks ties by position, so the result is stable either way.

Ring-3 waits (sys/native/syscall.c): syscalls 51 (wait for a key) and 71 (sleep) set g_frz_hold while they wait and restore it after.

Feature tests: the pages es.html, dom.html and b3.html (kept with the build scripts) list each check as name, function and expected value; Chromium 141 and the host test program both print the failures. Chromium's two failures are its own (SharedArrayBuffer needs cross-origin isolation there, Math.sumPrecise is not in 141).

New interfaces (web/prelude.js, last block): live collections are a Proxy over an array that is refilled when domGen changes; form validity follows the HTML rules for barred elements (disabled, read-only, hidden, buttons), with Chromium's messages; toDataURL writes an uncompressed (stored deflate) RGBA PNG with CRC-32 and Adler-32; cssRules splits the sheet text at top level into rule objects with selectorText, style, nested cssRules for @media/@supports, and insertRule/deleteRule rewrite the text; img.width of an element that is not rendered returns the attribute, then the natural size (web/js.c).

Horizontal scrolling: box.sx is the horizontal scroll offset (the field already existed and was unused) and node.scrollx keeps it across layouts (build_box, apply_scroll_offsets). box_scroll_w walks the box's descendants (not into boxes that clip horizontally, not into fixed boxes) for the right-most edge of boxes and fragments plus the right padding. Painting (paint_box, sub_bounds, absolute boxes, the focus ring), hit testing and caret positions subtract sx like sy. box_rect now subtracts the scroll offsets of the ancestors (it did not, so getBoundingClientRect was wrong inside any scrolled box); box_rect_s(..., 0) keeps the unscrolled position for offsetLeft/offsetTop. scrollLeft reads node.scrollx and clamps writes to 0 .. scroll width minus client width; scrollWidth uses box_scroll_w. A wheel or box scroll fires a non-bubbling scroll event at the element; a change of scrollTop/scrollLeft from a script queues the element (eng_queue_scroll, 16 entries) and the next tick fires its scroll event. Both setters lay out first (js_flush_n) and clamp to the scroll range when the layout is up to date; otherwise apply_scroll_offsets clamps after the next layout.

Wheel: do_wheel(x, y, dy, dx) dispatches wheel at the element under the pointer (deltaY in the low 16 bits of the key, deltaX in the high 16) and scrolls only if it was not cancelled: dx scrolls the nearest box with overflow-x: auto/scroll that can move, dy as before. Listeners keep a passive flag (default true for wheel and touch listeners on window, document, html and body); preventDefault does nothing while a passive listener runs. browser_wheel turns Shift+wheel into deltaX; browser_wheelxy also takes the horizontal amount the GUI loop now reads from mouse_hscroll (touchpads and PS/2 mice). ps2_mouse.c sends the 200-200-80 rate sequence after the wheel mode and uses ID 4 (IntelliMouse Explorer); mouse_feed decodes the fourth byte like Linux (0x40 horizontal, 0x80 vertical, else wheel nibble plus buttons 4 and 5). in_xbtn counts back/forward presses (PS/2 and USB boot or report mice); a back press goes back in the browser.

13.47 Request headers, animation events, frame load events (v10.23)

Request headers (apps/internet/webglue.c): g_nwk_kind tells req_head what kind of request it builds (1 page load, 2 script request, 0 other) and g_nwk_xhdr holds the page's own header lines. For kind 2 the page's headers are copied after removing CR and LF and dropping the forbidden names (xh_forbidden: Host, Cookie, Content-Length, Content-Type (sent separately), Connection, Origin, Referer, User-Agent, Proxy-*, Sec-* and the rest of the Fetch list); Accept: */* is added unless the page set one; Sec-Fetch-Site (same-origin, same-site by registrable domain from reg_dom, else cross-site), Sec-Fetch-Mode: cors and Sec-Fetch-Dest: empty are sent; Origin is sent for posts and cross-origin requests. Kind 1 sends Sec-Fetch-Mode: navigate, Sec-Fetch-Dest: document, Sec-Fetch-User: ?1 and Upgrade-Insecure-Requests: 1. browser.c sets g_nav_from_page when a page started the navigation; a typed address sends no Referer. nwk_fetch_real copies the response header block to nwk_resp.hdrs for kind 2.

Scripts (web/js.c, web/prelude.js): fetch takes a sixth argument (the header block, from hdrBlock) and returns the response headers as a fifth element, parsed by parseHdrs into the Headers of the Response. XMLHttpRequest checks the state in setRequestHeader, joins repeated names with ", ", fills status, statusText (from STATUS_TEXT when the server sent none), responseURL and the headers, fires loadstart, progress, load/error, loadend (and the upload events when there is a body) and sets readyState 4 on errors. The host test program tools/webhost.c can fetch from 127.0.0.1, which is how the test server checks the headers.

Animation events (web/anim.c): queued events carry the event name, the animation or property name and the elapsed time, and duplicates in one frame are dropped. Transitions fire transitionrun when created and transitionstart after the delay, transitioncancel when the element gets display: none, is removed or gets a new transition for the same property; animationcancel fires when an animation is removed. Property names come from css_prop_names. Snapshots skip nodes with NF_NOTRANS (512), which Web Animations set while they apply their own values so their end does not start a CSS transition. engine.c passes each event to js_anim_event, which builds a real AnimationEvent or TransitionEvent and lets it bubble.

Frames: frameScan runs every 40 ms and fires one load event per frame for each src or srcdoc value. For an empty frame, contentDocument is a document from N.parseDoc with open, write and close, and contentWindow an object in the same realm with document, parent, top, eval and a postMessage that delivers to its own listeners.

13.48 Name lookups, frames, streaming, faster built-ins (v10.25)

Lookups (net/stack/tcpip.c): dns_try and arp_resolve poll the card directly; frames that are not their answer now go to lxs_frame_ext (the socket layer) instead of being dropped. A query is resent after 100 and 200 ticks and a server is given 3 s. The cache has 64 entries with an expiry from the answer's TTL (at least 60 s, at most 1 h). pc_get writes host d<ms> t<ms> s<ms> r<n> to the network log for each new connection and keeps the slowest in g_nwk_slow; pc_wait calls nwk_busy always, which beats the main loop so long downloads do not trigger the freeze report.

Frames: N.frameOpen(url, srcdoc) (native 58) hands a srcdoc document to nwk_frame_open_doc, which loads it in the frame engine like a fetched page; frameScan picks the first iframe[srcdoc] or iframe[src].

Streaming: during a page fetch (g_nwk_kind 1) http_recv calls stream_try, which (at most twice, after 0.3 s and 1.5 s, with at least 2 KB new) de-chunks the bytes so far, inflates gzip with pz_inflate_part (returns what it could decode) and calls g_nwk_stream. The browser loads that HTML with btn 2: the engine parses without scripts, lays out and paints; g_nwk_offline makes fetches answer only from the cache.

QuickJS: js_regexp_replace_fast runs lre_exec directly for standard regular expressions without sticky flag or named groups when the replacement is a function or a string without $, collects all matches first and then calls the function with the captures, position and string. map_hash_key finishes with a 32-bit mix and the table grows at one entry per bucket. json_quote_to writes quoted strings in runs; arrays skip the index string and js_json_check for primitives when there is no replacer; numbers, booleans and null are written directly. After a collection the next one starts at twice the live size (at least the value given to JS_SetGCThreshold).