17 Commits
Author SHA1 Message Date
dhkatzandClaude Opus 5.5 db344b7b95 Crossfade animations between ticks
CI / build (push) Successful in 1m30s
ModelInstance::step ramps the weights of animations fading in and out in tenths
over 10 ticks; between ticks, the posing hook sets them partway to the next
tick's weight, and puts them back with the times. Only if ModelInstance::step
counts the fades where expected (checked when installing); otherwise
crossfades stay at the client's rate.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:41:23 -07:00
dhkatzandClaude Opus 5.5 9d0faecf4d Move particles between ticks
CI / build (push) Successful in 53s
The particle library moves particles by their velocity times a tick's time at
each tick; between ticks, the adapter (ModelInstance::render's call to
ParticleSystem::render, redirected after checking it) draws them moved on by
their velocity times the time since, then moves each back by its own velocity:
ParticleSystem::render sorts them by distance, reordering them in memory, so
restoring saved positions in order scrambled them. Like ParticleSystem::render,
it only reads particles when there's a group (the library throws without one,
which crashed the client).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:21:58 -07:00
dhkatzandClaude Opus 5.5 4670a6a191 Pan between ticks, and move animations at their own rate
CI / build (push) Successful in 27s
The point the camera looks at is its position plus an offset (setCameraFocus,
at each tick), so between ticks it now moves with the eased position: panning
moves the target the position eases to, and was still at 30 per second.

Between ticks, an animation is moved on at the rate it moved at the last step
(per tick: the client steps several ticks at once when it falls behind), not
only when it moved exactly a frame: animations played at other speeds were
posed at the tick, between smooth ones.

The engine's stats also log the longest time between frames.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:56:09 -07:00
dhkatzandClaude Opus 5.5 d1437a3923 Move the camera between ticks
CI / build (push) Successful in 1m38s
CameraController::step eases the camera's position, orbit angles and zoom a
share k (0.4) of the way to their targets at each tick; between ticks, the
adapter's getCameraTransform (its entry in CameraController's exported vtable,
replaced after checking it) sets them 1 - (1 - k)^fraction of the way for IMVU's
code to make the transform from, then puts them back. k is read from the
constant CameraController::step uses, checked to be referenced there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:44:56 -07:00
dhkatzandClaude Opus 5.5 2471b1fd4f Pose avatars between ticks
CI / build (push) Successful in 27s
ModelInstance::render poses its model (updateCalSkeleton) before drawing it;
the adapter redirects that one call (after checking it) to ours, which between
ticks moves each playing animation that went on a frame at the last tick ahead
by the fraction of a frame since, has IMVU's code pose the skeleton (Cal3D
poses between keyframes), and puts the animations back, marking the skeleton
to be posed again. Held, looping and starting animations are posed as they
are. The frame lengths come from zero.dll's millisecondsPerFrame and
secondsPerFrame.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:38:07 -07:00
dhkatzandClaude Opus 5.5 065c7e9300 Never make the client wait for drawing, or hold up its steps
CI / build (push) Successful in 1m15s
The renderer process now takes each view's chunks on a thread of its own as
they come, and draws the newest frame on its main thread: the client no longer
waits for the previous frame to be drawn (on a VM, 22 of a frame's 25 ms).

The adapter records the client's steps (SceneViewer::step, by replacing its
entry in the exported IFrameListener vtable, after checking it), and the frame
pacer doesn't ask for a frame that wouldn't end before the next step is due: a
late step made the client catch up two at once, which showed as hitches. While
the renderer process covers the view, the client's renderer doesn't begin or
present frames, which could wait for the display.

The engine's stats now include the time in the client renderer's methods.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:30:46 -07:00
dhkatzandClaude Opus 5.5 399d934a08 Pace all frames to the frame rate, at best effort
CI / build (push) Successful in 32s
The pacer counted only its own frames, so the client's 30 came on top (60 gave
about 80), and it added no frames at all when the rate was out of reach. It now
asks for a window's next frame once the interval since its last frame began has
passed, and the client has had a third of a frame's time idle: all frames
within the rate, and as many as the client can draw when it can't reach it.

The engine's stats also break a frame's time down: sending to the renderer
process, bytes sent, buffer updates.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:16:22 -07:00
dhkatzandClaude Opus 5.5 05a7be5d41 Ask the client for frames between its steps
CI / build (push) Successful in 1m31s
The frame pacer repaints the client's 3D windows at the user's frame rate
(HKCU\Software\T5DE\Graphics\FrameRate: 30, the default, adds nothing; 60, 120,
240, 360; 0 uncapped), so the client draws frames between its 30-per-second
steps. Only when affordable: a window is asked once its last frame is drawn,
and only if its frames take under three quarters of the interval; uncapped,
the client is left idle a third of a frame between frames. The engine's stats
now log frames per second.

