Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions examples/perry-embed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# perry-embed — Bloom rendering inside a Perry UI app

![screenshot](screenshot.png)

A normal Perry UI app whose viewport is a live Bloom 3D scene. The title label
and the stack are plain Perry UI; the viewport is a `BloomView`.

```bash
perry compile main.ts -o perry-embed && ./perry-embed
```

No Perry fork, and no Windows-only path — `BloomView` merged upstream
(perry #2395, extended by #5519) and Perry implements it on **windows, macos,
ios, tvos, visionos, android and gtk4**.

## How it fits together

Perry UI owns the window and the run loop. `BloomView(w, h)` reserves a native
view in Perry's own view tree; `bloomViewGetNativeHandle(view)` hands out that
view's platform handle (`HWND` / `NSView*` / `UIView*` / `GtkWidget*` /
`ANativeWindow*`). Bloom attaches its GPU surface to the handle with
`attachToNativeView()` and renders into it.

**Perry UI does not link, or know about, Bloom.** It reserves a native view and
exposes a handle; anything can render into it, and apps that never call
`BloomView` pull in nothing extra. It is the same shape as Flutter's
PlatformView — Flutter never learns about Flame.

## Three rules, each easy to get wrong

1. **The host owns the run loop.** Drive Bloom's frame from `onFrame` (re-armed
each frame). Never `runGame` — it blocks forever and deadlocks the UI.
2. **Attach on the first frame the handle is non-zero**, not merely the first
tick. The native view exists immediately; its handle is only usable once the
window is actually on screen.
3. **Use `attachToNativeView`.** It is portable and returns whether it worked.
The `attachToHwnd` / `bloomViewGetHwnd` pair is Windows-only and deprecated
at *both* ends (Bloom's side returns `void`, so you cannot even tell if it
failed).

## Related

Perry ships a smaller 2D version of this at `examples/bloomview_embed_demo.ts`
in the Perry tree. This one is the 3D counterpart: lighting, a camera, and depth.
122 changes: 122 additions & 0 deletions examples/perry-embed/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Bloom rendering inside a normal Perry UI app — perry issue #2395 / #5519.
//
// The window chrome (title label, vertical stack) is plain Perry UI. The large
// viewport is a `BloomView`: Perry UI reserves a native view in its own view
// tree and hands out that view's platform handle. Bloom attaches its GPU
// surface to the handle and renders a live 3D scene into it, while the
// surrounding Perry UI stays fully interactive.
//
// The split is the whole point: **Perry UI does not link, or know about, Bloom.**
// It reserves a native view and exposes a handle; anything can render into it.
// Apps that never call `BloomView` pull in nothing extra. (Same shape as
// Flutter's PlatformView — Flutter never learns about Flame.)
//
// Three rules this demo exists to demonstrate, each of which is easy to get
// wrong:
//
// 1. The host owns the run loop. Drive Bloom's frame yourself via `onFrame`
// (re-armed each frame) — NEVER `runGame`, which blocks forever and would
// deadlock the UI.
// 2. Attach on the first frame the handle is NON-ZERO, not merely the first
// tick. The native view exists immediately; its handle is only usable once
// the window is actually on screen.
// 3. `attachToNativeView` is portable and returns whether it worked. Do not
// reach for the Windows-only `attachToHwnd`/`bloomViewGetHwnd` pair — both
// are deprecated aliases at their respective ends.
//
// Perry implements BloomView on windows / macos / ios / tvos / visionos /
// android / gtk4, and `bloomViewGetNativeHandle` returns the right thing for
// each (HWND / NSView* / UIView* / GtkWidget* / ANativeWindow*). Bloom's
// `attachToNativeView` takes any of them, so the code below is not
// Windows-specific.
//
// Run (with a stock Perry — no fork required since #2395 merged):
// perry compile main.ts -o perry-embed && ./perry-embed

import { App, VStack, Text, BloomView, bloomViewGetNativeHandle, onFrame } from 'perry/ui';
import {
attachToNativeView,
beginDrawing, endDrawing, clearBackground,
beginMode3D, endMode3D,
drawCube, drawSphere,
setAmbientLight, setDirectionalLight,
getTime, vec3,
} from 'bloom';

const VIEW_W = 820;
const VIEW_H = 480;

// Perry UI reserves the native view; Bloom renders into it.
const view = BloomView(VIEW_W, VIEW_H);

let attached = false;

