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
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,22 @@ LOAD deliberately overwrites an occupied slot (remove first, then load), matchin

### LED mirror (`GET_LED`)

LOAD/REMOVE/MOVE are fire-and-forget; `GET_LED` is the one command that answers. It returns a fixed 30-byte snapshot of what the game has the three LED regions doing:
LOAD/REMOVE/MOVE are fire-and-forget; `GET_LED` is the one command that answers. It returns a fixed 40-byte snapshot (protocol version 2) of what the game has the three LED regions doing:

```
[0] 0x4C 'L' magic
[1] serial increments whenever any region actually changes
[2] 0x03 region count
[3..] 3 x 9 bytes: pad, mode, r, g, b, on_ms, off_ms, count, speed_ms
[2] 0x02 protocol version
[3] 0x03 region count
[4..] 3 x 12 bytes: pad, mode, r, g, b, from_r, from_g, from_b, on_ms, off_ms, count, speed_ms
```

| Field | Meaning |
|---|---|
| `pad` | LED region — `1` centre, `2` left, `3` right |
| `mode` | `0` off, `1` solid, `2` flash, `3` fade |
| `r`, `g`, `b` | The colour the game set, verbatim |
| `from_r`, `from_g`, `from_b` | For a fade, the colour the pad was already showing when the command landed; the real toypad cross-fades from it to `r/g/b` rather than ramping one colour's brightness |
| `on_ms` / `off_ms` | Flash on and off durations, in Toypad ticks (~40ms each) |
| `count` | Cycle count; `0` means "until the next command" (`0xFF` on the wire is normalized to `0`) |
| `speed_ms` | Fade tick time |
Expand All @@ -103,7 +105,7 @@ The emulated Toypad builds this snapshot in `handle_led_command`, parsing the ga

The **serial** only advances when a command actually changes a region's state — re-sending an identical command leaves it alone. A polling client can therefore skip snapshots it has already applied and never restart a flash mid-cycle.

This is byte-for-byte the same snapshot the Cemu fork returns, so LegoToypad's LED mirror works against either emulator with no changes.
This is the protocol version 2 snapshot the Cemu and shadPS4 listeners also return, so LegoToypad's LED mirror works against this emulator with no changes. (Version 1 was a 30-byte reply with no version byte and no `from_r/g/b`; the app now rejects it, which is why RPCS3 needed this update.)

## Code map

