admin and Claude Opus 5.5
d48f174caf
game-maker / gate (push) Successful in 4m15s
game-maker / suite (push) Failing after 15m49s
Play videos on sprites, converted on the server with clips ( #402 )
...
A video becomes an asset like a sound: uploaded once, converted to one
format by a worker of its own, cut into clips in its tab, and worn by a
sprite through its Texture row. Only Chrome is a target, without alpha.
- Server: POST ?r=videos queues the upload under the hash of its bytes,
so the MP4's name is known at once. The worker (VideoWorker over
VideoFormat) writes H.264/AAC, yuv420p, constant frame rate, a
keyframe every 15 frames, at most 1920x1080, with a poster and the
details beside it. A failed job says why and is tried again on the
next upload.
- An upload may be up to 2 GB. It is streamed from the request to disk
and hashed there, so php-fpm holds none of it in memory. The other
routes keep their 256 MB.
- The worker runs in the container game-maker-video on stein2, a
compose project of its own (tools/video/), and in CI beside the PHP
server. The CI image has ffmpeg for it.
- Model: asset type video with size, length, frame rate and sound, and
clips like a sound's. The clip helpers and commands serve both.
- A sprite's Texture row offers videos and clips after the pictures.
resolvePicture names a playing video by a key, and the texture library
hands it to a VideoLibrary with one player per key.
- The editor shows a placed video as a quiet preview. Play starts it
over at its clip's start and lets it be heard through the master bus,
a pause holds it, Esc makes it quiet again.
- Video tab: the picture, a ruler, the clip strip and a track on the
Sound tab's time axis. A drag spans a clip, a handle moves an edge,
Play plays the picked clip.
- The picker shows a video on its way with its progress and adopts an
upload from it once it is ready. Dropping a video file does the same.
Decided on my own:
- Two sprites wearing the same clip share one player and show the same
picture. Control per instance belongs to the follow-up with nodes.
- A video plays in real time, the game's time scale does not slow it.
- The container may use 3 of the 4 cores of stein2.
Findings:
- The first real upload (279 MB) got 413: the API's own ceiling of
256 MB for every request, not nginx or the Brandmauer.
- Chrome has no picture of a video that never played, and a Pixi
VideoSource drawn before the first frame stays empty for good. The
texture is made in the first frame callback (openVideo).
- A player nobody asks for in 2 s is let go. The inspector's PREVIEW
asked only on an edit, so with the sprite not on screen its texture
was destroyed under it and the batcher threw on every frame. The
preview now redraws every frame while it shows a video, like a Time
shader. A let-go texture keeps an empty source instead of none.
- VideoSource.load() reloads the element and cuts off an earlier
play(). Playing starts after loadeddata.
Tests:
- PHP videos.test.php: 68 checks, with the real ffmpeg
- 300 MB upload through Brandmauer and nginx: 201 in 21 s, php-fpm at
13.7 MB RSS at most
- Unit videos.test.ts: 20 tests
- e2e 66 (stein2, atx was off): ALL PASSED, 18 checks; e2e 04 with the
new asset count green
- Gate (atx): 294 files, 4520 tests. serialize.test.ts used video as
its example of an unknown type and now uses a made-up one.
- Counter-checks: a keyframe gap of 30 fails the PHP check, a key that
keeps the video's end fails 2 unit tests, a clip without its end
fails "plays only its stretch" in e2e 66
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-11 12:12:39 +02:00
..
Physics stage 1a runs on Rapier: a new Rigidbody child object (body type, mass, gravity scale, damping, fixed rotation, CCD) makes its parent fall, bounce and come to rest in play, colliders gain friction, bounciness and density, the scene gains gravity (0, 980), the new PhysicsSystem steps at a fixed 60 Hz with interpolation in world coordinates and treats a tilemap as static ground, On collision enter gains normal and impulse, play waits for Rapier and does not start without it, and the editor run keeps the old CollisionSystem; the existing collision tests run against both systems unchanged ( #146 )
2026-09-18 18:11:43 +02:00
2026-10-05 11:16:42 +02:00
2026-09-09 13:45:27 +02:00
2026-09-11 09:57:19 +02:00
2026-10-09 09:18:46 +02:00
2026-09-26 19:36:49 +02:00
2026-09-28 15:43:03 +02:00
2026-09-23 13:59:04 +02:00
2026-09-08 09:22:49 +02:00
2026-10-09 19:13:11 +02:00
2026-09-29 09:21:31 +02:00
2026-09-23 10:20:07 +02:00
Moving against a tilemap whose cells block with an outline no longer costs more than a whole frame: an outlined cell is its own polygon, split into convex parts, so a map that is a handful of merged rectangles when built from plain solid tiles is hundreds of solids under one entity - and Move brought every one of them into world coordinates and tested it exactly, including the ones on the far side of the map, which measured 148 ms per Move call on a 20x20 map with 16-corner outlines (34 ms at 8 corners, 0.4 ms with plain solid tiles), matching the report that standing still holds 60 fps while anything that moves stutters; the fix is a broad phase in two stages that decides nothing and only leaves out what cannot be reached - a move never ends further from its start than it is long, because sliding always shortens the rest, so every collider of the mover gets a rectangle grown by the move length and an obstacle whose rectangle does not meet it is never tested, a ray taking the rectangle around itself, and that rectangle is carried back into the map's own units with invert(m) and turned into cell coordinates (cellsWithin), a rotated map through its own axes, so a far cell is not brought into world form in the first place - solidShapesOf was O(cells) per call, which was the second cost block and the reason the 60x60 case still grew; CollisionSystem.overlaps takes the same range, so the editor run asks a map only for the cells within reach of the other side each frame, two maps narrowing nothing, and a Move obstacle now falls away at build time when its layer does not meet the mover's ( #152 ) instead of inside the loop; the measurements after: 148 -> 0.26 ms, 34 -> 0.27 ms, 60x60 20 -> 3.5 ms, plain tiles unchanged; this is no regression of the recent commits - the same benchmark in worktrees against 947a6b1 ( #141 ), 6a31f5e, 4f55f85 and ae3aa7d gives the same 85-148 ms, the cost has been there since outlined cells arrived with #141 ; new pure functions shapeBounds/grownBounds/boundsMeet in collider.ts and cellsWithin/cellsMeet in tilemapCollision.ts, 11 new unit tests in tests/core/broadphase.test.ts (the rectangles of all four shapes including a rotated box measuring its diagonal reach, touching counts as meeting, cellsWithin straight and turned by 90 degrees, a map handing out 400 cells without a range and under 10 with one, an 800 unit move from far away still stopping at the first cell, a rotated map blocking where its one painted cell was turned to, a ray across the whole map, and the editor run seeing a collider on a far cell), and four counter-checks hold: dropping the cell filter fails the counting test, padding without the move length fails 4 move tests, ignoring the rotation in cellsWithin fails 3, and a wrong range in the detection fails the touching test; one blunt test on the way - the first rotated map was painted on every cell, where even a wrongly computed cell range blocks and the counter-check stayed green, so exactly one cell is painted now and the test measures where it turned to; no e2e check and the check sum stays 1157, because a timing check in the browser would flutter under CI load while modules 40 and 42 cover the correctness anyway, the speed evidence being the node benchmark ( #184 )
2026-09-20 19:25:53 +02:00
2026-10-07 20:18:09 +02:00
2026-10-09 09:18:46 +02:00
2026-09-09 16:13:30 +02:00
2026-10-10 14:30:00 +02:00
2026-10-06 22:54:44 +02:00
2026-09-29 07:08:00 +02:00
2026-09-11 13:34:09 +02:00
2026-10-01 15:03:05 +02:00
2026-09-18 21:52:11 +02:00
2026-10-03 15:32:52 +02:00
One-way platforms, raycast and collision stay with contact data: a collider can be one-way with a side it blocks from (turning with its object, drawn thick in the scene view) and a tile can be one-way from above, one-way cells never merge with solid tiles, a contact blocks only when its normal is within 45 degrees of the blocking side, the new node Ignore one-way lets an object drop through for a while, and it all holds for Move and for dynamic bodies in Rapier, where the decision is made before each step because the hook may not touch the engine ( #155 ); the new Raycast node answers hit, object, collider, point, normal and distance in our own f64 geometry, the same in editor run and play, and the editor run draws each ray for 0.25 s ( #153 ); On collision stay gains an interval with its own clock per pair, and stay and enter gain the contact point next to normal and impulse ( #149 ); fixes that every Rapier collider of a tilemap had the shape of its last cell
2026-09-19 11:36:11 +02:00
2026-10-03 15:32:52 +02:00
2026-10-01 10:27:54 +02:00
2026-10-06 22:22:54 +02:00
2026-10-05 22:05:00 +02:00
2026-09-26 21:45:27 +02:00
2026-09-12 22:41:14 +02:00
2026-10-03 19:53:51 +02:00
2026-10-03 11:17:02 +02:00
2026-09-29 18:12:55 +02:00
2026-10-10 00:57:26 +02:00
2026-10-05 22:15:15 +02:00
2026-10-09 09:18:46 +02:00
2026-10-01 17:30:16 +02:00
2026-10-10 19:46:03 +02:00
2026-10-05 21:46:15 +02:00
2026-10-10 14:30:00 +02:00
2026-09-29 18:12:55 +02:00
2026-09-29 18:12:55 +02:00
2026-10-05 21:46:15 +02:00
2026-10-02 19:33:34 +02:00
2026-09-29 18:12:55 +02:00
2026-10-07 08:25:13 +02:00
2026-10-03 19:53:51 +02:00
2026-09-21 10:27:17 +02:00
A picture in the editor may now be drawn instead of uploaded, which is the piece #129 (Vector art) and #130 (Pixel editor) both need and neither could build alone: TextureRef.file has been a file name on the server, isTextureFile() checking a sha256 hash plus an extension and TextureLibrary fetching it over HTTP, so an asset that paints its own image had nowhere to put itself - the key now also takes the form gen:<assetId>@<revision>, which an uploaded file can never look like, and everything that only passes a ref along (sprite, tile, tilemap mesh, particle source, shaders, aspect ratio) is untouched because it still sees a Texture; the revision is a string, not a counter, which is the one deviation from the plan in the issue and costs nothing - gen:a1@7 and gen:a1@c0ffee go through the same door, so a generator may fingerprint its own content instead of relying on every command to remember to bump a number; two registries keyed by asset type as a plain string, so a plugin may bring its own type - registerPictureSource(type, read) in the new core/model/generated.ts tells the model canvas size and revision and resolvePicture() builds the ref from it (no rect, no slice, smooth from props.filter like a texture), and registerPictureGenerator(type, draw) in the new viewport/generatedPictures.ts hands out the finished texture, projectPictureFactory(() => state.project) tying the two together per session in App.ts; invalidation is a rule instead of guesswork at twenty call sites: GeneratedPictures<T> holds one picture per asset keyed on the revision and frees the old one as soon as the new is drawn, TextureLibrary destroying it and dropping the crops cut from it, while a generator that cannot draw yet is not remembered but asked again next time - the class imports Pixi as a type only, so the whole bookkeeping is measurable without a GPU; carried along are the picture picker (a drawn asset sits among the textures by name, without frames), the delete warning (counts tiles and animations for it too) and the asset context menu, where "Create sprite from picture" now covers it while "Create animation from texture" stays texture-only because it needs frames; one finding while probing - crop() could not have cut a generated texture at all, a RenderTexture having no source.resource to draw back from and drawImage running onto null, so it is handed out whole, a drawn picture having no sheet whose neighbouring row could bleed in; 18 new unit tests, 10 in tests/core/generated-picture.test.ts (building and splitting the key, an uploaded file is not one, no registry means no picture, resolving to canvas size, a new revision is a new key, a canvas without a size, the filter, the sprite size, picker order) and 8 in tests/viewport/generated-pictures.test.ts (drawing once, freeing the old revision and only for its own asset, null is not remembered, no key means nothing drawn, a session change gives everything back, peek), and three counter-checks hold: dropping generated assets from resolvePicture fails 5 of the 10 core tests, not freeing the old revision fails 1 viewport test, and narrowing the picker back to textures fails 3; no e2e check and the check sum stays 1157, because without a generator the groundwork shows nothing in the browser - that evidence comes with #129 ( #185 )
2026-09-20 20:09:42 +02:00
2026-09-22 21:15:21 +02:00
2026-10-07 15:06:32 +02:00
2026-09-16 11:24:34 +02:00
2026-09-29 10:36:10 +02:00
2026-10-09 09:18:46 +02:00
2026-10-04 19:13:58 +02:00
2026-10-09 09:18:46 +02:00
2026-10-07 08:25:13 +02:00
2026-09-29 18:12:55 +02:00
2026-10-10 19:46:03 +02:00
2026-10-03 10:09:45 +02:00
2026-10-07 19:13:32 +02:00
2026-10-04 18:38:06 +02:00
2026-10-02 17:59:22 +02:00
2026-09-29 09:21:31 +02:00
2026-09-30 22:08:22 +02:00
2026-10-07 21:13:18 +02:00
2026-09-26 22:33:20 +02:00
2026-09-28 07:23:00 +02:00
2026-09-26 21:12:44 +02:00
2026-09-18 16:01:12 +02:00
2026-09-29 18:12:55 +02:00
Physics stage 1a runs on Rapier: a new Rigidbody child object (body type, mass, gravity scale, damping, fixed rotation, CCD) makes its parent fall, bounce and come to rest in play, colliders gain friction, bounciness and density, the scene gains gravity (0, 980), the new PhysicsSystem steps at a fixed 60 Hz with interpolation in world coordinates and treats a tilemap as static ground, On collision enter gains normal and impulse, play waits for Rapier and does not start without it, and the editor run keeps the old CollisionSystem; the existing collision tests run against both systems unchanged ( #146 )
2026-09-18 18:11:43 +02:00
2026-10-09 15:31:33 +02:00
2026-10-07 13:15:52 +02:00
2026-09-29 18:12:55 +02:00
2026-09-28 14:35:17 +02:00
2026-10-07 19:13:32 +02:00
2026-09-29 18:12:55 +02:00
2026-09-29 18:12:55 +02:00
2026-10-01 07:28:49 +02:00
2026-09-28 14:35:17 +02:00
2026-10-09 20:27:32 +02:00
2026-10-02 19:33:34 +02:00
2026-09-21 07:29:26 +02:00
2026-09-23 10:20:07 +02:00
2026-09-19 16:30:52 +02:00
2026-10-10 12:40:11 +02:00
2026-10-03 14:51:18 +02:00
2026-09-23 06:57:43 +02:00
2026-10-06 17:38:44 +02:00
2026-10-09 15:31:33 +02:00
2026-10-07 12:11:54 +02:00
2026-09-18 16:56:47 +02:00
2026-10-01 09:52:57 +02:00
2026-10-06 11:34:30 +02:00
2026-10-07 13:15:52 +02:00
2026-09-29 06:01:27 +02:00
2026-09-09 13:09:13 +02:00
2026-10-02 23:52:49 +02:00
2026-09-29 18:12:55 +02:00
2026-09-28 13:54:57 +02:00
2026-10-05 21:47:00 +02:00
2026-09-29 18:12:55 +02:00
2026-10-04 19:13:58 +02:00
2026-09-23 16:44:19 +02:00
2026-09-29 13:30:38 +02:00
2026-10-03 21:45:04 +02:00
2026-10-03 15:32:52 +02:00
2026-10-07 21:27:22 +02:00
2026-10-07 21:04:16 +02:00
2026-09-22 10:23:57 +02:00
2026-09-23 06:06:53 +02:00
2026-09-28 10:00:22 +02:00
2026-09-19 16:30:52 +02:00
2026-10-11 12:12:39 +02:00
2026-10-03 14:51:18 +02:00
2026-10-03 11:43:54 +02:00
2026-10-03 17:09:53 +02:00
2026-10-03 14:51:18 +02:00
2026-10-02 16:50:50 +02:00
2026-10-03 11:43:54 +02:00
2026-10-03 15:32:52 +02:00
2026-09-19 12:54:53 +02:00
2026-10-09 15:31:33 +02:00
2026-10-09 19:13:11 +02:00
2026-10-08 21:22:59 +02:00
2026-10-09 15:31:33 +02:00
2026-10-08 21:13:22 +02:00
2026-10-10 20:30:59 +02:00
2026-10-09 20:27:32 +02:00
2026-10-04 18:15:56 +02:00
2026-10-04 18:38:06 +02:00
2026-10-09 20:27:32 +02:00
2026-10-09 20:53:49 +02:00
2026-09-12 18:00:23 +02:00
2026-09-29 16:16:37 +02:00
2026-09-12 13:35:02 +02:00
2026-09-29 16:16:37 +02:00
2026-09-24 10:55:42 +02:00
2026-10-08 10:36:25 +02:00
2026-09-11 18:40:47 +02:00
2026-09-29 06:01:28 +02:00
2026-10-03 15:32:52 +02:00
2026-09-27 21:07:04 +02:00
2026-10-07 08:25:13 +02:00
2026-09-29 19:00:22 +02:00
2026-10-02 07:28:39 +02:00
2026-09-25 06:18:41 +02:00
2026-09-18 15:12:07 +02:00
2026-09-18 21:52:11 +02:00
2026-09-29 19:00:22 +02:00
2026-10-01 09:52:57 +02:00
2026-09-18 13:58:03 +02:00
2026-10-07 16:54:59 +02:00
2026-09-14 09:31:53 +02:00
2026-09-21 08:00:30 +02:00
2026-09-14 08:25:49 +02:00
2026-09-29 16:16:37 +02:00
2026-09-30 22:08:22 +02:00
2026-09-23 13:59:04 +02:00
2026-09-29 18:12:55 +02:00
2026-09-29 18:12:55 +02:00
Refactoring round 5 covers the code since round 4 without changing behaviour: four identical copies of dot() and three of "nearest point on a segment" stood in the collision math, in the hit area of the collider outlines and in the polygon handles, so they moved into the new module src/core/util/vec.ts; the warning line, the busy state, info() and the caret reset were written out line by line in both the sign-in screen and the password dialog and are now FormStatus; the closing row "Cancel plus a confirming button, optionally one on the left" was built by hand in FormDialog, FilePicker, OpenProjectDialog and PasswordDialog and is now dialogButtons(); the header bar was drawn by the toolbar, the TabHeader and the Breadcrumb each on its own and is now drawHeaderBar(); AddColliderCommand and AddRigidbodyCommand share a base, each subclass only builds its own asset; Editor.ts derives the panes from assetEditors instead of repeating eight entries and clamps its splitters with clamp(); jscpd over src falls from 0.19 % to 0.14 %; dead code is gone - listScenes() has been orphaned since the Scene picker fell away ( #183 ), DEFAULT_FILTER ( #152 ) was never used, the two test hooks projectPickerRowAt/rowPosition and particleCompiles are read by no tool, and four functions lost an export they did not need; seven comments that told history ("used to", "no longer", "since #...") describe the state of things now, and that this changed nothing was proved by a rebuild - the bundle is md5 identical, b4ad0dfe... before and after; the README said Logout sits at the far right of the toolbar, which has been the Account menu since #182 , and wiki/Architecture.md was missing src/core/util/ altogether; 4 new unit tests cover vec.ts, and switching the clamp on the segment off makes exactly one of them fail while none of the 76 neighbouring tests notice, so that clamping was covered nowhere before ( #85 )
2026-09-20 17:24:42 +02:00
2026-10-01 23:13:43 +02:00
2026-10-01 23:13:43 +02:00
2026-10-01 23:13:43 +02:00
2026-10-02 11:20:55 +02:00
2026-10-01 23:13:43 +02:00
2026-10-01 23:13:43 +02:00
2026-10-11 12:12:39 +02:00
2026-09-18 21:52:11 +02:00
2026-10-01 10:27:54 +02:00