function frame(_timestampMs: number, _deltaMs: number): void {
// Rule 2: the handle is only usable once the window is on screen, so keep
// trying until it is non-zero rather than assuming the first tick is late
// enough. Rule 3: attachToNativeView tells us whether it actually worked.
if (!attached) {
const handle = bloomViewGetNativeHandle(view);
if (handle !== 0) {
attached = attachToNativeView(handle, VIEW_W, VIEW_H);
if (attached) {
setAmbientLight({ r: 120, g: 140, b: 180, a: 255 }, 0.40);
setDirectionalLight(vec3(0.6, 0.9, 0.4), { r: 255, g: 235, b: 205, a: 255 }, 0.85);
}
}
}

if (attached) {
const t = getTime();
const cx = Math.cos(t * 0.6) * 9.0;
const cz = Math.sin(t * 0.6) * 9.0;

beginDrawing();
clearBackground({ r: 18, g: 22, b: 34, a: 255 });

beginMode3D({
position: vec3(cx, 6.0, cz),
target: vec3(0, 0.5, 0),
up: vec3(0, 1, 0),
fovy: 45.0,
projection: 0.0,
});

// Ground plane.
drawCube(vec3(0, -0.6, 0), 40.0, 0.4, 40.0, { r: 40, g: 50, b: 70, a: 255 });

// A spinning ring of cubes around a glowing core.
const N = 8;
for (let i = 0; i < N; i++) {
const a = (i / N) * Math.PI * 2.0 + t;
const x = Math.cos(a) * 4.0;
const z = Math.sin(a) * 4.0;
const h = 1.2 + Math.sin(t * 2.0 + i) * 0.6;
drawCube(
vec3(x, h * 0.5, z),
0.9, h, 0.9,
{ r: 120 + i * 14, g: 200 - i * 10, b: 240, a: 255 },
);
}

drawSphere(vec3(0, 1.4, 0), 1.1, { r: 255, g: 210, b: 120, a: 255 });
drawSphere(vec3(0, 1.4, 0), 1.6, { r: 255, g: 180, b: 90, a: 60 });

endMode3D();
endDrawing();
}

onFrame(frame); // rule 1: re-arm; the host drives us, we never drive it
}

onFrame(frame);

App({
title: 'Perry UI × Bloom',
width: 880,
height: 600,
body: VStack(10, [
Text('🌸 Bloom engine rendering inside a Perry UI app — perry #2395 / #5519'),
view,
]),
});
12 changes: 12 additions & 0 deletions examples/perry-embed/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "bloom-perry-embed",
"version": "0.1.0",
"dependencies": {
"bloom": "file:../../"
},
"perry": {
"allow": {
"nativeLibrary": ["bloom", "bloom/core"]
}
}
}
Binary file added examples/perry-embed/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 13 additions & 6 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,20 @@ export function closeWindow(): void {
}

/**
* @deprecated Windows-only, and returns nothing so you cannot tell whether the
* attach actually worked. Use {@link attachToNativeView} — it is portable
* (NSView* / UIView* / GtkWidget* / ANativeWindow* / HWND) and returns a
* boolean. Perry's `bloomViewGetHwnd` is likewise a deprecated alias of
* `bloomViewGetNativeHandle` (perry #5519), so a caller on this path is using
* the old name at BOTH ends.
*
* Embed Bloom inside a host-provided native window — e.g. a Perry UI
* `BloomView` widget. Pass the window handle from `bloomViewGetHwnd(view)`
* and the logical viewport size. Bloom builds its render surface on that
* window and subclasses it for resize/input; the host owns the message loop,
* so drive frames yourself with `beginDrawing()` / `update` / `endDrawing()`
* (do NOT call `runGame`, which blocks). Call once, after the host window is
* shown and laid out (e.g. on the first `onFrame` tick).
* `BloomView` widget. Pass the window handle and the logical viewport size.
* Bloom builds its render surface on that window and subclasses it for
* resize/input; the host owns the message loop, so drive frames yourself with
* `beginDrawing()` / `update` / `endDrawing()` (do NOT call `runGame`, which
* blocks). Call once, after the host window is shown and laid out — i.e. on the
* first frame where the handle is non-zero, not merely the first tick.
*/
export function attachToHwnd(hwnd: number, width: number, height: number): void {
bloom_attach_hwnd(hwnd, width, height);
Expand Down
9 changes: 8 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
export {
initWindow, closeWindow, windowShouldClose, attachToHwnd, resize,
initWindow, closeWindow, windowShouldClose, resize,
// Embedding (Perry UI `BloomView`, or any host that owns a native view).
// attachToNativeView is the portable one and is what new code should use;
// attachToHwnd is Windows-only, returns nothing, and is kept for the existing
// callers. Until now only the DEPRECATED one was reachable from the `bloom`
// root, which is a good way to make everyone write the wrong call.
attachToNativeView, attachToNSView, attachToUIView, attachToSurface,
attachToHwnd,
beginDrawing, endDrawing, takeScreenshot, clearBackground, setEnvClearFromHdr,
setTargetFPS, getDeltaTime, getFPS, getTime,
getScreenWidth, getScreenHeight,
Expand Down
Loading