Expand Down
10 changes: 10 additions & 0 deletions rpcs3/Emu/Io/Dimensions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,10 @@ void dimensions_toypad::set_led_state(u8 pad, u8 mode, u8 r, u8 g, u8 b, u8 on_m
auto apply = [&](u8 target_pad)
{
led_pad_state& state = m_led_state[led_pad_index(target_pad)];
// A fade's "from" colour is whatever the pad was already showing (or
// already fading towards) the moment this command lands, so a fade
// issued mid-fade still anchors to something on-screen.
const u8 from_r = state.r, from_g = state.g, from_b = state.b;
if (state.mode == mode && state.r == r && state.g == g && state.b == b &&
state.on_ms == on_ms && state.off_ms == off_ms && state.count == count && state.speed_ms == speed_ms)
return; // unchanged - don't bump the poll serial
Expand All @@ -638,6 +642,12 @@ void dimensions_toypad::set_led_state(u8 pad, u8 mode, u8 r, u8 g, u8 b, u8 on_m
state.r = r;
state.g = g;
state.b = b;
if (mode == 3) // Fade: remember the pre-command colour to cross-fade from
{
state.from_r = from_r;
state.from_g = from_g;
state.from_b = from_b;
}
state.on_ms = on_ms;
state.off_ms = off_ms;
state.count = count;
Expand Down
9 changes: 6 additions & 3 deletions rpcs3/Emu/Io/Dimensions.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,17 @@ class dimensions_toypad
std::optional<std::array<u8, 32>> pop_added_removed_response();

// Mirror a pad region's LED state ("keystone puzzle" glow). Same encoding
// and wire snapshot as the Cemu fork, so the LegoToypad controller app can
// render the pads glowing for RPCS3 too. mode: 0 off, 1 solid, 2 flash,
// 3 fade; pad: 1=center, 2=left, 3=right.
// and wire snapshot as the Cemu/RPCS3 protocol version 2, so the LegoToypad
// controller app can render the pads glowing for RPCS3 too. mode: 0 off,
// 1 solid, 2 flash, 3 fade; pad: 1=center, 2=left, 3=right. from_r/g/b is
// the colour the pad was already showing when a fade command landed - the
// real toypad cross-fades between that and r/g/b, not one colour's brightness.
struct led_pad_state
{
u8 pad = 0;
u8 mode = 0;
u8 r = 0, g = 0, b = 0;
u8 from_r = 0, from_g = 0, from_b = 0;
u8 on_ms = 0, off_ms = 0, count = 0, speed_ms = 0;
};
void handle_led_command(const u8* buf, u32 buf_size);
Expand Down
27 changes: 20 additions & 7 deletions rpcs3/Emu/Io/DimensionsListener.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ namespace
std::atomic<bool> g_listener_running{false};
std::atomic<socket_t> g_listen_sock{invalid_sock};
constexpr auto move_pickup_delay = std::chrono::milliseconds(500);
// LED snapshot protocol version. v2 is a 40-byte reply that adds the
// pre-fade colour (from_r/g/b) alongside the target colour, so a client can
// render the real toypad's two-colour cross-fade. v1 was 30 bytes with no
// version byte; the companion app now requires v2.
constexpr u8 led_protocol_version = 2;

#ifdef _WIN32
std::thread g_picker_thread;
Expand Down Expand Up @@ -231,22 +236,30 @@ namespace
const auto states = g_dimensionstoypad.get_led_states();
const u8 serial = g_dimensionstoypad.get_led_serial();

u8 response[3 + 3 * 9] = {};
// Wire format v2: { 'L', serial, protocol version, region count,
// then 3 regions x 12 bytes: pad, mode, r, g, b, from_r, from_g,
// from_b, on_ms, off_ms, count, speed_ms }. from_r/g/b is the
// pre-fade colour; see dim_listener_log and Dimensions.h.
u8 response[4 + 3 * 12] = {};
response[0] = 0x4C; // 'L' magic
response[1] = serial;
response[2] = 0x03; // region count
response[2] = led_protocol_version;
response[3] = 0x03; // region count
for (usz i = 0; i < 3; ++i)
{
const usz off = 3 + i * 9;
const usz off = 4 + i * 12;
response[off + 0] = states[i].pad;
response[off + 1] = states[i].mode;
response[off + 2] = states[i].r;
response[off + 3] = states[i].g;
response[off + 4] = states[i].b;
response[off + 5] = states[i].on_ms;
response[off + 6] = states[i].off_ms;
response[off + 7] = states[i].count;
response[off + 8] = states[i].speed_ms;
response[off + 5] = states[i].from_r;
response[off + 6] = states[i].from_g;
response[off + 7] = states[i].from_b;
response[off + 8] = states[i].on_ms;
response[off + 9] = states[i].off_ms;
response[off + 10] = states[i].count;
response[off + 11] = states[i].speed_ms;
}
if (!send_all(client, response, sizeof(response)))
dim_listener_log.error("Failed to send GET_LED response");
Expand Down
8 changes: 6 additions & 2 deletions rpcs3/Emu/Io/DimensionsListener.h
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
#pragma once

// TCP listener that lets external companion apps (e.g. LegoToypad) drive the
// emulated LEGO Dimensions Toypad, wire-compatible with harrysof's
// Cemu-2.6-Remote-Toypad-Build listener:
// emulated LEGO Dimensions Toypad, wire-compatible with the Cemu and shadPS4
// listeners (LED protocol version 2):
//
// LOAD: 0x01 pad index 0x00 0x00 + 180 tag bytes + u16le path length + path
// REMOVE: 0x02 pad index 0x00 0x00
// MOVE: 0x03 destPad destIndex srcPad srcIndex
// GET_LED:0x04 0x00 0x00 0x00 0x00 -> 40-byte LED snapshot:
// { 'L', serial, version(2), region count(3), then 3 regions x 12
// bytes: pad, mode, r, g, b, from_r, from_g, from_b, on_ms, off_ms,
// count, speed_ms }
//
// pad: 1 = center, 2 = left, 3 = right; index: 0..6.
// Listens on 127.0.0.1:9191 (override with env RPCS3_TOYPAD_PORT).
Expand Down
Loading