Porting from 1.4 to 2.0
Migrate an application from Ultralight 1.4 to 2.0.
On this page
You can update an application from 1.4 to 2.0 by focusing on the specific subsystems your code touches. Most CPU-rendered apps need only a handful of edits— the heavier work lands on GPU drivers, Overlay users, custom clipboards, JSHelpers users, and C embedders.
Custom FileSystem, FontLoader, Logger, and ThreadFactory implementations compile against 2.0 untouched. A custom Surface gains one required method, Scroll().
At a Glance
- Everyone — recompile everything against the 2.0 SDK (the ABI changed) and skim Behavior Changes.
- AppCore apps —
Overlayis gone (the panel layout replaces it), window geometry moved to logical pixels, andWindowListenersignatures changed. - GPUDriver implementers — plan to update your entire driver for seven shader programs, new blend state, and new texture flags.
- Custom Clipboard implementers — the interface is replaced with whole-payload methods.
- Custom Surface implementers — add one required method,
Scroll(). The dirty area is also available as a list of rectangles. - JSHelpers users — the header is removed, replaced by the typed bridge.
- C embedders — every callback setter gained a fourth argument, and a few signatures changed.
Build and Toolchain
- The public headers now require C++20— compiling with an older compiler or language mode triggers a
#error. On Windows, you'll need Visual Studio 2022 or later with/std:c++20. - The SDK ships as a CMake package. Use
find_package(Ultralight REQUIRED), linkUltralight::AppCore, and callultralight_copy_runtime_files()to stage the runtime files. See Linking to the Library to configure your build targets. - The package layout is flat. Libraries live in
bin/andlib/with no per-OS subfolders, and reference platform implementations (GPU drivers, font loaders, and file systems that back AppCore) ship as Zlib-licensed source in the package's platform folder.
📘 Two headers share a name now
The new generated
<Ultralight/Config.h>(edition and feature macros, pulled in by every Ultralight header) is a different file from<Ultralight/platform/Config.h>(theConfigstruct you fill in). 1.4 had only the latter.
The generated header defines UL_HAS(x) for feature checks (eg, #if UL_HAS(MEDIA)) and UL_EDITION_AT_LEAST(x) for edition checks. These preprocessor gates remove declarations from Free-edition headers entirely, so referencing them causes a compile error instead of a runtime failure.
Behavior Changes
These changes affect how a rebuilt application runs without triggering compile errors.
- Text renders differently — Kerning and ligatures now apply (1.4 never shaped text), and a font profile controls glyph weight and hinting (
Config::font_gammanow defaults to0, which inherits fromConfig::font_profile). Under AppCore, generic CSS font families come from the host OS (monospaceresolves to Menlo on macOS and Consolas on Windows, sized at 13px). Metrics land much closer to mainstream browsers. See Text Rendering and Fonts to configure font profiles and the generic font families. - GPU-rendered Views use analytic rendering by default — Paths, strokes, and text now render more sharply. Set
Config::enable_photonandConfig::enable_photon_texttofalseto keep the 1.4 look. - Submitting an invalid form now blocks — Constraint validation runs the way browsers do. The
invalidevent fires, and the first invalid control takes focus. CallingreportValidity()checks the form without drawing a message bubble. Addnovalidateto a form to submit without validation. See Forms and Inputs to handle form validation and submission. - The compositor is enabled by default —
ViewConfig::enable_compositordefaults totrue(wasfalse). Elements with 3D or animated transforms, animated opacity, video,will-change, ortranslateZ(0)now render into their own layers— each layer uses extra memory. - WebAssembly is no longer available — The global
WebAssemblyobject is now undefined on all platforms (1.4 supported it on macOS and Linux, but never on Windows). Pages that check for the feature switch to their JavaScript fallback— pages that depend on it stop working. - Two
ViewConfigdefaults changed —ViewConfig::preferred_color_schemenow defaults toColorScheme::Auto, so AppCore apps match the OS dark mode and apply page dark styles (apps without AppCore are unaffected, andColorScheme::Lightkeeps the 1.4 light look).ViewConfig::clipboard_read_policynow lets page script in local content (file:///pages and HTML fromView::LoadHTML()) read the clipboard on a click or key press, excluding remote web pages. See Dark Mode and Color Schemes to configure theme settings and color schemes. - Editing follows the OS under AppCore —
Settings::match_native_editing_behaviordefaults totrue, so selection and caret behavior match each platform. Views you create manually keep uniform cross-platform editing unless you enableViewConfig::match_native_editing_behavior. - Memory management runs automatically —
Config::recycle_delaysets the interval between automatic lightweight recycles and defaults to0.5seconds (was4.0seconds), with0disabling them. Idle pages reclaim JavaScript heaps automatically (Config::idle_gc_enabled), andConfig::memory_cache_sizedefaults to 256 MiB (was 64 MiB). If you target memory-constrained devices, configure these values explicitly. See Managing Memory to tune memory profiles, cache limits, and reclamation. - The default User-Agent reports the host platform and
Ultralight/2.0.0— Version 1.4 reported Windows on every OS, so sites that sniff the User-Agent string may serve different content on macOS and Linux. OverridingViewConfig::user_agentis unavailable in the Free edition. - Smooth scrolling uses a physics-based spring model — The rewritten scrolling logic changes scroll feel to match macOS and iOS.
- CSS transitions take precedence over
!importantdeclarations — This change aligns with the CSS specification. Pages where transition targets use!importantanimate instead of snapping instantly. LogLevelgainedFatalandDebug—Fatalcomes first andDebuglast, so every level's number changed (in C,kLogLevel_Erroris now1). Updateswitchstatements in a customLoggerand any level numbers hard-coded in C or C# code. AppCore's default logger shows a message box for everyLogLevel::Fatalmessage.
Core API
RefPtr's boolean conversion is now anexplicit operator bool. Conditional checks likeif (ptr)still compile, but implicit conversions (such asbool ok = ptr;or passing aRefPtrto aboolparameter) requireptr.get() != nullptrinstead.- The
Config::animation_timer_delayandConfig::scroll_timer_delayfields are removed. Animations advance when you callRenderer::RefreshDisplay(). To throttle an individual View, setViewConfig::max_render_fpsor callView::set_max_render_fps()(0leaves the View unthrottled). KeyEventnow owns platform data— zeroing or copying it withmemsetormemcpycauses a crash. Construct and copy instances using standard C++ constructors and assignment.BitmapFormatgained new values, so updateswitchstatements to handle the new cases. Compressed formats don't have a bytes-per-pixel value— callGetBitmapFormatInfo()to get format details.View::LockJSContext(),View::JavaScriptVM(), andView::EvaluateScript()each take an optional frame name to target an<iframe>. The parameter defaults to the main frame, so existing call sites compile unchanged.Renderer::Recycle()reclaims memory on demand, andRenderer::PostTask()schedules work on the renderer thread from any thread.- Custom
Surfaceimplementations gain one required method,Scroll(), which shifts a rectangle of pixels when the page scrolls (most implementations are one call toSurface::ShiftPixels(), returningfalse). The dirty area is also available as a list of rectangles (dirty_rect_count(),dirty_rect()).dirty_bounds()is still their union, so existing display code keeps working. In C,ULSurfaceDefinitiongained ascrollfield appended last— zero-initialize the struct, and the library handles aNULLcallback itself. See Render Surfaces to implement scrolling and handle dirty rectangles.
GPUDriver Implementations
Custom GPU drivers require substantial updates for 2.0. See Implementing a GPUDriver to implement the complete driver interface.
- Seven shader programs are required instead of two.
FilterBasic,FilterBlur,FilterDropShadow,FillPhoton, andFillPhotonGridjoinFillandFillPath. All programs are mandatory because the library never queries for per-program support. Stock shaders for every program ship in the SDK's platform/shaders folder in compiled or generated form for each backend. GPUDriver::CreateTexture()takes a thirdflagsparameter (kGPUTextureFlag_RenderTarget,kGPUTextureFlag_Antialiased, andkGPUTextureFlag_Immutable). Texture flags now determine whether a texture is a render target, rather than checking for an empty bitmap.GPUDriver::UpdateTexture()takes adirty_rectparameter as an optimization hint. You can upload only that region, or ignore the hint and upload the entire bitmap.GPUStategainedblend_src_factor,blend_dst_factor,blend_equation,texture_4_id, anduniform_integer. Because the struct's byte layout changed, re-mirror your constant buffers and recompile. Map the blend fields directly to your graphics API's blend state— the renderer emits explicit blend state per draw instead of assuming premultiplied alpha.VertexBufferFormatgained a third layout,_2f_2ui, with the matchingVertex_2f_2uistruct (float pos[2],uint32_t header_addr,uint32_t path_slot). TheFillPhotonGridprogram draws with it, so a driver that hardcodes two input layouts needs a third.CommandType::Flushis new. Submit pending GPU work before executing the next command (Flush()on Direct3D 11 orglFlush()on OpenGL, and typically a no-op on Direct3D 12, Metal, or Vulkan where pipeline barriers order accesses).GPUDriver::GetDeviceCaps()is new and optional as the driver's only non-pure virtual method. Override it to report MSAA support, compressed texture formats, and size limits, or omit it to let the engine substitute conservative defaults.- Render targets can now be single-channel. Back an
A8_UNORMrender target with a renderable one-channel format (such as R8 on modern graphics APIs) and skip the alpha swizzle that sampled A8 textures use.
The C ULGPUDriver struct mirrors these changes. Its create and update callbacks take the new parameters, ULGPUState includes the new blend and texture fields, and the struct adds a get_device_caps function pointer.
Windows and AppCore
Moving from Overlay to Panels
Overlay is removed and replaced by the panel layout system. A panel is the unit of web content, and the window manages input, focus, and painting for each panel automatically:
///
/// 1.4: create an Overlay covering the window.
///
auto overlay = Overlay::Create(window, 1024, 768, 0, 0);
overlay->view()->LoadURL("file:///app.html");
///
/// 2.0: a bare AddPanel() fills the whole window.
///
auto panel = window->AddPanel();
panel->view()->LoadURL("file:///app.html");
Overlay calls map to window panel operations as shown below. See Laying Out Panels to construct and arrange the layout tree.
| 1.4 | 2.0 |
|---|---|
Overlay::Create(window, view, x, y) |
window->AdoptPanel(view, options) |
overlay->view() |
panel->view() |
overlay->Hide() / Show() / is_hidden() |
panel->Hide() / panel->Show() / panel->is_hidden() |
overlay->Focus() |
panel->Focus() |
overlay->Unfocus() |
window->ClearFocus() (clears focus for all panels) |
overlay->has_focus() |
window->focused_panel() (compare the handle) |
overlay->width() / x() |
panel->device_bounds() for device pixels (matching 1.4) or panel->bounds() for logical pixels— both return the resolved layout and are read-only. |
overlay->MoveTo() / Resize() |
Declare sizes in the panel's options— the window re-resolves the layout on every resize or DPI change. A panel you place freely belongs in the foreground layer with an anchor (see Floating Panels). |
overlay->NeedsRepaint() |
No replacement. The window paints panels itself, so nothing needs to poll. |
Window and Monitor Geometry
Window::width(), Window::height(), Monitor::width(), and Monitor::height() now return logical double pixels instead of device pixels (macOS monitors already used logical values). The method names haven't changed, so existing call sites still compile— but they return different values whenever the scale factor isn't 1.0. Window::Create(), MoveTo(), x(), and y() keep the same units, but now use double. Assigning those values to an int truncates them.
Device-pixel dimensions are now returned by device_width() and device_height() on both Window and Monitor. The conversions ScreenToPixels() and PixelsToScreen() are replaced by LogicalToDevice() and DeviceToLogical(). The old screen_width() and screen_height() functions are removed.
WindowListener Callbacks and WindowFlags
WindowFlags is now an enum class. For example, kWindowFlags_Titled | kWindowFlags_Resizable becomes WindowFlags::Titled | WindowFlags::Resizable. The C API enumerator names remain unchanged.
Mouse, keyboard, and scroll callbacks in WindowListener gained a leading Window* parameter, and OnResize() now receives logical double dimensions.
🚧 Mark your WindowListener overrides with
overrideA 1.4
OnKeyEvent(const KeyEvent&)override still compiles in 2.0— it declares a brand-new function and silently stops being called. Theoverridekeyword turns that into a compile error, so add it before you build.
Smaller AppCore Changes
Settings::load_shaders_from_file_systemand its C setter are removed with no replacement.App::RunOnce()is new. It executes a single iteration of the application event loop, allowing you to run AppCore from an existing main loop instead of passing thread control toRun().- The 1.4 public AppCore source repository (LGPL) is superseded. Reference implementations previously copied from it (GPU drivers, font loaders, file systems, and clipboards) now ship as Zlib-licensed source in the SDK's platform folder.
Clipboard Implementations
Clipboard::ReadPlainText() and WritePlainText() are replaced by whole-payload methods so a single copy operation can transfer multiple formats (eg, text/plain and text/html) at once:
///
/// 1.4: plain text only.
///
String ReadPlainText() override;
void WritePlainText(const String& text) override;
///
/// 2.0: whole payloads, one entry per format.
///
RefPtr<ClipboardData> Read() override;
void Write(RefPtr<ClipboardData> data) override;
A text-only backend remains simple. Return ClipboardData::Create(text) from Read(). In Write(), write data->AsText() and skip unrecognized data types.
The C ULClipboard struct mirrors these changes. Every callback gained a leading void* user_data parameter, the struct gained a user_data field, and the read and write functions operate on the new ULClipboardData handle rather than ULString.
JSHelpers Is Removed
Replacing JSHelpers with the Typed Bridge
The <AppCore/JSHelpers.h> header is removed, along with all its declarations (including JSValue, JSObject, JSFunction, SetJSContext(), and BindJSCallback). The typed bridge in <Ultralight/js/API.h> replaces these helpers. Unlike JSHelpers, bindings registered through the typed bridge survive page navigation:
///
/// 1.4: set a global context and re-bind on every page load.
///
auto lock = caller->LockJSContext();
SetJSContext(lock->ctx());
JSObject global = JSGlobalObject();
global["ShowMessage"] = BindJSCallback(&MyApp::ShowMessage);
///
/// 2.0: bind once and attach to the View-- bindings survive navigation.
///
js::API api("myApp");
api["ShowMessage"] = js::Bind(this, &MyApp::ShowMessage);
if (api.AttachTo(view.get()))
view->LoadURL("file:///app.html");
The page now calls myApp.ShowMessage() instead of a global ShowMessage().
Keep the js::API object alive (eg, as a member of your app) for as long as pages should have the bindings— destroying it detaches it from every View.
JSHelpers Pattern Map
Common JSHelpers patterns map to the 2.0 typed bridge as shown in this table. See About JavaScript Interop to register callbacks and export classes.
| JSHelpers (1.4) | ultralight::js (2.0) |
|---|---|
SetJSContext(...) global context state |
Not needed (values hold their context) |
global["fn"] = BindJSCallback(&C::Fn) |
api["fn"] = js::Bind(this, &C::Fn) then api.AttachTo(view.get()) |
Re-binding on every OnWindowObjectReady |
Not needed (bindings survive navigation) |
JSValue / JSObject / JSArray |
js::Value (typed reads via To(), Maybe(), Or(), writes via operator[]) |
JSEval("...") |
js::Context(view.get()).Evaluate("...") |
JSGlobalObject() |
js::Context(view.get()).GlobalObject() |
JSFunction + operator() |
js::Value::Invoke() or fn(args...) |
Manual args[0].ToNumber() conversions |
Typed parameters ([](double a) { ... }) |
Using JavaScriptCore Directly
The raw JavaScriptCore C API remains fully supported. You can obtain a context reference by calling View::LockJSContext().
The library's extensions to JavaScriptCore are purely additive. Because JSClassDefinition gained additional members, code using JavaScriptCore must be recompiled rather than relinked. See Using JavaScriptCore Directly to work with native contexts and low-level JavaScript values.
The C API
Every 1.4 callback setter gained a trailing destroy_user_data parameter. Passing NULL indicates there is nothing to free. Every callback registration added in 2.0 takes the same parameter.
The destroy callback runs once when the registration releases its user data, and it never runs while the callback itself is active. You must update every registration call site. See C API Conventions for user-data lifetime and ownership rules:
///
/// 1.4: callback plus user data.
///
ulWindowSetCloseCallback(window, OnClose, my_data);
///
/// 2.0: the same call with the new fourth parameter.
///
ulWindowSetCloseCallback(window, OnClose, my_data, NULL);
- The Overlay C API (the
ULOverlayhandle and its associated functions) is removed in favor of the layout API. CallulWindowAddPanel(window, NULL, NULL)to create a full-window panel, and callulPanelGetView()to access its View. Layout functions are declared in<AppCore/CAPI/CAPI_Layout.h>. ulCreateScrollEvent()gained two cursor-coordinate parameters. Pass-1, -1to preserve 1.4 behavior and dispatch at the last known cursor position.ulConfigSetAnimationTimerDelay()andulConfigSetScrollTimerDelay()are removed. Throttle individual views withulViewSetMaxRenderFps().- Window and monitor geometry moved to logical
doublepixels.ulWindowGetWidth()andulMonitorGetWidth()return logical pixels, whileulWindowGetDeviceWidth()provides the device-pixel measurement.ULResizeCallbackpassesdoublevalues, and screen-coordinate functions are removed. - The C API also gained window-level keyboard, mouse, and scroll callbacks (1.4 had no C way to intercept them), native message boxes,
ulAppRunOnce(), and dedicated setters for everyConfigfield.
What's New
Beyond the changes above, 2.0 adds new subsystems that have no 1.4 counterpart. Each subsystem is documented in its own guide:
- A native DOM API and declarative data bindings to inspect, modify, and bind UI state without JavaScript.
- The typed JavaScript bridge shown above to expose native functions and classes.
- Text editing, IME, and clipboard control, including
View::editor(). - HTML5 video and audio (Pro), compressed GPU textures (Pro), the Profiler (Pro), and render tracing (Enterprise).
- Handling View Events, Sessions and Site Data, Dark Mode and Color Schemes, and the
Color,URL, andJSONvalue types in Working with Colors, Working with URLs, and Working with JSON. - Window backdrops, custom chrome, transparency, and popups under AppCore.
- Frame timing is under your control through the timed form of
Renderer::RefreshDisplay(), declared display refresh rates, and custom clocks. See Updating and Rendering to configure custom animation timing. AppCore syncs animations to each frame's on-screen time for you and enumerates monitors throughApp::monitor().