The extra frames show the same scene for now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 20:38:31 -07:00
dhkatzandClaude Opus 5.5 8055099920 Draw all of a client's views in one renderer process
CI / build (push) Successful in 40s
The adapter starts one renderer process per client, with the first engine
(again if it has exited), and attaches each engine's channel to it through a
control section (protocol version 5: Control, the -attach event). The renderer
process keeps a window, swap chain and scene per channel on one Direct3D
device, takes the views' frames in turns, and closes a view when its engine
does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 19:16:01 -07:00
dhkatzandClaude Opus 5.5 6e4fe589e5 Draw render targets in the renderer process
CI / build (push) Successful in 48s
While the renderer process runs, the adapter makes the client's render targets
itself and the renderer process draws frames into them: texture targets are
drawn with like any texture, and a pixmap target's frame is read back through
the channel into its Pixmap (protocol version 4: BeginFrame names its target,
CreateRenderTarget, Channel::readSize and the -read event). Frames into render
targets are drawn even while the window is hidden.

The client uses pixmap targets for snapshots (outfit pictures, photos), checked
to look right. The render target interfaces are declared from IMVU's vtables.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 19:09:48 -07:00
dhkatzandClaude Opus 5.5 7bf29f3e4c Clear WS_CLIPCHILDREN on the client's window
CI / build (push) Successful in 54s
The client creates its 3D view's window with it, so not adding it wasn't
enough: covered entirely by the renderer's window, the client's window got no
WM_PAINT and the client still drew about once a second.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:18:23 -07:00
dhkatzandClaude Opus 5.5 2d9da249fa Draw the whole 3D view in place of the client's renderer
CI / build (push) Failing after 0s
The full view is now the default. While the renderer process covers the whole
view (Channel::covering, set after each frame it presents), the adapter skips
the client's renderer's clears and draws, decided per frame; the client's
renderer still gets state and resources, and draws again as soon as the
renderer process stops covering the view.

The client's window no longer gets WS_CLIPCHILDREN: the client draws when its
window gets WM_PAINT, which a window entirely covered by clipped children never
gets (it fell to about one frame a second). The flip model swap chain shows
above the client's drawing anyway.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:14:17 -07:00
dhkatzandClaude Opus 5.5 3cdda4c7ed Map projections' depth like the client's renderer
IMVU's projections give depth from -1 to 1; its Direct3D renderer maps it to 0 to 1, and now so does ours. Without that, the nearer half of the depth range was clipped, the whole 2D overlay (name tags, the frame rate, the room's title) with it. The adapter also draws drawTrianglesFromMemory without indices, and sums up what the client draws in its log.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 07:01:47 -07:00
dhkatzandClaude Opus 5.5 299cd8c312 Fix particle sizes, and add tools for comparing renderers
Particles: Direct3D 9's size limit evidently isn't the documented 64 pixels; unclamped (up to 2048) they match the client's renderer. A compare view shows the left half's scene over the right half, both renderers side by side; %TEMP%\t5de-renderer-view.txt switches views and %TEMP%\t5de-renderer-shaders\fixed_function.hlsl replaces the shaders while the client runs. The renderer process runs from a copy, so the program can be replaced meanwhile. The adapter recognizes only the textures and buffers it made (others reach the client renderer unchanged) and logs the others and render targets.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 06:43:34 -07:00
dhkatzandClaude Opus 5.5 a04dba18c1 Draw particles, match the client's fog, and measure frame delay
Particles (point sprites) are drawn as instanced camera-facing squares sized like Direct3D 9's. Fog is by depth buffer value, as Direct3D 9 does with IMVU's projections. At most one frame is queued for display, frames carry the adapter's timestamp so the renderer process logs the delay to presenting, and the log's lines from several processes no longer mix.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 06:11:51 -07:00
dhkatzandClaude Opus 5.5 42fc55cb91 Draw the client's scene in the renderer process
The adapter records everything the client draws (state, transforms, lights, fog, textures and buffers) as a command stream, sent in chunks through the shared memory channel, while IMVU's Direct3D renderer still draws the whole view. The renderer process draws it with Diligent, with shaders reproducing IMVU's Direct3D 9 fixed-function setup, over the right half of the 3D view to compare. It skips to the newest frame when it falls behind, and logs its errors and frame times.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 05:52:15 -07:00
dhkatzandClaude Opus 5.5 9a7d7d2908 Add the renderer process
The renderer becomes two programs: the adapter (t5deRenderEngine.dll, 32-bit, in IMVU's process) and a 64-bit renderer process (t5de-renderer.exe) drawing with Diligent Engine, fetched with FetchContent and pinned. For now a spike: the adapter starts the renderer process for each engine and signals each frame through a shared memory channel, and the renderer process draws a box into a child window in the client's 3D view.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 05:17:03 -07:00