diff --git a/flashdreams/flashdreams/api_v2/README.md b/flashdreams/flashdreams/api_v2/README.md index b5d9269b8..d7dbda2b8 100644 --- a/flashdreams/flashdreams/api_v2/README.md +++ b/flashdreams/flashdreams/api_v2/README.md @@ -21,8 +21,8 @@ what each contract promises. - `input_source.py`, `output_sink.py`, `client_window.py`: `IClientWindow` is both an `InputSource` and an `OutputSink`, grouping one client's input and output. -- `user_input_event_data.py`: base class for input event data. The concrete - types belong to the runtime. +- `user_input_event.py`: base class for timestamped input events. The concrete + event types belong to the runtime. Three of these are things you write: an application, a session, and a model loop. A UI loop is optional. Windows and sinks you only implement if you are diff --git a/flashdreams/flashdreams/api_v2/loop.py b/flashdreams/flashdreams/api_v2/loop.py index 78a21958e..c507004a3 100644 --- a/flashdreams/flashdreams/api_v2/loop.py +++ b/flashdreams/flashdreams/api_v2/loop.py @@ -17,7 +17,7 @@ from flashdreams.runtime_v2.event_buffer import EventBuffer from flashdreams.runtime_v2.step_result import StepResult -from flashdreams.runtime_v2.user_input_event import CloseUserInputEventData +from flashdreams.runtime_v2.user_input_event import CloseUserInputEvent from flashdreams.runtime_v2.user_input_events import UserInputEvents from flashdreams.runtime_v2.video_tensor import VideoTensorLayout @@ -310,10 +310,7 @@ def presented_model_frames(self) -> tuple[Tensor, ...]: def _contains_close(events: UserInputEvents) -> bool: - return any( - isinstance(event.get_event_data(), CloseUserInputEventData) - for event in events.get_events() - ) + return any(isinstance(event, CloseUserInputEvent) for event in events.get_events()) def _model_results( diff --git a/flashdreams/flashdreams/api_v2/user_input_event.py b/flashdreams/flashdreams/api_v2/user_input_event.py new file mode 100644 index 000000000..8d5d49977 --- /dev/null +++ b/flashdreams/flashdreams/api_v2/user_input_event.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""User input event protocol.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import ClassVar, final + +from numpy import uint64 + + +@dataclass(frozen=True, slots=True, eq=False, kw_only=True) +class UserInputEvent(ABC): + """Base class for timestamped user input events.""" + + _type_name_owners: ClassVar[dict[str, str]] = {} + """ClassVar tracking all registered UserInputEvent's.""" + + timestamp: uint64 + """Timestamp in microseconds since the start of the session.""" + + def __init_subclass__(cls, **kwargs: object) -> None: + """Register and validate the concrete event type name. + + Raises: + TypeError: The event type name is not a non-empty string. + ValueError: Another event class uses the same type name. + """ + super(UserInputEvent, cls).__init_subclass__(**kwargs) + type_name = cls.get_type_name() + if not isinstance(type_name, str) or not type_name: + raise TypeError("User input event type names must be non-empty strings.") + + owner = f"{cls.__module__}.{cls.__qualname__}" + registered_owner = cls._type_name_owners.get(type_name) + if registered_owner is not None and registered_owner != owner: + raise ValueError( + f"User input event type name {type_name!r} is already registered " + f"by {registered_owner}." + ) + cls._type_name_owners[type_name] = owner + + @classmethod + @abstractmethod + def get_type_name(cls) -> str: + """Return the event type name.""" + ... + + @final + def get_timestamp(self) -> uint64: + """Return the timestamp of the event.""" + return self.timestamp + + def __hash__(self) -> int: + """Return the hash of the concrete class name. + + The value is not stable across processes. + """ + return hash(type(self).__name__) diff --git a/flashdreams/flashdreams/api_v2/user_input_event_data.py b/flashdreams/flashdreams/api_v2/user_input_event_data.py deleted file mode 100644 index c89520954..000000000 --- a/flashdreams/flashdreams/api_v2/user_input_event_data.py +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""User input event data protocol.""" - -from abc import ABC, abstractmethod - - -class UserInputEventData(ABC): - """Base class for data stored in a user input event. - - Implementations provide :meth:`get_type_name` and may add fields for their - event data. The runtime owns the set of concrete types, in - :mod:`flashdreams.runtime_v2.user_input_event`, which covers the input - modalities supported today. - """ - - @classmethod - @abstractmethod - def get_type_name(cls) -> str: - """Return the event type name.""" - ... - - @classmethod - def __hash__(cls) -> int: - """Return the hash of the concrete class name. - - The value is not stable across processes. - """ - return hash(str(cls.__name__)) diff --git a/flashdreams/flashdreams/runtime_v2/README.md b/flashdreams/flashdreams/runtime_v2/README.md index 86d261202..f5509921e 100644 --- a/flashdreams/flashdreams/runtime_v2/README.md +++ b/flashdreams/flashdreams/runtime_v2/README.md @@ -41,7 +41,7 @@ Presenting it: - `blit_model_output_to_screen_loop.py` is the UI loop a session gets when it registers none of its own. -- `slangpy_ui_loop.py` and `_slangpy_ui_renderer.py` are the UI loop for +- `slangpy_ui_loop.py` and `slangpy_ui_renderer.py` are the UI loop for applications that draw widgets over the model output. - `mp4_client_window.py` and `webrtc_client_window.py` are the two windows. - `mp4_output_sink.py`, `metrics_output_sink.py`, `video_encoder.py` and @@ -50,7 +50,7 @@ Presenting it: Input: -- `user_input_event.py` defines the event data types, `user_input_events.py` the +- `user_input_event.py` defines the concrete event types, `user_input_events.py` the batch of them a source hands over. ## The command line @@ -119,7 +119,7 @@ that reader has not seen and moves its cursor to the end, and `collect_garbage` deletes the prefix every reader has passed. The UI loop is reader 0 and the model loop is reader 1. -Appending also counts resets. Every `ResetUserInputEventData` in a batch bumps +Appending also counts resets. Every `ResetUserInputEvent` in a batch bumps the generation number that loops and the presentation manager compare against their own. diff --git a/flashdreams/flashdreams/runtime_v2/event_buffer.py b/flashdreams/flashdreams/runtime_v2/event_buffer.py index 99dbd968e..66265a567 100644 --- a/flashdreams/flashdreams/runtime_v2/event_buffer.py +++ b/flashdreams/flashdreams/runtime_v2/event_buffer.py @@ -6,7 +6,7 @@ import threading from flashdreams.runtime_v2.user_input_event import ( - ResetUserInputEventData, + ResetUserInputEvent, UserInputEvent, ) from flashdreams.runtime_v2.user_input_events import UserInputEvents @@ -20,7 +20,7 @@ class EventBuffer: cursor per registered reader, hands each reader only what it has not seen, and drops the prefix they have all passed. - It also counts resets. Every :class:`ResetUserInputEventData` appended bumps + It also counts resets. Every :class:`ResetUserInputEvent` appended bumps :attr:`generation`, which the loops and the presentation manager compare against their own; that counter is how a reset reaches all of them without any of them talking to each other. @@ -50,8 +50,7 @@ def append(self, events: UserInputEvents) -> None: with self._lock: self._events.extend(received) self._generation += sum( - isinstance(event.get_event_data(), ResetUserInputEventData) - for event in received + isinstance(event, ResetUserInputEvent) for event in received ) def read(self, reader_id: int) -> tuple[UserInputEvents, int]: diff --git a/flashdreams/flashdreams/runtime_v2/serving/webrtc_server.py b/flashdreams/flashdreams/runtime_v2/serving/webrtc_server.py index aa8c08781..d31a71176 100644 --- a/flashdreams/flashdreams/runtime_v2/serving/webrtc_server.py +++ b/flashdreams/flashdreams/runtime_v2/serving/webrtc_server.py @@ -26,12 +26,12 @@ from flashdreams.runtime_v2.session_desc import SessionDesc from flashdreams.runtime_v2.step_result import StepResult from flashdreams.runtime_v2.user_input_event import ( - CloseUserInputEventData, - FocusUserInputEventData, + CloseUserInputEvent, + FocusUserInputEvent, KeyboardInputState, - KeyboardUserInputEventData, - MouseUserInputEventData, - ResetUserInputEventData, + KeyboardUserInputEvent, + MouseUserInputEvent, + ResetUserInputEvent, UserInputEvent, ) from flashdreams.runtime_v2.video_tensor import VideoTensorLayout @@ -380,6 +380,9 @@ def _buffer_browser_message(self, raw_message: object) -> None: if not isinstance(payload, dict): raise ValueError("Browser event must be a JSON object.") + timestamp_us = self._timestamp_us() + if timestamp_us is None: + return event_type = payload.get("type") if event_type == "keyboard": key = payload.get("key") @@ -388,7 +391,8 @@ def _buffer_browser_message(self, raw_message: object) -> None: raise ValueError("Keyboard event requires a non-empty key.") if not isinstance(pressed, bool): raise ValueError("Keyboard event requires a boolean pressed value.") - event_data = KeyboardUserInputEventData( + event = KeyboardUserInputEvent( + timestamp=timestamp_us, key=key, state=( KeyboardInputState.PRESSED @@ -412,7 +416,8 @@ def _buffer_browser_message(self, raw_message: object) -> None: raise ValueError("Mouse button must be a non-negative integer.") if not isinstance(pressed, bool): raise ValueError("Mouse pressed must be a boolean.") - event_data = MouseUserInputEventData( + event = MouseUserInputEvent( + timestamp=timestamp_us, action=action, x=x, y=y, @@ -425,31 +430,20 @@ def _buffer_browser_message(self, raw_message: object) -> None: focused = payload.get("focused") if not isinstance(focused, bool): raise ValueError("Focus event requires a boolean focused value.") - event_data = FocusUserInputEventData(focused=focused) + event = FocusUserInputEvent( + timestamp=timestamp_us, + focused=focused, + ) elif event_type == "reset": - event_data = ResetUserInputEventData() + event = ResetUserInputEvent(timestamp=timestamp_us) elif event_type == "close": - event_data = CloseUserInputEventData() + event = CloseUserInputEvent(timestamp=timestamp_us) else: raise ValueError("Unsupported browser event type.") - self._append_event(event_data) + self._append_event(event) - def _append_event( - self, - event_data: ( - KeyboardUserInputEventData - | MouseUserInputEventData - | FocusUserInputEventData - | ResetUserInputEventData - | CloseUserInputEventData - ), - ) -> None: - """Timestamp and buffer one validated browser event.""" - session_start_ns = self._session_start_ns - if session_start_ns is None: - return - timestamp_us = np.uint64((time.monotonic_ns() - session_start_ns) // 1_000) - event = UserInputEvent(timestamp=timestamp_us, event_data=event_data) + def _append_event(self, event: UserInputEvent) -> None: + """Buffer one validated browser event.""" callback = self._input_callback if callback is None: raise RuntimeError("WebRTC input callback is not registered.") @@ -463,7 +457,16 @@ def _record_client_disconnect(self) -> None: return self._client_connected = False if not self._closed: - self._append_event(CloseUserInputEventData()) + timestamp_us = self._timestamp_us() + if timestamp_us is not None: + self._append_event(CloseUserInputEvent(timestamp=timestamp_us)) + + def _timestamp_us(self) -> np.uint64 | None: + """Return the current session-relative event timestamp.""" + session_start_ns = self._session_start_ns + if session_start_ns is None: + return None + return np.uint64((time.monotonic_ns() - session_start_ns) // 1_000) async def _enqueue_frames( self, frames: tuple[np.ndarray[Any, np.dtype[np.uint8]], ...] diff --git a/flashdreams/flashdreams/runtime_v2/session_runner.py b/flashdreams/flashdreams/runtime_v2/session_runner.py index 38391cb9b..da84b5146 100644 --- a/flashdreams/flashdreams/runtime_v2/session_runner.py +++ b/flashdreams/flashdreams/runtime_v2/session_runner.py @@ -10,11 +10,11 @@ from flashdreams.api_v2.loop import IModelLoop, IUILoop from flashdreams.api_v2.output_sink import OutputSink from flashdreams.api_v2.session import ISession -from flashdreams.api_v2.user_input_event_data import UserInputEventData +from flashdreams.api_v2.user_input_event import UserInputEvent from flashdreams.runtime_v2.event_buffer import EventBuffer from flashdreams.runtime_v2.session_desc import PresentationMode from flashdreams.runtime_v2.step_result import StepResult -from flashdreams.runtime_v2.user_input_event import CloseUserInputEventData +from flashdreams.runtime_v2.user_input_event import CloseUserInputEvent from flashdreams.runtime_v2.user_input_events import UserInputEvents _LOGGER = logging.getLogger(__name__) @@ -23,11 +23,9 @@ _MODEL_READER_ID = 1 -def _contains(events: UserInputEvents, event_type: type[UserInputEventData]) -> bool: - """Return whether any event in ``events`` carries ``event_type`` data.""" - return any( - isinstance(event.get_event_data(), event_type) for event in events.get_events() - ) +def _contains(events: UserInputEvents, event_type: type[UserInputEvent]) -> bool: + """Return whether ``events`` contains an instance of ``event_type``.""" + return any(isinstance(event, event_type) for event in events.get_events()) def _log_secondary_failure(message: str, error: BaseException) -> None: @@ -93,7 +91,7 @@ def run_session( def collect_input() -> UserInputEvents: events = window.get_user_input_events() event_buffer.append(events) - if _contains(events, CloseUserInputEventData): + if _contains(events, CloseUserInputEvent): stop.set() return events diff --git a/flashdreams/flashdreams/runtime_v2/slangpy_ui_loop.py b/flashdreams/flashdreams/runtime_v2/slangpy_ui_loop.py index 4a4b9a39c..b93366ebd 100644 --- a/flashdreams/flashdreams/runtime_v2/slangpy_ui_loop.py +++ b/flashdreams/flashdreams/runtime_v2/slangpy_ui_loop.py @@ -9,7 +9,7 @@ from torch import Tensor from flashdreams.api_v2.loop import IUILoop -from flashdreams.runtime_v2._slangpy_ui_renderer import ( +from flashdreams.runtime_v2.slangpy_ui_renderer import ( _SlangPyUIRenderer, _UIRenderer, ) diff --git a/flashdreams/flashdreams/runtime_v2/_slangpy_ui_renderer.py b/flashdreams/flashdreams/runtime_v2/slangpy_ui_renderer.py similarity index 93% rename from flashdreams/flashdreams/runtime_v2/_slangpy_ui_renderer.py rename to flashdreams/flashdreams/runtime_v2/slangpy_ui_renderer.py index 3e76f1298..8666f0c89 100644 --- a/flashdreams/flashdreams/runtime_v2/_slangpy_ui_renderer.py +++ b/flashdreams/flashdreams/runtime_v2/slangpy_ui_renderer.py @@ -12,8 +12,8 @@ from flashdreams.runtime_v2.user_input_event import ( KeyboardInputState, - KeyboardUserInputEventData, - MouseUserInputEventData, + KeyboardUserInputEvent, + MouseUserInputEvent, ) from flashdreams.runtime_v2.user_input_events import UserInputEvents @@ -235,10 +235,9 @@ def _route_input_events( ) -> None: """Route supported runtime input events into SlangPy's UI context.""" for event in events.get_events(): - data = event.get_event_data() - if isinstance(data, KeyboardUserInputEventData): - pressed = data.state is KeyboardInputState.PRESSED - key = _resolve_slangpy_key(slangpy, data.key) + if isinstance(event, KeyboardUserInputEvent): + pressed = event.state is KeyboardInputState.PRESSED + key = _resolve_slangpy_key(slangpy, event.key) if key is not None: key_event = slangpy.KeyboardEvent() key_event.type = ( @@ -249,33 +248,33 @@ def _route_input_events( key_event.key = key key_event.mods = slangpy.KeyModifierFlags.none ui_context.handle_keyboard_event(key_event) - if pressed and len(data.key) == 1: + if pressed and len(event.key) == 1: text_event = slangpy.KeyboardEvent() text_event.type = slangpy.KeyboardEventType.input - text_event.codepoint = ord(data.key) + text_event.codepoint = ord(event.key) text_event.mods = slangpy.KeyModifierFlags.none ui_context.handle_keyboard_event(text_event) - elif isinstance(data, MouseUserInputEventData): + elif isinstance(event, MouseUserInputEvent): mouse_event = slangpy.MouseEvent() - mouse_event.pos = (data.x * width, data.y * height) + mouse_event.pos = (event.x * width, event.y * height) mouse_event.mods = slangpy.KeyModifierFlags.none - if data.action == "button": + if event.action == "button": buttons = ( slangpy.MouseButton.left, slangpy.MouseButton.middle, slangpy.MouseButton.right, ) - if not 0 <= data.button < len(buttons): + if not 0 <= event.button < len(buttons): continue mouse_event.type = ( slangpy.MouseEventType.button_down - if data.pressed + if event.pressed else slangpy.MouseEventType.button_up ) - mouse_event.button = buttons[data.button] - elif data.action == "wheel": + mouse_event.button = buttons[event.button] + elif event.action == "wheel": mouse_event.type = slangpy.MouseEventType.scroll - mouse_event.scroll = (data.wheel_x, data.wheel_y) + mouse_event.scroll = (event.wheel_x, event.wheel_y) else: mouse_event.type = slangpy.MouseEventType.move ui_context.handle_mouse_event(mouse_event) diff --git a/flashdreams/flashdreams/runtime_v2/user_input_event.py b/flashdreams/flashdreams/runtime_v2/user_input_event.py index d6dfd51c1..52cf02498 100644 --- a/flashdreams/flashdreams/runtime_v2/user_input_event.py +++ b/flashdreams/flashdreams/runtime_v2/user_input_event.py @@ -1,15 +1,13 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""User input events, each a timestamp plus the data for one input modality.""" +"""Concrete user input events for supported input modalities.""" from dataclasses import dataclass from enum import Enum from typing import Literal -from numpy import uint64 - -from flashdreams.api_v2.user_input_event_data import UserInputEventData +from flashdreams.api_v2.user_input_event import UserInputEvent class KeyboardInputState(Enum): @@ -23,8 +21,8 @@ class KeyboardInputState(Enum): @dataclass(frozen=True, slots=True, eq=False) -class NumeralKeypadUserInputEventData(UserInputEventData): - """User input event data for numeral keypad.""" +class NumeralKeypadUserInputEvent(UserInputEvent): + """User input event for a numeral keypad.""" @classmethod def get_type_name(cls) -> str: @@ -36,8 +34,8 @@ def get_type_name(cls) -> str: @dataclass(frozen=True, slots=True, eq=False) -class KeyboardUserInputEventData(UserInputEventData): - """User input event data for keyboard.""" +class KeyboardUserInputEvent(UserInputEvent): + """User input event for a keyboard.""" @classmethod def get_type_name(cls) -> str: @@ -51,7 +49,7 @@ def get_type_name(cls) -> str: @dataclass(frozen=True, slots=True, eq=False) -class CloseUserInputEventData(UserInputEventData): +class CloseUserInputEvent(UserInputEvent): """The client asked to end the run, or went away. A window reports this for its X button, a quit shortcut, or a client that @@ -65,7 +63,7 @@ def get_type_name(cls) -> str: @dataclass(frozen=True, slots=True, eq=False) -class ResetUserInputEventData(UserInputEventData): +class ResetUserInputEvent(UserInputEvent): """The client asked to start the run over. Every registered loop resets before its next ``step``, its step index starts @@ -80,8 +78,8 @@ def get_type_name(cls) -> str: @dataclass(frozen=True, slots=True, eq=False) -class MouseUserInputEventData(UserInputEventData): - """User input event data for mouse.""" +class MouseUserInputEvent(UserInputEvent): + """User input event for a mouse.""" @classmethod def get_type_name(cls) -> str: @@ -105,7 +103,7 @@ def get_type_name(cls) -> str: @dataclass(frozen=True, slots=True, eq=False) -class FocusUserInputEventData(UserInputEventData): +class FocusUserInputEvent(UserInputEvent): """Client viewport focus change.""" @classmethod @@ -123,8 +121,8 @@ def get_type_name(cls) -> str: @dataclass(frozen=True, slots=True, eq=False) -class TouchUserInputEventData(UserInputEventData): - """User input event data for touch.""" +class TouchUserInputEvent(UserInputEvent): + """User input event for touch.""" @classmethod def get_type_name(cls) -> str: @@ -133,8 +131,8 @@ def get_type_name(cls) -> str: @dataclass(frozen=True, slots=True, eq=False) -class GamepadUserInputEventData(UserInputEventData): - """User input event data for gamepad.""" +class GamepadUserInputEvent(UserInputEvent): + """User input event for a gamepad.""" @classmethod def get_type_name(cls) -> str: @@ -143,8 +141,8 @@ def get_type_name(cls) -> str: @dataclass(frozen=True, slots=True, eq=False) -class GameWheelUserInputEventData(UserInputEventData): - """User input event data for game wheel.""" +class GameWheelUserInputEvent(UserInputEvent): + """User input event for a game wheel.""" @classmethod def get_type_name(cls) -> str: @@ -153,8 +151,8 @@ def get_type_name(cls) -> str: @dataclass(frozen=True, slots=True, eq=False) -class XRControllerUserInputEventData(UserInputEventData): - """User input event data for XR controllers.""" +class XRControllerUserInputEvent(UserInputEvent): + """User input event for XR controllers.""" @classmethod def get_type_name(cls) -> str: @@ -163,33 +161,10 @@ def get_type_name(cls) -> str: @dataclass(frozen=True, slots=True, eq=False) -class UnknownUserInputEventData(UserInputEventData): - """User input event data for unknown.""" +class UnknownUserInputEvent(UserInputEvent): + """User input event for an unknown input modality.""" @classmethod def get_type_name(cls) -> str: """Return the event type name.""" return "unknown" - - -@dataclass(frozen=True, slots=True) -class UserInputEvent: - """One input event: when it happened, and what happened. - - The data decides what the event is, so a loop reading input dispatches on - the type of :meth:`get_event_data` rather than on anything here. - """ - - timestamp: uint64 - """Timestamp in microseconds since the start of the session.""" - - event_data: UserInputEventData - """What happened, as one of the types in this module.""" - - def get_timestamp(self) -> uint64: - """Return the timestamp.""" - return self.timestamp - - def get_event_data(self) -> UserInputEventData: - """Return the event data structure with type & data.""" - return self.event_data diff --git a/flashdreams/flashdreams/runtime_v2/user_input_events.py b/flashdreams/flashdreams/runtime_v2/user_input_events.py index 7248cbbd8..ea53b1d3e 100644 --- a/flashdreams/flashdreams/runtime_v2/user_input_events.py +++ b/flashdreams/flashdreams/runtime_v2/user_input_events.py @@ -9,13 +9,6 @@ @dataclass(frozen=True) -class UserInputEventsData: - """Sorted events held by one :class:`UserInputEvents` batch.""" - - events: list[UserInputEvent] - """Input events ordered by timestamp.""" - - class UserInputEvents: """One batch of user input events, sorted by timestamp and not modifiable. @@ -24,7 +17,7 @@ class UserInputEvents: neither end has to. """ - _data: UserInputEventsData + _data: list[UserInputEvent] """Immutable event collection data.""" def __init__(self, events: list[UserInputEvent]) -> None: @@ -33,10 +26,12 @@ def __init__(self, events: list[UserInputEvent]) -> None: Args: events: Events to hold, in any order. """ - self._data = UserInputEventsData( - events=sorted(events, key=lambda event: event.get_timestamp()), + object.__setattr__( + self, + "_data", + sorted(events, key=lambda event: event.get_timestamp()), ) def get_events(self) -> list[UserInputEvent]: """Return a copy of the events, oldest first.""" - return list(self._data.events) + return list(self._data) diff --git a/flashdreams/test_v2/test_application_runner.py b/flashdreams/test_v2/test_application_runner.py index b98574dd4..435c67c64 100644 --- a/flashdreams/test_v2/test_application_runner.py +++ b/flashdreams/test_v2/test_application_runner.py @@ -18,8 +18,7 @@ from flashdreams.runtime_v2.session_desc import PresentationMode, SessionDesc from flashdreams.runtime_v2.step_result import StepResult from flashdreams.runtime_v2.user_input_event import ( - CloseUserInputEventData, - UserInputEvent, + CloseUserInputEvent, ) from flashdreams.runtime_v2.user_input_events import UserInputEvents from flashdreams.runtime_v2.video_tensor import VideoTensorLayout @@ -119,9 +118,8 @@ def get_user_input_events(self) -> UserInputEvents: self._reported_close = True return UserInputEvents( [ - UserInputEvent( + CloseUserInputEvent( timestamp=uint64(0), - event_data=CloseUserInputEventData(), ) ] ) diff --git a/flashdreams/test_v2/test_client_window.py b/flashdreams/test_v2/test_client_window.py index 1336ff0ba..720ab4acc 100644 --- a/flashdreams/test_v2/test_client_window.py +++ b/flashdreams/test_v2/test_client_window.py @@ -14,8 +14,7 @@ from flashdreams.runtime_v2.session_desc import SessionDesc from flashdreams.runtime_v2.step_result import StepResult from flashdreams.runtime_v2.user_input_event import ( - NumeralKeypadUserInputEventData, - UserInputEvent, + NumeralKeypadUserInputEvent, ) from flashdreams.runtime_v2.user_input_events import UserInputEvents @@ -77,9 +76,9 @@ def test_client_window_for_null_model() -> None: current_step_index = 0 test_event_data = 2 while current_timestamp < 1000: - numeral_keypad_input = UserInputEvent( + numeral_keypad_input = NumeralKeypadUserInputEvent( timestamp=current_timestamp, - event_data=NumeralKeypadUserInputEventData(value=test_event_data), + value=test_event_data, ) # This is the client-windowing system updating user-inputs handled by the @@ -89,12 +88,12 @@ def test_client_window_for_null_model() -> None: # Model and UI threads share this input. get_user_input_events = client_window.get_user_input_events() assert get_user_input_events.get_events() == [numeral_keypad_input] - event_data = get_user_input_events.get_events()[0].get_event_data() - assert isinstance(event_data, NumeralKeypadUserInputEventData) + event = get_user_input_events.get_events()[0] + assert isinstance(event, NumeralKeypadUserInputEvent) # This is inside our `step` loop. output = pipeline.generate( - current_step_index, cache, input=torch.tensor([[event_data.value]]) + current_step_index, cache, input=torch.tensor([[event.value]]) ) ## Note: model output is in bcthw layout, but in theory the model could output bctwh and we would require a swizzle operation to get to bcthw client_window.write( @@ -107,12 +106,8 @@ def test_client_window_for_null_model() -> None: ) ) - assert ( - numeral_keypad_input.get_event_data().get_type_name() - == NumeralKeypadUserInputEventData.get_type_name() - ) - assert event_data.get_type_name() == "numeral_keypad" - assert event_data.value == test_event_data + assert event.get_type_name() == "numeral_keypad" + assert event.value == test_event_data assert output.shape == (1, 3, 1, 1, 1) assert output[0, 0, 0, 0, 0].item() == current_step_index + test_event_data diff --git a/flashdreams/test_v2/test_session_runner.py b/flashdreams/test_v2/test_session_runner.py index 171de6ee8..b1a2e91ec 100644 --- a/flashdreams/test_v2/test_session_runner.py +++ b/flashdreams/test_v2/test_session_runner.py @@ -13,7 +13,7 @@ from flashdreams.api_v2.client_window import IClientWindow from flashdreams.api_v2.loop import IModelLoop, IUILoop, invoke_async from flashdreams.api_v2.session import ISession -from flashdreams.api_v2.user_input_event_data import UserInputEventData +from flashdreams.api_v2.user_input_event import UserInputEvent from flashdreams.runtime_v2.blit_model_output_to_screen_loop import ( BlitModelOutputToScreenLoop, ) @@ -26,11 +26,10 @@ from flashdreams.runtime_v2.session_runner import run_session from flashdreams.runtime_v2.step_result import StepResult from flashdreams.runtime_v2.user_input_event import ( - CloseUserInputEventData, + CloseUserInputEvent, KeyboardInputState, - KeyboardUserInputEventData, - ResetUserInputEventData, - UserInputEvent, + KeyboardUserInputEvent, + ResetUserInputEvent, ) from flashdreams.runtime_v2.user_input_events import UserInputEvents from flashdreams.runtime_v2.video_tensor import VideoTensorLayout @@ -338,18 +337,17 @@ def _session_desc( def _key_event() -> UserInputEvents: return UserInputEvents( [ - UserInputEvent( + KeyboardUserInputEvent( timestamp=uint64(0), - event_data=KeyboardUserInputEventData( - key="a", state=KeyboardInputState.PRESSED - ), + key="a", + state=KeyboardInputState.PRESSED, ) ] ) -def _lifecycle_event(event_data: UserInputEventData) -> UserInputEvents: - return UserInputEvents([UserInputEvent(timestamp=uint64(0), event_data=event_data)]) +def _lifecycle_event(event_type: type[UserInputEvent]) -> UserInputEvents: + return UserInputEvents([event_type(timestamp=uint64(0))]) def test_run_session_presents_every_step_in_order() -> None: @@ -571,7 +569,7 @@ def test_run_session_gives_the_first_step_input_already_collected() -> None: def test_run_session_stops_when_the_window_reports_a_close() -> None: log = CallLog() session = FakeSession(_session_desc(), log) - window = RecordingClientWindow(log, [_lifecycle_event(CloseUserInputEventData())]) + window = RecordingClientWindow(log, [_lifecycle_event(CloseUserInputEvent)]) # No step count at all: the close is the only thing that ends this run. run_session(session, window, steps=None) @@ -583,7 +581,7 @@ def test_run_session_stops_when_the_window_reports_a_close() -> None: def test_run_session_resets_the_session_and_the_step_index() -> None: log = CallLog() session = FakeSession(_session_desc(), log) - window = RecordingClientWindow(log, [_lifecycle_event(ResetUserInputEventData())]) + window = RecordingClientWindow(log, [_lifecycle_event(ResetUserInputEvent)]) run_session(session, window, steps=2) @@ -623,7 +621,7 @@ def test_run_session_lets_a_reset_restart_a_finished_session() -> None: """A session that starts over is asked about the run it is starting.""" log = CallLog() session = FiniteSession(_session_desc(), log, length=1, generated=1) - window = RecordingClientWindow(log, [_lifecycle_event(ResetUserInputEventData())]) + window = RecordingClientWindow(log, [_lifecycle_event(ResetUserInputEvent)]) run_session(session, window, steps=3) @@ -662,9 +660,7 @@ def test_run_session_gives_the_step_after_a_reset_the_whole_batch() -> None: UserInputEvents( [ held_key, - UserInputEvent( - timestamp=uint64(1), event_data=ResetUserInputEventData() - ), + ResetUserInputEvent(timestamp=uint64(1)), ] ) ], @@ -683,7 +679,7 @@ def test_run_session_keeps_polling_while_the_final_result_is_pending() -> None: session = FakeSession(_session_desc(), log) window = RecordingClientWindow( log, - [UserInputEvents([]), _lifecycle_event(ResetUserInputEventData())], + [UserInputEvents([]), _lifecycle_event(ResetUserInputEvent)], ) run_session(session, window, steps=1) @@ -716,7 +712,7 @@ def get_user_input_events(self) -> UserInputEvents: session = SlowFirstStep(_session_desc(), log) window = ResettingWindow( log, - [UserInputEvents([]), _lifecycle_event(ResetUserInputEventData())], + [UserInputEvents([]), _lifecycle_event(ResetUserInputEvent)], ) run_session(session, window, steps=2) @@ -811,8 +807,8 @@ def test_run_session_discards_results_generated_before_a_reset( log, [ UserInputEvents([]), - _lifecycle_event(ResetUserInputEventData()), - _lifecycle_event(CloseUserInputEventData()), + _lifecycle_event(ResetUserInputEvent), + _lifecycle_event(CloseUserInputEvent), ], ) diff --git a/flashdreams/test_v2/test_slangpy_ui_renderer.py b/flashdreams/test_v2/test_slangpy_ui_renderer.py index 33a7ae0eb..949d53e59 100644 --- a/flashdreams/test_v2/test_slangpy_ui_renderer.py +++ b/flashdreams/test_v2/test_slangpy_ui_renderer.py @@ -9,10 +9,9 @@ import pytest from numpy import uint64 -from flashdreams.runtime_v2._slangpy_ui_renderer import _route_input_events +from flashdreams.runtime_v2.slangpy_ui_renderer import _route_input_events from flashdreams.runtime_v2.user_input_event import ( - MouseUserInputEventData, - UserInputEvent, + MouseUserInputEvent, ) from flashdreams.runtime_v2.user_input_events import UserInputEvents @@ -34,15 +33,13 @@ def test_mouse_input_is_routed_through_slangpy_ui_context() -> None: ) events = UserInputEvents( [ - UserInputEvent( + MouseUserInputEvent( timestamp=uint64(0), - event_data=MouseUserInputEventData( - action="button", - x=0.25, - y=0.75, - button=0, - pressed=True, - ), + action="button", + x=0.25, + y=0.75, + button=0, + pressed=True, ) ] ) diff --git a/flashdreams/test_v2/test_webrtc_client_window.py b/flashdreams/test_v2/test_webrtc_client_window.py index 18fba8860..8dfa3ddbe 100644 --- a/flashdreams/test_v2/test_webrtc_client_window.py +++ b/flashdreams/test_v2/test_webrtc_client_window.py @@ -26,10 +26,10 @@ from flashdreams.runtime_v2.session_desc import SessionDesc from flashdreams.runtime_v2.step_result import StepResult from flashdreams.runtime_v2.user_input_event import ( - FocusUserInputEventData, + FocusUserInputEvent, KeyboardInputState, - KeyboardUserInputEventData, - MouseUserInputEventData, + KeyboardUserInputEvent, + MouseUserInputEvent, ) from flashdreams.runtime_v2.video_tensor import VideoTensorLayout from flashdreams.runtime_v2.webrtc_client_window import WebRTCClientWindow @@ -133,9 +133,7 @@ async def test_window_buffers_browser_events_until_drained() -> None: assert len(events) == 4 keyboard_events = [ - data - for event in events - if isinstance(data := event.get_event_data(), KeyboardUserInputEventData) + event for event in events if isinstance(event, KeyboardUserInputEvent) ] assert [(event.key, event.state) for event in keyboard_events] == [ ("w", KeyboardInputState.PRESSED), @@ -143,15 +141,11 @@ async def test_window_buffers_browser_events_until_drained() -> None: ] assert events[0].get_timestamp() <= events[1].get_timestamp() mouse = next( - data - for event in events - if isinstance(data := event.get_event_data(), MouseUserInputEventData) + event for event in events if isinstance(event, MouseUserInputEvent) ) assert (mouse.action, mouse.x, mouse.y) == ("move", 0.25, 0.75) focus = next( - data - for event in events - if isinstance(data := event.get_event_data(), FocusUserInputEventData) + event for event in events if isinstance(event, FocusUserInputEvent) ) assert focus.focused assert window.get_user_input_events().get_events() == [] diff --git a/integrations_v2/red_screen/red_screen/app.py b/integrations_v2/red_screen/red_screen/app.py index d64b79cce..e1d263600 100644 --- a/integrations_v2/red_screen/red_screen/app.py +++ b/integrations_v2/red_screen/red_screen/app.py @@ -19,7 +19,7 @@ from flashdreams.runtime_v2.step_result import StepResult from flashdreams.runtime_v2.user_input_event import ( KeyboardInputState, - KeyboardUserInputEventData, + KeyboardUserInputEvent, ) from flashdreams.runtime_v2.user_input_events import UserInputEvents from flashdreams.runtime_v2.video_tensor import VideoTensorLayout @@ -118,14 +118,14 @@ def _apply_events(state: RedScreenModelState, events: UserInputEvents) -> None: received_events = events.get_events() if not received_events: return - data = received_events[-1].get_event_data() - if not isinstance(data, KeyboardUserInputEventData): + event = received_events[-1] + if not isinstance(event, KeyboardUserInputEvent): return - if data.key == state.config.activation_key: - state.key_held = data.state is KeyboardInputState.PRESSED - elif data.state is KeyboardInputState.PRESSED and data.key.lower() == "w": + if event.key == state.config.activation_key: + state.key_held = event.state is KeyboardInputState.PRESSED + elif event.state is KeyboardInputState.PRESSED and event.key.lower() == "w": state.color_intensity = min(1.0, state.color_intensity + 0.1) - elif data.state is KeyboardInputState.PRESSED and data.key.lower() == "s": + elif event.state is KeyboardInputState.PRESSED and event.key.lower() == "s": state.color_intensity = max(0.0, state.color_intensity - 0.1) diff --git a/integrations_v2/red_screen/red_screen/tests/test_red_screen.py b/integrations_v2/red_screen/red_screen/tests/test_red_screen.py index 8d5343011..a5d784b67 100644 --- a/integrations_v2/red_screen/red_screen/tests/test_red_screen.py +++ b/integrations_v2/red_screen/red_screen/tests/test_red_screen.py @@ -17,8 +17,7 @@ from flashdreams.runtime_v2.step_result import StepResult from flashdreams.runtime_v2.user_input_event import ( KeyboardInputState, - KeyboardUserInputEventData, - UserInputEvent, + KeyboardUserInputEvent, ) from flashdreams.runtime_v2.user_input_events import UserInputEvents from flashdreams.runtime_v2.video_tensor import VideoTensorLayout @@ -92,15 +91,13 @@ def _session_desc( def _key_event(*, pressed: bool, key: str = _ACTIVATION_KEY) -> UserInputEvents: return UserInputEvents( [ - UserInputEvent( + KeyboardUserInputEvent( timestamp=uint64(0), - event_data=KeyboardUserInputEventData( - key=key, - state=( - KeyboardInputState.PRESSED - if pressed - else KeyboardInputState.RELEASED - ), + key=key, + state=( + KeyboardInputState.PRESSED + if pressed + else KeyboardInputState.RELEASED ), ) ] @@ -182,17 +179,15 @@ def test_red_screen_uses_last_event_to_adjust_color_intensity() -> None: 1, UserInputEvents( [ - UserInputEvent( + KeyboardUserInputEvent( timestamp=uint64(0), - event_data=KeyboardUserInputEventData( - key="w", state=KeyboardInputState.PRESSED - ), + key="w", + state=KeyboardInputState.PRESSED, ), - UserInputEvent( + KeyboardUserInputEvent( timestamp=uint64(1), - event_data=KeyboardUserInputEventData( - key="s", state=KeyboardInputState.PRESSED - ), + key="s", + state=KeyboardInputState.PRESSED, ), ] ), diff --git a/integrations_v2/slangpy_ui_demo/slangpy_ui_demo/invoke_async_app.py b/integrations_v2/slangpy_ui_demo/slangpy_ui_demo/invoke_async_app.py index cc936c410..9e2c95b1b 100644 --- a/integrations_v2/slangpy_ui_demo/slangpy_ui_demo/invoke_async_app.py +++ b/integrations_v2/slangpy_ui_demo/slangpy_ui_demo/invoke_async_app.py @@ -18,7 +18,7 @@ from flashdreams.runtime_v2.step_result import StepResult from flashdreams.runtime_v2.user_input_event import ( KeyboardInputState, - KeyboardUserInputEventData, + KeyboardUserInputEvent, ) from flashdreams.runtime_v2.user_input_events import UserInputEvents from flashdreams.runtime_v2.video_tensor import VideoTensorLayout @@ -93,11 +93,10 @@ def step_ui( self.state.instructions = ui.Text(window, "Press W to toggle red / blue") for event in events.get_events(): - data = event.get_event_data() if ( - isinstance(data, KeyboardUserInputEventData) - and data.state is KeyboardInputState.PRESSED - and data.key.lower() == "w" + isinstance(event, KeyboardUserInputEvent) + and event.state is KeyboardInputState.PRESSED + and event.key.lower() == "w" ): invoke_async(self.state.model_loop, lambda state: state._toggle_color()) return self.presented_model_frame() diff --git a/integrations_v2/slangpy_ui_demo/slangpy_ui_demo/tests/test_slangpy_ui_demo.py b/integrations_v2/slangpy_ui_demo/slangpy_ui_demo/tests/test_slangpy_ui_demo.py index f49e00494..91d308c40 100644 --- a/integrations_v2/slangpy_ui_demo/slangpy_ui_demo/tests/test_slangpy_ui_demo.py +++ b/integrations_v2/slangpy_ui_demo/slangpy_ui_demo/tests/test_slangpy_ui_demo.py @@ -24,13 +24,12 @@ ) from slangpy_ui_demo.text_input_app import TextInputSlangPyUILoop, TextInputState -from flashdreams.runtime_v2._slangpy_ui_renderer import _route_input_events from flashdreams.runtime_v2.presentation_manager import PresentationManager from flashdreams.runtime_v2.session_desc import SessionDesc +from flashdreams.runtime_v2.slangpy_ui_renderer import _route_input_events from flashdreams.runtime_v2.user_input_event import ( KeyboardInputState, - KeyboardUserInputEventData, - UserInputEvent, + KeyboardUserInputEvent, ) from flashdreams.runtime_v2.user_input_events import UserInputEvents @@ -69,11 +68,10 @@ def test_invoke_async_toggles_model_owned_color_on_w_press() -> None: ) w_pressed = UserInputEvents( [ - UserInputEvent( + KeyboardUserInputEvent( timestamp=uint64(0), - event_data=KeyboardUserInputEventData( - key="W", state=KeyboardInputState.PRESSED - ), + key="W", + state=KeyboardInputState.PRESSED, ) ] ) @@ -144,9 +142,10 @@ def test_slangpy_ui_routes_pressed_and_released_key_edges() -> None: ) events = UserInputEvents( [ - UserInputEvent( + KeyboardUserInputEvent( timestamp=uint64(index), - event_data=KeyboardUserInputEventData(key="ArrowLeft", state=state), + key="ArrowLeft", + state=state, ) for index, state in enumerate( (KeyboardInputState.PRESSED, KeyboardInputState.RELEASED)