@indiegems/gem-web-sdk 20.1.0 → 21.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +1 -0
- package/brand/ticket-hero-128.png +0 -0
- package/brand/ticket-hero-256.png +0 -0
- package/brand/ticket-hero-384.png +0 -0
- package/brand/ticket-hero-512.png +0 -0
- package/brand/ticket-hero-768.png +0 -0
- package/brand/ticket-icon-20-1.5x.png +0 -0
- package/brand/ticket-icon-20-1x.png +0 -0
- package/brand/ticket-icon-20-2x.png +0 -0
- package/brand/ticket-icon-20-3x.png +0 -0
- package/brand/ticket-icon-32-1.5x.png +0 -0
- package/brand/ticket-icon-32-1x.png +0 -0
- package/brand/ticket-icon-32-2x.png +0 -0
- package/brand/ticket-icon-32-3x.png +0 -0
- package/brand/ticket-icon-ingame-20-1.5x.png +0 -0
- package/brand/ticket-icon-ingame-20-1x.png +0 -0
- package/brand/ticket-icon-ingame-20-2x.png +0 -0
- package/brand/ticket-icon-ingame-20-3x.png +0 -0
- package/brand/ticket-icon-ingame-32-1.5x.png +0 -0
- package/brand/ticket-icon-ingame-32-1x.png +0 -0
- package/brand/ticket-icon-ingame-32-2x.png +0 -0
- package/brand/ticket-icon-ingame-32-3x.png +0 -0
- package/brand/ticket-pile-1-128.png +0 -0
- package/brand/ticket-pile-1-256.png +0 -0
- package/brand/ticket-pile-1-384.png +0 -0
- package/brand/ticket-pile-1-512.png +0 -0
- package/brand/ticket-pile-1-768.png +0 -0
- package/brand/ticket-pile-2-128.png +0 -0
- package/brand/ticket-pile-2-256.png +0 -0
- package/brand/ticket-pile-2-384.png +0 -0
- package/brand/ticket-pile-2-512.png +0 -0
- package/brand/ticket-pile-2-768.png +0 -0
- package/dist/conformance.d.ts +1 -1
- package/dist/dev-tools.d.ts +1 -0
- package/dist/function.d.ts +11 -3
- package/dist/game.d.ts +79 -12
- package/dist/gem-web-sdk.bridge.js +2 -2
- package/dist/gem-web-sdk.conformance.js +3 -3
- package/dist/gem-web-sdk.dev-tools.js +2 -0
- package/dist/gem-web-sdk.esm.js +3 -3
- package/dist/gem-web-sdk.function.js +3 -3
- package/dist/gem-web-sdk.game.js +3 -3
- package/dist/gem-web-sdk.min.js +3 -3
- package/dist/gem-web-sdk.sdk.js +3 -3
- package/dist/gem-web-sdk.server.js +2 -2
- package/dist/index.d.ts +103 -48
- package/dist/sdk.d.ts +79 -12
- package/docs/analytics.md +12 -0
- package/docs/api/api-reference-function.md +14 -2
- package/docs/api/api-reference-game.md +357 -21
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +413 -16
- package/docs/arcade-economy.md +2 -2
- package/docs/brand-art.md +65 -16
- package/docs/changelog.md +40 -0
- package/docs/game-economy-tutorial.md +6 -3
- package/docs/getting-started.md +122 -30
- package/docs/graphics-route.md +188 -0
- package/docs/inventory-and-catalogs.md +69 -27
- package/docs/manifest.json +23 -18
- package/docs/server-bundles.md +3 -3
- package/docs/what-a-game-must-do.md +33 -2
- package/package.json +5 -1
- package/schema/bridge.v1.json +263 -12
- package/schema/capabilities.public.json +26 -0
package/docs/brand-art.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
The SDK ships the platform's own art for its two currencies, **Gems** and **Coins**, and for
|
|
4
4
|
**trophies**. You need it only if your game draws Gems, Coins or a trophy on its own screens. Read
|
|
5
|
-
§1 before you pick a file: each currency has
|
|
6
|
-
the screen is for.
|
|
5
|
+
§1 before you pick a file: each currency has more than one kind of art, and which one you use
|
|
6
|
+
depends on what the screen is for.
|
|
7
7
|
|
|
8
8
|
> **Draw the platform's current marks with `gem.branding`, and keep these files as the fallback.**
|
|
9
9
|
> `gem.branding.currencyMark('GEMS', { size: 20 })` and `gem.branding.trophy()` hand you the same
|
|
@@ -19,25 +19,35 @@ screen, and how much more your game can still pay (the earn allowance) — and n
|
|
|
19
19
|
own. §5 is the creator's ruling and is not optional.
|
|
20
20
|
|
|
21
21
|
> **The Coins art is a 3D render, open to a designer pass.** It is a gold coin with the platform's
|
|
22
|
-
> gem
|
|
23
|
-
>
|
|
24
|
-
>
|
|
25
|
-
>
|
|
26
|
-
>
|
|
27
|
-
>
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
> gem at its centre, so a player reads it as the Arcade's currency in every game rather than one
|
|
23
|
+
> game's gold. The hero and the two piles (§3) are one coin — painted gold, a thick rounded edge, a
|
|
24
|
+
> faceted gem standing on its face; the hero's also has a thin groove inside its rim and its gem
|
|
25
|
+
> raised on a sloped base — and the icons keep the earlier gold coin, whose rings and gem channel
|
|
26
|
+
> read better in a few pixels. Every coin is turned so its edge shows on the right. This
|
|
27
|
+
> art replaced the gold coin of 2026-10-25, which replaced the tangerine coin of 2026-10-21. The
|
|
28
|
+
> files keep their `ticket-*` names, as the currency keeps its slug `ARCADE_TICKETS`. If the art is
|
|
29
|
+
> redrawn again, the new art replaces these files **under the same names** in a later SDK release,
|
|
30
|
+
> so a game that imports them picks it up on its next SDK update. Lay your screens out from the
|
|
31
|
+
> sizes on this page, not from the drawing.
|
|
32
|
+
|
|
33
|
+
## 1. Pick the currency art by the role of the screen
|
|
30
34
|
|
|
31
35
|
| You are drawing | Use | Drawn at |
|
|
32
36
|
| --------------- | --- | -------- |
|
|
33
37
|
| Gems **inline**, beside a number: a price, a "+15" reward label, a small chip | the **icon**, `gem-icon-20-*` or `gem-icon-32-*` | 16–40 CSS px |
|
|
34
38
|
| Gems as **the subject of a reward presentation**: a victory or reward screen where the gem is what the player looks at, such as a victory overlay with a sunburst behind the reward | the **hero**, `gem-hero-*` | 64–256 CSS px |
|
|
35
|
-
| Coins **inline
|
|
39
|
+
| Coins **inline** in your game's screens, beside a number | the **in-game icon**, `ticket-icon-ingame-20-*` or `ticket-icon-ingame-32-*` | 16–40 CSS px |
|
|
40
|
+
| Coins as **a reward sized by how much it is**: a reward card, a results screen's payout | a **pile**, `ticket-pile-1-*` or `ticket-pile-2-*` (§3) | 64–256 CSS px |
|
|
36
41
|
| Coins as **the subject of a reward presentation** | the **hero**, `ticket-hero-*` | 64–256 CSS px |
|
|
37
42
|
|
|
38
43
|
The role decides first and the size follows from it. A price tag is inline even if you make it
|
|
39
44
|
large; a reward screen is a presentation even on a small phone.
|
|
40
45
|
|
|
46
|
+
`ticket-icon-20-*` and `ticket-icon-32-*` are the **arcade's wallet icon**: the same coin inside the
|
|
47
|
+
violet rim the Gems icon wears, which the arcade draws in its own wallet. Inside your game that rim
|
|
48
|
+
reads as the arcade's chrome, so draw the in-game icon there; a game that already draws the wallet
|
|
49
|
+
icon keeps working, and moves to the in-game one when it next touches the screen.
|
|
50
|
+
|
|
41
51
|
- **Never use the hero at icon sizes.** It is one render drawn to be seen large; shrunk to 20 px its
|
|
42
52
|
outline goes under a pixel, its facet lines vanish and the gem smudges into the background.
|
|
43
53
|
- **Never use the icon at hero sizes.** Each icon file is a separate render drawn for one small box,
|
|
@@ -114,12 +124,30 @@ outline weight — 1.5 px at 20, 2 px at 32.
|
|
|
114
124
|
| | | `ticket-icon-32-2x.png` | 64 × 64 |
|
|
115
125
|
| | | `ticket-icon-32-3x.png` | 96 × 96 |
|
|
116
126
|
|
|
117
|
-
Like the Gems icon, each size is **its own render
|
|
118
|
-
|
|
119
|
-
groove inside the rim, a crease above the edge, a channel round the gem —
|
|
127
|
+
Like the Gems icon, each size is **its own render**, not the hero scaled down: the earlier gold
|
|
128
|
+
coin, turned a little so its edge shows on the right, with the lines that part its rim, field and
|
|
129
|
+
gem — a groove inside the rim, a crease above the edge, a channel round the gem — drawn wider and
|
|
120
130
|
darker the smaller the size, so they still read in a few pixels. The 20 px coin has a slightly
|
|
121
131
|
smaller gem and no ridges on its edge. Each is drawn on its own pixel grid inside the gem icon's
|
|
122
|
-
violet outline.
|
|
132
|
+
violet outline. These are the **wallet icon**: the arcade's wallet draws the same files.
|
|
133
|
+
|
|
134
|
+
### The in-game Coins icon
|
|
135
|
+
|
|
136
|
+
The same coin for **your game's own screens**, without the violet rim. A burnt-orange edge runs
|
|
137
|
+
round its light right-hand side instead, so it stays distinct on a pale background, and its edge
|
|
138
|
+
band is smooth. It comes in the same renditions and ratios, framed exactly as the wallet icon, so
|
|
139
|
+
one swaps for the other in the same box.
|
|
140
|
+
|
|
141
|
+
| Rendition | For a CSS box of | File | Pixels |
|
|
142
|
+
| --------- | ---------------- | ---- | ------ |
|
|
143
|
+
| 20 | up to about 24 px | `ticket-icon-ingame-20-1x.png` | 20 × 20 |
|
|
144
|
+
| | | `ticket-icon-ingame-20-1.5x.png` | 30 × 30 |
|
|
145
|
+
| | | `ticket-icon-ingame-20-2x.png` | 40 × 40 |
|
|
146
|
+
| | | `ticket-icon-ingame-20-3x.png` | 60 × 60 |
|
|
147
|
+
| 32 | above 24, up to about 40 px | `ticket-icon-ingame-32-1x.png` | 32 × 32 |
|
|
148
|
+
| | | `ticket-icon-ingame-32-1.5x.png` | 48 × 48 |
|
|
149
|
+
| | | `ticket-icon-ingame-32-2x.png` | 64 × 64 |
|
|
150
|
+
| | | `ticket-icon-ingame-32-3x.png` | 96 × 96 |
|
|
123
151
|
|
|
124
152
|
## 3. The hero art
|
|
125
153
|
|
|
@@ -147,10 +175,31 @@ size you draw it multiplied by the screen's `devicePixelRatio`:
|
|
|
147
175
|
**Do not draw it larger than 256 CSS pixels.** On a 3× screen that needs more than the largest file,
|
|
148
176
|
and the art softens as it is stretched. Below 64 px, use the icon (§2).
|
|
149
177
|
|
|
178
|
+
### The coin piles
|
|
179
|
+
|
|
180
|
+
Two heaps of the hero's coin, for a Coins reward your game **offers or has just paid** where the
|
|
181
|
+
size of the heap says how much: a reward card, or a results screen's payout. Pile 1 is a few short
|
|
182
|
+
stacks; pile 2 holds about twice the coins.
|
|
183
|
+
|
|
184
|
+
| Pile | Use it for | Files |
|
|
185
|
+
| ---- | ---------- | ----- |
|
|
186
|
+
| 1 | an everyday reward | `ticket-pile-1-128.png`, `ticket-pile-1-256.png`, `ticket-pile-1-384.png`, `ticket-pile-1-512.png`, `ticket-pile-1-768.png` |
|
|
187
|
+
| 2 | a bigger reward | `ticket-pile-2-128.png`, `ticket-pile-2-256.png`, `ticket-pile-2-384.png`, `ticket-pile-2-512.png`, `ticket-pile-2-768.png` |
|
|
188
|
+
|
|
189
|
+
**Pick the pile by where the reward sits among your game's own rewards, not by a number of Coins.**
|
|
190
|
+
Where a reward's size doesn't matter, the hero does as well.
|
|
191
|
+
|
|
192
|
+
Both piles share one frame: a square, transparent PNG with a margin all round, each heap centred
|
|
193
|
+
across and standing on the same baseline. Pile 2 fills the frame and pile 1 is drawn smaller, so the
|
|
194
|
+
two side by side climb from left to right with no scaling in your layout. Draw them at the hero's
|
|
195
|
+
sizes, **64 to 256 CSS pixels**, and pick the file as for the hero above.
|
|
196
|
+
|
|
150
197
|
## 4. Loading and drawing the art
|
|
151
198
|
|
|
152
|
-
The platform serves
|
|
199
|
+
The platform serves the icons and the heroes through `gem.branding` ([branding.md](branding.md)),
|
|
153
200
|
which is what to draw first. The copies below are the fallback for when it answers `unavailable`.
|
|
201
|
+
The in-game Coins icon and the coin piles are not in the platform's manifest yet: draw them from
|
|
202
|
+
here.
|
|
154
203
|
|
|
155
204
|
All of it is in the package you already install, under `brand/`, and published beside it on the CDN —
|
|
156
205
|
the same bytes either way, for the version you are on.
|
package/docs/changelog.md
CHANGED
|
@@ -16,6 +16,46 @@ Its entries are grouped under up to five headings:
|
|
|
16
16
|
- **Rebuild**: a type changed. Recompile against the new version; code that compiles today keeps compiling unless the entry says otherwise.
|
|
17
17
|
- **New capability**: something a game can now do. Nothing changes unless you use it.
|
|
18
18
|
|
|
19
|
+
## 21.1.0
|
|
20
|
+
|
|
21
|
+
Upgrading from 21.0.0. Impact: required.
|
|
22
|
+
|
|
23
|
+
### Must change
|
|
24
|
+
|
|
25
|
+
- ShareApi.request now returns a promise instead of nothing. When the Arcade refuses a share, the promise rejects with a ShareRefusal that names the reason and the offending param. Await the call and handle the rejection, for example by telling the player the share could not be sent. With an older Arcade the promise resolves at once, as before. The SDK also refuses, before sending, share params whose keys the Arcade reserves for its own play links, and the error names the key. If your game uses such a name as its own share param, rename it. A current Arcade carries most reserved keys through a share link and returns them unchanged, but an older one still refuses them.
|
|
26
|
+
- PurchaseRefusalCode has a new value, guest_upgrade_required, returned when a web guest tries to spend platform currency directly. Treat it like any other decline: draw no purchase, and do not retry. If you switch over the refusal codes exhaustively, add a case for it or your code will stop compiling.
|
|
27
|
+
|
|
28
|
+
### Should change
|
|
29
|
+
|
|
30
|
+
- Spend Gems and Coins through the Arcade's purchase sheet, not through a direct spend on the entity mutations. A web guest's direct spend is now refused. The SDK hands that one refusal to the Arcade as a safety net and rejects your call with the original error straight away, without waiting for an answer or retrying. Move any direct platform-currency spend to the purchase sheet flow. See [the deprecation policy](deprecation-policy.md#4-what-is-deprecated-today).
|
|
31
|
+
|
|
32
|
+
### Rebuild
|
|
33
|
+
|
|
34
|
+
- Several types and signatures changed in compatible ways: GemFrame.requestPurchase gained an optional origin argument, GemFrame.requestShare gained an optional requestId argument, the MutationsApi constructor gained an optional hand-off callback, and GemCapability, ParentMessage and PARENT_TO_FRAME gained new capabilities and message kinds for purchase settlement, graphics routes and share results. Recompiling is enough. If you test against your own stand-in for the Arcade, rebuild it against this release and bring it up to the newer parent conformance version.
|
|
35
|
+
|
|
36
|
+
### New capability
|
|
37
|
+
|
|
38
|
+
- PurchaseApi.onSettled lets a game hear a purchase that completed after its original call had already been answered, for example one finished through the Arcade after a guest's refused spend. The game is told once the platform confirms the purchase, as a PurchaseSettlement, and the player's data is refreshed. Subscribe to it so you can grant the purchase when it lands. It only fires on an Arcade that supports it.
|
|
39
|
+
- Gem.graphics lets a game ask which renderer to create just before it creates one. The answer says the route to use and whether the device is on the platform's list of devices known to draw wrongly on WebGL. The answer is WebGL everywhere except on a listed device, and there WebGPU only if it hands out an adapter. A listed device with no working WebGPU gets your safe WebGL recipe. The call is time-bounded and never rejects. The list ships built in, and the Arcade can replace it for live games without a rebuild. Call it before creating a renderer and make every canvas follow the answer.
|
|
40
|
+
- A new dev-tools entry point is available for a game's dev-only code, such as a cheat panel, level skipper or debug menu. Importing it from that code marks the build as carrying dev-only code. Builds so marked can be uploaded and played on the dev channel but are refused on the test and release channels. Keep the import behind a build-time switch that is off for test and release builds so the marker is absent from them.
|
|
41
|
+
|
|
42
|
+
## 21.0.0
|
|
43
|
+
|
|
44
|
+
Upgrading from 20.1.0. Impact: required.
|
|
45
|
+
|
|
46
|
+
### May stop
|
|
47
|
+
|
|
48
|
+
- You do not need to write your own code to count lost WebGL contexts for analytics. Every game now reports each loss automatically, with no code of its own. The SDK only observes the loss, so whether your game restores its context stays your decision, and your own handling keeps working unchanged.
|
|
49
|
+
|
|
50
|
+
### Rebuild
|
|
51
|
+
|
|
52
|
+
- The set of platform event names now includes a new member, gem_graphics_context_lost, which the SDK emits when the browser takes away one of the game's WebGL contexts. Recompiling is enough for most games. If your game switches over every platform event name exhaustively, or maps each name to a value, add a case for the new name so it still compiles.
|
|
53
|
+
|
|
54
|
+
### New capability
|
|
55
|
+
|
|
56
|
+
- A new in-game Coins icon is available in two sizes, 20 and 32, each at four pixel ratios: 1x, 1.5x, 2x and 3x. It is the Coins coin without the violet rim, so it does not look like the Arcade's own chrome inside a game. Use it wherever your game shows Coins. The wallet icon keeps its violet rim.
|
|
57
|
+
- Two coin piles are now available for showing a Coins reward sized by how much it is: pile 1 for an everyday reward and pile 2 for a bigger one. Each comes in five widths: 128, 256, 384, 512 and 768. They share one frame and baseline, so a row of them climbs from the smaller to the larger. Larger piles are reserved for the Arcade's own shop. The Coins hero art remains the choice when the size of the reward does not matter.
|
|
58
|
+
|
|
19
59
|
## 20.1.0
|
|
20
60
|
|
|
21
61
|
Upgrading from 20.0.0. Impact: required.
|
|
@@ -268,7 +268,7 @@ if (av.locked) {
|
|
|
268
268
|
The platform checks the prerequisite **before** it takes any Coins: a locked player who buys
|
|
269
269
|
anyway is refused `recipe_locked` and charged nothing. See
|
|
270
270
|
[`inventory-and-catalogs.md`](inventory-and-catalogs.md#16-selling-for-a-platform-currency) for the
|
|
271
|
-
|
|
271
|
+
full rule, and why platform currency is spent through the arcade's sheet.
|
|
272
272
|
|
|
273
273
|
### Buying it, and reading a refusal
|
|
274
274
|
|
|
@@ -291,8 +291,11 @@ if (res.outcome === 'ok' || res.outcome === 'unavailable') {
|
|
|
291
291
|
}
|
|
292
292
|
```
|
|
293
293
|
|
|
294
|
-
|
|
295
|
-
|
|
294
|
+
Spend Coins and Gems through this sheet, never `gem.me.mutations.purchase`: on gemarcade.com a web
|
|
295
|
+
guest is refused a direct spend (`guest_upgrade_required`), and the sheet is where the arcade offers
|
|
296
|
+
them an account. A recipe your game charges in its own currency (`payment: { kind: 'configured' }`)
|
|
297
|
+
reports the same strings as `err.code` — see
|
|
298
|
+
[`inventory-and-catalogs.md`](inventory-and-catalogs.md#why-a-purchase-was-refused).
|
|
296
299
|
|
|
297
300
|
**Never draw a top-up or payment screen of your own** — no "get more Coins", no checkout, nothing
|
|
298
301
|
shaped like the arcade's purchase sheet. A player short of Coins earns more by playing; your shop
|
package/docs/getting-started.md
CHANGED
|
@@ -50,7 +50,7 @@ either way.
|
|
|
50
50
|
```jsonc
|
|
51
51
|
// package.json
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@indiegems/gem-web-sdk": "
|
|
53
|
+
"@indiegems/gem-web-sdk": "21.1.0"
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -294,21 +294,33 @@ parameters the link should open your game with:
|
|
|
294
294
|
shareButton.hidden = !gem.share.available;
|
|
295
295
|
shareButton.addEventListener('click', () => {
|
|
296
296
|
if (!gem.share.available) return;
|
|
297
|
-
gem.share.request({ params: { jar: 'JAR-K7P3QXM', n: '2', beat: '812' } })
|
|
297
|
+
gem.share.request({ params: { jar: 'JAR-K7P3QXM', n: '2', beat: '812' } }).catch((err) => {
|
|
298
|
+
// A ShareRefusal: the arcade refused this link. err.reason and err.key say
|
|
299
|
+
// what to fix. Log it for yourself; show the player nothing.
|
|
300
|
+
console.warn(err);
|
|
301
|
+
});
|
|
298
302
|
});
|
|
299
303
|
```
|
|
300
304
|
|
|
301
305
|
The arcade builds the link, which unfurls with your game's own card, and shares it through the
|
|
302
|
-
player's share sheet, a copy, or its own Share panel.
|
|
303
|
-
whether
|
|
304
|
-
|
|
305
|
-
|
|
306
|
+
player's share sheet, a copy, or its own Share panel. Nothing about the player comes back — not
|
|
307
|
+
whether they shared, cancelled, or closed the panel. Where the arcade handles it,
|
|
308
|
+
`gem.share.available` is true; outside the arcade, before the bridge is up, and anywhere the
|
|
309
|
+
arcade does not share your game, it is false.
|
|
306
310
|
|
|
307
311
|
`request` checks every parameter against the rules below **before** anything is sent, and
|
|
308
312
|
throws on the first one the arcade would refuse, naming it: more than 16, a key that breaks the
|
|
309
|
-
key rule or is one
|
|
310
|
-
a character a link cannot carry. A mistake shows up at your desk, not as a Share button
|
|
311
|
-
does nothing for players — try it in the SDK's `examples/dev-harness/` with **share**
|
|
313
|
+
key rule or is one the arcade will not carry, or a value that is not a string, is too long, or
|
|
314
|
+
holds a character a link cannot carry. A mistake shows up at your desk, not as a Share button
|
|
315
|
+
that does nothing for players — try it in the SDK's `examples/dev-harness/` with **share**
|
|
316
|
+
ticked.
|
|
317
|
+
|
|
318
|
+
`request` also returns a promise. It **rejects with a `ShareRefusal`** when the arcade refuses
|
|
319
|
+
the link anyway, which only it can judge — the whole link over 2 KB, say — with `err.reason`
|
|
320
|
+
(`reserved_key`, `round_trip`, `too_long`, or a code newer than your SDK) and `err.key`, the
|
|
321
|
+
parameter that caused it when one did. Otherwise it resolves, and resolving says only that the
|
|
322
|
+
arcade took the request, never that a player shared. An older arcade does not answer, so there
|
|
323
|
+
it resolves at once. Catch it: an uncaught refusal is an unhandled rejection in your console.
|
|
312
324
|
|
|
313
325
|
**Where `available` is false, build the link yourself.** Use the `/games/<id>` page with
|
|
314
326
|
`play=1`, which opens straight into your game and unfurls with its card, and hand it to the
|
|
@@ -343,16 +355,18 @@ const jarIndex = Number.isInteger(n) && n >= 1 && n <= JAR_COUNT ? n : null;
|
|
|
343
355
|
|
|
344
356
|
That is the whole contract for your own keys. The bridge carries no launch message, and the SDK
|
|
345
357
|
has no API for them: the arcade does the forwarding, so a build you published on any SDK version
|
|
346
|
-
gets it without a code change.
|
|
347
|
-
`gemChallenge`, which belongs to [friend challenges](friends.md#friend-challenges),
|
|
358
|
+
gets it without a code change. Three keys are not yours, and they are the platform's (below):
|
|
359
|
+
`gemChallenge`, which belongs to [friend challenges](friends.md#friend-challenges),
|
|
348
360
|
`gemPlayWith`, an opaque ticket the arcade adds when it opens your game to play with a friend or a
|
|
349
|
-
party mate, which names nobody your game can recognise
|
|
350
|
-
|
|
351
|
-
|
|
361
|
+
party mate, which names nobody your game can recognise, and `gemGraphics`, which a developer adds
|
|
362
|
+
to force [the graphics route](graphics-route.md#testing-the-gemgraphics-launch-key) while
|
|
363
|
+
testing. Never define any of them, and never parse them yourself. The SDK reads them for you —
|
|
364
|
+
`gem.friends.launchedChallenge()` answers the challenge `gemChallenge` names, or `null`, every
|
|
352
365
|
`gem.rooms.quickJoin()` passes `gemPlayWith` on
|
|
353
|
-
([Landing in a friend's room](rooms-and-matches.md#5e-landing-in-a-friends-room-play-with))
|
|
354
|
-
|
|
355
|
-
|
|
366
|
+
([Landing in a friend's room](rooms-and-matches.md#5e-landing-in-a-friends-room-play-with)), and
|
|
367
|
+
`gem.graphics.preferredRoute()` answers what `gemGraphics` forces — and they are the only launch
|
|
368
|
+
keys it reads. Each is read once, when `createGem` runs, so cleaning your
|
|
369
|
+
URL afterwards does not lose it. Call `createGem` before you clean the URL, or leave these keys in
|
|
356
370
|
place: a key removed before `createGem` runs is never seen, so `launchedChallenge()` resolves
|
|
357
371
|
`null` with no error and a quick-join plays with nobody.
|
|
358
372
|
|
|
@@ -363,12 +377,18 @@ What reaches the frame:
|
|
|
363
377
|
only keys the arcade keeps for itself; it never interprets yours:
|
|
364
378
|
- Store and play-page switches: `play`, `channel`, `purchase`, and `build`, which pins one
|
|
365
379
|
of your own builds when you play it from your Builds page.
|
|
366
|
-
- Platform switches: `devtools`, `devLocale`,
|
|
367
|
-
rewarded TEST ads.
|
|
368
|
-
- The arcade's own sign-in
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
`fbclid`, `ttclid`, `gclid`, `
|
|
380
|
+
- Platform switches: `devtools`, `devLocale`, `gem_test_ads`, the mode of the arcade's
|
|
381
|
+
rewarded TEST ads, and `moments` and `ads`, the dev host's test moment and ad modes.
|
|
382
|
+
- The arcade's own sign-up and sign-in keys: `invite`, a friend's invite code, and `social`,
|
|
383
|
+
the sign-in return marker.
|
|
384
|
+
- Attribution and campaign keys: `_s`, `utm_*` (every key with that prefix, matched
|
|
385
|
+
case-sensitively, so UTM_X and utmost are yours), `fbclid`, `ttclid`, `gclid`, `gbraid`,
|
|
386
|
+
`wbraid`, `msclkid`. The arcade strips the visitor's own campaign and click ids, but a link
|
|
387
|
+
a game shares, or one anyone writes by hand, can still put `utm_*` and the click ids into
|
|
388
|
+
your frame, restored from their escaped form.
|
|
389
|
+
|
|
390
|
+
A link your game shares can still carry most of them: see
|
|
391
|
+
[Sharing a reserved key](#sharing-a-reserved-key) below.
|
|
372
392
|
- `embedder`, which stays what
|
|
373
393
|
[the embedder allowlist](what-a-game-must-do.md#1-the-embedder-allowlist) says: never read
|
|
374
394
|
from the URL — a link cannot even try.
|
|
@@ -376,16 +396,40 @@ What reaches the frame:
|
|
|
376
396
|
A key starting with `_` can never be one of yours: the key rule below requires a leading
|
|
377
397
|
letter, so the arcade keeps that space for itself.
|
|
378
398
|
|
|
379
|
-
- **
|
|
380
|
-
first when a player opens a friend challenge, and the second when it opens
|
|
381
|
-
with someone
|
|
382
|
-
`gem.friends.launchedChallenge()`,
|
|
383
|
-
|
|
384
|
-
|
|
399
|
+
- **Three platform keys.** `gemChallenge`, `gemPlayWith` and `gemGraphics` do reach the frame —
|
|
400
|
+
the arcade adds the first when a player opens a friend challenge, and the second when it opens
|
|
401
|
+
your game to play with someone, and you add the third yourself to test the graphics route — but
|
|
402
|
+
they are the platform's, not yours: read the challenge with `gem.friends.launchedChallenge()`,
|
|
403
|
+
let `gem.rooms.quickJoin()` carry `gemPlayWith`, ask `gem.graphics.preferredRoute()` rather than
|
|
404
|
+
reading `gemGraphics`, and never read any of them from the URL. Do not define a key of any of
|
|
405
|
+
these names, and treat every key beginning with `gem` as reserved: a friend-challenge template
|
|
406
|
+
refuses a param named that way.
|
|
385
407
|
- **Bounded.** At most 16 parameters. A key is a letter followed by up to 63 letters, digits,
|
|
386
408
|
`_` or `-`. A value is at most 512 characters with no control or format characters. An entry
|
|
387
409
|
that breaks a rule is dropped on its own; the rest of the link still arrives.
|
|
388
410
|
|
|
411
|
+
#### Sharing a reserved key
|
|
412
|
+
|
|
413
|
+
A current arcade carries the arcade's own keys in a link your game shares, so you never need to
|
|
414
|
+
rename a parameter for it. It writes each one into the link under an escaped name it never reads
|
|
415
|
+
itself, and puts the original name back before your game reads its URL: share
|
|
416
|
+
`{ island: '7', invite: 'K2' }` and the frame that link opens reads `island=7&invite=K2`. The
|
|
417
|
+
arcade's own uses of those keys are unaffected — an invite it put in a link itself still signs a
|
|
418
|
+
friend up, and still never reaches your game.
|
|
419
|
+
|
|
420
|
+
A restored click id or `utm_` value is whatever the link says. Anyone who edits a share link can
|
|
421
|
+
forge one, and third-party tags in your frame (analytics, ads) will read it as attribution. Never
|
|
422
|
+
trust it as attribution.
|
|
423
|
+
|
|
424
|
+
Two keys it never carries: `embedder`, and `_s`, which breaks the key rule anyway. A key it has
|
|
425
|
+
to escape must also stay a valid key once escaped, so keep a reserved one (a `utm_` key, say) to
|
|
426
|
+
57 characters. `gem.share.request` throws on each of these before it sends.
|
|
427
|
+
|
|
428
|
+
An older arcade, or a development host that has not caught up, refuses the whole share on any
|
|
429
|
+
reserved key and does not say why. So where the arcade has not said it carries them,
|
|
430
|
+
`gem.share.request` throws on every reserved key before it sends, naming the key — rename the
|
|
431
|
+
parameter there, or wait for the arcade.
|
|
432
|
+
|
|
389
433
|
Four things to hold onto when you share:
|
|
390
434
|
|
|
391
435
|
**Call `request` synchronously from your own Share button's click handler.** Never from a
|
|
@@ -393,7 +437,8 @@ timer, after an `await`, or on a game event such as a win: the arcade only share
|
|
|
393
437
|
player's own click, and anywhere else it opens its Share panel instead, if it does anything.
|
|
394
438
|
|
|
395
439
|
**Never gate progress or a reward on sharing.** You cannot tell whether a player shared — the
|
|
396
|
-
arcade does not say, by design
|
|
440
|
+
arcade does not say, by design, and a resolved `request` does not mean it — so a reward for
|
|
441
|
+
sharing is a reward for pressing a button.
|
|
397
442
|
|
|
398
443
|
**Put no personal identifiers in the parameters.** No player id, name, email or anything that
|
|
399
444
|
leads back to a person: the link is public, and it is handed to whoever the player sends it to.
|
|
@@ -461,6 +506,53 @@ disagree. Running it twice is safe — it replaces what it finds.
|
|
|
461
506
|
**Your bundler does not matter.** It is a command rather than a plugin, so anything that ends
|
|
462
507
|
with a directory you upload can run it, including a build you assembled by hand.
|
|
463
508
|
|
|
509
|
+
### Keep dev-only code on the dev channel
|
|
510
|
+
|
|
511
|
+
A cheat panel, a level skipper or a debug menu is worth having in the builds you test, and must
|
|
512
|
+
never reach a player. **Mark it, and the platform keeps it off the test and release channels for
|
|
513
|
+
you.** Import the SDK's dev-tools mark from your dev-only code, and from nowhere else:
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
// src/dev/index.ts — everything dev-only lives under src/dev/
|
|
517
|
+
import '@indiegems/gem-web-sdk/dev-tools';
|
|
518
|
+
|
|
519
|
+
export function mountDevPanel() {
|
|
520
|
+
/* ... */
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
A build that carries that code carries the mark. It uploads and plays on `dev` as usual, and a
|
|
525
|
+
promote to `test` or `release` is refused, naming the build.
|
|
526
|
+
|
|
527
|
+
Load the dev code behind one switch your bundler sets at build time, so a release build leaves
|
|
528
|
+
it out entirely, mark included:
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
// src/main.ts
|
|
532
|
+
declare const __DEV_TOOLS__: boolean;
|
|
533
|
+
if (__DEV_TOOLS__) void import('./dev').then((m) => m.mountDevPanel());
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
// vite.config.ts
|
|
538
|
+
export default defineConfig(({ mode }) => ({
|
|
539
|
+
define: { __DEV_TOOLS__: JSON.stringify(mode !== 'release') },
|
|
540
|
+
}));
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
```jsonc
|
|
544
|
+
// package.json
|
|
545
|
+
"scripts": {
|
|
546
|
+
"build": "vite build && gem-sdk-stamp dist",
|
|
547
|
+
"build:release": "vite build --mode release && gem-sdk-stamp dist"
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
`build` is the one you play and put on `dev`; `build:release` is the one you upload for `test`
|
|
552
|
+
and `release`. **Use a build-time switch, never one read at run time** (a URL parameter, a key
|
|
553
|
+
combination, the channel the game finds itself on): those leave the code in the files every
|
|
554
|
+
player downloads, for anyone to read and switch on.
|
|
555
|
+
|
|
464
556
|
### Then upload and promote
|
|
465
557
|
|
|
466
558
|
```bash
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# WebGL or WebGPU: ask before you make your renderer
|
|
2
|
+
|
|
3
|
+
Some phones crash under WebGL. On the Pixel 10 (its PowerVR D-Series GPU), the phone's OpenGL ES
|
|
4
|
+
graphics driver fails under WebGL. In Chrome a three.js game there shows a white board while its
|
|
5
|
+
HTML still draws. In the Gem Arcade Android app it is worse: the app's WebView draws on the same
|
|
6
|
+
driver, inside the app's own process, so the crash closes the whole app. WebGPU on the same phone
|
|
7
|
+
runs on a different driver and draws cleanly, in Chrome and in the app.
|
|
8
|
+
|
|
9
|
+
So the SDK tells your game which renderer to make. Ask it once, just before you make your first
|
|
10
|
+
renderer, and follow the answer:
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
const { route, listed } = await gem.graphics.preferredRoute();
|
|
14
|
+
// route: 'webgpu' or 'webgl'. listed: whether this device is listed as broken on WebGL.
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**WebGL is the answer almost everywhere.** WebGPU generally performs worse out of the box, so
|
|
18
|
+
`route` is `webgpu` only on a device the platform lists as broken on WebGL, and only when WebGPU is
|
|
19
|
+
available there. Do not detect devices yourself: the list is the platform's, and it changes without
|
|
20
|
+
your game shipping a build.
|
|
21
|
+
|
|
22
|
+
**Every game that renders with WebGL asks, before its first draw.** In the app the first crash
|
|
23
|
+
closes the app, so a reload after a lost context cannot help there: only a choice made before
|
|
24
|
+
anything draws does. A game that does not use WebGL at all has nothing to ask.
|
|
25
|
+
|
|
26
|
+
## The three answers
|
|
27
|
+
|
|
28
|
+
| `route` | `listed` | What your game draws |
|
|
29
|
+
| -------- | -------- | -------------------------------------------------------------------------- |
|
|
30
|
+
| `webgpu` | `true` | Your WebGPU renderer. If it fails to start, your **safe WebGL recipe**. |
|
|
31
|
+
| `webgl` | `true` | Your **safe WebGL recipe**: a listed device with no WebGPU to move to. |
|
|
32
|
+
| `webgl` | `false` | Your normal WebGL renderer. This is almost every player. |
|
|
33
|
+
|
|
34
|
+
**The safe WebGL recipe** is your game drawn without what crashes a listed device's driver. For
|
|
35
|
+
every listed device today (the PowerVR D-Series) that is **no shadow maps**. The crash follows
|
|
36
|
+
shader makeup, so test your safe recipe on the device if you can, and turn off whatever else your
|
|
37
|
+
testing shows. On a listed device, never fall back to three.js's own automatic WebGL 2 backend (the
|
|
38
|
+
check below): it runs on the same broken driver.
|
|
39
|
+
|
|
40
|
+
**Follow the answer for every canvas on the page**, menus, HUDs and 2D-looking WebGL plates
|
|
41
|
+
included. One GPU process serves every canvas (in the app, the app's own process), so when one
|
|
42
|
+
WebGL canvas crashes the driver, every context on the page goes with it. A game whose 3D board
|
|
43
|
+
draws on WebGPU but whose menu plates stay on WebGL keeps those plates on the safe recipe. Ask once
|
|
44
|
+
and keep the answer: it is the same for the whole page.
|
|
45
|
+
|
|
46
|
+
## What `preferredRoute()` promises
|
|
47
|
+
|
|
48
|
+
- **It is quick, and it never blocks your first frame.** It is a bounded async call: it resolves
|
|
49
|
+
within `timeoutMs` (default 1000 ms, at most 5000), and normally at once after
|
|
50
|
+
`await gem.awaitBridge()`. When the time runs out it answers from what it knows by then.
|
|
51
|
+
- **It never rejects.** Anything that goes wrong inside it answers `webgl`, not listed: the
|
|
52
|
+
default.
|
|
53
|
+
- **`webgpu` means WebGPU is there.** The SDK checks that the browser hands out a WebGPU adapter
|
|
54
|
+
before it answers `webgpu`. Your game still checks that its WebGPU renderer really started (the
|
|
55
|
+
pattern below).
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
const { route, listed } = await gem.graphics.preferredRoute({ timeoutMs: 500 });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## three.js
|
|
62
|
+
|
|
63
|
+
Make `WebGLRenderer` by default, and `WebGPURenderer` only when the answer is `webgpu`. Load the
|
|
64
|
+
WebGPU build with a dynamic `import()`, so it is a separate chunk that only those players download:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
import * as THREE from 'three';
|
|
68
|
+
|
|
69
|
+
// `gem` is what createGem() returned. Call createGem before you make any renderer.
|
|
70
|
+
await gem.awaitBridge();
|
|
71
|
+
const { route, listed } = await gem.graphics.preferredRoute();
|
|
72
|
+
|
|
73
|
+
async function createRenderer() {
|
|
74
|
+
if (route === 'webgpu') {
|
|
75
|
+
try {
|
|
76
|
+
const { WebGPURenderer } = await import('three/webgpu');
|
|
77
|
+
const renderer = new WebGPURenderer({ antialias: true });
|
|
78
|
+
await renderer.init();
|
|
79
|
+
// WebGPURenderer quietly falls back to its own WebGL 2 backend when it cannot
|
|
80
|
+
// get a WebGPU device. On a listed device that backend is the broken driver,
|
|
81
|
+
// and everywhere it is slower than WebGLRenderer: never keep it.
|
|
82
|
+
if (renderer.backend.isWebGPUBackend) return { renderer, safe: false };
|
|
83
|
+
renderer.dispose();
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.warn('WebGPU renderer failed; drawing the safe WebGL recipe', error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { renderer: new THREE.WebGLRenderer({ antialias: true }), safe: listed };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { renderer, safe } = await createRenderer();
|
|
92
|
+
// The safe WebGL recipe on a listed device: for the PowerVR D-Series, no shadow maps.
|
|
93
|
+
renderer.shadowMap.enabled = !safe;
|
|
94
|
+
document.body.appendChild(renderer.domElement);
|
|
95
|
+
renderer.setAnimationLoop(() => renderer.render(scene, camera));
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Every other `WebGLRenderer` the page makes (a menu, a preview, a second view) follows `listed` the
|
|
99
|
+
same way: on a listed device it draws the safe recipe.
|
|
100
|
+
|
|
101
|
+
Things to know about that pattern:
|
|
102
|
+
|
|
103
|
+
- **Keep the backend check.** After `await renderer.init()`, `renderer.backend.isWebGPUBackend`
|
|
104
|
+
says whether WebGPU really started. Without it, a WebGPU failure on a listed device lands on
|
|
105
|
+
three.js's WebGL 2 backend, on the very driver the answer was steering you away from.
|
|
106
|
+
- **Let each renderer make its own canvas** (`renderer.domElement`). A canvas that has handed out a
|
|
107
|
+
WebGPU context cannot hand out a WebGL one, so the fallback needs a fresh canvas.
|
|
108
|
+
- **Use three.js r171 or later**, where `three` and `three/webgpu` share one core, so the same scene,
|
|
109
|
+
meshes and built-in materials draw with either renderer. Pin three.js to an exact version: what
|
|
110
|
+
`WebGPURenderer` draws differently from `WebGLRenderer` moves between releases.
|
|
111
|
+
|
|
112
|
+
Other engines take the same shape: ask, try WebGPU when `route` is `webgpu`, check it really
|
|
113
|
+
started, and on a listed device make your WebGL renderer with the safe recipe.
|
|
114
|
+
|
|
115
|
+
## What supporting both costs
|
|
116
|
+
|
|
117
|
+
**"Both" means `WebGLRenderer` by default, plus `WebGPURenderer` on real WebGPU for listed
|
|
118
|
+
devices.** It never means `WebGPURenderer` everywhere: on its WebGL 2 backend it is about 40%
|
|
119
|
+
slower than `WebGLRenderer` ([as three.js measured it](https://github.com/mrdoob/three.js/issues/34585)).
|
|
120
|
+
On real WebGPU it keeps up: one of our games drew its board in 4.05 ms against 4.81 ms on desktop,
|
|
121
|
+
and at 61 fps with shadows on the Pixel 10.
|
|
122
|
+
|
|
123
|
+
- **Custom shaders need a TSL port.** `WebGPURenderer` runs no GLSL and no `onBeforeCompile`. A
|
|
124
|
+
`ShaderMaterial`, `RawShaderMaterial`, an `onBeforeCompile` patch or an `EffectComposer` pass is
|
|
125
|
+
WebGL-only; under `WebGPURenderer` it needs a TSL (node material) equivalent.
|
|
126
|
+
- **The look can differ.** Colour blending, clipping, clear coat and ambient light have each needed
|
|
127
|
+
a fix in one game to look the same on both renderers. Compare the two side by side (testing,
|
|
128
|
+
below).
|
|
129
|
+
- **The WebGPU build is big, so load it only when chosen.** One game's WebGPU chunk is 662 KB
|
|
130
|
+
(188 KB gzipped). With `await import('three/webgpu')` behind `route === 'webgpu'`, as above,
|
|
131
|
+
players on WebGL never download it.
|
|
132
|
+
|
|
133
|
+
## Lost WebGPU devices are counted for you
|
|
134
|
+
|
|
135
|
+
The SDK counts every lost WebGL context, per device, across every game (the
|
|
136
|
+
`gem_graphics_context_lost` event in [analytics](analytics.md)). It counts a lost WebGPU device
|
|
137
|
+
the same way, with `route` set to `webgpu`, whether or not your game recovers. A device your game
|
|
138
|
+
destroys itself is not counted. Your game still handles a loss itself: three.js reports it through
|
|
139
|
+
`renderer.onDeviceLost`.
|
|
140
|
+
|
|
141
|
+
## Where the list comes from
|
|
142
|
+
|
|
143
|
+
The SDK ships with a built-in list. Today it has one entry: the PowerVR D-Series GPU on **every
|
|
144
|
+
route but Vulkan**. Chrome reports this GPU on its OpenGL ES route (`angle_gles`), and the Arcade
|
|
145
|
+
app's WebView names the bare chip with no ANGLE (`native_gl`); both crash, and both are listed. The
|
|
146
|
+
same GPU on the Vulkan route (`angle_vulkan`) draws fine on WebGL and is not listed, so when Chrome
|
|
147
|
+
moves this GPU to Vulkan, Chrome players there go back to WebGL on their own. No other device is
|
|
148
|
+
listed.
|
|
149
|
+
|
|
150
|
+
The Arcade can send its current list to your game when it connects, and that list replaces the
|
|
151
|
+
built-in one. That is how a new entry reaches your game while it is live, with no new build from
|
|
152
|
+
you: the next time a player opens your game, the Arcade's list applies.
|
|
153
|
+
|
|
154
|
+
When the Arcade sends no list, or one the SDK cannot read, your game uses the built-in list. It
|
|
155
|
+
never crashes or hangs on a missing or broken list.
|
|
156
|
+
|
|
157
|
+
## Testing: the `gemGraphics` launch key
|
|
158
|
+
|
|
159
|
+
Add `gemGraphics` to your game's address in the Arcade to force one of the three answers:
|
|
160
|
+
|
|
161
|
+
| Launch key | Answer | Stands for |
|
|
162
|
+
| --------------------- | ---------------------------------------- | ---------------------------------- |
|
|
163
|
+
| `gemGraphics=webgpu` | `{ route: 'webgpu', listed: true }` | a listed device with WebGPU |
|
|
164
|
+
| `gemGraphics=safe` | `{ route: 'webgl', listed: true }` | a listed device without WebGPU |
|
|
165
|
+
| `gemGraphics=webgl` | `{ route: 'webgl', listed: false }` | any other device |
|
|
166
|
+
|
|
167
|
+
The Arcade forwards it to your game like any [launch key](getting-started.md), and the SDK reads it
|
|
168
|
+
once, when `createGem` runs. To check your game on every route:
|
|
169
|
+
|
|
170
|
+
1. Open your game in a browser with WebGPU (desktop Chrome or Edge) with `gemGraphics=webgpu` added
|
|
171
|
+
to the address, for example
|
|
172
|
+
`https://gemarcade.com/games/<gameId>?play=1&gemGraphics=webgpu`.
|
|
173
|
+
2. The browser console shows `[gem] graphics route forced to webgpu by the gemGraphics launch key`.
|
|
174
|
+
3. Play through your game's scenes and check that everything draws. With three.js,
|
|
175
|
+
`renderer.backend.isWebGPUBackend` is `true` on this route.
|
|
176
|
+
4. Open it again with `gemGraphics=safe` and check the safe recipe draws, on every canvas.
|
|
177
|
+
5. Open it once more with `gemGraphics=webgl` and check your normal WebGL renderer.
|
|
178
|
+
|
|
179
|
+
A browser without WebGPU answers `{ route: 'webgl', listed: true }` for `gemGraphics=webgpu`, as a
|
|
180
|
+
listed device without WebGPU would, and the console says so.
|
|
181
|
+
|
|
182
|
+
## What the SDK reads, and what it sends
|
|
183
|
+
|
|
184
|
+
To match the list, the SDK makes one throwaway WebGL context on an offscreen canvas of its own,
|
|
185
|
+
never on one of yours, reads which GPU and graphics route the browser uses, and releases it at
|
|
186
|
+
once. It does this once a page, before your renderer exists. Nothing about the answer is sent
|
|
187
|
+
anywhere: the SDK does not report which answer it gave. What it does send is the count of lost
|
|
188
|
+
contexts and devices above.
|