461 Commits
0.44.1 ... dev

Author SHA1 Message Date
Schmarni
e00b487167 fix: destroy prebound parts on model drop
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-31 23:41:28 +01:00
Schmarni
771a79cd33 fix: don't panic when Model is dropped before parts are generated
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-31 20:57:36 +01:00
Schmarni
a3afb08664 fix: only re-mesh lines if needed
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-31 08:22:13 +01:00
Schmarni
4de91ef20b fix: improve xr input accuracy
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-31 08:07:03 +01:00
Schmarni
94630c451a chore: remove unneeded system
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-31 07:45:25 +01:00
Schmarni
8ab891edb5 chore: silence unimportant warning
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-31 07:44:12 +01:00
Schmarni
9f89d1a9ec fix: don't recursivly despawn entities that handles depsawning themselfs
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-31 07:40:04 +01:00
Schmarni
c8e8ae2506 chore: update bevy-dmabuf, again
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-31 05:55:01 +01:00
Schmarni
929c061f36 chore: update bevy-dmabuf
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-31 05:42:20 +01:00
Schmarni
189c09ed79 chore: update bevy-dmabuf
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-31 04:38:51 +01:00
Schmarni
ccbd773cee fix: gate bevy-dmabuf plugin behind wayland
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-31 04:00:23 +01:00
Schmarni
87857090b4 fix: compiling without the wayland feature
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-31 02:23:37 +01:00
Schmarni
5f152df9f7 fix: getting bounding boxes before model is fully loaded now waits until the model and bounding boxes are loaded
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-31 01:25:20 +01:00
Nova
b70a188e67 fix(main): don't init winit on display or wayland_display empty 2025-10-29 20:32:34 -07:00
Schmarni
5a2bb6faed fix: lines not moving when its moved using spatials: bad impl
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-29 16:28:35 +01:00
Schmarni
65c426f981 feat: impl proper entity handles
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-28 23:02:24 +01:00
Schmarni
ec468b6752 chore: cleanup
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-26 00:43:25 +02:00
Schmarni
23f0b5f880 fix: properly implement lines by only transforming points, not the mesh, also make lines and holdout bindless
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-26 00:01:40 +02:00
Schmarni
8bfb01808a fix: fix broken transforms on model spawn
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-24 23:42:15 +02:00
Schmarni
e621f4b60e feat: use entity hashmap for transform sync
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-24 03:10:36 +02:00
Schmarni
7e0b956f49 fix: make the dirty flag work
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-24 02:30:59 +02:00
Nova
4d229b95b6 refactor(spatial): dirty flag 2025-10-24 02:30:59 +02:00
Nova
392eaf4ee5 fix(spatial): proper visibility culling scaling 2025-10-23 15:46:02 -07:00
Nova
6e2de6ac87 fix(spatial): no more zero values in scale 2025-10-23 15:37:19 -07:00
Nova
04e20a3bbf refactor(spatial): local visibility calculation 2025-10-22 15:51:21 -07:00
Schmarni
429a6efcee fix: disable hands when xr is not available
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-22 23:40:29 +02:00
Schmarni
b1ab37c8dc Revert "fix: don't wait for frame"
This reverts commit 2b07345286.
2025-10-22 17:25:09 +02:00
Nova
181224767d fix: make color proper 2025-10-21 16:44:34 -07:00
Nova
2b07345286 fix: don't wait for frame 2025-10-21 15:08:40 -07:00
Nova
d5034b4034 fix: instrumentation 2025-10-21 15:07:21 -07:00
Nova
891d90fc5e upgrade: schemas 2025-10-21 02:15:12 -07:00
Nova
ba97528ed6 feat(objects/mouse_pointer): more key compatibility 2025-10-20 22:51:52 -07:00
Nova
536fafb4cf fix(objects/mouse_pointer): right modifier keys 2025-10-20 22:42:56 -07:00
Nova
59e6a11079 fix: mouse pointer target reliability issues 2025-10-20 22:42:36 -07:00
Nova
0e4f5de529 refactor(objects/mouse_pointer): single task pointer 2025-10-20 15:10:48 -07:00
Schmarni
05d4670609 feat: impl Pipelined Rendering
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-21 00:09:17 +02:00
Schmarni
08cec3c700 chore: fmt
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-20 20:52:58 +02:00
Schmarni
0b29f2f6c9 fix(input/controllers): fix cursor rendering
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-20 20:52:21 +02:00
Nova
38a0520299 feat(objects): async tracked abstraction 2025-10-11 21:46:27 -07:00
Nova
a080560c9c update(schemas): more efficient in tokio tasks 2025-10-11 18:37:00 -07:00
Nova
024ae4ddd7 refactor: rename flatscreen to force flatscreen 2025-10-11 18:36:47 -07:00
Nova
6742caa967 revert: bring back task abstraction 2025-10-11 15:51:26 -07:00
Nova
ba5415653e fix(wayland): make tokio tasks actually close properly 2025-10-11 13:32:22 -07:00
Nova
ddef55879a fix: tokio tracing 2025-10-11 12:19:24 -07:00
Nova
c63416d1f3 fix: tokio tasks 2025-10-11 03:19:56 -07:00
Schmarni
b0ee7e9f54 fix(wayland): fix panic when an app requests presentation_feedback without having bound a display
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-10 21:41:02 +02:00
Nova
ec871f5963 update: stardust xr core 2025-10-05 13:12:29 -07:00
Nova
418e3a2ccb refactor: make flatscreen flag explicit 2025-10-03 02:40:29 -07:00
Schmarni
75bdb44371 chore: bump mesh-text crate version and remove unneeded fd
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-01 10:21:08 +02:00
Schmarni
6678681c2c fix(wayland/shm): don't leak fds on shm pool destruction
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-01 01:19:32 +02:00
Schmarni
3edaaf2dfc fix(wayland/pointer): don't send axis_discrete events when using version 8 or above, as required per spec
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-10-01 00:00:12 +02:00
Schmarni
0ebfc1153e chore(wayland): update waynest
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-30 22:32:09 +02:00
Schmarni
2d6bc06cbe fix(wayland): manually remove objects from connection on destroy
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-29 17:45:03 +02:00
Schmarni
bbf12b9e31 fix(wayland/keyboard): fix modifier key not working properly for some keyboard layouts
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-29 13:18:14 +02:00
Nova
621a9c6d85 upgrade: waynest 2025-09-26 16:32:46 -07:00
Nova
96e910c450 update: stardust-xr core 2025-09-23 14:08:52 -07:00
Nova
25b0760913 refactor(spatial): general improvements and efficiency 2025-09-21 04:17:45 -07:00
Nova
b542dc1b23 fix: cache global transform 2025-09-21 02:47:34 -07:00
Nova
3e4be41d3f fix(lockfile): idk why but it insisted 2025-09-21 00:44:29 -07:00
Nova
76fc1bfab5 cleanup: thingys 2025-09-21 00:40:14 -07:00
Nova
928886563d update: rust version and cargo.toml 2025-09-21 00:40:14 -07:00
Schmarni
e3a3db246e fix(lines): working OIT for lines!
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-18 09:40:40 +02:00
Schmarni
b4dccf6f89 fix(input/controllers): don't apply a scale to the exported Spatial
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-17 05:51:29 +02:00
Nova
0599eece82 update: waynest 2025-09-15 18:32:13 -07:00
Nova
27196e2dda fix(wayland): ignore viewport struct fields 2025-09-15 02:00:26 -07:00
Nova
0a4d6adf74 update(cargo.lock): it insisted 2025-09-14 23:49:32 -07:00
Schmarni
cf1cb90642 fix(wayland/dmabuf): only suggest formats that have srgb variants, fixes blender and vkcube, in the future it might be better to do such a conversion in a shader for more formats
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-15 08:36:07 +02:00
Schmarni
2343bbc974 chore(lines): a bit of cleanup
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-15 08:24:01 +02:00
Nova
fd7cad1ab4 fix(wayland/popup): give real configure geometry 2025-09-14 20:49:54 -07:00
Schmarni
0c3efe9477 feat(bevy/oit): use patched bevy for Premultiplied Alpha in OIT
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-15 05:07:58 +02:00
Nova
065214c873 fix(wayland/core): surface role set properly 2025-09-14 17:08:59 -07:00
Nova
795f111ebc fix(wayland/output): give it a name/description 2025-09-14 01:12:00 -07:00
Nova
f40c6dcbd4 fix(wayland/surface): keep frame callback order 2025-09-14 00:55:26 -07:00
Nova
2632a0c5f3 fix(wayland): proper frame callback handling 2025-09-14 00:40:17 -07:00
Schmarni
1bfd9c95f0 feat(wayland/presentation_time): hook up presentation time to OpenXR
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-12 06:34:14 +02:00
Nova
a753001f15 fix(wayland/core): surface role set properly 2025-09-11 18:01:19 -07:00
Nova
cbd77fe704 cleanup: code thingys 2025-09-11 17:55:55 -07:00
Nova
c5246c9dc8 refactor(wayland/surface): better error formatting 2025-09-11 16:02:39 -07:00
Nova
209171abfc cleanup: clippy 2025-09-11 16:02:25 -07:00
Nova
7e53db3d33 feat(wayland): try set role modularization 2025-09-11 15:53:31 -07:00
Schmarni
4bc71b01cb feat(wayland/shm): impl shm ontop of dmabuf (again), and make the upload async
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-12 00:51:17 +02:00
Nova
c4ccda2118 fix(wayland/xdg/positioner): properly calculate with gravity 2025-09-10 13:33:42 -07:00
Nova
3a55aaa2cf fix(wayland/shm/backing): allow shm pool different size 2025-09-09 22:21:59 -07:00
Nova
0650956ab4 clean(wayland): unnecessary variables 2025-09-09 18:49:55 -07:00
Nova
45ec292b99 fix(wayland/cursor): things 2025-09-09 14:55:56 -07:00
Nova
550087841f feat(wayland): cursor stuff 2025-09-09 03:13:22 -07:00
Nova
bd1b54cf03 fix(wayland/xdg/popup): configure the popup then xdg surface 2025-09-09 00:37:18 -07:00
Nova
707452462d cleanup: wayland 2025-09-09 00:37:00 -07:00
Nova
1c8aa93850 refactor(wayland): many things 2025-09-06 23:56:29 -07:00
Nova
a0b014576e refactor(wayland): remove surface specialization from roles 2025-09-06 21:16:55 -07:00
Nova
50e1921cd9 refactor(wayland): strong reference to all "parent" types 2025-09-06 16:48:23 -07:00
Nova
14c5c355b5 fix(wayland+wgpu): bad recursion limit increase 2025-09-06 15:34:14 -07:00
Nova
4641f4f724 refactor(wayland): weak references to objects in roles 2025-09-06 15:19:37 -07:00
Nova
053d468035 refactor(wayland): naming conventions 2025-09-06 15:09:39 -07:00
Nova
f44abad5b0 fix(wayland): set xdg surface role properly 2025-09-06 14:40:56 -07:00
Schmarni
9e8f09fe97 feat: add spectator camera flag
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-06 23:36:12 +02:00
Schmarni
ae68d0e135 chore(text): update text mesh crate
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-06 04:55:58 +02:00
Nova
7314428ce7 refactor(wayland): move surface id to wl_surface 2025-09-05 17:04:59 -07:00
Nova
c5440bc426 chore: cleanup text 2025-09-05 17:01:01 -07:00
Nova
ad1c97aad6 refactor(wayland): xdg surface sub-roles 2025-09-04 20:15:07 -07:00
Nova
51b0942c49 chore: cargo fmt 2025-09-04 15:37:12 -07:00
Nova
c665f33d25 fix(wayland/dmabuf_backing): always update surface 2025-09-04 14:14:46 -07:00
Nova
a6a1195922 fix: text (partially) 2025-09-03 13:16:57 -07:00
Nova
d6ad00bf53 chore: cleanup clippy 2025-09-02 14:41:15 -07:00
Schmarni
b6524e90e1 fix(spatial): fix model nodes not despawning properly
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-02 22:47:00 +02:00
Schmarni
6f113a9ec4 fix(lines): add a check to prevenmodel.enh
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-02 22:40:47 +02:00
Schmarni
bf85140b65 fix(lines): add a check to prevent a crash
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-02 06:28:18 +02:00
Schmarni
2b9ba2b957 fix(lines): make lines no longer crash the server on invalid input from clients
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-02 06:28:09 +02:00
Schmarni
8df1ba549e chore(logging): disable useless errors from the bevy_mesh_text_3d crate
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-02 06:27:34 +02:00
Schmarni
dd0e45cffe chore(entity handles): disable error if the destroy channel was closed, was triggerd on shutdown
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-02 06:27:34 +02:00
Schmarni
a14457ecc1 fix(Spatial/Transforms): fix transform propagation by making bevy entities for all spatials
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-02 06:27:34 +02:00
Schmarni
eac44ded78 fix(lines): fix the lines material shader
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-02 06:12:31 +02:00
Nova
2357cba5f9 feat: nonfunctional improvements 2025-09-02 06:12:31 +02:00
Nova
e69d85cc56 it heccin borken 2025-09-02 06:12:31 +02:00
Nova
a5b4939b57 fix(wayland/toplevel): don't unwrap 2025-09-01 20:36:19 -07:00
Schmarni
d9a6ca9bef chore: update bevy_mod_openxr to fix transparency rendering issue
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-09-01 09:05:11 +02:00
Nova
2e3e944f99 fix(mouse_pointer): don't overload the d-bus 2025-08-31 14:38:25 -07:00
AnnoyingRains
fefa66060a fix: nix 2025-08-30 22:55:11 -07:00
matthewcroughan
6b93a1a095 nix/gnome-graphical-test: fix pkgs references to updated names 2025-08-30 22:55:11 -07:00
matthewcroughan
5f6b48c9b4 nix/stardust-xr-server: fix build 2025-08-30 22:55:11 -07:00
Nova
bda2d06e81 cleanup(wayland/registry): clippy 2025-08-29 16:20:31 -07:00
Nova
87907984f6 cleanup(wayland/popup): just imports 2025-08-29 16:20:16 -07:00
Nova
83dbde9bc0 refactor(sky): add/remove ambient light dynamically 2025-08-29 16:20:01 -07:00
Nova
c69b2652c8 fix: disable tonemapping and up ambient light 2025-08-29 16:17:35 -07:00
Nova
ec50f38dfd refactor: session state save data as bin file 2025-08-29 11:54:06 -07:00
Nova
8e9ac71ebb feat(codegen): integrated logging 2025-08-29 10:31:04 -07:00
Schmarni
b95ea8e90f fix(sky): actually commit the file...
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-08-26 23:43:07 +02:00
Schmarni
712678a666 fix(sky): update bevy-equirect to fix skytex
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-08-26 18:47:27 +02:00
Schmarni
1c6b42e69a feat: implement SkyTex and SkyLight
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-08-26 15:11:54 +02:00
Nova
51de346f6b fix(lines): add aabb to bevy to fix frustum culling 2025-08-23 04:21:48 -07:00
Nova
71b1792ee2 fix(drawable/lines): make them unlit 2025-08-22 21:21:51 -07:00
Schmarni
30f340fe41 fix(input): don't unwrap on getting the string of a path, fixes crash sometimes experienced with handtracking
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-08-23 02:06:55 +02:00
Nova
040f86d50d refactor: remove as_any function from Aspect since trait upcasting added 2025-08-21 13:47:19 -07:00
Thomas Colliers
f0a494392a Formatting 2025-08-19 03:52:09 +02:00
Thomas Colliers
a7aa609651 fix: profile_tokio broken due to incompatible types 2025-08-19 03:52:09 +02:00
Thomas Colliers
7a3322efad feat(wayland): WIP implementation of wp_importer 2025-08-18 01:49:45 +02:00
Nova
877a32ab09 fix: presentation feedback send unwrap 2025-08-13 12:01:42 -07:00
Nova
4ccee1bf89 fix(lines): properly break line points with same position 2025-08-13 11:57:51 -07:00
Nova
6b78684650 fix: remove out 2025-08-13 11:22:31 -07:00
Nova
4f75aa5edf fix: window opaqueness 2025-08-13 11:21:18 -07:00
Nova
d92309d0ef fix: ci 2025-08-12 21:24:50 -07:00
Schmarni
c8d9af5217 feat(flatscreen): add option to make flatscreen window transparent
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-08-13 05:16:21 +02:00
Schmarni
127641064a fix(wayland): use a horrible wgpu patch to make dmabuf importing work
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-08-12 05:27:28 +02:00
Nova
76d58fe2f5 fix(wayland): popup 2025-08-11 19:06:20 -07:00
Nova
565cb29b85 fix(wayland/xdg_shell): proper versioning 2025-08-11 18:22:45 -07:00
Nova
5383bbedcd cleanup(wayland): clippy 2025-08-11 18:07:43 -07:00
Nova
41d6b02506 fix: full wayland version compliance 2025-08-11 18:02:25 -07:00
Nova
2719c0b00a fix: cargo.toml indentation 2025-08-11 17:07:27 -07:00
Schmarni
c475ed04a7 fix(wayland): actually commit the presentation module, oops
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-08-11 01:27:12 +02:00
Schmarni
00086221cd feat(wayland): WIP implement wp_presentation_time
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-08-09 00:00:58 +02:00
Schmarni
b31f6bc983 fix(wayland): use an actual timestamp for the frame callback
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-08-07 03:33:18 +02:00
Schmarni
9e72edae67 fix(core): don't display warning if entity handle can't despawn entity
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-08-05 23:07:08 +02:00
Nova
bb0f023040 fix: always submit output enter event 2025-08-05 00:28:34 -07:00
Nova
8e143f97d4 fix(wayland): frame callbacks in order 2025-08-04 22:59:46 -07:00
Nova
e1e773befb fix(wayland): double buffer frame callbacks 2025-08-04 22:02:06 -07:00
Nova
c5b1869f42 refactor(wayland): send frame event right before render 2025-08-04 19:37:21 -07:00
Nova
0384bb8014 fix(wayland): allow multiple frame callbacks 2025-08-04 18:22:36 -07:00
Nova
6c498c60f2 fix(play_space): default to 1.65m down 2025-08-04 14:10:08 -07:00
Nova
07ea966c01 fix: spatial and field exports retained after death 2025-08-04 13:19:21 -07:00
Schmarni
fb0f8d4115 fix(init): ignore display env vars if empty
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-08-04 21:29:49 +02:00
Nova
24beda4a4a fix(wayland): protocol error 2025-08-03 15:43:51 -07:00
Nova
de045a1c68 fix(wayland): dmabuf format picking 2025-08-03 13:47:04 -07:00
Nova
923c1a3cc1 fix(wayland): don't double configure surface 2025-08-03 13:05:01 -07:00
Nova
b26d82c991 refactor(wayland/xdg): cleanup 2025-08-02 17:10:16 -07:00
Nova
2d91ae6162 fix(wayland/drm): typo 2025-08-02 17:10:03 -07:00
Nova
173f32706d fix(wayland): name the seat 2025-07-31 17:43:51 -07:00
Nova
d234d6f765 feat(wayland): data device 2025-07-31 03:05:50 -07:00
Nova
f4c75c5705 feat: wl_drm 2025-07-30 23:24:39 -07:00
Nova
72c5312c5e upgrade: bevy-dmabuf 2025-07-28 19:03:06 -07:00
Nova
00c5190200 fix: force proper working tracy-client version 2025-07-28 18:44:34 -07:00
Nova
2acb75a3fc fix(wayland): lock contention (theoretically) 2025-07-28 17:01:28 -07:00
Nova
c6754bd689 refactor(wayland): reenable dmabuf 2025-07-28 14:55:43 -07:00
Nova
28613f8585 fix: actually remove clients on disconnect ya dunce 2025-07-28 14:49:02 -07:00
Nova
aadf6f6f07 fix: annoying clippy 2025-07-28 14:29:16 -07:00
Schmarni
fe2410e92a chore(fields): make gizmos higher res
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-28 23:11:05 +02:00
Schmarni
0b9f2c47fd feat(fields): implement debug gizmos for fields, controlled over dbus
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-28 23:05:20 +02:00
Schmarni
6dc6628d7e fix(ModelNode): don't import cameras or lights
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-28 02:25:45 +02:00
Schmarni
9c936eb277 fix(wayland): move model node updates to Update to fix texture updates
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-27 18:22:59 +02:00
Nova
8b3d4e611a refactor(line): names 2025-07-26 18:19:45 -07:00
Nova
f2eac318da feat(lines): comment 2025-07-26 17:56:04 -07:00
Nova
840fada1e1 refactor(lines): rename indecies to indices 2025-07-26 17:33:59 -07:00
Schmarni
cd3cf3721a refactor(waylan): WIP, use a basic bevy image for shm again
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-27 02:02:47 +02:00
Nova
e5cfa249df fix(wayland): don't advertise dmabuf 2025-07-26 15:33:54 -07:00
Nova
3fbc659904 fix: wayland buffer handling improvements 2025-07-26 01:45:03 -07:00
Nova
38ea600846 cleanup 2025-07-26 00:44:56 -07:00
Nova
292e3988c5 fix(wayland): frame pacing compared to bevy 2025-07-26 00:38:20 -07:00
Nova
218b5f959a fix: unnecessary render device 2025-07-25 11:30:30 -07:00
Nova
9015c3e6c4 fix: my stupidity 2025-07-25 10:48:53 -07:00
Nova
ec3ced272a refactor(wayland): bit of cleanup 2025-07-25 10:47:16 -07:00
Nova
030fd6ed53 update: everything 2025-07-18 18:36:14 -07:00
Nova
c9d2f92142 refactor: remove unnecessary dependencies 2025-07-18 18:12:49 -07:00
Schmarni
9466e97dd1 refactor(wayland): continue implementing better buffer release
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-18 02:37:30 +02:00
Nova
eca5bb4bf2 feat(wayland): broken buffer usage code 2025-07-17 12:46:05 -07:00
Schmarni
4426d14bc5 fix(wayland): update bevy-dmabuf and tell it to use srgb formats
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-17 17:13:20 +02:00
Schmarni
66a3ae22cc fix(wayland): fix function instrumentation
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-17 17:01:40 +02:00
Schmarni
6cb46cf4f3 fix(offset): forgot to save the file before commiting
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-16 21:47:18 +02:00
Schmarni
119c7026b4 fix: offset tracking at startup to center properly
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-16 21:43:37 +02:00
Schmarni
a8b5ff47a6 feat(objects): implement hmd spatial again (i forgor)
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-16 03:05:45 +02:00
Schmarni
a8144dbd22 feat(wayland): implement shm ontop of dmabuf
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-16 01:13:38 +02:00
Nova
826b2413c9 feat: more cleanup 2025-07-15 14:22:37 -07:00
Nova
a29a04d3f5 refactor: remove unnecessary debugging and lists 2025-07-15 14:13:55 -07:00
Schmarni
0b4c7edc92 feat(wayland): set output refreshrate to i32::MAX and instrument a bunch of functions
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-15 22:15:15 +02:00
Nova
81a741ad36 fix(wayland/dmabuf): more robust format handling 2025-07-15 02:26:22 -07:00
Nova
d9dded54ca fix: zed debug 2025-07-15 02:25:52 -07:00
Nova
72d1173d2e fix(wayland): support all dmabuf protocol versions 2025-07-15 00:44:23 -07:00
Nova
12a3dc26af fix: feedback lacking formats 2025-07-14 15:58:25 -07:00
Nova
7fab72f903 fix(wayland): feedback padding 2025-07-14 11:23:35 -07:00
Schmarni
856d738267 feat(wayland): provide more rendering formats and modifiers
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-14 19:55:53 +02:00
Schmarni
f855ca9820 feat(wayland): add vulkano infra
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-14 19:55:51 +02:00
Nova
3571fa96aa fix(wayland): naming 2025-07-13 23:42:34 -07:00
Nova
e4186a90fc feat: zed debug json 2025-07-13 21:27:03 -07:00
Nova
d360a57f6e fix: wayland lockfile 2025-07-13 21:26:56 -07:00
Nova
58328cd63b cleanup: unneeded code 2025-07-12 21:35:43 -07:00
Nova
00fdaf5b9f feat(wayland): virtual output 2025-07-12 21:17:48 -07:00
Nova
3225819121 fix: remove test what was i thinking 2025-07-12 21:17:39 -07:00
Nova
b0d623e9de refactor(wayland): don't do fancy alpha setting when xrgb 2025-07-12 21:17:30 -07:00
Schmarni
6ec09809d9 feat(wayland): working dmabuf!
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-13 06:01:52 +02:00
Schmarni
0ec465ac39 refactor: try to use async dmabuf importing
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-13 03:36:42 +02:00
Schmarni
c59198b4a2 feat(wayland): implement infrastructure for async dmatex importing
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-12 06:17:12 +02:00
Schmarni
49224ad6b5 refactor: move WaylandPlugin init
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-10 21:01:05 +02:00
Nova
4b68544a0b fix: mouse pointer grab 2025-07-10 11:50:16 -07:00
Nova
42d36627ff fix: add plugin actually :p 2025-07-10 11:22:27 -07:00
Nova
63cf0db448 feat: dmatex!! (sorta, borken) 2025-07-10 10:13:38 -07:00
Nova
929ea054f3 fix: mouse pointer 2025-07-10 07:57:05 -07:00
Nova
c052ad22ba fix: clippy 2025-07-10 07:36:48 -07:00
Schmarni
e37d43eeb8 feat: setup bevy-dmabuf, not hooked up yet
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-10 16:34:53 +02:00
Nova
14544bfd3e fix(wayland): proper shm double buffering 2025-07-10 06:32:49 -07:00
Schmarni
c1d3a4cbcb fix: set spatial node transform
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-10 14:56:08 +02:00
Nova
dfb59ee7fa fix(wayland): shading 2025-07-08 16:46:19 -07:00
Nova
cb9460c344 feat: OIT 2025-07-08 12:30:11 -07:00
Nova
e9078bfaf8 clippy: fix 2025-07-08 12:12:46 -07:00
Nova
5a042bf11c refactor: remove sk material 2025-07-08 12:08:53 -07:00
Schmarni
a8d3b1fda1 Revert "refactor: use bevy_sk PbrMaterial instead of StandardMaterial"
This reverts commit 7b126557df.
2025-07-08 00:37:29 +02:00
Nova
a4a43d3ceb fix: keyboard crashiness 2025-07-06 12:52:05 -07:00
Nova
f4d08dac9c feat: wayland 2025-07-05 19:51:40 -07:00
Schmarni
7b126557df refactor: use bevy_sk PbrMaterial instead of StandardMaterial
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-02 22:00:27 +02:00
Nova
859d38f1b8 feat: stochastic alpha!!! 2025-07-02 12:37:26 -07:00
Nova
98ae69c858 refactor: remove animation plugin 2025-07-01 02:50:51 -07:00
Schmarni
e31d3e2197 refactor: use minimal bevy features
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-01 11:33:14 +02:00
Nova
017a7d4c7b feat: animation plugin 2025-07-01 02:20:56 -07:00
Nova
13210f858d fix: ci 2025-07-01 01:31:42 -07:00
Schmarni
692fc13863 fix: crash on flatscreen mode exit
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-07-01 10:30:59 +02:00
Nova
4bcca6af99 fix: ci 2025-06-30 14:36:47 -07:00
Schmarni
165dc1d259 feat: flatscreen only mode when passing --flatscreen or -f
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 13:13:48 +02:00
Nova
3a91ce8158 fix: rebasing bug in models 2025-06-30 02:30:08 -07:00
Nova
4b0969d9cf cleanup: cargo fmt 2025-06-30 02:05:07 -07:00
Nova
d0f88c13cd refactor: remove some unneeded stuff 2025-06-30 02:05:07 -07:00
Nova
07e9474c79 fix: clippy cleanup 2025-06-30 02:05:07 -07:00
Schmarni
0abc38c83a fix: don't use path dependency... again
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Schmarni
3b4a42c0cb feat: various minor improvements
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Schmarni
c22bf9b511 refactor: switch to bevys StandardMaterial for now
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Schmarni
806857b738 chore: cargo update
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Schmarni
3446ae5a4e fix: use git dependency
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Schmarni
5a5695f2cc refactor: remove stereokit dependency and fix all warnings
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Schmarni
cc8b9c0378 feat: reimpl flatscreen mode
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Schmarni
ac2f4a9e27 fix(lines): fix lines not clearing when having no points
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Schmarni
c93036278f feat: implement audio! thats all nodes!
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Schmarni
600eab9d2a feat: add entity handles and a bevy channel abstraction
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Schmarni
85bb21414d feat: finish line impl and switch to bevy bounds
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Schmarni
2bf244bb6e feat(lines): line impl mostly done
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Schmarni
3374473265 feat: mostly reimpl text rendering
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Schmarni
2e87fceae1 feat: disable controller methods if the interaction profile matches khr/simple_controller
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Schmarni
e678ca38ae feat: working controller input methods
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Schmarni
cbb54fd3d2 feat: working handtracking input methodsRUST_LOG=info,naga=warn dbus-run-session cargo run -- -e ~/build/stardust/env.sh -o 300!
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Schmarni
aeec63c070 refactor: let the bevy rewrite begin: mostly working model nodes
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-06-30 03:04:00 -06:00
Nova
fd1c6ed0cf fix: make spatial parenting more stable 2025-06-13 18:43:08 -07:00
Nova
13c6dbfd4d fix(input): send proper handler IDs to method client 2025-06-05 22:10:41 -07:00
Nova
4fb7c3df84 fix(input): send input method request/release to client 2025-06-05 20:49:15 -07:00
Nova
9d0e1ce021 fix: root sending frame events to dead clients 2025-05-15 21:21:14 -07:00
Nova
dd38b590c1 ci: remove artifacts 2025-05-15 18:24:03 -07:00
Nova
24b7195297 fix: ci (hopefully) 2025-05-15 18:15:58 -07:00
Nova
7d8993b640 refactor: set default log level to warn 2025-05-15 01:13:47 -07:00
Nova
4c70ded2b0 fix: io safety error 2025-05-15 00:59:59 -07:00
Nova
7f7a8b5264 fix: cargo.lock 2025-05-14 20:00:33 -07:00
Cyberneticmelon
43246900db Updated dependencies 2025-05-14 19:58:24 -07:00
Nova
b7a123f9c9 rewrite(README): tell to use release 2025-05-14 19:58:24 -07:00
Nova
900316968a fix: some zone weirdness ig 2025-05-14 19:58:24 -07:00
Schmarni
db30f8e61b fix(wayland): fix keyboard holding onto surfaces without causing visual or functional issues
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-05-14 19:58:00 -07:00
Schmarni
0a005b9864 Revert "fix: panel items being grabbed by keyboard seat"
This reverts commit a58ab46f4a.
2025-05-14 19:58:00 -07:00
Nova
f4ed8bc37d refactor(zone): use zoneable zone distance 2025-05-14 19:58:00 -07:00
Nova
49ee4d3b67 fix(zones): don't add ancestors of zone to be captured 2025-05-14 19:58:00 -07:00
Nova
c2f1f737a0 fix: panel items being grabbed by keyboard seat 2025-05-14 19:58:00 -07:00
Nova
c9a57773d1 fix: flatscreen keyboard 2025-05-14 19:58:00 -07:00
Nova
68a7c06b9e fix: cursor hotspot positionind 2025-05-14 19:58:00 -07:00
Nova
b196cbfa3a chore: clippy 2025-05-14 19:58:00 -07:00
Nova
7067d048d6 fix(input): cull capture *attempts*, not captures 2025-05-14 19:58:00 -07:00
Nova
ef09b69378 fix(wayland): _ prefix to viewporter state 2025-05-14 19:58:00 -07:00
Nova
c5dea3b7c9 fix(input): don't limit to closest handler 2025-05-14 19:58:00 -07:00
Cyberneticmelon
5ea147f9fe Added cli update 2025-05-14 19:58:00 -07:00
Cyberneticmelon
3d6fceb0dd Fixing syntax 2025-05-14 19:58:00 -07:00
Cyberneticmelon
b1900de652 Updated dependency documentation 2025-05-14 19:58:00 -07:00
Cyberneticmelon
76ff476112 Updated documentation 2025-05-14 19:58:00 -07:00
Schmarni
57f9516a81 feat(wayland): implement wp_viewporter
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-05-14 19:58:00 -07:00
Nova
fe6ed81255 fix(input): dropped input handlers properly release methods 2025-05-14 19:58:00 -07:00
Nova
173b033963 fix(input): unresponsive clients get uncaptured 2025-05-14 19:58:00 -07:00
Nova
fe9ae8225c feat(input): retained mode capture system 2025-05-14 19:58:00 -07:00
Nova
a149098044 feat: unset sky 2025-05-14 19:58:00 -07:00
Nova
2a5bddbb5a feat: support right click drag 2025-05-14 19:58:00 -07:00
Schmarni
a7d5992b6b refactor: remove unused macro
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-05-14 19:58:00 -07:00
Schmarni
94b9b9ddcf chore: remove unneeded cargo patch
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-05-14 19:58:00 -07:00
Schmarni
cfb193251f refactor: change errors to warnings and fix unions/enums in protocol
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-05-14 19:58:00 -07:00
Schmarni
14e899db0e feat: add better logging to codegen
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-05-14 19:58:00 -07:00
Schmarni
42fc3c3f44 feat: wip debugging improvements and protocol change
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-05-14 19:58:00 -07:00
Nova
9bfbade9a2 refactor: make cylinders go on the XZ plane by default 2025-05-14 19:58:00 -07:00
Nova
3f4002881c fix(codegen): make serde use tagged type for enums 2025-05-14 19:58:00 -07:00
Schmarni
8a8121f1a8 feat: make stage tracking space always available
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-05-14 19:58:00 -07:00
Schmarni
8fc017a6fc refactor: switch to dashmap for Aspects and Registries
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-05-14 19:58:00 -07:00
Schmarni
7016904adb feat: add --nvidia flag
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-05-14 19:58:00 -07:00
Nova
93692f365e feat(wayland): logging 2025-05-14 19:58:00 -07:00
Nova
b765b68d41 fix: force stereokit revision 2025-05-14 19:58:00 -07:00
Schmarni
5d82e42820 refactor: remove unneeded AtomicBool
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-05-14 19:58:00 -07:00
Schmarni
15fe997237 feat: add Tracked Interface to dbus to allow clients to query the tracking state of controllers/hands
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-05-14 19:58:00 -07:00
Nova
44a3480022 refactor: minimize dependencies 2025-05-14 19:58:00 -07:00
Nova
f0c50ba237 refactor: remove portable_atomic 2025-05-14 19:58:00 -07:00
Nova
30a05a3218 refactor: remove once_cell dependency 2025-05-14 19:58:00 -07:00
Nova
779706d792 refactor: upgrade to rust 2024 2025-05-14 19:58:00 -07:00
Schmarni
d65163553e fix: remove stereokits controller models
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-05-14 19:58:00 -07:00
Schmarni
33ccc66411 fix(session): remove unneeded wayland environment variables
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-05-14 19:58:00 -07:00
Nova
fb1627dccc fix(session): set $XDG_SESSION_TYPE properly
Author:    Nova <technobaboo@gmail.com>
Date:      Fri Jan 31 12:44:36 2025 -0800
2025-05-14 19:58:00 -07:00
Schmarni
9f49ba729d fix: material batching/wrong texture issue
* fix: incorrect material batching

Signed-off-by: Schmarni <marnistromer@gmail.com>

* fix: prevent memory leak with batched materials

Signed-off-by: Schmarni <marnistromer@gmail.com>

---------

Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-05-14 19:58:00 -07:00
Nova
a44f36641e fix: wayland inconsistencies 2025-05-14 19:58:00 -07:00
6543
34fd7e6e49 format flake with nixpkgs#nixfmt-rfc-style 2025-05-14 19:58:00 -07:00
ash lea
a5f087d29f start to convert ad-hoc errors to explicit types 2025-04-01 14:24:06 -07:00
Nova
3b996c46e2 fix(ci): libfuse2 for appimages 2025-04-01 14:24:06 -07:00
Nova
2e50491144 fix(ci): appimage path 2025-04-01 14:24:06 -07:00
Nova
c3e4b2ed2a fix(ci): update to use v4 of stuff 2025-04-01 14:24:06 -07:00
Nova
c0141da88b refactor: optimize get aspect hotpath 2025-04-01 14:24:06 -07:00
Nova
8f18d83694 refactor: delete data protocol 2025-04-01 14:24:06 -07:00
Nova
6822e4bdb7 feat: remove reference to unnecessary pulse receivers 2025-04-01 14:24:06 -07:00
Nova
3c66109c45 refactor: use new xkb input code 2025-04-01 14:24:06 -07:00
Nova
d27ec84496 fix: use old stereokit hand 2025-04-01 14:24:06 -07:00
Nova
239e0c0318 feat: openxr transparency when available 2025-04-01 14:24:06 -07:00
Nova
ab913a8d84 fix(lockfile): update 2025-04-01 14:24:06 -07:00
Nova
a5653853f8 feat: switch back to dev branch of core 2025-04-01 14:24:06 -07:00
Nova
58a17fedba refactor: justfile 2025-04-01 14:24:06 -07:00
Nova
be709efbdd fix: scenegraph errors 2025-04-01 14:24:06 -07:00
Nova
4f01bd5eec feat: update zbus 2025-04-01 14:24:06 -07:00
Nova
242eed37fe clippy: cleanup 2025-04-01 14:24:06 -07:00
Nova
fe22d3954a feat: justfile 2025-04-01 14:24:06 -07:00
Nova
96b4e22e10 clippy 2025-04-01 14:24:06 -07:00
Nova
a51db703fd fix(audio): stop sound when dropped 2025-04-01 14:24:06 -07:00
Nova
7e755a44b8 fix(objects/sk_hand): draw pinky 2025-04-01 14:24:06 -07:00
Nova
80c9386f79 fix(items): add the proper aspects 2025-04-01 14:24:06 -07:00
Nova
4730f0732b refactor: alias_id 2025-04-01 14:24:06 -07:00
Nova
79935befb7 refactor: upgrade packages 2025-04-01 14:24:06 -07:00
Nova
b2e452326b fix(cargo.toml): build settings that don't deadlock 2025-04-01 14:24:06 -07:00
Cyberneticmelon
3e31905b5b feat: better server docs
Replaced embedded youtube with gifs

Replaced embedded videos with GIFs

Workflow img

Workflow img

Updated youtube links with thumbnails

Updated text
2025-04-01 14:23:33 -07:00
Nova
c830becbff fix(packaging): use workspace deps 2024-11-01 15:10:00 -04:00
Nova
e8e485b472 fix(packaging): even more shenanigans 2024-11-01 12:31:44 -04:00
Nova
bf68b87813 fix(packaging): don't do dev in core 2024-11-01 12:10:45 -04:00
Nova
9546a36200 fix(packaging): update stereokit 2024-09-15 12:27:10 -04:00
Nova
c12300e756 refactor(cargo.toml): upgrade and refine 2024-09-05 08:52:10 -04:00
Nova
4683710f09 feat(main): debug launched clients flag 2024-09-04 05:14:43 -04:00
Nova
1ca054c2b3 feat: proper version bump 2024-08-24 20:59:38 -04:00
Nova
5f7e9d4eb6 Bump version to 0.45.0 2024-08-24 20:47:40 -04:00
6543
c9fe1be10b cargo fmt (#23) 2024-08-22 01:24:12 +00:00
6543
f58c748f80 Return dedicated error message if dbus session could not be opened (#22)
* Return dedicated error message if dbus session could not be opened

* cargo fmt
2024-08-22 01:23:27 +00:00
Nova
499aa2be28 fix(nix): FINALLY 2024-08-21 00:49:11 -04:00
Taylor Coffelt
7ba710e8b7 fix(nix): fixes nix build (#21) 2024-08-21 04:10:13 +00:00
Nova
62802367eb fix: clippy 2024-08-20 17:24:57 -04:00
Nova
853f779930 fix(model): don't keep material references 2024-08-20 17:18:48 -04:00
Nova
21d10a15ee clippy: cleanup 2024-08-20 16:59:37 -04:00
Nova
6146a5b63a fix(model): rebatch identical materials to save on draw calls 2024-08-20 16:59:15 -04:00
Nova
01485e2020 fix: stereokit rust to make it compile at all 2024-08-19 10:18:44 -04:00
Nova
6e77c3667d fix(nix): force local deps only 2024-08-15 16:13:01 -04:00
Nova
003dd9fcc1 fix: nix meshoptimizer 2024-08-13 21:51:32 -04:00
Nova
42738b739f fix(objects/input): push all buttons through 2024-08-12 19:27:51 -04:00
Nova
fe83a69bb9 feat: upgrade dependencies 2024-08-10 22:07:07 -04:00
Nova
e0a7d4f44a fix: upgrade stereokit-rust and attempt nix upgrade 2024-08-07 14:36:19 -04:00
Nova
14ebe85493 refactor(wayland): update data in user data map for everything 2024-08-06 20:42:46 -04:00
Nova
08135b03a2 feat(wayland): receives_input 2024-08-06 19:12:44 -04:00
Nova
4827561f88 refactor(objects/input): cleanup 2024-08-06 19:12:29 -04:00
Nova
72f7fd8e9d refactor: modularize input object capture system 2024-08-06 17:41:39 -04:00
Nova
827c630a70 refactor(input): rename internal capture requests 2024-08-06 12:08:19 -04:00
Nova
98a6ad6f32 refactor: cleanup 2024-07-28 06:14:02 -04:00
Nova
c09afba366 feat: subsurface support 2024-07-27 15:58:55 -04:00
Nova
48f15e848d fix(wayland): lock on commit 2024-07-26 17:42:58 -04:00
Nova
78912c8f1b fix: vram issues 2024-07-26 06:32:36 -04:00
Nova
13815d4d20 refactor(tracy): mark/demark relevant functions 2024-07-26 05:53:13 -04:00
Nova
36d91fd999 fix(popups): unconstrain them 2024-07-24 17:25:26 -04:00
Nova
b7191c3183 fix(wayland): popups don't crash anymore 2024-07-24 17:02:02 -04:00
Nova
796ee1a34e feat(main): disable hands/controllers args 2024-07-24 10:21:54 -04:00
Nova
de10a7101e feat: update stereokit 2024-07-23 04:33:00 -04:00
Nova
41d22da691 fix: force origin local mode 2024-07-23 04:32:37 -04:00
Nova
73ed1210ed fix(wayland): set virtual output to 1024x1024 2024-07-20 09:19:14 -04:00
Nova
a8e16a8411 fix(objects/input): filter out handlers with disabled fields 2024-07-19 00:34:50 -04:00
Nova
4d8d38e4e5 fix(zone): if parents none 2024-07-18 23:46:51 -04:00
Nova
01f4a8fbcd feat: stereokit hax 2024-07-18 13:44:33 -04:00
Nova
f639a1662a feat(objects): add palm 2024-07-18 12:09:38 -04:00
Nova
6350444559 fix(input): only allow handlers with enabled field nodes 2024-07-18 11:24:06 -04:00
Nova
ecc7d7b912 feat(node): not enabled if spatial and size is 0 2024-07-18 10:49:50 -04:00
Nova
ae40158dec feat(objects): controllers 2024-07-18 09:19:06 -04:00
Nova
1b28290cbb feat(objects): rename inputs 2024-07-18 08:07:35 -04:00
Nova
a3bcff035a refactor(objects): move into objects module 2024-07-18 07:56:36 -04:00
Nova
71ca32a560 cleanup(wayland/compositor) 2024-07-18 03:09:46 -04:00
Nova
d28b477c8a feat(main): use d-bus object manager
Signed-off-by: Nova <technobaboo@gmail.com>
2024-07-10 07:03:14 -04:00
Nova
643697ea33 refactor(cargo.toml): reorder deps 2024-07-08 06:04:13 -04:00
Nova
f5259f2977 refactor(wayland): use latest smithay goods 2024-07-07 20:05:57 -04:00
Nova
7bedd2c27f fix(wayland): send pending configure on resize just in case 2024-07-05 04:09:01 -04:00
Nova
707b81871c fix(main): new tracy api 2024-07-05 04:08:49 -04:00
Nova
1fddcd9033 fix(main): overlays 2024-07-05 04:08:35 -04:00
Nova
7ff470e1db fix(wayland): touches not being properly released 2024-07-04 20:37:52 -04:00
Nova
a7df6d8a08 fix(wayland): only emit toplevel_sized_changed when it changes 2024-07-04 20:31:54 -04:00
Nova
a9ff97e39c feat(main): better errors 2024-07-03 22:04:43 -04:00
Nova
126e9f4ca6 fix(zone): set zoneable false reparents to nothing 2024-07-01 22:01:42 -04:00
Nova
e379dae4ad fix(root): elapsed time 2024-07-01 21:55:26 -04:00
Nova
b3fa529f77 fix(objects): properly destroy nodes 2024-06-30 22:45:20 -04:00
Nova
1e8d3a3d4c fix(input): send events even when not in handler queue 2024-06-30 22:22:41 -04:00
Nova
2988ae3c28 refactor(main): pass args into stereokit loop 2024-06-28 21:00:07 -04:00
Nova
9c04b5710e feat(objects): simulated input switching 2024-06-28 20:55:02 -04:00
Nova
1324c26c74 fix(fields): actually add the methods onto the nodes 2024-06-28 20:07:28 -04:00
Nova
0b9f79158c feat(objects/controller): color signifier 2024-06-27 17:06:27 -04:00
Nova
e5d0906d73 feat(objects/hand): make fingertips pointy 2024-06-27 16:54:08 -04:00
Nova
f08f3e4e4b fix: polish stuff a bit 2024-06-27 16:51:33 -04:00
Nova
d17841ec7a fix(model): no panics 2024-06-26 15:21:48 -04:00
Nova
ebaf113d94 feat: use d-bus for server objects 2024-06-25 16:17:44 -04:00
Nova
5be25da46f feat: use dbus for hmd 2024-06-24 19:26:01 -04:00
Nova
f5e8156400 fix(anchors.txt): get that outta here :/ 2024-06-24 19:25:43 -04:00
Nova
02bf210c51 feat(fields): use new field ref and field types 2024-06-21 17:22:52 -04:00
Nova
eba317ace9 fix(release): make it not glitch 2024-06-19 13:29:39 -04:00
Nova
84e42499e7 feat: todos 2024-06-17 12:11:04 -04:00
Nova
9b6b450d18 fix(objects/input) mouse pointer gives correct keymap now 2024-06-16 21:42:25 -04:00
Nova
53979ce167 fix(item): item destroy not sending correctly 2024-06-15 13:58:36 -04:00
Nova
e2c9e06cd3 refactor: change println to tracing 2024-06-15 12:40:09 -04:00
Nova
65a9957be1 fix(wayland): context in xr 2024-06-14 22:33:08 -04:00
Nova
fbe941749a refactor(everything): streamline stereokit and main loops 2024-06-13 18:55:46 -04:00
Nova
36fd3216c7 fix(wayland): strip out all the xwayland stuff finally 2024-06-13 13:34:42 -04:00
Nova
f73c8f968d fix(wayland): properly kill off xwayland 2024-06-13 10:18:42 -04:00
Nova
83e3a913c5 fix(wayland): remove unnecessary backend impl 2024-06-11 14:33:59 -04:00
Nova
de56aa53d6 feat: update 2024-06-11 07:13:58 -04:00
Nova
9f0043f406 refactor: remove xwayland (we can always just use the old code) 2024-06-11 07:08:41 -04:00
Nova
99eb0ea547 fix: clippy 2024-06-09 19:58:41 -04:00
Nova
04535895e8 fix: stereokit not exiting on ctrl+c 2024-06-09 19:40:32 -04:00
Nova
69311125ba fix(objects/input): multimodal hand input 2024-06-07 15:13:13 -04:00
Nova
45b832455b fix: stupid dbg statements 2024-06-07 09:01:22 -04:00
Nova
b590e82b05 fix: input capturing 2024-06-07 08:22:51 -04:00
Nova
f770357010 fix: state 2024-06-06 09:24:25 -04:00
Nova
3cbc10d91e fix(model): no panic on material change 2024-06-06 09:02:32 -04:00
Nova
9425d30cb3 fix(input): input not working 2024-06-06 06:41:27 -04:00
Nova
8d2aac12d6 feat: upgrade to numerical IDs 2024-06-05 14:34:45 -04:00
Nova
5f9d9d4714 refactor(items): codegen prototocol 2024-05-30 00:34:29 -04:00
Nova
eda50b7d51 feat: upgrade stereokit 2024-05-28 09:13:15 -04:00
Nova
01c5ad3b04 feat: update dependencies 2024-05-28 03:17:02 -04:00
Nova
d7fba79be9 fix: new apis 2024-05-28 01:24:49 -04:00
Nova
89fcc55cca feat(codegen): inherits 2024-05-27 02:25:05 -04:00
Nova
a831fcb99f feat(nodes/input): update protocol 2024-05-27 02:24:11 -04:00
Nova
8e37d27130 fix(objects/input): grave key 2024-05-27 02:23:43 -04:00
Nova
149131f322 fix(objects/sk_hand): addref to material 2024-04-24 20:02:11 -04:00
Nova
4a90b936b1 fix(wayland): stop unwrap of repositioned popup 2024-04-24 20:00:02 -04:00
Nova
ab6998407b feat(flake.nix): default app 2024-04-24 16:28:47 -04:00
Nova
be2f5b8e37 refactor(input): switch to manual handler order 2024-04-16 08:00:07 -04:00
Nova
226554fadc refactor(input): use idl 2024-04-14 09:46:29 -04:00
Nova
47cbc2b8fc feat(nodes/root): log base prefixes change 2024-04-13 05:07:46 -04:00
Nova
90f60c9178 refactor(wayland/xwayland_rootless): use tokio 2024-04-04 23:55:49 -04:00
Nova
49253567cb fix(wayland/decoration): make decoration SSD 2024-03-28 19:30:35 -04:00
Nova
350007cbaf fix(wayland): remove nonexistent capabilities 2024-03-28 19:29:50 -04:00
Nova
7318d5317c feat: lapce run toml 2024-03-09 23:51:07 -05:00
Nova
15f771ff93 fix(drawable/text): alignment 2024-03-09 23:17:56 -05:00
Nova
302a64785d fix(wayland): warnings 2024-03-09 23:16:32 -05:00
Nova
34aab266a3 refactor(wayland): use smithay xdg_shell handler 2024-03-09 02:23:40 -05:00
Nova
fba4e10611 refactor(wayland): use smithay seats 2024-03-08 05:57:52 -05:00
138 changed files with 18849 additions and 10189 deletions

View File

@@ -1,2 +1,2 @@
[build]
rustflags = ["--cfg", "tokio_unstable"]
rustflags = ["--cfg", "tokio_unstable"]

View File

@@ -1,46 +1,26 @@
name: Build
name: CI
on:
push:
branches:
- '*'
- "*"
jobs:
build_and_package:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install runtime dependencies
run: sudo apt install -y --no-install-recommends libxkbcommon-dev libstdc++6 libopenxr-dev libx11-dev libxfixes-dev libgl1-mesa-dev libegl1-mesa-dev libgbm-dev libfontconfig-dev libjsoncpp-dev libxcb1-dev libglx-dev libxcb-glx0-dev libdrm-dev libwayland-dev libfreetype-dev libpng-dev
- name: Install build dependencies
run: sudo apt install -y --no-install-recommends cmake ninja-build
run: sudo apt install -y --no-install-recommends libopenxr-dev libwayland-dev libasound2-dev
- name: Set up Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
toolchain: 1.88.0
override: true
- name: Build server
run: cargo build --release
- name: Install appimagetool
run: |
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$(uname -m).AppImage -O /usr/local/bin/appimagetool; \
chmod +x /usr/local/bin/appimagetool; \
sed -i 's|AI\x02|\x00\x00\x00|' /usr/local/bin/appimagetool
- name: Install cargo-appimage
run: cargo install cargo-appimage
- name: Generate AppImage
run: cargo appimage
- name: Upload AppImage
uses: actions/upload-artifact@v2
with:
name: appimage
path: '*.AppImage'

3
.gitignore vendored
View File

@@ -12,4 +12,5 @@
/libs/
*.AppImage
*.blend1
*.blend1
anchors.txt

29
.lapce/run.toml Normal file
View File

@@ -0,0 +1,29 @@
# The run config is used for both run mode and debug mode
[[configs]]
# the name of this task
name = "task"
# the type of the debugger. If not set, it can't be debugged but can still be run
# type = "lldb"
# the program to run
program = "./target/debug/stardust-xr-server"
# the program arguments, e.g. args = ["arg1", "arg2"], optional
# args = []
# current working directory, optional
cwd = "${workspace}"
# enviroment variables, optional
# [configs.env]
# VAR1 = "VAL1"
# VAR2 = "VAL2"
# task to run before the run/debug session is started, optional
# [configs.prelaunch]
# program = "cargo"
# args = [
# "build",
# ]

94
.zed/debug.json Normal file
View File

@@ -0,0 +1,94 @@
[
{
"label": "Debug",
"adapter": "CodeLLDB",
"program": "target/debug/stardust-xr-server",
"args": [],
"env": {
"RUST_LOG": "debug"
},
"request": "launch",
"build": {
"command": "cargo",
"args": [
"build",
"--bin=stardust-xr-server",
"--package=stardust-xr-server"
]
}
},
{
"label": "Debug (Overlay)",
"adapter": "CodeLLDB",
"program": "target/debug/stardust-xr-server",
"args": ["-o", "1"],
"env": {
"RUST_LOG": "debug"
},
"request": "launch",
"build": {
"command": "cargo",
"args": [
"build",
"--bin=stardust-xr-server",
"--package=stardust-xr-server"
]
}
},
{
"label": "Debug (Headless)",
"adapter": "CodeLLDB",
"program": "target/debug/stardust-xr-server",
"args": [],
"env": {
"RUST_LOG": "debug",
"DISPLAY": "",
"WAYLAND_DISPLAY": ""
},
"request": "launch",
"build": {
"command": "cargo",
"args": [
"build",
"--bin=stardust-xr-server",
"--package=stardust-xr-server"
]
}
},
{
"label": "Debug (Flatscreen)",
"adapter": "CodeLLDB",
"program": "target/debug/stardust-xr-server",
"args": ["-f"],
"env": {
"RUST_LOG": "debug"
},
"request": "launch",
"build": {
"command": "cargo",
"args": [
"build",
"--bin=stardust-xr-server",
"--package=stardust-xr-server"
]
}
},
{
"label": "Debug (Flatscreen, Restore Latest)",
"adapter": "CodeLLDB",
"program": "target/debug/stardust-xr-server",
"args": ["-f", "--restore", "latest"],
"env": {
"RUST_LOG": "debug"
},
"request": "launch",
"build": {
"command": "cargo",
"args": [
"build",
"--bin=stardust-xr-server",
"--package=stardust-xr-server"
]
}
}
]

6471
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
[package]
edition = "2021"
rust-version = "1.75"
edition = "2024"
rust-version = "1.89"
name = "stardust-xr-server"
version = "0.44.1"
version = "0.45.0"
authors = ["Nova King <technobaboo@proton.me>"]
description = "Stardust XR reference display server"
license = "GPLv2"
@@ -12,103 +12,166 @@ homepage = "https://stardustxr.org"
[workspace]
members = ["codegen"]
[workspace.dependencies.stardust-xr]
git = "https://github.com/StardustXR/core.git"
branch = "dev"
[[bin]]
name = "stardust-xr-server"
path = "src/main.rs"
[features]
default = ["wayland"]
wayland = ["dep:smithay", "dep:xkbcommon"]
xwayland_rootful = []
xwayland_rootless = ["smithay/xwayland"]
wayland = [
"dep:waynest",
"dep:waynest-protocols",
"dep:waynest-server",
"dep:tokio-stream",
"dep:memmap2",
"dep:drm-fourcc",
"dep:memfd",
"dep:vulkano",
"dep:wgpu-hal",
"dep:ash",
]
profile_tokio = ["dep:console-subscriber", "tokio/tracing"]
profile_app = ["dep:tracing-tracy"]
profile_app = [
"dep:tracing-tracy",
"bevy/trace_tracy",
"bevy/trace",
"dep:tracy-client",
]
bevy_debugging = ["bevy/bevy_remote", "bevy/track_location"]
[package.metadata.appimage]
auto_link = true
auto_link_exclude_list = [
"libc*",
"libdl*",
"libpthread*",
"ld-linux*",
"libGL*",
"libEGL*",
]
auto_link_exclude_list = ["libc*", "libdl*", "libpthread*", "ld-linux*"]
[profile.release]
opt-level = "s"
lto = true
codegen-units = 1
panic = "abort"
# strip = "symbols"
debug = true
[profile.dev.package."*"]
opt-level = 3
[profile.release]
strip = true
lto = true
[patch.crates-io]
bevy_mod_openxr = { git = "https://github.com/awtterpip/bevy_oxr", rev = "9fd0c797" }
bevy_mod_xr = { git = "https://github.com/awtterpip/bevy_oxr", rev = "9fd0c797" }
bevy_gltf = { git = "https://github.com/Schmarni-Dev/bevy", branch = "gltf_backport" }
# TODO: figure out how to not need this horrifing patch
wgpu = { git = "https://github.com/Schmarni-Dev/wgpu", branch = "bad_dmabuf_workaround" }
wgpu-types = { git = "https://github.com/Schmarni-Dev/wgpu", branch = "bad_dmabuf_workaround" }
wgpu-core = { git = "https://github.com/Schmarni-Dev/wgpu", branch = "bad_dmabuf_workaround" }
naga = { git = "https://github.com/Schmarni-Dev/wgpu", branch = "bad_dmabuf_workaround" }
wgpu-hal = { git = "https://github.com/Schmarni-Dev/wgpu", branch = "bad_dmabuf_workaround" }
bevy_pbr = { git = "https://github.com/Schmarni-Dev/bevy", branch = "premul_oit_016" }
bevy_core_pipeline = { git = "https://github.com/Schmarni-Dev/bevy", branch = "premul_oit_016" }
bevy_render = { git = "https://github.com/Schmarni-Dev/bevy", branch = "render_thread_init_callback" }
[dependencies]
color-eyre = { version = "0.6.2", default-features = false }
clap = { version = "4.2.4", features = ["derive"] }
glam = { version = "0.23.0", features = ["mint"] }
lazy_static = "1.4.0"
mint = "0.5.9"
# small utility thingys
nanoid = "0.4.0"
once_cell = "1.17.1"
parking_lot = "0.12.1"
portable-atomic = { version = "1.2.0", features = ["float", "std"] }
rustc-hash = "1.1.0"
tokio = { version = "1.27.0", features = ["rt-multi-thread", "signal", "time"] }
send_wrapper = "0.6.0"
prisma = "0.1.1"
directories = "5.0.0"
serde = { version = "1.0.160", features = ["derive"] }
serde_repr = "0.1.16"
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
global_counter = "0.2.2"
rand = "0.8.5"
atty = "0.2.14"
xkbcommon = { version = "0.7.0", default-features = false, optional = true }
ctrlc = "3.4.1"
libc = "0.2.148"
input-event-codes = "5.16.8"
nix = "0.27.1"
wayland-scanner = "0.31.1"
wayland-backend = "0.3.3"
cluFlock = "1.2.7"
fxtypemap = "0.2.0"
toml = "0.8.10"
lazy_static = "1.5.0"
rand = "0.9.2"
rustc-hash = "2.0.0"
slotmap = "1.0.7"
global_counter = "=0.2.2"
parking_lot = "0.12.3"
dashmap = "6.1.0"
[dependencies.smithay]
# git = "https://github.com/technobaboo/smithay.git" # Until we get stereokit to understand OES samplers and external textures
git = "https://github.com/smithay/smithay.git" # Until we get stereokit to understand OES samplers and external textures
# path = "../smithay"
default-features = false
features = [
"desktop",
"backend_drm",
"backend_egl",
"renderer_gl",
"wayland_frontend",
]
version = "*"
optional = true
# rust errors/logging
color-eyre = { version = "0.6.3", default-features = false }
clap = { version = "4.5.13", features = ["derive"] }
console-subscriber = { version = "0.4.0", optional = true }
thiserror = "2.0.11"
tracing = { version = "0.1.40", features = ["release_max_level_warn"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing-tracy = { version = "0.11.1", optional = true }
tracy-client = { version = "=0.18.0", optional = true }
[dependencies.stereokit]
default-features = false
features = ["linux-egl"]
version = "0.16.9"
# (de)serialization
serde = { version = "1.0.205", features = ["derive"] }
serde_repr = "0.1.19"
toml = "0.9.7"
[dependencies.console-subscriber]
version = "0.1.8"
optional = true
# mathy stuffs
glam = { version = "0.29.0", features = ["mint", "serde"] }
mint = "0.5.9"
tokio = { version = "1.39.2", features = ["rt-multi-thread", "signal", "time"] }
[dependencies.tracing-tracy]
version = "0.10.4"
optional = true
# bevy
bevy = { version = "0.16", default-features = false, features = [
"bevy_asset",
"bevy_audio",
"bevy_color",
"bevy_core_pipeline",
"bevy_gizmos",
"bevy_gltf",
"bevy_log",
"bevy_pbr",
"bevy_render",
"bevy_window",
"bevy_winit",
"std",
"wayland",
"mp3",
"wav",
"qoi",
"png",
"hdr",
"jpeg",
"multi_threaded",
] }
bevy_mod_xr = "0.3"
bevy_mod_openxr = "0.3"
# bevy_sk.git = "https://github.com/MalekiRe/bevy_sk"
bevy_sk = { git = "https://github.com/technobaboo/bevy_sk", branch = "stochastic" }
# bevy-dmabuf = "0.2.0"
bevy-dmabuf.git = "https://github.com/Schmarni-Dev/bevy-dmabuf"
bevy-equirect = "0.1.1"
# bevy-mesh-text-3d.git = "https://github.com/terhechte/bevy-mesh-text-3d"
# use my fork until my pr to use minimal bevy features was merged
bevy-mesh-text-3d.git = "https://github.com/Schmarni-Dev/bevy-mesh-text-3d"
openxr = "0.19"
# linux stuffs
input-event-codes = "6.2.0"
zbus = { version = "5.11.0", features = [
"blocking-api",
"tokio",
], default-features = false }
directories = "6.0.0"
xkbcommon-rs = "0.1.0"
cosmic-text = "0.14.2"
# Wayland
waynest = { git = "https://github.com/verdiwm/waynest.git", default-features = false, optional = true }
waynest-protocols = { git = "https://github.com/verdiwm/waynest.git", features = [
"server",
"stable",
"mesa",
"tracing",
], default-features = false, optional = true }
waynest-server = { git = "https://github.com/verdiwm/waynest.git", default-features = false, optional = true }
tokio-stream = { version = "0.1.17", optional = true }
memmap2 = { version = "0.9.5", optional = true }
drm-fourcc = { version = "2.2.0", optional = true }
memfd = { version = "0.6.4", optional = true }
vulkano = { git = "https://github.com/Schmarni-Dev/vulkano", branch = "0_35_dmabuf_fixes", default-features = false, optional = true }
wgpu-hal = { version = "24", optional = true, features = ["vulkan"] }
ash = { version = "0.38.0", optional = true, default-features = false }
rustix = { version = "1.0.8", features = ["time"] }
pin-project-lite = "0.2.16"
futures-sink = "0.3.31"
[dependencies.stardust-xr]
git = "https://github.com/StardustXR/core.git"
workspace = true
[dependencies.stardust-xr-server-codegen]
path = "codegen"
# [patch.crates-io.stereokit]
# path = "../stereokit-rs"
# [patch.crates-io.stereokit-sys]
# path = "../stereokit-sys"

154
README.md
View File

@@ -1,85 +1,113 @@
# Stardust XR Reference Server
# Stardust XR Server
This project is a usable Linux display server that reinvents human-computer interaction for all kinds of XR, from putting 2D/XR apps into various 3D shells for varying uses to SDF-based interaction.
Stardust XR is a display server for VR and AR headsets on Linux-based systems. [Stardust provides a 3D environment](https://www.youtube.com/watch?v=v2WblwbaLaA), where anything from 2D windows (including your existing apps!), to 3D apps built from objects, can exist together in physical space.
## Prerequisites
1. Cargo
2. CMake
3. EGL+GLES 3.2
4. GLX+Xlib
5. fontconfig
6. dlopen
7. OpenXR Loader (required even if run in flatscreen mode)
![workflow](/img/workflow.png)
Command line installation of core & dynamic dependencies are provided below:
<details>
<summary>Ubuntu/Debian</summary>
<pre><code class="language-bash">
sudo apt update && sudo apt install \
build-essential \
cargo \
cmake \
libxkbcommon-dev libudev1 libinput10 libcap2 libmtdev1 libevdev2 libwacom9 libgudev-1.0-0 \
libglib2.0-dev libffi8 libpcre2-dev libxkbcommon-x11-dev libxcb-dev libxcb-xkb-dev libxau-dev \
libstdc++-dev libx11-dev libxfixes-dev libegl-dev libgbm-dev libfontconfig1-dev libgl-dev \
libdrm-dev libexpat1-dev libfreetype6-dev libxml2-dev zlib1g-dev libbz2-dev libpng-dev \
libharfbuzz-dev libbrotli-dev liblzma-dev libraphite2-dev
</code></pre>
</details>
<details>
<summary>Fedora</summary>
<pre><code class="language-bash">
sudo apt update && sudo apt install \
cargo \
cmake \
libxkbcommon-devel systemd-devel libinput-devel libcap-devel mtdev-devel libevdev-devel glib2-devel \
libffi-devel pcre2-devel libxkbcommon-x11-devel libxcb-devel libXau-devel libstdc++-devel libx11-devel libxfixes-devel \
mesa-libEGL-devel mesa-libgbm-devel fontconfig-devel libdrm-devel expat-devel freetype-devel libxml2-devel zlib-devel \
bzip2-devel libpng-devel harfbuzz-devel brotli-devel xz-devel graphite2-devel
</code></pre>
</details>
<details>
<summary>Arch Linux</summary>
<pre><code class="language-bash">
sudo pacman -Syu --needed \
cargo \
cmake \
libxkbcommon systemd libinput libcap mtdev libevdev libwacom glib2 libffi pcre2 libxkbcommon-x11 \
libxcb libxau libx11 libxfixes mesa fontconfig libdrm expat freetype2 libxml2 zlib bzip2 \
libpng harfbuzz brotli xz graphite
</code></pre>
</details>
## Installation
More detailed instructions and walkthroughs are provided at https://www.stardustxr.org
The [Terra Repository](https://terra.fyralabs.com/) is required, and comes pre-installed with [Ultramarine Linux](https://ultramarine-linux.org/). Other Fedora Editions and derivatives can directly install terra-release:
## Build
```bash
cargo build
sudo dnf install --nogpgcheck --repofrompath 'terra,https://repos.fyralabs.com/terra$releasever' terra-release
```
For a full installation of the Stardust XR server *and* a selected group of clients, run:
```bash
sudo dnf group install stardust-xr
```
## Manual Build
We've provided a manual installation script [here](https://github.com/cyberneticmelon/usefulscripts/blob/main/stardustxr_setup.sh) that clones and builds the Stardust XR server along with a number of other clients from their respective repositories, and provides a startup script for automatically launching some clients.
After cloning the repository
```bash
cargo build --release # this is needed to skip validation layers
```
The latest stable server is automatically built to an appimage at https://github.com/StardustXR/server/releases for easy testing.
## Usage
> [!NOTE]
> For help with setting up an XR headset on linux, visit https://stardustxr.org/docs/get-started/setup-openxr
First, try running `cargo run` in a terminal window. If a headset is plugged in and OpenXR is working no window will show up. However, the headset should show the same things as the window that opens:
![A pitch black void with a single bleach white hand in the middle](/img/xr_mode_windowed_blank.png)
The **Stardust XR Server** is a server that runs clients, so without any running, you will see a black screen. If you only have the server installed, we recommend also cloning and building the following clients to start: [Flatland](https://github.com/StardustXR/flatland), which allows normal 2D apps to run in Stardust, [Protostar](https://github.com/StardustXR/protostar), which contains Hexagon Launcher, an app launcher menu, and [Black Hole](https://github.com/StardustXR/black-hole) to quickly tuck away your objects and apps (kind of like desktop peek on Windows).
Stardust won't do anything interesting without clients! Try some from https://github.com/StardustXR.
First, try running `cargo run -- -f` in a terminal window to check out flatscreen mode, (or `stardust-xr-server -f` / `stardust-xr-server_dev -f` if you installed via dnf or the manual installation script, respectively, as they provide symlinks.)
### Default Sky
If there aren't already any clients running, you'll need to manually launch them by either navigating to their repositories and running `cargo run`, or running them via their names if you installed via dnf or the manual installation script, such as `flatland`, `hexagon_launcher`, etc.
You can set a default skytex/skylight by putting your favorite HDRI equirectangular sky in `~/.config/stardust/skytex.hdr`. Certain clients can override this.
> [!IMPORTANT]
> [Flatland](https://github.com/StardustXR/flatland) must be running for 2D apps to launch.
Flatscreen mode when the default skybox is [Zhengyang Gate](https://polyhaven.com/a/zhengyang_gate):
![A pitch black window representing Stardust in flatscreen mode](/img/flatscreen_3.png)
### Startup Script
A startup script can be created at `~/.config/stardust/startup` that will launch specified settings and clients/applications, an example of which is shown [here](https://github.com/cyberneticmelon/usefulscripts/blob/main/startup). If you used the [installation script](https://github.com/cyberneticmelon/usefulscripts/blob/main/stardustxr_setup.sh), one will have already been made for you. This allows wide flexibility of what clients to launch upon startup (and, for example, *where*, using the [Gravity](https://github.com/StardustXR/gravity) client to specify X Y and Z co-ordinates).
### Windowed Mode
### Flatscreen Navigation
A video guide showcasing flatscreen controls is available [here](https://www.youtube.com/watch?v=JCYecSlKlDI)
If the stardust server can't connect to an OpenXR runtime or you force it into flatscreen mode with `-f`, the server will show in a window.
![A black void representing Stardust in XR mode with a hand skeleton in the middle](/img/flatscreen_2.png)
To move around, hold down `Shift + W A S D`, with `Q` for moving down and `E` for moving up.
![wasd](https://github.com/StardustXR/website/blob/main/static/img/updated_flat_wasd.GIF)
You can navigate around by right click + dragging to look around, Shift+W/A/S/D/Q/E to move. If you have a virtual hand, left click pinches, right click points, both make a fist.
To look around, hold down `Shift + Right` Click while moving the mouse.
![updated_look](https://github.com/StardustXR/website/blob/main/static/img/updated_flat_look.GIF)
### Flags
#### Flatscreen (-f)
To drag applications out of the app launcher, hold down `Shift + ~`
![updated_drag](https://github.com/StardustXR/website/blob/main/static/img/updated_flat_drag.GIF)
The server will show up in windowed mode no matter what with your mouse pointer being turned into a 3D pointer. Keyboard input will be sent to whatever your mouse is hovering over like visionOS simulator.
Flatscreen mode upon initial startup:
![A pitch black window representing Stardust in flatscreen mode](/img/flatscreen_1.png)
### XR Navigation
A video guide showcasing XR controls is available [here](https://www.youtube.com/watch?v=RbxFq6JjliA)
#### Overlay (-o \<PRIORITY>)
**Quest 3 Hand tracking**:
Pinch to drag and drop, grasp with full hand for grabbing, point and click with pointer finger to click or pinch from a distance
The server will, if in XR mode, be overlaid using the OpenXR overlay extension with the given priority.
![hand_pinching](https://github.com/StardustXR/website/blob/main/static/img/hand_pinching.GIF)
#### Disable controller (--disable-controller)
**Quest 3 Controller**:
Grab with the grip buttons, click by touching the tip of the cones or by using the trigger from a distance
Some runtimes such as Monado may emulate a controller using a hand, and this messes with Stardust's input system. Set this flag to ignore the controllers that the OpenXR runtime provides.
#### Execute (-e </path/to/executable>)
When wayland and OpenXR and such are initialized, run the given executable (such as a bash script) with all the environment variables needed to connect all clients of any type to the server. If not set, the server will run the executable at `~/.config/stardust/startup` if it exists. This is how stardust desktop environments can be made.
#### Help (-h, --help)
help
## Test
##### Gnome Graphical Integration Test
- `nix build .#gnome-graphical-test`
This test uses Nix to reproducibly execute a QEMU virtual machine which
spawns a full Gnome desktop. It runs `monado-service`, `stardust-xr-server`
`flatland` underneath of Gnome and then attaches `weston-cliptest` to the
`flatland` process running underneath of `stardust-xr-server`, the result is
a screenshot in PNG format that should look like expected. If any process in
this test produces an exit code above 0, the test will fail, graphical bugs
should be visible in the screenshot. An example of the result is below.
###### Result
![image](https://github.com/StardustXR/server/assets/26458780/e21cd039-2528-4568-b20a-ce4abfab6d9b)
##### Everything
`nix flake check` will build every test underneath of the `checks` attribute in the `flake.nix`
![controller_click](https://github.com/StardustXR/website/blob/main/static/img/controller_click.GIF)

View File

@@ -9,9 +9,6 @@ proc-macro = true
[dependencies]
convert_case = "0.6.0"
quote = "1.0.33"
mint = "0.5.9"
proc-macro2 = "1.0.71"
split-iter = "0.1.0"
[dependencies.stardust-xr-schemas]
git = "https://github.com/StardustXR/core.git"
stardust-xr = { workspace = true }

View File

@@ -1,17 +1,16 @@
use convert_case::{Case, Casing};
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, ToTokens};
use split_iter::Splittable;
use stardust_xr_schemas::protocol::*;
use stardust_xr::schemas::protocol::*;
fn fold_tokens(a: TokenStream, b: TokenStream) -> TokenStream {
quote!(#a #b)
}
// #[proc_macro]
// pub fn codegen_root_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
// codegen_protocol(ROOT_PROTOCOL)
// }
#[proc_macro]
pub fn codegen_root_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
codegen_protocol(ROOT_PROTOCOL)
}
#[proc_macro]
pub fn codegen_node_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
codegen_protocol(NODE_PROTOCOL)
@@ -25,10 +24,6 @@ pub fn codegen_field_protocol(_input: proc_macro::TokenStream) -> proc_macro::To
codegen_protocol(FIELD_PROTOCOL)
}
#[proc_macro]
pub fn codegen_data_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
codegen_protocol(DATA_PROTOCOL)
}
#[proc_macro]
pub fn codegen_audio_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
codegen_protocol(AUDIO_PROTOCOL)
}
@@ -36,27 +31,57 @@ pub fn codegen_audio_protocol(_input: proc_macro::TokenStream) -> proc_macro::To
pub fn codegen_drawable_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
codegen_protocol(DRAWABLE_PROTOCOL)
}
// #[proc_macro]
// pub fn codegen_input_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
// codegen_protocol(INPUT_PROTOCOL)
// }
#[proc_macro]
pub fn codegen_input_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
codegen_protocol(INPUT_PROTOCOL)
}
#[proc_macro]
pub fn codegen_item_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
codegen_protocol(ITEM_PROTOCOL)
}
#[proc_macro]
pub fn codegen_item_camera_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
codegen_protocol(ITEM_CAMERA_PROTOCOL)
}
#[proc_macro]
pub fn codegen_item_panel_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
codegen_protocol(ITEM_PANEL_PROTOCOL)
}
fn codegen_protocol(protocol: &'static str) -> proc_macro::TokenStream {
let protocol = Protocol::parse(protocol).unwrap();
let interface = protocol
.interface
.map(|p| {
let virtual_aspect_name = p.path[1..]
.split('/')
.map(ToString::to_string)
.reduce(|a, b| format!("{a}_{b}"))
.unwrap_or_default()
+ "_interface";
generate_aspect(&Aspect {
name: virtual_aspect_name,
let node_id = p.node_id;
let node_id = quote! {
const INTERFACE_NODE_ID: u64 = #node_id;
};
let aspect = generate_aspect(&Aspect {
name: "interface".to_string(),
id: 0,
description: protocol.description.clone(),
inherits: vec![],
members: p.members,
})
inherited_aspects: vec![],
});
quote! {
#node_id
#aspect
pub struct Interface;
impl crate::nodes::AspectIdentifier for Interface {
impl_aspect_for_interface_aspect_id!{}
}
impl crate::nodes::Aspect for Interface {
impl_aspect_for_interface_aspect!{}
}
pub fn create_interface(client: &std::sync::Arc<crate::core::client::Client>) -> crate::core::error::Result<()>{
let node = crate::nodes::Node::from_id(client,INTERFACE_NODE_ID,false);
node.add_aspect(Interface);
node.add_to_scenegraph()?;
Ok(())
}
}
})
.unwrap_or_default();
let custom_enums = protocol
@@ -80,15 +105,9 @@ fn codegen_protocol(protocol: &'static str) -> proc_macro::TokenStream {
let aspects = protocol
.aspects
.iter()
.map(generate_aspect)
.map(|a| generate_aspect(&a.blocking_read()))
.reduce(fold_tokens)
.unwrap_or_default();
// let nodes = protocol
// .nodes
// .iter()
// .map(generate_node)
// .reduce(fold_tokens)
// .unwrap_or_default();
quote!(#custom_enums #custom_unions #custom_structs #aspects #interface).into()
}
@@ -124,7 +143,7 @@ fn generate_custom_union(custom_union: &CustomUnion) -> TokenStream {
quote! {
#[doc = #description]
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
#[serde(tag = "t", content = "c")]
pub enum #name {#option_decls}
}
}
@@ -162,99 +181,196 @@ fn generate_custom_struct(custom_struct: &CustomStruct) -> TokenStream {
}
}
// fn generate_node(node: &Node) -> TokenStream {
// let node_name = Ident::new(&node.name, Span::call_site());
// let description = &node.description;
// let aspects = node
// .aspects
// .iter()
// .map(|a| {
// let aspect_name = Ident::new(&format!("{a}Aspect"), Span::call_site());
// quote!(impl #aspect_name for #node_name {})
// })
// .reduce(fold_tokens)
// .unwrap_or_default();
// quote! {
// #[doc = #description]
// #[derive(Debug)]
// pub struct #node_name (crate::node::Node);
// impl crate::node::NodeType for #node_name {
// fn node(&self) -> &crate::node::Node {
// &self.0
// }
// fn alias(&self) -> Self {
// #node_name(self.0.alias())
// }
// fn from_path(client: &std::sync::Arc<crate::client::Client>, path: String, destroyable: bool) -> Self {
// #node_name(crate::node::Node::from_path(client, path, destroyable))
// }
// }
// impl serde::Serialize for #node_name {
// fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
// let node_path = self.0.get_path().map_err(|e| serde::ser::Error::custom(e))?;
// serializer.serialize_str(&node_path)
// }
// }
// #aspects
// }
// }
fn generate_aspect(aspect: &Aspect) -> TokenStream {
let description = &aspect.description;
let (client_members, server_members) = aspect.members.iter().split(|m| m.side == Side::Server);
let (client_members, server_members) = aspect
.members
.iter()
.partition::<Vec<_>, _>(|m| m.side == Side::Client);
let client_mod_name = Ident::new(
&format!("{}_client", &aspect.name.to_case(Case::Snake)),
Span::call_site(),
);
let client_side_members = client_members
.map(generate_member)
.into_iter()
.map(|m| generate_member(aspect.id, &aspect.name.to_case(Case::Pascal), m))
.reduce(fold_tokens)
.map(|t| {
// TODO: properly import all dependencies
quote! {
#[allow(clippy::all)]
pub mod #client_mod_name {
use super::*;
#t
}
}
})
.unwrap_or_default();
let opcodes = aspect
.members
.iter()
.map(|m| {
let aspect_name = aspect.name.to_case(Case::ScreamingSnake);
let member_name = m.name.to_case(Case::ScreamingSnake);
let name_type = if m.side == Side::Client {
"CLIENT"
} else {
"SERVER"
};
let name = Ident::new(
&format!("{aspect_name}_{member_name}_{name_type}_OPCODE"),
Span::call_site(),
);
let opcode = m.opcode;
quote!(pub(crate) const #name: u64 = #opcode;)
})
.reduce(fold_tokens)
.unwrap_or_default();
let alias_info = generate_alias_info(aspect);
let server_side_members = server_members
.into_iter()
.map(|m| generate_member(aspect.id, &aspect.name.to_case(Case::Pascal), m))
.reduce(fold_tokens)
.unwrap_or_default();
let aspect_trait_name = Ident::new(
&format!("{}Aspect", &aspect.name.to_case(Case::Pascal)),
Span::call_site(),
);
let server_side_members = server_members
.map(generate_member)
.reduce(fold_tokens)
.unwrap_or_default();
let add_node_members = aspect
let run_signals = aspect
.members
.iter()
.filter(|m| m.side == Side::Server)
.map(generate_handler)
.filter(|m| m._type == MemberType::Signal)
.map(|m| generate_run_member(&aspect_trait_name, MemberType::Signal, m))
.reduce(fold_tokens)
.unwrap_or_default();
let run_methods = aspect
.members
.iter()
.filter(|m| m.side == Side::Server)
.filter(|m| m._type == MemberType::Method)
.map(|m| generate_run_member(&aspect_trait_name, MemberType::Method, m))
.reduce(fold_tokens)
.map(|members| {
quote! {
fn add_node_members(node: &crate::nodes::Node) {
#members
}
}
})
.unwrap_or_default();
let server_side_members = quote! {
#[allow(clippy::all)]
#[doc = #description]
pub trait #aspect_trait_name {
#add_node_members
#server_side_members
}
};
quote!(#client_side_members #server_side_members)
let aspect_id_macro_name = Ident::new(
&format!(
"impl_aspect_for_{}_aspect_id",
aspect.name.to_case(Case::Snake)
),
Span::call_site(),
);
let aspect_macro_name = Ident::new(
&format!(
"impl_aspect_for_{}_aspect",
aspect.name.to_case(Case::Snake)
),
Span::call_site(),
);
let aspect_id = aspect.id;
let aspect_macro = quote! {
macro_rules! #aspect_id_macro_name {
() => {
const ID: u64 = #aspect_id;
}
}
macro_rules! #aspect_macro_name {
() => {
#[allow(clippy::all)]
fn run_signal(
&self,
_calling_client: std::sync::Arc<crate::core::client::Client>,
_node: std::sync::Arc<crate::nodes::Node>,
_signal: u64,
_message: crate::nodes::Message
) -> Result<(), stardust_xr::scenegraph::ScenegraphError> {
match _signal {
#run_signals
_ => Err(stardust_xr::scenegraph::ScenegraphError::MemberNotFound)
}
}
#[allow(clippy::all)]
fn run_method(
&self,
_calling_client: std::sync::Arc<crate::core::client::Client>,
_node: std::sync::Arc<crate::nodes::Node>,
_method: u64,
_message: crate::nodes::Message,
_method_response: crate::core::scenegraph::MethodResponseSender,
) {
match _method {
#run_methods
_ => {
let _ = _method_response.send_err(stardust_xr::scenegraph::ScenegraphError::MemberNotFound);
}
}
}
};
}
};
quote!(#opcodes #alias_info #client_side_members #server_side_members #aspect_macro)
}
fn generate_member(member: &Member) -> TokenStream {
let name_str = &member.name;
fn generate_alias_opcodes(aspect: &Aspect, side: Side, _type: MemberType) -> TokenStream {
aspect
.members
.iter()
.filter(|m| m.side == side)
.filter(|m| m._type == _type)
.map(|m| m.opcode)
.map(|o| quote!(#o))
.reduce(|a, b| quote!(#a, #b))
.unwrap_or_default()
}
fn generate_alias_info(aspect: &Aspect) -> TokenStream {
let aspect_alias_info_name = Ident::new(
&format!(
"{}_ASPECT_ALIAS_INFO",
aspect.name.to_case(Case::ScreamingSnake)
),
Span::call_site(),
);
let local_signals = generate_alias_opcodes(aspect, Side::Server, MemberType::Signal);
let local_methods = generate_alias_opcodes(aspect, Side::Server, MemberType::Method);
let remote_signals = generate_alias_opcodes(aspect, Side::Client, MemberType::Signal);
let inherits = aspect
.inherits
.iter()
.map(|a| {
Ident::new(
&format!("{}_ASPECT_ALIAS_INFO", a.to_case(Case::ScreamingSnake)),
Span::call_site(),
)
})
.map(|a| quote!(#a.clone()))
.fold(quote!(), |a, b| quote!(#a + #b));
quote! {
lazy_static::lazy_static! {
#[allow(clippy::all)]
pub static ref #aspect_alias_info_name: crate::nodes::alias::AliasInfo = crate::nodes::alias::AliasInfo {
server_signals: vec![#local_signals],
server_methods: vec![#local_methods],
client_signals: vec![#remote_signals],
}
#inherits;
}
}
}
fn generate_member(aspect_id: u64, aspect_name: &str, member: &Member) -> TokenStream {
let opcode = member.opcode;
let name = Ident::new(&member.name.to_case(Case::Snake), Span::call_site());
let description = &member.description;
@@ -267,6 +383,18 @@ fn generate_member(member: &Member) -> TokenStream {
}
Side::Client => quote!(_node: &crate::nodes::Node),
};
let arguments = member
.arguments
.iter()
.map(|a| Ident::new(&a.name.to_case(Case::Snake), Span::call_site()));
let argument_debug = member
.arguments
.iter()
.map(|a| Ident::new(&a.name.to_case(Case::Snake), Span::call_site()))
.map(|n| quote!(?#n))
.reduce(|a, b| quote!(#a, #b))
.map(|args| quote!(#args,));
let argument_decls = member
.arguments
.iter()
@@ -282,60 +410,67 @@ fn generate_member(member: &Member) -> TokenStream {
let return_type = member
.return_type
.as_ref()
.map(|r| generate_argument_type(&r, false, true))
.map(|r| generate_argument_type(r, false, true))
.unwrap_or_else(|| quote!(()));
let name_str = name.to_string();
match (side, _type) {
(Side::Client, MemberType::Method) => {
quote! {
#[doc = #description]
pub async fn #name(#argument_decls) -> color_eyre::eyre::Result<#return_type> {
_node.execute_remote_method(#name_str, &(#argument_uses)).await
}
}
}
(Side::Client, MemberType::Signal) => {
quote! {
#[doc = #description]
pub fn #name(#argument_decls) -> color_eyre::eyre::Result<()> {
let serialized = stardust_xr::schemas::flex::serialize((#argument_uses))?;
_node.send_remote_signal(#name_str, serialized)
pub fn #name(#argument_decls) -> crate::core::error::Result<()> {
let arguments = (#argument_uses);
let (#(#arguments),*) = &arguments;
::tracing::trace!(#argument_debug "sent signal to client: {}::{}", #aspect_name, #name_str);
let result = stardust_xr::schemas::flex::serialize(&arguments).map_err(|e|e.into()).and_then(|serialized|_node.send_remote_signal(#aspect_id, #opcode, serialized));
if let Err(err) = result.as_ref() {
::tracing::warn!(#argument_debug "failed to send signal to client : {}::{}, error: {}",#aspect_name,#name_str,err);
}
result
}
}
}
(Side::Client, MemberType::Method) => {
quote! {
#[doc = #description]
pub async fn #name(#argument_decls) -> crate::core::error::Result<(#return_type, Vec<std::os::fd::OwnedFd>)> {
let arguments = (#argument_uses);
let (#(#arguments),*) = &arguments;
::tracing::trace!(#argument_debug "called client method: {}::{}",#aspect_name,#name_str);
let result = _node.execute_remote_method_typed(#aspect_id, #opcode, &arguments, vec![]).await;
match result.as_ref() {
Ok(value) => {
::tracing::trace!(?value, "client method returned value: {}::{}",#aspect_name,#name_str);
},
Err(err) => {
::tracing::warn!(#argument_debug "client method returned error: {}::{}, error: {}",#aspect_name,#name_str,err);
}
}
result
}
}
}
(Side::Server, MemberType::Signal) => {
quote! {
#[doc = #description]
fn #name(#argument_decls) -> crate::core::error::Result<()>;
}
}
(Side::Server, MemberType::Method) => {
quote! {
#[doc = #description]
fn #name(#argument_decls) -> impl std::future::Future<Output = color_eyre::eyre::Result<#return_type>> + Send + 'static;
}
}
(Side::Server, MemberType::Signal) => {
let prefix =
if let Some(ArgumentType::Node { _type, return_info }) = &member.return_type {
if let Some(return_info) = return_info {
let parent_name = Ident::new(
&(name_str.to_case(Case::ScreamingSnake) + "_PARENT_PATH"),
Span::call_site(),
);
let parent_path = &return_info.parent;
quote!(const #parent_name: &'static str = #parent_path;)
} else {
TokenStream::default()
}
} else {
TokenStream::default()
};
quote! {
#prefix
#[doc = #description]
fn #name(#argument_decls) -> color_eyre::eyre::Result<()>;
fn #name(#argument_decls) -> impl std::future::Future<Output = crate::core::error::Result<#return_type>> + Send + Sync + 'static;
}
}
}
}
fn generate_handler(member: &Member) -> TokenStream {
let member_name = &member.name;
let member_name_ident = Ident::new(&member_name, Span::call_site());
fn generate_run_member(aspect_name: &Ident, _type: MemberType, member: &Member) -> TokenStream {
let opcode = member.opcode;
let member_name_ident = Ident::new(&member.name, Span::call_site());
let member_name = member_name_ident.to_string();
let aspect_name_str = aspect_name.to_string();
let argument_names = member
.arguments
@@ -350,34 +485,60 @@ fn generate_handler(member: &Member) -> TokenStream {
generate_argument_type(&_type, a.optional, true)
})
.reduce(|a, b| quote!(#a, #b));
let argument_debug = member
.arguments
.iter()
.map(|a| Ident::new(&a.name.to_case(Case::Snake), Span::call_site()))
.map(|n| quote!(?#n))
.reduce(|a, b| quote!(#a, #b))
.map(|args| quote!(#args,));
// dbg!(&argument_types);
let deserialize = argument_names
.clone()
.zip(argument_types)
.map(|(argument_names, argument_types)| {
quote!(let (#argument_names): (#argument_types) = stardust_xr::schemas::flex::deserialize(_message.as_ref())?;)
quote!{
#[allow(unused_parens)]
let (#argument_names): (#argument_types) = stardust_xr::schemas::flex::deserialize(_message.as_ref())?;
}
})
.unwrap_or_default();
let serialize = generate_argument_serialize(
"result",
&member.return_type.clone().unwrap_or(ArgumentType::Empty),
false,
);
let argument_uses = member
.arguments
.iter()
.map(|a| generate_argument_deserialize(&a.name, &a._type, a.optional))
.reduce(|a, b| quote!(#a, #b))
.unwrap_or_default();
match member._type {
match _type {
MemberType::Signal => quote! {
node.add_local_signal(#member_name, |_node, _calling_client, _message| {
#opcode => (move || {
#deserialize
Self::#member_name_ident(_node, _calling_client.clone(), #argument_uses)
});
::tracing::trace!(#argument_debug "received local signal: {}::{}",#aspect_name_str,#member_name);
<Self as #aspect_name>::#member_name_ident(_node, _calling_client.clone(), #argument_uses)
})().map_err(|e: crate::core::error::ServerError| stardust_xr::scenegraph::ScenegraphError::MemberError { error: e.to_string() }),
},
MemberType::Method => quote! {
node.add_local_method(#member_name, |_node, _calling_client, _message, _method_response| {
_method_response.wrap_async(async move {
#deserialize
Ok((Self::#member_name_ident(_node, _calling_client.clone(), #argument_uses).await?, Vec::new()))
});
});
#opcode => _method_response.wrap_async(async move {
#deserialize
::tracing::trace!(#argument_debug "called local method: {}::{}",#aspect_name_str,#member_name);
let result = <Self as #aspect_name>::#member_name_ident(_node, _calling_client.clone(), #argument_uses).await;
match result.as_ref() {
Ok(value) => {
::tracing::trace!(?value, "client method returned value: {}::{}",#aspect_name_str,#member_name);
},
Err(err) => {
::tracing::warn!("client method returned error: {}::{}, error: {}",#aspect_name_str,#member_name,err);
}
}
let result = result?;
Ok((#serialize, Vec::<std::os::fd::OwnedFd>::new()))
}),
},
}
}
@@ -387,7 +548,13 @@ fn generate_argument_name(argument: &Argument) -> TokenStream {
fn convert_deserializeable_argument_type(argument_type: &ArgumentType) -> ArgumentType {
match argument_type {
ArgumentType::Node { .. } => ArgumentType::String,
ArgumentType::Node { .. } => ArgumentType::NodeID,
ArgumentType::Vec(v) => {
ArgumentType::Vec(Box::new(convert_deserializeable_argument_type(v.as_ref())))
}
ArgumentType::Map(v) => {
ArgumentType::Map(Box::new(convert_deserializeable_argument_type(v.as_ref())))
}
f => f.clone(),
}
}
@@ -397,20 +564,26 @@ fn generate_argument_deserialize(
optional: bool,
) -> TokenStream {
let name = Ident::new(&argument_name.to_case(Case::Snake), Span::call_site());
if let ArgumentType::Node { .. } = argument_type {
return match optional {
true => quote!(#name.map(|n| _calling_client.get_node(#argument_name, n)?)),
false => quote!(_calling_client.get_node(#argument_name, #name)?),
};
}
if optional {
let mapping = generate_argument_deserialize("o", argument_type, false);
return quote!(#name.map(|o| Ok::<_, crate::core::error::ServerError>(#mapping)).transpose()?);
}
match argument_type {
ArgumentType::Node { .. } => match optional {
true => quote!(#name.map(|n| _calling_client.get_node(#argument_name, &n)?)),
false => quote!(_calling_client.get_node(#argument_name, &#name)?),
},
ArgumentType::Color => quote!(color::rgba_linear!(#name[0], #name[1], #name[2], #name[3])),
ArgumentType::Vec(v) => {
let mapping = generate_argument_deserialize("a", v, false);
quote!(#name.into_iter().map(|a| Ok(#mapping)).collect::<color_eyre::eyre::Result<Vec<_>>>()?)
quote!(#name.into_iter().map(|a| Ok(#mapping)).collect::<crate::core::error::Result<Vec<_>>>()?)
}
ArgumentType::Map(v) => {
let mapping = generate_argument_deserialize("a", v, false);
quote!(#name.into_iter().map(|(k, a)| Ok((k, #mapping))).collect::<color_eyre::eyre::Result<rustc_hash::FxHashMap<String, _>>>()?)
quote!(#name.into_iter().map(|(k, a)| Ok((k, #mapping))).collect::<crate::core::error::Result<rustc_hash::FxHashMap<String, _>>>()?)
}
_ => quote!(#name),
}
@@ -424,19 +597,19 @@ fn generate_argument_serialize(
match argument_type {
ArgumentType::Node {
_type,
return_info: _,
return_id_parameter_name: _,
} => match optional {
true => quote!(#name.map(|n| n.get_path())),
false => quote!(#name.get_path()),
true => quote!(#name.map(|n| n.get_id())),
false => quote!(#name.get_id()),
},
ArgumentType::Color => quote!([#name.c.r, #name.c.g, #name.c.b, #name.a]),
ArgumentType::Vec(v) => {
let mapping = generate_argument_serialize("a", v, false);
quote!(#name.into_iter().map(|a| Ok(#mapping)).collect::<color_eyre::eyre::Result<Vec<_>>>()?)
quote!(#name.into_iter().map(|a| Ok(#mapping)).collect::<crate::core::error::Result<Vec<_>>>()?)
}
ArgumentType::Map(v) => {
let mapping = generate_argument_serialize("a", v, false);
quote!(#name.into_iter().map(|(k, a)| Ok((k, #mapping))).collect::<color_eyre::eyre::Result<rustc_hash::FxHashMap<String, _>>>()?)
quote!(#name.into_iter().map(|(k, a)| Ok((k, #mapping))).collect::<crate::core::error::Result<rustc_hash::FxHashMap<String, _>>>()?)
}
_ => quote!(#name),
}
@@ -448,24 +621,28 @@ fn generate_argument_decl(argument: &Argument, owned_values: bool) -> TokenStrea
}
fn argument_type_option_name(argument_type: &ArgumentType) -> String {
match argument_type {
ArgumentType::Empty => "Empty".to_string(),
ArgumentType::Bool => "Bool".to_string(),
ArgumentType::Int => "Int".to_string(),
ArgumentType::UInt => "UInt".to_string(),
ArgumentType::Float => "Float".to_string(),
ArgumentType::Vec2 => "Vec2".to_string(),
ArgumentType::Vec3 => "Vec3".to_string(),
ArgumentType::Vec2(_) => "Vec2".to_string(),
ArgumentType::Vec3(_) => "Vec3".to_string(),
ArgumentType::Quat => "Quat".to_string(),
ArgumentType::Mat4 => "Mat4".to_string(),
ArgumentType::Color => "Color".to_string(),
ArgumentType::String => "String".to_string(),
ArgumentType::Bytes => "Bytes".to_string(),
ArgumentType::Vec(v) => format!("{}Vector", argument_type_option_name(&v)),
ArgumentType::Map(m) => format!("{}Map", argument_type_option_name(&m)),
ArgumentType::Vec(v) => format!("{}Vector", argument_type_option_name(v)),
ArgumentType::Map(m) => format!("{}Map", argument_type_option_name(m)),
ArgumentType::NodeID => "Node ID".to_string(),
ArgumentType::Datamap => "Datamap".to_string(),
ArgumentType::ResourceID => "ResourceID".to_string(),
ArgumentType::Enum(e) => e.clone(),
ArgumentType::Union(u) => u.clone(),
ArgumentType::Struct(s) => s.clone(),
ArgumentType::Node { _type, .. } => _type.clone(),
ArgumentType::Fd => "File Descriptor".to_string(),
}
}
fn generate_argument_type(
@@ -474,13 +651,21 @@ fn generate_argument_type(
owned: bool,
) -> TokenStream {
let _type = match argument_type {
ArgumentType::Empty => quote!(()),
ArgumentType::Bool => quote!(bool),
ArgumentType::Int => quote!(i32),
ArgumentType::UInt => quote!(u32),
ArgumentType::Float => quote!(f32),
ArgumentType::Vec2 => quote!(mint::Vector2<f32>),
ArgumentType::Vec3 => quote!(mint::Vector3<f32>),
ArgumentType::Quat => quote!(mint::Quaternion<f32>),
ArgumentType::Vec2(t) => {
let t = generate_argument_type(t, false, true);
quote!(stardust_xr::values::Vector2<#t>)
}
ArgumentType::Vec3(t) => {
let t = generate_argument_type(t, false, true);
quote!(stardust_xr::values::Vector3<#t>)
}
ArgumentType::Quat => quote!(stardust_xr::values::Quaternion),
ArgumentType::Mat4 => quote!(stardust_xr::values::Mat4),
ArgumentType::Color => quote!(stardust_xr::values::Color),
ArgumentType::Bytes => {
if !owned {
@@ -497,7 +682,7 @@ fn generate_argument_type(
}
}
ArgumentType::Vec(t) => {
let t = generate_argument_type(&t, false, true);
let t = generate_argument_type(t, false, true);
if !owned {
quote!(&[#t])
} else {
@@ -505,14 +690,15 @@ fn generate_argument_type(
}
}
ArgumentType::Map(t) => {
let t = generate_argument_type(&t, false, true);
let t = generate_argument_type(t, false, true);
if !owned {
quote!(&rustc_hash::FxHashMap<String, #t>)
quote!(&stardust_xr::values::Map<String, #t>)
} else {
quote!(rustc_hash::FxHashMap<String, #t>)
quote!(stardust_xr::values::Map<String, #t>)
}
}
ArgumentType::NodeID => quote!(u64),
ArgumentType::Datamap => {
if !owned {
quote!(&stardust_xr::values::Datamap)
@@ -545,7 +731,7 @@ fn generate_argument_type(
}
ArgumentType::Node {
_type,
return_info: _,
return_id_parameter_name: _,
} => {
if !owned {
quote!(&std::sync::Arc<crate::nodes::Node>)
@@ -553,6 +739,9 @@ fn generate_argument_type(
quote!(std::sync::Arc<crate::nodes::Node>)
}
}
ArgumentType::Fd => {
quote!(&std::os::fd::OwnedFd)
}
};
if optional {

97
flake.lock generated
View File

@@ -1,24 +1,23 @@
{
"nodes": {
"fenix": {
"crane": {
"inputs": {
"nixpkgs": [
"flatland",
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
]
},
"locked": {
"lastModified": 1700115779,
"narHash": "sha256-oajhxEBg+16/KH74CaygAQ6b5KUHS7DwBoL9ecD9qeI=",
"owner": "nix-community",
"repo": "fenix",
"rev": "4378e7e5f5bdef438eee5ce967f37593b9b5cd16",
"lastModified": 1712681629,
"narHash": "sha256-bMDXn4AkTXLCpoZbII6pDGoSeSe9gI87jxPsHRXgu/E=",
"owner": "ipetkov",
"repo": "crane",
"rev": "220387ac8e99cbee0ca4c95b621c4bc782b6a235",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
@@ -27,11 +26,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1709336216,
"narHash": "sha256-Dt/wOWeW6Sqm11Yh+2+t0dfEWxoMxGBvv3JpIocFl9E=",
"lastModified": 1754487366,
"narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f7b3c975cf067e56e7cda6cb098ebe3fb4d74ca2",
"rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18",
"type": "github"
},
"original": {
@@ -48,11 +47,11 @@
]
},
"locked": {
"lastModified": 1701473968,
"narHash": "sha256-YcVE5emp1qQ8ieHUnxt1wCZCC3ZfAS+SRRWZ2TMda7E=",
"lastModified": 1754487366,
"narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "34fed993f1674c8d06d58b37ce1e0fe5eebcb9f5",
"rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18",
"type": "github"
},
"original": {
@@ -62,15 +61,15 @@
},
"flatland": {
"inputs": {
"fenix": "fenix",
"crane": "crane",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1709774755,
"narHash": "sha256-6tSG4G+SB+l71101XNyEHN/J8pSuJvbNPhKgzqP5sVU=",
"lastModified": 1725868201,
"narHash": "sha256-rDBQ9tXQCCA7emikSYH59ADJELE2IpzB7eoLrpHYzU4=",
"owner": "StardustXR",
"repo": "flatland",
"rev": "fef8c317928a1d1798d8cee9af94d219a7c09e8c",
"rev": "0914dd3df54a5e6258dfc0a02d65af1c0fc0fc90",
"type": "github"
},
"original": {
@@ -85,11 +84,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1708547820,
"narHash": "sha256-xU/KC1PWqq5zL9dQ9wYhcdgxAwdeF/dJCLPH3PNZEBg=",
"lastModified": 1755233722,
"narHash": "sha256-AavrbMltJKcC2Fx0lfJoZfmy7g87ebXU0ddVenhajLA=",
"owner": "hercules-ci",
"repo": "hercules-ci-effects",
"rev": "0ca27bd58e4d5be3135a4bef66b582e57abe8f4a",
"rev": "99e03e72e3f7e13506f80ef9ebaedccb929d84d0",
"type": "github"
},
"original": {
@@ -100,11 +99,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1699781429,
"narHash": "sha256-UYefjidASiLORAjIvVsUHG6WBtRhM67kTjEY4XfZOFs=",
"lastModified": 1712791164,
"narHash": "sha256-3sbWO1mbpWsLepZGbWaMovSO7ndZeFqDSdX0hZ9nVyw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e44462d6021bfe23dfb24b775cc7c390844f773d",
"rev": "1042fd8b148a9105f3c0aca3a6177fd1d9360ba5",
"type": "github"
},
"original": {
@@ -116,29 +115,26 @@
},
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1709237383,
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
"lastModified": 1753579242,
"narHash": "sha256-zvaMGVn14/Zz8hnp4VWT9xVnhc8vuL3TStRqwk22biA=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "0f36c44e01a6129be94e3ade315a5883f0228a6e",
"type": "github"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1703637592,
"narHash": "sha256-8MXjxU0RfFfzl57Zy3OfXCITS0qWDNLzlBAdwxGZwfY=",
"lastModified": 1755027561,
"narHash": "sha256-IVft239Bc8p8Dtvf7UAACMG5P3ZV+3/aO28gXpGtMXI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "cfc3698c31b1fb9cdcf10f36c9643460264d0ca8",
"rev": "005433b926e16227259a1843015b5b2b7f7d1fc3",
"type": "github"
},
"original": {
@@ -150,11 +146,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1709479366,
"narHash": "sha256-n6F0n8UV6lnTZbYPl1A9q1BS0p4hduAv1mGAP17CVd0=",
"lastModified": 1755186698,
"narHash": "sha256-wNO3+Ks2jZJ4nTHMuks+cxAiVBGNuEBXsT29Bz6HASo=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "b8697e57f10292a6165a20f03d2f42920dfaf973",
"rev": "fbcf476f790d8a217c3eab4e12033dc4a0f6d23c",
"type": "github"
},
"original": {
@@ -171,23 +167,6 @@
"hercules-ci-effects": "hercules-ci-effects",
"nixpkgs": "nixpkgs_3"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1700077026,
"narHash": "sha256-Vf7ykubXsriSjBbeYAm8bzBIvSOYVUmRiCQ3iLL/E+U=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "58de0b130a763f3a2d373f508ac0c18a8e7d0acd",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
}
},
"root": "root",

133
flake.nix
View File

@@ -1,7 +1,9 @@
{
nixConfig = {
extra-substituters = [ "https://stardustxr.cachix.org" ];
extra-trusted-public-keys = [ "stardustxr.cachix.org-1:mWSn8Ap2RLsIWT/8gsj+VfbJB6xoOkPaZpbjO+r9HBo=" ];
extra-trusted-public-keys = [
"stardustxr.cachix.org-1:mWSn8Ap2RLsIWT/8gsj+VfbJB6xoOkPaZpbjO+r9HBo="
];
};
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
@@ -12,16 +14,23 @@
# it to create VM Tests
flatland.url = "github:StardustXR/flatland";
};
outputs = inputs@{ self, flake-parts, nixpkgs, hercules-ci-effects, flatland, ... }:
outputs =
inputs@{
self,
flake-parts,
nixpkgs,
hercules-ci-effects,
flatland,
...
}:
let
name = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package.name;
src = builtins.path {
name = "${name}-source";
path = toString ./.;
filter = path: type:
nixpkgs.lib.all
(n: builtins.baseNameOf path != n)
[
filter =
path: type:
nixpkgs.lib.all (n: builtins.baseNameOf path != n) [
"flake.nix"
"flake.lock"
"nix"
@@ -30,51 +39,79 @@
};
in
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
flake-parts.flakeModules.easyOverlay
imports = [ flake-parts.flakeModules.easyOverlay ];
systems = [
"aarch64-linux"
"x86_64-linux"
"riscv64-linux"
];
systems = [ "aarch64-linux" "x86_64-linux" "riscv64-linux" ];
perSystem = { config, self', inputs', pkgs, system, ... }: {
_module.args.pkgs = import inputs.nixpkgs { inherit system; overlays = [ inputs.self.overlays.default ]; };
overlayAttrs = config.packages;
packages = {
default = self'.packages.${name};
gnome-graphical-test = self'.checks.gnome-graphical-test;
"${name}" = pkgs.callPackage ./nix/stardust-xr-server.nix { inherit name src; };
};
checks.gnome-graphical-test = pkgs.nixosTest (import ./nix/gnome-graphical-test.nix { inherit pkgs self; });
devShells.default = pkgs.mkShell {
inputsFrom = [ self'.packages.default ];
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
};
};
flake = {
herculesCI.ciSystems = [ "x86_64-linux" ];
effects = let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
hci-effects = hercules-ci-effects.lib.withPkgs pkgs;
in { ref, rev, ... }: {
gnome-graphical-test = hci-effects.mkEffect {
secretsMap."stardustxrDiscord" = "stardustxrDiscord";
secretsMap."stardustxrIpfs" = "stardustxrIpfs";
effectScript = ''
readSecretString stardustxrDiscord .webhook > .webhook
readSecretString stardustxrIpfs .basicauth > .basicauth
set -x
export RESPONSE=$(curl -H @.basicauth -F file=@${self.packages."x86_64-linux".gnome-graphical-test}/screen.png https://ipfs-api.stardustxr.org/api/v0/add)
export CID=$(echo "$RESPONSE" | ${pkgs.jq}/bin/jq -r .Hash)
set +x
export ADDRESS="https://ipfs.stardustxr.org/ipfs/$CID"
${pkgs.discord-sh}/bin/discord.sh \
--description "\`stardustxr/server\` has been modified, here's how it renders \`weston-cliptest\` on \`flatland\` via \`monado-service\` inside of the \`gnome-graphical-test\`" \
--field "Ref;${ref}" \
--field "Commit ID;${rev}" \
--field "Flatland Revision;${flatland.rev}" \
--field "Reproducer;\`nix build github:stardustxr/server/${rev}#gnome-graphical-test\`" \
--image "$ADDRESS"
'';
perSystem =
{
config,
self',
inputs',
pkgs,
system,
...
}:
{
_module.args.pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ inputs.self.overlays.default ];
};
overlayAttrs = config.packages;
packages =
{
default = self'.packages.${name};
gnome-graphical-test = self'.checks.gnome-graphical-test;
"${name}" = pkgs.callPackage ./nix/stardust-xr-server.nix {
inherit name src;
};
};
apps.default = {
type = "app";
program = self'.packages.${name} + "/bin/stardust-xr-server";
};
checks.gnome-graphical-test = pkgs.nixosTest (
import ./nix/gnome-graphical-test.nix { inherit pkgs self; }
);
devShells.default = pkgs.mkShell {
inputsFrom = [ self'.packages.default ];
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
};
};
flake = {
herculesCI.ciSystems = [ "x86_64-linux" ];
effects =
let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
hci-effects = hercules-ci-effects.lib.withPkgs pkgs;
in
{ ref, rev, ... }:
{
gnome-graphical-test = hci-effects.mkEffect {
secretsMap."stardustxrDiscord" = "stardustxrDiscord";
secretsMap."stardustxrIpfs" = "stardustxrIpfs";
effectScript = ''
readSecretString stardustxrDiscord .webhook > .webhook
readSecretString stardustxrIpfs .basicauth > .basicauth
set -x
export RESPONSE=$(curl -H @.basicauth -F file=@${
self.packages."x86_64-linux".gnome-graphical-test
}/screen.png https://ipfs-api.stardustxr.org/api/v0/add)
export CID=$(echo "$RESPONSE" | ${pkgs.jq}/bin/jq -r .Hash)
set +x
export ADDRESS="https://ipfs.stardustxr.org/ipfs/$CID"
${pkgs.discord-sh}/bin/discord.sh \
--description "\`stardustxr/server\` has been modified, here's how it renders \`weston-cliptest\` on \`flatland\` via \`monado-service\` inside of the \`gnome-graphical-test\`" \
--field "Ref;${ref}" \
--field "Commit ID;${rev}" \
--field "Flatland Revision;${flatland.rev}" \
--field "Reproducer;\`nix build github:stardustxr/server/${rev}#gnome-graphical-test\`" \
--image "$ADDRESS"
'';
};
};
};
};
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 789 KiB

BIN
img/workflow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

12
justfile Normal file
View File

@@ -0,0 +1,12 @@
PREFIX := "usr"
BINARY := PREFIX / "bin"
DESTDIR := "/"
build:
cargo build --release
test:
cargo test
install:
install -Dm755 target/release/stardust-xr-server {{ DESTDIR }}{{ BINARY }}/stardust-xr-server

View File

@@ -20,8 +20,8 @@
# Set a nice desktop background that is pleasing to the eyes :3
extraGSettingsOverrides = ''
[org.gnome.desktop.background]
picture-uri='file://${pkgs.gnome.gnome-backgrounds}/share/backgrounds/gnome/blobs-l.svg'
picture-uri-dark='file://${pkgs.gnome.gnome-backgrounds}/share/backgrounds/gnome/blobs-l.svg'
picture-uri='file://${pkgs.gnome-backgrounds}/share/backgrounds/gnome/blobs-l.svg'
picture-uri-dark='file://${pkgs.gnome-backgrounds}/share/backgrounds/gnome/blobs-l.svg'
'';
};
displayManager = {
@@ -100,7 +100,7 @@
# Eval API is now internal so Shell needs to run in unsafe mode.
# TODO: improve test driver so that it supports openqa-like manipulation
# that would allow us to drop this mess.
"${pkgs.gnome.gnome-shell}/bin/gnome-shell --unsafe-mode"
"${pkgs.gnome-shell}/bin/gnome-shell --unsafe-mode"
];
};
};

27
nix/meshoptimizer.nix Normal file
View File

@@ -0,0 +1,27 @@
{ lib, stdenv, fetchFromGitHub, cmake }:
stdenv.mkDerivation rec {
pname = "meshoptimizer";
version = "0.20";
src = fetchFromGitHub {
owner = "zeux";
repo = "meshoptimizer";
rev = "c21d3be6ddf627f8ca852ba4b6db9903b0557858";
sha256 = "sha256-QCxpM2g8WtYSZHkBzLTJNQ/oHb5j/n9rjaVmZJcCZIA=";
};
nativeBuildInputs = [ cmake ];
cmakeFlags = [
"-DCMAKE_POSITION_INDEPENDENT_CODE=ON"
];
meta = with lib; {
description = "Mesh optimization library that makes meshes smaller and faster to render";
homepage = "https://github.com/zeux/meshoptimizer";
license = licenses.mit;
platforms = platforms.all;
};
}

16
nix/sk_gpu.nix Normal file
View File

@@ -0,0 +1,16 @@
{ stdenv, fetchurl, unzip }:
let
sk_gpu_zip = fetchurl {
url =
"https://github.com/StereoKit/sk_gpu/releases/download/v2024.8.16/sk_gpu.v2024.8.16.zip";
sha256 = "sha256-Wk3PZFlWqhrsQ8xG0sQaV2xSasdg2D7TMiPvl/CgtGU=";
};
in stdenv.mkDerivation rec {
name = "sk_gpu";
src = sk_gpu_zip;
unpackPhase = ''
unzip -d $out ${sk_gpu_zip}
'';
nativeBuildInputs = [ unzip ];
}

View File

@@ -1,17 +1,27 @@
{ rustPlatform
, src
, name
, openxr-loader
, libGL
, mesa
, xorg
, fontconfig
, libxkbcommon
, libclang
, cmake
, cpm-cmake
, pkg-config
, llvmPackages
{
rustPlatform,
src,
name,
vulkan-loader,
vulkan-headers,
mesa,
xorg,
fontconfig,
libxkbcommon,
libclang,
cmake,
pkg-config,
llvmPackages,
fetchFromGitHub,
libXau,
libXdmcp,
stdenv,
lib,
openxr-loader,
wayland,
alsa-lib,
}:
rustPlatform.buildRustPackage rec {
@@ -20,21 +30,39 @@ rustPlatform.buildRustPackage rec {
lockFile = (src + "/Cargo.lock");
allowBuiltinFetchGit = true;
};
CPM_SOURCE_CACHE = "./build";
postPatch = ''
sk=$(echo $cargoDepsCopy/stereokit-sys-*/StereoKit)
mkdir -p $sk/build/cpm
# This is not ideal, the original approach was to fetch the exact cmake
# file version that was wanted from GitHub directly, but at least this way it comes from Nixpkgs.. so meh
cp ${cpm-cmake}/share/cpm/CPM.cmake $sk/build/cpm/CPM_0.32.2.cmake
preBuild = ''
substituteInPlace /build/cargo-vendor-dir/bevy_gltf-0.16.1/Cargo.toml \
--replace-fail '[lints]' "" \
--replace-fail 'workspace = true' ""
'';
postFixup = ''
patchelf $out/bin/stardust-xr-server --add-rpath ${vulkan-loader}/lib
patchelf $out/bin/stardust-xr-server --add-rpath ${openxr-loader}/lib
patchelf $out/bin/stardust-xr-server --add-rpath ${libxkbcommon}/lib
'';
nativeBuildInputs = [
cmake pkg-config llvmPackages.libcxxClang
cmake
pkg-config
llvmPackages.libcxxClang
];
buildInputs = [
openxr-loader libGL mesa xorg.libX11 fontconfig libxkbcommon
vulkan-loader
vulkan-headers
mesa
xorg.libX11.dev
xorg.libXft
xorg.libXfixes
fontconfig
libxkbcommon
libXau
libXdmcp
openxr-loader
wayland
alsa-lib
];
LIBCLANG_PATH = "${libclang.lib}/lib";
}

31
src/core/bevy_channel.rs Normal file
View File

@@ -0,0 +1,31 @@
use std::sync::OnceLock;
use bevy::prelude::*;
use tokio::sync::mpsc::{self, error::TryRecvError};
#[derive(Resource)]
pub struct BevyChannelReader<T: Send + Sync + 'static>(mpsc::UnboundedReceiver<T>);
pub struct BevyChannel<T: Send + Sync + 'static>(OnceLock<mpsc::UnboundedSender<T>>);
impl<T: Send + Sync + 'static> BevyChannel<T> {
pub const fn new() -> Self {
Self(OnceLock::new())
}
pub fn init(&self, app: &mut App) {
let (tx, rx) = mpsc::unbounded_channel();
self.0.set(tx).unwrap();
app.insert_resource(BevyChannelReader(rx));
}
pub fn send(&self, msg: T) -> Option<()> {
self.0.get()?.send(msg).ok()
}
}
impl<T: Send + Sync + 'static> BevyChannelReader<T> {
pub fn read(&mut self) -> Option<T> {
match self.0.try_recv() {
Ok(v) => Some(v),
Err(TryRecvError::Disconnected) => panic!("bevy channel should never disconnect"),
Err(TryRecvError::Empty) => None,
}
}
}

View File

@@ -1,43 +1,57 @@
use super::{
client_state::{ClientState, CLIENT_STATES},
destroy_queue,
client_state::{CLIENT_STATES, ClientStateParsed},
scenegraph::Scenegraph,
};
use crate::{
core::{registry::OwnedRegistry, task},
nodes::{audio, data, drawable, fields, hmd, input, items, root::Root, spatial, Node},
nodes::{
Node, audio, drawable, fields, input, items,
root::{ClientState, Root},
spatial,
},
};
use color_eyre::eyre::{eyre, Result};
use color_eyre::eyre::{Result, eyre};
use global_counter::primitive::exact::CounterU32;
use lazy_static::lazy_static;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use rustc_hash::FxHashMap;
use stardust_xr::{
messenger::{self, MessageSenderHandle},
schemas::flex::serialize,
use stardust_xr::messenger::{self, MessageSenderHandle};
use std::{
fmt::Debug,
fs,
iter::FromIterator,
path::PathBuf,
sync::{Arc, OnceLock},
time::Instant,
};
use std::{fmt::Debug, fs, iter::FromIterator, path::PathBuf, sync::Arc};
use tokio::{net::UnixStream, task::JoinHandle};
use tokio::{net::UnixStream, sync::watch, task::JoinHandle};
use tracing::info;
pub static CLIENTS: OwnedRegistry<Client> = OwnedRegistry::new();
lazy_static! {
pub static ref CLIENTS: OwnedRegistry<Client> = OwnedRegistry::new();
static ref INTERNAL_CLIENT_MESSAGE_TIMES: (watch::Sender<Instant>, watch::Receiver<Instant>) = watch::channel(Instant::now());
pub static ref INTERNAL_CLIENT: Arc<Client> = CLIENTS.add(Client {
pid: None,
// env: None,
exe: None,
dispatch_join_handle: OnceCell::new(),
flush_join_handle: OnceCell::new(),
disconnect_status: OnceCell::new(),
dispatch_join_handle: OnceLock::new(),
flush_join_handle: OnceLock::new(),
disconnect_status: OnceLock::new(),
id_counter: CounterU32::new(0),
message_last_received: INTERNAL_CLIENT_MESSAGE_TIMES.1.clone(),
message_sender_handle: None,
scenegraph: Default::default(),
root: OnceCell::new(),
root: OnceLock::new(),
base_resource_prefixes: Default::default(),
state: Arc::new(ClientState::default()),
state: OnceLock::default(),
});
}
pub fn tick_internal_client() {
let _ = INTERNAL_CLIENT_MESSAGE_TIMES.0.send(Instant::now());
}
pub fn get_env(pid: i32) -> Result<FxHashMap<String, String>, std::io::Error> {
let env = fs::read_to_string(format!("/proc/{pid}/environ"))?;
@@ -47,7 +61,7 @@ pub fn get_env(pid: i32) -> Result<FxHashMap<String, String>, std::io::Error> {
.map(|(k, v)| (k.to_string(), v.to_string())),
))
}
pub fn state(env: &FxHashMap<String, String>) -> Option<Arc<ClientState>> {
pub fn state(env: &FxHashMap<String, String>) -> Option<Arc<ClientStateParsed>> {
let token = env.get("STARDUST_STARTUP_TOKEN")?;
CLIENT_STATES.lock().get(token).cloned()
}
@@ -56,21 +70,23 @@ pub struct Client {
pub pid: Option<i32>,
// env: Option<FxHashMap<String, String>>,
exe: Option<PathBuf>,
dispatch_join_handle: OnceCell<JoinHandle<Result<()>>>,
flush_join_handle: OnceCell<JoinHandle<Result<()>>>,
disconnect_status: OnceCell<Result<()>>,
dispatch_join_handle: OnceLock<JoinHandle<Result<()>>>,
flush_join_handle: OnceLock<JoinHandle<Result<()>>>,
disconnect_status: OnceLock<Result<()>>,
id_counter: CounterU32,
message_last_received: watch::Receiver<Instant>,
pub message_sender_handle: Option<MessageSenderHandle>,
pub scenegraph: Arc<Scenegraph>,
pub root: OnceCell<Arc<Root>>,
pub root: OnceLock<Arc<Root>>,
pub base_resource_prefixes: Mutex<Vec<PathBuf>>,
pub state: Arc<ClientState>,
pub state: OnceLock<ClientState>,
}
impl Client {
pub fn from_connection(connection: UnixStream) -> Result<Arc<Self>> {
let pid = connection.peer_cred().ok().and_then(|c| c.pid());
let env = pid.and_then(|pid| get_env(pid).ok());
let exe = pid.and_then(|pid| fs::read_link(format!("/proc/{}/exe", pid)).ok());
let exe = pid.and_then(|pid| fs::read_link(format!("/proc/{pid}/exe")).ok());
info!(
pid,
exe = exe
@@ -84,40 +100,37 @@ impl Client {
let state = env
.as_ref()
.and_then(state)
.unwrap_or_else(|| Arc::new(ClientState::default()));
.unwrap_or_else(|| Arc::new(ClientStateParsed::default()));
let (message_time_tx, message_last_received) = watch::channel(Instant::now());
let client = CLIENTS.add(Client {
pid,
// env,
exe: exe.clone(),
dispatch_join_handle: OnceCell::new(),
flush_join_handle: OnceCell::new(),
disconnect_status: OnceCell::new(),
dispatch_join_handle: OnceLock::new(),
flush_join_handle: OnceLock::new(),
disconnect_status: OnceLock::new(),
id_counter: CounterU32::new(256),
message_last_received,
message_sender_handle: Some(messenger_tx.handle()),
scenegraph: scenegraph.clone(),
root: OnceCell::new(),
root: OnceLock::new(),
base_resource_prefixes: Default::default(),
state,
state: OnceLock::default(),
});
let _ = client.scenegraph.client.set(Arc::downgrade(&client));
let _ = client.root.set(Root::create(&client)?);
hmd::make_alias(&client)?;
let _ = client.root.set(Root::create(&client, state.root)?);
spatial::create_interface(&client)?;
fields::create_interface(&client)?;
drawable::create_interface(&client)?;
audio::create_interface(&client)?;
data::create_interface(&client)?;
items::create_interface(&client)?;
input::create_interface(&client)?;
items::camera::create_interface(&client)?;
items::panel::create_interface(&client)?;
client
.root
.get()
.unwrap()
.node
.send_remote_signal("restore_state", serialize(client.state.apply_to(&client))?)?;
let _ = client.state.set(state.apply_to(&client));
let pid_printable = pid
.map(|pid| pid.to_string())
@@ -129,46 +142,38 @@ impl Client {
.map(|exe| exe.to_string())
})
.unwrap_or_else(|| "??".to_string());
let _ = client.dispatch_join_handle.get_or_try_init(|| {
let _ = client.dispatch_join_handle.get_or_init(|| {
task::new(
|| {
format!(
"client dispatch pid={} exe={}",
&pid_printable, &exe_printable,
)
},
|| format!("Stardust client \"{exe_printable}\" dispatch, pid={pid_printable}"),
{
let client = client.clone();
async move {
loop {
match messenger_rx.dispatch(&*scenegraph).await {
Err(e) => {
client.disconnect(Err(e.into()));
}
_ => (),
if let Err(e) = messenger_rx.dispatch(&*scenegraph).await {
client.disconnect(Err(e.into()));
}
let _ = message_time_tx.send(Instant::now());
}
}
},
)
.unwrap()
});
let _ = client.flush_join_handle.get_or_try_init(|| {
let _ = client.flush_join_handle.get_or_init(|| {
task::new(
|| format!("client flush pid={} exe={}", &pid_printable, &exe_printable,),
|| format!("Stardust client \"{exe_printable}\" flush, pid={pid_printable}"),
{
let client = client.clone();
async move {
loop {
match messenger_tx.flush().await {
Err(e) => {
client.disconnect(Err(e.into()));
}
_ => (),
if let Err(e) = messenger_tx.flush().await {
client.disconnect(Err(e.into()));
}
}
}
},
)
.unwrap()
});
Ok(client)
@@ -190,18 +195,29 @@ impl Client {
let cwd_proc_path = format!("/proc/{pid}/cwd");
std::fs::read_link(cwd_proc_path).ok()
}
pub async fn save_state(&self) -> Option<ClientState> {
pub async fn save_state(&self) -> Option<ClientStateParsed> {
println!("start save state");
let internal = self.root.get()?.save_state().await.ok()?;
Some(ClientState::from_deserialized(self, internal))
println!("finished save state");
Some(ClientStateParsed::from_deserialized(self, internal))
}
pub fn generate_id(&self) -> u64 {
self.id_counter.inc() as u64
}
#[inline]
pub fn get_node(&self, name: &'static str, path: &str) -> Result<Arc<Node>> {
pub fn get_node(&self, name: &'static str, id: u64) -> Result<Arc<Node>> {
self.scenegraph
.get_node(path)
.get_node(id)
.ok_or_else(|| eyre!("{} not found", name))
}
pub fn unresponsive(&self) -> bool {
let time_since_last_message = self.message_last_received.borrow().elapsed();
time_since_last_message.as_millis() > 500
}
pub fn disconnect(&self, reason: Result<()>) {
let _ = self.disconnect_status.set(reason);
if let Some(dispatch_join_handle) = self.dispatch_join_handle.get() {
@@ -210,9 +226,7 @@ impl Client {
if let Some(flush_join_handle) = self.flush_join_handle.get() {
flush_join_handle.abort();
}
if let Some(client) = CLIENTS.remove(self) {
destroy_queue::add(client);
}
CLIENTS.remove(self);
}
}
impl Debug for Client {

View File

@@ -1,5 +1,5 @@
use super::client::{get_env, Client};
use crate::nodes::{spatial::Spatial, Node};
use super::client::{Client, get_env};
use crate::nodes::{Node, root::ClientState, spatial::Spatial};
use glam::Mat4;
use parking_lot::Mutex;
use rustc_hash::FxHashMap;
@@ -11,7 +11,7 @@ use std::{
};
lazy_static::lazy_static! {
pub static ref CLIENT_STATES: Mutex<FxHashMap<String, Arc<ClientState>>> = Default::default();
pub static ref CLIENT_STATES: Mutex<FxHashMap<String, Arc<ClientStateParsed>>> = Default::default();
}
#[derive(Debug, Serialize, Deserialize)]
@@ -31,28 +31,28 @@ impl LaunchInfo {
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ClientState {
pub struct ClientStateParsed {
pub launch_info: Option<LaunchInfo>,
pub data: Vec<u8>,
#[serde(skip)]
pub data: Option<Vec<u8>>,
pub root: Mat4,
pub spatial_anchors: FxHashMap<String, Mat4>,
}
impl ClientState {
pub fn from_deserialized(client: &Client, state: ClientStateInternal) -> Self {
ClientState {
impl ClientStateParsed {
pub fn from_deserialized(client: &Client, state: ClientState) -> Self {
ClientStateParsed {
launch_info: LaunchInfo::from_client(client),
data: state.data.unwrap_or_default(),
root: Self::spatial_transform(client, &state.root.unwrap_or_default())
.unwrap_or_default(),
data: state.data,
root: Self::spatial_transform(client, state.root).unwrap_or_default(),
spatial_anchors: state
.spatial_anchors
.into_iter()
.filter_map(|(k, v)| Some((k, Self::spatial_transform(client, &v)?)))
.filter_map(|(k, v)| Some((k, Self::spatial_transform(client, v)?)))
.collect(),
}
}
fn spatial_transform(client: &Client, path: &str) -> Option<Mat4> {
let node = client.scenegraph.get_node(path)?;
fn spatial_transform(client: &Client, id: u64) -> Option<Mat4> {
let node = client.scenegraph.get_node(id)?;
let spatial = node.get_aspect::<Spatial>().ok()?;
Some(spatial.global_transform())
}
@@ -64,39 +64,40 @@ impl ClientState {
}
pub fn from_file(file: &Path) -> Option<Self> {
let file_string = std::fs::read_to_string(file).ok()?;
toml::from_str(&file_string).ok()
let mut client_state: Self = toml::from_str(&file_string).ok()?;
client_state.data = std::fs::read(file.with_extension("bin")).ok();
Some(client_state)
}
pub fn to_file(self, directory: &Path) {
pub fn to_file(&self, directory: &Path) {
let app_name = self
.launch_info
.as_ref()
.map(|l| l.cmdline.get(0).unwrap().split('/').last().unwrap())
.map(|l| l.cmdline.first().unwrap().split('/').next_back().unwrap())
.unwrap_or("unknown");
let state_file_path = directory
.join(format!("{app_name}-{}", nanoid::nanoid!()))
.with_extension("toml");
let state_file_prefix = directory.join(format!("{app_name}-{}", nanoid::nanoid!()));
let state_metadata_path = state_file_prefix.with_extension("toml");
let state_data_path = state_file_prefix.with_extension("bin");
std::fs::write(state_file_path, toml::to_string(&self).unwrap()).unwrap();
std::fs::write(state_metadata_path, toml::to_string(&self).unwrap()).unwrap();
if let Some(data) = self.data.as_deref() {
std::fs::write(state_data_path, data).unwrap();
}
}
pub fn apply_to(&self, client: &Arc<Client>) -> ClientStateInternal {
pub fn apply_to(&self, client: &Arc<Client>) -> ClientState {
if let Some(root) = client.root.get() {
root.set_transform(self.root)
}
ClientStateInternal {
data: Some(self.data.clone()),
root: Some("/".to_string()),
ClientState {
data: self.data.clone(),
root: 0,
spatial_anchors: self
.spatial_anchors
.iter()
.map(|(k, v)| {
(k.clone(), {
let node = Node::create_parent_name(client, "/spatial/anchor", k, true)
.add_to_scenegraph()
.unwrap();
Spatial::add_to(&node, None, *v, false);
k.clone()
})
let node = Node::generate(client, true).add_to_scenegraph().unwrap();
Spatial::add_to(&node, None, *v, false);
(k.clone(), node.get_id())
})
.collect(),
}
@@ -112,20 +113,13 @@ impl ClientState {
Some(command)
}
}
impl Default for ClientState {
impl Default for ClientStateParsed {
fn default() -> Self {
Self {
launch_info: None,
data: Default::default(),
data: None,
root: Mat4::IDENTITY,
spatial_anchors: Default::default(),
}
}
}
#[derive(Default, Serialize, Deserialize)]
pub struct ClientStateInternal {
data: Option<Vec<u8>>,
root: Option<String>,
spatial_anchors: FxHashMap<String, String>,
}

10
src/core/color.rs Normal file
View File

@@ -0,0 +1,10 @@
use stardust_xr::values::color::{AlphaColor, Rgb, color_space::LinearRgb};
pub trait ColorConvert {
fn to_bevy(&self) -> bevy::color::Color;
}
impl ColorConvert for AlphaColor<f32, Rgb<f32, LinearRgb>> {
fn to_bevy(&self) -> bevy::color::Color {
bevy::color::Color::linear_rgba(self.c.r, self.c.g, self.c.b, self.a)
}
}

View File

@@ -1,22 +0,0 @@
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use std::any::Any;
use tokio::sync::mpsc::{self, unbounded_channel};
static MAIN_DESTROY_QUEUE: Lazy<(
mpsc::UnboundedSender<Box<dyn Any + Send + Sync>>,
Mutex<mpsc::UnboundedReceiver<Box<dyn Any + Send + Sync>>>,
)> = Lazy::new(|| {
let (tx, rx) = unbounded_channel();
(tx, Mutex::new(rx))
});
pub fn add<T: Any + Sync + Send>(thing: T) {
MAIN_DESTROY_QUEUE.0.send(Box::new(thing)).unwrap();
}
pub fn clear() {
while let Ok(thing) = MAIN_DESTROY_QUEUE.1.lock().try_recv() {
drop(thing)
}
}

60
src/core/entity_handle.rs Normal file
View File

@@ -0,0 +1,60 @@
use std::ops::Deref;
use std::sync::Arc;
use bevy::prelude::*;
use crate::nodes::spatial::SpatialNode;
use super::bevy_channel::{BevyChannel, BevyChannelReader};
pub struct EntityHandlePlugin;
impl Plugin for EntityHandlePlugin {
fn build(&self, app: &mut App) {
DESTROY.init(app);
app.add_systems(PreUpdate, despawn);
}
}
fn despawn(
mut cmds: Commands,
mut reader: ResMut<BevyChannelReader<Entity>>,
child_query: Query<&Children>,
has_spatial: Query<Has<SpatialNode>>,
) {
while let Some(e) = reader.read() {
if let Ok(children) = child_query.get(e) {
for e in children {
if has_spatial.get(*e).unwrap_or_default() {
cmds.entity(*e).try_remove::<ChildOf>();
}
}
}
cmds.entity(e).despawn();
}
}
#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct EntityHandle(Arc<EntityHandleInner>);
impl EntityHandle {
pub fn get(&self) -> Entity {
self.0.0
}
pub fn new(entity: Entity) -> Self {
Self(EntityHandleInner(entity).into())
}
}
impl Deref for EntityHandle {
type Target = Entity;
fn deref(&self) -> &Self::Target {
&self.0.0
}
}
static DESTROY: BevyChannel<Entity> = BevyChannel::new();
#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct EntityHandleInner(Entity);
impl Drop for EntityHandleInner {
fn drop(&mut self) {
DESTROY.send(self.0);
}
}

72
src/core/error.rs Normal file
View File

@@ -0,0 +1,72 @@
use std::any::TypeId;
use color_eyre::eyre::Report;
use stardust_xr::{
messenger::MessengerError,
schemas::flex::{
FlexSerializeError,
flexbuffers::{DeserializationError, ReaderError},
},
};
use thiserror::Error;
pub type Result<T, E = ServerError> = std::result::Result<T, E>;
#[derive(Error, Debug)]
pub enum ServerError {
#[error("Internal: Unable to get client")]
NoClient,
#[error("Messenger does not exist for this node")]
NoMessenger,
#[error("Messenger error: {0}")]
MessengerError(#[from] MessengerError),
#[error("Remote method error: {0}")]
RemoteMethodError(String),
#[error("Serialization error: {0}")]
SerializationError(#[from] FlexSerializeError),
#[error("Deserialization error: {0}")]
DeserializationError(#[from] DeserializationError),
#[error("Reader error: {0}")]
ReaderError(#[from] ReaderError),
#[error("Aspect {} does not exist for node", 0.to_string())]
NoAspect(TypeId),
#[error("{0}")]
Report(#[from] Report),
}
#[macro_export]
macro_rules! bail {
($msg:literal $(,)?) => {
return Err($crate::core::error::ServerError::from(color_eyre::eyre::eyre!($msg)));
};
($err:expr $(,)?) => {
return Err($crate::core::error::ServerError::from(color_eyre::eyre::eyre!($err)));
};
($fmt:expr, $($arg:tt)*) => {
return Err($crate::core::error::ServerError::from(color_eyre::eyre::eyre!($fmt, $($arg)*)));
};
}
#[macro_export]
macro_rules! ensure {
($cond:expr $(,)?) => {
if !$cond {
$crate::ensure!($cond, concat!("Condition failed: `", stringify!($cond), "`"))
}
};
($cond:expr, $msg:literal $(,)?) => {
if !$cond {
return Err($crate::core::error::ServerError::from(color_eyre::eyre::eyre!($msg)));
}
};
($cond:expr, $err:expr $(,)?) => {
if !$cond {
return Err($crate::core::error::ServerError::from(color_eyre::eyre::eyre!($err)));
}
};
($cond:expr, $fmt:expr, $($arg:tt)*) => {
if !$cond {
return Err($crate::core::error::ServerError::from(color_eyre::eyre::eyre!($fmt, $($arg)*)));
}
};
}

View File

@@ -1,36 +0,0 @@
use super::client::Client;
use super::task;
use color_eyre::eyre::Result;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::net::UnixListener;
use tokio::task::JoinHandle;
use tracing::error;
pub struct EventLoop {
join_handle: JoinHandle<()>,
}
impl EventLoop {
pub fn new(socket_path: PathBuf) -> Result<Arc<Self>> {
let socket = UnixListener::bind(socket_path)?;
let join_handle = task::new(|| "event loop", async move {
loop {
let Ok((socket, _)) = socket.accept().await else { continue };
if let Err(e) = Client::from_connection(socket) {
error!(?e, "Unable to create client from connection");
}
}
})?;
let event_loop = Arc::new(EventLoop { join_handle });
Ok(event_loop)
}
}
impl Drop for EventLoop {
fn drop(&mut self) {
self.join_handle.abort();
}
}

View File

@@ -1,11 +0,0 @@
#[macro_export]
macro_rules! create_interface {
($iface:ident, $aspect:ident, $path:expr) => {
pub fn create_interface(client: &Arc<Client>) -> Result<()> {
let node = Node::create_path(client, $path, false);
<$iface as $aspect>::add_node_members(&node);
node.add_to_scenegraph()?;
Ok(())
}
};
}

View File

@@ -1,10 +1,10 @@
pub mod bevy_channel;
pub mod client;
pub mod client_state;
pub mod color;
pub mod delta;
pub mod destroy_queue;
pub mod eventloop;
pub mod idl_utils;
pub mod node_collections;
pub mod entity_handle;
pub mod error;
pub mod registry;
pub mod resource;
pub mod scenegraph;

View File

@@ -1,83 +0,0 @@
use crate::nodes::Node;
use parking_lot::Mutex;
use rustc_hash::FxHashMap;
use std::{
borrow::Borrow,
hash::Hash,
sync::{Arc, Weak},
};
// #[derive(Default)]
// pub struct LifeLinkedNodeList {
// nodes: Mutex<Vec<Weak<Node>>>,
// }
// impl LifeLinkedNodeList {
// pub fn add(&self, node: Weak<Node>) {
// self.nodes.lock().push(node);
// }
// pub fn clear(&self) {
// self.nodes
// .lock()
// .iter()
// .filter_map(|node| node.upgrade())
// .for_each(|node| {
// node.destroy();
// });
// self.nodes.lock().clear();
// }
// }
// impl Drop for LifeLinkedNodeList {
// fn drop(&mut self) {
// self.clear();
// }
// }
#[derive(Default, Debug)]
pub struct LifeLinkedNodeMap<K: Hash + Eq> {
nodes: Mutex<FxHashMap<K, Weak<Node>>>,
}
#[allow(dead_code)]
impl<K: Hash + Eq> LifeLinkedNodeMap<K> {
pub fn add(&self, key: K, node: &Arc<Node>) {
self.nodes.lock().insert(key, Arc::downgrade(node));
}
pub fn get<Q>(&self, key: &Q) -> Option<Arc<Node>>
where
Q: ?Sized,
K: Borrow<Q>,
Q: Hash + Eq,
{
self.nodes.lock().get(key).and_then(|n| n.upgrade())
}
pub fn nodes(&self) -> Vec<Arc<Node>> {
self.nodes
.lock()
.values()
.filter_map(|v| v.upgrade())
.collect()
}
pub fn remove<Q>(&self, key: &Q) -> Option<Arc<Node>>
where
Q: ?Sized,
K: Borrow<Q>,
Q: Hash + Eq,
{
self.nodes.lock().remove(key).and_then(|n| n.upgrade())
}
pub fn clear(&self) {
let mut nodes = self.nodes.lock();
nodes
.values()
.filter_map(|node| node.upgrade())
.for_each(|node| {
node.destroy();
});
nodes.clear();
}
}
impl<K: Hash + Eq> Drop for LifeLinkedNodeMap<K> {
fn drop(&mut self) {
self.clear();
}
}

View File

@@ -1,21 +1,18 @@
#![allow(dead_code)]
use parking_lot::{const_mutex, MappedMutexGuard, Mutex, MutexGuard};
use dashmap::DashMap;
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard, const_mutex};
use rustc_hash::FxHashMap;
use std::ops::Deref;
use std::ptr;
use std::sync::{Arc, Weak};
use std::sync::{Arc, LazyLock, Weak};
#[derive(Debug)]
pub struct Registry<T: Send + Sync + ?Sized>(Mutex<Option<FxHashMap<usize, Weak<T>>>>);
pub struct Registry<T: Send + Sync + ?Sized>(MaybeLazy<DashMap<usize, Weak<T>>>);
impl<T: Send + Sync + ?Sized> Registry<T> {
pub const fn new() -> Self {
Registry(const_mutex(None))
}
fn lock(&self) -> MappedMutexGuard<FxHashMap<usize, Weak<T>>> {
MutexGuard::map(self.0.lock(), |r| {
r.get_or_insert_with(|| FxHashMap::default())
})
Registry(MaybeLazy::Lazy(LazyLock::new(DashMap::default)))
}
pub fn add(&self, t: T) -> Arc<T>
where
@@ -26,49 +23,78 @@ impl<T: Send + Sync + ?Sized> Registry<T> {
t_arc
}
pub fn add_raw(&self, t: &Arc<T>) {
self.lock()
self.0
.insert(Arc::as_ptr(t) as *const () as usize, Arc::downgrade(t));
}
pub fn contains(&self, t: &T) -> bool {
self.lock()
self.0
.contains_key(&(ptr::addr_of!(*t) as *const () as usize))
}
pub fn get_changes(old: &Registry<T>, new: &Registry<T>) -> (Vec<Arc<T>>, Vec<Arc<T>>) {
let mut added = Vec::new();
let mut removed = Vec::new();
for pair in new.0.iter() {
let (id, entry) = pair.pair();
if let Some(entry) = entry.upgrade()
&& !old.0.contains_key(id)
{
added.push(entry);
}
}
for pair in old.0.iter() {
let (id, entry) = pair.pair();
if let Some(entry) = entry.upgrade()
&& !new.0.contains_key(id)
{
removed.push(entry);
}
}
(added, removed)
}
pub fn get_valid_contents(&self) -> Vec<Arc<T>> {
self.lock()
self.0
.iter()
.filter_map(|pair| pair.1.upgrade())
.filter_map(|pair| pair.value().upgrade())
.collect()
}
pub fn set(&self, other: &Registry<T>) {
self.clear();
for (key, value) in other.0.deref().clone().into_iter() {
self.0.insert(key, value);
}
}
pub fn take_valid_contents(&self) -> Vec<Arc<T>> {
self.0
.lock()
.take()
.unwrap_or_default()
.into_iter()
.filter_map(|pair| pair.1.upgrade())
.collect()
let contents = self.get_valid_contents();
self.0.clear();
contents
}
pub fn retain<F: Fn(&Arc<T>) -> bool>(&self, f: F) {
self.0.retain(|_, v| {
let Some(v) = v.upgrade() else {
// why would we want to retain things we can't upgrade?
return true;
};
(f)(&v)
})
}
pub fn remove(&self, t: &T) {
self.lock()
.remove(&(ptr::addr_of!(*t) as *const () as usize));
self.0.remove(&(ptr::addr_of!(*t) as *const () as usize));
}
pub fn clear(&self) {
self.lock().clear();
self.0.clear();
}
pub fn is_empty(&self) -> bool {
let registry = self.0.lock();
let Some(registry) = &*registry else {
return true;
};
if registry.is_empty() {
if self.0.is_empty() {
return true;
}
registry.values().all(|v| v.strong_count() == 0)
self.0.iter().all(|v| v.value().strong_count() == 0)
}
}
impl<T: Send + Sync + ?Sized> Clone for Registry<T> {
fn clone(&self) -> Self {
Self(Mutex::new(self.0.lock().clone()))
Self(self.0.clone())
}
}
impl<T: Send + Sync + ?Sized> Default for Registry<T> {
@@ -77,16 +103,48 @@ impl<T: Send + Sync + ?Sized> Default for Registry<T> {
}
}
impl<T: Send + Sync + Sized> FromIterator<Arc<T>> for Registry<T> {
fn from_iter<I: IntoIterator<Item = Arc<T>>>(iter: I) -> Self {
Registry(MaybeLazy::NonLazy(
iter.into_iter()
.map(|i| (Arc::as_ptr(&i) as usize, Arc::downgrade(&i)))
.collect(),
))
}
}
#[derive(Debug)]
enum MaybeLazy<T> {
Lazy(LazyLock<T>),
NonLazy(T),
}
impl<T: Clone> Clone for MaybeLazy<T> {
fn clone(&self) -> Self {
match self {
MaybeLazy::Lazy(lazy_lock) => Self::NonLazy(lazy_lock.deref().clone()),
MaybeLazy::NonLazy(v) => Self::NonLazy(v.clone()),
}
}
}
impl<T> Deref for MaybeLazy<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
MaybeLazy::Lazy(lazy_lock) => lazy_lock,
MaybeLazy::NonLazy(v) => v,
}
}
}
pub struct OwnedRegistry<T: Send + Sync + ?Sized>(Mutex<Option<FxHashMap<usize, Arc<T>>>>);
impl<T: Send + Sync + ?Sized> OwnedRegistry<T> {
pub const fn new() -> Self {
OwnedRegistry(const_mutex(None))
}
fn lock(&self) -> MappedMutexGuard<FxHashMap<usize, Arc<T>>> {
MutexGuard::map(self.0.lock(), |r| {
r.get_or_insert_with(|| FxHashMap::default())
})
fn lock(&self) -> MappedMutexGuard<'_, FxHashMap<usize, Arc<T>>> {
MutexGuard::map(self.0.lock(), |r| r.get_or_insert_with(FxHashMap::default))
}
pub fn add(&self, t: T) -> Arc<T>
where

View File

@@ -1,13 +1,16 @@
use stardust_xr::values::ResourceID;
use std::{ffi::OsStr, path::PathBuf};
use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
use super::client::Client;
lazy_static::lazy_static! {
static ref THEMES: Vec<PathBuf> = std::env::var("STARDUST_THEMES").map(|s| s.split(":").map(PathBuf::from).collect()).unwrap_or_default();
static ref THEMES: Vec<PathBuf> = std::env::var("STARDUST_THEMES").map(|s| s.split(':').map(PathBuf::from).collect()).unwrap_or_default();
}
fn has_extension(path: &PathBuf, extensions: &[&OsStr]) -> bool {
fn has_extension(path: &Path, extensions: &[&OsStr]) -> bool {
if let Some(path_extension) = path.extension() {
extensions.contains(&path_extension)
} else {

View File

@@ -1,25 +1,84 @@
use crate::nodes::alias::Alias;
use crate::nodes::Node;
use crate::{core::client::Client, nodes::Message};
use color_eyre::eyre::Result;
use once_cell::sync::OnceCell;
use crate::{
core::{
client::Client,
error::{Result, ServerError},
},
nodes::{Message, Node, alias::get_original},
};
use parking_lot::Mutex;
use portable_atomic::Ordering;
use rustc_hash::FxHashMap;
use serde::Serialize;
use stardust_xr::scenegraph;
use stardust_xr::scenegraph::ScenegraphError;
use stardust_xr::schemas::flex::serialize;
use std::future::Future;
use std::os::fd::OwnedFd;
use std::sync::{Arc, Weak};
use tokio::sync::oneshot;
use stardust_xr::{
messenger::MethodResponse,
scenegraph::{self, ScenegraphError},
schemas::flex::serialize,
};
use std::{
os::fd::OwnedFd,
sync::{Arc, OnceLock, Weak},
};
use tracing::{debug, debug_span};
pub struct MethodResponseSender(pub(crate) MethodResponse);
impl MethodResponseSender {
pub fn send_err(self, error: ScenegraphError) {
self.0.send(Err(error));
}
pub fn send<T: Serialize>(self, result: Result<T, ServerError>) {
let data = match result {
Ok(d) => d,
Err(e) => {
self.0.send(Err(ScenegraphError::MemberError {
error: e.to_string(),
}));
return;
}
};
let Ok(serialized) = stardust_xr::schemas::flex::serialize(data) else {
self.0.send(Err(ScenegraphError::MemberError {
error: "Internal: Failed to serialize".to_string(),
}));
return;
};
self.0.send(Ok((&serialized, Vec::<OwnedFd>::new())));
}
pub fn wrap<T: Serialize, F: FnOnce() -> Result<T>>(self, f: F) {
self.send(f())
}
pub fn wrap_async<T: Serialize>(
self,
f: impl Future<Output = Result<(T, Vec<OwnedFd>)>> + Send + 'static,
) {
tokio::task::spawn(async move {
let (value, fds) = match f.await {
Ok(d) => d,
Err(e) => {
self.0.send(Err(ScenegraphError::MemberError {
error: e.to_string(),
}));
return;
}
};
let Ok(serialized) = serialize(value) else {
self.0.send(Err(ScenegraphError::MemberError {
error: "Internal: Failed to serialize".to_string(),
}));
return;
};
self.0.send(Ok((&serialized, fds)));
});
}
}
impl std::fmt::Debug for MethodResponseSender {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TypedMethodResponse").finish()
}
}
#[derive(Default)]
pub struct Scenegraph {
pub(super) client: OnceCell<Weak<Client>>,
nodes: Mutex<FxHashMap<String, Arc<Node>>>,
pub(super) client: OnceLock<Weak<Client>>,
nodes: Mutex<FxHashMap<u64, Arc<Node>>>,
}
impl Scenegraph {
@@ -34,79 +93,37 @@ impl Scenegraph {
}
pub fn add_node_raw(&self, node: Arc<Node>) {
debug!(node = ?&*node, "Add node");
let path = node.get_path().to_string();
self.nodes.lock().insert(path, node);
self.nodes.lock().insert(node.get_id(), node);
}
pub fn get_node(&self, path: &str) -> Option<Arc<Node>> {
let mut node = self.nodes.lock().get(path)?.clone();
while let Ok(alias) = node.get_aspect::<Alias>() {
if alias.enabled.load(Ordering::Acquire) {
node = alias.original.upgrade()?;
} else {
return None;
}
}
Some(node)
pub fn get_node(&self, node: u64) -> Option<Arc<Node>> {
let node = self.nodes.lock().get(&node)?.clone();
get_original(node, true)
}
pub fn remove_node(&self, path: &str) -> Option<Arc<Node>> {
debug!(path, "Remove node");
self.nodes.lock().remove(path)
pub fn remove_node(&self, node: u64) -> Option<Arc<Node>> {
debug!(node, "Remove node");
self.nodes.lock().remove(&node)
}
}
pub struct MethodResponseSender(oneshot::Sender<Result<(Vec<u8>, Vec<OwnedFd>), ScenegraphError>>);
impl MethodResponseSender {
pub fn send(self, t: Result<Message, ScenegraphError>) {
let _ = self.0.send(t.map(|m| (m.data, m.fds)));
}
// pub fn send_method_return<T: Serialize>(
// self,
// result: color_eyre::eyre::Result<(T, Vec<OwnedFd>)>,
// ) {
// let _ = self.0.send(map_method_return(result));
// }
pub fn wrap_sync<F: FnOnce() -> color_eyre::eyre::Result<Message>>(self, f: F) {
self.send(f().map_err(|e| ScenegraphError::MethodError {
error: e.to_string(),
}))
}
pub fn wrap_async<T: Serialize>(
self,
f: impl Future<Output = color_eyre::eyre::Result<(T, Vec<OwnedFd>)>> + Send + 'static,
) {
tokio::task::spawn(async move { self.0.send(map_method_return(f.await)) });
}
}
fn map_method_return<T: Serialize>(
result: color_eyre::eyre::Result<(T, Vec<OwnedFd>)>,
) -> Result<(Vec<u8>, Vec<OwnedFd>), ScenegraphError> {
let (value, fds) = result.map_err(|e| ScenegraphError::MethodError {
error: e.to_string(),
})?;
let serialized_value = serialize(value).map_err(|e| ScenegraphError::MethodError {
error: format!("Internal: Serialization failed: {e}"),
})?;
Ok((serialized_value, fds))
}
impl scenegraph::Scenegraph for Scenegraph {
fn send_signal(
&self,
path: &str,
method: &str,
node_id: u64,
aspect_id: u64,
method: u64,
data: &[u8],
fds: Vec<OwnedFd>,
) -> Result<(), ScenegraphError> {
let Some(client) = self.get_client() else {
return Err(ScenegraphError::SignalNotFound);
return Err(ScenegraphError::NodeNotFound);
};
debug_span!("Handle signal", path, method).in_scope(|| {
self.get_node(path)
debug_span!("Handle signal", aspect_id, node_id, method).in_scope(|| {
self.get_node(node_id)
.ok_or(ScenegraphError::NodeNotFound)?
.send_local_signal(
client,
aspect_id,
method,
Message {
data: data.to_vec(),
@@ -117,23 +134,25 @@ impl scenegraph::Scenegraph for Scenegraph {
}
fn execute_method(
&self,
path: &str,
method: &str,
node_id: u64,
aspect_id: u64,
method: u64,
data: &[u8],
fds: Vec<OwnedFd>,
response: oneshot::Sender<Result<(Vec<u8>, Vec<OwnedFd>), ScenegraphError>>,
response: MethodResponse,
) {
let Some(client) = self.get_client() else {
let _ = response.send(Err(ScenegraphError::MethodNotFound));
response.send(Err(ScenegraphError::NodeNotFound));
return;
};
debug!(path, method, "Handle method");
let Some(node) = self.get_node(path) else {
let _ = response.send(Err(ScenegraphError::NodeNotFound));
debug!(aspect_id, node_id, method, "Handle method");
let Some(node) = self.get_node(node_id) else {
response.send(Err(ScenegraphError::NodeNotFound));
return;
};
node.execute_local_method(
client,
aspect_id,
method,
Message {
data: data.to_vec(),

View File

@@ -1,4 +1,3 @@
use color_eyre::eyre::Result;
use std::future::Future;
use tokio::task::JoinHandle;
@@ -11,7 +10,7 @@ pub fn new<
>(
name_fn: F,
async_future: A,
) -> Result<JoinHandle<O>> {
) -> std::io::Result<JoinHandle<O>> {
#[cfg(not(feature = "profile_tokio"))]
let result = Ok(tokio::task::spawn(async_future));
#[cfg(feature = "profile_tokio")]

View File

@@ -1,57 +1,124 @@
#![recursion_limit = "256"]
#![allow(clippy::empty_docs)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::type_complexity)]
mod core;
mod nodes;
mod objects;
mod session;
mod spectator_cam;
pub mod tracking_offset;
#[cfg(feature = "wayland")]
mod wayland;
use crate::core::client::CLIENTS;
use crate::core::destroy_queue;
use crate::nodes::items::camera;
use crate::nodes::{audio, drawable, hmd, input};
use crate::objects::input::eye_pointer::EyePointer;
use crate::objects::input::mouse_pointer::MousePointer;
use crate::objects::input::sk_controller::SkController;
use crate::objects::input::sk_hand::SkHand;
use crate::objects::play_space::PlaySpace;
use crate::wayland::X_DISPLAY;
use crate::nodes::drawable::sky::SkyPlugin;
use crate::nodes::input;
use crate::spectator_cam::SpectatorCameraPlugin;
use self::core::eventloop::EventLoop;
use clap::Parser;
use core::client_state::ClientState;
use directories::ProjectDirs;
use once_cell::sync::OnceCell;
use stardust_xr::server;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::Arc;
use std::time::Duration;
use stereokit::{
named_colors::BLACK, DepthMode, DisplayMode, Handed, LogLevel, StereoKitMultiThread,
TextureFormat, TextureType,
use bevy::MinimalPlugins;
use bevy::a11y::AccessibilityPlugin;
use bevy::app::{App, ScheduleRunnerPlugin, TerminalCtrlCHandlerPlugin};
use bevy::asset::{AssetMetaCheck, UnapprovedPathMode};
use bevy::audio::AudioPlugin;
use bevy::core_pipeline::CorePipelinePlugin;
use bevy::core_pipeline::oit::OrderIndependentTransparencySettings;
use bevy::core_pipeline::tonemapping::Tonemapping;
use bevy::diagnostic::DiagnosticsPlugin;
use bevy::ecs::schedule::{ExecutorKind, ScheduleLabel};
use bevy::gizmos::GizmoPlugin;
use bevy::gltf::GltfPlugin;
use bevy::input::InputPlugin;
use bevy::pbr::PbrPlugin;
use bevy::render::pipelined_rendering::{
PipelinedRenderThreadOnCreateCallback, PipelinedRenderingPlugin,
};
use stereokit::{DisplayBlend, Sk};
use bevy::render::settings::{Backends, RenderCreation, WgpuSettings};
use bevy::render::{RenderDebugFlags, RenderPlugin};
use bevy::scene::ScenePlugin;
use bevy::window::{CompositeAlphaMode, PresentMode};
use bevy::winit::{WakeUp, WinitPlugin};
use bevy_dmabuf::import::DmabufImportPlugin;
use bevy_mod_openxr::action_set_attaching::OxrActionAttachingPlugin;
use bevy_mod_openxr::action_set_syncing::OxrActionSyncingPlugin;
use bevy_mod_openxr::add_xr_plugins;
use bevy_mod_openxr::exts::OxrExtensions;
use bevy_mod_openxr::features::overlay::OxrOverlaySettings;
use bevy_mod_openxr::graphics::{GraphicsBackend, OxrManualGraphicsConfig};
use bevy_mod_openxr::init::{OxrInitPlugin, should_run_frame_loop};
use bevy_mod_openxr::reference_space::OxrReferenceSpacePlugin;
use bevy_mod_openxr::render::{OxrRenderPlugin, OxrWaitFrameSystem};
use bevy_mod_openxr::resources::{OxrFrameState, OxrFrameWaiter, OxrSessionConfig};
use bevy_mod_openxr::types::AppInfo;
use bevy_mod_xr::camera::XrProjection;
use bevy_mod_xr::session::{XrFirst, XrHandleEvents, XrSessionPlugin};
use clap::Parser;
use core::client::{Client, tick_internal_client};
use core::entity_handle::EntityHandlePlugin;
use core::task;
use directories::ProjectDirs;
use nodes::audio::AudioNodePlugin;
use nodes::drawable::lines::LinesNodePlugin;
use nodes::drawable::model::ModelNodePlugin;
use nodes::drawable::text::TextNodePlugin;
use nodes::fields::FieldDebugGizmoPlugin;
use nodes::spatial::SpatialNodePlugin;
use objects::hmd::HmdPlugin;
use objects::input::mouse_pointer::FlatscreenInputPlugin;
use objects::input::oxr_controller::ControllerPlugin;
use objects::input::oxr_hand::HandPlugin;
use objects::play_space::PlaySpacePlugin;
use openxr::{EnvironmentBlendMode, ReferenceSpaceType};
use session::{launch_start, save_session};
use stardust_xr::schemas::dbus::object_registry::ObjectRegistry;
use stardust_xr::server::LockedSocket;
use std::ops::DerefMut as _;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{Arc, OnceLock};
use tokio::net::UnixListener;
use tokio::sync::Notify;
use tokio::task::LocalSet;
use tokio::{runtime::Handle, sync::oneshot};
use tokio::task::JoinError;
use tracing::metadata::LevelFilter;
use tracing::{debug_span, error, info};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use tracing::{error, info};
use tracing_subscriber::filter::Directive;
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
use tracking_offset::TrackingOffsetPlugin;
#[cfg(feature = "wayland")]
use wayland::{Wayland, WaylandPlugin};
use zbus::Connection;
use zbus::fdo::ObjectManager;
#[derive(Parser)]
use bevy::prelude::*;
#[derive(Debug, Clone, Parser)]
#[clap(author, version, about, long_about = None)]
struct CliArgs {
/// Force flatscreen mode and use the mouse pointer as a 3D pointer
#[clap(short, long, action)]
flatscreen: bool,
force_flatscreen: bool,
/// Replaces the flatscreen mode with a first person spectator camera
#[clap(short, long, action)]
spectator: bool,
/// Creates a transparent window fot the flatscreen mode
#[clap(short, long, action)]
transparent_flatscreen: bool,
/// If monado insists on emulating them, set this flag...we want the raw input
#[clap(long)]
disable_controllers: bool,
/// If monado insists on emulating , set this flag...we want the raw input
#[clap(long)]
disable_hands: bool,
/// Run Stardust XR as an overlay with given priority
#[clap(id = "PRIORITY", short = 'o', long = "overlay", action)]
overlay_priority: Option<u32>,
/// Don't create a tip input for controller because SOME RUNTIMES will lie
#[clap(long, action)]
disable_controller: bool,
/// Debug the clients started by the server
#[clap(short = 'd', long = "debug", action)]
debug_launched_clients: bool,
/// Run a script when ready for clients to connect. If this is not set the script at $HOME/.config/stardust/startup will be ran if it exists.
#[clap(id = "PATH", short = 'e', long = "execute-startup-script", action)]
@@ -62,411 +129,397 @@ struct CliArgs {
restore: Option<String>,
}
static STARDUST_INSTANCE: OnceCell<String> = OnceCell::new();
static SK_MULTITHREAD: OnceCell<Sk> = OnceCell::new();
static STOP_NOTIFIER: Notify = Notify::const_new();
pub type BevyMaterial = StandardMaterial;
struct EventLoopInfo {
tokio_handle: Handle,
socket_path: PathBuf,
}
static STARDUST_INSTANCE: OnceLock<String> = OnceLock::new();
fn main() {
ctrlc::set_handler(|| {
if atty::isnt(atty::Stream::Stdout) {
STOP_NOTIFIER.notify_waiters()
}
})
.unwrap();
// #[tokio::main(flavor = "current_thread")]
#[tokio::main]
async fn main() -> Result<AppExit, JoinError> {
color_eyre::install().unwrap();
let registry = tracing_subscriber::registry();
#[cfg(feature = "profile_app")]
let registry = registry.with(tracing_tracy::TracyLayer::new().with_filter(LevelFilter::DEBUG));
let registry = registry.with(
tracing_tracy::TracyLayer::new(tracing_tracy::DefaultConfig::default())
.with_filter(LevelFilter::DEBUG),
);
#[cfg(feature = "profile_tokio")]
let (console_layer, _) = console_subscriber::ConsoleLayer::builder().build();
#[cfg(feature = "profile_tokio")]
let registry = registry.with(console_layer);
let registry = registry.with(console_subscriber::spawn());
let log_layer = fmt::Layer::new()
.with_thread_names(true)
.with_ansi(true)
.with_line_number(true)
.with_filter(EnvFilter::from_default_env());
.with_filter(
EnvFilter::builder()
.with_default_directive(LevelFilter::WARN.into())
.from_env_lossy()
.add_directive(Directive::from_str("bevy_mesh_text_3d::text_glyphs=off").unwrap()),
);
registry.with(log_layer).init();
let cli_args = CliArgs::parse();
let locked_socket =
LockedSocket::get_free().expect("Unable to find a free stardust socket path");
STARDUST_INSTANCE.set(locked_socket.socket_path.file_name().unwrap().to_string_lossy().into_owned()).expect("Someone hasn't done their job, yell at Nova because how is this set multiple times what the hell");
info!(
socket_path = ?locked_socket.socket_path.display(),
"Stardust socket created"
);
let socket = UnixListener::bind(locked_socket.socket_path)
.expect("Couldn't spawn stardust server at {socket_path}");
task::new(|| "Stardust socket accept loop", async move {
loop {
let Ok((stream, _)) = socket.accept().await else {
continue;
};
if let Err(e) = Client::from_connection(stream) {
error!(?e, "Unable to create client from connection");
}
}
})
.unwrap();
info!("Init client join loop");
let project_dirs = ProjectDirs::from("", "", "stardust");
if project_dirs.is_none() {
error!("Unable to get Stardust project directories, default skybox and startup script will not work.");
}
let cli_args = Arc::new(CliArgs::parse());
let sk = stereokit::Settings {
app_name: "Stardust XR".to_string(),
display_preference: if cli_args.flatscreen {
DisplayMode::Flatscreen
} else {
DisplayMode::MixedReality
},
blend_preference: DisplayBlend::AnyTransparent,
depth_mode: DepthMode::D32,
log_filter: match EnvFilter::from_default_env().max_level_hint() {
Some(LevelFilter::ERROR) => LogLevel::Error,
Some(LevelFilter::WARN) => LogLevel::Warning,
Some(LevelFilter::INFO) => LogLevel::Inform,
Some(LevelFilter::DEBUG) => LogLevel::Diagnostic,
Some(LevelFilter::TRACE) => LogLevel::Diagnostic,
Some(LevelFilter::OFF) => LogLevel::None,
None => LogLevel::Warning,
},
overlay_app: cli_args.overlay_priority.is_some(),
overlay_priority: cli_args.overlay_priority.unwrap_or(u32::MAX),
disable_desktop_input_window: true,
render_scaling: 2.0,
..Default::default()
}
.init()
.expect("StereoKit failed to initialize");
let _ = SK_MULTITHREAD.set(sk.multithreaded());
info!("Init StereoKit");
sk.render_set_multisample(0);
sk.material_set_shader(
sk.material_find("default/material_pbr").unwrap(),
sk.shader_find("default/shader_pbr_clip").unwrap(),
);
// Skytex/light stuff
{
if let Some((light, tex)) = project_dirs
.as_ref()
.and_then(|dirs| {
let skytex_path = dirs.config_dir().join("skytex.hdr");
skytex_path
.exists()
.then(|| sk.tex_create_cubemap_file(&skytex_path, true, 100).ok())
})
.flatten()
{
sk.render_set_skytex(&tex);
sk.render_set_skylight(light);
} else {
sk.render_set_skytex(sk.tex_gen_color(
BLACK,
1,
1,
TextureType::CUBEMAP,
TextureFormat::RGBA32,
));
}
error!(
"Unable to get Stardust project directories, default skybox and startup script will not work."
);
}
let mut mouse_pointer = cli_args
.flatscreen
.then(MousePointer::new)
.transpose()
.unwrap();
let mut hands = (!cli_args.flatscreen)
.then(|| {
let left = SkHand::new(Handed::Left).ok();
let right = SkHand::new(Handed::Right).ok();
left.zip(right)
})
.flatten();
let mut controllers = (!cli_args.flatscreen && !cli_args.disable_controller)
.then(|| {
let left = SkController::new(&sk, Handed::Left).ok();
let right = SkController::new(&sk, Handed::Right).ok();
left.zip(right)
})
.flatten();
let eye_pointer = (sk.active_display_mode() == DisplayMode::MixedReality
&& sk.device_has_eye_gaze())
.then(EyePointer::new)
.transpose()
.unwrap();
let dbus_connection = Connection::session()
.await
.expect("Could not open dbus session");
// why is this requested here? should there be a specific server bus name that we check
// instead?
dbus_connection
.request_name("org.stardustxr.HMD")
.await
.expect(
"Another instance of the server is running. This is not supported currently (but is planned).",
);
if hands.is_none() {
sk.input_hand_visible(Handed::Left, false);
sk.input_hand_visible(Handed::Right, false);
}
dbus_connection
.object_server()
.at("/", ObjectManager)
.await
.expect("Couldn't add the object manager");
let play_space = sk
.world_has_bounds()
.then(|| PlaySpace::new().ok())
.flatten();
let (info_sender, info_receiver) = oneshot::channel::<EventLoopInfo>();
let event_thread = std::thread::Builder::new()
.name("event_loop".to_owned())
.spawn({
let project_dirs = project_dirs.clone();
move || event_loop(info_sender, project_dirs.clone())
})
.unwrap();
let event_loop_info = info_receiver.blocking_recv().unwrap();
let _tokio_handle = event_loop_info.tokio_handle.enter();
let object_registry = ObjectRegistry::new(&dbus_connection).await;
#[cfg(feature = "wayland")]
let mut wayland = wayland::Wayland::new().expect("Could not initialize wayland");
info!("Stardust ready!");
let _wayland = Wayland::new().expect("Couldn't create Wayland instance");
let ready_notifier = Arc::new(Notify::new());
let io_loop = tokio::task::spawn_blocking({
let ready_notifier = ready_notifier.clone();
let project_dirs = project_dirs.clone();
let cli_args = cli_args.clone();
let dbus_connection = dbus_connection.clone();
move || {
bevy_loop(
ready_notifier,
project_dirs,
cli_args,
dbus_connection,
object_registry,
)
}
});
ready_notifier.notified().await;
let mut startup_children = project_dirs
.as_ref()
.map(|project_dirs| {
launch_start(
&cli_args,
project_dirs,
&event_loop_info,
#[cfg(feature = "wayland")]
&wayland,
)
})
.map(|project_dirs| launch_start(&cli_args, project_dirs))
.unwrap_or_default();
let mut last_frame_delta = Duration::ZERO;
let mut sleep_duration = Duration::ZERO;
debug_span!("StereoKit").in_scope(|| {
sk.run(
|sk| {
let _span = debug_span!("StereoKit step");
let _span = _span.enter();
hmd::frame(sk);
camera::update(sk);
#[cfg(feature = "wayland")]
wayland.frame_event(sk);
destroy_queue::clear();
if let Some(mouse_pointer) = &mut mouse_pointer {
mouse_pointer.update(sk);
}
if let Some((left_hand, right_hand)) = &mut hands {
left_hand.update(!cli_args.disable_controller, sk);
right_hand.update(!cli_args.disable_controller, sk);
}
if let Some((left_controller, right_controller)) = &mut controllers {
left_controller.update(sk);
right_controller.update(sk);
}
if let Some(eye_pointer) = &eye_pointer {
eye_pointer.update(sk);
}
if let Some(play_space) = &play_space {
play_space.update(sk);
}
input::process_input();
nodes::root::Root::send_frame_events(sk.time_elapsed_unscaled());
adaptive_sleep(
sk,
&mut last_frame_delta,
&mut sleep_duration,
Duration::from_micros(250),
);
#[cfg(feature = "wayland")]
wayland.update(sk);
drawable::draw(sk);
audio::update(sk);
#[cfg(feature = "wayland")]
wayland.make_context_current();
},
|_sk| {
info!("Cleanly shut down StereoKit");
},
)
});
#[cfg(feature = "wayland")]
drop(wayland);
STOP_NOTIFIER.notify_waiters();
event_thread
.join()
.expect("Failed to cleanly shut down event loop")
.unwrap();
let return_value = io_loop.await;
info!("Stopping...");
if let Some(project_dirs) = project_dirs {
save_session(&project_dirs).await;
}
for mut startup_child in startup_children.drain(..) {
let _ = startup_child.kill();
}
info!("Cleanly shut down Stardust");
return_value
}
fn adaptive_sleep(
sk: &impl StereoKitMultiThread,
last_frame_delta: &mut Duration,
sleep_duration: &mut Duration,
sleep_duration_increase: Duration,
) {
let frame_delta = Duration::from_secs_f64(sk.time_elapsed_unscaled());
if *last_frame_delta < frame_delta {
if let Some(frame_delta_delta) = frame_delta.checked_sub(*last_frame_delta) {
if let Some(new_sleep_duration) = sleep_duration.checked_sub(frame_delta_delta) {
*sleep_duration = new_sleep_duration;
}
}
} else {
*sleep_duration += sleep_duration_increase;
}
// static DEFAULT_SKYTEX: OnceLock<Tex> = OnceLock::new();
// static DEFAULT_SKYLIGHT: OnceLock<SphericalHarmonics> = OnceLock::new();
debug_span!("Sleep", ?sleep_duration, ?frame_delta, ?last_frame_delta).in_scope(|| {
*last_frame_delta = frame_delta;
std::thread::sleep(*sleep_duration); // to give clients a chance to even update anything before drawing
#[derive(ScheduleLabel, Hash, Debug, PartialEq, Eq, Clone, Copy)]
pub struct PreFrameWait;
#[derive(Resource, Deref)]
pub struct ObjectRegistryRes(Arc<ObjectRegistry>);
#[derive(Resource, Deref)]
pub struct DbusConnection(Connection);
fn bevy_loop(
ready_notifier: Arc<Notify>,
_project_dirs: Option<ProjectDirs>,
args: CliArgs,
dbus_connection: Connection,
object_registry: Arc<ObjectRegistry>,
) -> AppExit {
let mut app = App::new();
app.insert_resource(DbusConnection(dbus_connection));
app.insert_resource(OxrManualGraphicsConfig {
fallback_backend: GraphicsBackend::Vulkan(()),
vk_instance_exts: Vec::new(),
vk_device_exts: bevy_dmabuf::required_device_extensions(),
});
}
// #[tokio::main]
#[tokio::main(flavor = "current_thread")]
async fn event_loop(
info_sender: oneshot::Sender<EventLoopInfo>,
project_dirs: Option<ProjectDirs>,
) -> color_eyre::eyre::Result<()> {
let socket_path =
server::get_free_socket_path().expect("Unable to find a free stardust socket path");
STARDUST_INSTANCE.set(socket_path.file_name().unwrap().to_string_lossy().into_owned()).expect("Someone hasn't done their job, yell at Nova because how is this set multiple times what the hell");
let _event_loop = EventLoop::new(socket_path.clone()).expect("Couldn't create server socket");
info!("Init event loop");
info!(
socket_path = ?socket_path.display(),
"Stardust socket created"
);
let _ = info_sender.send(EventLoopInfo {
tokio_handle: Handle::current(),
socket_path,
app.add_plugins(AssetPlugin {
meta_check: AssetMetaCheck::Never,
unapproved_path_mode: UnapprovedPathMode::Allow,
..default()
});
STOP_NOTIFIER.notified().await;
println!("Stopping...");
if let Some(project_dirs) = project_dirs {
save_session(&project_dirs).await;
}
info!("Cleanly shut down event loop");
unsafe {
stereokit::sys::sk_quit();
}
Ok(())
}
fn launch_start(
cli_args: &CliArgs,
project_dirs: &ProjectDirs,
event_loop_info: &EventLoopInfo,
#[cfg(feature = "wayland")] wayland: &wayland::Wayland,
) -> Vec<Child> {
if let Some(session_id) = &cli_args.restore {
let session_dir = project_dirs.state_dir().unwrap().join(session_id);
return restore_session(&session_dir, event_loop_info, wayland);
}
let startup_script_path = cli_args
.startup_script
.clone()
.and_then(|p| p.canonicalize().ok())
.unwrap_or_else(|| project_dirs.config_dir().join("startup"));
run_script(&startup_script_path, event_loop_info, wayland)
}
fn restore_session(
session_dir: &Path,
event_loop_info: &EventLoopInfo,
#[cfg(feature = "wayland")] wayland: &wayland::Wayland,
) -> Vec<Child> {
let Ok(clients) = session_dir.read_dir() else {
return Vec::new();
};
clients
.filter_map(Result::ok)
.filter_map(|c| ClientState::from_file(&c.path()))
.filter_map(ClientState::launch_command)
.filter_map(|startup_command| {
run_client(
startup_command,
event_loop_info,
#[cfg(feature = "wayland")]
wayland,
)
let mut plugins = MinimalPlugins
.build()
.add(DiagnosticsPlugin)
.add(TransformPlugin)
.add(InputPlugin)
.add(AccessibilityPlugin);
plugins = plugins
.add(TerminalCtrlCHandlerPlugin)
// bevy_mod_openxr will replace this, TODO: figure out how to mix this with
// bevy-dmabuf
.add(RenderPlugin {
render_creation: RenderCreation::Automatic(WgpuSettings {
backends: Some(Backends::VULKAN),
..Default::default()
}),
..Default::default()
})
.collect()
}
fn run_script(
script_path: &Path,
event_loop_info: &EventLoopInfo,
#[cfg(feature = "wayland")] wayland: &wayland::Wayland,
) -> Vec<Child> {
let _ = std::fs::set_permissions(script_path, std::fs::Permissions::from_mode(0o755));
let startup_command = Command::new(script_path);
run_client(
startup_command,
event_loop_info,
#[cfg(feature = "wayland")]
wayland,
)
.map(|c| vec![c])
.unwrap_or_default()
}
fn run_client(
mut command: Command,
event_loop_info: &EventLoopInfo,
#[cfg(feature = "wayland")] wayland: &wayland::Wayland,
) -> Option<Child> {
command.stdin(Stdio::null());
command.stdout(Stdio::null());
command.stderr(Stdio::null());
command.env(
"FLAT_WAYLAND_DISPLAY",
std::env::var_os("WAYLAND_DISPLAY").unwrap_or_default(),
);
command.env(
"STARDUST_INSTANCE",
event_loop_info
.socket_path
.file_name()
.expect("Stardust socket path not found"),
);
.add(ImagePlugin::default())
.add(CorePipelinePlugin)
// theoretically we shouldn't need this because of bevy_sk, but everything is tangled in
// there and idk what we actually need to run
.add(PbrPlugin {
// this seems to only apply to StandardMaterial, we don't use that
prepass_enabled: true,
add_default_deferred_lighting_plugin: false,
use_gpu_instance_buffer_builder: true,
debug_flags: RenderDebugFlags::default(),
})
// required for gltf
.add(ScenePlugin)
.add(GltfPlugin::default())
// .add(AnimationPlugin)
.add(AudioPlugin::default())
.add(GizmoPlugin)
.add(WindowPlugin::default());
#[cfg(feature = "wayland")]
{
if let Some(wayland_socket) = wayland.socket_name.as_ref() {
command.env("WAYLAND_DISPLAY", &wayland_socket);
}
command.env(
"DISPLAY",
format!(":{}", X_DISPLAY.get().cloned().unwrap_or_default()),
);
command.env("GDK_BACKEND", "wayland");
command.env("QT_QPA_PLATFORM", "wayland");
command.env("MOZ_ENABLE_WAYLAND", "1");
command.env("CLUTTER_BACKEND", "wayland");
command.env("SDL_VIDEODRIVER", "wayland");
plugins = plugins.add(DmabufImportPlugin);
}
let child = command.spawn().ok()?;
Some(child)
let mut task_pool_plugin = TaskPoolPlugin::default();
// make tokio work
let handle = tokio::runtime::Handle::current();
let enter_runtime_context = Arc::new(move || {
// TODO: this might be a memory leak
std::mem::forget(handle.enter());
});
task_pool_plugin.task_pool_options.io.on_thread_spawn = Some(enter_runtime_context.clone());
task_pool_plugin.task_pool_options.compute.on_thread_spawn =
Some(enter_runtime_context.clone());
task_pool_plugin
.task_pool_options
.async_compute
.on_thread_spawn = Some(enter_runtime_context.clone());
plugins = plugins.set(task_pool_plugin);
if std::env::var("DISPLAY").is_ok_and(|s| !s.is_empty())
|| std::env::var("WAYLAND_DISPLAY").is_ok_and(|s| !s.is_empty())
{
let mut plugin = WinitPlugin::<WakeUp>::default();
plugin.run_on_any_thread = true;
plugins = plugins.add(plugin).disable::<ScheduleRunnerPlugin>();
plugins = match args.spectator {
true => plugins.add(SpectatorCameraPlugin),
false => plugins.add(FlatscreenInputPlugin),
};
}
app.insert_resource(PipelinedRenderThreadOnCreateCallback(
enter_runtime_context.clone(),
));
app.add_plugins(
if !args.force_flatscreen {
add_xr_plugins(plugins)
.set(OxrInitPlugin {
app_info: AppInfo {
name: "Stardust XR".into(),
version: bevy_mod_openxr::types::Version(0, 44, 1),
},
exts: {
// all OpenXR extensions can be requested here
let mut exts = OxrExtensions::default();
exts.enable_hand_tracking();
if args.overlay_priority.is_some() {
exts.enable_extx_overlay();
}
exts.khr_convert_timespec_time = true;
exts
},
..default()
})
.set(OxrRenderPlugin {
default_wait_frame: false,
..default()
})
.set(OxrReferenceSpacePlugin {
default_primary_ref_space: ReferenceSpaceType::LOCAL,
})
// Disable a bunch of unneeded plugins
// we don't do any action stuff that needs to integrate with the ecosystem
.disable::<OxrActionAttachingPlugin>()
.disable::<OxrActionSyncingPlugin>()
} else {
// enable a event
plugins = plugins.add(XrSessionPlugin { auto_handle: false });
bevy_dmabuf::wgpu_init::add_dmabuf_init_plugin(plugins)
}
.set(WindowPlugin {
primary_window: Some(Window {
transparent: args.transparent_flatscreen,
present_mode: PresentMode::AutoNoVsync,
composite_alpha_mode: if args.transparent_flatscreen {
CompositeAlphaMode::PreMultiplied
} else {
CompositeAlphaMode::Auto
},
title: "StardustXR server flatscreen mode".to_string(),
..default()
}),
..default()
}),
);
app.add_plugins(PipelinedRenderingPlugin);
app.add_plugins(bevy_sk::hand::HandPlugin);
app.add_plugins(bevy_equirect::EquirectangularPlugin);
// app.add_plugins(HandGizmosPlugin);
app.world_mut().resource_mut::<AmbientLight>().brightness = 1000.0;
if let Some(priority) = args.overlay_priority {
app.insert_resource(OxrOverlaySettings {
session_layer_placement: priority,
..default()
});
}
app.insert_resource(OxrSessionConfig {
blend_mode_preference: vec![
EnvironmentBlendMode::ALPHA_BLEND,
EnvironmentBlendMode::ADDITIVE,
EnvironmentBlendMode::OPAQUE,
],
..default()
});
let mut pre_frame_wait = Schedule::new(PreFrameWait);
pre_frame_wait.set_executor_kind(ExecutorKind::MultiThreaded);
app.add_schedule(pre_frame_wait);
app.insert_resource(ClearColor(Color::BLACK.with_alpha(0.0)));
app.insert_resource(ObjectRegistryRes(object_registry));
#[cfg(feature = "bevy_debugging")]
{
use bevy::remote::{RemotePlugin, http::RemoteHttpPlugin};
app.add_plugins((RemotePlugin::default(), RemoteHttpPlugin::default()));
}
// the Stardust server plugins
// infra plugins
app.add_plugins(EntityHandlePlugin);
// node plugins
app.add_plugins((
SpatialNodePlugin,
ModelNodePlugin,
TextNodePlugin,
LinesNodePlugin,
AudioNodePlugin,
// not really a node ig? at least for now
SkyPlugin,
));
// object plugins
app.add_plugins((PlaySpacePlugin, HandPlugin, ControllerPlugin, HmdPlugin));
// feature plugins
#[cfg(feature = "wayland")]
app.add_plugins(WaylandPlugin);
app.add_plugins((TrackingOffsetPlugin, FieldDebugGizmoPlugin));
app.add_systems(PostStartup, move || {
ready_notifier.notify_waiters();
});
app.add_observer(cam_settings);
app.add_systems(
XrFirst,
xr_step
.in_set(OxrWaitFrameSystem)
.in_set(XrHandleEvents::FrameLoop),
);
app.run()
}
async fn save_session(project_dirs: &ProjectDirs) {
let session_id = nanoid::nanoid!();
let state_dir = project_dirs.state_dir().unwrap();
let session_dir = state_dir.join(&session_id);
std::fs::create_dir_all(&session_dir).unwrap();
let _ = std::fs::remove_dir_all(state_dir.join("latest"));
std::os::unix::fs::symlink(&session_dir, state_dir.join("latest")).unwrap();
fn cam_settings(
trigger: Trigger<OnAdd, Camera3d>,
mut query: Query<(Entity, &mut Projection, &mut Msaa, &mut Tonemapping), With<Camera3d>>,
mut cmds: Commands,
) {
let Ok((entity, mut projection, mut msaa, mut tonemapping)) = query.get_mut(trigger.target())
else {
return;
};
info!("modifying cam");
match projection.deref_mut() {
Projection::Perspective(perspective_projection) => perspective_projection.near = 0.003,
Projection::Orthographic(orthographic_projection) => orthographic_projection.near = 0.003,
Projection::Custom(custom_projection) => {
if let Some(xr) = custom_projection.get_mut::<XrProjection>() {
xr.near = 0.003
} else {
error_once!("unknown custom camera projection");
}
}
}
*msaa = Msaa::Off;
*tonemapping = Tonemapping::None;
cmds.entity(entity)
.insert(OrderIndependentTransparencySettings::default());
}
let local_set = LocalSet::new();
for client in CLIENTS.get_vec() {
let session_dir = session_dir.clone();
local_set.spawn_local(async move {
tokio::select! {
biased;
s = client.save_state() => {s.map(|s| s.to_file(&session_dir));},
_ = tokio::time::sleep(Duration::from_millis(100)) => (),
fn xr_step(world: &mut World) {
// update things like the Xr input methods
world.run_schedule(PreFrameWait);
input::process_input();
let time = world.resource::<bevy::prelude::Time>().delta_secs_f64();
nodes::root::Root::send_frame_events(time);
let should_wait = world
.run_system_cached(should_run_frame_loop)
.unwrap_or(false);
// we might want to do an adaptive sleep when not OpenXR waiting
if should_wait {
world.resource_scope::<OxrFrameWaiter, _>(|world, mut waiter| {
let state = waiter
.wait()
.inspect_err(|err| error!("failed to wait OpenXR frame: {err}"))
.ok();
if let Some(state) = state {
world.insert_resource(OxrFrameState(state));
}
});
}
local_set.await;
println!("Session ID for restore is {session_id}");
tick_internal_client();
}
pub fn get_time(pipelined: bool, state: &OxrFrameState) -> openxr::Time {
if pipelined {
openxr::Time::from_nanos(
state.predicted_display_time.as_nanos() + state.predicted_display_period.as_nanos(),
)
} else {
state.predicted_display_time
}
}

View File

@@ -1,52 +1,163 @@
use super::{Aspect, Node};
use crate::core::client::Client;
use color_eyre::eyre::{ensure, Result};
use portable_atomic::AtomicBool;
use std::sync::{Arc, Weak};
use super::{Aspect, AspectIdentifier, Node};
use crate::core::{
client::Client, error::Result, registry::Registry, scenegraph::MethodResponseSender,
};
use std::{
ops::Add,
sync::{Arc, Weak},
};
#[derive(Debug, Default, Clone)]
pub struct AliasInfo {
pub(super) server_signals: Vec<&'static str>,
pub(super) server_methods: Vec<&'static str>,
pub(super) client_signals: Vec<&'static str>,
pub(super) server_signals: Vec<u64>,
pub(super) server_methods: Vec<u64>,
pub(super) client_signals: Vec<u64>,
}
impl Add for AliasInfo {
type Output = AliasInfo;
fn add(mut self, mut rhs: Self) -> Self::Output {
self.server_signals.append(&mut rhs.server_signals);
self.server_methods.append(&mut rhs.server_methods);
self.client_signals.append(&mut rhs.client_signals);
self
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub struct Alias {
pub enabled: Arc<AtomicBool>,
pub(super) node: Weak<Node>,
pub original: Weak<Node>,
pub info: AliasInfo,
pub(super) original: Weak<Node>,
pub(super) info: AliasInfo,
}
impl Alias {
pub fn create(
client: &Arc<Client>,
parent: &str,
name: &str,
original: &Arc<Node>,
client: &Arc<Client>,
info: AliasInfo,
list: Option<&AliasList>,
) -> Result<Arc<Node>> {
ensure!(
client
.scenegraph
.get_node(&(parent.to_string() + "/" + name))
.is_none(),
"Node already exists"
);
let node = Node::generate(client, true).add_to_scenegraph()?;
Self::add_to(&node, original, info)?;
if let Some(list) = list {
list.add(&node);
}
Ok(node)
}
pub fn create_with_id(
original: &Arc<Node>,
client: &Arc<Client>,
new_id: u64,
info: AliasInfo,
list: Option<&AliasList>,
) -> Result<Arc<Node>> {
let node = Node::from_id(client, new_id, true).add_to_scenegraph()?;
Self::add_to(&node, original, info)?;
if let Some(list) = list {
list.add(&node);
}
Ok(node)
}
let node = Node::create_parent_name(client, parent, name, true).add_to_scenegraph()?;
fn add_to(new_node: &Arc<Node>, original: &Arc<Node>, info: AliasInfo) -> Result<()> {
let alias = Alias {
enabled: Arc::new(AtomicBool::new(true)),
node: Arc::downgrade(&node),
node: Arc::downgrade(new_node),
original: Arc::downgrade(original),
info,
};
let alias = original.aliases.add(alias);
node.add_aspect_raw(alias);
Ok(node)
new_node.add_aspect_raw(alias);
Ok(())
}
}
impl Aspect for Alias {
const NAME: &'static str = "Alias";
impl AspectIdentifier for Alias {
const ID: u64 = 0;
}
impl Aspect for Alias {
fn run_signal(
&self,
_calling_client: Arc<Client>,
_node: Arc<Node>,
_signal: u64,
_message: super::Message,
) -> Result<(), stardust_xr::scenegraph::ScenegraphError> {
Ok(())
}
fn run_method(
&self,
_calling_client: Arc<Client>,
_node: Arc<Node>,
_method: u64,
_message: super::Message,
_response: MethodResponseSender,
) {
}
}
pub fn get_original(node: Arc<Node>, stop_on_disabled: bool) -> Option<Arc<Node>> {
let Ok(alias) = node.get_aspect::<Alias>() else {
return Some(node);
};
if stop_on_disabled && !node.enabled() {
return None;
}
get_original(alias.original.upgrade()?, stop_on_disabled)
}
pub fn links_to(alias: Arc<Node>, original: Weak<Node>) -> bool {
let Ok(alias) = alias.get_aspect::<Alias>() else {
return false;
};
if alias.original.ptr_eq(&original) {
return true;
}
let Some(original_strong) = alias.original.upgrade() else {
return false;
};
links_to(original_strong, original)
}
#[derive(Debug, Default, Clone)]
pub struct AliasList(Registry<Node>);
impl AliasList {
fn add(&self, node: &Arc<Node>) {
self.0.add_raw(node);
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn get_from_original_node(&self, original: Weak<Node>) -> Option<Arc<Node>> {
self.0
.get_valid_contents()
.into_iter()
.find(move |node| links_to(node.clone(), original.clone()))
}
pub fn get_from_aspect<A: AspectIdentifier>(&self, aspect: &A) -> Option<Arc<Node>> {
self.0.get_valid_contents().into_iter().find(|node| {
let Some(node) = get_original(node.clone(), false) else {
return false;
};
let Ok(aspect2) = node.get_aspect::<A>() else {
return false;
};
std::ptr::eq(Arc::as_ptr(&aspect2), aspect)
})
}
pub fn get_aliases(&self) -> Vec<Arc<Node>> {
self.0.get_valid_contents()
}
pub fn remove_aspect<A: AspectIdentifier>(&self, aspect: &A) {
self.0.retain(|node| {
let Some(original) = get_original(node.clone(), false) else {
return false;
};
let Ok(aspect2) = original.get_aspect::<A>() else {
return false;
};
!std::ptr::eq(Arc::as_ptr(&aspect2), aspect)
})
}
}
impl Drop for AliasList {
fn drop(&mut self) {
for node in self.0.take_valid_contents() {
node.destroy();
}
}
}

View File

@@ -1,32 +1,102 @@
use super::{Aspect, Node};
use super::spatial::SpatialNode;
use super::{Aspect, AspectIdentifier, Node};
use crate::core::client::Client;
use crate::core::destroy_queue;
use crate::core::entity_handle::EntityHandle;
use crate::core::error::Result;
use crate::core::registry::Registry;
use crate::core::resource::get_resource_file;
use crate::create_interface;
use crate::nodes::spatial::{Spatial, Transform};
use color_eyre::eyre::{eyre, Result};
use glam::{vec3, Vec4Swizzles};
use once_cell::sync::OnceCell;
use crate::nodes::spatial::{SPATIAL_ASPECT_ALIAS_INFO, Spatial, Transform};
use bevy::audio::{PlaybackMode, Volume};
use bevy_mod_openxr::session::OxrSession;
use bevy_mod_xr::session::{XrPreDestroySession, XrSessionCreated};
use bevy_mod_xr::spaces::XrSpace;
use color_eyre::eyre::eyre;
use parking_lot::Mutex;
use send_wrapper::SendWrapper;
use stardust_xr::values::ResourceID;
use std::ops::DerefMut;
use std::sync::Arc;
use bevy::prelude::*;
use bevy::transform::components::Transform as BevyTransform;
use std::sync::{Arc, OnceLock};
use std::{ffi::OsStr, path::PathBuf};
use stereokit::{Sound as SkSound, SoundInstance, StereoKitDraw};
pub struct AudioNodePlugin;
impl Plugin for AudioNodePlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, update_sound_event);
app.add_systems(XrSessionCreated, spawn_hmd_audio_listener);
app.add_systems(XrPreDestroySession, despawn_hmd_audio_listener);
}
}
fn despawn_hmd_audio_listener(mut cmds: Commands, session: Res<OxrSession>, res: Res<HmdListener>) {
cmds.remove_resource::<HmdListener>();
cmds.entity(res.0).despawn();
_ = session.destroy_space(res.1);
}
fn spawn_hmd_audio_listener(mut cmds: Commands, session: Res<OxrSession>) {
let space = session
.create_reference_space(openxr::ReferenceSpaceType::VIEW, BevyTransform::IDENTITY)
.unwrap();
let listener = cmds
.spawn((
Name::new("HMD audio listener"),
space.0,
SpatialListener::new(0.2),
))
.id();
cmds.insert_resource(HmdListener(listener, space.0));
}
#[derive(Resource)]
struct HmdListener(Entity, XrSpace);
fn update_sound_event(
mut cmds: Commands,
sinks: Query<&SpatialAudioSink>,
asset_server: Res<AssetServer>,
) {
for sound in SOUND_REGISTRY.get_valid_contents() {
if sound.entity.get().is_none() {
let handle = asset_server.load(sound.pending_audio_path.as_path());
let entity = cmds
.spawn((
Name::new("Audio Node"),
SpatialNode(Arc::downgrade(&sound.spatial)),
AudioPlayer::new(handle),
PlaybackSettings {
mode: PlaybackMode::Once,
volume: Volume::Linear(sound.volume),
speed: 1.0,
paused: true,
muted: false,
spatial: true,
spatial_scale: None,
},
))
.id();
let entity = EntityHandle::new(entity);
sound.spatial.set_entity(entity.clone());
sound.entity.set(entity).unwrap();
}
if let Some(sink) = sound.entity.get().and_then(|e| sinks.get(e.get()).ok()) {
if sound.play.lock().take().is_some() {
sink.play();
}
if sound.stop.lock().take().is_some() {
sink.stop();
}
}
}
}
static SOUND_REGISTRY: Registry<Sound> = Registry::new();
stardust_xr_server_codegen::codegen_audio_protocol!();
pub struct Sound {
space: Arc<Spatial>,
spatial: Arc<Spatial>,
volume: f32,
pending_audio_path: PathBuf,
sk_sound: OnceCell<SendWrapper<SkSound>>,
instance: Mutex<Option<SoundInstance>>,
entity: OnceLock<EntityHandle>,
stop: Mutex<Option<()>>,
play: Mutex<Option<()>>,
}
@@ -39,43 +109,23 @@ impl Sound {
)
.ok_or_else(|| eyre!("Resource not found"))?;
let sound = Sound {
space: node.get_aspect::<Spatial>().unwrap().clone(),
spatial: node.get_aspect::<Spatial>().unwrap().clone(),
volume: 1.0,
pending_audio_path,
sk_sound: OnceCell::new(),
instance: Mutex::new(None),
entity: OnceLock::new(),
stop: Mutex::new(None),
play: Mutex::new(None),
};
let sound_arc = SOUND_REGISTRY.add(sound);
node.add_aspect_raw(sound_arc.clone());
<Sound as SoundAspect>::add_node_members(node);
Ok(sound_arc)
}
fn update(&self, sk: &impl StereoKitDraw) {
let sound = self.sk_sound.get_or_init(|| {
SendWrapper::new(sk.sound_create(self.pending_audio_path.clone()).unwrap())
});
if self.stop.lock().take().is_some() {
if let Some(instance) = self.instance.lock().take() {
sk.sound_inst_stop(instance);
}
}
if self.instance.lock().is_none() && self.play.lock().take().is_some() {
self.instance.lock().replace(sk.sound_play(
sound.as_ref(),
vec3(0.0, 0.0, 0.0),
self.volume,
));
}
if let Some(instance) = self.instance.lock().deref_mut() {
sk.sound_inst_set_pos(*instance, self.space.global_transform().w_axis.xyz());
}
}
}
impl AspectIdentifier for Sound {
impl_aspect_for_sound_aspect_id! {}
}
impl Aspect for Sound {
const NAME: &'static str = "Sound";
impl_aspect_for_sound_aspect! {}
}
impl SoundAspect for Sound {
fn play(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
@@ -91,33 +141,21 @@ impl SoundAspect for Sound {
}
impl Drop for Sound {
fn drop(&mut self) {
if let Some(sk_sound) = self.sk_sound.take() {
destroy_queue::add(sk_sound);
}
SOUND_REGISTRY.remove(self);
}
}
pub fn update(sk: &impl StereoKitDraw) {
for sound in SOUND_REGISTRY.get_valid_contents() {
sound.update(sk)
}
}
create_interface!(AudioInterface, AudioInterfaceAspect, "/audio");
struct AudioInterface;
impl AudioInterfaceAspect for AudioInterface {
impl InterfaceAspect for Interface {
#[doc = "Create a sound node. WAV and MP3 are supported."]
fn create_sound(
_node: Arc<Node>,
calling_client: Arc<Client>,
name: String,
id: u64,
parent: Arc<Node>,
transform: Transform,
resource: ResourceID,
) -> Result<()> {
let node =
Node::create_parent_name(&calling_client, Self::CREATE_SOUND_PARENT_PATH, &name, true);
let node = Node::from_id(&calling_client, id, true);
let parent = parent.get_aspect::<Spatial>()?;
let transform = transform.to_mat4(true, true, true);
let node = node.add_to_scenegraph()?;

View File

@@ -1,288 +0,0 @@
use super::alias::AliasInfo;
use super::fields::Field;
use super::spatial::{parse_transform, Spatial};
use super::{Alias, Aspect, Node};
use crate::core::client::Client;
use crate::core::node_collections::LifeLinkedNodeMap;
use crate::core::registry::Registry;
use crate::create_interface;
use crate::nodes::fields::FIELD_ALIAS_INFO;
use crate::nodes::spatial::Transform;
use color_eyre::eyre::{bail, ensure, eyre, Result};
use lazy_static::lazy_static;
use nanoid::nanoid;
use parking_lot::Mutex;
use rustc_hash::FxHashMap;
use stardust_xr::schemas::flex::flexbuffers;
use stardust_xr::values::Datamap;
use std::sync::{Arc, Weak};
lazy_static! {
pub static ref KEYMAPS: Mutex<FxHashMap<String, String>> = Mutex::new(FxHashMap::default());
}
static PULSE_SENDER_REGISTRY: Registry<PulseSender> = Registry::new();
pub static PULSE_RECEIVER_REGISTRY: Registry<PulseReceiver> = Registry::new();
pub fn get_mask(datamap: &Datamap) -> Result<flexbuffers::MapReader<&[u8]>> {
flexbuffers::Reader::get_root(datamap.raw().as_slice())
.map_err(|_| eyre!("Mask is not a valid flexbuffer"))?
.get_map()
.map_err(|_| eyre!("Mask is not a valid map"))
}
pub fn mask_matches(mask_map_lesser: &Datamap, mask_map_greater: &Datamap) -> bool {
(|| -> Result<_> {
for key in get_mask(mask_map_lesser)?.iter_keys() {
let lesser_key = get_mask(mask_map_lesser)?.index(key)?;
let greater_key = get_mask(mask_map_greater)?.index(key)?;
// otherwise zero-length vectors don't count the same as a single type vector
if lesser_key.flexbuffer_type().is_heterogenous_vector()
&& lesser_key.as_vector().len() == 0
&& greater_key.flexbuffer_type().is_vector()
{
continue;
}
if !lesser_key.flexbuffer_type().is_null()
&& lesser_key.flexbuffer_type() != greater_key.flexbuffer_type()
{
return Err(flexbuffers::ReaderError::InvalidPackedType {}.into());
}
}
Ok(())
})()
.is_ok()
}
stardust_xr_server_codegen::codegen_data_protocol!();
pub struct PulseSender {
node: Weak<Node>,
pub mask: Datamap,
aliases: LifeLinkedNodeMap<String>,
}
impl PulseSender {
pub fn add_to(node: &Arc<Node>, mask: Datamap) -> Result<Arc<PulseSender>> {
let sender = PulseSender {
node: Arc::downgrade(node),
mask,
aliases: LifeLinkedNodeMap::default(),
};
// <PulseSender as PulseSenderAspect>::add_node_members(node);
let sender = PULSE_SENDER_REGISTRY.add(sender);
node.add_aspect_raw(sender.clone());
for receiver in PULSE_RECEIVER_REGISTRY.get_valid_contents() {
sender.handle_new_receiver(&receiver);
}
Ok(sender.clone())
}
fn handle_new_receiver(&self, receiver: &PulseReceiver) {
if !mask_matches(&self.mask, &receiver.mask) {
return;
}
let Some(tx_node) = self.node.upgrade() else {
return;
};
let Some(tx_client) = tx_node.get_client() else {
return;
};
let Some(rx_node) = receiver.node.upgrade() else {
return;
};
// Receiver itself
let Ok(rx_alias) = Alias::create(
&tx_client,
tx_node.get_path(),
receiver.uid.as_str(),
&rx_node,
AliasInfo {
server_methods: vec!["send_data"],
..Default::default()
},
) else {
return;
};
self.aliases.add(receiver.uid.clone(), &rx_alias);
// Receiver's field
let Ok(rx_field_alias) = Alias::create(
&tx_client,
rx_alias.get_path(),
"field",
&rx_node.get_aspect::<PulseReceiver>().unwrap().field_node,
FIELD_ALIAS_INFO.clone(),
) else {
return;
};
self.aliases
.add(receiver.uid.clone() + "-field", &rx_field_alias);
let _ =
pulse_sender_client::new_receiver(&tx_node, &receiver.uid, &rx_alias, &rx_field_alias);
}
fn handle_drop_receiver(&self, receiver: &PulseReceiver) {
let uid = receiver.uid.as_str();
self.aliases.remove(uid);
self.aliases.remove(&(uid.to_string() + "-field"));
let Some(tx_node) = self.node.upgrade() else {
return;
};
let _ = pulse_sender_client::drop_receiver(&tx_node, uid);
}
}
impl Aspect for PulseSender {
const NAME: &'static str = "PulseSender";
}
impl PulseSenderAspect for PulseSender {}
impl Drop for PulseSender {
fn drop(&mut self) {
PULSE_SENDER_REGISTRY.remove(self);
}
}
pub struct PulseReceiver {
uid: String,
pub node: Weak<Node>,
pub field_node: Arc<Node>,
pub mask: Datamap,
}
impl PulseReceiver {
pub fn add_to(
node: &Arc<Node>,
field_node: Arc<Node>,
mask: Datamap,
) -> Result<Arc<PulseReceiver>> {
let receiver = PulseReceiver {
uid: nanoid!(),
node: Arc::downgrade(node),
field_node,
mask,
};
let receiver = PULSE_RECEIVER_REGISTRY.add(receiver);
<PulseReceiver as PulseReceiverAspect>::add_node_members(node);
node.add_aspect_raw(receiver.clone());
for sender in PULSE_SENDER_REGISTRY.get_valid_contents() {
sender.handle_new_receiver(&receiver);
}
Ok(receiver)
}
}
impl Aspect for PulseReceiver {
const NAME: &'static str = "PulseReceiver";
}
impl PulseReceiverAspect for PulseReceiver {
fn send_data(
node: Arc<Node>,
_calling_client: Arc<Client>,
sender: Arc<Node>,
data: Datamap,
) -> Result<()> {
let this_receiver = node.get_aspect::<PulseReceiver>().unwrap();
ensure!(
mask_matches(&this_receiver.mask, &data),
"Message ({data:?}) does not contain the same keys as the receiver's mask ({:?})",
this_receiver.mask
);
pulse_receiver_client::data(&node, &sender.uid, &data)?;
Ok(())
}
}
impl Drop for PulseReceiver {
fn drop(&mut self) {
PULSE_RECEIVER_REGISTRY.remove(self);
for sender in PULSE_SENDER_REGISTRY.get_valid_contents() {
sender.handle_drop_receiver(self);
}
}
}
create_interface!(DataInterface, DataInterfaceAspect, "/data");
struct DataInterface;
impl DataInterfaceAspect for DataInterface {
fn create_pulse_sender(
_node: Arc<Node>,
calling_client: Arc<Client>,
name: String,
parent: Arc<Node>,
transform: Transform,
mask: Datamap,
) -> Result<()> {
get_mask(&mask)?;
let node = Node::create_parent_name(
&calling_client,
Self::CREATE_PULSE_SENDER_PARENT_PATH,
&name,
true,
);
let parent = parent.get_aspect::<Spatial>()?;
let transform = transform.to_mat4(true, true, false);
let node = node.add_to_scenegraph()?;
Spatial::add_to(&node, Some(parent.clone()), transform, false);
PulseSender::add_to(&node, mask)?;
Ok(())
}
fn create_pulse_receiver(
_node: Arc<Node>,
calling_client: Arc<Client>,
name: String,
parent: Arc<Node>,
transform: Transform,
field: Arc<Node>,
mask: Datamap,
) -> Result<()> {
get_mask(&mask)?;
let node = Node::create_parent_name(
&calling_client,
Self::CREATE_PULSE_RECEIVER_PARENT_PATH,
&name,
true,
);
let parent = parent.get_aspect::<Spatial>()?;
let transform = parse_transform(transform, true, true, false);
let _ = field.get_aspect::<Field>()?;
let node = node.add_to_scenegraph()?;
Spatial::add_to(&node, Some(parent.clone()), transform, false);
PulseReceiver::add_to(&node, field, mask)?;
Ok(())
}
async fn register_keymap(
_node: Arc<Node>,
_calling_client: Arc<Client>,
keymap: String,
) -> Result<String> {
let mut keymaps = KEYMAPS.lock();
if let Some(found_keymap_id) = keymaps
.iter()
.filter(|(_k, v)| *v == &keymap)
.map(|(k, _v)| k)
.last()
{
return Ok(found_keymap_id.clone());
}
let generated_id = nanoid!();
keymaps.insert(generated_id.clone(), keymap);
Ok(generated_id)
}
async fn get_keymap(
_node: Arc<Node>,
_calling_client: Arc<Client>,
keymap_id: String,
) -> Result<String> {
let keymaps = KEYMAPS.lock();
let Some(keymap) = keymaps.get(&keymap_id) else {
bail!("Could not find keymap. Try registering it")
};
Ok(keymap.clone())
}
}

View File

@@ -0,0 +1,5 @@
fn fragment(
in: VertexOutput
) -> FragmentOutput {
return vec4(0.0);
}

View File

@@ -0,0 +1,56 @@
#import bevy_pbr::{
pbr_fragment::pbr_input_from_standard_material,
pbr_functions::alpha_discard,
}
#ifdef PREPASS_PIPELINE
#import bevy_pbr::{
prepass_io::{VertexOutput, FragmentOutput},
pbr_deferred_functions::deferred_output,
}
#else
#import bevy_pbr::{
forward_io::{VertexOutput, FragmentOutput},
pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing},
}
#endif
#ifdef OIT_ENABLED
#import bevy_core_pipeline::oit::oit_draw
#endif
@fragment
fn fragment(
in: VertexOutput,
@builtin(front_facing) is_front: bool,
) -> FragmentOutput {
// generate a PbrInput struct from the StandardMaterial bindings
var pbr_input = pbr_input_from_standard_material(in, is_front);
#ifdef VERTEX_COLORS
// Multiply emissive color by vertex color
pbr_input.material.emissive *= vec4(in.color.rgb, 1.0);
#endif
// alpha discard
// pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color);
#ifdef PREPASS_PIPELINE
// in deferred mode we can't modify anything after that, as lighting is run in a separate fullscreen shader.
let out = deferred_output(in, pbr_input);
#else
var out: FragmentOutput;
// apply lighting
out.color = apply_pbr_lighting(pbr_input);
// apply in-shader post processing (fog, alpha-premultiply, and also tonemapping, debanding if the camera is non-hdr)
// note this does not include fullscreen postprocessing effects like bloom.
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
#endif
#ifdef OIT_ENABLED
oit_draw(in.position, out.color, false);
discard;
#else
return out;
#endif
}

View File

@@ -1,22 +1,329 @@
use super::{Line, LinesAspect};
use crate::{
core::{client::Client, registry::Registry},
nodes::{spatial::Spatial, Aspect, Node},
BevyMaterial,
core::{
client::Client, color::ColorConvert, entity_handle::EntityHandle, error::Result,
registry::Registry,
},
nodes::{Node, drawable::LinePoint, spatial::Spatial},
};
use color_eyre::eyre::Result;
use glam::Vec3A;
use bevy::{
asset::{AssetEvents, RenderAssetUsages, weak_handle},
pbr::{ExtendedMaterial, MaterialExtension},
prelude::*,
render::{
mesh::{Indices, PrimitiveTopology, VertexAttributeValues},
primitives::Aabb,
render_resource::{AsBindGroup, ShaderRef},
view::VisibilitySystems,
},
};
use glam::Vec3;
use parking_lot::Mutex;
use portable_atomic::{AtomicBool, Ordering};
use prisma::Lerp;
use std::{collections::VecDeque, sync::Arc};
use stereokit::{bounds_grow_to_fit_pt, Bounds, Color128, LinePoint as SkLinePoint, StereoKitDraw};
use std::sync::{
Arc, OnceLock,
atomic::{AtomicBool, Ordering},
};
use tokio::sync::Notify;
type LineMaterial = ExtendedMaterial<BevyMaterial, LineExtension>;
const LINE_SHADER_HANDLE: Handle<Shader> = weak_handle!("7d28aa5a-3abd-43bb-b0e9-0de8b81b650d");
// No extra data needed for a simple holdout
#[derive(Default, Asset, AsBindGroup, TypePath, Debug, Clone)]
#[data(50, u32, binding_array(101))]
#[bindless(index_table(range(50..51), binding(100)))]
pub struct LineExtension {}
impl From<&LineExtension> for u32 {
fn from(_: &LineExtension) -> Self {
0
}
}
impl MaterialExtension for LineExtension {
fn fragment_shader() -> ShaderRef {
LINE_SHADER_HANDLE.into()
}
fn prepass_fragment_shader() -> ShaderRef {
LINE_SHADER_HANDLE.into()
}
fn deferred_fragment_shader() -> ShaderRef {
LINE_SHADER_HANDLE.into()
}
fn alpha_mode() -> Option<AlphaMode> {
Some(AlphaMode::Blend)
}
}
pub struct LinesNodePlugin;
impl Plugin for LinesNodePlugin {
fn build(&self, app: &mut App) {
app.add_systems(
PostUpdate,
build_line_mesh
.after(TransformSystem::TransformPropagate)
.before(AssetEvents)
.after(VisibilitySystems::VisibilityPropagate)
.before(VisibilitySystems::CheckVisibility),
);
app.world_mut().resource_mut::<Assets<Shader>>().insert(
LINE_SHADER_HANDLE.id(),
Shader::from_wgsl(
include_str!("line.wgsl"),
std::path::Path::new(file!())
.parent()
.unwrap()
.join("line.wgsl")
.to_string_lossy(),
),
);
app.add_plugins(MaterialPlugin::<LineMaterial>::default());
}
}
fn build_line_mesh(
mut cmds: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<LineMaterial>>,
query: Query<(Ref<GlobalTransform>, &InheritedVisibility)>,
) {
for lines in LINES_REGISTRY.get_valid_contents().into_iter()
{
let Some((transform, visibil)) = lines.spatial.get_entity().and_then(|e| query.get(e).ok())
else {
continue;
};
if !(lines.gen_mesh.load(Ordering::Relaxed) || transform.is_changed()) {
continue;
}
lines.gen_mesh.store(false, Ordering::Relaxed);
let mut vertex_positions = Vec::<Vec3>::new();
let mut vertex_normals = Vec::<Vec3>::new();
let mut vertex_colors = Vec::<[f32; 4]>::new();
let mut vertex_indices = Vec::<u32>::new();
let lines_data = lines.data.lock();
if lines_data.is_empty() {
*lines.bounds.lock() = Some(Aabb::default());
lines.setup_complete.notify_waiters();
match lines.entity.get() {
Some(e) => cmds.entity(**e),
None => {
// if we couldn't get the lines entity then we need to gen the mesh later
lines.gen_mesh.store(true, Ordering::Relaxed);
continue;
}
}
.remove::<Mesh3d>();
continue;
}
let mut indices_set = 0;
for line in lines_data.iter() {
// yes this alloc is suboptimal, but good enough for now
let line_points = line
.points
.iter()
.map(|p: &LinePoint| LinePoint {
// point: transform.transform_point(p.point.into()).into(),
point: transform.transform_point(p.point.into()).into(),
thickness: p.thickness,
color: p.color,
})
.collect::<Vec<_>>();
let start_set = indices_set;
// Create a sliding window of points to process each segment of the line
// For cyclic lines: wraps around by connecting last point back to first
// For non-cyclic lines: handles endpoints with None values
let point_windows = {
let mut out = Vec::new();
let mut last = line.cyclic.then(|| line_points.last()).flatten();
let mut peekable = line_points.iter().peekable();
while let Some(curr) = peekable.next() {
// Skip this point if it has the same position as the previous point
if let Some(prev) = last
&& Vec3::from(prev.point) == Vec3::from(curr.point)
{
last = Some(curr);
continue;
}
let mut end = false;
// Determine the next point - either the next in sequence or
// for cyclic lines, wrap back to first point at the end
let next = match peekable.peek() {
Some(v) => Some(*v),
None => {
end = true;
line.cyclic.then(|| line_points.first()).flatten()
}
};
out.push((last, curr, next, end));
last = Some(curr);
}
out
};
// if we can't make a full line, don't bother trying
if point_windows.len() < 2 {
continue;
}
for (last, curr, next, last_point) in point_windows {
let last_quat = last.map(|v| {
Quat::from_rotation_arc(
Vec3::Y,
(Vec3::from(curr.point) - Vec3::from(v.point)).normalize(),
)
});
let next_quat = next.map(|v| {
Quat::from_rotation_arc(
Vec3::Y,
(Vec3::from(v.point) - Vec3::from(curr.point)).normalize(),
)
});
let quat = match (last_quat, next_quat) {
(None, None) => {
error!("no previous or next point in line");
break;
}
(None, Some(next)) => next,
(Some(last), None) => last,
(Some(last), Some(next)) => last.lerp(next, 0.5),
};
if !quat.is_finite() {
error!("non finite quat: next: {next:?}, last: {last:?}, curr: {curr:?},");
break;
}
let normals = [
Vec3::X,
Vec3::new(1., 0., 1.).normalize(),
Vec3::Z,
Vec3::new(-1., 0., 1.).normalize(),
Vec3::NEG_X,
Vec3::new(-1., 0., -1.).normalize(),
Vec3::NEG_Z,
Vec3::new(1., 0., -1.).normalize(),
]
.map(Vec3::normalize)
.map(|v| quat * v);
let points = normals.map(|v| (v * curr.thickness) + Vec3::from(curr.point));
vertex_normals.extend(normals);
vertex_positions.extend(points);
vertex_colors.extend([curr.color.to_bevy().to_linear().to_f32_array(); 8]);
// Only connect vertices between segments if this isn't the end point
if !last_point {
vertex_indices.extend(indices(indices_set));
}
indices_set += 1;
}
if indices_set > 0 {
// Handle the connection between start and end points:
// - For cyclic lines: connect last segment back to first
// - For non-cyclic lines: add caps at both ends
if line.cyclic {
vertex_indices.extend(cyclic_indices(start_set, indices_set - 1));
} else {
vertex_indices.extend(cap_indices(start_set, false));
vertex_indices.extend(cap_indices(indices_set - 1, true));
}
}
}
let mut mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::RENDER_WORLD,
);
mesh.insert_indices(Indices::U32(vertex_indices));
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, vertex_positions);
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, vertex_normals);
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, vertex_colors);
let mut entity = match lines.entity.get() {
Some(e) => cmds.entity(**e),
None => {
let e = cmds.spawn((
Name::new("LinesNode"),
MeshMaterial3d(materials.add(ExtendedMaterial {
base: BevyMaterial {
base_color: Color::WHITE,
perceptual_roughness: 1.0,
alpha_mode: AlphaMode::Premultiplied,
emissive: Color::linear_rgba(0.25, 0.25, 0.25, 1.0).into(),
..default()
},
extension: LineExtension {},
})),
));
_ = lines.entity.set(EntityHandle::new(e.id()));
e
}
};
if let Some(VertexAttributeValues::Float32x3(values)) =
mesh.attribute(Mesh::ATTRIBUTE_POSITION)
{
let global_to_local = transform.affine().inverse();
let local_aabb = Aabb::enclosing(
values
.iter()
.map(|p| global_to_local.transform_point3(Vec3::from_slice(p))),
)
.unwrap_or_default();
let global_aabb =
Aabb::enclosing(values.iter().map(|p| Vec3::from_slice(p))).unwrap_or_default();
*lines.bounds.lock() = Some(local_aabb);
lines.setup_complete.notify_waiters();
entity.insert(global_aabb);
}
entity
.insert(Mesh3d(meshes.add(mesh)))
.insert(*visibil)
.insert(match visibil.get() {
true => Visibility::Visible,
false => Visibility::Hidden,
});
}
}
const END_CAP_INDICES: [u32; 18] = [0, 1, 7, 7, 1, 2, 7, 2, 6, 6, 2, 3, 6, 3, 5, 5, 3, 4];
fn cap_indices(set: u32, flip: bool) -> [u32; END_CAP_INDICES.len()] {
let mut out = END_CAP_INDICES.map(|v| v + (set * 8));
if flip {
out.reverse();
}
out
}
// const BASE: [u16; 6] = [0, 8, 1, 8, 9, 1];
// Defines how vertices are connected between consecutive cross-sections to form the tube
const INDICES: [u32; 48] = [
0, 8, 1, 8, 9, 1, 1, 9, 2, 9, 10, 2, 2, 10, 3, 10, 11, 3, 3, 11, 4, 11, 12, 4, 4, 12, 5, 12,
13, 5, 5, 13, 6, 13, 14, 6, 6, 14, 7, 14, 15, 7, 7, 15, 0, 15, 8, 0,
];
fn indices(set: u32) -> [u32; INDICES.len()] {
INDICES.map(|v| v + (set * 8))
}
fn cyclic_indices(start_set: u32, end_set: u32) -> [u32; INDICES.len()] {
let mut out = INDICES.map(|v| {
if v < 8 {
v + ((start_set) * 8)
} else {
v + ((end_set - 1) * 8)
}
});
out.reverse();
out
}
static LINES_REGISTRY: Registry<Lines> = Registry::new();
pub struct Lines {
enabled: Arc<AtomicBool>,
space: Arc<Spatial>,
spatial: Arc<Spatial>,
data: Mutex<Vec<Line>>,
gen_mesh: AtomicBool,
entity: OnceLock<EntityHandle>,
bounds: Mutex<Option<Aabb>>,
setup_complete: Notify,
}
impl Lines {
pub fn add_to(node: &Arc<Node>, lines: Vec<Line>) -> Result<Arc<Lines>> {
@@ -25,80 +332,39 @@ impl Lines {
.unwrap()
.bounding_box_calc
.set(|node| {
let mut bounds = Bounds::default();
if let Ok(lines) = node.get_aspect::<Lines>() {
for line in &*lines.data.lock() {
for point in &line.points {
bounds = bounds_grow_to_fit_pt(bounds, point.point);
Box::pin(async {
let Ok(lines) = node.get_aspect::<Lines>() else {
return Default::default();
};
let bounds = *lines.bounds.lock();
match bounds {
Some(aabb) => aabb,
None => {
lines.setup_complete.notified().await;
lines.bounds.lock().unwrap_or_default()
}
}
}
bounds
})
});
let lines = LINES_REGISTRY.add(Lines {
enabled: node.enabled.clone(),
space: node.get_aspect::<Spatial>()?.clone(),
spatial: node.get_aspect::<Spatial>()?.clone(),
data: Mutex::new(lines),
gen_mesh: AtomicBool::new(true),
entity: OnceLock::new(),
bounds: Mutex::new(Some(Aabb::default())),
setup_complete: Notify::new(),
});
<Lines as LinesAspect>::add_node_members(node);
node.add_aspect_raw(lines.clone());
Ok(lines)
}
fn draw(&self, draw_ctx: &impl StereoKitDraw) {
let transform_mat = self.space.global_transform();
let data = self.data.lock().clone();
for line in &data {
let mut points: VecDeque<SkLinePoint> = line
.points
.iter()
.map(|p| SkLinePoint {
pt: transform_mat.transform_point3a(Vec3A::from(p.point)).into(),
thickness: p.thickness,
color: stereokit::sys::color128::from([
p.color.c.r,
p.color.c.g,
p.color.c.b,
p.color.a,
])
.into(),
})
.collect();
if line.cyclic && !points.is_empty() {
let first = line.points.first().unwrap();
let last = line.points.last().unwrap();
let color = Color128 {
r: first.color.c.r.lerp(&last.color.c.r, 0.5),
g: first.color.c.g.lerp(&last.color.c.g, 0.5),
b: first.color.c.b.lerp(&last.color.c.b, 0.5),
a: first.color.a.lerp(&last.color.a, 0.5),
};
let connect_point = SkLinePoint {
pt: transform_mat
.transform_point3a(
Vec3A::from(first.point).lerp(Vec3A::from(last.point), 0.5),
)
.into(),
thickness: (first.thickness + last.thickness) * 0.5,
color: color.into(),
};
points.push_front(connect_point.clone());
points.push_back(connect_point);
}
draw_ctx.line_add_listv(points.make_contiguous());
}
}
}
impl Aspect for Lines {
const NAME: &'static str = "Lines";
}
impl LinesAspect for Lines {
fn set_lines(node: Arc<Node>, _calling_client: Arc<Client>, lines: Vec<Line>) -> Result<()> {
let lines_aspect = node.get_aspect::<Lines>()?;
*lines_aspect.data.lock() = lines;
lines_aspect.gen_mesh.store(true, Ordering::Relaxed);
Ok(())
}
}
@@ -107,11 +373,3 @@ impl Drop for Lines {
LINES_REGISTRY.remove(self);
}
}
pub fn draw_all(draw_ctx: &impl StereoKitDraw) {
for lines in LINES_REGISTRY.get_valid_contents() {
if lines.enabled.load(Ordering::Relaxed) {
lines.draw(draw_ctx);
}
}
}

View File

@@ -1,52 +1,63 @@
pub mod lines;
pub mod model;
pub mod shaders;
pub mod sky;
pub mod text;
use self::{lines::Lines, model::Model, text::Text};
use super::{
Aspect, AspectIdentifier, Node,
spatial::{Spatial, Transform},
Node,
};
use crate::{
core::{client::Client, resource::get_resource_file},
create_interface,
};
use color_eyre::eyre::{self, Result};
use crate::core::{client::Client, error::Result, resource::get_resource_file};
use crate::nodes::spatial::SPATIAL_ASPECT_ALIAS_INFO;
use color_eyre::eyre::eyre;
use model::ModelPart;
use parking_lot::Mutex;
use stardust_xr::values::ResourceID;
use std::{ffi::OsStr, path::PathBuf, sync::Arc};
use stereokit::StereoKitDraw;
// #[instrument(level = "debug", skip(sk))]
pub fn draw(sk: &impl StereoKitDraw) {
lines::draw_all(sk);
model::draw_all(sk);
text::draw_all(sk);
if let Some(skytex) = QUEUED_SKYTEX.lock().take() {
if let Ok((_skylight, skytex)) = sk.tex_create_cubemap_file(&skytex, true, i32::MAX) {
sk.render_set_skytex(&skytex);
}
}
if let Some(skylight) = QUEUED_SKYLIGHT.lock().take() {
if let Ok((skylight, _)) = sk.tex_create_cubemap_file(&skylight, true, i32::MAX) {
sk.render_set_skylight(skylight);
}
}
}
static QUEUED_SKYLIGHT: Mutex<Option<PathBuf>> = Mutex::new(None);
static QUEUED_SKYTEX: Mutex<Option<PathBuf>> = Mutex::new(None);
static QUEUED_SKYLIGHT: Mutex<Option<Option<PathBuf>>> = Mutex::new(None);
static QUEUED_SKYTEX: Mutex<Option<Option<PathBuf>>> = Mutex::new(None);
stardust_xr_server_codegen::codegen_drawable_protocol!();
create_interface!(DrawableInterface, DrawableInterfaceAspect, "/drawable");
pub struct DrawableInterface;
impl DrawableInterfaceAspect for DrawableInterface {
fn set_sky_tex(_node: Arc<Node>, calling_client: Arc<Client>, tex: ResourceID) -> Result<()> {
let resource_path = get_resource_file(&tex, &calling_client, &[OsStr::new("hdr")])
.ok_or(eyre::eyre!("Could not find resource"))?;
impl AspectIdentifier for Lines {
impl_aspect_for_lines_aspect_id! {}
}
impl Aspect for Lines {
impl_aspect_for_lines_aspect! {}
}
impl AspectIdentifier for Model {
impl_aspect_for_model_aspect_id! {}
}
impl Aspect for Model {
impl_aspect_for_model_aspect! {}
}
impl AspectIdentifier for ModelPart {
impl_aspect_for_model_part_aspect_id! {}
}
impl Aspect for ModelPart {
impl_aspect_for_model_part_aspect! {}
}
impl AspectIdentifier for Text {
impl_aspect_for_text_aspect_id! {}
}
impl Aspect for Text {
impl_aspect_for_text_aspect! {}
}
impl InterfaceAspect for Interface {
fn set_sky_tex(
_node: Arc<Node>,
calling_client: Arc<Client>,
tex: Option<ResourceID>,
) -> Result<()> {
let resource_path = tex
.map(|tex| {
get_resource_file(&tex, &calling_client, &[OsStr::new("hdr")])
.ok_or(eyre!("Could not find resource"))
})
.transpose()?;
QUEUED_SKYTEX.lock().replace(resource_path);
Ok(())
}
@@ -54,10 +65,14 @@ impl DrawableInterfaceAspect for DrawableInterface {
fn set_sky_light(
_node: Arc<Node>,
calling_client: Arc<Client>,
light: ResourceID,
light: Option<ResourceID>,
) -> Result<()> {
let resource_path = get_resource_file(&light, &calling_client, &[OsStr::new("hdr")])
.ok_or(eyre::eyre!("Could not find resource"))?;
let resource_path = light
.map(|light| {
get_resource_file(&light, &calling_client, &[OsStr::new("hdr")])
.ok_or(eyre!("Could not find resource"))
})
.transpose()?;
QUEUED_SKYLIGHT.lock().replace(resource_path);
Ok(())
}
@@ -65,13 +80,12 @@ impl DrawableInterfaceAspect for DrawableInterface {
fn create_lines(
_node: Arc<Node>,
calling_client: Arc<Client>,
name: String,
id: u64,
parent: Arc<Node>,
transform: Transform,
lines: Vec<Line>,
) -> Result<()> {
let node =
Node::create_parent_name(&calling_client, Self::CREATE_LINES_PARENT_PATH, &name, true);
let node = Node::from_id(&calling_client, id, true);
let parent = parent.get_aspect::<Spatial>()?;
let transform = transform.to_mat4(true, true, true);
@@ -84,13 +98,12 @@ impl DrawableInterfaceAspect for DrawableInterface {
fn load_model(
_node: Arc<Node>,
calling_client: Arc<Client>,
name: String,
id: u64,
parent: Arc<Node>,
transform: Transform,
model: ResourceID,
) -> Result<()> {
let node =
Node::create_parent_name(&calling_client, Self::LOAD_MODEL_PARENT_PATH, &name, true);
let node = Node::from_id(&calling_client, id, true);
let parent = parent.get_aspect::<Spatial>()?;
let transform = transform.to_mat4(true, true, true);
let node = node.add_to_scenegraph()?;
@@ -102,14 +115,13 @@ impl DrawableInterfaceAspect for DrawableInterface {
fn create_text(
_node: Arc<Node>,
calling_client: Arc<Client>,
name: String,
id: u64,
parent: Arc<Node>,
transform: Transform,
text: String,
style: TextStyle,
) -> Result<()> {
let node =
Node::create_parent_name(&calling_client, Self::CREATE_TEXT_PARENT_PATH, &name, true);
let node = Node::from_id(&calling_client, id, true);
let parent = parent.get_aspect::<Spatial>()?;
let transform = transform.to_mat4(true, true, true);

View File

@@ -1,245 +1,498 @@
use super::{MaterialParameter, ModelAspect, ModelPartAspect, Node};
use super::{MODEL_PART_ASPECT_ALIAS_INFO, MaterialParameter, ModelAspect, ModelPartAspect};
use crate::core::bevy_channel::{BevyChannel, BevyChannelReader};
use crate::core::client::Client;
use crate::core::destroy_queue;
use crate::core::node_collections::LifeLinkedNodeMap;
use crate::core::color::ColorConvert as _;
use crate::core::entity_handle::EntityHandle;
use crate::core::error::Result;
use crate::core::registry::Registry;
use crate::core::resource::get_resource_file;
use crate::nodes::spatial::Spatial;
use crate::nodes::Aspect;
use crate::SK_MULTITHREAD;
use color_eyre::eyre::{eyre, Result};
use once_cell::sync::OnceCell;
use crate::nodes::Node;
use crate::nodes::alias::{Alias, AliasList};
use crate::nodes::spatial::{Spatial, SpatialNode};
use crate::{BevyMaterial, bail};
use bevy::asset::{load_internal_asset, weak_handle};
use bevy::gltf::GltfLoaderSettings;
use bevy::pbr::{ExtendedMaterial, MaterialExtension};
use bevy::prelude::*;
use bevy::render::primitives::Aabb;
use bevy::render::render_resource::{AsBindGroup, ShaderRef};
use color_eyre::eyre::eyre;
use parking_lot::Mutex;
use portable_atomic::{AtomicBool, Ordering};
use rustc_hash::FxHashMap;
use rustc_hash::{FxHashMap, FxHasher};
use stardust_xr::values::ResourceID;
use std::ffi::OsStr;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::sync::{Arc, Weak};
use stereokit::named_colors::WHITE;
use stereokit::{
Bounds, Color128, Material, Model as SKModel, RenderLayer, Shader, StereoKitDraw,
StereoKitMultiThread, Transparency,
};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock, Weak};
use tokio::sync::Notify;
static LOAD_MODEL: BevyChannel<(Arc<Model>, PathBuf)> = BevyChannel::new();
type HoldoutMaterial = ExtendedMaterial<BevyMaterial, HoldoutExtension>;
const HOLDOUT_SHADER_HANDLE: Handle<Shader> = weak_handle!("92b481b7-d3da-4188-b252-2335ec814ee2");
const HOLDOUT_MATERIAL_HANDLE: Handle<HoldoutMaterial> =
weak_handle!("d56f1d62-9121-434b-a34f-9f0bbd6b3390");
pub struct ModelNodePlugin;
#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone, Copy)]
pub struct ModelNodeSystemSet;
impl Plugin for ModelNodePlugin {
fn build(&self, app: &mut App) {
LOAD_MODEL.init(app);
load_internal_asset!(
app,
HOLDOUT_SHADER_HANDLE,
"holdout.wgsl",
Shader::from_wgsl
);
app.add_plugins(MaterialPlugin::<HoldoutMaterial>::default());
app.world_mut()
.resource_mut::<Assets<HoldoutMaterial>>()
.insert(&HOLDOUT_MATERIAL_HANDLE, HoldoutMaterial::default());
app.init_resource::<MaterialRegistry>();
app.add_systems(
Update,
(load_models, gen_model_parts, apply_materials)
.chain()
.in_set(ModelNodeSystemSet),
);
}
}
// No extra data needed for a simple holdout
#[derive(Default, Asset, AsBindGroup, TypePath, Debug, Clone)]
#[data(50, u32, binding_array(101))]
#[bindless(index_table(range(50..51), binding(100)))]
pub struct HoldoutExtension {}
impl From<&HoldoutExtension> for u32 {
fn from(_: &HoldoutExtension) -> Self {
0
}
}
impl MaterialExtension for HoldoutExtension {
fn fragment_shader() -> ShaderRef {
HOLDOUT_SHADER_HANDLE.into()
}
fn alpha_mode() -> Option<AlphaMode> {
Some(AlphaMode::Opaque)
}
}
#[derive(Component)]
struct ModelNode(Weak<Model>);
fn load_models(
asset_server: Res<AssetServer>,
mut cmds: Commands,
mut mpsc_receiver: ResMut<BevyChannelReader<(Arc<Model>, PathBuf)>>,
) {
while let Some((model, path)) = mpsc_receiver.read() {
// idk of the asset label is the correct approach here
let handle = asset_server.load_with_settings(
GltfAssetLabel::Scene(0).from_asset(path),
|settings: &mut GltfLoaderSettings| {
settings.load_cameras = false;
settings.load_lights = false;
},
);
let entity = cmds
.spawn((
Name::new("ModelNode"),
SceneRoot(handle),
ModelNode(Arc::downgrade(&model)),
SpatialNode(Arc::downgrade(&model.spatial)),
Visibility::Hidden,
))
.id();
model
.bevy_scene_entity
.set(EntityHandle::new(entity))
.unwrap();
}
}
fn apply_materials(
mut commands: Commands,
mut query: Query<&mut MeshMaterial3d<BevyMaterial>>,
mut material_registry: ResMut<MaterialRegistry>,
asset_server: Res<AssetServer>,
mut materials: ResMut<Assets<BevyMaterial>>,
) -> bevy::prelude::Result {
for model_part in MODEL_REGISTRY
.get_valid_contents()
.iter()
.filter_map(|p| p.parts.get())
.flatten()
{
let entity = **model_part.mesh_entity.get().unwrap();
let Ok(mut mesh_mat) = query.get_mut(entity) else {
continue;
};
if model_part.holdout.load(Ordering::Relaxed) {
commands
.entity(entity)
.remove::<MeshMaterial3d<BevyMaterial>>()
.insert(MeshMaterial3d(HOLDOUT_MATERIAL_HANDLE));
continue;
}
if let Some(material) = model_part.pending_material_replacement.lock().take()
&& let Some(material) = materials.get(&material)
{
let handle = material_registry.get_handle(material.clone(), &mut materials);
mesh_mat.0 = handle;
}
for (param_name, param) in model_part.pending_material_parameters.lock().drain() {
let mut new_mat = materials.get(&mesh_mat.0).unwrap().clone();
param.apply_to_material(
&model_part.spatial.node().unwrap().get_client().unwrap(),
&mut new_mat,
&param_name,
&asset_server,
);
let handle = material_registry.get_handle(new_mat, &mut materials);
mesh_mat.0 = handle;
}
}
Ok(())
}
fn gen_model_parts(
scenes: Res<Assets<Scene>>,
query: Query<(&SceneRoot, &ModelNode, &Children)>,
children_query: Query<&Children>,
part_query: Query<(&Name, Option<&Children>, &Transform), Without<Mesh3d>>,
part_mesh_query: Query<(&Transform, &Aabb), With<Mesh3d>>,
has_mesh: Query<Has<Mesh3d>>,
mut cmds: Commands,
) {
for (scene_root, model_node, model_children) in query.iter() {
let Some(model) = model_node.0.upgrade() else {
continue;
};
if model.parts.get().is_some() {
continue;
}
if scenes.get(scene_root.0.id()).is_none() {
continue;
}
let mut parts = Vec::new();
for entity in model_children
.iter()
.filter_map(|e| children_query.get(e).ok())
.flat_map(|c| c.iter())
{
gen_path(
entity,
&part_query,
None,
&mut |entity, name, transform, parent, children| {
let path = parent
.as_ref()
.map(|p| format!("{}/{}", &p.path, name.as_str()))
.unwrap_or_else(|| name.to_string());
let parent_spatial = parent
.as_ref()
.map(|p| p.spatial.clone())
.unwrap_or_else(|| model.spatial.clone());
let client = model.spatial.node()?.get_client()?;
let (spatial, model_part) =
match model.pre_bound_parts.lock().iter().find(|v| v.path == path) {
None => {
let node =
client.scenegraph.add_node(Node::generate(&client, false));
let spatial = Spatial::add_to(
&node,
Some(parent_spatial.clone()),
transform.compute_matrix(),
false,
);
let model_part = node.add_aspect(ModelPart {
entity: OnceLock::new(),
mesh_entity: OnceLock::new(),
path,
spatial: spatial.clone(),
pending_material_parameters: Mutex::default(),
pending_material_replacement: Mutex::default(),
holdout: AtomicBool::new(false),
aliases: AliasList::default(),
bounds: OnceLock::new(),
});
(spatial, model_part)
}
Some(part) => {
part.spatial.set_spatial_parent(&parent_spatial).unwrap();
(part.spatial.clone(), part.clone())
}
};
let aabb = Aabb::enclosing(
children
.iter()
.flat_map(|v| v.iter())
.filter_map(|e| part_mesh_query.get(e).ok())
.flat_map(|(transform, aabb)| {
[
transform.transform_point(aabb.min().into()),
transform.transform_point(aabb.max().into()),
]
}),
)
.unwrap_or_default();
_ = spatial.bounding_box_calc.set(move |n| {
Box::pin(async {
n.get_aspect::<ModelPart>()
.ok()
.and_then(|v| v.bounds.get().copied())
.unwrap_or_default()
})
});
let _ = spatial.set_spatial_parent(&parent_spatial);
spatial.set_local_transform(transform.compute_matrix());
let entity_handle = EntityHandle::new(entity);
spatial.set_entity(entity_handle.clone());
cmds.entity(entity)
.insert(SpatialNode(Arc::downgrade(&spatial)));
let mesh_entity = children_query
.get(entity)
.iter()
.flat_map(|v| v.iter())
.find(|e| has_mesh.get(*e).unwrap_or(false))?;
_ = model_part.bounds.set(aabb);
_ = model_part.entity.set(entity_handle);
_ = model_part.mesh_entity.set(EntityHandle::new(mesh_entity));
parts.push(model_part.clone());
Some(model_part)
},
);
}
_ = model.parts.set(parts);
model.pre_bound_parts.lock().clear();
model
.spatial
.set_entity(model.bevy_scene_entity.get().unwrap().clone());
model.setup_complete.store(true, Ordering::Relaxed);
model.setup_complete_notify.notify_waiters();
}
}
fn gen_path(
current_entity: Entity,
part_query: &Query<(&Name, Option<&Children>, &Transform), Without<Mesh3d>>,
parent: Option<Arc<ModelPart>>,
func: &mut dyn FnMut(
Entity,
&Name,
&Transform,
Option<Arc<ModelPart>>,
Option<&Children>,
) -> Option<Arc<ModelPart>>,
) {
let Ok((name, children, transform)) = part_query.get(current_entity) else {
return;
};
let Some(parent) = func(current_entity, name, transform, parent, children) else {
return;
};
for e in children.iter().flat_map(|c| c.iter()) {
gen_path(e, part_query, Some(parent.clone()), func);
}
}
#[derive(PartialEq, Deref, DerefMut, Clone, Copy, Eq, PartialOrd, Ord, Hash)]
struct HashedPbrMaterial(u64);
impl HashedPbrMaterial {
fn new(material: &BevyMaterial) -> Self {
let mut hasher = FxHasher::default();
Self::hash_pbr_mat(material, &mut hasher);
Self(hasher.finish())
}
fn hash_pbr_mat<H: Hasher>(mat: &BevyMaterial, state: &mut H) {
hash_color(mat.base_color, state);
hash_color(mat.emissive.into(), state);
state.write_u32(mat.metallic.to_bits());
state.write_u32(mat.perceptual_roughness.to_bits());
match mat.alpha_mode {
AlphaMode::Opaque => state.write_u8(0),
AlphaMode::Mask(v) => {
state.write_u8(1);
state.write_u32(v.to_bits());
}
AlphaMode::Blend => state.write_u8(2),
AlphaMode::Premultiplied => state.write_u8(3),
AlphaMode::AlphaToCoverage => state.write_u8(4),
AlphaMode::Add => state.write_u8(5),
AlphaMode::Multiply => state.write_u8(6),
}
state.write_u8(mat.double_sided as u8);
mat.base_color_texture.hash(state);
mat.emissive_texture.hash(state);
mat.metallic_roughness_texture.hash(state);
mat.occlusion_texture.hash(state);
}
}
fn hash_color<H: Hasher>(color: Color, state: &mut H) {
match color {
Color::Srgba(srgba) => {
state.write_u8(0);
state.write(&srgba.to_u8_array());
}
Color::LinearRgba(linear_rgba) => {
state.write_u8(1);
state.write(&linear_rgba.to_u8_array());
}
Color::Hsla(hsla) => {
state.write_u8(2);
hsla.to_f32_array()
.iter()
.for_each(|v| state.write_u32(v.to_bits()));
}
Color::Hsva(hsva) => {
state.write_u8(3);
hsva.to_f32_array()
.iter()
.for_each(|v| state.write_u32(v.to_bits()));
}
Color::Hwba(hwba) => {
state.write_u8(4);
hwba.to_f32_array()
.iter()
.for_each(|v| state.write_u32(v.to_bits()));
}
Color::Laba(laba) => {
state.write_u8(5);
laba.to_f32_array()
.iter()
.for_each(|v| state.write_u32(v.to_bits()));
}
Color::Lcha(lcha) => {
state.write_u8(6);
lcha.to_f32_array()
.iter()
.for_each(|v| state.write_u32(v.to_bits()));
}
Color::Oklaba(oklaba) => {
state.write_u8(7);
oklaba
.to_f32_array()
.iter()
.for_each(|v| state.write_u32(v.to_bits()));
}
Color::Oklcha(oklcha) => {
state.write_u8(8);
oklcha
.to_f32_array()
.iter()
.for_each(|v| state.write_u32(v.to_bits()));
}
Color::Xyza(xyza) => {
state.write_u8(9);
xyza.to_f32_array()
.iter()
.for_each(|v| state.write_u32(v.to_bits()));
}
}
}
static MODEL_REGISTRY: Registry<Model> = Registry::new();
static HOLDOUT_MATERIAL: OnceCell<Arc<Material>> = OnceCell::new();
impl MaterialParameter {
fn apply_to_material(
&self,
client: &Client,
sk: &impl StereoKitMultiThread,
material: &Material,
mat: &mut BevyMaterial,
parameter_name: &str,
asset_server: &AssetServer,
) {
match self {
MaterialParameter::Bool(val) => {
sk.material_set_bool(material, parameter_name, *val);
MaterialParameter::Bool(val) => match parameter_name {
"double_sided" => mat.double_sided = *val,
v => {
error!("unknown param_name ({v}) for color")
}
},
MaterialParameter::Int(_val) => {
// nothing uses an int
}
MaterialParameter::Int(val) => {
sk.material_set_int(material, parameter_name, *val);
}
MaterialParameter::UInt(val) => {
sk.material_set_uint(material, parameter_name, *val);
MaterialParameter::UInt(_val) => {
// nothing uses an uint
}
MaterialParameter::Float(val) => {
sk.material_set_float(material, parameter_name, *val);
match parameter_name {
"metallic" => mat.metallic = *val,
"roughness" => mat.perceptual_roughness = *val,
// we probably don't want to expose tex_scale
// "tex_scale" => mat.tex_scale = *val,
v => {
error!("unknown param_name ({v}) for float")
}
}
}
MaterialParameter::Vec2(val) => {
sk.material_set_vector2(material, parameter_name, *val);
MaterialParameter::Vec2(_val) => {
// nothing uses a Vec2
}
MaterialParameter::Vec3(val) => {
sk.material_set_vector3(material, parameter_name, *val);
}
MaterialParameter::Color(val) => {
sk.material_set_color(
material,
parameter_name,
Color128::new(val.c.r, val.c.g, val.c.b, val.a),
);
MaterialParameter::Vec3(_val) => {
// nothing uses a Vec3
}
MaterialParameter::Color(color) => match parameter_name {
"color" => mat.base_color = color.to_bevy(),
"emission_factor" => mat.emissive = color.to_bevy().to_linear(),
v => {
error!("unknown param_name ({v}) for color")
}
},
MaterialParameter::Texture(resource) => {
let Some(texture_path) =
get_resource_file(&resource, &client, &[OsStr::new("png"), OsStr::new("jpg")])
get_resource_file(resource, client, &[OsStr::new("png"), OsStr::new("jpg")])
else {
return;
};
if let Ok(tex) = sk.tex_create_file(texture_path, true, 0) {
sk.material_set_texture(material, parameter_name, &tex);
let handle = asset_server.load(texture_path);
match parameter_name {
"diffuse" => mat.base_color_texture = Some(handle),
"emission" => mat.emissive_texture = Some(handle),
"metal" => mat.metallic_roughness_texture = Some(handle),
"occlusion" => mat.occlusion_texture = Some(handle),
v => {
error!("unknown param_name ({v}) for texture");
}
}
// mat.alpha_mode = AlphaMode::Blend;
}
}
}
}
pub struct ModelPart {
id: i32,
path: PathBuf,
space: Arc<Spatial>,
model: Weak<Model>,
entity: OnceLock<EntityHandle>,
mesh_entity: OnceLock<EntityHandle>,
path: String,
spatial: Arc<Spatial>,
pending_material_parameters: Mutex<FxHashMap<String, MaterialParameter>>,
pending_material_replacement: Mutex<Option<Arc<Material>>>,
pending_material_replacement: Mutex<Option<Handle<BevyMaterial>>>,
holdout: AtomicBool,
aliases: AliasList,
bounds: OnceLock<Aabb>,
}
impl ModelPart {
fn create_for_model(sk: &impl StereoKitMultiThread, model: &Arc<Model>, sk_model: &SKModel) {
HOLDOUT_MATERIAL.get_or_init(|| {
let mat = sk.material_copy(Material::UNLIT);
sk.material_set_transparency(&mat, Transparency::None);
sk.material_set_color(
&mat,
"color",
stereokit::sys::color128 {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.0,
},
);
Arc::new(mat)
});
let first_root_part = sk.model_node_get_root(sk_model);
let mut current_option_part = Some(first_root_part);
while let Some(current_part) = &mut current_option_part {
ModelPart::create(sk, model, sk_model, *current_part);
if let Some(child) = sk.model_node_child(sk_model, *current_part) {
*current_part = child;
} else if let Some(sibling) = sk.model_node_sibling(sk_model, *current_part) {
*current_part = sibling;
} else {
while let Some(current_part) = &mut current_option_part {
if let Some(sibling) = sk.model_node_sibling(sk_model, *current_part) {
*current_part = sibling;
break;
}
current_option_part = sk.model_node_parent(sk_model, *current_part);
}
}
}
}
fn create(
sk: &impl StereoKitMultiThread,
model: &Arc<Model>,
sk_model: &SKModel,
id: i32,
) -> Option<Arc<Self>> {
let parent_node = sk
.model_node_parent(sk_model, id)
.and_then(|id| model.parts.get(&id));
let parent_part = parent_node
.as_ref()
.and_then(|node| node.get_aspect::<ModelPart>().ok());
let stardust_model_part = model.space.node()?;
let client = stardust_model_part.get_client()?;
let mut part_path = parent_part.map(|n| n.path.clone()).unwrap_or_default();
part_path.push(sk.model_node_get_name(sk_model, id)?);
let node = client.scenegraph.add_node(Node::create_parent_name(
&client,
stardust_model_part.get_path(),
part_path.to_str()?,
false,
));
let spatial_parent = parent_node
.and_then(|n| n.get_aspect::<Spatial>().ok())
.unwrap_or_else(|| model.space.clone());
let space = Spatial::add_to(
&node,
Some(spatial_parent),
sk.model_node_get_transform_local(sk_model, id),
false,
);
let _ = node
.get_aspect::<Spatial>()
.unwrap()
.bounding_box_calc
.set(|node| {
let Ok(model_part) = node.get_aspect::<ModelPart>() else {
return Bounds::default();
};
let Some(sk) = SK_MULTITHREAD.get() else {
return Bounds::default();
};
let Some(model) = model_part.model.upgrade() else {
return Bounds::default();
};
let Some(sk_model) = model.sk_model.get() else {
return Bounds::default();
};
let Some(sk_mesh) = sk.model_node_get_mesh(sk_model, model_part.id) else {
return Bounds::default();
};
sk.mesh_get_bounds(sk_mesh)
});
let model_part = Arc::new(ModelPart {
id,
path: part_path,
space,
model: Arc::downgrade(model),
pending_material_parameters: Mutex::new(FxHashMap::default()),
pending_material_replacement: Mutex::new(None),
});
<ModelPart as ModelPartAspect>::add_node_members(&node);
node.add_aspect_raw(model_part.clone());
model.parts.add(id, &node);
Some(model_part)
}
pub fn replace_material(&self, replacement: Arc<Material>) {
pub fn replace_material(&self, replacement: Handle<BevyMaterial>) {
self.pending_material_replacement
.lock()
.replace(replacement);
}
fn update(&self, sk: &impl StereoKitDraw) {
let Some(model) = self.model.upgrade() else {
return;
};
let Some(sk_model) = model.sk_model.get() else {
return;
};
let Some(node) = model.space.node() else {
return;
};
let Some(client) = node.get_client() else {
return;
};
if let Some(material_replacement) = self.pending_material_replacement.lock().take() {
sk.model_node_set_material(sk_model, self.id, material_replacement.as_ref().as_ref());
}
let mut material_parameters = self.pending_material_parameters.lock();
for (parameter_name, parameter_value) in material_parameters.drain() {
let Some(material) = sk.model_node_get_material(sk_model, self.id) else {
continue;
};
let new_material = sk.material_copy(material);
parameter_value.apply_to_material(&client, sk, &new_material, parameter_name.as_str());
sk.model_node_set_material(sk_model, self.id, &new_material);
}
sk.model_node_set_transform_model(
sk_model,
self.id,
Spatial::space_to_space_matrix(Some(&self.space), Some(&model.space)),
);
pub fn set_material_parameter(&self, parameter_name: String, value: MaterialParameter) {
self.pending_material_parameters
.lock()
.insert(parameter_name, value);
}
}
impl Aspect for ModelPart {
const NAME: &'static str = "ModelPart";
}
impl ModelPartAspect for ModelPart {
#[doc = "Set this model part's material to one that cuts a hole in the world. Often used for overlays/passthrough where you want to show the background through an object."]
fn apply_holdout_material(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
let model_part = node.get_aspect::<ModelPart>()?;
model_part.replace_material(HOLDOUT_MATERIAL.get().unwrap().clone());
model_part.holdout.store(true, Ordering::Relaxed);
Ok(())
}
@@ -251,26 +504,44 @@ impl ModelPartAspect for ModelPart {
value: MaterialParameter,
) -> Result<()> {
let model_part = node.get_aspect::<ModelPart>()?;
model_part
.pending_material_parameters
.lock()
.insert(parameter_name, value);
model_part.set_material_parameter(parameter_name, value);
Ok(())
}
}
#[derive(Default, Resource)]
pub struct MaterialRegistry(FxHashMap<HashedPbrMaterial, Handle<BevyMaterial>>);
impl MaterialRegistry {
/// returns strong handle for PbrMaterial elminitating duplications
pub fn get_handle(
&mut self,
material: BevyMaterial,
materials: &mut ResMut<Assets<BevyMaterial>>,
) -> Handle<BevyMaterial> {
let hash = HashedPbrMaterial::new(&material);
match self
.0
.get(&hash)
.and_then(|v| materials.get_strong_handle(v.id()))
{
Some(v) => v,
None => {
let handle = materials.add(material);
self.0.insert(hash, handle.clone_weak());
handle
}
}
}
}
pub struct Model {
self_ref: Weak<Model>,
enabled: Arc<AtomicBool>,
space: Arc<Spatial>,
spatial: Arc<Spatial>,
_resource_id: ResourceID,
sk_model: OnceCell<SKModel>,
parts: LifeLinkedNodeMap<i32>,
bevy_scene_entity: OnceLock<EntityHandle>,
parts: OnceLock<Vec<Arc<ModelPart>>>,
pre_bound_parts: Mutex<Vec<Arc<ModelPart>>>,
setup_complete: AtomicBool,
setup_complete_notify: Notify,
}
unsafe impl Send for Model {}
unsafe impl Sync for Model {}
impl Model {
pub fn add_to(node: &Arc<Node>, resource_id: ResourceID) -> Result<Arc<Model>> {
let pending_model_path = get_resource_file(
@@ -280,61 +551,111 @@ impl Model {
)
.ok_or_else(|| eyre!("Resource not found"))?;
let model = Arc::new_cyclic(|self_ref| Model {
self_ref: self_ref.clone(),
enabled: node.enabled.clone(),
space: node.get_aspect::<Spatial>().unwrap().clone(),
let model = Arc::new(Model {
spatial: node.get_aspect::<Spatial>().unwrap().clone(),
_resource_id: resource_id,
sk_model: OnceCell::new(),
parts: LifeLinkedNodeMap::default(),
bevy_scene_entity: OnceLock::new(),
pre_bound_parts: Mutex::default(),
parts: OnceLock::new(),
setup_complete_notify: Notify::new(),
setup_complete: AtomicBool::new(false),
});
_ = model.spatial.bounding_box_calc.set(|n| {
Box::pin(async {
if let Ok(model) = n.get_aspect::<Model>()
&& !model.setup_complete.load(Ordering::Relaxed)
{
model.setup_complete_notify.notified().await;
}
Aabb::default()
})
});
LOAD_MODEL
.send((model.clone(), pending_model_path))
.unwrap();
MODEL_REGISTRY.add_raw(&model);
let sk = SK_MULTITHREAD.get().unwrap();
let sk_model = sk.model_copy(
sk.model_create_file(pending_model_path.to_str().unwrap(), None::<Shader>)?,
);
ModelPart::create_for_model(sk, &model.self_ref.upgrade().unwrap(), &sk_model);
let _ = model.sk_model.set(sk_model);
node.add_aspect_raw(model.clone());
Ok(model)
}
fn draw(&self, sk: &impl StereoKitDraw) {
let Some(sk_model) = self.sk_model.get() else {
return;
pub fn get_model_part(self: &Arc<Self>, part_path: String) -> Result<Arc<ModelPart>> {
let part = match self
.parts
.get()
.map(|v| v.iter().find(|p| p.path == part_path))
{
Some(Some(part)) => part.clone(),
Some(None) => {
let paths = self
.parts
.get()
.unwrap()
.iter()
.map(|p| &p.path)
.collect::<Vec<_>>();
bail!(
"Couldn't find model part at path {part_path}, all available paths: {paths:?}",
);
}
None => {
let client = self.spatial.node().unwrap().get_client().unwrap();
let part_node = client.scenegraph.add_node(Node::generate(&client, false));
let spatial = Spatial::add_to(
&part_node,
Some(self.spatial.clone()),
Mat4::IDENTITY,
false,
);
let part = part_node.add_aspect(ModelPart {
entity: OnceLock::new(),
mesh_entity: OnceLock::new(),
path: part_path,
spatial,
pending_material_parameters: Mutex::default(),
pending_material_replacement: Mutex::default(),
holdout: AtomicBool::new(false),
aliases: AliasList::default(),
bounds: OnceLock::new(),
});
self.pre_bound_parts.lock().push(part.clone());
part
}
};
for model_node_node in self.parts.nodes() {
if let Ok(model_node) = model_node_node.get_aspect::<ModelPart>() {
model_node.update(sk);
};
}
sk.model_draw(
sk_model,
self.space.global_transform(),
WHITE,
RenderLayer::LAYER0,
);
Ok(part)
}
}
impl Aspect for Model {
const NAME: &'static str = "Model";
impl ModelAspect for Model {
#[doc = "Bind a model part to the node with the ID input."]
fn bind_model_part(
node: Arc<Node>,
calling_client: Arc<Client>,
id: u64,
part_path: String,
) -> Result<()> {
let model = node.get_aspect::<Model>()?;
let part = model.get_model_part(part_path)?;
Alias::create_with_id(
&part.spatial.node().unwrap(),
&calling_client,
id,
MODEL_PART_ASPECT_ALIAS_INFO.clone(),
Some(&part.aliases),
)?;
Ok(())
}
}
impl ModelAspect for Model {}
impl Drop for Model {
fn drop(&mut self) {
if let Some(sk_model) = self.sk_model.take() {
destroy_queue::add(sk_model);
for p in self.parts.get().iter().flat_map(|v| v.iter()) {
if let Some(node) = p.spatial.node() {
node.destroy();
}
}
for p in self.pre_bound_parts.lock().iter() {
if let Some(node) = p.spatial.node() {
node.destroy();
}
}
MODEL_REGISTRY.remove(self);
}
}
pub fn draw_all(sk: &impl StereoKitDraw) {
for model in MODEL_REGISTRY.get_valid_contents() {
if model.enabled.load(Ordering::Relaxed) {
model.draw(sk);
}
}
}

View File

@@ -1,144 +0,0 @@
#![allow(dead_code)]
use smithay::backend::renderer::gles::{
ffi::{self, Gles2, FRAGMENT_SHADER, VERTEX_SHADER},
GlesError,
};
use std::mem::transmute;
use stereokit::Shader;
use tracing::error;
// Simula shader with fancy lanzcos sampling
pub const UNLIT_SHADER_BYTES: &[u8] = include_bytes!("shader_unlit_gamma.sks");
// Simula shader with fancy lanzcos sampling
pub const PANEL_SHADER_BYTES: &[u8] = include_bytes!("shader_unlit_simula.sks");
struct FfiAssetHeader {
asset_type: i32,
asset_state: i32,
id: u64,
index: u64,
refs: i32,
debug: *mut u8,
}
struct FfiSkgShader {
meta: *mut u8,
vertex: u32,
pixel: u32,
program: u32,
compute: u32,
}
struct FfiShader {
header: FfiAssetHeader,
shader: FfiSkgShader,
}
unsafe fn compile_shader(
gl: &ffi::Gles2,
variant: ffi::types::GLuint,
src: &str,
) -> Result<ffi::types::GLuint, GlesError> {
let shader = gl.CreateShader(variant);
if shader == 0 {
return Err(GlesError::CreateShaderObject);
}
gl.ShaderSource(
shader,
1,
&src.as_ptr() as *const *const u8 as *const *const ffi::types::GLchar,
&(src.len() as i32) as *const _,
);
gl.CompileShader(shader);
let mut status = ffi::FALSE as i32;
gl.GetShaderiv(shader, ffi::COMPILE_STATUS, &mut status as *mut _);
if status == ffi::FALSE as i32 {
let mut max_len = 0;
gl.GetShaderiv(shader, ffi::INFO_LOG_LENGTH, &mut max_len as *mut _);
let mut error = Vec::with_capacity(max_len as usize);
let mut len = 0;
gl.GetShaderInfoLog(
shader,
max_len as _,
&mut len as *mut _,
error.as_mut_ptr() as *mut _,
);
error.set_len(len as usize);
error!(
"[GL] {}",
std::str::from_utf8(&error).unwrap_or("<Error Message no utf8>")
);
gl.DeleteShader(shader);
return Err(GlesError::ShaderCompileError);
}
Ok(shader)
}
unsafe fn link_program(
gl: &ffi::Gles2,
vert: ffi::types::GLuint,
frag: ffi::types::GLuint,
) -> Result<ffi::types::GLuint, GlesError> {
let program = gl.CreateProgram();
gl.AttachShader(program, vert);
gl.AttachShader(program, frag);
gl.LinkProgram(program);
// gl.DetachShader(program, vert);
// gl.DetachShader(program, frag);
// gl.DeleteShader(vert);
// gl.DeleteShader(frag);
let mut status = ffi::FALSE as i32;
gl.GetProgramiv(program, ffi::LINK_STATUS, &mut status as *mut _);
if status == ffi::FALSE as i32 {
let mut max_len = 0;
gl.GetProgramiv(program, ffi::INFO_LOG_LENGTH, &mut max_len as *mut _);
let mut error = Vec::with_capacity(max_len as usize);
let mut len = 0;
gl.GetProgramInfoLog(
program,
max_len as _,
&mut len as *mut _,
error.as_mut_ptr() as *mut _,
);
error.set_len(len as usize);
error!(
"[GL] {}",
std::str::from_utf8(&error).unwrap_or("<Error Message no utf8>")
);
gl.DeleteProgram(program);
return Err(GlesError::ProgramLinkError);
}
Ok(program)
}
pub unsafe fn shader_inject(
c: &Gles2,
sk_shader: &mut Shader,
vert_str: &str,
frag_str: &str,
) -> Result<(), GlesError> {
let gl_vert = compile_shader(c, VERTEX_SHADER, vert_str)?;
let gl_frag = compile_shader(c, FRAGMENT_SHADER, frag_str)?;
let gl_prog = link_program(c, gl_vert, gl_frag)?;
let shader: *mut FfiShader = transmute(sk_shader.0.as_mut());
if let Some(shader) = shader.as_mut() {
shader.shader.vertex = gl_vert;
shader.shader.pixel = gl_frag;
shader.shader.program = gl_prog;
}
Ok(())
}

View File

@@ -1,39 +0,0 @@
#include "stereokit.hlsli"
//--name = sk/unlit
//--diffuse = white
//--uv_offset = 0.0, 0.0
//--uv_scale = 1.0, 1.0
Texture2D diffuse : register(t0);
SamplerState diffuse_s : register(s0);
float2 uv_scale;
float2 uv_offset;
struct vsIn {
float4 pos : SV_Position;
float3 norm : NORMAL0;
float2 uv : TEXCOORD0;
};
struct psIn {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
uint view_id : SV_RenderTargetArrayIndex;
};
psIn vs(vsIn input, uint id : SV_InstanceID) {
psIn o;
o.view_id = id % sk_view_count;
id = id / sk_view_count;
float3 world = mul(float4(input.pos.xyz, 1), sk_inst[id].world).xyz;
o.pos = mul(float4(world, 1), sk_viewproj[o.view_id]);
o.uv = (input.uv + uv_offset) * uv_scale;
return o;
}
float4 ps(psIn input) : SV_TARGET {
float4 col = diffuse.Sample(diffuse_s, input.uv);
col.rgb = pow(col.rgb, float3(2.2));
return col;
}

View File

@@ -1,119 +0,0 @@
#include "stereokit.hlsli"
// Port of https://github.com/SimulaVR/Simula/blob/master/addons/godot-haskell-plugin/TextShader.tres to StereoKit and HLSL.
//--name = stardust/text_shader
//--diffuse = white
//--uv_offset = 0.0, 0.0
//--uv_scale = 1.0, 1.0
//--fcFactor = 1.0
//--ripple = 4.0
//--alpha_min = 0.0
//--alpha_max = 1.0
Texture2D diffuse : register(t0);
SamplerState diffuse_s : register(s0);
float4 diffuse_i;
float2 uv_scale;
float2 uv_offset;
float fcFactor;
float ripple;
float alpha_min;
float alpha_max;
struct vsIn {
float4 pos : SV_Position;
float3 norm : NORMAL0;
float2 uv : TEXCOORD0;
};
struct psIn {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
uint view_id : SV_RenderTargetArrayIndex;
};
psIn vs(vsIn input, uint id : SV_InstanceID) {
psIn o;
o.view_id = id % sk_view_count;
id = id / sk_view_count;
float3 world = mul(float4(input.pos.xyz, 1), sk_inst[id].world).xyz;
o.pos = mul(float4(world, 1), sk_viewproj[o.view_id]);
o.uv = (input.uv + uv_offset) * uv_scale;
return o;
}
float map(float value, float min1, float max1, float min2, float max2) {
return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
}
// float gaussian(float x, float t) {
// float PI = 3.14159265358;
// return exp(-x*x/(2.0 * t*t))/(sqrt(2.0*PI)*t);
// }
float besselI0(float x) {
return 1.0 + pow(x, 2.0) * (0.25 + pow(x, 2.0) * (0.015625 + pow(x, 2.0) * (0.000434028 + pow(x, 2.0) * (6.78168e-6 + pow(x, 2.0) * (6.78168e-8 + pow(x, 2.0) * (4.7095e-10 + pow(x, 2.0) * (2.40281e-12 + pow(x, 2.0) * (9.38597e-15 + pow(x, 2.0) * (2.8969e-17 + 7.24226e-20 * pow(x, 2.0))))))))));
}
float kaiser(float x, float alpha) {
if (x > 1.0) {
return 0.0;
}
return besselI0(alpha * sqrt(1.0-x*x));
}
float4 lowpassFilter(Texture2D tex, sampler2D texSampler, float2 uv, float alpha) {
float PI = 3.14159265358;
float4 q = float4(0.0);
float2 dx_uv = ddx(uv);
float2 dy_uv = ddy(uv);
//float width = sqrt(max(dot(dx_uv, dx_uv), dot(dy_uv, dy_uv)));
float2 width = abs(float2(dx_uv.x, dy_uv.y));
float2 pixelWidth = floor(width * diffuse_i.xy);
float2 aspectRatio = normalize(pixelWidth);
float2 xyf = uv * diffuse_i.xy;
int2 xy = int2(xyf);
pixelWidth = clamp(pixelWidth, float2(1.0), float2(2.0));
int2 start = xy - int2(pixelWidth);
int2 end = xy + int2(pixelWidth);
float4 outColor = float4(0.0);
float qSum = 0.0;
for (int v = start.y; v <= end.y; v++) {
for (int u = start.x; u <= end.x; u++) {
float kx = fcFactor * (xyf.x - float(u))/pixelWidth.x;
float ky = fcFactor * (xyf.y - float(v))/pixelWidth.y;
//float lanczosValue = gaussian(kx, fcx);
float lanczosValue = kaiser(sqrt(kx*kx + ky*ky), alpha);
q += tex.Sample(texSampler, (float2(u, v)+float2(0.5))/diffuse_i.xy) * lanczosValue;
// q += tex.Load(int3(u, v, 0)) * lanczosValue;
qSum += lanczosValue;
}
}
return q/qSum;
}
float4 ps(psIn input) : SV_TARGET {
float gamma = 2.2;
// float4 col = diffuse.Sample(diffuse_s, input.uv);
// float4 col = lowpassFilter(diffuse, diffuse_s, diffuse_i.xy, float2(1.0 - input.uv.x, input.uv.y), ripple);
float4 col = lowpassFilter(diffuse, diffuse_s, input.uv, ripple);
// float4 col = diffuse.Sample(diffuse_s, input.uv);
col.rgb = pow(col.rgb, float3(gamma));
col.a = map(col.a, 0, 1, alpha_min, alpha_max);
return col;
}

72
src/nodes/drawable/sky.rs Normal file
View File

@@ -0,0 +1,72 @@
use bevy::{
app::{Plugin, Update},
color::Color,
core_pipeline::{Skybox, core_3d::Camera3d},
ecs::{
entity::Entity,
query::With,
system::{Commands, Query, ResMut},
},
pbr::{AmbientLight, environment_map::EnvironmentMapLight},
};
use bevy_equirect::EquirectManager;
use glam::Quat;
pub struct SkyPlugin;
impl Plugin for SkyPlugin {
fn build(&self, app: &mut bevy::app::App) {
app.add_systems(Update, apply_sky);
}
}
// TODO: make this work with cameras spawned after setting the sky texture
fn apply_sky(
mut equirect: ResMut<EquirectManager>,
cameras: Query<Entity, With<Camera3d>>,
mut cmds: Commands,
) {
if let Some(tex) = super::QUEUED_SKYTEX.lock().take() {
if let Some(path) = tex {
let image_handle = equirect.load_equirect_as_cubemap(path, 1024);
for cam in cameras {
cmds.entity(cam).insert(Skybox {
image: image_handle.clone(),
brightness: 1000.0,
rotation: Quat::IDENTITY,
});
}
} else {
for cam in cameras {
cmds.entity(cam).remove::<Skybox>();
}
}
}
if let Some(light) = super::QUEUED_SKYLIGHT.lock().take() {
if let Some(path) = light {
let image_handle = equirect.load_equirect_as_cubemap(path, 1024);
for cam in cameras {
cmds.entity(cam)
.insert(EnvironmentMapLight {
diffuse_map: image_handle.clone(),
// we might want to use the SkyTex for this?
specular_map: image_handle.clone(),
intensity: 1000.0,
rotation: Quat::IDENTITY,
affects_lightmapped_mesh_diffuse: false,
})
.remove::<AmbientLight>();
}
} else {
for cam in cameras {
cmds.entity(cam)
.insert(AmbientLight {
color: Color::WHITE,
brightness: 1000.0,
affects_lightmapped_meshes: true,
})
.remove::<EnvironmentMapLight>();
}
}
}
}

View File

@@ -1,42 +1,184 @@
use crate::{
core::{client::Client, destroy_queue, registry::Registry, resource::get_resource_file},
nodes::{spatial::Spatial, Aspect, Node},
BevyMaterial,
core::{
bevy_channel::{BevyChannel, BevyChannelReader},
client::Client,
color::ColorConvert,
entity_handle::EntityHandle,
error::Result,
registry::Registry,
resource::get_resource_file,
},
nodes::{
Node,
drawable::{TextFit, XAlign},
spatial::{Spatial, SpatialNode},
},
};
use color_eyre::eyre::{eyre, Result};
use glam::{vec3, Mat4, Vec2};
use once_cell::sync::OnceCell;
use bevy::{platform::collections::HashMap, prelude::*};
use bevy_mesh_text_3d::{
Align, Attrs, HorizontalAnchorPoint, MeshTextPlugin, Settings as FontSettings, VerticalAlign,
VerticalAnchorPoint, generate_meshes,
};
use color_eyre::eyre::eyre;
use core::f32;
use parking_lot::Mutex;
use portable_atomic::{AtomicBool, Ordering};
use std::{ffi::OsStr, path::PathBuf, sync::Arc};
use stereokit::{
named_colors::WHITE, Color128, StereoKitDraw, TextAlign, TextFit, TextStyle as SkTextStyle,
};
use std::{ffi::OsStr, mem, path::PathBuf, sync::Arc};
use super::{TextAspect, TextStyle};
static SPAWN_TEXT: BevyChannel<Arc<Text>> = BevyChannel::new();
pub struct TextNodePlugin;
impl Plugin for TextNodePlugin {
fn build(&self, app: &mut App) {
// Text init stuff
app.add_plugins(MeshTextPlugin);
app.world_mut()
.resource_mut::<FontSettings>()
.font_system
.db_mut()
.load_system_fonts();
SPAWN_TEXT.init(app);
app.init_resource::<MaterialRegistry>();
app.add_systems(Update, spawn_text);
}
}
fn spawn_text(
mut mpsc: ResMut<BevyChannelReader<Arc<Text>>>,
mut cmds: Commands,
mut font_settings: ResMut<FontSettings>,
mut material_registry: ResMut<MaterialRegistry>,
mut materials: ResMut<Assets<BevyMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
mut font_registry: Local<FontDatabaseRegistry>,
) {
while let Some(text) = mpsc.read() {
if let Some(entity) = text.entity.lock().take() {
cmds.entity(*entity).despawn();
}
let style = text.data.lock();
let old_db = text.font_path.clone().map(|p| {
let db = font_registry.get(p);
mem::swap(font_settings.font_system.db_mut(), db);
db
});
let attrs = Attrs::new().weight(cosmic_text::Weight::BOLD);
let alignment = Some(match style.text_align_x {
super::XAlign::Left => Align::Right,
super::XAlign::Center => Align::Center,
super::XAlign::Right => Align::Left,
});
let vertical_alignment = Some(match style.text_align_y {
super::YAlign::Top => VerticalAlign::Top,
super::YAlign::Center => VerticalAlign::Middle,
super::YAlign::Bottom => VerticalAlign::Bottom,
});
let text_string = text.text.lock().clone();
let max_width = style.bounds.as_ref().map(|v| v.bounds.x);
let max_height = style.bounds.as_ref().map(|v| v.bounds.y);
let horizontal_anchor_point = style
.bounds
.as_ref()
.map(|v| match v.anchor_align_x {
XAlign::Left => HorizontalAnchorPoint::Left,
XAlign::Center => HorizontalAnchorPoint::Middle,
XAlign::Right => HorizontalAnchorPoint::Right,
})
.unwrap_or(HorizontalAnchorPoint::Middle);
let vertical_anchor_point = style
.bounds
.as_ref()
.map(|v| match v.anchor_align_y {
YAlign::Top => VerticalAnchorPoint::Top,
YAlign::Center => VerticalAnchorPoint::Middle,
YAlign::Bottom => VerticalAnchorPoint::Bottom,
})
.unwrap_or(VerticalAnchorPoint::Middle);
let wrap = matches!(style.bounds.as_ref().map(|v| v.fit), Some(TextFit::Wrap));
let char_meshes = generate_meshes(
bevy_mesh_text_3d::InputText::Simple {
text: text_string,
material: material_registry.get_handle(
BevyMaterial {
base_color: style.color.to_bevy(),
emissive: Color::WHITE.to_linear(),
metallic: 0.0,
perceptual_roughness: 1.0,
alpha_mode: AlphaMode::Premultiplied,
double_sided: false,
..default()
},
&mut materials,
),
attrs,
},
&mut font_settings,
bevy_mesh_text_3d::Parameters {
extrusion_depth: 0.0,
font_size: style.character_height,
line_height: style.character_height * 1.1,
alignment,
max_width: wrap.then_some(0).and(max_width),
max_height: wrap.then_some(0).and(max_height),
vertical_alignment,
horizontal_anchor_point,
vertical_anchor_point,
},
&mut meshes,
);
if let Some(db) = old_db {
mem::swap(font_settings.font_system.db_mut(), db);
}
let Ok((char_meshes, _text_size)) =
char_meshes.inspect_err(|err| error!("unable to create text meshes: {err}"))
else {
continue;
};
let letters = char_meshes
.into_iter()
.map(|v| {
cmds.spawn((Mesh3d(v.mesh), MeshMaterial3d(v.material), v.transform))
.id()
})
.collect::<Vec<_>>();
let entity = cmds
.spawn((
Name::new("TextNode"),
SpatialNode(Arc::downgrade(&text.spatial)),
))
.add_children(&letters)
.id();
let entity = EntityHandle::new(entity);
text.entity.lock().replace(entity.clone());
text.spatial.set_entity(entity);
}
}
#[derive(Default)]
struct FontDatabaseRegistry(HashMap<PathBuf, cosmic_text::fontdb::Database>);
impl FontDatabaseRegistry {
fn get(&mut self, path: PathBuf) -> &mut cosmic_text::fontdb::Database {
self.0.entry(path).or_insert_with_key(|path| {
let mut db = cosmic_text::fontdb::Database::new();
if let Err(err) = db.load_font_file(path) {
error!("unable to load font file {} {err}", path.to_string_lossy());
};
db
})
}
}
use super::{TextAspect, TextStyle, YAlign, model::MaterialRegistry};
static TEXT_REGISTRY: Registry<Text> = Registry::new();
fn convert_align(x_align: super::XAlign, y_align: super::YAlign) -> TextAlign {
let x_align = match x_align {
super::XAlign::Left => TextAlign::XLeft,
super::XAlign::Center => TextAlign::XCenter,
super::XAlign::Right => TextAlign::XRight,
} as u32;
let y_align = match y_align {
super::YAlign::Top => TextAlign::YTop,
super::YAlign::Center => TextAlign::YCenter,
super::YAlign::Bottom => TextAlign::YBottom,
} as u32;
unsafe { std::mem::transmute(x_align | y_align) }
}
pub struct Text {
enabled: Arc<AtomicBool>,
space: Arc<Spatial>,
spatial: Arc<Spatial>,
font_path: Option<PathBuf>,
style: OnceCell<SkTextStyle>,
entity: Mutex<Option<EntityHandle>>,
text: Mutex<String>,
data: Mutex<TextStyle>,
}
@@ -44,77 +186,20 @@ impl Text {
pub fn add_to(node: &Arc<Node>, text: String, style: TextStyle) -> Result<Arc<Text>> {
let client = node.get_client().ok_or_else(|| eyre!("Client not found"))?;
let text = TEXT_REGISTRY.add(Text {
enabled: node.enabled.clone(),
space: node.get_aspect::<Spatial>().unwrap().clone(),
spatial: node.get_aspect::<Spatial>().unwrap().clone(),
font_path: style.font.as_ref().and_then(|res| {
get_resource_file(&res, &client, &[OsStr::new("ttf"), OsStr::new("otf")])
get_resource_file(res, &client, &[OsStr::new("ttf"), OsStr::new("otf")])
}),
style: OnceCell::new(),
entity: Mutex::new(None),
text: Mutex::new(text),
data: Mutex::new(style),
});
<Text as TextAspect>::add_node_members(node);
node.add_aspect_raw(text.clone());
_ = SPAWN_TEXT.send(text.clone());
Ok(text)
}
fn draw(&self, sk: &impl StereoKitDraw) {
let style =
self.style
.get_or_try_init(|| -> Result<SkTextStyle, color_eyre::eyre::Error> {
let font = self
.font_path
.as_deref()
.and_then(|path| sk.font_create(path).ok())
.unwrap_or_else(|| sk.font_find("default/font").unwrap());
Ok(unsafe { sk.text_make_style(font, 1.0, WHITE) })
});
if let Ok(style) = style {
let text = self.text.lock();
let data = self.data.lock();
let transform = self.space.global_transform()
* Mat4::from_scale(vec3(
data.character_height,
data.character_height,
data.character_height,
));
if let Some(bounds) = &data.bounds {
sk.text_add_in(
&*text,
transform,
Vec2::from(bounds.bounds) / data.character_height,
match bounds.fit {
super::TextFit::Wrap => TextFit::Wrap,
super::TextFit::Clip => TextFit::Clip,
super::TextFit::Squeeze => TextFit::Squeeze,
super::TextFit::Exact => TextFit::Exact,
super::TextFit::Overflow => TextFit::Overflow,
},
*style,
convert_align(bounds.anchor_align_x.clone(), bounds.anchor_align_y.clone()),
convert_align(data.text_align_x.clone(), data.text_align_y.clone()),
vec3(0.0, 0.0, 0.0),
Color128::from([data.color.c.r, data.color.c.g, data.color.c.b, data.color.a]),
);
} else {
sk.text_add_at(
&*text,
transform,
*style,
TextAlign::Center,
convert_align(data.text_align_x.clone(), data.text_align_y.clone()),
vec3(0.0, 0.0, 0.0),
Color128::from([data.color.c.r, data.color.c.g, data.color.c.b, data.color.a]),
);
}
}
}
}
impl Aspect for Text {
const NAME: &'static str = "Text";
}
impl TextAspect for Text {
fn set_character_height(
@@ -124,28 +209,19 @@ impl TextAspect for Text {
) -> Result<()> {
let this_text = node.get_aspect::<Text>()?;
this_text.data.lock().character_height = height;
_ = SPAWN_TEXT.send(this_text);
Ok(())
}
fn set_text(node: Arc<Node>, _calling_client: Arc<Client>, text: String) -> Result<()> {
let this_text = node.get_aspect::<Text>()?;
*this_text.text.lock() = text;
_ = SPAWN_TEXT.send(this_text);
Ok(())
}
}
impl Drop for Text {
fn drop(&mut self) {
if let Some(style) = self.style.take() {
destroy_queue::add(style);
}
TEXT_REGISTRY.remove(self);
}
}
pub fn draw_all(sk: &impl StereoKitDraw) {
for text in TEXT_REGISTRY.get_valid_contents() {
if text.enabled.load(Ordering::Relaxed) {
text.draw(sk);
}
}
}

402
src/nodes/fields.rs Normal file
View File

@@ -0,0 +1,402 @@
use super::alias::{Alias, AliasInfo};
use super::spatial::{
SPATIAL_REF_GET_LOCAL_BOUNDING_BOX_SERVER_OPCODE,
SPATIAL_REF_GET_RELATIVE_BOUNDING_BOX_SERVER_OPCODE, SPATIAL_REF_GET_TRANSFORM_SERVER_OPCODE,
Spatial,
};
use super::{Aspect, AspectIdentifier, Node};
use crate::DbusConnection;
use crate::core::client::Client;
use crate::core::error::Result;
use crate::core::registry::Registry;
use crate::nodes::spatial::SPATIAL_ASPECT_ALIAS_INFO;
use crate::nodes::spatial::SPATIAL_REF_ASPECT_ALIAS_INFO;
use crate::nodes::spatial::Transform;
use bevy::app::{Plugin, Update};
use bevy::color::Color;
use bevy::ecs::resource::Resource;
use bevy::ecs::schedule::IntoScheduleConfigs;
use bevy::ecs::system::Res;
use bevy::gizmos::gizmos::Gizmos;
use bevy::gizmos::primitives::dim3::GizmoPrimitive3d;
use bevy::math::primitives::{Cylinder, Torus};
use color_eyre::eyre::OptionExt;
use glam::{Vec3, Vec3A, Vec3Swizzles, vec2, vec3, vec3a};
use parking_lot::Mutex;
use rustc_hash::FxHashMap;
use stardust_xr::values::Vector3;
use std::sync::{Arc, LazyLock, Weak};
use zbus::interface;
// TODO: get SDFs working properly with non-uniform scale and so on, output distance relative to the spatial it's compared against
pub static FIELD_ALIAS_INFO: LazyLock<AliasInfo> = LazyLock::new(|| AliasInfo {
server_methods: vec![
SPATIAL_REF_GET_TRANSFORM_SERVER_OPCODE,
SPATIAL_REF_GET_LOCAL_BOUNDING_BOX_SERVER_OPCODE,
SPATIAL_REF_GET_RELATIVE_BOUNDING_BOX_SERVER_OPCODE,
FIELD_REF_DISTANCE_SERVER_OPCODE,
FIELD_REF_NORMAL_SERVER_OPCODE,
FIELD_REF_CLOSEST_POINT_SERVER_OPCODE,
FIELD_REF_RAY_MARCH_SERVER_OPCODE,
],
..Default::default()
});
pub struct FieldDebugGizmoPlugin;
impl Plugin for FieldDebugGizmoPlugin {
fn build(&self, app: &mut bevy::app::App) {
let (tx, rx) = tokio::sync::watch::channel(false);
let conn = app.world().resource::<DbusConnection>().0.clone();
tokio::spawn(async move {
_ = conn
.object_server()
.at("/org/stardustxr/Server", FieldDebugGizmos { state: tx })
.await;
});
app.insert_resource(FieldDebugGizmosEnabled(rx));
app.add_systems(
Update,
draw_field_gizmos.run_if(|res: Res<FieldDebugGizmosEnabled>| *res.0.borrow()),
);
}
}
#[derive(Resource)]
struct FieldDebugGizmosEnabled(tokio::sync::watch::Receiver<bool>);
fn draw_field_gizmos(mut gizmos: Gizmos) {
FIELD_REGISTRY_DEBUG_GIZMOS
.get_valid_contents()
.iter()
.for_each(|f| {
let transform =
bevy::transform::components::Transform::from_matrix(f.spatial.global_transform());
let color = Color::srgb_u8(0x04, 0xFD, 0x4C);
match f.shape.lock().clone() {
Shape::Box(size) => gizmos.cuboid(transform.with_scale(size.into()), color),
Shape::Cylinder(CylinderShape { length, radius }) => {
gizmos
.primitive_3d(
&Cylinder {
radius,
half_height: length * 0.5,
},
transform.to_isometry(),
color,
)
.resolution(32);
}
Shape::Sphere(radius) => {
gizmos.sphere(transform.to_isometry(), radius, color);
}
Shape::Torus(TorusShape { radius_a, radius_b }) => {
let minor_radius;
let major_radius;
if radius_a >= radius_b {
major_radius = radius_a;
minor_radius = radius_b;
} else {
major_radius = radius_b;
minor_radius = radius_a;
}
gizmos
.primitive_3d(
&Torus {
minor_radius,
major_radius,
},
transform.to_isometry(),
color,
)
.minor_resolution(32)
.major_resolution(32);
}
}
});
}
struct FieldDebugGizmos {
state: tokio::sync::watch::Sender<bool>,
}
#[interface(name = "org.stardustxr.debug.FieldDebugGizmos")]
impl FieldDebugGizmos {
fn enable(&mut self) {
_ = self.state.send(true);
}
fn disable(&mut self) {
_ = self.state.send(false);
}
}
static FIELD_REGISTRY_DEBUG_GIZMOS: Registry<Field> = Registry::new();
stardust_xr_server_codegen::codegen_field_protocol!();
lazy_static::lazy_static! {
pub static ref EXPORTED_FIELDS: Mutex<FxHashMap<u64, Weak<Node>>> = Mutex::new(FxHashMap::default());
}
pub trait FieldTrait: Send + Sync + 'static {
fn spatial_ref(&self) -> &Spatial;
fn local_distance(&self, p: Vec3A) -> f32;
fn local_normal(&self, p: Vec3A, r: f32) -> Vec3A {
let d = self.local_distance(p);
let e = vec2(r, 0_f32);
let n = vec3a(d, d, d)
- vec3a(
self.local_distance(vec3a(e.x, e.y, e.y)),
self.local_distance(vec3a(e.y, e.x, e.y)),
self.local_distance(vec3a(e.y, e.y, e.x)),
);
n.normalize()
}
fn local_closest_point(&self, p: Vec3A, r: f32) -> Vec3A {
p - (self.local_normal(p, r) * self.local_distance(p))
}
fn distance(&self, reference_space: &Spatial, p: Vec3A) -> f32 {
let reference_to_local_space =
Spatial::space_to_space_matrix(Some(reference_space), Some(self.spatial_ref()));
let local_p = reference_to_local_space.transform_point3a(p);
self.local_distance(local_p)
}
fn normal(&self, reference_space: &Spatial, p: Vec3A, r: f32) -> Vec3A {
let reference_to_local_space =
Spatial::space_to_space_matrix(Some(reference_space), Some(self.spatial_ref()));
let local_p = reference_to_local_space.transform_point3a(p);
reference_to_local_space
.inverse()
.transform_vector3a(self.local_normal(local_p, r))
}
fn closest_point(&self, reference_space: &Spatial, p: Vec3A, r: f32) -> Vec3A {
let reference_to_local_space =
Spatial::space_to_space_matrix(Some(reference_space), Some(self.spatial_ref()));
let local_p = reference_to_local_space.transform_point3a(p);
reference_to_local_space
.inverse()
.transform_point3a(self.local_closest_point(local_p, r))
}
fn ray_march(&self, ray: Ray) -> RayMarchResult {
let mut result = RayMarchResult {
ray_origin: ray.origin.into(),
ray_direction: ray.direction.into(),
min_distance: f32::MAX,
deepest_point_distance: 0_f32,
ray_length: 0_f32,
ray_steps: 0,
};
let ray_to_field_matrix =
Spatial::space_to_space_matrix(Some(&ray.space), Some(self.spatial_ref()));
let mut ray_point = ray_to_field_matrix.transform_point3a(ray.origin.into());
let ray_direction = ray_to_field_matrix
.transform_vector3a(ray.direction.into())
.normalize();
while result.ray_steps < MAX_RAY_STEPS && result.ray_length < MAX_RAY_LENGTH {
let distance = self.local_distance(ray_point);
let march_distance = distance.clamp(MIN_RAY_MARCH, MAX_RAY_MARCH);
result.ray_length += march_distance;
ray_point += ray_direction * march_distance;
if result.min_distance > distance {
result.deepest_point_distance = result.ray_length;
result.min_distance = distance;
}
result.ray_steps += 1;
}
result
}
}
pub struct Ray {
pub origin: Vec3,
pub direction: Vec3,
pub space: Arc<Spatial>,
}
// const MIN_RAY_STEPS: u32 = 0;
const MAX_RAY_STEPS: u32 = 1000;
const MIN_RAY_MARCH: f32 = 0.001_f32;
const MAX_RAY_MARCH: f32 = f32::MAX;
// const MIN_RAY_LENGTH: f32 = 0_f32;
const MAX_RAY_LENGTH: f32 = 1000_f32;
pub struct Field {
pub spatial: Arc<Spatial>,
pub shape: Mutex<Shape>,
}
impl Field {
pub fn add_to(node: &Arc<Node>, shape: Shape) -> Result<Arc<Field>> {
let spatial = node.get_aspect::<Spatial>()?;
let field = Field {
spatial,
shape: Mutex::new(shape),
};
let field = node.add_aspect(field);
FIELD_REGISTRY_DEBUG_GIZMOS.add_raw(&field);
node.add_aspect(FieldRef);
Ok(field)
}
}
impl Drop for Field {
fn drop(&mut self) {
FIELD_REGISTRY_DEBUG_GIZMOS.remove(self);
}
}
impl AspectIdentifier for Field {
impl_aspect_for_field_aspect_id! {}
}
impl Aspect for Field {
impl_aspect_for_field_aspect! {}
}
impl FieldAspect for Field {
fn set_shape(node: Arc<Node>, _calling_client: Arc<Client>, shape: Shape) -> Result<()> {
let field = node.get_aspect::<Field>()?;
*field.shape.lock() = shape;
Ok(())
}
async fn export_field(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<u64> {
let id = rand::random();
EXPORTED_FIELDS.lock().insert(id, Arc::downgrade(&node));
Ok(id)
}
}
impl FieldTrait for Field {
fn spatial_ref(&self) -> &Spatial {
&self.spatial
}
fn local_distance(&self, p: Vec3A) -> f32 {
match self.shape.lock().clone() {
Shape::Box(size) => {
let q = vec3(
p.x.abs() - (size.x * 0.5_f32),
p.y.abs() - (size.y * 0.5_f32),
p.z.abs() - (size.z * 0.5_f32),
);
let v = vec3a(q.x.max(0_f32), q.y.max(0_f32), q.z.max(0_f32));
v.length() + q.x.max(q.y.max(q.z)).min(0_f32)
}
Shape::Cylinder(CylinderShape { length, radius }) => {
let d = vec2(p.xz().length().abs() - radius, p.y.abs() - (length * 0.5));
d.x.max(d.y).min(0.0) + d.max(vec2(0.0, 0.0)).length()
}
Shape::Sphere(radius) => p.length() - radius,
Shape::Torus(TorusShape { radius_a, radius_b }) => {
let q = vec2(p.xz().length() - radius_a, p.y);
q.length() - radius_b
}
}
}
}
pub struct FieldRef;
impl AspectIdentifier for FieldRef {
impl_aspect_for_field_ref_aspect_id! {}
}
impl Aspect for FieldRef {
impl_aspect_for_field_ref_aspect! {}
}
impl FieldRefAspect for FieldRef {
async fn distance(
node: Arc<Node>,
_calling_client: Arc<Client>,
space: Arc<Node>,
point: Vector3<f32>,
) -> Result<f32> {
let reference_space = space.get_aspect::<Spatial>()?;
let field = node.get_aspect::<Field>()?;
Ok(field.distance(&reference_space, point.into()))
}
async fn normal(
node: Arc<Node>,
_calling_client: Arc<Client>,
space: Arc<Node>,
point: Vector3<f32>,
) -> Result<Vector3<f32>> {
let reference_space = space.get_aspect::<Spatial>()?;
let field = node.get_aspect::<Field>()?;
Ok(field.normal(&reference_space, point.into(), 0.0001).into())
}
async fn closest_point(
node: Arc<Node>,
_calling_client: Arc<Client>,
space: Arc<Node>,
point: Vector3<f32>,
) -> Result<Vector3<f32>> {
let reference_space = space.get_aspect::<Spatial>()?;
let field = node.get_aspect::<Field>()?;
Ok(field
.closest_point(&reference_space, point.into(), 0.0001)
.into())
}
async fn ray_march(
node: Arc<Node>,
_calling_client: Arc<Client>,
space: Arc<Node>,
ray_origin: Vector3<f32>,
ray_direction: Vector3<f32>,
) -> Result<RayMarchResult> {
let space = space.get_aspect::<Spatial>()?;
let field = node.get_aspect::<Field>()?;
Ok(field.ray_march(Ray {
origin: ray_origin.into(),
direction: ray_direction.into(),
space,
}))
}
}
impl InterfaceAspect for Interface {
async fn import_field_ref(
_node: Arc<Node>,
calling_client: Arc<Client>,
uid: u64,
) -> Result<Arc<Node>> {
Ok(EXPORTED_FIELDS
.lock()
.get(&uid)
.and_then(|s| s.upgrade())
.map(|s| {
Alias::create(
&s,
&calling_client,
FIELD_REF_ASPECT_ALIAS_INFO.clone(),
None,
)
.unwrap()
})
.ok_or_eyre("Couldn't import field with that ID")?)
}
fn create_field(
_node: Arc<Node>,
calling_client: Arc<Client>,
id: u64,
parent: Arc<Node>,
transform: Transform,
shape: Shape,
) -> Result<()> {
let transform = transform.to_mat4(true, true, false);
let parent = parent.get_aspect::<Spatial>()?;
let node = Node::from_id(&calling_client, id, true).add_to_scenegraph()?;
Spatial::add_to(&node, Some(parent.clone()), transform, false);
Field::add_to(&node, shape)?;
Ok(())
}
}

View File

@@ -1,60 +0,0 @@
use super::{BoxFieldAspect, FieldTrait, Node};
use crate::nodes::fields::FieldAspect;
use crate::nodes::spatial::Spatial;
use crate::{core::client::Client, nodes::fields::Field};
use color_eyre::eyre::Result;
use glam::{vec3, vec3a, Vec3, Vec3A};
use mint::Vector3;
use parking_lot::Mutex;
use std::sync::Arc;
pub struct BoxField {
space: Arc<Spatial>,
size: Mutex<Vec3>,
}
impl BoxField {
pub fn add_to(node: &Arc<Node>, size: Vector3<f32>) {
let box_field = BoxField {
space: node.get_aspect::<Spatial>().unwrap().clone(),
size: Mutex::new(size.into()),
};
<BoxField as FieldAspect>::add_node_members(node);
<BoxField as BoxFieldAspect>::add_node_members(node);
node.add_aspect(Field::Box(box_field));
}
pub fn set_size(&self, size: Vector3<f32>) {
*self.size.lock() = size.into();
}
}
impl FieldTrait for BoxField {
fn local_distance(&self, p: Vec3A) -> f32 {
let size = self.size.lock();
let q = vec3(
p.x.abs() - (size.x * 0.5_f32),
p.y.abs() - (size.y * 0.5_f32),
p.z.abs() - (size.z * 0.5_f32),
);
let v = vec3a(q.x.max(0_f32), q.y.max(0_f32), q.z.max(0_f32));
v.length() + q.x.max(q.y.max(q.z)).min(0_f32)
}
fn spatial_ref(&self) -> &Spatial {
self.space.as_ref()
}
}
impl BoxFieldAspect for BoxField {
fn set_size(
node: Arc<Node>,
_calling_client: Arc<Client>,
size: mint::Vector3<f32>,
) -> Result<()> {
let this_field = node.get_aspect::<Field>()?;
let Field::Box(this_field) = &*this_field else {
return Ok(());
};
this_field.set_size(size.into());
Ok(())
}
}

View File

@@ -1,61 +0,0 @@
use super::{CylinderFieldAspect, Field, FieldTrait, Node};
use crate::core::client::Client;
use crate::nodes::fields::FieldAspect;
use crate::nodes::spatial::Spatial;
use color_eyre::eyre::Result;
use glam::{swizzles::*, vec2, Vec3A};
use portable_atomic::AtomicF32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub struct CylinderField {
space: Arc<Spatial>,
length: AtomicF32,
radius: AtomicF32,
}
impl CylinderField {
pub fn add_to(node: &Arc<Node>, length: f32, radius: f32) {
let cylinder_field = CylinderField {
space: node.get_aspect::<Spatial>().unwrap().clone(),
length: AtomicF32::new(length.abs()),
radius: AtomicF32::new(radius.abs()),
};
<CylinderField as FieldAspect>::add_node_members(node);
<CylinderField as CylinderFieldAspect>::add_node_members(node);
node.add_aspect(Field::Cylinder(cylinder_field));
}
pub fn set_size(&self, length: f32, radius: f32) {
self.length.store(length.abs(), Ordering::Relaxed);
self.radius.store(radius.abs(), Ordering::Relaxed);
}
}
impl FieldTrait for CylinderField {
fn local_distance(&self, p: Vec3A) -> f32 {
let radius = self.radius.load(Ordering::Relaxed);
let length = self.length.load(Ordering::Relaxed);
let d = vec2(p.xy().length().abs() - radius, p.z.abs() - (length * 0.5));
d.x.max(d.y).min(0.0) + d.max(vec2(0.0, 0.0)).length()
}
fn spatial_ref(&self) -> &Spatial {
self.space.as_ref()
}
}
impl CylinderFieldAspect for CylinderField {
fn set_size(
node: Arc<Node>,
_calling_client: Arc<Client>,
length: f32,
radius: f32,
) -> Result<()> {
let this_field = node.get_aspect::<Field>()?;
let Field::Cylinder(this_field) = &*this_field else {
return Ok(());
};
this_field.set_size(length, radius);
Ok(())
}
}

View File

@@ -1,369 +0,0 @@
pub mod r#box;
mod cylinder;
mod sphere;
mod torus;
use self::cylinder::CylinderField;
use self::r#box::BoxField;
use self::sphere::SphereField;
use self::torus::TorusField;
use super::alias::AliasInfo;
use super::spatial::Spatial;
use super::{Aspect, Node};
use crate::core::client::Client;
use crate::create_interface;
use crate::nodes::spatial::Transform;
use color_eyre::eyre::Result;
use glam::{vec2, vec3a, Mat4, Vec3, Vec3A};
use mint::Vector3;
use once_cell::sync::Lazy;
use std::ops::Deref;
use std::sync::Arc;
// TODO: get SDFs working properly with non-uniform scale and so on, output distance relative to the spatial it's compared against
pub static FIELD_ALIAS_INFO: Lazy<AliasInfo> = Lazy::new(|| AliasInfo {
server_methods: vec!["distance", "normal", "closest_point", "ray_march"],
..Default::default()
});
stardust_xr_server_codegen::codegen_field_protocol!();
pub trait FieldTrait: Send + Sync + 'static {
fn spatial_ref(&self) -> &Spatial;
fn local_distance(&self, p: Vec3A) -> f32;
fn local_normal(&self, p: Vec3A, r: f32) -> Vec3A {
let d = self.local_distance(p);
let e = vec2(r, 0_f32);
let n = vec3a(d, d, d)
- vec3a(
self.local_distance(vec3a(e.x, e.y, e.y)),
self.local_distance(vec3a(e.y, e.x, e.y)),
self.local_distance(vec3a(e.y, e.y, e.x)),
);
n.normalize()
}
fn local_closest_point(&self, p: Vec3A, r: f32) -> Vec3A {
p - (self.local_normal(p, r) * self.local_distance(p))
}
fn distance(&self, reference_space: &Spatial, p: Vec3A) -> f32 {
let reference_to_local_space =
Spatial::space_to_space_matrix(Some(reference_space), Some(self.spatial_ref()));
let local_p = reference_to_local_space.transform_point3a(p);
self.local_distance(local_p)
}
fn normal(&self, reference_space: &Spatial, p: Vec3A, r: f32) -> Vec3A {
let reference_to_local_space =
Spatial::space_to_space_matrix(Some(reference_space), Some(self.spatial_ref()));
let local_p = reference_to_local_space.transform_point3a(p);
reference_to_local_space
.inverse()
.transform_vector3a(self.local_normal(local_p, r))
}
fn closest_point(&self, reference_space: &Spatial, p: Vec3A, r: f32) -> Vec3A {
let reference_to_local_space =
Spatial::space_to_space_matrix(Some(reference_space), Some(self.spatial_ref()));
let local_p = reference_to_local_space.transform_point3a(p);
reference_to_local_space
.inverse()
.transform_point3a(self.local_closest_point(local_p, r))
}
fn ray_march(&self, ray: Ray) -> RayMarchResult {
let mut result = RayMarchResult {
ray_origin: ray.origin.into(),
ray_direction: ray.direction.into(),
min_distance: f32::MAX,
deepest_point_distance: 0_f32,
ray_length: 0_f32,
ray_steps: 0,
};
let ray_to_field_matrix =
Spatial::space_to_space_matrix(Some(&ray.space), Some(self.spatial_ref()));
let mut ray_point = ray_to_field_matrix.transform_point3a(ray.origin.into());
let ray_direction = ray_to_field_matrix
.transform_vector3a(ray.direction.into())
.normalize();
while result.ray_steps < MAX_RAY_STEPS && result.ray_length < MAX_RAY_LENGTH {
let distance = self.local_distance(ray_point);
let march_distance = distance.clamp(MIN_RAY_MARCH, MAX_RAY_MARCH);
result.ray_length += march_distance;
ray_point += ray_direction * march_distance;
if result.min_distance > distance {
result.deepest_point_distance = result.ray_length;
result.min_distance = distance;
}
result.ray_steps += 1;
}
result
}
}
impl<Fi: FieldTrait + 'static> FieldAspect for Fi {
async fn distance(
node: Arc<Node>,
_calling_client: Arc<Client>,
space: Arc<Node>,
point: mint::Vector3<f32>,
) -> Result<f32> {
let reference_space = space.get_aspect::<Spatial>()?;
let this_field = node.get_aspect::<Field>()?;
Ok((*this_field).distance(reference_space.as_ref(), point.into()))
}
async fn normal(
node: Arc<Node>,
_calling_client: Arc<Client>,
space: Arc<Node>,
point: mint::Vector3<f32>,
) -> Result<Vector3<f32>> {
let reference_space = space.get_aspect::<Spatial>()?;
let this_field = node.get_aspect::<Field>()?;
Ok(this_field
.normal(reference_space.as_ref(), point.into(), 0.001)
.into())
}
async fn closest_point(
node: Arc<Node>,
_calling_client: Arc<Client>,
space: Arc<Node>,
point: mint::Vector3<f32>,
) -> Result<Vector3<f32>> {
let reference_space = space.get_aspect::<Spatial>()?;
let this_field = node.get_aspect::<Field>()?;
Ok(this_field
.closest_point(reference_space.as_ref(), point.into(), 0.001)
.into())
}
async fn ray_march(
node: Arc<Node>,
_calling_client: Arc<Client>,
space: Arc<Node>,
ray_origin: mint::Vector3<f32>,
ray_direction: mint::Vector3<f32>,
) -> Result<RayMarchResult> {
let reference_space = space.get_aspect::<Spatial>()?;
let this_field = node.get_aspect::<Field>()?;
Ok(this_field.ray_march(Ray {
origin: ray_origin.into(),
direction: ray_direction.into(),
space: reference_space.clone(),
}))
}
}
pub struct Ray {
pub origin: Vec3,
pub direction: Vec3,
pub space: Arc<Spatial>,
}
// const MIN_RAY_STEPS: u32 = 0;
const MAX_RAY_STEPS: u32 = 1000;
const MIN_RAY_MARCH: f32 = 0.001_f32;
const MAX_RAY_MARCH: f32 = f32::MAX;
// const MIN_RAY_LENGTH: f32 = 0_f32;
const MAX_RAY_LENGTH: f32 = 1000_f32;
pub enum Field {
Box(BoxField),
Cylinder(CylinderField),
Sphere(SphereField),
Torus(TorusField),
}
impl Aspect for Field {
const NAME: &'static str = "Field";
}
impl Deref for Field {
type Target = dyn FieldTrait;
fn deref(&self) -> &Self::Target {
match self {
Field::Box(field) => field,
Field::Cylinder(field) => field,
Field::Sphere(field) => field,
Field::Torus(field) => field,
}
}
}
// impl FieldTrait for Field {
// fn spatial_ref(&self) -> &Spatial {
// match self {
// Field::Box(field) => field.spatial_ref(),
// Field::Cylinder(field) => field.spatial_ref(),
// Field::Sphere(field) => field.spatial_ref(),
// Field::Torus(field) => field.spatial_ref(),
// }
// }
// fn local_distance(&self, p: Vec3A) -> f32 {
// match self {
// Field::Box(field) => field.local_distance(p),
// Field::Cylinder(field) => field.local_distance(p),
// Field::Sphere(field) => field.local_distance(p),
// Field::Torus(field) => field.local_distance(p),
// }
// }
// fn local_normal(&self, p: Vec3A, r: f32) -> Vec3A {
// match self {
// Field::Box(field) => field.local_normal(p, r),
// Field::Cylinder(field) => field.local_normal(p, r),
// Field::Sphere(field) => field.local_normal(p, r),
// Field::Torus(field) => field.local_normal(p, r),
// }
// }
// fn local_closest_point(&self, p: Vec3A, r: f32) -> Vec3A {
// match self {
// Field::Box(field) => field.local_closest_point(p, r),
// Field::Cylinder(field) => field.local_closest_point(p, r),
// Field::Sphere(field) => field.local_closest_point(p, r),
// Field::Torus(field) => field.local_closest_point(p, r),
// }
// }
// fn distance(&self, reference_space: &Spatial, p: Vec3A) -> f32 {
// match self {
// Field::Box(field) => field.distance(reference_space, p),
// Field::Cylinder(field) => field.distance(reference_space, p),
// Field::Sphere(field) => field.distance(reference_space, p),
// Field::Torus(field) => field.distance(reference_space, p),
// }
// }
// fn normal(&self, reference_space: &Spatial, p: Vec3A, r: f32) -> Vec3A {
// match self {
// Field::Box(field) => field.normal(reference_space, p, r),
// Field::Cylinder(field) => field.normal(reference_space, p, r),
// Field::Sphere(field) => field.normal(reference_space, p, r),
// Field::Torus(field) => field.normal(reference_space, p, r),
// }
// }
// fn closest_point(&self, reference_space: &Spatial, p: Vec3A, r: f32) -> Vec3A {
// match self {
// Field::Box(field) => field.closest_point(reference_space, p, r),
// Field::Cylinder(field) => field.closest_point(reference_space, p, r),
// Field::Sphere(field) => field.closest_point(reference_space, p, r),
// Field::Torus(field) => field.closest_point(reference_space, p, r),
// }
// }
// fn ray_march(&self, ray: Ray) -> RayMarchResult {
// match self {
// Field::Box(field) => field.ray_march(ray),
// Field::Cylinder(field) => field.ray_march(ray),
// Field::Sphere(field) => field.ray_march(ray),
// Field::Torus(field) => field.ray_march(ray),
// }
// }
// }
create_interface!(FieldInterface, FieldInterfaceAspect, "/field");
pub struct FieldInterface;
impl FieldInterfaceAspect for FieldInterface {
fn create_box_field(
_node: Arc<Node>,
calling_client: Arc<Client>,
name: String,
parent: Arc<Node>,
transform: Transform,
size: mint::Vector3<f32>,
) -> Result<()> {
let transform = transform.to_mat4(true, true, false);
let parent = parent.get_aspect::<Spatial>()?;
let node = Node::create_parent_name(
&calling_client,
Self::CREATE_BOX_FIELD_PARENT_PATH,
&name,
true,
)
.add_to_scenegraph()?;
Spatial::add_to(&node, Some(parent.clone()), transform, false);
BoxField::add_to(&node, size);
Ok(())
}
fn create_cylinder_field(
_node: Arc<Node>,
calling_client: Arc<Client>,
name: String,
parent: Arc<Node>,
transform: Transform,
length: f32,
radius: f32,
) -> Result<()> {
let transform = transform.to_mat4(true, true, false);
let parent = parent.get_aspect::<Spatial>()?;
let node = Node::create_parent_name(
&calling_client,
Self::CREATE_CYLINDER_FIELD_PARENT_PATH,
&name,
true,
)
.add_to_scenegraph()?;
Spatial::add_to(&node, Some(parent.clone()), transform, false);
CylinderField::add_to(&node, length, radius);
Ok(())
}
fn create_sphere_field(
_node: Arc<Node>,
calling_client: Arc<Client>,
name: String,
parent: Arc<Node>,
position: mint::Vector3<f32>,
radius: f32,
) -> Result<()> {
let parent = parent.get_aspect::<Spatial>()?;
let node = Node::create_parent_name(
&calling_client,
Self::CREATE_SPHERE_FIELD_PARENT_PATH,
&name,
true,
)
.add_to_scenegraph()?;
Spatial::add_to(
&node,
Some(parent.clone()),
Mat4::from_translation(position.into()),
false,
);
SphereField::add_to(&node, radius);
Ok(())
}
fn create_torus_field(
_node: Arc<Node>,
calling_client: Arc<Client>,
name: String,
parent: Arc<Node>,
transform: Transform,
radius_a: f32,
radius_b: f32,
) -> Result<()> {
let transform = transform.to_mat4(true, true, false);
let parent = parent.get_aspect::<Spatial>()?;
let node = Node::create_parent_name(
&calling_client,
Self::CREATE_TORUS_FIELD_PARENT_PATH,
&name,
true,
)
.add_to_scenegraph()?;
Spatial::add_to(&node, Some(parent.clone()), transform, false);
TorusField::add_to(&node, radius_a, radius_b);
Ok(())
}
}
pub fn find_field(client: &Client, path: &str) -> Result<Arc<Field>> {
client.get_node("Field", path)?.get_aspect::<Field>()
}

View File

@@ -1,55 +0,0 @@
use super::{Field, FieldTrait, Node, SphereFieldAspect};
use crate::core::client::Client;
use crate::nodes::fields::FieldAspect;
use crate::nodes::spatial::Spatial;
use color_eyre::eyre::Result;
use glam::Vec3A;
use portable_atomic::AtomicF32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub struct SphereField {
space: Arc<Spatial>,
radius: AtomicF32,
}
impl SphereField {
pub fn add_to(node: &Arc<Node>, radius: f32) {
let sphere_field = SphereField {
space: node.get_aspect::<Spatial>().unwrap().clone(),
radius: AtomicF32::new(radius),
};
<SphereField as FieldAspect>::add_node_members(node);
<SphereField as SphereFieldAspect>::add_node_members(node);
node.add_aspect(Field::Sphere(sphere_field));
}
pub fn set_radius(&self, radius: f32) {
self.radius.store(radius, Ordering::Relaxed);
}
}
impl FieldTrait for SphereField {
fn local_distance(&self, p: Vec3A) -> f32 {
p.length() - self.radius.load(Ordering::Relaxed)
}
fn local_normal(&self, p: Vec3A, _r: f32) -> Vec3A {
-p.normalize()
}
fn local_closest_point(&self, p: Vec3A, _r: f32) -> Vec3A {
p.normalize() * self.radius.load(Ordering::Relaxed)
}
fn spatial_ref(&self) -> &Spatial {
self.space.as_ref()
}
}
impl SphereFieldAspect for SphereField {
fn set_radius(node: Arc<Node>, _calling_client: Arc<Client>, radius: f32) -> Result<()> {
let this_field = node.get_aspect::<Field>()?;
let Field::Sphere(this_field) = &*this_field else {
return Ok(());
};
this_field.set_radius(radius);
Ok(())
}
}

View File

@@ -1,60 +0,0 @@
use super::{Field, FieldTrait, Node, TorusFieldAspect};
use crate::core::client::Client;
use crate::nodes::fields::FieldAspect;
use crate::nodes::spatial::Spatial;
use color_eyre::eyre::Result;
use glam::{swizzles::*, vec2, Vec3A};
use portable_atomic::AtomicF32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub struct TorusField {
space: Arc<Spatial>,
radius_a: AtomicF32,
radius_b: AtomicF32,
}
impl TorusField {
pub fn add_to(node: &Arc<Node>, radius_a: f32, radius_b: f32) {
let torus_field = TorusField {
space: node.get_aspect::<Spatial>().unwrap().clone(),
radius_a: AtomicF32::new(radius_a.abs()),
radius_b: AtomicF32::new(radius_b.abs()),
};
<TorusField as FieldAspect>::add_node_members(node);
<TorusField as TorusFieldAspect>::add_node_members(node);
node.add_aspect(Field::Torus(torus_field));
}
pub fn set_size(&self, radius_a: f32, radius_b: f32) {
self.radius_a.store(radius_a.abs(), Ordering::Relaxed);
self.radius_b.store(radius_b.abs(), Ordering::Relaxed);
}
}
impl FieldTrait for TorusField {
fn local_distance(&self, p: Vec3A) -> f32 {
let radius_a = self.radius_a.load(Ordering::Relaxed);
let radius_b = self.radius_b.load(Ordering::Relaxed);
let q = vec2(p.xz().length() - radius_a, p.y);
q.length() - radius_b
}
fn spatial_ref(&self) -> &Spatial {
self.space.as_ref()
}
}
impl TorusFieldAspect for TorusField {
fn set_size(
node: Arc<Node>,
_calling_client: Arc<Client>,
radius_a: f32,
radius_b: f32,
) -> Result<()> {
let this_field = node.get_aspect::<Field>()?;
let Field::Torus(this_field) = &*this_field else {
return Ok(());
};
this_field.set_size(radius_a, radius_b);
Ok(())
}
}

View File

@@ -1,43 +0,0 @@
use super::{alias::Alias, spatial::Spatial, Node};
use crate::{
core::client::{Client, INTERNAL_CLIENT},
nodes::alias::AliasInfo,
};
use color_eyre::eyre::Result;
use glam::{vec3, Mat4};
use std::sync::Arc;
use stereokit::StereoKitMultiThread;
lazy_static::lazy_static! {
static ref HMD: Arc<Node> = create();
}
fn create() -> Arc<Node> {
let node = Arc::new(Node::create_parent_name(&INTERNAL_CLIENT, "", "hmd", false));
Spatial::add_to(&node, None, Mat4::IDENTITY, false);
node
}
pub fn frame(sk: &impl StereoKitMultiThread) {
let spatial = HMD.get_aspect::<Spatial>().unwrap();
let hmd_pose = sk.input_head();
*spatial.transform.lock() = Mat4::from_scale_rotation_translation(
vec3(1.0, 1.0, 1.0),
hmd_pose.orientation.into(),
hmd_pose.position.into(),
);
}
pub fn make_alias(client: &Arc<Client>) -> Result<Arc<Node>> {
Alias::create(
client,
"",
"hmd",
&HMD,
AliasInfo {
server_signals: vec!["get_bounds", "get_transform"],
..Default::default()
},
)
}

View File

@@ -1,51 +1,89 @@
use crate::nodes::fields::Field;
use super::{Finger, Hand, InputDataTrait, InputHandler, InputMethod, Joint, Thumb};
use crate::nodes::fields::{Field, FieldTrait};
use crate::nodes::spatial::Spatial;
use glam::{vec3a, Mat4};
use stardust_xr::schemas::flat::{Hand as FlatHand, InputDataType, Joint};
use glam::{Mat4, Quat, vec3a};
use std::sync::Arc;
use super::{DistanceLink, InputSpecialization};
#[derive(Debug, Default)]
pub struct Hand {
pub base: FlatHand,
}
impl InputSpecialization for Hand {
fn compare_distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
self.true_distance(space, field).abs()
impl Default for Joint {
fn default() -> Self {
Joint {
position: [0.0; 3].into(),
rotation: Quat::IDENTITY.into(),
radius: 0.0,
distance: 0.0,
}
}
fn true_distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
}
#[allow(clippy::derivable_impls)]
impl Default for Finger {
fn default() -> Self {
Finger {
tip: Default::default(),
distal: Default::default(),
intermediate: Default::default(),
proximal: Default::default(),
metacarpal: Default::default(),
}
}
}
#[allow(clippy::derivable_impls)]
impl Default for Thumb {
fn default() -> Self {
Thumb {
tip: Default::default(),
distal: Default::default(),
proximal: Default::default(),
metacarpal: Default::default(),
}
}
}
#[allow(clippy::derivable_impls)]
impl Default for Hand {
fn default() -> Self {
Hand {
right: Default::default(),
thumb: Default::default(),
index: Default::default(),
middle: Default::default(),
ring: Default::default(),
little: Default::default(),
palm: Default::default(),
wrist: Default::default(),
elbow: Default::default(),
}
}
}
impl InputDataTrait for Hand {
fn distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
let mut min_distance = f32::MAX;
for tip in [
&self.base.thumb.tip.position,
&self.base.index.tip.position,
&self.base.middle.tip.position,
&self.base.ring.tip.position,
&self.base.little.tip.position,
&self.thumb.tip.position,
&self.index.tip.position,
&self.middle.tip.position,
&self.ring.tip.position,
&self.little.tip.position,
] {
min_distance = min_distance.min(field.distance(space, vec3a(tip.x, tip.y, tip.z)));
}
min_distance
}
fn serialize(
&self,
distance_link: &DistanceLink,
local_to_handler_matrix: Mat4,
) -> InputDataType {
let mut hand = self.base;
fn transform(&mut self, method: &InputMethod, handler: &InputHandler) {
let local_to_handler_matrix =
Spatial::space_to_space_matrix(Some(&method.spatial), Some(&handler.spatial));
let mut joints: Vec<&mut Joint> = Vec::new();
joints.extend([&mut hand.palm, &mut hand.wrist]);
if let Some(elbow) = &mut hand.elbow {
joints.extend([&mut self.palm, &mut self.wrist]);
if let Some(elbow) = &mut self.elbow {
joints.push(elbow);
}
for finger in [
&mut hand.index,
&mut hand.middle,
&mut hand.ring,
&mut hand.little,
&mut self.index,
&mut self.middle,
&mut self.ring,
&mut self.little,
] {
joints.extend([
&mut finger.tip,
@@ -56,10 +94,10 @@ impl InputSpecialization for Hand {
]);
}
joints.extend([
&mut hand.thumb.tip,
&mut hand.thumb.distal,
&mut hand.thumb.proximal,
&mut hand.thumb.metacarpal,
&mut self.thumb.tip,
&mut self.thumb.distal,
&mut self.thumb.proximal,
&mut self.thumb.metacarpal,
]);
for joint in joints {
@@ -68,12 +106,7 @@ impl InputSpecialization for Hand {
let (_, rotation, position) = joint_matrix.to_scale_rotation_translation();
joint.position = position.into();
joint.rotation = rotation.into();
joint.distance = distance_link
.handler
.field
.distance(&distance_link.handler.spatial, position.into());
joint.distance = handler.field.distance(&handler.spatial, position.into());
}
InputDataType::Hand(Box::new(hand))
}
}

View File

@@ -0,0 +1,39 @@
use super::{INPUT_HANDLER_REGISTRY, INPUT_METHOD_REGISTRY, InputHandlerAspect};
use crate::nodes::{Node, alias::AliasList, fields::Field, spatial::Spatial};
use color_eyre::eyre::Result;
use std::sync::Arc;
pub struct InputHandler {
pub spatial: Arc<Spatial>,
pub field: Arc<Field>,
pub(super) method_aliases: AliasList,
}
impl InputHandler {
pub fn add_to(node: &Arc<Node>, field: &Arc<Field>) -> Result<()> {
let handler = InputHandler {
spatial: node.get_aspect::<Spatial>().unwrap().clone(),
field: field.clone(),
method_aliases: AliasList::default(),
};
for method in INPUT_METHOD_REGISTRY.get_valid_contents() {
method.handle_new_handler(&handler);
}
let handler = INPUT_HANDLER_REGISTRY.add(handler);
node.add_aspect_raw(handler);
Ok(())
}
}
impl InputHandlerAspect for InputHandler {}
impl PartialEq for InputHandler {
fn eq(&self, other: &Self) -> bool {
self.spatial == other.spatial
}
}
impl Drop for InputHandler {
fn drop(&mut self) {
INPUT_HANDLER_REGISTRY.remove(self);
for method in INPUT_METHOD_REGISTRY.get_valid_contents() {
method.handle_drop_handler(self);
}
}
}

280
src/nodes/input/method.rs Normal file
View File

@@ -0,0 +1,280 @@
use super::{
INPUT_HANDLER_REGISTRY, INPUT_METHOD_REF_ASPECT_ALIAS_INFO, INPUT_METHOD_REGISTRY, InputData,
InputDataTrait, InputDataType, InputHandler, InputMethodAspect, InputMethodRefAspect,
input_method_client,
};
use crate::{
core::{
client::Client,
error::{Result, ServerError},
registry::Registry,
},
nodes::{
Node,
alias::{Alias, AliasList},
fields::{FIELD_ALIAS_INFO, Field},
spatial::Spatial,
},
};
use color_eyre::eyre::eyre;
use parking_lot::Mutex;
use stardust_xr::values::Datamap;
use std::sync::{Arc, Weak};
pub struct InputMethod {
pub spatial: Arc<Spatial>,
pub data: Mutex<InputDataType>,
pub datamap: Mutex<Datamap>,
handler_aliases: AliasList,
handler_field_aliases: AliasList,
pub(super) handler_order: Mutex<Vec<Weak<InputHandler>>>,
pub capture_attempts: Registry<InputHandler>,
pub captures: Registry<InputHandler>,
}
impl InputMethod {
pub fn add_to(
node: &Arc<Node>,
data: InputDataType,
datamap: Datamap,
) -> Result<Arc<InputMethod>> {
let method = InputMethod {
spatial: node.get_aspect::<Spatial>().unwrap().clone(),
data: Mutex::new(data),
datamap: Mutex::new(datamap),
handler_aliases: AliasList::default(),
handler_field_aliases: AliasList::default(),
handler_order: Mutex::new(Vec::new()),
capture_attempts: Registry::new(),
captures: Registry::new(),
};
for handler in INPUT_HANDLER_REGISTRY.get_valid_contents() {
method.handle_new_handler(&handler);
}
let method = INPUT_METHOD_REGISTRY.add(method);
node.add_aspect_raw(method.clone());
node.add_aspect(InputMethodRef);
Ok(method)
}
pub fn distance(&self, to: &Field) -> f32 {
self.data.lock().distance(&self.spatial, to)
}
pub fn set_handler_order<'a>(&self, handlers: impl Iterator<Item = &'a Arc<InputHandler>>) {
*self.handler_order.lock() = handlers.map(Arc::downgrade).collect();
}
pub(super) fn make_alias(&self, handler: &InputHandler) {
let Some(method_node) = self.spatial.node() else {
return;
};
let Some(handler_node) = handler.spatial.node() else {
return;
};
let Some(client) = handler_node.get_client() else {
return;
};
let Ok(method_alias) = Alias::create(
&method_node,
&client,
INPUT_METHOD_REF_ASPECT_ALIAS_INFO.clone(),
Some(&handler.method_aliases),
) else {
return;
};
method_alias.set_enabled(false);
}
pub(super) fn handle_new_handler(&self, handler: &InputHandler) {
self.make_alias(handler);
let Some(method_node) = self.spatial.node() else {
return;
};
let Some(method_client) = method_node.get_client() else {
return;
};
let Some(handler_node) = handler.spatial.node() else {
return;
};
// Receiver itself
let Ok(handler_alias) = Alias::create(
&handler_node,
&method_client,
INPUT_METHOD_REF_ASPECT_ALIAS_INFO.clone(),
Some(&self.handler_aliases),
) else {
return;
};
let Some(handler_field_node) = handler.field.spatial.node() else {
return;
};
// Handler's field
let Ok(rx_field_alias) = Alias::create(
&handler_field_node,
&method_client,
FIELD_ALIAS_INFO.clone(),
Some(&self.handler_field_aliases),
) else {
return;
};
let _ = input_method_client::create_handler(&method_node, &handler_alias, &rx_field_alias);
}
pub(super) fn handle_drop_handler(&self, handler: &InputHandler) {
let Some(tx_node) = self.spatial.node() else {
return;
};
let Some(handler_alias) = self.handler_aliases.get_from_aspect(handler) else {
return;
};
let _ = input_method_client::destroy_handler(&tx_node, handler_alias.id);
self.handler_aliases.remove_aspect(handler);
self.handler_field_aliases
.remove_aspect(handler.field.as_ref());
self.capture_attempts.remove(handler);
}
#[tracing::instrument(level = "trace", skip(self, handler))]
pub(super) fn serialize(&self, alias_id: u64, handler: &Arc<InputHandler>) -> InputData {
let mut input = self.data.lock().clone();
input.transform(self, handler);
InputData {
id: alias_id,
input,
distance: self.distance(&handler.field),
datamap: self.datamap.lock().clone(),
order: self
.handler_order
.lock()
.iter()
.enumerate()
.find(|(_, h)| h.ptr_eq(&Arc::downgrade(handler)))
.unwrap()
.0 as u32,
captured: self.captures.get_valid_contents().contains(handler),
}
}
pub(super) fn cull_capture_attempts(&self) {
let sent = self
.handler_order
.lock()
.iter()
.filter_map(Weak::upgrade)
.collect::<Registry<InputHandler>>();
self.capture_attempts.retain(|handler| {
!handler
.spatial
.node()
.and_then(|n| n.get_client())
.map(|c| c.unresponsive())
.unwrap_or(false)
&& sent.contains(handler)
});
}
}
impl InputMethodAspect for InputMethod {
#[doc = "Set the spatial input component of this input method. You must keep the same input data type throughout the entire thing."]
fn set_input(
node: Arc<Node>,
_calling_client: Arc<Client>,
input: InputDataType,
) -> Result<()> {
let input_method = node.get_aspect::<InputMethod>()?;
*input_method.data.lock() = input;
Ok(())
}
#[doc = "Set the datmap of this input method"]
fn set_datamap(node: Arc<Node>, _calling_client: Arc<Client>, datamap: Datamap) -> Result<()> {
let input_method = node.get_aspect::<InputMethod>()?;
*input_method.datamap.lock() = datamap;
Ok(())
}
#[doc = "Manually set the order of handlers to propagate input to, or else let the server decide."]
fn set_handler_order(
node: Arc<Node>,
_calling_client: Arc<Client>,
handlers: Vec<Arc<Node>>,
) -> Result<()> {
let input_method = node.get_aspect::<InputMethod>()?;
let handlers = handlers
.into_iter()
.filter_map(|p| p.get_aspect::<InputHandler>().ok())
.map(|i| Arc::downgrade(&i))
.collect::<Vec<_>>();
*input_method.handler_order.lock() = handlers;
Ok(())
}
#[doc = "Set which handlers are captured."]
fn set_captures(
node: Arc<Node>,
_calling_client: Arc<Client>,
handlers: Vec<Arc<Node>>,
) -> Result<()> {
let input_method = node.get_aspect::<InputMethod>()?;
input_method.captures.clear();
for handler in handlers {
let Ok(handler) = handler.get_aspect::<InputHandler>() else {
continue;
};
input_method.captures.add_raw(&handler);
}
Ok(())
}
}
impl Drop for InputMethod {
fn drop(&mut self) {
INPUT_METHOD_REGISTRY.remove(self);
}
}
pub struct InputMethodRef;
impl InputMethodRefAspect for InputMethodRef {
#[doc = "Try to capture the input method with the given handler. When the handler does not get input from the method, it will be released."]
fn try_capture(
node: Arc<Node>,
_calling_client: Arc<Client>,
handler: Arc<Node>,
) -> Result<()> {
let input_method = node.get_aspect::<InputMethod>()?;
let input_handler = handler.get_aspect::<InputHandler>()?;
input_method.capture_attempts.add_raw(&input_handler);
let Some(handler_alias) = input_method
.handler_aliases
.get_from_aspect(&*input_handler)
else {
return Err(ServerError::Report(eyre!(
"Internal: Couldn't get handler alias somehow?"
)));
};
input_method_client::request_capture_handler(&node, handler_alias.get_id())
}
#[doc = "If captured by this handler, release it (e.g. the object is let go of after grabbing)."]
fn release(node: Arc<Node>, _calling_client: Arc<Client>, handler: Arc<Node>) -> Result<()> {
let input_method = node.get_aspect::<InputMethod>()?;
let input_handler = handler.get_aspect::<InputHandler>()?;
input_method.capture_attempts.remove(&input_handler);
let Some(handler_alias) = input_method
.handler_aliases
.get_from_aspect(&*input_handler)
else {
return Err(ServerError::Report(eyre!(
"Internal: Couldn't get handler alias somehow?"
)));
};
input_method_client::release_handler(&node, handler_alias.get_id())
}
}

View File

@@ -1,462 +1,182 @@
pub mod hand;
pub mod pointer;
pub mod tip;
#![allow(clippy::needless_question_mark)]
use self::hand::Hand;
use self::pointer::Pointer;
use self::tip::Tip;
mod hand;
mod handler;
mod method;
mod pointer;
mod tip;
use super::{
alias::{Alias, AliasInfo},
fields::{find_field, Field, FIELD_ALIAS_INFO},
spatial::{parse_transform, Spatial},
Aspect, Message, Node,
};
use crate::core::{client::Client, node_collections::LifeLinkedNodeMap};
use bevy::tasks::ComputeTaskPool;
use bevy::tasks::ParallelSlice;
pub use handler::*;
pub use method::*;
use tracing::debug_span;
use super::Aspect;
use super::AspectIdentifier;
use super::fields::Field;
use super::spatial::Spatial;
use crate::core::error::Result;
use crate::nodes::spatial::SPATIAL_ASPECT_ALIAS_INFO;
use crate::nodes::spatial::SPATIAL_REF_ASPECT_ALIAS_INFO;
use crate::{core::client::Client, nodes::Node};
use crate::{core::registry::Registry, nodes::spatial::Transform};
use color_eyre::eyre::Result;
use glam::Mat4;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use portable_atomic::AtomicBool;
use serde::Deserialize;
use stardust_xr::schemas::{flat::InputDataType, flex::serialize};
use stardust_xr::{
schemas::{flat::InputData, flex::deserialize},
values::Datamap,
};
use std::ops::Deref;
use std::sync::atomic::Ordering;
use std::sync::{Arc, Weak};
use tracing::{debug_span, instrument};
use stardust_xr::values::Datamap;
use std::sync::Arc;
static INPUT_METHOD_REGISTRY: Registry<InputMethod> = Registry::new();
static INPUT_HANDLER_REGISTRY: Registry<InputHandler> = Registry::new();
pub static INPUT_HANDLER_REGISTRY: Registry<InputHandler> = Registry::new();
pub trait InputSpecialization: Send + Sync {
fn compare_distance(&self, space: &Arc<Spatial>, field: &Field) -> f32;
fn true_distance(&self, space: &Arc<Spatial>, field: &Field) -> f32;
fn serialize(
&self,
distance_link: &DistanceLink,
local_to_handler_matrix: Mat4,
) -> InputDataType;
}
pub enum InputType {
Pointer(Pointer),
Hand(Box<Hand>),
Tip(Tip),
}
impl Deref for InputType {
type Target = dyn InputSpecialization;
fn deref(&self) -> &Self::Target {
match self {
InputType::Pointer(p) => p,
InputType::Hand(h) => h.as_ref(),
InputType::Tip(t) => t,
}
}
}
stardust_xr_server_codegen::codegen_input_protocol!();
pub struct InputMethod {
node: Weak<Node>,
uid: String,
pub enabled: Mutex<bool>,
pub spatial: Arc<Spatial>,
pub specialization: Mutex<InputType>,
captures: Registry<InputHandler>,
pub datamap: Mutex<Option<Datamap>>,
handler_aliases: LifeLinkedNodeMap<String>,
handler_order: OnceCell<Mutex<Vec<Weak<InputHandler>>>>,
}
impl InputMethod {
#[allow(dead_code)]
pub fn add_to(
node: &Arc<Node>,
specialization: InputType,
datamap: Option<Datamap>,
) -> Result<Arc<InputMethod>> {
node.add_local_signal("capture", InputMethod::capture_flex);
node.add_local_signal("set_datamap", InputMethod::set_datamap_flex);
node.add_local_signal("set_handlers", InputMethod::set_handlers_flex);
let method = InputMethod {
node: Arc::downgrade(node),
uid: node.uid.clone(),
enabled: Mutex::new(true),
spatial: node.get_aspect::<Spatial>().unwrap().clone(),
specialization: Mutex::new(specialization),
captures: Registry::new(),
datamap: Mutex::new(datamap),
handler_aliases: LifeLinkedNodeMap::default(),
handler_order: OnceCell::new(),
};
for handler in INPUT_HANDLER_REGISTRY.get_valid_contents() {
method.handle_new_handler(&handler);
method.make_alias(&handler);
}
let method = INPUT_METHOD_REGISTRY.add(method);
node.add_aspect_raw(method.clone());
Ok(method)
}
fn get(node: &Node) -> Result<Arc<Self>> {
node.get_aspect::<Self>()
}
fn capture_flex(node: Arc<Node>, calling_client: Arc<Client>, message: Message) -> Result<()> {
let method = InputMethod::get(&node)?;
let handler = InputHandler::find(&calling_client, deserialize(message.as_ref())?)?;
method.captures.add_raw(&handler);
node.send_remote_signal("capture", message)
}
fn set_datamap_flex(
node: Arc<Node>,
_calling_client: Arc<Client>,
message: Message,
) -> Result<()> {
let method = InputMethod::get(&node)?;
method
.datamap
.lock()
.replace(Datamap::from_raw(message.data)?);
Ok(())
}
fn set_handlers_flex(
node: Arc<Node>,
calling_client: Arc<Client>,
message: Message,
) -> Result<()> {
let method = InputMethod::get(&node)?;
let handler_paths: Vec<&str> = deserialize(message.as_ref())?;
let handlers: Vec<Weak<InputHandler>> = handler_paths
.into_iter()
.filter_map(|p| InputHandler::find(&calling_client, p).ok())
.map(|h| Arc::downgrade(&h))
.collect();
*method
.handler_order
.get_or_init(|| Mutex::new(Vec::new()))
.lock() = handlers;
Ok(())
}
fn make_alias(&self, handler: &InputHandler) {
let Some(method_node) = self.node.upgrade() else {
return;
};
let Some(handler_node) = handler.node.upgrade() else {
return;
};
let Some(client) = handler_node.get_client() else {
return;
};
let Ok(method_alias) = Alias::create(
&client,
handler_node.get_path(),
&self.uid,
&method_node,
AliasInfo {
server_signals: vec!["capture"],
..Default::default()
},
) else {
return;
};
method_alias.enabled.store(false, Ordering::Relaxed);
handler
.method_aliases
.add(self as *const InputMethod as usize, &method_alias);
}
fn compare_distance(&self, to: &InputHandler) -> f32 {
let distance = self
.specialization
.lock()
.compare_distance(&self.spatial, &to.field);
if self.captures.contains(to) {
distance * 0.5
} else {
distance
}
}
fn true_distance(&self, to: &Field) -> f32 {
self.specialization.lock().true_distance(&self.spatial, to)
}
fn handle_new_handler(&self, handler: &InputHandler) {
let Some(method_node) = self.node.upgrade() else {
return;
};
let Some(method_client) = method_node.get_client() else {
return;
};
let Some(handler_node) = handler.node.upgrade() else {
return;
};
// Receiver itself
let Ok(handler_alias) = Alias::create(
&method_client,
method_node.get_path(),
handler.uid.as_str(),
&handler_node,
AliasInfo {
server_methods: vec!["get_transform"],
..Default::default()
},
) else {
return;
};
self.handler_aliases
.add(handler.uid.clone(), &handler_alias);
if let Some(handler_field_node) = handler.field.spatial_ref().node.upgrade() {
// Handler's field
let Ok(rx_field_alias) = Alias::create(
&method_client,
handler_alias.get_path(),
"field",
&handler_field_node,
FIELD_ALIAS_INFO.clone(),
) else {
return;
};
self.handler_aliases
.add(handler.uid.clone() + "-field", &rx_field_alias);
}
let Ok(data) = serialize(&handler.uid) else {
return;
};
let _ = method_node.send_remote_signal("handler_created", data);
}
fn handle_drop_handler(&self, handler: &InputHandler) {
let uid = handler.uid.as_str();
self.handler_aliases.remove(uid);
self.handler_aliases.remove(&(uid.to_string() + "-field"));
let Some(tx_node) = self.node.upgrade() else {
return;
};
let Ok(data) = serialize(&uid) else { return };
let _ = tx_node.send_remote_signal("handler_destroyed", data);
}
}
impl Aspect for InputMethod {
const NAME: &'static str = "InputMethod";
}
impl Drop for InputMethod {
fn drop(&mut self) {
INPUT_METHOD_REGISTRY.remove(self);
}
}
pub struct DistanceLink {
distance: f32,
method: Arc<InputMethod>,
handler: Arc<InputHandler>,
}
impl DistanceLink {
fn from(method: Arc<InputMethod>, handler: Arc<InputHandler>) -> Self {
DistanceLink {
distance: method.compare_distance(&handler),
method,
handler,
}
}
fn send_input(&self, order: u32, captured: bool, datamap: Datamap) {
self.handler.send_input(order, captured, self, datamap);
}
#[instrument(level = "debug", skip(self))]
fn serialize(&self, order: u32, captured: bool, datamap: Datamap) -> Vec<u8> {
let input = self.method.specialization.lock().serialize(
self,
Spatial::space_to_space_matrix(Some(&self.method.spatial), Some(&self.handler.spatial)),
);
let root = InputData {
uid: self.method.uid.clone(),
input,
distance: self.method.true_distance(&self.handler.field),
datamap,
order,
captured,
};
root.serialize()
}
}
pub struct InputHandler {
enabled: Arc<AtomicBool>,
uid: String,
node: Weak<Node>,
spatial: Arc<Spatial>,
field: Arc<Field>,
method_aliases: LifeLinkedNodeMap<usize>,
}
impl InputHandler {
pub fn add_to(node: &Arc<Node>, field: &Arc<Field>) -> Result<()> {
let handler = InputHandler {
enabled: node.enabled.clone(),
uid: node.uid.clone(),
node: Arc::downgrade(node),
spatial: node.get_aspect::<Spatial>().unwrap().clone(),
field: field.clone(),
method_aliases: LifeLinkedNodeMap::default(),
};
for method in INPUT_METHOD_REGISTRY.get_valid_contents() {
method.make_alias(&handler);
method.handle_new_handler(&handler);
}
let handler = INPUT_HANDLER_REGISTRY.add(handler);
node.add_aspect_raw(handler);
Ok(())
}
fn find(client: &Client, path: &str) -> Result<Arc<Self>> {
client.get_node("Input Handler", path)?.get_aspect::<Self>()
}
#[instrument(level = "debug", skip(self, distance_link))]
fn send_input(
&self,
order: u32,
captured: bool,
distance_link: &DistanceLink,
datamap: Datamap,
) {
let Some(node) = self.node.upgrade() else {
return;
};
let _ = node.send_remote_signal("input", distance_link.serialize(order, captured, datamap));
}
impl AspectIdentifier for InputHandler {
impl_aspect_for_input_handler_aspect_id! {}
}
impl Aspect for InputHandler {
const NAME: &'static str = "InputHandler";
impl_aspect_for_input_handler_aspect! {}
}
impl PartialEq for InputHandler {
fn eq(&self, other: &Self) -> bool {
self.spatial == other.spatial
impl AspectIdentifier for InputMethod {
impl_aspect_for_input_method_aspect_id! {}
}
impl Aspect for InputMethod {
impl_aspect_for_input_method_aspect! {}
}
impl AspectIdentifier for InputMethodRef {
impl_aspect_for_input_method_ref_aspect_id! {}
}
impl Aspect for InputMethodRef {
impl_aspect_for_input_method_ref_aspect! {}
}
pub trait InputDataTrait {
fn transform(&mut self, method: &InputMethod, handler: &InputHandler);
fn distance(&self, space: &Arc<Spatial>, field: &Field) -> f32;
}
impl InputDataTrait for InputDataType {
fn transform(&mut self, method: &InputMethod, handler: &InputHandler) {
match self {
InputDataType::Pointer(i) => i.transform(method, handler),
InputDataType::Hand(i) => i.transform(method, handler),
InputDataType::Tip(i) => i.transform(method, handler),
}
}
}
impl Drop for InputHandler {
fn drop(&mut self) {
INPUT_HANDLER_REGISTRY.remove(self);
for method in INPUT_METHOD_REGISTRY.get_valid_contents() {
method.handle_drop_handler(self);
fn distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
match self {
InputDataType::Pointer(i) => i.distance(space, field),
InputDataType::Hand(i) => i.distance(space, field),
InputDataType::Tip(i) => i.distance(space, field),
}
}
}
pub fn create_interface(client: &Arc<Client>) -> Result<()> {
let node = Node::create_path(client, "/input", false);
node.add_local_signal("create_input_handler", create_input_handler_flex);
node.add_local_signal("create_input_method_pointer", pointer::create_pointer_flex);
node.add_local_signal("create_input_method_tip", tip::create_tip_flex);
node.add_to_scenegraph().map(|_| ())
}
pub fn create_input_handler_flex(
_node: Arc<Node>,
calling_client: Arc<Client>,
message: Message,
) -> Result<()> {
#[derive(Deserialize)]
struct CreateInputHandlerInfo<'a> {
name: &'a str,
parent_path: &'a str,
impl InterfaceAspect for Interface {
#[doc = "Create an input method node"]
fn create_input_method(
_node: Arc<Node>,
calling_client: Arc<Client>,
id: u64,
parent: Arc<Node>,
transform: Transform,
field_path: &'a str,
}
let info: CreateInputHandlerInfo = deserialize(message.as_ref())?;
let parent = calling_client
.get_node("Spatial parent", info.parent_path)?
.get_aspect::<Spatial>()?;
let transform = parse_transform(info.transform, true, true, true);
let field = find_field(&calling_client, info.field_path)?;
initial_data: InputDataType,
datamap: Datamap,
) -> Result<()> {
let parent = parent.get_aspect::<Spatial>()?;
let transform = transform.to_mat4(true, true, true);
let node = Node::create_parent_name(&calling_client, "/input/handler", info.name, true)
.add_to_scenegraph()?;
Spatial::add_to(&node, Some(parent.clone()), transform, false);
InputHandler::add_to(&node, &field)?;
Ok(())
let node = Node::from_id(&calling_client, id, true).add_to_scenegraph()?;
Spatial::add_to(&node, Some(parent.clone()), transform, false);
InputMethod::add_to(&node, initial_data, datamap)?;
Ok(())
}
#[doc = "Create an input handler node"]
fn create_input_handler(
_node: Arc<Node>,
calling_client: Arc<Client>,
id: u64,
parent: Arc<Node>,
transform: Transform,
field: Arc<Node>,
) -> Result<()> {
let parent = parent.get_aspect::<Spatial>()?;
let transform = transform.to_mat4(true, true, true);
let field = field.get_aspect::<Field>()?;
let node = Node::from_id(&calling_client, id, true).add_to_scenegraph()?;
Spatial::add_to(&node, Some(parent.clone()), transform, false);
InputHandler::add_to(&node, &field)?;
Ok(())
}
}
#[tracing::instrument(level = "debug")]
pub fn process_input() {
// Iterate over all valid input methods
let methods = debug_span!("Get valid methods").in_scope(|| {
INPUT_METHOD_REGISTRY
.get_valid_contents()
.into_iter()
.filter(|method| *method.enabled.lock())
.filter(|method| method.datamap.lock().is_some())
});
let handlers = INPUT_HANDLER_REGISTRY.get_valid_contents();
const LIMIT: usize = 50;
for method in methods {
for alias in method.node.upgrade().unwrap().aliases.get_valid_contents() {
alias.enabled.store(false, Ordering::Release);
}
let methods = INPUT_METHOD_REGISTRY
.get_valid_contents()
.into_iter()
.filter(|method| {
let Some(node) = method.spatial.node() else {
return false;
};
node.enabled()
});
INPUT_HANDLER_REGISTRY
.get_valid_contents()
.into_iter()
.par_splat_map(ComputeTaskPool::get(), None, |_, handlers| {
for handler in handlers {
let _span = debug_span!("handle input handler").entered();
for method_alias in handler.method_aliases.get_aliases() {
method_alias.set_enabled(false);
}
debug_span!("Process input method").in_scope(|| {
// Get all valid input handlers and convert them to DistanceLink objects
let distance_links: Vec<DistanceLink> = debug_span!("Generate distance links")
.in_scope(|| {
if let Some(handler_order) = method.handler_order.get() {
let handler_order = handler_order.lock();
handler_order
.iter()
.filter_map(|h| h.upgrade())
.filter(|handler| handler.enabled.load(Ordering::Relaxed))
.map(|handler| DistanceLink::from(method.clone(), handler))
.collect()
} else {
let mut distance_links: Vec<_> = handlers
.iter()
.filter(|handler| handler.enabled.load(Ordering::Relaxed))
.map(|handler| {
debug_span!("Create distance link").in_scope(|| {
DistanceLink::from(method.clone(), handler.clone())
})
})
.collect();
// Sort the distance links by their distance in ascending order
debug_span!("Sort distance links").in_scope(|| {
distance_links.sort_unstable_by(|a, b| {
a.distance.abs().partial_cmp(&b.distance.abs()).unwrap()
});
});
distance_links.truncate(LIMIT);
distance_links
}
});
let captures = method.captures.take_valid_contents();
// Iterate over the distance links and send input to them
for (i, distance_link) in distance_links.into_iter().enumerate() {
if let Some(method_alias) = distance_link
.handler
.method_aliases
.get(&(Arc::as_ptr(&distance_link.method) as usize))
.and_then(|a| a.get_aspect::<Alias>().ok())
let Some(handler_node) = handler.spatial.node() else {
continue;
};
if !handler_node.enabled() {
continue;
}
if let Some(handler_field_node) = handler.field.spatial.node()
&& !handler_field_node.enabled()
{
method_alias.enabled.store(true, Ordering::Release);
}
let captured = captures.contains(&distance_link.handler);
distance_link.send_input(
i as u32,
captured,
method.datamap.lock().clone().unwrap(),
);
continue;
};
// If the current distance link is in the list of captured input handlers,
// break out of the loop to avoid sending input to the remaining distance links
if captured {
break;
}
let ser_span = debug_span!("serializing input").entered();
let (methods, datas) = methods
.clone()
// filter out methods without the handler in their handler order
.filter(|a| {
a.handler_order
.lock()
.iter()
.any(|h| h.ptr_eq(&Arc::downgrade(handler)))
})
// filter out methods without the proper alias
.filter_map(|m| {
Some((
handler
.method_aliases
.get_from_original_node(m.spatial.node.clone())?,
m,
))
})
// make sure the input method alias is enabled
.inspect(|(a, _)| {
a.set_enabled(true);
})
// serialize the data
.map(|(a, m)| (a.clone(), m.serialize(a.get_id(), handler)))
.unzip::<_, _, Vec<_>, Vec<_>>();
drop(ser_span);
let _span = debug_span!("client input").entered();
let _ = input_handler_client::input(&handler_node, &methods, &datas);
}
});
for method in methods {
method.cull_capture_attempts();
}
}

View File

@@ -1,96 +1,52 @@
use super::{DistanceLink, InputSpecialization};
use crate::core::client::Client;
use crate::nodes::fields::{Field, Ray, RayMarchResult};
use crate::nodes::input::{InputMethod, InputType};
use crate::nodes::spatial::{parse_transform, Spatial, Transform};
use crate::nodes::{Message, Node};
use glam::{vec3, Mat4};
use serde::Deserialize;
use stardust_xr::schemas::flat::{InputDataType, Pointer as FlatPointer};
use stardust_xr::schemas::flex::deserialize;
use stardust_xr::values::Datamap;
use super::{InputDataTrait, InputHandler, InputMethod, Pointer};
use crate::nodes::{
fields::{Field, FieldTrait, Ray, RayMarchResult},
spatial::Spatial,
};
use glam::{Mat4, Quat, vec3};
use std::sync::{Arc, Weak};
use std::sync::Arc;
#[derive(Default)]
pub struct Pointer;
// impl Default for Pointer {
// fn default() -> Self {
// Pointer {
// grab: Default::default(),
// select: Default::default(),
// }
// }
// }
impl Default for Pointer {
fn default() -> Self {
Pointer {
origin: [0.0; 3].into(),
orientation: Quat::IDENTITY.into(),
deepest_point: [0.0; 3].into(),
}
}
}
impl Pointer {
fn ray_march(&self, space: &Arc<Spatial>, field: &Field) -> RayMarchResult {
fn ray_march(&self, method_space: &Arc<Spatial>, field: &Field) -> RayMarchResult {
field.ray_march(Ray {
origin: vec3(0.0, 0.0, 0.0),
direction: vec3(0.0, 0.0, -1.0),
space: space.clone(),
space: Spatial::new(
Weak::new(),
Some(method_space.clone()),
Mat4::from_rotation_translation(self.orientation.into(), self.origin.into()),
),
})
}
}
impl InputSpecialization for Pointer {
fn compare_distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
let ray_info = self.ray_march(space, field);
if ray_info.min_distance > 0.0 {
ray_info.deepest_point_distance + 1000.0
} else {
ray_info
.deepest_point_distance
.hypot(0.001 / ray_info.min_distance)
}
}
fn true_distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
impl InputDataTrait for Pointer {
fn distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
let ray_info = self.ray_march(space, field);
ray_info.min_distance
}
fn serialize(
&self,
distance_link: &DistanceLink,
local_to_handler_matrix: Mat4,
) -> InputDataType {
fn transform(&mut self, method: &InputMethod, handler: &InputHandler) {
let local_to_handler_matrix =
Mat4::from_rotation_translation(self.orientation.into(), self.origin.into())
* Spatial::space_to_space_matrix(Some(&method.spatial), Some(&handler.spatial));
let (_, orientation, origin) = local_to_handler_matrix.to_scale_rotation_translation();
let direction = local_to_handler_matrix.transform_vector3(vec3(0.0, 0.0, -1.0));
let ray_march = self.ray_march(&distance_link.method.spatial, &distance_link.handler.field);
let ray_march = self.ray_march(&method.spatial, &handler.field);
let direction = local_to_handler_matrix
.transform_vector3(vec3(0.0, 0.0, -1.0))
.normalize();
let deepest_point = (direction * ray_march.deepest_point_distance) + origin;
InputDataType::Pointer(FlatPointer {
origin: origin.into(),
orientation: orientation.into(),
deepest_point: deepest_point.into(),
})
self.origin = origin.into();
self.orientation = orientation.into();
self.deepest_point = deepest_point.into();
}
}
pub fn create_pointer_flex(
_node: Arc<Node>,
calling_client: Arc<Client>,
message: Message,
) -> color_eyre::eyre::Result<()> {
#[derive(Deserialize)]
struct CreatePointerInfo<'a> {
name: &'a str,
parent_path: &'a str,
transform: Transform,
datamap: Option<Vec<u8>>,
}
let info: CreatePointerInfo = deserialize(message.as_ref())?;
let node = Node::create_parent_name(&calling_client, "/input/method/pointer", info.name, true);
let parent = calling_client
.get_node("Spatial parent", info.parent_path)?
.get_aspect::<Spatial>()?;
let transform = parse_transform(info.transform, true, true, false);
let node = node.add_to_scenegraph()?;
Spatial::add_to(&node, Some(parent.clone()), transform, false);
InputMethod::add_to(
&node,
InputType::Pointer(Pointer),
info.datamap
.and_then(|datamap| Datamap::from_raw(datamap).ok()),
)?;
Ok(())
}

View File

@@ -1,82 +1,29 @@
use super::{DistanceLink, InputSpecialization};
use crate::core::client::Client;
use crate::nodes::fields::Field;
use crate::nodes::input::{InputMethod, InputType};
use crate::nodes::spatial::{parse_transform, Spatial, Transform};
use crate::nodes::{Message, Node};
use color_eyre::eyre::Result;
use glam::{vec3a, Mat4};
use serde::Deserialize;
use stardust_xr::schemas::flat::{InputDataType, Tip as FlatTip};
use stardust_xr::schemas::flex::deserialize;
use stardust_xr::values::Datamap;
use super::{InputDataTrait, InputHandler, InputMethod, Tip};
use crate::nodes::{
fields::{Field, FieldTrait},
spatial::Spatial,
};
use glam::{Mat4, Quat};
use std::sync::Arc;
#[derive(Default)]
pub struct Tip {
pub radius: f32,
}
impl Tip {
fn set_radius(node: Arc<Node>, _calling_client: Arc<Client>, message: Message) -> Result<()> {
let input_method = node.get_aspect::<InputMethod>()?;
if let InputType::Tip(tip) = &mut *input_method.specialization.lock() {
tip.radius = deserialize(message.as_ref())?;
impl Default for Tip {
fn default() -> Self {
Tip {
origin: [0.0; 3].into(),
orientation: Quat::IDENTITY.into(),
}
Ok(())
}
}
impl InputSpecialization for Tip {
fn compare_distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
field.distance(space, vec3a(0.0, 0.0, 0.0)).abs()
impl InputDataTrait for Tip {
fn distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
field.distance(space, self.origin.into())
}
fn true_distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
field.distance(space, vec3a(0.0, 0.0, 0.0))
}
fn serialize(
&self,
_distance_link: &DistanceLink,
local_to_handler_matrix: Mat4,
) -> InputDataType {
fn transform(&mut self, method: &InputMethod, handler: &InputHandler) {
let local_to_handler_matrix =
Spatial::space_to_space_matrix(Some(&method.spatial), Some(&handler.spatial))
* Mat4::from_rotation_translation(self.orientation.into(), self.origin.into());
let (_, orientation, origin) = local_to_handler_matrix.to_scale_rotation_translation();
InputDataType::Tip(FlatTip {
origin: origin.into(),
orientation: orientation.into(),
radius: self.radius,
})
self.origin = origin.into();
self.orientation = orientation.into();
}
}
pub fn create_tip_flex(
_node: Arc<Node>,
calling_client: Arc<Client>,
message: Message,
) -> Result<()> {
#[derive(Deserialize)]
struct CreateTipInfo<'a> {
name: &'a str,
parent_path: &'a str,
transform: Transform,
radius: f32,
datamap: Option<Vec<u8>>,
}
let info: CreateTipInfo = deserialize(message.as_ref())?;
let node = Node::create_parent_name(&calling_client, "/input/method/tip", info.name, true);
let parent = calling_client
.get_node("Spatial parent", info.parent_path)?
.get_aspect::<Spatial>()?;
let transform = parse_transform(info.transform, true, true, false);
let node = node.add_to_scenegraph()?;
Spatial::add_to(&node, Some(parent.clone()), transform, false);
InputMethod::add_to(
&node,
InputType::Tip(Tip {
radius: info.radius,
}),
info.datamap
.and_then(|datamap| Datamap::from_raw(datamap).ok()),
)?;
node.add_local_signal("set_radius", Tip::set_radius);
Ok(())
}

View File

@@ -1,40 +1,46 @@
use super::{Item, ItemType};
#![allow(dead_code)]
use super::{Item, ItemType, create_item_acceptor_flex, register_item_ui_flex};
use crate::bail;
use crate::core::error::Result;
use crate::nodes::Aspect;
use crate::nodes::AspectIdentifier;
use crate::nodes::items::ITEM_ACCEPTOR_ASPECT_ALIAS_INFO;
use crate::nodes::items::ITEM_ASPECT_ALIAS_INFO;
use crate::{
core::{
client::{Client, INTERNAL_CLIENT},
registry::Registry,
scenegraph::MethodResponseSender,
},
core::{client::Client, registry::Registry, scenegraph::MethodResponseSender},
nodes::{
drawable::{model::ModelPart, shaders::UNLIT_SHADER_BYTES},
items::TypeInfo,
spatial::{parse_transform, Spatial, Transform},
Message, Node,
drawable::model::ModelPart,
items::TypeInfo,
spatial::{Spatial, Transform},
},
};
use color_eyre::eyre::{bail, eyre, Result};
use glam::Mat4;
use lazy_static::lazy_static;
use mint::{RowMatrix4, Vector2};
use nanoid::nanoid;
use once_cell::sync::OnceCell;
use mint::{ColumnMatrix4, Vector2};
use parking_lot::Mutex;
use serde::Deserialize;
use stardust_xr::schemas::flex::{deserialize, serialize};
use std::sync::Arc;
use stereokit::{
Color128, Material, Rect, RenderLayer, StereoKitDraw, Tex, TextureType, Transparency,
};
stardust_xr_server_codegen::codegen_item_camera_protocol!();
lazy_static! {
pub(super) static ref ITEM_TYPE_INFO_CAMERA: TypeInfo = TypeInfo {
type_name: "camera",
aliased_local_signals: vec!["apply_preview_material", "frame"],
aliased_local_methods: vec![],
aliased_remote_signals: vec![],
alias_info: CAMERA_ITEM_ASPECT_ALIAS_INFO.clone(),
ui_node_id: INTERFACE_NODE_ID,
ui: Default::default(),
items: Registry::new(),
acceptors: Registry::new(),
add_acceptor_aspect: |node| {
node.add_aspect(CameraItemAcceptor);
},
add_ui_aspect: |node| {
node.add_aspect(CameraItemUi);
},
new_acceptor_fn: |node, acceptor, acceptor_field| {
let _ = camera_item_ui_client::create_acceptor(node, acceptor, acceptor_field);
}
};
}
@@ -46,34 +52,23 @@ struct FrameInfo {
pub struct CameraItem {
space: Arc<Spatial>,
frame_info: Mutex<FrameInfo>,
sk_tex: OnceCell<Tex>,
sk_mat: OnceCell<Arc<Material>>,
applied_to: Registry<ModelPart>,
apply_to: Registry<ModelPart>,
}
#[allow(unused)]
impl CameraItem {
pub fn add_to(node: &Arc<Node>, proj_matrix: Mat4, px_size: Vector2<u32>) {
Item::add_to(
node,
nanoid!(),
&ITEM_TYPE_INFO_CAMERA,
ItemType::Camera(CameraItem {
space: node.get_aspect::<Spatial>().unwrap().clone(),
frame_info: Mutex::new(FrameInfo {
proj_matrix,
px_size,
}),
sk_tex: OnceCell::new(),
sk_mat: OnceCell::new(),
applied_to: Registry::new(),
apply_to: Registry::new(),
let item = Arc::new(CameraItem {
space: node.get_aspect::<Spatial>().unwrap().clone(),
frame_info: Mutex::new(FrameInfo {
proj_matrix,
px_size,
}),
);
node.add_local_method("frame", CameraItem::frame_flex);
node.add_local_signal(
"apply_preview_material",
CameraItem::apply_preview_material_flex,
);
applied_to: Registry::new(),
apply_to: Registry::new(),
});
Item::add_to(node, &ITEM_TYPE_INFO_CAMERA, ItemType::Camera(item.clone()));
node.add_aspect_raw(item);
}
fn frame_flex(
@@ -82,12 +77,12 @@ impl CameraItem {
_message: Message,
response: MethodResponseSender,
) {
response.wrap_sync(move || {
response.wrap(move || {
let ItemType::Camera(_camera) = &node.get_aspect::<Item>().unwrap().specialization
else {
return Err(eyre!("Wrong item type?"));
bail!("Wrong item type?");
};
Ok(serialize(())?.into())
Ok(serialize(())?)
});
}
@@ -97,7 +92,7 @@ impl CameraItem {
message: Message,
) -> Result<()> {
let ItemType::Camera(camera) = &node.get_aspect::<Item>().unwrap().specialization else {
bail!("Wrong item type?")
bail!("Wrong item type?");
};
let model_part_node =
calling_client.get_node("Model part", deserialize(&message.data).unwrap())?;
@@ -107,87 +102,86 @@ impl CameraItem {
Ok(())
}
pub fn serialize_start_data(&self, id: &str) -> Result<Message> {
Ok(serialize(id)?.into())
pub fn send_ui_item_created(&self, node: &Node, item: &Arc<Node>) {
let _ = camera_item_ui_client::create_item(node, item);
}
pub fn send_acceptor_item_created(&self, node: &Node, item: &Arc<Node>) {
let _ = camera_item_acceptor_client::capture_item(node, item);
}
}
impl AspectIdentifier for CameraItem {
impl_aspect_for_camera_item_aspect_id! {}
}
impl Aspect for CameraItem {
impl_aspect_for_camera_item_aspect! {}
}
impl CameraItemAspect for CameraItem {}
pub fn update(&self, sk: &impl StereoKitDraw) {
let frame_info = self.frame_info.lock();
let sk_tex = self.sk_tex.get_or_init(|| {
sk.tex_gen_color(
Color128::default(),
frame_info.px_size.x as i32,
frame_info.px_size.y as i32,
TextureType::RENDER_TARGET,
stereokit::TextureFormat::RGBA32Linear,
)
});
let sk_mat = self.sk_mat.get_or_init(|| {
let shader = sk.shader_create_mem(&UNLIT_SHADER_BYTES).unwrap();
let mat = sk.material_create(&shader);
sk.material_set_texture(&mat, "diffuse", sk_tex.as_ref());
sk.material_set_transparency(&mat, Transparency::Blend);
Arc::new(mat)
});
for model_part in self.apply_to.take_valid_contents() {
model_part.replace_material(sk_mat.clone())
}
pub struct CameraItemUi;
impl AspectIdentifier for CameraItemUi {
impl_aspect_for_camera_item_ui_aspect_id! {}
}
impl Aspect for CameraItemUi {
impl_aspect_for_camera_item_ui_aspect! {}
}
impl CameraItemUiAspect for CameraItemUi {}
if !self.applied_to.is_empty() {
sk.render_to(
sk_tex,
frame_info.proj_matrix,
self.space.global_transform(),
RenderLayer::all(),
stereokit::RenderClear::All,
Rect {
x: 0.0,
y: 0.0,
w: 0.0,
h: 0.0,
},
);
}
pub struct CameraItemAcceptor;
impl AspectIdentifier for CameraItemAcceptor {
impl_aspect_for_camera_item_acceptor_aspect_id! {}
}
impl Aspect for CameraItemAcceptor {
impl_aspect_for_camera_item_acceptor_aspect! {}
}
impl CameraItemAcceptorAspect for CameraItemAcceptor {
fn capture_item(node: Arc<Node>, _calling_client: Arc<Client>, item: Arc<Node>) -> Result<()> {
super::acceptor_capture_item_flex(node, item)
}
}
pub fn update(sk: &impl StereoKitDraw) {
for camera in ITEM_TYPE_INFO_CAMERA.items.get_valid_contents() {
let ItemType::Camera(camera) = &camera.specialization else {
continue;
};
camera.update(sk);
}
}
pub(super) fn create_camera_item_flex(
_node: Arc<Node>,
calling_client: Arc<Client>,
message: Message,
) -> Result<()> {
#[derive(Deserialize)]
struct CreateCameraItemInfo<'a> {
name: &'a str,
parent_path: &'a str,
impl InterfaceAspect for Interface {
#[doc = "Create a camera item at a specific location"]
fn create_camera_item(
_node: Arc<Node>,
calling_client: Arc<Client>,
id: u64,
parent: Arc<Node>,
transform: Transform,
proj_matrix: RowMatrix4<f32>,
proj_matrix: ColumnMatrix4<f32>,
px_size: Vector2<u32>,
}
let info: CreateCameraItemInfo = deserialize(message.as_ref())?;
let parent_name = format!("/item/{}/item", ITEM_TYPE_INFO_CAMERA.type_name);
let space = calling_client
.get_node("Spatial parent", info.parent_path)?
.get_aspect::<Spatial>()?;
let transform = parse_transform(info.transform, true, true, false);
) -> Result<()> {
let space = parent.get_aspect::<Spatial>()?;
let transform = transform.to_mat4(true, true, false);
let node = Node::create_parent_name(&INTERNAL_CLIENT, &parent_name, info.name, false)
.add_to_scenegraph()?;
Spatial::add_to(&node, None, transform * space.global_transform(), false);
CameraItem::add_to(&node, info.proj_matrix.into(), info.px_size);
node.get_aspect::<Item>().unwrap().make_alias_named(
&calling_client,
&parent_name,
info.name,
)?;
Ok(())
let node = Node::from_id(&calling_client, id, false).add_to_scenegraph()?;
Spatial::add_to(&node, None, transform * space.global_transform(), false);
CameraItem::add_to(&node, proj_matrix.into(), px_size);
Ok(())
}
#[doc = "Register this client to manage camera items and create default 3D UI for them."]
fn register_camera_item_ui(node: Arc<Node>, calling_client: Arc<Client>) -> Result<()> {
node.add_aspect(CameraItemUi);
register_item_ui_flex(calling_client, &ITEM_TYPE_INFO_CAMERA)
}
#[doc = "Create an item acceptor to allow temporary ownership of a given type of item. Creates a node at `/item/camera/acceptor/<name>`."]
fn create_camera_item_acceptor(
node: Arc<Node>,
calling_client: Arc<Client>,
id: u64,
parent: Arc<Node>,
transform: Transform,
field: Arc<Node>,
) -> Result<()> {
node.add_aspect(CameraItemAcceptor);
create_item_acceptor_flex(
calling_client,
id,
parent,
transform,
&ITEM_TYPE_INFO_CAMERA,
field,
)
}
}

View File

@@ -1,97 +0,0 @@
use super::{Item, ItemType};
use crate::{
core::{
client::{Client, INTERNAL_CLIENT},
registry::Registry,
scenegraph::MethodResponseSender,
},
nodes::{
items::TypeInfo,
spatial::{parse_transform, Spatial, Transform},
Message, Node,
},
};
use color_eyre::eyre::{eyre, Result};
use lazy_static::lazy_static;
use nanoid::nanoid;
use serde::Deserialize;
use stardust_xr::schemas::flex::{deserialize, serialize};
use std::sync::Arc;
lazy_static! {
pub(super) static ref ITEM_TYPE_INFO_ENVIRONMENT: TypeInfo = TypeInfo {
type_name: "environment",
aliased_local_signals: vec!["apply_sky_tex", "apply_sky_light"],
aliased_local_methods: vec![],
aliased_remote_signals: vec![],
ui: Default::default(),
items: Registry::new(),
acceptors: Registry::new(),
};
}
pub struct EnvironmentItem {
path: String,
}
impl EnvironmentItem {
pub fn add_to(node: &Arc<Node>, path: String) {
Item::add_to(
node,
nanoid!(),
&ITEM_TYPE_INFO_ENVIRONMENT,
ItemType::Environment(EnvironmentItem { path }),
);
node.add_local_method("get_path", EnvironmentItem::get_path_flex);
}
fn get_path_flex(
node: Arc<Node>,
_calling_client: Arc<Client>,
_message: Message,
response: MethodResponseSender,
) {
response.wrap_sync(move || {
let ItemType::Environment(environment_item) =
&node.get_aspect::<Item>().unwrap().specialization
else {
return Err(eyre!("Wrong item type?"));
};
Ok(serialize(environment_item.path.as_str())?.into())
});
}
pub fn serialize_start_data(&self, id: &str) -> Result<Message> {
Ok(serialize((id, self.path.as_str()))?.into())
}
}
pub(super) fn create_environment_item_flex(
_node: Arc<Node>,
calling_client: Arc<Client>,
message: Message,
) -> Result<()> {
#[derive(Deserialize)]
struct CreateEnvironmentItemInfo<'a> {
name: &'a str,
parent_path: &'a str,
transform: Transform,
item_data: String,
}
let info: CreateEnvironmentItemInfo = deserialize(message.as_ref())?;
let parent_name = format!("/item/{}/item", ITEM_TYPE_INFO_ENVIRONMENT.type_name);
let space = calling_client
.get_node("Spatial parent", info.parent_path)?
.get_aspect::<Spatial>()?;
let transform = parse_transform(info.transform, true, true, false);
let node = Node::create_parent_name(&INTERNAL_CLIENT, &parent_name, info.name, false)
.add_to_scenegraph()?;
Spatial::add_to(&node, None, transform * space.global_transform(), false);
EnvironmentItem::add_to(&node, info.item_data);
node.get_aspect::<Item>().unwrap().make_alias_named(
&calling_client,
&parent_name,
info.name,
)?;
Ok(())
}

View File

@@ -1,45 +1,26 @@
pub mod camera;
mod environment;
pub mod panel;
use self::camera::CameraItem;
use self::environment::{EnvironmentItem, ITEM_TYPE_INFO_ENVIRONMENT};
use self::panel::{PanelItemTrait, ITEM_TYPE_INFO_PANEL};
use super::fields::Field;
use super::spatial::{parse_transform, Spatial};
use super::{Alias, Aspect, Message, Node};
use self::panel::PanelItemTrait;
use super::alias::AliasList;
use super::fields::{FIELD_ALIAS_INFO, Field};
use super::spatial::Spatial;
use super::{Alias, Aspect, AspectIdentifier, Node};
use crate::core::client::Client;
use crate::core::node_collections::LifeLinkedNodeMap;
use crate::core::error::Result;
use crate::core::registry::Registry;
use crate::ensure;
use crate::nodes::alias::AliasInfo;
use crate::nodes::fields::find_field;
use crate::nodes::spatial::SPATIAL_ASPECT_ALIAS_INFO;
use crate::nodes::spatial::Transform;
use color_eyre::eyre::{ensure, eyre, Result};
use lazy_static::lazy_static;
use nanoid::nanoid;
use parking_lot::Mutex;
use portable_atomic::Ordering;
use serde::Deserialize;
use stardust_xr::schemas::flex::{deserialize, serialize};
use std::hash::Hash;
use std::sync::{Arc, Weak};
lazy_static! {
static ref ITEM_ALIAS_LOCAL_SIGNALS: Vec<&'static str> = vec![
"get_bounds",
"get_transform",
"set_transform",
"set_spatial_parent",
"set_spatial_parent_in_place",
"set_zoneable",
"release",
];
static ref ITEM_ALIAS_LOCAL_METHODS: Vec<&'static str> = vec![];
static ref ITEM_ALIAS_REMOTE_SIGNALS: Vec<&'static str> = vec![];
}
stardust_xr_server_codegen::codegen_item_protocol!();
pub fn capture(item: &Arc<Item>, acceptor: &Arc<ItemAcceptor>) {
fn capture(item: &Arc<Item>, acceptor: &Arc<ItemAcceptor>) {
if item.captured_acceptor.lock().strong_count() > 0 {
release(item);
}
@@ -55,19 +36,21 @@ fn release(item: &Item) {
*captured_acceptor = Weak::default();
acceptor.handle_release(item);
if let Some(ui) = item.type_info.ui.lock().upgrade() {
ui.handle_release_item(item, &acceptor);
ui.handle_release_item(item, acceptor);
}
}
}
pub struct TypeInfo {
pub type_name: &'static str,
pub aliased_local_signals: Vec<&'static str>,
pub aliased_local_methods: Vec<&'static str>,
pub aliased_remote_signals: Vec<&'static str>,
pub alias_info: AliasInfo,
pub ui_node_id: u64,
pub ui: Mutex<Weak<ItemUI>>,
pub items: Registry<Item>,
pub acceptors: Registry<ItemAcceptor>,
pub add_ui_aspect: fn(node: &Node),
pub add_acceptor_aspect: fn(node: &Node),
pub new_acceptor_fn: fn(node: &Node, acceptor: &Arc<Node>, acceptor_field: &Arc<Node>),
}
impl Hash for TypeInfo {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
@@ -82,8 +65,7 @@ impl PartialEq for TypeInfo {
impl Eq for TypeInfo {}
pub struct Item {
node: Weak<Node>,
uid: String,
spatial: Arc<Spatial>,
type_info: &'static TypeInfo,
captured_acceptor: Mutex<Weak<ItemAcceptor>>,
pub specialization: ItemType,
@@ -91,20 +73,17 @@ pub struct Item {
impl Item {
pub fn add_to(
node: &Arc<Node>,
uid: String,
type_info: &'static TypeInfo,
specialization: ItemType,
) -> Arc<Self> {
let item = Item {
node: Arc::downgrade(node),
uid,
spatial: node.aspects.get::<Spatial>().unwrap(),
type_info,
captured_acceptor: Default::default(),
specialization,
};
let item = type_info.items.add(item);
node.add_local_signal("release", Item::release_flex);
if let Some(ui) = type_info.ui.lock().upgrade() {
ui.handle_create_item(&item);
}
@@ -122,53 +101,27 @@ impl Item {
item
}
fn make_alias_named(
&self,
client: &Arc<Client>,
parent: &str,
name: &str,
) -> Result<Arc<Node>> {
fn make_alias(&self, client: &Arc<Client>, alias_list: &AliasList) -> Result<Arc<Node>> {
Alias::create(
&self.spatial.node().unwrap(),
client,
parent,
name,
&self.node.upgrade().unwrap(),
AliasInfo {
server_signals: [
&self.type_info.aliased_local_signals,
ITEM_ALIAS_LOCAL_SIGNALS.as_slice(),
]
.concat(),
server_methods: [
&self.type_info.aliased_local_methods,
ITEM_ALIAS_LOCAL_METHODS.as_slice(),
]
.concat(),
client_signals: [
&self.type_info.aliased_remote_signals,
ITEM_ALIAS_REMOTE_SIGNALS.as_slice(),
]
.concat(),
},
self.type_info.alias_info.clone() + ITEM_ASPECT_ALIAS_INFO.clone(),
Some(alias_list),
)
}
fn make_alias(&self, client: &Arc<Client>, parent: &str) -> Result<Arc<Node>> {
self.make_alias_named(client, parent, &self.uid)
}
fn release_flex(
node: Arc<Node>,
_calling_client: Arc<Client>,
_message: Message,
) -> Result<()> {
let item = node.get_aspect::<Item>()?;
release(&item);
Ok(())
}
}
impl AspectIdentifier for Item {
impl_aspect_for_item_aspect_id! {}
}
impl Aspect for Item {
const NAME: &'static str = "Item";
impl_aspect_for_item_aspect! {}
}
impl ItemAspect for Item {
fn release(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
let item = node.get_aspect::<Item>()?;
release(&item);
Ok(())
}
}
impl Drop for Item {
fn drop(&mut self) {
@@ -180,17 +133,22 @@ impl Drop for Item {
}
}
#[cfg_attr(not(feature = "wayland"), allow(dead_code))]
pub enum ItemType {
Camera(CameraItem),
Environment(EnvironmentItem),
Camera(Arc<CameraItem>),
Panel(Arc<dyn PanelItemTrait>),
}
impl ItemType {
fn serialize_start_data(&self, id: &str) -> Result<Message> {
fn send_ui_item_created(&self, node: &Node, item: &Arc<Node>) {
match self {
ItemType::Camera(c) => c.serialize_start_data(id),
ItemType::Environment(e) => e.serialize_start_data(id),
ItemType::Panel(p) => p.serialize_start_data(id),
ItemType::Camera(c) => c.send_ui_item_created(node, item),
ItemType::Panel(p) => p.send_ui_item_created(node, item),
}
}
fn send_acceptor_item_created(&self, node: &Node, item: &Arc<Node>) {
match self {
ItemType::Camera(c) => c.send_acceptor_item_created(node, item),
ItemType::Panel(p) => p.send_acceptor_item_created(node, item),
}
}
}
@@ -208,9 +166,9 @@ impl ItemType {
pub struct ItemUI {
node: Weak<Node>,
type_info: &'static TypeInfo,
item_aliases: LifeLinkedNodeMap<String>,
acceptor_aliases: LifeLinkedNodeMap<String>,
acceptor_field_aliases: LifeLinkedNodeMap<String>,
item_aliases: AliasList,
acceptor_aliases: AliasList,
acceptor_field_aliases: AliasList,
}
impl ItemUI {
fn add_to(node: &Arc<Node>, type_info: &'static TypeInfo) -> Result<()> {
@@ -222,9 +180,9 @@ impl ItemUI {
let ui = Arc::new(ItemUI {
node: Arc::downgrade(node),
type_info,
item_aliases: Default::default(),
acceptor_aliases: Default::default(),
acceptor_field_aliases: Default::default(),
item_aliases: AliasList::default(),
acceptor_aliases: AliasList::default(),
acceptor_field_aliases: AliasList::default(),
});
*type_info.ui.lock() = Arc::downgrade(&ui);
node.add_aspect_raw(ui.clone());
@@ -237,16 +195,6 @@ impl ItemUI {
}
Ok(())
}
fn send_state(&self, state: &str, name: &str) {
let Ok(serialized_data) = serialize(name) else {
return;
};
let _ = self
.node
.upgrade()
.unwrap()
.send_remote_signal(state, serialized_data);
}
fn handle_create_item(&self, item: &Item) {
let Some(node) = self.node.upgrade() else {
@@ -256,38 +204,47 @@ impl ItemUI {
return;
};
if let Ok(alias_node) = item.make_alias(&client, &(node.get_path().to_string() + "/item")) {
self.item_aliases.add(item.uid.clone(), &alias_node);
}
let Ok(serialized_data) = item.specialization.serialize_start_data(&item.uid) else {
let Ok(item_alias) = item.make_alias(&client, &self.item_aliases) else {
return;
};
let _ = node.send_remote_signal("create_item", serialized_data);
}
fn handle_destroy_item(&self, item: &Item) {
self.item_aliases.remove(&item.uid);
self.send_state("destroy_item", item.uid.as_str());
item.specialization.send_ui_item_created(&node, &item_alias);
}
fn handle_capture_item(&self, item: &Item, acceptor: &ItemAcceptor) {
let Some(node) = self.node.upgrade() else {
let Some(item_alias) = self.item_aliases.get_from_aspect(item) else {
return;
};
let Ok(message) = serialize((item.uid.as_str(), acceptor.uid.as_str())) else {
let Some(acceptor_alias) = self.acceptor_aliases.get_from_aspect(acceptor) else {
return;
};
let _ = node.send_remote_signal("capture_item", message);
let _ = item_ui_client::capture_item(
&self.node.upgrade().unwrap(),
item_alias.id,
acceptor_alias.id,
);
}
fn handle_release_item(&self, item: &Item, acceptor: &ItemAcceptor) {
let Some(node) = self.node.upgrade() else {
let Some(item_alias) = self.item_aliases.get_from_aspect(item) else {
return;
};
let Ok(message) = serialize((item.uid.as_str(), acceptor.uid.as_str())) else {
let Some(acceptor_alias) = self.acceptor_aliases.get_from_aspect(acceptor) else {
return;
};
let _ = node.send_remote_signal("release_item", message);
let _ = item_ui_client::release_item(
&self.node.upgrade().unwrap(),
item_alias.id,
acceptor_alias.id,
);
}
fn handle_destroy_item(&self, item: &Item) {
let Some(item_alias) = self
.item_aliases
.get_from_original_node(item.spatial.node.clone())
else {
return;
};
let _ = item_ui_client::destroy_item(&self.node.upgrade().unwrap(), item_alias.id);
self.item_aliases.remove_aspect(item);
}
fn handle_create_acceptor(&self, acceptor: &ItemAcceptor) {
let Some(node) = self.node.upgrade() else {
@@ -297,28 +254,47 @@ impl ItemUI {
return;
};
let Ok((alias, field_alias)) = acceptor.make_aliases(
let Some(acceptor_node) = acceptor.spatial.node() else {
return;
};
let Ok(acceptor_alias) = Alias::create(
&acceptor_node,
&client,
&format!("/item/{}/acceptor", self.type_info.type_name),
ITEM_ACCEPTOR_ASPECT_ALIAS_INFO.clone(),
Some(&self.acceptor_aliases),
) else {
return;
};
self.acceptor_aliases.add(acceptor.uid.clone(), &alias);
self.acceptor_field_aliases
.add(acceptor.uid.clone(), &field_alias);
let Ok(message) = serialize(&acceptor.uid) else {
let Some(acceptor_field_node) = acceptor.field.spatial.node() else {
return;
};
let _ = node.send_remote_signal("create_acceptor", message);
let Ok(acceptor_field_alias) = Alias::create(
&acceptor_field_node,
&client,
FIELD_ALIAS_INFO.clone(),
Some(&self.acceptor_aliases),
) else {
return;
};
(acceptor.type_info.new_acceptor_fn)(&node, &acceptor_alias, &acceptor_field_alias);
}
fn handle_destroy_acceptor(&self, acceptor: &ItemAcceptor) {
self.send_state("destroy_acceptor", acceptor.uid.as_str());
self.acceptor_aliases.remove(&acceptor.uid);
self.acceptor_field_aliases.remove(&acceptor.uid);
let acceptor_alias = self.acceptor_aliases.get_from_aspect(acceptor).unwrap();
let _ = item_ui_client::destroy_acceptor(&self.node.upgrade().unwrap(), acceptor_alias.id);
self.acceptor_aliases
.remove_aspect(acceptor.spatial.as_ref());
self.acceptor_field_aliases
.remove_aspect(acceptor.field.as_ref());
}
}
impl AspectIdentifier for ItemUI {
impl_aspect_for_item_ui_aspect_id! {}
}
impl Aspect for ItemUI {
const NAME: &'static str = "Item";
impl_aspect_for_item_ui_aspect! {}
}
impl Drop for ItemUI {
fn drop(&mut self) {
@@ -327,69 +303,29 @@ impl Drop for ItemUI {
}
pub struct ItemAcceptor {
uid: String,
node: Weak<Node>,
spatial: Arc<Spatial>,
pub type_info: &'static TypeInfo,
field: Arc<Field>,
accepted_aliases: LifeLinkedNodeMap<String>,
accepted_aliases: AliasList,
accepted_registry: Registry<Item>,
}
impl ItemAcceptor {
fn add_to(node: &Arc<Node>, type_info: &'static TypeInfo, field: Arc<Field>) {
let acceptor = type_info.acceptors.add(ItemAcceptor {
uid: nanoid!(),
node: Arc::downgrade(node),
spatial: node.get_aspect::<Spatial>().unwrap(),
type_info,
field,
accepted_aliases: Default::default(),
accepted_aliases: AliasList::default(),
accepted_registry: Registry::new(),
});
node.add_local_signal("capture", ItemAcceptor::capture_flex);
if let Some(ui) = type_info.ui.lock().upgrade() {
ui.handle_create_acceptor(&acceptor);
}
node.add_aspect_raw(acceptor);
node.add_aspect_raw(acceptor.clone());
}
fn capture_flex(node: Arc<Node>, calling_client: Arc<Client>, message: Message) -> Result<()> {
if !node.enabled.load(Ordering::Relaxed) {
return Ok(());
}
let acceptor = node.get_aspect::<ItemAcceptor>().unwrap();
let item_path: &str = deserialize(message.as_ref())?;
let item_node = calling_client.get_node("Item", item_path)?;
let item = item_node.get_aspect::<Item>()?;
capture(&item, &acceptor);
Ok(())
}
fn make_aliases(&self, client: &Arc<Client>, parent: &str) -> Result<(Arc<Node>, Arc<Node>)> {
let acceptor_node = &self.node.upgrade().unwrap();
let acceptor_alias = Alias::create(
client,
parent,
&self.uid,
acceptor_node,
AliasInfo {
server_signals: vec!["capture"],
..Default::default()
},
)?;
let acceptor_field_alias = Alias::create(
client,
acceptor_alias.get_path(),
"field",
&self.field.spatial_ref().node.upgrade().unwrap(),
AliasInfo::default(),
)?;
Ok((acceptor_alias, acceptor_field_alias))
}
fn handle_capture(&self, item: &Arc<Item>) {
let Some(node) = self.node.upgrade() else {
let Some(node) = self.spatial.node() else {
return;
};
let Some(client) = node.get_client() else {
@@ -397,31 +333,31 @@ impl ItemAcceptor {
};
self.accepted_registry.add_raw(item);
if let Ok(alias_node) = item.make_alias(&client, &node.path) {
self.accepted_aliases.add(item.uid.clone(), &alias_node);
}
let Ok(serialized_data) = item.specialization.serialize_start_data(&item.uid) else {
let Ok(alias_node) = item.make_alias(&client, &self.accepted_aliases) else {
return;
};
let _ = node.send_remote_signal("capture", serialized_data);
item.specialization
.send_acceptor_item_created(&node, &alias_node);
}
fn handle_release(&self, item: &Item) {
let Some(node) = self.node.upgrade() else {
return;
};
self.accepted_registry.remove(item);
self.accepted_aliases.remove(&item.uid);
let Ok(message) = serialize(&item.uid) else {
self.accepted_aliases.remove_aspect(item);
let Some(node) = self.spatial.node() else {
return;
};
let _ = node.send_remote_signal("release", message);
let alias = self.accepted_aliases.get_from_aspect(item).unwrap();
let _ = item_acceptor_client::release_item(&node, alias.id);
}
}
impl Aspect for ItemAcceptor {
const NAME: &'static str = "ItemAcceptor";
impl AspectIdentifier for ItemAcceptor {
impl_aspect_for_item_acceptor_aspect_id! {}
}
impl Aspect for ItemAcceptor {
impl_aspect_for_item_acceptor_aspect! {}
}
impl ItemAcceptorAspect for ItemAcceptor {}
impl Drop for ItemAcceptor {
fn drop(&mut self) {
self.type_info.acceptors.remove(self);
@@ -434,73 +370,38 @@ impl Drop for ItemAcceptor {
}
}
pub fn create_interface(client: &Arc<Client>) -> Result<()> {
let node = Node::create_parent_name(client, "", "item", false);
node.add_local_signal("create_camera_item", camera::create_camera_item_flex);
node.add_local_signal(
"create_environment_item",
environment::create_environment_item_flex,
);
node.add_local_signal("register_item_ui", register_item_ui_flex);
node.add_local_signal("create_item_acceptor", create_item_acceptor_flex);
node.add_to_scenegraph().map(|_| ())
}
fn type_info(name: &str) -> Result<&'static TypeInfo> {
match name {
"environment" => Ok(&ITEM_TYPE_INFO_ENVIRONMENT),
#[cfg(feature = "wayland")]
"panel" => Ok(&ITEM_TYPE_INFO_PANEL),
_ => Err(eyre!("Invalid item type")),
}
}
pub fn register_item_ui_flex(
_node: Arc<Node>,
calling_client: Arc<Client>,
message: Message,
type_info: &'static TypeInfo,
) -> Result<()> {
#[derive(Deserialize)]
struct RegisterItemUIInfo<'a> {
item_type: &'a str,
}
let info: RegisterItemUIInfo = deserialize(message.as_ref())?;
let type_info = type_info(info.item_type)?;
let ui = Node::create_parent_name(&calling_client, "/item", type_info.type_name, true)
.add_to_scenegraph()?;
let ui = Node::from_id(&calling_client, type_info.ui_node_id, true).add_to_scenegraph()?;
ItemUI::add_to(&ui, type_info)?;
(type_info.add_ui_aspect)(&ui);
Ok(())
}
fn create_item_acceptor_flex(
_node: Arc<Node>,
calling_client: Arc<Client>,
message: Message,
id: u64,
parent: Arc<Node>,
transform: Transform,
type_info: &'static TypeInfo,
field: Arc<Node>,
) -> Result<()> {
#[derive(Deserialize)]
struct CreateItemAcceptorInfo<'a> {
name: &'a str,
parent_path: &'a str,
transform: Transform,
field_path: &'a str,
item_type: &'a str,
}
let info: CreateItemAcceptorInfo = deserialize(message.as_ref())?;
let space = calling_client
.get_node("Reference space", info.parent_path)?
.get_aspect::<Spatial>()?;
let transform = parse_transform(info.transform, true, true, false);
let field = find_field(&calling_client, info.field_path)?;
let type_info = type_info(info.item_type)?;
let space = parent.get_aspect::<Spatial>()?;
let field = field.get_aspect::<Field>()?;
let transform = transform.to_mat4(true, true, false);
let node = Node::create_parent_name(
&calling_client,
&format!("/item/{}/acceptor", type_info.type_name),
info.name,
true,
)
.add_to_scenegraph()?;
let node = Node::from_id(&calling_client, id, true).add_to_scenegraph()?;
Spatial::add_to(&node, Some(space.clone()), transform, false);
ItemAcceptor::add_to(&node, type_info, field);
(type_info.add_acceptor_aspect)(&node);
Ok(())
}
fn acceptor_capture_item_flex(node: Arc<Node>, item: Arc<Node>) -> Result<()> {
let acceptor = node.get_aspect::<ItemAcceptor>()?;
let item = item.get_aspect::<Item>()?;
capture(&item, &acceptor);
Ok(())
}

View File

@@ -1,205 +1,89 @@
use super::camera::CameraItemAcceptor;
use super::{create_item_acceptor_flex, register_item_ui_flex};
use crate::bail;
use crate::nodes::{
Aspect, AspectIdentifier,
items::{ITEM_ACCEPTOR_ASPECT_ALIAS_INFO, ITEM_ASPECT_ALIAS_INFO, ITEM_UI_ASPECT_ALIAS_INFO},
};
use crate::{
core::{
client::{get_env, state, Client, INTERNAL_CLIENT},
client::{Client, INTERNAL_CLIENT, get_env, state},
error::Result,
registry::Registry,
},
nodes::{
Node,
drawable::model::ModelPart,
items::{Item, ItemType, TypeInfo},
spatial::Spatial,
Message, Node,
spatial::{Spatial, Transform},
},
};
use color_eyre::eyre::{eyre, Result};
use glam::Mat4;
use lazy_static::lazy_static;
use mint::Vector2;
use nanoid::nanoid;
use rustc_hash::FxHashMap;
use serde::{
de::{Deserializer, Error, SeqAccess, Visitor},
ser::Serializer,
Deserialize, Serialize,
};
use stardust_xr::schemas::flex::{deserialize, serialize};
use parking_lot::Mutex;
use slotmap::{DefaultKey, Key, KeyData, SlotMap};
use std::sync::{Arc, Weak};
use tracing::{debug, info};
use tracing::debug;
stardust_xr_server_codegen::codegen_item_panel_protocol!();
impl Default for Geometry {
fn default() -> Self {
Geometry {
origin: [0, 0].into(),
size: [0, 0].into(),
}
}
}
impl Copy for Geometry {}
lazy_static! {
pub static ref KEYMAPS: Mutex<SlotMap<DefaultKey, String>> = Mutex::new(SlotMap::default());
pub static ref ITEM_TYPE_INFO_PANEL: TypeInfo = TypeInfo {
type_name: "panel",
aliased_local_signals: vec![
"apply_surface_material",
"close_toplevel",
"auto_size_toplevel",
"set_toplevel_size",
"set_toplevel_focused_visuals",
"pointer_motion",
"pointer_button",
"pointer_scroll",
"keyboard_keymap",
"keyboard_key",
"touch_down",
"touch_move",
"touch_up",
"reset_touches",
],
aliased_local_methods: vec![],
aliased_remote_signals: vec![
"toplevel_parent_changed",
"toplevel_title_changed",
"toplevel_app_id_changed",
"toplevel_fullscreen_active",
"toplevel_move_request",
"toplevel_resize_request",
"toplevel_size_changed",
"set_cursor",
"new_child",
"reposition_child",
"drop_child",
],
alias_info: PANEL_ITEM_ASPECT_ALIAS_INFO.clone(),
ui_node_id: INTERFACE_NODE_ID,
ui: Default::default(),
items: Registry::new(),
acceptors: Registry::new(),
add_acceptor_aspect: |node| {
node.add_aspect(PanelItemUi);
},
add_ui_aspect: |node| {
node.add_aspect(PanelItemAcceptor);
},
new_acceptor_fn: |node, acceptor, acceptor_field| {
let _ = panel_item_ui_client::create_acceptor(node, acceptor, acceptor_field);
}
};
}
/// An ID for a surface inside this panel item
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum SurfaceID {
Cursor,
Toplevel,
Child(String),
}
impl Default for SurfaceID {
fn default() -> Self {
Self::Toplevel
}
}
impl<'de> serde::Deserialize<'de> for SurfaceID {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_seq(SurfaceIDVisitor)
}
}
struct SurfaceIDVisitor;
impl<'de> Visitor<'de> for SurfaceIDVisitor {
type Value = SurfaceID;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("idk")
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let Some(discrim) = seq.next_element()? else {
return Err(A::Error::missing_field("discrim"));
};
// idk if you wanna check for extraneous elements
// I didn't bother
match discrim {
"Cursor" => Ok(SurfaceID::Cursor),
"Toplevel" => Ok(SurfaceID::Toplevel),
"Child" => {
let Some(text) = seq.next_element()? else {
return Err(A::Error::missing_field("child_text"));
};
Ok(SurfaceID::Child(text))
}
_ => Err(A::Error::unknown_variant(
discrim,
&["Cursor", "Toplevel", "Child"],
)),
}
}
}
impl serde::Serialize for SurfaceID {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::Cursor => ["Cursor"].serialize(serializer),
Self::Toplevel => ["Toplevel"].serialize(serializer),
Self::Child(text) => ["Child", text].serialize(serializer),
}
}
}
/// The origin and size of the surface's "solid" part.
#[derive(Debug, Serialize, Clone, Copy)]
pub struct Geometry {
pub origin: Vector2<i32>,
pub size: Vector2<u32>,
}
/// The state of the panel item's toplevel.
#[derive(Debug, Clone, Serialize)]
pub struct ToplevelInfo {
/// The UID of the panel item of the parent of this toplevel, if it exists
pub parent: Option<String>,
/// Equivalent to the window title
pub title: Option<String>,
/// Application identifier, see <https://standards.freedesktop.org/desktop-entry-spec/>
pub app_id: Option<String>,
/// Current size in pixels
pub size: Vector2<u32>,
/// Recommended minimum size in pixels
pub min_size: Option<Vector2<u32>>,
/// Recommended maximum size in pixels
pub max_size: Option<Vector2<u32>>,
/// Surface geometry
pub logical_rectangle: Geometry,
}
/// Data on positioning a child
#[derive(Debug, Clone, Serialize)]
pub struct ChildInfo {
pub parent: SurfaceID,
pub geometry: Geometry,
}
/// The init data for the panel item.
#[derive(Debug, Clone, Serialize)]
pub struct PanelItemInitData {
/// The cursor, if applicable.
pub cursor: Option<Geometry>,
/// Size of the toplevel surface in pixels.
pub toplevel: ToplevelInfo,
/// Vector of childs that already exist
pub children: FxHashMap<String, ChildInfo>,
/// The surface, if any, that has exclusive input to the pointer.
pub pointer_grab: Option<SurfaceID>,
/// The surface, if any, that has exclusive input to the keyboard.
pub keyboard_grab: Option<SurfaceID>,
}
pub trait Backend: Send + Sync + 'static {
fn start_data(&self) -> Result<PanelItemInitData>;
fn apply_surface_material(&self, surface: SurfaceID, model_part: &Arc<ModelPart>);
fn apply_cursor_material(&self, model_part: &Arc<ModelPart>);
fn apply_surface_material(&self, surface: SurfaceId, model_part: &Arc<ModelPart>);
fn close_toplevel(&self);
fn auto_size_toplevel(&self);
fn set_toplevel_size(&self, size: Vector2<u32>);
fn set_toplevel_focused_visuals(&self, focused: bool);
fn pointer_motion(&self, surface: &SurfaceID, position: Vector2<f32>);
fn pointer_button(&self, surface: &SurfaceID, button: u32, pressed: bool);
fn pointer_motion(&self, surface: &SurfaceId, position: Vector2<f32>);
fn pointer_button(&self, surface: &SurfaceId, button: u32, pressed: bool);
fn pointer_scroll(
&self,
surface: &SurfaceID,
surface: &SurfaceId,
scroll_distance: Option<Vector2<f32>>,
scroll_steps: Option<Vector2<f32>>,
);
fn keyboard_keys(&self, surface: &SurfaceID, keymap_id: &str, keys: Vec<i32>);
fn keyboard_key(&self, surface: &SurfaceId, keymap_id: u64, key: u32, pressed: bool);
fn touch_down(&self, surface: &SurfaceID, id: u32, position: Vector2<f32>);
fn touch_down(&self, surface: &SurfaceId, id: u32, position: Vector2<f32>);
fn touch_move(&self, id: u32, position: Vector2<f32>);
fn touch_up(&self, id: u32);
fn reset_touches(&self);
fn reset_input(&self);
}
pub fn panel_item_from_node(node: &Node) -> Option<Arc<dyn PanelItemTrait>> {
@@ -209,17 +93,19 @@ pub fn panel_item_from_node(node: &Node) -> Option<Arc<dyn PanelItemTrait>> {
Some(panel_item.clone())
}
pub trait PanelItemTrait: Backend + Send + Sync + 'static {
fn uid(&self) -> &str;
fn serialize_start_data(&self, id: &str) -> Result<Message>;
pub trait PanelItemTrait: Send + Sync + 'static {
fn backend(&self) -> &dyn Backend;
fn send_ui_item_created(&self, node: &Node, item: &Arc<Node>);
fn send_acceptor_item_created(&self, node: &Node, item: &Arc<Node>);
}
pub struct PanelItem<B: Backend + ?Sized> {
pub uid: String,
node: Weak<Node>,
#[derive(Debug)]
pub struct PanelItem<B: Backend> {
pub node: Weak<Node>,
pub backend: Box<B>,
}
impl<B: Backend + ?Sized> PanelItem<B> {
impl<B: Backend> PanelItem<B> {
#[cfg_attr(not(feature = "wayland"), allow(dead_code))]
pub fn create(backend: Box<B>, pid: Option<i32>) -> (Arc<Node>, Arc<PanelItem<B>>) {
debug!(?pid, "Create panel item");
@@ -227,20 +113,13 @@ impl<B: Backend + ?Sized> PanelItem<B> {
.and_then(|pid| get_env(pid).ok())
.and_then(|env| state(&env));
let uid = nanoid!();
let node = Arc::new(Node::create_parent_name(
&INTERNAL_CLIENT,
"/item/panel/item",
&uid,
true,
));
let node = Arc::new(Node::generate(&INTERNAL_CLIENT, true));
let spatial = Spatial::add_to(&node, None, Mat4::IDENTITY, false);
if let Some(startup_settings) = &startup_settings {
spatial.set_local_transform(startup_settings.root);
}
let panel_item = Arc::new(PanelItem {
uid: uid.clone(),
node: Arc::downgrade(&node),
backend,
});
@@ -248,354 +127,399 @@ impl<B: Backend + ?Sized> PanelItem<B> {
let generic_panel_item: Arc<dyn PanelItemTrait> = panel_item.clone();
Item::add_to(
&node,
uid,
&ITEM_TYPE_INFO_PANEL,
ItemType::Panel(generic_panel_item),
);
node.add_local_signal("apply_surface_material", Self::apply_surface_material_flex);
node.add_local_signal("close_toplevel", Self::close_toplevel_flex);
node.add_local_signal("auto_size_toplevel", Self::auto_size_toplevel_flex);
node.add_local_signal("set_toplevel_size", Self::set_toplevel_size_flex);
node.add_local_signal("pointer_motion", Self::pointer_motion_flex);
node.add_local_signal("pointer_button", Self::pointer_button_flex);
node.add_local_signal("pointer_scroll", Self::pointer_scroll_flex);
node.add_local_signal("keyboard_key", Self::keyboard_keys_flex);
node.add_local_signal("touch_down", Self::touch_down_flex);
node.add_local_signal("touch_move", Self::touch_move_flex);
node.add_local_signal("touch_up", Self::touch_up_flex);
node.add_local_signal("reset_touches", Self::reset_touches_flex);
node.add_aspect_raw(panel_item.clone());
(node, panel_item)
}
pub fn drop_toplevel(&self) {
let Some(node) = self.node.upgrade() else {
return;
};
node.destroy();
}
}
// Remote signals
impl<B: Backend + ?Sized> PanelItem<B> {
pub fn toplevel_parent_changed(&self, parent: &str) {
#[allow(unused)]
impl<B: Backend> PanelItem<B> {
pub fn toplevel_parent_changed(&self, parent: u64) {
let Some(node) = self.node.upgrade() else {
return;
};
let _ = node.send_remote_signal("toplevel_parent_changed", serialize(parent).unwrap());
panel_item_client::toplevel_parent_changed(&node, parent);
}
pub fn toplevel_title_changed(&self, title: &str) {
let Some(node) = self.node.upgrade() else {
return;
};
let _ = node.send_remote_signal("toplevel_title_changed", serialize(title).unwrap());
panel_item_client::toplevel_title_changed(&node, title);
}
pub fn toplevel_app_id_changed(&self, app_id: &str) {
let Some(node) = self.node.upgrade() else {
return;
};
let _ = node.send_remote_signal("toplevel_app_id_changed", serialize(app_id).unwrap());
panel_item_client::toplevel_app_id_changed(&node, app_id);
}
pub fn toplevel_fullscreen_active(&self, active: bool) {
let Some(node) = self.node.upgrade() else {
return;
};
let _ = node.send_remote_signal("toplevel_fullscreen_active", serialize(active).unwrap());
panel_item_client::toplevel_fullscreen_active(&node, active);
}
pub fn toplevel_move_request(&self) {
let Some(node) = self.node.upgrade() else {
return;
};
let _ = node.send_remote_signal("toplevel_move_request", Vec::<u8>::new());
panel_item_client::toplevel_move_request(&node);
}
pub fn toplevel_resize_request(&self, up: bool, down: bool, left: bool, right: bool) {
let Some(node) = self.node.upgrade() else {
return;
};
let _ = node.send_remote_signal(
"toplevel_resize_request",
serialize((up, down, left, right)).unwrap(),
);
panel_item_client::toplevel_resize_request(&node, up, down, left, right);
}
pub fn toplevel_size_changed(&self, size: Vector2<u32>) {
let Some(node) = self.node.upgrade() else {
return;
};
let _ = node.send_remote_signal("toplevel_size_changed", serialize(size).unwrap());
panel_item_client::toplevel_size_changed(&node, size);
}
pub fn set_cursor(&self, geometry: Option<Geometry>) {
let Some(node) = self.node.upgrade() else {
return;
};
let _ = node.send_remote_signal("set_cursor", serialize(geometry).unwrap());
if let Some(geometry) = geometry {
panel_item_client::set_cursor(&node, &geometry);
} else {
panel_item_client::hide_cursor(&node);
}
}
pub fn new_child(&self, uid: &str, info: ChildInfo) {
pub fn create_child(&self, id: u64, info: &ChildInfo) {
let Some(node) = self.node.upgrade() else {
return;
};
let _ = node.send_remote_signal("new_child", serialize((uid, info)).unwrap());
panel_item_client::create_child(&node, id, info);
}
pub fn reposition_child(&self, uid: &str, geometry: Geometry) {
pub fn reposition_child(&self, id: u64, geometry: &Geometry) {
let Some(node) = self.node.upgrade() else {
return;
};
let _ = node.send_remote_signal("reposition_child", serialize((uid, geometry)).unwrap());
panel_item_client::reposition_child(&node, id, geometry);
}
pub fn drop_child(&self, uid: &str) {
pub fn destroy_child(&self, id: u64) {
let Some(node) = self.node.upgrade() else {
return;
};
let _ = node.send_remote_signal("drop_child", serialize(uid).unwrap());
panel_item_client::destroy_child(&node, id);
}
}
// Local signals
macro_rules! flex_no_args {
($fn_name: ident, $trait_fn: ident) => {
fn $fn_name(
node: Arc<Node>,
_calling_client: Arc<Client>,
_message: Message,
) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
panel_item.$trait_fn();
Ok(())
}
};
impl<B: Backend> AspectIdentifier for PanelItem<B> {
impl_aspect_for_panel_item_aspect_id! {}
}
macro_rules! flex_deserialize {
($fn_name: ident, $trait_fn: ident) => {
fn $fn_name(node: Arc<Node>, _calling_client: Arc<Client>, message: Message) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
panel_item.$trait_fn(deserialize(message.as_ref())?);
Ok(())
}
};
impl<B: Backend> Aspect for PanelItem<B> {
impl_aspect_for_panel_item_aspect! {}
}
impl<B: Backend + ?Sized> PanelItem<B> {
fn apply_surface_material_flex(
#[allow(unused)]
impl<B: Backend> PanelItemAspect for PanelItem<B> {
#[doc = "Apply the cursor as a material to a model."]
fn apply_cursor_material(
node: Arc<Node>,
_calling_client: Arc<Client>,
model_part: Arc<Node>,
) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
let model_part = model_part.get_aspect::<ModelPart>()?;
panel_item.backend().apply_cursor_material(&model_part);
Ok(())
}
#[doc = "Apply a surface's visuals as a material to a model."]
fn apply_surface_material(
node: Arc<Node>,
calling_client: Arc<Client>,
message: Message,
surface: SurfaceId,
model_part: Arc<Node>,
) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
let model_part = model_part.get_aspect::<ModelPart>()?;
#[derive(Debug, Deserialize)]
struct SurfaceMaterialInfo<'a> {
surface: SurfaceID,
model_node_path: &'a str,
}
let info: SurfaceMaterialInfo = deserialize(message.as_ref())?;
let model_node = calling_client
.scenegraph
.get_node(info.model_node_path)
.ok_or_else(|| eyre!("Model node not found"))?;
let model_part = model_node.get_aspect::<ModelPart>()?;
debug!(?info, "Apply surface material");
panel_item.apply_surface_material(info.surface, &model_part);
panel_item
.backend()
.apply_surface_material(surface, &model_part);
Ok(())
}
flex_no_args!(close_toplevel_flex, close_toplevel);
flex_no_args!(auto_size_toplevel_flex, auto_size_toplevel);
flex_deserialize!(set_toplevel_size_flex, set_toplevel_size);
#[doc = "Try to close the toplevel.\n \n The panel item UI handler or panel item acceptor will drop the panel item if this succeeds."]
fn close_toplevel(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
panel_item.backend().close_toplevel();
Ok(())
}
fn pointer_motion_flex(
#[doc = "Request a resize of the surface to whatever size the 2D app wants."]
fn auto_size_toplevel(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
panel_item.backend().auto_size_toplevel();
Ok(())
}
#[doc = "Request a resize of the surface (in pixels)."]
fn set_toplevel_size(
node: Arc<Node>,
_calling_client: Arc<Client>,
message: Message,
size: mint::Vector2<u32>,
) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
let (surface_id, position): (SurfaceID, Vector2<f32>) = deserialize(message.as_ref())?;
debug!(?surface_id, ?position, "Pointer deactivate");
panel_item.pointer_motion(&surface_id, position);
panel_item.backend().set_toplevel_size(size);
Ok(())
}
fn pointer_button_flex(
#[doc = "Tell the toplevel to appear focused visually if true, or unfocused if false."]
fn set_toplevel_focused_visuals(
node: Arc<Node>,
_calling_client: Arc<Client>,
message: Message,
focused: bool,
) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
let (surface_id, button, state): (SurfaceID, u32, u32) = deserialize(message.as_ref())?;
debug!(?surface_id, button, state, "Pointer button");
panel_item.pointer_button(&surface_id, button, state != 0);
panel_item.backend().set_toplevel_focused_visuals(focused);
Ok(())
}
fn pointer_scroll_flex(
#[doc = "Send an event to set the pointer's position (in pixels, relative to top-left of surface). This will activate the pointer."]
fn pointer_motion(
node: Arc<Node>,
_calling_client: Arc<Client>,
message: Message,
surface: SurfaceId,
position: mint::Vector2<f32>,
) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
#[derive(Debug, Deserialize)]
struct PointerScrollInfo {
surface_id: SurfaceID,
axis_continuous: Option<Vector2<f32>>,
axis_discrete: Option<Vector2<f32>>,
}
let info: PointerScrollInfo = deserialize(message.as_ref())?;
debug!(?info, "Pointer scroll");
panel_item.pointer_scroll(&info.surface_id, info.axis_continuous, info.axis_discrete);
panel_item.backend().pointer_motion(&surface, position);
Ok(())
}
fn keyboard_keys_flex(
#[doc = "Send an event to set a pointer button's state if the pointer's active. The `button` is from the `input_event_codes` crate (e.g. BTN_LEFT for left click)."]
fn pointer_button(
node: Arc<Node>,
_calling_client: Arc<Client>,
message: Message,
surface: SurfaceId,
button: u32,
pressed: bool,
) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
let (surface_id, keymap_id, keys): (SurfaceID, &str, Vec<i32>) =
deserialize(message.as_ref())?;
debug!(?keys, "Set keyboard key state");
panel_item.keyboard_keys(&surface_id, keymap_id, keys);
panel_item
.backend()
.pointer_button(&surface, button, pressed);
Ok(())
}
pub fn grab_keyboard(&self, sid: Option<SurfaceID>) {
let Some(node) = self.node.upgrade() else {
#[doc = "Send an event to scroll the pointer if it's active.\nScroll distance is a value in pixels corresponding to the `distance` the surface should be scrolled.\nScroll steps is a value in columns/rows corresponding to the wheel clicks of a mouse or such. This also supports fractions of a wheel click."]
fn pointer_scroll(
node: Arc<Node>,
_calling_client: Arc<Client>,
surface: SurfaceId,
scroll_distance: mint::Vector2<f32>,
scroll_steps: mint::Vector2<f32>,
) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
panel_item
.backend()
.pointer_scroll(&surface, Some(scroll_distance), Some(scroll_steps));
Ok(())
}
#[doc = "Send an event to stop scrolling the pointer."]
fn pointer_stop_scroll(
node: Arc<Node>,
_calling_client: Arc<Client>,
surface: SurfaceId,
) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
panel_item.backend().pointer_scroll(&surface, None, None);
Ok(())
}
#[doc = "Send a series of key presses and releases (positive keycode for pressed, negative for released)."]
fn keyboard_key(
node: Arc<Node>,
_calling_client: Arc<Client>,
surface: SurfaceId,
keymap_id: u64,
key: u32,
pressed: bool,
) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
panel_item
.backend()
.keyboard_key(&surface, keymap_id, key, pressed);
Ok(())
}
#[doc = "Put a touch down on this surface with the unique ID `uid` at `position` (in pixels) from top left corner of the surface."]
fn touch_down(
node: Arc<Node>,
_calling_client: Arc<Client>,
surface: SurfaceId,
uid: u32,
position: mint::Vector2<f32>,
) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
panel_item.backend().touch_down(&surface, uid, position);
Ok(())
}
#[doc = "Move an existing touch point."]
fn touch_move(
node: Arc<Node>,
_calling_client: Arc<Client>,
uid: u32,
position: mint::Vector2<f32>,
) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
panel_item.backend().touch_move(uid, position);
Ok(())
}
#[doc = "Release a touch from its surface."]
fn touch_up(node: Arc<Node>, _calling_client: Arc<Client>, uid: u32) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
panel_item.backend().touch_up(uid);
Ok(())
}
#[doc = "Reset all input, such as pressed keys and pointer clicks and touches. Useful for when it's newly captured into an item acceptor to make sure no input gets stuck."]
fn reset_input(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
panel_item.backend().reset_input();
Ok(())
}
}
pub struct PanelItemUi;
impl AspectIdentifier for PanelItemUi {
impl_aspect_for_panel_item_ui_aspect_id! {}
}
impl Aspect for PanelItemUi {
impl_aspect_for_panel_item_ui_aspect! {}
}
impl PanelItemUiAspect for PanelItemUi {}
pub struct PanelItemAcceptor;
impl AspectIdentifier for PanelItemAcceptor {
impl_aspect_for_panel_item_acceptor_aspect_id! {}
}
impl Aspect for PanelItemAcceptor {
impl_aspect_for_panel_item_acceptor_aspect! {}
}
impl PanelItemAcceptorAspect for PanelItemAcceptor {
fn capture_item(node: Arc<Node>, _calling_client: Arc<Client>, item: Arc<Node>) -> Result<()> {
super::acceptor_capture_item_flex(node, item)
}
}
impl<B: Backend> PanelItemTrait for PanelItem<B> {
fn backend(&self) -> &dyn Backend {
self.backend.as_ref()
}
fn send_ui_item_created(&self, node: &Node, item: &Arc<Node>) {
let Ok(init_data) = self.backend.start_data() else {
return;
};
let _ = panel_item_ui_client::create_item(node, item, init_data);
}
fn send_acceptor_item_created(&self, node: &Node, item: &Arc<Node>) {
let Ok(init_data) = self.backend.start_data() else {
return;
};
let _ = panel_item_acceptor_client::capture_item(node, item, init_data);
}
}
let Ok(message) = serialize(sid) else { return };
let _ = node.send_remote_signal("grab_keyboard", message);
impl InterfaceAspect for Interface {
#[doc = "Register this client to manage the items of a certain type and create default 3D UI for them."]
fn register_panel_item_ui(node: Arc<Node>, calling_client: Arc<Client>) -> Result<()> {
node.add_aspect(CameraItemAcceptor);
register_item_ui_flex(calling_client, &ITEM_TYPE_INFO_PANEL)
}
fn touch_down_flex(
#[doc = "Create an item acceptor to allow temporary ownership of a given type of item. Creates a node at `/item/<item_type>/acceptor/<name>`."]
fn create_panel_item_acceptor(
node: Arc<Node>,
_calling_client: Arc<Client>,
message: Message,
calling_client: Arc<Client>,
id: u64,
parent: Arc<Node>,
transform: Transform,
field: Arc<Node>,
) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
node.add_aspect(PanelItemAcceptor);
create_item_acceptor_flex(
calling_client,
id,
parent,
transform,
&ITEM_TYPE_INFO_PANEL,
field,
)
}
async fn register_keymap(
_node: Arc<Node>,
_calling_client: Arc<Client>,
keymap: String,
) -> Result<u64> {
let mut keymaps = KEYMAPS.lock();
if let Some(found_keymap_id) = keymaps
.iter()
.filter(|(_k, v)| *v == &keymap)
.map(|(k, _v)| k)
.last()
{
return Ok(found_keymap_id.data().as_ffi());
}
let key = keymaps.insert(keymap);
Ok(key.data().as_ffi())
}
async fn get_keymap(
_node: Arc<Node>,
_calling_client: Arc<Client>,
keymap_id: u64,
) -> Result<String> {
let keymaps = KEYMAPS.lock();
let Some(keymap) = keymaps.get(KeyData::from_ffi(keymap_id).into()) else {
bail!("Could not find keymap. Try registering it");
};
let (surface_id, id, position): (SurfaceID, u32, Vector2<f32>) =
deserialize(message.as_ref())?;
debug!(?surface_id, id, ?position, "Touch down");
panel_item.touch_down(&surface_id, id, position);
Ok(())
}
fn touch_move_flex(
node: Arc<Node>,
_calling_client: Arc<Client>,
message: Message,
) -> Result<()> {
let Some(panel_item) = panel_item_from_node(&node) else {
return Ok(());
};
let (id, position): (u32, Vector2<f32>) = deserialize(message.as_ref())?;
debug!(?position, "Touch move");
panel_item.touch_move(id, position);
Ok(())
}
flex_deserialize!(touch_up_flex, touch_up);
flex_no_args!(reset_touches_flex, reset_touches);
}
impl<B: Backend + ?Sized> PanelItemTrait for PanelItem<B> {
fn uid(&self) -> &str {
&self.uid
}
fn serialize_start_data(&self, id: &str) -> Result<Message> {
Ok(serialize((id, self.start_data()?))?.into())
}
}
impl<B: Backend + ?Sized> Backend for PanelItem<B> {
fn start_data(&self) -> Result<PanelItemInitData> {
self.backend.start_data()
}
fn apply_surface_material(&self, surface: SurfaceID, model_part: &Arc<ModelPart>) {
self.backend.apply_surface_material(surface, model_part)
}
fn close_toplevel(&self) {
self.backend.close_toplevel()
}
fn auto_size_toplevel(&self) {
self.backend.auto_size_toplevel()
}
fn set_toplevel_size(&self, size: Vector2<u32>) {
self.backend.set_toplevel_size(size)
}
fn set_toplevel_focused_visuals(&self, focused: bool) {
self.backend.set_toplevel_focused_visuals(focused)
}
fn pointer_motion(&self, surface: &SurfaceID, position: Vector2<f32>) {
self.backend.pointer_motion(surface, position)
}
fn pointer_button(&self, surface: &SurfaceID, button: u32, pressed: bool) {
self.backend.pointer_button(surface, button, pressed)
}
fn pointer_scroll(
&self,
surface: &SurfaceID,
scroll_distance: Option<Vector2<f32>>,
scroll_steps: Option<Vector2<f32>>,
) {
self.backend
.pointer_scroll(surface, scroll_distance, scroll_steps)
}
fn keyboard_keys(&self, surface: &SurfaceID, keymap_id: &str, keys: Vec<i32>) {
self.backend.keyboard_keys(surface, keymap_id, keys)
}
fn touch_down(&self, surface: &SurfaceID, id: u32, position: Vector2<f32>) {
self.backend.touch_down(surface, id, position)
}
fn touch_move(&self, id: u32, position: Vector2<f32>) {
self.backend.touch_move(id, position)
}
fn touch_up(&self, id: u32) {
self.backend.touch_up(id)
}
fn reset_touches(&self) {
self.backend.reset_touches()
}
}
impl<B: Backend + ?Sized> Drop for PanelItem<B> {
fn drop(&mut self) {
// Dropped panel item, basically just a debug breakpoint place
info!("Dropped panel item {}", self.uid);
Ok(keymap.clone())
}
}

View File

@@ -1,35 +1,30 @@
pub mod alias;
pub mod audio;
pub mod data;
pub mod drawable;
pub mod fields;
pub mod hmd;
pub mod input;
pub mod items;
pub mod root;
pub mod spatial;
use color_eyre::eyre::{eyre, Result};
use nanoid::nanoid;
use parking_lot::Mutex;
use portable_atomic::{AtomicBool, Ordering};
use rustc_hash::FxHashMap;
use serde::{de::DeserializeOwned, Serialize};
use self::alias::Alias;
use crate::core::client::Client;
use crate::core::error::{Result, ServerError};
use crate::core::registry::Registry;
use crate::core::scenegraph::MethodResponseSender;
use dashmap::DashMap;
use serde::{Serialize, de::DeserializeOwned};
use spatial::Spatial;
use stardust_xr::messenger::MessageSenderHandle;
use stardust_xr::scenegraph::ScenegraphError;
use stardust_xr::schemas::flex::{deserialize, serialize};
use std::any::{Any, TypeId};
use std::fmt::Debug;
use std::os::fd::OwnedFd;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Weak};
use std::vec::Vec;
use crate::core::client::Client;
use crate::core::registry::Registry;
use crate::core::scenegraph::MethodResponseSender;
use self::alias::Alias;
#[derive(Default)]
pub struct Message {
pub data: Vec<u8>,
@@ -49,20 +44,42 @@ impl AsRef<[u8]> for Message {
}
}
pub type Signal = fn(Arc<Node>, Arc<Client>, Message) -> Result<()>;
pub type Method = fn(Arc<Node>, Arc<Client>, Message, MethodResponseSender);
stardust_xr_server_codegen::codegen_node_protocol!();
pub struct Owned;
impl AspectIdentifier for Owned {
impl_aspect_for_owned_aspect_id! {}
}
impl Aspect for Owned {
impl_aspect_for_owned_aspect! {}
}
impl OwnedAspect for Owned {
fn set_enabled(node: Arc<Node>, _calling_client: Arc<Client>, enabled: bool) -> Result<()> {
node.set_enabled(enabled);
Ok(())
}
fn destroy(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
if node.destroyable {
node.destroy();
}
Ok(())
}
}
pub struct OwnedNode(pub Arc<Node>);
impl Drop for OwnedNode {
fn drop(&mut self) {
self.0.destroy();
}
}
pub struct Node {
pub enabled: Arc<AtomicBool>,
pub(super) uid: String,
path: String,
enabled: AtomicBool,
id: u64,
client: Weak<Client>,
message_sender_handle: Option<MessageSenderHandle>,
// trailing_slash_pos: usize,
local_signals: Mutex<FxHashMap<String, Signal>>,
local_methods: Mutex<FxHashMap<String, Method>>,
aliases: Registry<Alias>,
aspects: Aspects,
destroyable: bool,
@@ -71,51 +88,58 @@ impl Node {
pub fn get_client(&self) -> Option<Arc<Client>> {
self.client.upgrade()
}
// pub fn get_name(&self) -> &str {
// &self.path[self.trailing_slash_pos + 1..]
// }
pub fn get_path(&self) -> &str {
self.path.as_str()
pub fn get_id(&self) -> u64 {
self.id
}
pub fn create_parent_name(
client: &Arc<Client>,
parent: &str,
name: &str,
destroyable: bool,
) -> Self {
let mut path = parent.to_string();
path.push('/');
path.push_str(name);
Self::create_path(client, path, destroyable)
pub fn generate(client: &Arc<Client>, destroyable: bool) -> Self {
Self::from_id(client, client.generate_id(), destroyable)
}
pub fn create_path(client: &Arc<Client>, path: impl ToString, destroyable: bool) -> Self {
pub fn from_id(client: &Arc<Client>, id: u64, destroyable: bool) -> Self {
let node = Node {
enabled: Arc::new(AtomicBool::new(true)),
uid: nanoid!(),
enabled: AtomicBool::new(true),
client: Arc::downgrade(client),
message_sender_handle: client.message_sender_handle.clone(),
path: path.to_string(),
// trailing_slash_pos: parent.len(),
local_signals: Default::default(),
local_methods: Default::default(),
id,
aliases: Default::default(),
aspects: Default::default(),
destroyable,
};
<Node as NodeAspect>::add_node_members(&node);
node.aspects.add(Owned);
node
}
pub fn add_to_scenegraph(self) -> Result<Arc<Node>> {
Ok(self
.get_client()
.ok_or_else(|| eyre!("Internal: Unable to get client"))?
.ok_or(ServerError::NoClient)?
.scenegraph
.add_node(self))
}
pub fn add_to_scenegraph_owned(self) -> Result<OwnedNode> {
Ok(OwnedNode(
self.get_client()
.ok_or(ServerError::NoClient)?
.scenegraph
.add_node(self),
))
}
pub fn enabled(&self) -> bool {
self.enabled.load(Ordering::Relaxed)
&& if let Ok(spatial) = self.get_aspect::<Spatial>() {
spatial.visible()
} else {
true
}
}
pub fn set_enabled(&self, enabled: bool) {
self.enabled.store(enabled, Ordering::Relaxed);
if let Ok(spatial) = self.get_aspect::<Spatial>() {
spatial.mark_dirty();
}
}
pub fn destroy(&self) {
if let Some(client) = self.get_client() {
client.scenegraph.remove_node(self.get_path());
client.scenegraph.remove_node(self.get_id());
}
}
@@ -133,68 +157,66 @@ impl Node {
// Ok(serialize(pid)?.into())
// }
pub fn add_local_signal(&self, name: &str, signal: Signal) {
self.local_signals.lock().insert(name.to_string(), signal);
}
pub fn add_local_method(&self, name: &str, method: Method) {
self.local_methods.lock().insert(name.to_string(), method);
}
pub fn add_aspect<A: Aspect>(&self, aspect: A) -> Arc<A> {
pub fn add_aspect<A: AspectIdentifier>(&self, aspect: A) -> Arc<A> {
self.aspects.add(aspect)
}
pub fn add_aspect_raw<A: Aspect>(&self, aspect: Arc<A>) {
pub fn add_aspect_raw<A: AspectIdentifier>(&self, aspect: Arc<A>) {
self.aspects.add_raw(aspect)
}
pub fn get_aspect<A: Aspect>(&self) -> Result<Arc<A>> {
pub fn get_aspect<A: AspectIdentifier>(&self) -> Result<Arc<A>> {
self.aspects.get()
}
pub fn send_local_signal(
self: Arc<Self>,
calling_client: Arc<Client>,
method: &str,
aspect_id: u64,
method: u64,
message: Message,
) -> Result<(), ScenegraphError> {
if let Ok(alias) = self.get_aspect::<Alias>() {
if !alias.info.server_signals.iter().any(|e| e == &method) {
return Err(ScenegraphError::SignalNotFound);
if !alias.info.server_signals.contains(&method) {
return Err(ScenegraphError::MemberNotFound);
}
alias
.original
.upgrade()
.ok_or(ScenegraphError::BrokenAlias)?
.send_local_signal(calling_client, method, message)
.send_local_signal(calling_client, aspect_id, method, message)
} else {
let signal = self
.local_signals
.lock()
.get(method)
.cloned()
.ok_or(ScenegraphError::SignalNotFound)?;
signal(self, calling_client, message).map_err(|error| ScenegraphError::SignalError {
error: error.to_string(),
})
let aspect = self
.aspects
.0
.get(&aspect_id)
.ok_or(ScenegraphError::AspectNotFound)?
.clone();
aspect
.run_signal(calling_client, self.clone(), method, message)
.map_err(|error| ScenegraphError::MemberError {
error: error.to_string(),
})
}
}
pub fn execute_local_method(
self: Arc<Self>,
calling_client: Arc<Client>,
method: &str,
aspect_id: u64,
method: u64,
message: Message,
response: MethodResponseSender,
) {
if let Ok(alias) = self.get_aspect::<Alias>() {
if !alias.info.server_methods.iter().any(|e| e == &method) {
response.send(Err(ScenegraphError::MethodNotFound));
if !alias.info.server_methods.contains(&method) {
response.send_err(ScenegraphError::MemberNotFound);
return;
}
let Some(alias) = alias.original.upgrade() else {
response.send(Err(ScenegraphError::BrokenAlias));
response.send_err(ScenegraphError::BrokenAlias);
return;
};
alias.execute_local_method(
calling_client,
aspect_id,
method,
Message {
data: message.data.clone(),
@@ -203,14 +225,19 @@ impl Node {
response,
)
} else {
let Some(method) = self.local_methods.lock().get(method).cloned() else {
response.send(Err(ScenegraphError::MethodNotFound));
let Some(aspect) = self.aspects.0.get(&aspect_id).map(|v| v.clone()) else {
response.send_err(ScenegraphError::AspectNotFound);
return;
};
method(self, calling_client, message, response);
aspect.run_method(calling_client, self.clone(), method, message, response);
}
}
pub fn send_remote_signal(&self, method: &str, message: impl Into<Message>) -> Result<()> {
pub fn send_remote_signal(
&self,
aspect_id: u64,
method: u64,
message: impl Into<Message>,
) -> Result<()> {
let message = message.into();
self.aliases
.get_valid_contents()
@@ -220,6 +247,7 @@ impl Node {
.for_each(|node| {
// Beware! file descriptors will not be sent to aliases!!!
let _ = node.send_remote_signal(
aspect_id,
method,
Message {
data: message.data.clone(),
@@ -227,29 +255,28 @@ impl Node {
},
);
});
let path = self.path.clone();
let method = method.to_string();
if let Some(handle) = self.message_sender_handle.as_ref() {
handle.signal(path.as_str(), method.as_str(), &message.data, message.fds)?;
handle.signal(self.id, aspect_id, method, &message.data, message.fds)?;
}
Ok(())
}
pub async fn execute_remote_method_typed<S: Serialize, D: DeserializeOwned>(
&self,
method: &str,
aspect_id: u64,
method: u64,
input: S,
fds: Vec<OwnedFd>,
) -> Result<(D, Vec<OwnedFd>)> {
let message_sender_handle = self
.message_sender_handle
.as_ref()
.ok_or(eyre!("Messenger does not exist for this node"))?;
.ok_or(ServerError::NoMessenger)?;
let serialized = serialize(input)?;
let result = message_sender_handle
.method(self.path.as_str(), method, &serialized, fds)?
.await
.map_err(|e| eyre!(e))?;
.method(self.id, aspect_id, method, &serialized, fds)
.await?
.map_err(ServerError::RemoteMethodError)?;
let (message, fds) = result.into_components();
let deserialized: D = deserialize(&message)?;
@@ -259,59 +286,61 @@ impl Node {
impl Debug for Node {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Node")
.field("uid", &self.uid)
.field("path", &self.path)
.field("id", &self.id)
.field("destroyable", &self.destroyable)
.finish()
}
}
impl NodeAspect for Node {
fn set_enabled(node: Arc<Node>, _calling_client: Arc<Client>, enabled: bool) -> Result<()> {
node.enabled.store(enabled, Ordering::Relaxed);
Ok(())
}
fn destroy(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
if node.destroyable {
node.destroy();
}
Ok(())
}
}
impl Drop for Node {
fn drop(&mut self) {
// Debug breakpoint
}
}
pub trait AspectIdentifier: Aspect {
const ID: u64;
}
pub trait Aspect: Any + Send + Sync + 'static {
const NAME: &'static str;
fn run_signal(
&self,
calling_client: Arc<Client>,
node: Arc<Node>,
signal: u64,
message: Message,
) -> Result<(), stardust_xr::scenegraph::ScenegraphError>;
fn run_method(
&self,
calling_client: Arc<Client>,
node: Arc<Node>,
method: u64,
message: Message,
response: MethodResponseSender,
);
}
#[derive(Default)]
struct Aspects(Mutex<FxHashMap<TypeId, Arc<dyn Any + Send + Sync + 'static>>>);
struct Aspects(DashMap<u64, Arc<dyn Aspect>>);
impl Aspects {
fn add<A: Aspect>(&self, t: A) -> Arc<A> {
fn add<A: AspectIdentifier>(&self, t: A) -> Arc<A> {
let aspect = Arc::new(t);
self.add_raw(aspect.clone());
aspect
}
fn add_raw<A: Aspect>(&self, aspect: Arc<A>) {
self.0.lock().insert(Self::type_key::<A>(), aspect);
fn add_raw<A: AspectIdentifier>(&self, aspect: Arc<A>) {
self.0.insert(A::ID, aspect);
}
fn get<A: Aspect + Any + Send + Sync + 'static>(&self) -> Result<Arc<A>> {
fn get<A: Aspect + AspectIdentifier>(&self) -> Result<Arc<A>> {
self.0
.lock()
.get(&Self::type_key::<A>())
.and_then(|a| Arc::downcast(a.clone()).ok())
.ok_or(eyre!("Couldn't get aspect {}", A::NAME.to_lowercase()))
}
fn type_key<A: 'static>() -> TypeId {
TypeId::of::<A>()
.get(&A::ID)
// .cloned doesn't work for some reason
.map(|v| v.clone())
.and_then(|a| Arc::downcast(a).ok())
.ok_or(ServerError::NoAspect(TypeId::of::<A>()))
}
}
impl Drop for Aspects {
fn drop(&mut self) {
self.0.lock().clear()
// why would this be needed? do drop impls not run otherwise?
self.0.clear()
}
}

View File

@@ -1,142 +1,105 @@
use super::spatial::Spatial;
use super::{Message, Node};
use crate::core::client::Client;
use crate::core::client_state::{ClientState, ClientStateInternal};
use crate::core::registry::Registry;
use crate::core::scenegraph::MethodResponseSender;
#[cfg(feature = "wayland")]
use crate::wayland::WAYLAND_DISPLAY;
#[cfg(feature = "xwayland")]
use crate::wayland::X_DISPLAY;
use crate::STARDUST_INSTANCE;
use color_eyre::eyre::Result;
use super::{Aspect, AspectIdentifier, Node};
use crate::bail;
use crate::core::client::{CLIENTS, Client};
use crate::core::client_state::ClientStateParsed;
use crate::core::error::Result;
use crate::nodes::spatial::SPATIAL_REF_ASPECT_ALIAS_INFO;
use crate::session::connection_env;
use glam::Mat4;
use rustc_hash::FxHashMap;
use stardust_xr::schemas::flex::{deserialize, serialize};
use std::sync::atomic::{AtomicBool, Ordering};
use std::path::PathBuf;
use std::sync::Arc;
use tracing::instrument;
use std::time::Instant;
use tracing::info;
static ROOT_REGISTRY: Registry<Root> = Registry::new();
stardust_xr_server_codegen::codegen_root_protocol!();
pub struct Root {
pub node: Arc<Node>,
send_frame_event: AtomicBool,
node: Arc<Node>,
connect_instant: Instant,
}
impl Root {
pub fn create(client: &Arc<Client>) -> Result<Arc<Self>> {
let node = Node::create_parent_name(client, "", "", false);
node.add_local_signal("subscribe_frame", Root::subscribe_frame_flex);
node.add_local_signal("set_base_prefixes", Root::set_base_prefixes_flex);
node.add_local_method("state_token", Root::state_token_flex);
node.add_local_method(
"get_connection_environment",
get_connection_environment_flex,
);
pub fn create(client: &Arc<Client>, transform: Mat4) -> Result<Arc<Self>> {
let node = Node::from_id(client, 0, false);
let node = node.add_to_scenegraph()?;
let _ = Spatial::add_to(&node, None, client.state.root, false);
Ok(ROOT_REGISTRY.add(Root {
node,
send_frame_event: AtomicBool::from(false),
}))
let _ = Spatial::add_to(&node, None, transform, false);
let root_aspect = node.add_aspect(Root {
node: node.clone(),
connect_instant: Instant::now(),
});
Ok(root_aspect)
}
fn subscribe_frame_flex(
_node: Arc<Node>,
calling_client: Arc<Client>,
_message: Message,
) -> Result<()> {
calling_client
.root
.get()
.unwrap()
.send_frame_event
.store(true, Ordering::Relaxed);
Ok(())
}
#[instrument(level = "debug")]
pub fn send_frame_events(delta: f64) {
if let Ok(data) = serialize((delta, 0.0)) {
for root in ROOT_REGISTRY.get_valid_contents() {
if root.send_frame_event.load(Ordering::Relaxed) {
let _ = root.node.send_remote_signal("frame", data.clone());
}
}
for client in CLIENTS.get_vec() {
let Some(root) = client.root.get() else {
continue;
};
let _ = root_client::frame(
&root.node,
&FrameInfo {
delta: delta as f32,
elapsed: root.connect_instant.elapsed().as_secs_f32(),
},
);
}
}
fn set_base_prefixes_flex(
_node: Arc<Node>,
calling_client: Arc<Client>,
message: Message,
) -> Result<()> {
*calling_client.base_resource_prefixes.lock() = deserialize(message.as_ref())?;
Ok(())
}
fn state_token_flex(
_node: Arc<Node>,
calling_client: Arc<Client>,
message: Message,
response: MethodResponseSender,
) {
response.wrap_sync(|| {
let state: ClientStateInternal = deserialize(message.as_ref())?;
let token = ClientState::from_deserialized(&calling_client, state).token();
Ok(serialize(token)?.into())
})
}
pub fn set_transform(&self, transform: Mat4) {
let spatial = self.node.get_aspect::<Spatial>().unwrap();
spatial.set_spatial_parent(None).unwrap();
// spatial.set_spatial_parent(None).unwrap();
spatial.set_local_transform(transform);
}
pub async fn save_state(&self) -> Result<ClientStateInternal> {
self.node
.execute_remote_method_typed("save_state", (), Vec::new())
.await
.map(|(m, _)| m)
pub async fn save_state(&self) -> Result<ClientState> {
Ok(root_client::save_state(&self.node).await?.0)
}
}
impl AspectIdentifier for Root {
impl_aspect_for_root_aspect_id! {}
}
impl Aspect for Root {
impl_aspect_for_root_aspect! {}
}
impl RootAspect for Root {
async fn get_state(_node: Arc<Node>, calling_client: Arc<Client>) -> Result<ClientState> {
let Some(state) = calling_client.state.get() else {
bail!("Couldn't get state");
};
Ok(state.clone())
}
impl Drop for Root {
fn drop(&mut self) {
ROOT_REGISTRY.remove(self);
#[doc = "Get a hashmap of all the environment variables to connect a given app to the stardust server"]
async fn get_connection_environment(
_node: Arc<Node>,
_calling_client: Arc<Client>,
) -> Result<stardust_xr::values::Map<String, String>> {
Ok(connection_env())
}
#[doc = "Generate a client state token and return it back.\n\n When launching a new client, set the environment variable `STARDUST_STARTUP_TOKEN` to the returned string.\n Make sure the environment variable shows in `/proc/{pid}/environ` as that's the only reliable way to pass the value to the server (suggestions welcome).\n"]
async fn generate_state_token(
_node: Arc<Node>,
calling_client: Arc<Client>,
state: ClientState,
) -> Result<String> {
Ok(ClientStateParsed::from_deserialized(&calling_client, state).token())
}
#[doc = "Set initial list of folders to look for namespaced resources in"]
fn set_base_prefixes(
_node: Arc<Node>,
calling_client: Arc<Client>,
prefixes: Vec<String>,
) -> Result<()> {
info!(?calling_client, ?prefixes, "Set base prefixes");
*calling_client.base_resource_prefixes.lock() =
prefixes.into_iter().map(PathBuf::from).collect();
Ok(())
}
#[doc = "Cleanly disconnect from the server"]
fn disconnect(_node: Arc<Node>, calling_client: Arc<Client>) -> Result<()> {
calling_client.disconnect(Ok(()));
Ok(())
}
}
macro_rules! var_env_insert {
($env:ident, $name:ident) => {
$env.insert(stringify!($name).to_string(), $name.get().unwrap().clone());
};
}
pub fn get_connection_environment_flex(
_node: Arc<Node>,
_calling_client: Arc<Client>,
_message: Message,
response: MethodResponseSender,
) {
response.wrap_sync(move || {
let mut env: FxHashMap<String, String> = FxHashMap::default();
var_env_insert!(env, STARDUST_INSTANCE);
#[cfg(feature = "wayland")]
{
var_env_insert!(env, WAYLAND_DISPLAY);
#[cfg(feature = "xwayland")]
env.insert(
"DISPLAY".to_string(),
format!(":{}", X_DISPLAY.get().unwrap()),
);
env.insert("GDK_BACKEND".to_string(), "wayland".to_string());
env.insert("QT_QPA_PLATFORM".to_string(), "wayland".to_string());
env.insert("MOZ_ENABLE_WAYLAND".to_string(), "1".to_string());
env.insert("CLUTTER_BACKEND".to_string(), "wayland".to_string());
env.insert("SDL_VIDEODRIVER".to_string(), "wayland".to_string());
}
Ok(serialize(env)?.into())
});
}

View File

@@ -1,25 +1,92 @@
pub mod zone;
use self::zone::Zone;
use super::fields::Field;
use super::{Aspect, Node};
use super::alias::Alias;
use super::fields::{Field, FieldTrait};
use super::{Aspect, AspectIdentifier};
use crate::bail;
use crate::core::client::Client;
use crate::core::entity_handle::EntityHandle;
use crate::core::error::Result;
use crate::core::registry::Registry;
use crate::create_interface;
use color_eyre::eyre::{eyre, Result};
use glam::{vec3a, Mat4, Quat};
use crate::nodes::{Node, OWNED_ASPECT_ALIAS_INFO};
use bevy::ecs::entity::EntityHashMap;
use bevy::prelude::Transform as BevyTransform;
use bevy::prelude::*;
use bevy::render::primitives::Aabb;
use color_eyre::eyre::OptionExt;
use glam::{Mat4, Quat, Vec3, vec3a};
use mint::Vector3;
use nanoid::nanoid;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use parking_lot::{Mutex, RwLock};
use rustc_hash::FxHashMap;
use std::fmt::Debug;
use std::ptr;
use std::sync::{Arc, Weak};
use stereokit::{bounds_grow_to_fit_box, Bounds};
use std::pin::Pin;
use std::sync::atomic::Ordering;
use std::sync::{Arc, OnceLock, Weak};
use std::{f32, ptr};
pub struct SpatialNodePlugin;
impl Plugin for SpatialNodePlugin {
fn build(&self, app: &mut App) {
app.add_systems(
PostUpdate,
(spawn_spatial_nodes, update_spatial_nodes)
.chain()
.before(TransformSystem::TransformPropagate),
);
}
}
fn spawn_spatial_nodes(mut cmds: Commands) {
for spatial in SPATIAL_REGISTRY
.get_valid_contents()
.into_iter()
.filter(|v| v.entity.read().is_none())
{
let entity = cmds
.spawn((SpatialNode(Arc::downgrade(&spatial)), Name::new("Spatial")))
.id();
spatial.set_entity(EntityHandle::new(entity));
}
}
fn update_spatial_nodes(
mut query: Query<(&mut BevyTransform, &mut Visibility, Option<&ChildOf>)>,
mut cmds: Commands,
) {
for (entity, (transform, parent_entity)) in UPDATED_SPATIALS_NODES.lock().drain() {
let _span = debug_span!("updating spatial node").entered();
let Ok((mut bevy_transform, mut vis, parent)) = query.get_mut(entity) else {
continue;
};
// Set visibility based on node enabled state
if let Some(transform) = transform {
*vis = Visibility::Inherited;
*bevy_transform = transform;
} else {
*vis = Visibility::Hidden;
}
if parent.map(|v| v.0) != parent_entity {
match parent_entity {
Some(e) => cmds.entity(entity).insert(ChildOf(e)),
None => cmds.entity(entity).remove::<ChildOf>(),
};
}
}
}
static SPATIAL_REGISTRY: Registry<Spatial> = Registry::new();
#[derive(Clone, Component, Debug)]
#[require(BevyTransform, Visibility)]
pub struct SpatialNode(pub Weak<Spatial>);
const EPSILON: f32 = 0.00001;
stardust_xr_server_codegen::codegen_spatial_protocol!();
impl Transform {
pub fn to_mat4(self, position: bool, rotation: bool, scale: bool) -> Mat4 {
pub fn to_mat4(&self, position: bool, rotation: bool, scale: bool) -> Mat4 {
let position = position
.then_some(self.translation)
.flatten()
@@ -28,40 +95,70 @@ impl Transform {
.then_some(self.rotation)
.flatten()
.unwrap_or_else(|| Quat::IDENTITY.into());
// Zero scale values break everything
let scale = scale
.then_some(self.scale)
.flatten()
.map(|s| Vector3 {
x: if s.x == 0.0 { EPSILON } else { s.x },
y: if s.y == 0.0 { EPSILON } else { s.y },
z: if s.z == 0.0 { EPSILON } else { s.z },
})
.unwrap_or_else(|| Vector3::from([1.0; 3]));
Mat4::from_scale_rotation_translation(scale.into(), rotation.into(), position.into())
}
}
impl AspectIdentifier for Zone {
impl_aspect_for_zone_aspect_id! {}
}
impl Aspect for Zone {
impl_aspect_for_zone_aspect! {}
}
lazy_static::lazy_static! {
pub static ref EXPORTED_SPATIALS: Mutex<FxHashMap<u64, Weak<Node>>> = Mutex::new(FxHashMap::default());
}
static ZONEABLE_REGISTRY: Registry<Spatial> = Registry::new();
pub struct Spatial {
uid: String,
pub(super) node: Weak<Node>,
parent: Mutex<Option<Arc<Spatial>>>,
old_parent: Mutex<Option<Arc<Spatial>>>,
pub(super) transform: Mutex<Mat4>,
zone: Mutex<Weak<Zone>>,
pub node: Weak<Node>,
entity: RwLock<Option<EntityHandle>>,
parent: RwLock<Option<Arc<Spatial>>>,
old_parent: RwLock<Option<Arc<Spatial>>>,
transform: RwLock<Mat4>,
zone: RwLock<Weak<Zone>>,
children: Registry<Spatial>,
pub(super) bounding_box_calc: OnceCell<fn(&Node) -> Bounds>,
pub bounding_box_calc:
OnceLock<for<'a> fn(&'a Node) -> Pin<Box<dyn Future<Output = Aabb> + 'a + Send + Sync>>>,
}
impl Spatial {
pub fn new(node: Weak<Node>, parent: Option<Arc<Spatial>>, transform: Mat4) -> Arc<Self> {
Arc::new(Spatial {
uid: nanoid!(),
let spatial = SPATIAL_REGISTRY.add(Spatial {
node,
parent: Mutex::new(parent),
old_parent: Mutex::new(None),
transform: Mutex::new(transform),
zone: Mutex::new(Weak::new()),
entity: RwLock::new(None),
parent: RwLock::new(parent),
old_parent: RwLock::new(None),
transform: RwLock::new(transform),
zone: RwLock::new(Weak::new()),
children: Registry::new(),
bounding_box_calc: OnceCell::default(),
})
bounding_box_calc: OnceLock::default(),
});
spatial.mark_dirty();
spatial
}
pub fn set_entity(&self, entity: EntityHandle) {
self.entity.write().replace(entity);
self.mark_dirty();
for child in self.children.get_valid_contents() {
child.mark_dirty();
}
}
pub fn get_entity(&self) -> Option<Entity> {
self.entity.read().as_ref().map(|v| v.get())
}
pub fn add_to(
node: &Arc<Node>,
@@ -70,15 +167,14 @@ impl Spatial {
zoneable: bool,
) -> Arc<Spatial> {
let spatial = Spatial::new(Arc::downgrade(node), parent.clone(), transform);
<Spatial as SpatialAspect>::add_node_members(node);
if zoneable {
ZONEABLE_REGISTRY.add_raw(&spatial);
}
if let Some(parent) = parent {
parent.children.add_raw(&spatial);
}
<Spatial as SpatialAspect>::add_node_members(node);
node.add_aspect_raw(spatial.clone());
node.add_aspect(SpatialRef);
spatial
}
@@ -93,27 +189,68 @@ impl Spatial {
}
// the output bounds are probably way bigger than they need to be
pub fn get_bounding_box(&self) -> Bounds {
pub async fn get_bounding_box(&self) -> Aabb {
let Some(node) = self.node() else {
return Bounds::default();
return Aabb::default();
};
let mut bounds = match self.bounding_box_calc.get() {
Some(f) => f(&node).await,
None => Aabb::default(),
};
let mut bounds = self
.bounding_box_calc
.get()
.map(|b| (b)(&node))
.unwrap_or_default();
for child in self.children.get_valid_contents() {
bounds = bounds_grow_to_fit_box(
bounds,
child.get_bounding_box(),
Some(child.local_transform()),
);
let mat = child.local_transform();
let child_aabb = Box::pin(child.get_bounding_box()).await;
bounds = Aabb::enclosing([
bounds.min().into(),
bounds.max().into(),
mat.transform_point3(child_aabb.min().into()),
mat.transform_point3(child_aabb.max().into()),
])
.unwrap();
}
bounds
}
pub(super) fn mark_dirty(&self) {
let Some(entity) = self.entity.read().as_ref().map(|v| v.get()) else {
return;
};
let enabled = self
.node()
.is_none_or(|n| n.enabled.load(Ordering::Relaxed))
&& self.local_visible();
let transform = enabled.then(|| BevyTransform::from_matrix(self.local_transform()));
let parent = self
.get_parent()
.and_then(|v| v.entity.read().as_ref().map(|v| v.get()));
UPDATED_SPATIALS_NODES
.lock()
.insert(entity, (transform, parent));
}
pub fn local_transform(&self) -> Mat4 {
*self.transform.lock()
*self.transform.read()
}
fn local_visible(&self) -> bool {
// Check our own scale by looking at matrix column lengths
let mat = self.local_transform();
let x_scale = mat.x_axis.length_squared();
let y_scale = mat.y_axis.length_squared();
let z_scale = mat.z_axis.length_squared();
x_scale >= EPSILON.powi(2) || y_scale >= EPSILON.powi(2) || z_scale >= EPSILON.powi(2)
}
/// Check if this node or any ancestor has zero scale (for visibility culling)
pub fn visible(&self) -> bool {
// Check parent chain
if let Some(parent) = self.get_parent()
&& !parent.visible()
{
return false;
}
// Check our own scale by looking at matrix column lengths
self.local_visible()
}
pub fn global_transform(&self) -> Mat4 {
let parent_transform = self
@@ -124,7 +261,8 @@ impl Spatial {
parent_transform * self.local_transform()
}
pub fn set_local_transform(&self, transform: Mat4) {
*self.transform.lock() = transform;
*self.transform.write() = transform;
self.mark_dirty();
}
pub fn set_local_transform_components(
&self,
@@ -132,9 +270,7 @@ impl Spatial {
transform: Transform,
) {
if reference_space == Some(self) {
self.set_local_transform(
parse_transform(transform, true, true, true) * self.local_transform(),
);
self.set_local_transform(transform.to_mat4(true, true, true) * self.local_transform());
return;
}
let reference_to_parent_transform = reference_space
@@ -185,47 +321,32 @@ impl Spatial {
}
fn get_parent(&self) -> Option<Arc<Spatial>> {
self.parent.lock().clone()
self.parent.read().clone()
}
fn set_parent(self: &Arc<Self>, new_parent: Option<&Arc<Spatial>>) {
fn set_parent(self: &Arc<Self>, new_parent: &Arc<Spatial>) {
if let Some(parent) = self.get_parent() {
parent.children.remove(&self);
}
if let Some(new_parent) = &new_parent {
new_parent.children.add_raw(self);
parent.children.remove(self);
}
new_parent.children.add_raw(self);
*self.parent.lock() = new_parent.cloned();
*self.parent.write() = Some(new_parent.clone());
self.mark_dirty();
}
pub fn set_spatial_parent(self: &Arc<Self>, parent: Option<&Arc<Spatial>>) -> Result<()> {
let is_ancestor = parent
.as_ref()
.map(|parent| self.is_ancestor_of((*parent).clone()))
.unwrap_or(false);
if is_ancestor {
return Err(eyre!("Setting spatial parent would cause a loop"));
pub fn set_spatial_parent(self: &Arc<Self>, parent: &Arc<Spatial>) -> Result<()> {
if self.is_ancestor_of(parent.clone()) {
bail!("Setting spatial parent would cause a loop");
}
self.set_parent(parent);
Ok(())
}
pub fn set_spatial_parent_in_place(
self: &Arc<Self>,
parent: Option<&Arc<Spatial>>,
) -> Result<()> {
let is_ancestor = parent
.as_ref()
.map(|parent| self.is_ancestor_of((*parent).clone()))
.unwrap_or(false);
if is_ancestor {
return Err(eyre!("Setting spatial parent would cause a loop"));
pub fn set_spatial_parent_in_place(self: &Arc<Self>, parent: &Arc<Spatial>) -> Result<()> {
if self.is_ancestor_of(parent.clone()) {
bail!("Setting spatial parent would cause a loop");
}
self.set_local_transform(Spatial::space_to_space_matrix(
Some(&self),
parent.map(AsRef::as_ref),
));
self.set_local_transform(Spatial::space_to_space_matrix(Some(self), Some(parent)));
self.set_parent(parent);
Ok(())
@@ -233,78 +354,22 @@ impl Spatial {
pub(self) fn zone_distance(&self) -> f32 {
self.zone
.lock()
.read()
.upgrade()
.and_then(|zone| zone.field.upgrade())
.map(|zone| zone.field.clone())
.map(|field| field.distance(self, vec3a(0.0, 0.0, 0.0)))
.unwrap_or(f32::MAX)
.unwrap_or(f32::NEG_INFINITY)
}
}
static UPDATED_SPATIALS_NODES: Mutex<EntityHashMap<(Option<BevyTransform>, Option<Entity>)>> =
Mutex::new(EntityHashMap::new());
impl AspectIdentifier for Spatial {
impl_aspect_for_spatial_aspect_id! {}
}
impl Aspect for Spatial {
const NAME: &'static str = "Spatial";
impl_aspect_for_spatial_aspect! {}
}
impl SpatialAspect for Spatial {
async fn get_local_bounding_box(
node: Arc<Node>,
_calling_client: Arc<Client>,
) -> Result<BoundingBox> {
let this_spatial = node.get_aspect::<Spatial>()?;
let bounds = this_spatial.get_bounding_box();
Ok(BoundingBox {
center: mint::Vector3::from(bounds.center),
size: mint::Vector3::from(bounds.dimensions),
})
}
async fn get_relative_bounding_box(
node: Arc<Node>,
_calling_client: Arc<Client>,
relative_to: Arc<Node>,
) -> Result<BoundingBox> {
let this_spatial = node.get_aspect::<Spatial>()?;
let relative_spatial = relative_to.get_aspect::<Spatial>()?;
let center = Spatial::space_to_space_matrix(Some(&this_spatial), Some(&relative_spatial))
.transform_point3([0.0; 3].into());
let bounds = bounds_grow_to_fit_box(
Bounds {
center,
dimensions: [0.0; 3].into(),
},
this_spatial.get_bounding_box(),
Some(Spatial::space_to_space_matrix(
Some(&this_spatial),
Some(&relative_spatial),
)),
);
Ok(BoundingBox {
center: mint::Vector3::from(bounds.center),
size: mint::Vector3::from(bounds.dimensions),
})
}
async fn get_transform(
node: Arc<Node>,
_calling_client: Arc<Client>,
relative_to: Arc<Node>,
) -> Result<Transform> {
let this_spatial = node.get_aspect::<Spatial>()?;
let relative_spatial = relative_to.get_aspect::<Spatial>()?;
let (scale, rotation, position) = Spatial::space_to_space_matrix(
Some(this_spatial.as_ref()),
Some(relative_spatial.as_ref()),
)
.to_scale_rotation_translation();
Ok(Transform {
translation: Some(position.into()),
rotation: Some(rotation.into()),
scale: Some(scale.into()),
})
}
fn set_local_transform(
node: Arc<Node>,
_calling_client: Arc<Client>,
@@ -335,7 +400,7 @@ impl SpatialAspect for Spatial {
let this_spatial = node.get_aspect::<Spatial>()?;
let parent = parent.get_aspect::<Spatial>()?;
this_spatial.set_spatial_parent(Some(&parent))?;
this_spatial.set_spatial_parent(&parent)?;
Ok(())
}
@@ -347,7 +412,7 @@ impl SpatialAspect for Spatial {
let this_spatial = node.get_aspect::<Spatial>()?;
let parent = parent.get_aspect::<Spatial>()?;
this_spatial.set_spatial_parent_in_place(Some(&parent))?;
this_spatial.set_spatial_parent_in_place(&parent)?;
Ok(())
}
@@ -361,16 +426,22 @@ impl SpatialAspect for Spatial {
}
Ok(())
}
// legit gotta find a way to remove old ones, this just keeps the node alive
async fn export_spatial(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<u64> {
let id = rand::random();
EXPORTED_SPATIALS.lock().insert(id, Arc::downgrade(&node));
Ok(id)
}
}
impl PartialEq for Spatial {
fn eq(&self, other: &Self) -> bool {
self.uid == other.uid
self.node.as_ptr() == other.node.as_ptr()
}
}
impl Debug for Spatial {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Spatial")
.field("uid", &self.uid)
.field("parent", &self.parent)
.field("old_parent", &self.old_parent)
.field("transform", &self.transform)
@@ -379,63 +450,127 @@ impl Debug for Spatial {
}
impl Drop for Spatial {
fn drop(&mut self) {
zone::release_drop(self);
zone::release(self);
ZONEABLE_REGISTRY.remove(self);
SPATIAL_REGISTRY.remove(self);
}
}
pub fn parse_transform(transform: Transform, position: bool, rotation: bool, scale: bool) -> Mat4 {
let position = position
.then_some(transform.translation)
.flatten()
.unwrap_or_else(|| Vector3::from([0.0; 3]));
let rotation = rotation
.then_some(transform.rotation)
.flatten()
.unwrap_or_else(|| Quat::IDENTITY.into());
let scale = scale
.then_some(transform.scale)
.flatten()
.unwrap_or_else(|| Vector3::from([1.0; 3]));
pub struct SpatialRef;
impl AspectIdentifier for SpatialRef {
impl_aspect_for_spatial_ref_aspect_id! {}
}
impl Aspect for SpatialRef {
impl_aspect_for_spatial_ref_aspect! {}
}
impl SpatialRefAspect for SpatialRef {
async fn get_local_bounding_box(
node: Arc<Node>,
_calling_client: Arc<Client>,
) -> Result<BoundingBox> {
let this_spatial = node.get_aspect::<Spatial>()?;
let bounds = this_spatial.get_bounding_box().await;
Mat4::from_scale_rotation_translation(scale.into(), rotation.into(), position.into())
Ok(BoundingBox {
center: Vec3::from(bounds.center).into(),
size: Vec3::from(bounds.half_extents * 2.0).into(),
})
}
async fn get_relative_bounding_box(
node: Arc<Node>,
_calling_client: Arc<Client>,
relative_to: Arc<Node>,
) -> Result<BoundingBox> {
let this_spatial = node.get_aspect::<Spatial>()?;
let relative_spatial = relative_to.get_aspect::<Spatial>()?;
let mat = Spatial::space_to_space_matrix(Some(&this_spatial), Some(&relative_spatial));
let bb = this_spatial.get_bounding_box().await;
let bounds = Aabb::enclosing([
mat.transform_point3(bb.min().into()),
mat.transform_point3(bb.max().into()),
])
.unwrap();
Ok(BoundingBox {
center: Vec3::from(bounds.center).into(),
size: Vec3::from(bounds.half_extents * 2.0).into(),
})
}
async fn get_transform(
node: Arc<Node>,
_calling_client: Arc<Client>,
relative_to: Arc<Node>,
) -> Result<Transform> {
let this_spatial = node.get_aspect::<Spatial>()?;
let relative_spatial = relative_to.get_aspect::<Spatial>()?;
let (scale, rotation, position) = Spatial::space_to_space_matrix(
Some(this_spatial.as_ref()),
Some(relative_spatial.as_ref()),
)
.to_scale_rotation_translation();
Ok(Transform {
translation: Some(position.into()),
rotation: Some(rotation.into()),
scale: Some(scale.into()),
})
}
}
pub struct SpatialInterface;
impl SpatialInterfaceAspect for SpatialInterface {
impl InterfaceAspect for Interface {
fn create_spatial(
_node: Arc<Node>,
calling_client: Arc<Client>,
name: String,
id: u64,
parent: Arc<Node>,
transform: Transform,
zoneable: bool,
) -> Result<()> {
let parent = parent.get_aspect::<Spatial>()?;
let transform = parse_transform(transform, true, true, true);
let node = Node::create_parent_name(&calling_client, "/spatial/spatial", &name, true)
.add_to_scenegraph()?;
let transform = transform.to_mat4(true, true, true);
let node = Node::from_id(&calling_client, id, true).add_to_scenegraph()?;
Spatial::add_to(&node, Some(parent.clone()), transform, zoneable);
Ok(())
}
fn create_zone(
_node: Arc<Node>,
calling_client: Arc<Client>,
name: String,
id: u64,
parent: Arc<Node>,
transform: Transform,
field: Arc<Node>,
) -> Result<()> {
let parent = parent.get_aspect::<Spatial>()?;
let transform = parse_transform(transform, true, true, false);
let transform = transform.to_mat4(true, true, false);
let field = field.get_aspect::<Field>()?;
let node = Node::create_parent_name(&calling_client, "/spatial/zone", &name, true)
.add_to_scenegraph()?;
let node = Node::from_id(&calling_client, id, true).add_to_scenegraph()?;
let space = Spatial::add_to(&node, Some(parent.clone()), transform, false);
Zone::add_to(&node, space, &field);
Zone::add_to(&node, space, field);
Ok(())
}
}
create_interface!(SpatialInterface, SpatialInterfaceAspect, "/spatial");
async fn import_spatial_ref(
_node: Arc<Node>,
calling_client: Arc<Client>,
uid: u64,
) -> Result<Arc<Node>> {
Ok(EXPORTED_SPATIALS
.lock()
.get(&uid)
.and_then(|s| s.upgrade())
.map(|s| {
Alias::create(
&s,
&calling_client,
SPATIAL_REF_ASPECT_ALIAS_INFO.clone(),
None,
)
.unwrap()
})
.ok_or_eyre("Couldn't find spatial with that ID")?)
}
}

View File

@@ -1,166 +1,149 @@
use super::{Spatial, ZoneAspect, ZONEABLE_REGISTRY};
use super::{
SPATIAL_ASPECT_ALIAS_INFO, SPATIAL_REF_ASPECT_ALIAS_INFO, Spatial, ZONEABLE_REGISTRY,
ZoneAspect,
};
use crate::{
core::{client::Client, registry::Registry},
core::{client::Client, error::Result, registry::Registry},
nodes::{
alias::{Alias, AliasInfo},
fields::Field,
Aspect, Node,
Node,
alias::{Alias, AliasList, get_original},
fields::{Field, FieldTrait},
},
};
use glam::vec3a;
use parking_lot::Mutex;
use rustc_hash::FxHashMap;
use std::sync::{Arc, Weak};
pub fn capture(spatial: &Arc<Spatial>, zone: &Arc<Zone>) {
let old_distance = spatial.zone_distance();
let new_distance = zone
.field
.upgrade()
.map(|field| field.distance(spatial, vec3a(0.0, 0.0, 0.0)))
.unwrap_or(f32::MAX);
if new_distance.abs() < old_distance.abs() {
release(spatial);
*spatial.old_parent.lock() = spatial.get_parent();
*spatial.zone.lock() = Arc::downgrade(zone);
zone.captured.add_raw(spatial);
let Some(node) = zone.spatial.node.upgrade() else {
return;
};
let _ = super::zone_client::capture(&node, &spatial.uid);
let new_distance = zone.field.distance(spatial, vec3a(0.0, 0.0, 0.0));
if new_distance.abs() > old_distance.abs() {
return;
}
release(spatial);
*spatial.old_parent.write() = spatial.get_parent();
*spatial.zone.write() = Arc::downgrade(zone);
let Some(zone_node) = zone.spatial.node.upgrade() else {
return;
};
let Some(spatial_node) = spatial.node.upgrade() else {
return;
};
let Ok(spatial_alias) = Alias::create(
&spatial_node,
&zone_node.get_client().unwrap(),
SPATIAL_ASPECT_ALIAS_INFO.clone(),
Some(&zone.captured),
) else {
return;
};
let _ = super::zone_client::capture(&zone_node, &spatial_alias);
}
pub fn release(spatial: &Arc<Spatial>) {
let _ = spatial.set_spatial_parent_in_place(spatial.old_parent.lock().take().as_ref());
let mut spatial_zone = spatial.zone.lock();
pub fn release(spatial: &Spatial) {
let Some(spatial_node) = spatial.node.upgrade() else {
return;
};
let spatial = spatial_node.get_aspect::<Spatial>().unwrap();
let Some(old_parent) = spatial.old_parent.read().clone() else {
return;
};
let _ = spatial.set_spatial_parent_in_place(&old_parent);
let mut spatial_zone = spatial.zone.write();
if let Some(spatial_zone) = spatial_zone.upgrade() {
spatial_zone.captured.remove_aspect(spatial.as_ref());
let Some(node) = spatial_zone.spatial.node.upgrade() else {
return;
};
spatial_zone.captured.remove(spatial);
let _ = super::zone_client::release(&node, &spatial.uid);
let _ = super::zone_client::release(&node, spatial_node.id);
}
*spatial_zone = Weak::new();
}
pub(super) fn release_drop(spatial: &Spatial) {
let spatial_zone = spatial.zone.lock();
if let Some(spatial_zone) = spatial_zone.upgrade() {
let Some(node) = spatial_zone.spatial.node.upgrade() else {
return;
};
spatial_zone.captured.remove(spatial);
let _ = super::zone_client::release(&node, &spatial.uid);
}
}
pub struct Zone {
spatial: Arc<Spatial>,
pub field: Weak<Field>,
zoneables: Mutex<FxHashMap<String, Arc<Node>>>,
captured: Registry<Spatial>,
pub field: Arc<Field>,
intersecting_spatials: Registry<Spatial>,
intersecting: AliasList,
captured: AliasList,
}
impl Zone {
pub fn add_to(node: &Arc<Node>, spatial: Arc<Spatial>, field: &Arc<Field>) -> Arc<Zone> {
pub fn add_to(node: &Arc<Node>, spatial: Arc<Spatial>, field: Arc<Field>) -> Arc<Zone> {
let zone = Arc::new(Zone {
spatial,
field: Arc::downgrade(field),
zoneables: Mutex::new(FxHashMap::default()),
captured: Registry::new(),
field,
intersecting_spatials: Registry::default(),
intersecting: AliasList::default(),
captured: AliasList::default(),
});
<Zone as ZoneAspect>::add_node_members(node);
node.add_aspect_raw(zone.clone());
zone
}
}
impl Aspect for Zone {
const NAME: &'static str = "Zone";
}
impl ZoneAspect for Zone {
fn update(node: Arc<Node>, _calling_client: Arc<Client>) -> color_eyre::eyre::Result<()> {
let zone = node.get_aspect::<Zone>()?;
let Some(field) = zone.field.upgrade() else {
return Err(color_eyre::eyre::eyre!("Zone's field has been destroyed"));
};
let Some((zone_client, zone_node)) = zone
.spatial
.node
.upgrade()
.and_then(|n| n.get_client().zip(Some(n)))
else {
return Err(color_eyre::eyre::eyre!("No client on node?"));
};
let mut old_zoneables = zone.zoneables.lock();
for (_uid, zoneable) in old_zoneables.iter() {
zoneable.destroy();
}
let captured = zone.captured.get_valid_contents();
let zoneables = ZONEABLE_REGISTRY
.get_valid_contents()
.into_iter()
.filter(|zoneable| zoneable.node.upgrade().is_some())
.filter(|zoneable| {
if captured
.iter()
.any(|captured| Arc::ptr_eq(captured, zoneable))
{
return true;
}
let spatial_zone_distance = zoneable.zone_distance();
let self_zone_distance = field.distance(zoneable, vec3a(0.0, 0.0, 0.0));
self_zone_distance < 0.0 && spatial_zone_distance > self_zone_distance
})
.filter_map(|zoneable| {
let alias = Alias::create(
&zone_client,
zone_node.get_path(),
&zoneable.uid,
&zoneable.node.upgrade().unwrap(),
AliasInfo {
server_signals: vec![
"set_transform",
"set_spatial_parent",
"set_spatial_parent_in_place",
],
server_methods: vec!["get_bounds", "get_transform"],
..Default::default()
},
)
.ok()?;
Some((zoneable.uid.clone(), alias))
})
.collect::<FxHashMap<String, Arc<Node>>>();
pub fn update(&self) -> Result<()> {
let node = self.spatial.node().unwrap();
for (uid, zoneable) in zoneables
.iter()
.filter(|(k, _)| !old_zoneables.contains_key(*k))
{
super::zone_client::enter(&node, uid, zoneable)?;
}
for left_uid in old_zoneables.keys().filter(|k| !zoneables.contains_key(*k)) {
super::zone_client::leave(&node, &left_uid)?;
let current_zoneables = Registry::new();
for zoneable in ZONEABLE_REGISTRY.get_valid_contents() {
// Skip if the zoneable is an ancestor of the zone or the zone itself
if zoneable.is_ancestor_of(self.spatial.clone()) {
continue;
}
let distance = self.field.distance(&zoneable, [0.0; 3].into());
if distance > 0.0 {
continue;
}
if distance < zoneable.zone_distance() {
continue;
}
current_zoneables.add_raw(&zoneable);
}
*old_zoneables = zoneables;
let (added, removed) =
Registry::get_changes(&self.intersecting_spatials, &current_zoneables);
for added in added {
let Some(added_node) = added.node() else {
continue;
};
let Ok(alias) = Alias::create(
&added_node,
&self.spatial.node().unwrap().get_client().unwrap(),
SPATIAL_REF_ASPECT_ALIAS_INFO.clone(),
Some(&self.intersecting),
) else {
continue;
};
let _ = super::zone_client::enter(&node, &alias);
}
for removed in removed {
let Some(removed_node) = removed.node() else {
continue;
};
release(&removed);
let _ = super::zone_client::leave(&node, removed_node.id);
self.intersecting.remove_aspect(removed.as_ref());
}
self.intersecting_spatials.set(&current_zoneables);
Ok(())
}
}
impl ZoneAspect for Zone {
fn update(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
let zone = node.get_aspect::<Zone>()?;
let _ = zone.update();
Ok(())
}
fn capture(
node: Arc<Node>,
_calling_client: Arc<Client>,
spatial: Arc<Node>,
) -> color_eyre::eyre::Result<()> {
fn capture(node: Arc<Node>, _calling_client: Arc<Client>, spatial: Arc<Node>) -> Result<()> {
let zone = node.get_aspect::<Zone>()?;
let spatial = spatial.get_aspect()?;
capture(&spatial, &zone);
Ok(())
}
fn release(
_node: Arc<Node>,
_calling_client: Arc<Client>,
spatial: Arc<Node>,
) -> color_eyre::eyre::Result<()> {
fn release(_node: Arc<Node>, _calling_client: Arc<Client>, spatial: Arc<Node>) -> Result<()> {
let spatial = spatial.get_aspect()?;
release(&spatial);
Ok(())
@@ -168,7 +151,13 @@ impl ZoneAspect for Zone {
}
impl Drop for Zone {
fn drop(&mut self) {
for captured in self.captured.get_valid_contents() {
for captured in self
.captured
.get_aliases()
.into_iter()
.filter_map(|n| get_original(n, false))
.filter_map(|n| n.get_aspect::<Spatial>().ok())
{
release(&captured);
}
}

109
src/objects/hmd.rs Normal file
View File

@@ -0,0 +1,109 @@
use std::sync::Arc;
use bevy::prelude::*;
use bevy_mod_openxr::{
helper_traits::{ToQuat as _, ToVec3 as _},
resources::{OxrFrameState, Pipelined},
session::OxrSession,
};
use bevy_mod_xr::{
session::{XrPreDestroySession, XrSessionCreated, session_running},
spaces::{XrPrimaryReferenceSpace, XrSpace},
};
use openxr::SpaceLocationFlags;
use crate::{DbusConnection, PreFrameWait, get_time, nodes::spatial::Spatial};
use super::{ObjectHandle, SpatialRef, input::mouse_pointer::FlatscreenCam};
pub struct HmdPlugin;
impl Plugin for HmdPlugin {
fn build(&self, app: &mut App) {
app.add_systems(XrPreDestroySession, destroy_view_space);
app.add_systems(XrSessionCreated, create_view_space);
app.add_systems(PreFrameWait, update_xr.run_if(session_running));
app.add_systems(PreFrameWait, update_flat.run_if(not(session_running)));
app.add_systems(Startup, setup);
}
}
fn setup(connection: Res<DbusConnection>, mut cmds: Commands) {
let (spatial, _spatial_handle) = SpatialRef::create(&connection, "/org/stardustxr/HMD");
let hmd = Hmd {
spatial,
_spatial_handle,
space: None,
};
cmds.insert_resource(hmd);
}
fn create_view_space(session: Res<OxrSession>, mut hmd: ResMut<Hmd>) {
let space = session
.create_reference_space(openxr::ReferenceSpaceType::VIEW, Transform::IDENTITY)
.inspect_err(|err| error!("failed to create View XrSpace"))
.ok();
hmd.space = space.map(|v| v.0);
}
fn destroy_view_space(session: Res<OxrSession>, mut cmds: Commands, mut hmd: ResMut<Hmd>) {
let Some(space) = hmd.space.take() else {
return;
};
session.destroy_space(space);
}
#[derive(Resource)]
struct Hmd {
spatial: Arc<Spatial>,
_spatial_handle: ObjectHandle<SpatialRef>,
space: Option<XrSpace>,
}
fn update_flat(cam: Single<&GlobalTransform, With<FlatscreenCam>>, hmd: Res<Hmd>) {
// this shouldn't be parented to anything, so global and local spaces should be the same
hmd.spatial.set_local_transform(cam.compute_matrix());
}
fn update_xr(
session: Option<Res<OxrSession>>,
ref_space: Option<Res<XrPrimaryReferenceSpace>>,
hmd: Res<Hmd>,
state: Option<Res<OxrFrameState>>,
pipelined: Option<Res<Pipelined>>,
) {
let (Some(session), Some(view), Some(ref_space), Some(state)) =
(session, hmd.space, ref_space, state)
else {
// tokio::task::spawn({
// let handle = hmd.tracked_handle.clone();
// async move {
// handle.set_tracked(false);
// }
// });
return;
};
let time = get_time(pipelined.is_some(), &state);
let location = session
.locate_space(&view, &ref_space, time)
.inspect_err(|err| error!("Error while Locating OpenXR Stage Space {err}"));
if let Ok(location) = location {
let is_tracked = location
.location_flags
.contains(SpaceLocationFlags::POSITION_VALID | SpaceLocationFlags::POSITION_TRACKED)
|| location.location_flags.contains(
SpaceLocationFlags::ORIENTATION_VALID | SpaceLocationFlags::ORIENTATION_TRACKED,
);
// tokio::task::spawn({
// let handle = play_space.tracked_handle.clone();
// async move {
// handle.set_tracked(is_tracked);
// }
// });
if is_tracked {
hmd.spatial
.set_local_transform(Mat4::from_rotation_translation(
location.pose.orientation.to_quat(),
location.pose.position.to_vec3(),
));
}
}
}

View File

@@ -1,18 +1,22 @@
use crate::{
core::client::INTERNAL_CLIENT,
nodes::{
input::{pointer::Pointer, InputMethod, InputType},
Node, OwnedNode,
fields::{FieldTrait, Ray},
input::{INPUT_HANDLER_REGISTRY, InputDataType, InputMethod, Pointer},
spatial::Spatial,
Node,
},
};
use color_eyre::eyre::Result;
use glam::Mat4;
use nanoid::nanoid;
use serde::Serialize;
use stardust_xr::{schemas::flex::flexbuffers, values::Datamap};
use glam::{Mat4, vec3};
use serde::{Deserialize, Serialize};
use stardust_xr::values::Datamap;
use std::sync::Arc;
use stereokit::StereoKitMultiThread;
#[derive(Default, Deserialize, Serialize)]
pub struct EyeDatamap {
eye: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct KeyboardEvent {
@@ -23,33 +27,25 @@ pub struct KeyboardEvent {
}
pub struct EyePointer {
node: OwnedNode,
spatial: Arc<Spatial>,
pointer: Arc<InputMethod>,
}
impl EyePointer {
pub fn new() -> Result<Self> {
let node = Node::create_parent_name(&INTERNAL_CLIENT, "", &nanoid!(), false)
.add_to_scenegraph()?;
let spatial = Spatial::add_to(&node, None, Mat4::IDENTITY, false);
let pointer =
InputMethod::add_to(&node, InputType::Pointer(Pointer::default()), None).unwrap();
let node = Node::generate(&INTERNAL_CLIENT, false).add_to_scenegraph_owned()?;
let spatial = Spatial::add_to(&node.0, None, Mat4::IDENTITY, false);
let pointer = InputMethod::add_to(
&node.0,
InputDataType::Pointer(Pointer::default()),
Datamap::from_typed(EyeDatamap::default())?,
)
.unwrap();
Ok(EyePointer { spatial, pointer })
}
pub fn update(&self, sk: &impl StereoKitMultiThread) {
let ray = sk.input_eyes();
self.spatial
.set_local_transform(Mat4::from_rotation_translation(
ray.orientation,
ray.position,
));
{
// Set pointer input datamap
let mut fbb = flexbuffers::Builder::default();
let mut map = fbb.start_map();
map.push("eye", 2);
map.end_map();
*self.pointer.datamap.lock() = Datamap::from_raw(fbb.take_buffer()).ok();
}
Ok(EyePointer {
node,
spatial,
pointer,
})
}
}

View File

@@ -1,4 +1,92 @@
pub mod eye_pointer;
pub mod mouse_pointer;
pub mod sk_controller;
pub mod sk_hand;
pub mod oxr_controller;
pub mod oxr_hand;
use crate::nodes::{
fields::{Field, FieldTrait, Ray},
input::{INPUT_HANDLER_REGISTRY, InputDataTrait, InputDataType, InputHandler, InputMethod},
spatial::Spatial,
};
use glam::vec3;
use std::{
collections::VecDeque,
sync::{Arc, Weak},
};
#[derive(Default)]
pub struct CaptureManager {
pub capture: Weak<InputHandler>,
}
impl CaptureManager {
pub fn update_capture(&mut self, method: &InputMethod) {
if let Some(capture) = &self.capture.upgrade()
&& !method
.capture_attempts
.get_valid_contents()
.contains(capture)
{
self.capture = Weak::new();
}
}
pub fn set_new_capture(
&mut self,
method: &InputMethod,
distance_calculator: DistanceCalculator,
) {
if self.capture.upgrade().is_none() {
self.capture = find_closest_capture(method, distance_calculator);
}
}
pub fn apply_capture(&self, method: &InputMethod) {
method.captures.clear();
if let Some(capture) = &self.capture.upgrade() {
method.set_handler_order([capture].into_iter());
method.captures.add_raw(capture);
}
}
}
type DistanceCalculator = fn(&Arc<Spatial>, &InputDataType, &Field) -> Option<f32>;
pub fn find_closest_capture(
method: &InputMethod,
distance_calculator: DistanceCalculator,
) -> Weak<InputHandler> {
method
.capture_attempts
.get_valid_contents()
.into_iter()
.filter_map(|h| {
distance_calculator(&method.spatial, &method.data.lock(), &h.field)
.map(|dist| (h.clone(), dist))
})
.min_by(|(_, dist_a), (_, dist_b)| dist_a.partial_cmp(dist_b).unwrap())
.map(|(handler, _)| Arc::downgrade(&handler))
.unwrap_or_default()
}
/// sorts them greatest to least distance (so you can pop off the closest ones easily)
pub fn get_sorted_handlers(
method: &InputMethod,
distance_calculator: DistanceCalculator,
) -> Vec<(Arc<InputHandler>, f32)> {
let mut handlers = INPUT_HANDLER_REGISTRY
.get_valid_contents()
.into_iter()
.filter(|handler| handler.spatial.node().is_some_and(|node| node.enabled()))
.filter(|handler| {
handler
.field
.spatial
.node()
.is_some_and(|node| node.enabled())
})
.filter_map(|handler| {
distance_calculator(&method.spatial, &method.data.lock(), &handler.field)
.map(|distance| (handler, distance))
})
.collect::<Vec<_>>();
handlers.sort_by(|(_, dist_a), (_, dist_b)| dist_a.partial_cmp(dist_b).unwrap());
handlers
}

View File

@@ -1,250 +1,589 @@
use super::{CaptureManager, DistanceCalculator, get_sorted_handlers};
use crate::{
core::client::INTERNAL_CLIENT,
DbusConnection, ObjectRegistryRes,
core::{client::INTERNAL_CLIENT, task},
nodes::{
data::{
mask_matches, pulse_receiver_client, PulseSender, KEYMAPS, PULSE_RECEIVER_REGISTRY,
},
fields::{Field, Ray},
input::{pointer::Pointer, InputMethod, InputType},
Node, OwnedNode,
fields::{EXPORTED_FIELDS, Field, FieldTrait, Ray},
input::{InputDataType, InputMethod, Pointer},
items::panel::KEYMAPS,
spatial::Spatial,
Node,
},
objects::FieldRef,
};
use bevy::{
input::{
ButtonState,
keyboard::{KeyboardInput, NativeKey, NativeKeyCode},
mouse::{MouseMotion, MouseWheel},
},
prelude::*,
window::PrimaryWindow,
};
use color_eyre::eyre::Result;
use glam::{vec2, vec3, Mat4, Vec2, Vec3};
use nanoid::nanoid;
use dashmap::DashMap;
use glam::{Mat4, Vec3, vec3};
use mint::Vector2;
use rustc_hash::{FxHashMap, FxHasher};
use serde::{Deserialize, Serialize};
use stardust_xr::values::Datamap;
use std::{convert::TryFrom, sync::Arc};
use stereokit::{ray_from_mouse, ButtonState, Key, StereoKitMultiThread};
use xkbcommon::xkb::{Context, Keymap, FORMAT_TEXT_V1};
use slotmap::{DefaultKey, Key as SlotKey};
use stardust_xr::{
schemas::dbus::{
ObjectInfo,
interfaces::FieldRefProxy,
list_query::{ListEvent, ObjectListQuery},
object_registry::ObjectRegistry,
query::{ObjectQuery, QueryContext, QueryEvent},
},
values::Datamap,
};
use std::sync::{Arc, Weak};
use tokio::sync::{Notify, mpsc, watch};
use tokio::task::{AbortHandle, JoinSet};
use tokio::time::{Duration, timeout};
use xkbcommon_rs::{Context, Keymap, KeymapFormat, xkb_keymap::CompileFlags};
use zbus::{Connection, names::OwnedInterfaceName};
#[derive(Default, Deserialize, Serialize)]
#[derive(Clone)]
struct HandlerInfo {
handler: ObjectInfo,
field_ref: Arc<Field>,
keyboard_proxy: KeyboardHandlerProxy<'static>,
}
#[derive(Debug, Clone)]
struct InputEvent {
key: u32,
pressed: bool,
}
pub struct FlatscreenInputPlugin;
impl Plugin for FlatscreenInputPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, setup);
// yes the input method will be delayed by one frame, its only for debugging anyways
app.add_systems(Update, update_pointer);
}
}
#[derive(Component)]
#[require(Camera3d)]
pub struct FlatscreenCam;
fn setup(mut cmds: Commands, object_registry: Res<ObjectRegistryRes>) {
let Ok(pointer) = MousePointer::new(object_registry.0.clone())
.inspect_err(|err| error!("unable to create mouse pointer: {err}"))
else {
return;
};
cmds.spawn((FlatscreenCam, Name::new("Flatscreen Camera")));
cmds.insert_resource(pointer);
}
fn update_pointer(
window: Single<(&Window), With<PrimaryWindow>>,
mut cam: Single<(&Camera, &GlobalTransform, &mut Transform), With<FlatscreenCam>>,
mut pointer: ResMut<MousePointer>,
connection: Res<DbusConnection>,
object_registry: Res<ObjectRegistryRes>,
mouse_buttons: Res<ButtonInput<MouseButton>>,
keyboard_buttons: Res<ButtonInput<KeyCode>>,
mut scroll: EventReader<MouseWheel>,
mut motion: EventReader<MouseMotion>,
mut keyboard_input_events: EventReader<KeyboardInput>,
time: Res<Time>,
) {
let (cam, cam_transform, mut cam_local_transform) = cam.into_inner();
if keyboard_buttons.pressed(KeyCode::ShiftLeft) && mouse_buttons.pressed(MouseButton::Right) {
let (mut yaw, mut pitch, _) = cam_local_transform.rotation.to_euler(EulerRot::YXZ);
for e in motion.read() {
let scale = -0.003;
pitch += e.delta.y * scale;
yaw += e.delta.x * scale;
}
cam_local_transform.rotation = Quat::from_rotation_y(yaw) * Quat::from_rotation_x(pitch);
let mut move_vec = Vec3::ZERO;
move_vec.x += keyboard_buttons.pressed(KeyCode::KeyD) as u32 as f32;
move_vec.x -= keyboard_buttons.pressed(KeyCode::KeyA) as u32 as f32;
move_vec.z += keyboard_buttons.pressed(KeyCode::KeyS) as u32 as f32;
move_vec.z -= keyboard_buttons.pressed(KeyCode::KeyW) as u32 as f32;
move_vec.y += keyboard_buttons.pressed(KeyCode::KeyE) as u32 as f32;
move_vec.y -= keyboard_buttons.pressed(KeyCode::KeyQ) as u32 as f32;
let move_vec = cam_local_transform.rotation * move_vec.normalize_or_zero();
cam_local_transform.translation += move_vec * time.delta_secs() * 3.0;
return;
}
let Some(ray) = window
.cursor_position()
.and_then(|pos| get_viewport_pos(pos, cam))
.and_then(|pos| cam.viewport_to_world(cam_transform, pos).ok())
else {
return;
};
pointer.update(
&connection,
&object_registry,
ray,
&mouse_buttons,
&keyboard_buttons,
scroll,
keyboard_input_events,
);
}
fn get_viewport_pos(logical_pos: Vec2, cam: &Camera) -> Option<Vec2> {
if let Some(viewport_rect) = cam.logical_viewport_rect() {
if !viewport_rect.contains(logical_pos) {
return None;
}
Some(logical_pos - viewport_rect.min)
} else {
Some(logical_pos)
}
}
#[derive(Debug, Deserialize, Serialize)]
struct MouseEvent {
select: f32,
middle: f32,
context: f32,
grab: f32,
scroll_continuous: Vec2,
scroll_discrete: Vec2,
scroll_continuous: Vector2<f32>,
scroll_discrete: Vector2<f32>,
raw_input_events: Vec<u32>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct KeyboardEvent {
pub keyboard: (),
pub xkbv1: (),
pub keymap_id: String,
pub keys: Vec<i32>,
}
impl Default for KeyboardEvent {
impl Default for MouseEvent {
fn default() -> Self {
Self {
keyboard: (),
xkbv1: (),
keymap_id: "flatscreen".to_string(),
keys: Default::default(),
MouseEvent {
select: 0.0,
middle: 0.0,
context: 0.0,
grab: 0.0,
scroll_continuous: [0.0; 2].into(),
scroll_discrete: [0.0; 2].into(),
raw_input_events: vec![],
}
}
}
#[zbus::proxy(
interface = "org.stardustxr.XKBv1",
default_service = "org.stardustxr.XKBv1"
)]
trait KeyboardHandler {
async fn keymap(&self, keymap_id: u64) -> zbus::Result<()>;
async fn key_state(&self, key: u32, pressed: bool) -> zbus::Result<()>;
async fn reset(&self) -> zbus::Result<()>;
}
// Make KeyboardHandlerProxy queryable
stardust_xr::schemas::impl_queryable_for_proxy!(KeyboardHandlerProxy);
// Query context for keyboard handlers
#[derive(Debug, Clone)]
struct KeyboardQueryContext;
impl QueryContext for KeyboardQueryContext {}
#[derive(Resource)]
pub struct MousePointer {
node: Arc<Node>,
node: OwnedNode,
keymap: DefaultKey,
spatial: Arc<Spatial>,
pointer: Arc<InputMethod>,
capture_manager: CaptureManager,
mouse_datamap: MouseEvent,
keyboard_datamap: KeyboardEvent,
keyboard_sender: Arc<PulseSender>,
// Task management
focus_task_abort_handle: AbortHandle,
input_delivery_task_abort_handle: AbortHandle,
// Channels
input_event_tx: mpsc::UnboundedSender<InputEvent>,
// Notification for focus recalculation
focus_notify: Arc<Notify>,
}
impl MousePointer {
pub fn new() -> Result<Self> {
let node = Node::create_parent_name(&INTERNAL_CLIENT, "", &nanoid!(), false)
.add_to_scenegraph()?;
let spatial = Spatial::add_to(&node, None, Mat4::IDENTITY, false);
let pointer =
InputMethod::add_to(&node, InputType::Pointer(Pointer::default()), None).unwrap();
pub fn new(object_registry: Arc<ObjectRegistry>) -> Result<Self> {
let node = Node::generate(&INTERNAL_CLIENT, false).add_to_scenegraph_owned()?;
let spatial = Spatial::add_to(&node.0, None, Mat4::IDENTITY, false);
let pointer = InputMethod::add_to(
&node.0,
InputDataType::Pointer(Pointer::default()),
Datamap::from_typed(MouseEvent::default())?,
)?;
KEYMAPS.lock().insert(
"flatscreen".to_string(),
Keymap::new_from_names(&Context::new(0), "evdev", "", "", "", None, 0)
let context = Context::new(0).unwrap();
let keymap = KEYMAPS.lock().insert(
Keymap::new_from_names(context, None, CompileFlags::NO_FLAGS)
.unwrap()
.get_as_string(FORMAT_TEXT_V1),
.get_as_string(KeymapFormat::TextV1)
.unwrap(),
);
let keyboard_sender = PulseSender::add_to(
&node,
Datamap::from_typed(KeyboardEvent::default()).unwrap(),
)
.unwrap();
// Create channels and notification
let (focused_handler_tx, focused_handler_rx) = watch::channel::<Option<HandlerInfo>>(None);
let (input_event_tx, input_event_rx) = mpsc::unbounded_channel::<InputEvent>();
let focus_notify = Arc::new(Notify::new());
// Spawn input delivery task
info!("Creating input delivery task");
let input_delivery_task_abort_handle = task::new(
|| "Mouse pointer input delivery task",
Self::input_delivery_task(
object_registry.get_connection().clone(),
focused_handler_rx,
input_event_rx,
keymap.data().as_ffi(),
),
)?
.abort_handle();
info!("Input delivery task created successfully");
// Spawn focus tracking task
info!("Creating focus tracking task");
let focus_task_abort_handle = task::new(
|| "Mouse pointer focus task",
Self::focus_tracking_task(
object_registry,
focus_notify.clone(),
spatial.clone(),
pointer.clone(),
focused_handler_tx,
),
)?
.abort_handle();
info!("Focus tracking task created successfully");
Ok(MousePointer {
node,
spatial,
pointer,
capture_manager: CaptureManager::default(),
mouse_datamap: Default::default(),
keyboard_datamap: Default::default(),
keyboard_sender,
keymap,
focus_task_abort_handle,
input_delivery_task_abort_handle,
input_event_tx,
focus_notify,
})
}
pub fn update(&mut self, sk: &impl StereoKitMultiThread) {
let mouse = sk.input_mouse();
pub fn update(
&mut self,
dbus_connection: &Connection,
object_registry: &ObjectRegistry,
ray: Ray3d,
mouse_buttons: &ButtonInput<MouseButton>,
keyboard_buttons: &ButtonInput<KeyCode>,
mut scroll: EventReader<MouseWheel>,
mut keyboard_input_events: EventReader<KeyboardInput>,
) {
let mut discrete = Vec2::ZERO;
let mut continuous = Vec2::ZERO;
for e in scroll.read() {
match e.unit {
bevy::input::mouse::MouseScrollUnit::Line => {
discrete.x += e.x;
discrete.y += e.y;
}
bevy::input::mouse::MouseScrollUnit::Pixel => {
continuous.x += e.x;
continuous.y += e.y;
}
}
}
let ray = ray_from_mouse(mouse.pos).unwrap();
self.spatial.set_local_transform(
Mat4::look_to_rh(
Vec3::from(ray.pos),
Vec3::from(ray.dir),
vec3(0.0, 1.0, 0.0),
)
.inverse(),
Mat4::look_to_rh(ray.origin, Vec3::from(ray.direction), Vec3::Y).inverse(),
);
{
// Set pointer input datamap
self.mouse_datamap.select =
if sk.input_key(Key::MouseLeft).contains(ButtonState::ACTIVE) {
1.0f32
} else {
0.0f32
};
self.mouse_datamap.middle =
if sk.input_key(Key::MouseCenter).contains(ButtonState::ACTIVE) {
1.0f32
} else {
0.0f32
};
self.mouse_datamap.context =
if sk.input_key(Key::MouseRight).contains(ButtonState::ACTIVE) {
1.0f32
} else {
0.0f32
};
self.mouse_datamap.grab = if sk.input_key(Key::MouseBack).contains(ButtonState::ACTIVE)
|| sk
.input_key(Key::MouseForward)
.contains(ButtonState::ACTIVE)
{
1.0f32
} else {
0.0f32
self.mouse_datamap = MouseEvent {
select: mouse_buttons.pressed(MouseButton::Left) as u32 as f32,
middle: mouse_buttons.pressed(MouseButton::Middle) as u32 as f32,
context: mouse_buttons.pressed(MouseButton::Right) as u32 as f32,
grab: mouse_buttons.pressed(MouseButton::Right) as u32 as f32, // Was Mouse 5
scroll_continuous: continuous.into(),
scroll_discrete: discrete.into(),
raw_input_events: mouse_buttons
.get_pressed()
.map(|button| match button {
MouseButton::Left => input_event_codes::BTN_LEFT!(),
MouseButton::Right => input_event_codes::BTN_RIGHT!(),
MouseButton::Middle => input_event_codes::BTN_MIDDLE!(),
MouseButton::Back => input_event_codes::BTN_BACK!(),
MouseButton::Forward => input_event_codes::BTN_FORWARD!(),
MouseButton::Other(b) => *b as u32,
})
.collect(),
};
self.mouse_datamap.scroll_continuous = vec2(0.0, mouse.scroll_change / 120.0);
self.mouse_datamap.scroll_discrete = vec2(0.0, mouse.scroll_change / 120.0);
*self.pointer.datamap.lock() = Datamap::from_typed(&self.mouse_datamap).ok();
*self.pointer.datamap.lock() = Datamap::from_typed(&self.mouse_datamap).unwrap();
}
self.send_keyboard_input(sk);
self.target_pointer_input();
// Send keyboard input events via channel
for event in keyboard_input_events.read() {
if let Some(key) = map_key(event.key_code) {
let input_event = InputEvent {
key,
pressed: matches!(event.state, ButtonState::Pressed),
};
info!(
"Sending keyboard input event: key={}, pressed={}",
key, input_event.pressed
);
if let Err(e) = self.input_event_tx.send(input_event) {
error!("Failed to send keyboard input event: {}", e);
}
} else {
warn!("Unable to map key code: {:?}", event.key_code);
}
}
// Notify focus tracking task to recalculate focus
self.focus_notify.notify_waiters();
}
fn target_pointer_input(&mut self) {
let distance_calculator: DistanceCalculator = |space, data, field| {
let result = field.ray_march(Ray {
origin: vec3(0.0, 0.0, 0.0),
direction: vec3(0.0, 0.0, -1.0),
space: space.clone(),
});
let valid =
result.deepest_point_distance > 0.0 && result.min_distance.is_sign_negative();
valid.then_some(result.deepest_point_distance)
};
self.capture_manager.update_capture(&self.pointer);
self.capture_manager
.set_new_capture(&self.pointer, distance_calculator);
self.capture_manager.apply_capture(&self.pointer);
if self.capture_manager.capture.upgrade().is_some() {
return;
}
let mut handlers = get_sorted_handlers(&self.pointer, distance_calculator);
let first_distance = handlers
.first()
.map(|(_, distance)| *distance)
.unwrap_or(f32::NEG_INFINITY);
self.pointer.set_handler_order(
handlers
.iter()
.filter(|(handler, distance)| (distance - first_distance).abs() <= 0.001)
.map(|(handler, _)| handler),
);
}
fn send_keyboard_input(&mut self, sk: &impl StereoKitMultiThread) {
let rx = PULSE_RECEIVER_REGISTRY
.get_valid_contents()
.into_iter()
.filter(|rx| mask_matches(&rx.mask, &self.keyboard_sender.mask))
.map(|rx| {
let result = rx.field_node.get_aspect::<Field>().unwrap().ray_march(Ray {
async fn focus_tracking_task(
object_registry: Arc<ObjectRegistry>,
focus_notify: Arc<Notify>,
spatial: Arc<Spatial>,
pointer: Arc<InputMethod>,
focused_handler_tx: watch::Sender<Option<HandlerInfo>>,
) {
info!("Focus tracking task started");
// Create keyboard handler query inside the task
let mut keyboard_query = ObjectQuery::<
(FieldRefProxy<'static>, KeyboardHandlerProxy<'static>),
_,
>::new(object_registry.clone(), ());
let (keyboard_handlers, mapper) = keyboard_query.to_list_query();
task::new(
|| "Focus tracking mapper",
mapper.init(async |ev| match ev {
ListEvent::NewMatch((field_ref, keyboard_proxy)) => {
info!("New keyboard handler found");
let uid = timeout(Duration::from_millis(100), field_ref.uid())
.await
.ok()?
.ok()?;
let field_node = EXPORTED_FIELDS.lock().get(&uid)?.upgrade()?;
let field = field_node.get_aspect::<Field>();
Some((field, keyboard_proxy))
}
ListEvent::Modified((field_ref, keyboard_proxy)) => {
let uid = timeout(Duration::from_millis(100), field_ref.uid())
.await
.ok()?
.ok()?;
let field_node = EXPORTED_FIELDS.lock().get(&uid)?.upgrade()?;
let field = field_node.get_aspect::<Field>();
Some((field, keyboard_proxy))
}
_ => None,
}),
);
// Main focus calculation loop
loop {
let mut closest_handler = None;
let mut closest_distance = f32::MAX;
// Find closest handler
for (handler, (field_ref, keyboard_proxy)) in &*keyboard_handlers.iter().await {
let Ok(field_ref) = field_ref else {
continue;
};
let result = field_ref.ray_march(Ray {
origin: vec3(0.0, 0.0, 0.0),
direction: vec3(0.0, 0.0, -1.0),
space: self.spatial.clone(),
space: spatial.clone(),
});
(rx, result)
})
.filter(|(_rx, result)| {
result.deepest_point_distance > 0.0 && result.min_distance < 0.05
})
.reduce(|(rx_a, result_a), (rx_b, result_b)| {
if result_a.deepest_point_distance < result_b.deepest_point_distance {
(rx_a, result_a)
} else {
(rx_b, result_b)
if result.deepest_point_distance > 0.0
&& result.min_distance < 0.05
&& result.deepest_point_distance < closest_distance
{
closest_distance = result.deepest_point_distance;
closest_handler = Some(HandlerInfo {
handler: handler.clone(),
field_ref: field_ref.clone(),
keyboard_proxy: keyboard_proxy.clone(),
});
}
})
.map(|(rx, _)| rx);
}
if let Some(rx) = rx {
let keys = (8_u32..254)
.filter_map(|i| Key::try_from(i).ok())
.filter_map(|k| Some((map_key(k)?, sk.input_key(k))))
.filter_map(|(i, k)| {
if k.contains(ButtonState::JUST_ACTIVE) {
Some(i as i32)
} else if k.contains(ButtonState::JUST_INACTIVE) {
Some(-(i as i32))
} else {
None
}
})
.collect();
// Update focused handler
if let Some(ref handler_info) = closest_handler {
info!(
"Focus tracking task: Focused on handler at distance {}",
closest_distance
);
} else {
debug!("Focus tracking task: No handler in focus");
}
let _ = focused_handler_tx.send(closest_handler);
self.keyboard_datamap.keys = keys;
if !self.keyboard_datamap.keys.is_empty() {
pulse_receiver_client::data(
&rx.node.upgrade().unwrap(),
&self.node.uid,
&Datamap::from_typed(&self.keyboard_datamap).unwrap(),
)
.unwrap();
// Wait for next frame signal
focus_notify.notified().await;
}
}
async fn input_delivery_task(
dbus_connection: Connection,
mut focused_handler_rx: watch::Receiver<Option<HandlerInfo>>,
mut input_event_rx: mpsc::UnboundedReceiver<InputEvent>,
keymap_id: u64,
) {
info!("Input delivery task started");
loop {
// Handle input events
while let Some(input_event) = input_event_rx.recv().await {
info!(
"Input delivery task: Received input event key={}, pressed={}",
input_event.key, input_event.pressed
);
// Get current focused handler
let current_handler = focused_handler_rx.borrow().clone();
let Some(handler_info) = current_handler else {
continue;
};
// Send input to handler using cached proxy
info!("Input delivery task: Sending to handler");
let keyboard_handler = &handler_info.keyboard_proxy;
// Register keymap first
if let Err(e) = keyboard_handler.keymap(keymap_id).await {
warn!("Input delivery task: Failed to register keymap: {}", e);
}
// Send key state
if let Err(e) = keyboard_handler
.key_state(input_event.key + 8, input_event.pressed)
.await
{
error!("Input delivery task: Failed to send key state: {}", e);
} else {
info!(
"Input delivery task: Successfully sent key {} (pressed={})",
input_event.key + 8,
input_event.pressed
);
}
}
}
}
}
fn map_key(key: Key) -> Option<u32> {
impl Drop for MousePointer {
fn drop(&mut self) {
// Abort the persistent tasks when MousePointer is dropped
self.focus_task_abort_handle.abort();
self.input_delivery_task_abort_handle.abort();
}
}
fn map_key(key: KeyCode) -> Option<u32> {
use KeyCode as Key;
match key {
Key::Unidentified(NativeKeyCode::Xkb(code)) => Some(code),
Key::Backspace => Some(input_event_codes::KEY_BACKSPACE!()),
Key::Tab => Some(input_event_codes::KEY_TAB!()),
Key::Return => Some(input_event_codes::KEY_ENTER!()),
Key::Shift => Some(input_event_codes::KEY_LEFTSHIFT!()),
Key::Ctrl => Some(input_event_codes::KEY_LEFTCTRL!()),
Key::Alt => Some(input_event_codes::KEY_LEFTALT!()),
Key::Enter => Some(input_event_codes::KEY_ENTER!()),
Key::ShiftLeft => Some(input_event_codes::KEY_LEFTSHIFT!()),
Key::ShiftRight => Some(input_event_codes::KEY_RIGHTSHIFT!()),
Key::ControlLeft => Some(input_event_codes::KEY_LEFTCTRL!()),
Key::ControlRight => Some(input_event_codes::KEY_RIGHTCTRL!()),
Key::AltLeft => Some(input_event_codes::KEY_LEFTALT!()),
Key::AltRight => Some(input_event_codes::KEY_RIGHTALT!()),
Key::CapsLock => Some(input_event_codes::KEY_CAPSLOCK!()),
Key::Esc => Some(input_event_codes::KEY_ESC!()),
Key::Escape => Some(input_event_codes::KEY_ESC!()),
Key::Space => Some(input_event_codes::KEY_SPACE!()),
Key::End => Some(input_event_codes::KEY_END!()),
Key::Home => Some(input_event_codes::KEY_HOME!()),
Key::Left => Some(input_event_codes::KEY_LEFT!()),
Key::Right => Some(input_event_codes::KEY_RIGHT!()),
Key::Up => Some(input_event_codes::KEY_UP!()),
Key::Down => Some(input_event_codes::KEY_DOWN!()),
Key::ArrowLeft => Some(input_event_codes::KEY_LEFT!()),
Key::ArrowRight => Some(input_event_codes::KEY_RIGHT!()),
Key::ArrowUp => Some(input_event_codes::KEY_UP!()),
Key::ArrowDown => Some(input_event_codes::KEY_DOWN!()),
Key::PageUp => Some(input_event_codes::KEY_PAGEUP!()),
Key::PageDown => Some(input_event_codes::KEY_PAGEDOWN!()),
Key::PrintScreen => Some(input_event_codes::KEY_PRINT!()),
Key::KeyInsert => Some(input_event_codes::KEY_INSERT!()),
Key::Del => Some(input_event_codes::KEY_DELETE!()),
Key::Key0 => Some(input_event_codes::KEY_0!()),
Key::Key1 => Some(input_event_codes::KEY_1!()),
Key::Key2 => Some(input_event_codes::KEY_2!()),
Key::Key3 => Some(input_event_codes::KEY_3!()),
Key::Key4 => Some(input_event_codes::KEY_4!()),
Key::Key5 => Some(input_event_codes::KEY_5!()),
Key::Key6 => Some(input_event_codes::KEY_6!()),
Key::Key7 => Some(input_event_codes::KEY_7!()),
Key::Key8 => Some(input_event_codes::KEY_8!()),
Key::Key9 => Some(input_event_codes::KEY_9!()),
Key::A => Some(input_event_codes::KEY_A!()),
Key::B => Some(input_event_codes::KEY_B!()),
Key::C => Some(input_event_codes::KEY_C!()),
Key::D => Some(input_event_codes::KEY_D!()),
Key::E => Some(input_event_codes::KEY_E!()),
Key::F => Some(input_event_codes::KEY_F!()),
Key::G => Some(input_event_codes::KEY_G!()),
Key::H => Some(input_event_codes::KEY_H!()),
Key::I => Some(input_event_codes::KEY_I!()),
Key::J => Some(input_event_codes::KEY_J!()),
Key::K => Some(input_event_codes::KEY_K!()),
Key::L => Some(input_event_codes::KEY_L!()),
Key::M => Some(input_event_codes::KEY_M!()),
Key::N => Some(input_event_codes::KEY_N!()),
Key::O => Some(input_event_codes::KEY_O!()),
Key::P => Some(input_event_codes::KEY_P!()),
Key::Q => Some(input_event_codes::KEY_Q!()),
Key::R => Some(input_event_codes::KEY_R!()),
Key::S => Some(input_event_codes::KEY_S!()),
Key::T => Some(input_event_codes::KEY_T!()),
Key::U => Some(input_event_codes::KEY_U!()),
Key::V => Some(input_event_codes::KEY_V!()),
Key::W => Some(input_event_codes::KEY_W!()),
Key::X => Some(input_event_codes::KEY_X!()),
Key::Y => Some(input_event_codes::KEY_Y!()),
Key::Z => Some(input_event_codes::KEY_Z!()),
Key::Insert => Some(input_event_codes::KEY_INSERT!()),
Key::Delete => Some(input_event_codes::KEY_DELETE!()),
Key::Digit0 => Some(input_event_codes::KEY_0!()),
Key::Digit1 => Some(input_event_codes::KEY_1!()),
Key::Digit2 => Some(input_event_codes::KEY_2!()),
Key::Digit3 => Some(input_event_codes::KEY_3!()),
Key::Digit4 => Some(input_event_codes::KEY_4!()),
Key::Digit5 => Some(input_event_codes::KEY_5!()),
Key::Digit6 => Some(input_event_codes::KEY_6!()),
Key::Digit7 => Some(input_event_codes::KEY_7!()),
Key::Digit8 => Some(input_event_codes::KEY_8!()),
Key::Digit9 => Some(input_event_codes::KEY_9!()),
Key::KeyA => Some(input_event_codes::KEY_A!()),
Key::KeyB => Some(input_event_codes::KEY_B!()),
Key::KeyC => Some(input_event_codes::KEY_C!()),
Key::KeyD => Some(input_event_codes::KEY_D!()),
Key::KeyE => Some(input_event_codes::KEY_E!()),
Key::KeyF => Some(input_event_codes::KEY_F!()),
Key::KeyG => Some(input_event_codes::KEY_G!()),
Key::KeyH => Some(input_event_codes::KEY_H!()),
Key::KeyI => Some(input_event_codes::KEY_I!()),
Key::KeyJ => Some(input_event_codes::KEY_J!()),
Key::KeyK => Some(input_event_codes::KEY_K!()),
Key::KeyL => Some(input_event_codes::KEY_L!()),
Key::KeyM => Some(input_event_codes::KEY_M!()),
Key::KeyN => Some(input_event_codes::KEY_N!()),
Key::KeyO => Some(input_event_codes::KEY_O!()),
Key::KeyP => Some(input_event_codes::KEY_P!()),
Key::KeyQ => Some(input_event_codes::KEY_Q!()),
Key::KeyR => Some(input_event_codes::KEY_R!()),
Key::KeyS => Some(input_event_codes::KEY_S!()),
Key::KeyT => Some(input_event_codes::KEY_T!()),
Key::KeyU => Some(input_event_codes::KEY_U!()),
Key::KeyV => Some(input_event_codes::KEY_V!()),
Key::KeyW => Some(input_event_codes::KEY_W!()),
Key::KeyX => Some(input_event_codes::KEY_X!()),
Key::KeyY => Some(input_event_codes::KEY_Y!()),
Key::KeyZ => Some(input_event_codes::KEY_Z!()),
Key::Numpad0 => Some(input_event_codes::KEY_NUMERIC_0!()),
Key::Numpad1 => Some(input_event_codes::KEY_NUMERIC_1!()),
Key::Numpad2 => Some(input_event_codes::KEY_NUMERIC_2!()),
@@ -267,24 +606,64 @@ fn map_key(key: Key) -> Option<u32> {
Key::F10 => Some(input_event_codes::KEY_F10!()),
Key::F11 => Some(input_event_codes::KEY_F11!()),
Key::F12 => Some(input_event_codes::KEY_F12!()),
Key::F13 => Some(input_event_codes::KEY_F13!()),
Key::F14 => Some(input_event_codes::KEY_F14!()),
Key::F15 => Some(input_event_codes::KEY_F15!()),
Key::F16 => Some(input_event_codes::KEY_F16!()),
Key::F17 => Some(input_event_codes::KEY_F17!()),
Key::F18 => Some(input_event_codes::KEY_F18!()),
Key::F19 => Some(input_event_codes::KEY_F19!()),
Key::F20 => Some(input_event_codes::KEY_F20!()),
Key::F21 => Some(input_event_codes::KEY_F21!()),
Key::F22 => Some(input_event_codes::KEY_F22!()),
Key::F23 => Some(input_event_codes::KEY_F23!()),
Key::F24 => Some(input_event_codes::KEY_F24!()),
Key::Comma => Some(input_event_codes::KEY_COMMA!()),
Key::Period => Some(input_event_codes::KEY_DOT!()),
Key::SlashFwd => Some(input_event_codes::KEY_SLASH!()),
Key::SlashBack => Some(input_event_codes::KEY_BACKSLASH!()),
Key::Slash => Some(input_event_codes::KEY_SLASH!()),
Key::Backslash => Some(input_event_codes::KEY_BACKSLASH!()),
Key::Semicolon => Some(input_event_codes::KEY_SEMICOLON!()),
Key::Apostrophe => Some(input_event_codes::KEY_APOSTROPHE!()),
Key::BracketOpen => Some(input_event_codes::KEY_LEFTBRACE!()),
Key::BracketClose => Some(input_event_codes::KEY_RIGHTBRACE!()),
Key::Quote => Some(input_event_codes::KEY_APOSTROPHE!()),
Key::BracketLeft => Some(input_event_codes::KEY_LEFTBRACE!()),
Key::BracketRight => Some(input_event_codes::KEY_RIGHTBRACE!()),
Key::Minus => Some(input_event_codes::KEY_MINUS!()),
Key::Equals => Some(input_event_codes::KEY_EQUAL!()),
Key::Backtick => None,
Key::LCmd => Some(input_event_codes::KEY_LEFTMETA!()),
Key::RCmd => Some(input_event_codes::KEY_RIGHTMETA!()),
Key::Multiply => Some(input_event_codes::KEY_NUMERIC_STAR!()),
Key::Add => Some(input_event_codes::KEY_KPPLUS!()),
Key::Subtract => Some(input_event_codes::KEY_MINUS!()),
Key::Decimal => Some(input_event_codes::KEY_DOT!()),
Key::Divide => Some(input_event_codes::KEY_SLASH!()),
Key::Equal => Some(input_event_codes::KEY_EQUAL!()),
Key::Backquote => Some(input_event_codes::KEY_GRAVE!()),
Key::SuperLeft => Some(input_event_codes::KEY_LEFTMETA!()),
Key::SuperRight => Some(input_event_codes::KEY_RIGHTMETA!()),
Key::NumpadMultiply => Some(input_event_codes::KEY_NUMERIC_STAR!()),
Key::NumpadAdd => Some(input_event_codes::KEY_KPPLUS!()),
Key::NumpadSubtract => Some(input_event_codes::KEY_MINUS!()),
Key::NumpadDecimal => Some(input_event_codes::KEY_DOT!()),
Key::NumpadDivide => Some(input_event_codes::KEY_SLASH!()),
Key::ContextMenu => Some(input_event_codes::KEY_CONTEXT_MENU!()),
Key::Help => Some(input_event_codes::KEY_HELP!()),
Key::NumLock => Some(input_event_codes::KEY_NUMLOCK!()),
Key::NumpadBackspace => Some(input_event_codes::KEY_BACKSPACE!()),
Key::NumpadClear => Some(input_event_codes::KEY_CLEAR!()),
Key::NumpadClearEntry => Some(input_event_codes::KEY_CLEAR!()),
Key::NumpadComma => Some(input_event_codes::KEY_COMMA!()),
Key::NumpadEnter => Some(input_event_codes::KEY_ENTER!()),
Key::NumpadEqual => Some(input_event_codes::KEY_EQUAL!()),
Key::NumpadHash => Some(input_event_codes::KEY_NUMERIC_POUND!()),
Key::NumpadStar => Some(input_event_codes::KEY_KPASTERISK!()),
Key::Fn => Some(input_event_codes::KEY_FN!()),
Key::ScrollLock => Some(input_event_codes::KEY_SCROLLLOCK!()),
Key::Pause => Some(input_event_codes::KEY_PAUSE!()),
Key::Power => Some(input_event_codes::KEY_POWER!()),
Key::Sleep => Some(input_event_codes::KEY_SLEEP!()),
Key::Suspend => Some(input_event_codes::KEY_SUSPEND!()),
Key::Again => Some(input_event_codes::KEY_AGAIN!()),
Key::Copy => Some(input_event_codes::KEY_COPY!()),
Key::Cut => Some(input_event_codes::KEY_CUT!()),
Key::Find => Some(input_event_codes::KEY_FIND!()),
Key::Open => Some(input_event_codes::KEY_OPEN!()),
Key::Paste => Some(input_event_codes::KEY_PASTE!()),
Key::Props => Some(input_event_codes::KEY_PROPS!()),
Key::Select => Some(input_event_codes::KEY_SELECT!()),
Key::Undo => Some(input_event_codes::KEY_UNDO!()),
Key::Hiragana => Some(input_event_codes::KEY_HIRAGANA!()),
Key::Katakana => Some(input_event_codes::KEY_KATAKANA!()),
_ => None,
}
}

View File

@@ -0,0 +1,417 @@
use super::{CaptureManager, get_sorted_handlers};
use crate::{
DbusConnection, PreFrameWait,
core::client::INTERNAL_CLIENT,
get_time,
nodes::{
Node, OwnedNode,
drawable::{
MaterialParameter,
model::{Model, ModelPart},
},
fields::{Field, FieldTrait},
input::{INPUT_HANDLER_REGISTRY, InputDataType, InputHandler, InputMethod, Tip},
spatial::Spatial,
},
objects::{AsyncTracked, ObjectHandle, SpatialRef, Tracked},
};
use bevy::{asset::Handle, ecs::resource::Resource};
use bevy::{math::Affine3, prelude::*};
use bevy_mod_openxr::{
action_binding::{OxrSendActionBindings, OxrSuggestActionBinding},
helper_traits::{ToIsometry3d, ToVec2},
resources::{OxrFrameState, OxrInstance, Pipelined},
session::OxrSession,
};
use bevy_mod_xr::{
hands::HandSide,
session::{XrPreDestroySession, XrSessionCreated, XrSessionCreatedEvent},
spaces::{XrPrimaryReferenceSpace, XrReferenceSpace, XrSpace},
};
use color_eyre::eyre::Result;
use glam::{Affine3A, Mat4, Vec2, Vec3};
use openxr::{Action, ActiveActionSet, SpaceLocationFlags};
use serde::{Deserialize, Serialize};
use stardust_xr::values::{Datamap, ResourceID, color::Rgb};
use std::{
borrow::Cow,
fs,
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use tracing::instrument;
use zbus::Connection;
pub struct ControllerPlugin;
const CURSOR_MODEL_PATH: &str = "/tmp/stardust_server/models/cursor.glb";
impl Plugin for ControllerPlugin {
fn build(&self, app: &mut App) {
let cursor = include_bytes!("cursor.glb");
fs::create_dir_all(
PathBuf::from_str(CURSOR_MODEL_PATH)
.unwrap()
.parent()
.unwrap(),
);
fs::write(CURSOR_MODEL_PATH, cursor).expect("can't write tmp cursor model file");
app.add_systems(OxrSendActionBindings, suggest_bindings.run_if(run_once));
app.add_systems(
PostUpdate,
create_spaces.run_if(on_event::<XrSessionCreatedEvent>),
);
app.add_systems(XrPreDestroySession, destroy_spaces);
app.add_systems(Startup, setup.run_if(resource_exists::<OxrInstance>));
app.add_systems(PreFrameWait, update.run_if(resource_exists::<Controllers>));
}
}
// the api is just slightly nicer when using the bevy_mod_openxr solution okay?
fn suggest_bindings(
instance: Res<OxrInstance>,
actions: Res<Actions>,
mut suggest: EventWriter<OxrSuggestActionBinding>,
) {
let mut bind_all = |interaction_profile: &'static str,
bindings: &[(openxr::sys::Action, &[&'static str])]| {
for (action, bindings) in bindings {
suggest.write(OxrSuggestActionBinding {
action: *action,
interaction_profile: interaction_profile.into(),
bindings: bindings.iter().copied().map(Cow::Borrowed).collect(),
});
}
};
bind_all(
"/interaction_profiles/oculus/touch_controller",
&[
(
actions.trigger.as_raw(),
&[
"/user/hand/left/input/trigger/value",
"/user/hand/right/input/trigger/value",
],
),
(
actions.stick_click.as_raw(),
&[
"/user/hand/left/input/thumbstick/click",
"/user/hand/right/input/thumbstick/click",
],
),
(
actions.button.as_raw(),
&[
"/user/hand/left/input/x/click",
"/user/hand/left/input/y/click",
"/user/hand/right/input/a/click",
"/user/hand/right/input/b/click",
],
),
(
actions.grip.as_raw(),
&[
"/user/hand/left/input/squeeze/value",
"/user/hand/right/input/squeeze/value",
],
),
(
actions.stick.as_raw(),
&[
"/user/hand/left/input/thumbstick",
"/user/hand/right/input/thumbstick",
],
),
(
actions.space.as_raw(),
&[
"/user/hand/left/input/aim/pose",
"/user/hand/right/input/aim/pose",
],
),
],
);
bind_all(
"/interaction_profiles/khr/simple_controller",
&[(
actions.space.as_raw(),
&[
"/user/hand/left/input/aim/pose",
"/user/hand/right/input/aim/pose",
],
)],
);
}
fn update(
mut controllers: ResMut<Controllers>,
actions: Res<Actions>,
session: Option<Res<OxrSession>>,
ref_space: Option<Res<XrPrimaryReferenceSpace>>,
state: Option<Res<OxrFrameState>>,
pipelined: Option<Res<Pipelined>>,
) {
let (Some(session), Some(state), Some(ref_space)) = (session, state, ref_space) else {
controllers.left.set_enabled(false);
controllers.right.set_enabled(false);
return;
};
debug_span!("sync actions").in_scope(|| {
session
.sync_actions(&[ActiveActionSet::new(&actions.set)])
.unwrap();
});
let time = get_time(pipelined.is_some(), &state);
controllers
.left
.update(&session, &actions, time, ref_space.0);
controllers
.right
.update(&session, &actions, time, ref_space.0);
}
fn create_spaces(
session: Res<OxrSession>,
mut controllers: ResMut<Controllers>,
actions: Res<Actions>,
) {
// if we ever need more actions than just these we should fully swith to the
// bevy_mod_openxr provided stuff
session.attach_action_sets(&[&actions.set]);
session
.sync_actions(&[ActiveActionSet::new(&actions.set)])
.unwrap();
let instance = session.instance();
let left = instance.string_to_path("/user/hand/left").unwrap();
let right = instance.string_to_path("/user/hand/right").unwrap();
let left = session
.create_action_space(&actions.space, left, Isometry3d::IDENTITY)
.unwrap();
let right = session
.create_action_space(&actions.space, right, Isometry3d::IDENTITY)
.unwrap();
controllers.left.space = Some(left);
controllers.right.space = Some(right);
}
fn destroy_spaces(session: Res<OxrSession>, mut controllers: ResMut<Controllers>) {
if let Some(space) = controllers.left.space.take() {
session.destroy_space(space);
}
if let Some(space) = controllers.right.space.take() {
session.destroy_space(space);
}
}
fn setup(instance: Res<OxrInstance>, connection: Res<DbusConnection>, mut cmds: Commands) {
tokio::task::spawn({
let connection = connection.clone();
async move {
connection
.request_name("org.stardustxr.Controllers")
.await
.unwrap();
}
});
let set = instance
.create_action_set("input_method_actions", "Input Method Action Source", 0)
.unwrap();
let paths = &[
instance.string_to_path("/user/hand/left").unwrap(),
instance.string_to_path("/user/hand/right").unwrap(),
];
let actions = Actions {
trigger: set.create_action("trigger", "Select", paths).unwrap(),
stick_click: set.create_action("stick_click", "Middle", paths).unwrap(),
button: set.create_action("face_button", "Context", paths).unwrap(),
grip: set.create_action("grip", "Grab", paths).unwrap(),
stick: set.create_action("stick", "Scroll", paths).unwrap(),
space: set.create_action("pose", "Location", paths).unwrap(),
set,
};
let controllers = Controllers {
left: OxrControllerInput::new(&connection, HandSide::Left).unwrap(),
right: OxrControllerInput::new(&connection, HandSide::Right).unwrap(),
};
cmds.insert_resource(controllers);
cmds.insert_resource(actions);
}
#[derive(Default, Debug, Deserialize, Serialize)]
struct ControllerDatamap {
select: f32,
middle: f32,
context: f32,
grab: f32,
scroll: Vec2,
}
#[derive(Resource)]
struct Actions {
set: openxr::ActionSet,
trigger: openxr::Action<f32>,
stick_click: openxr::Action<f32>,
button: openxr::Action<f32>,
grip: openxr::Action<f32>,
space: openxr::Action<openxr::Posef>,
stick: openxr::Action<openxr::Vector2f>,
}
#[derive(Resource)]
struct Controllers {
left: OxrControllerInput,
right: OxrControllerInput,
}
pub struct OxrControllerInput {
object_handle: ObjectHandle<SpatialRef>,
input: Arc<InputMethod>,
side: HandSide,
model: Arc<Model>,
model_part: Arc<ModelPart>,
capture_manager: CaptureManager,
datamap: ControllerDatamap,
tracked: AsyncTracked,
space: Option<XrSpace>,
_model_node: OwnedNode,
}
impl OxrControllerInput {
fn new(connection: &Connection, side: HandSide) -> Result<Self> {
let path = "/org/stardustxr/Controller/".to_string()
+ match side {
HandSide::Left => "left",
HandSide::Right => "right",
};
let (spatial, object_handle) = SpatialRef::create(connection, &path);
let tracked = AsyncTracked::new(connection, &path);
let tip = InputDataType::Tip(Tip::default());
let node = spatial.node().unwrap();
node.set_enabled(false);
let model_node = Arc::new(Node::generate(&INTERNAL_CLIENT, true));
let model_spatial = Spatial::add_to(
&model_node,
Some(spatial.clone()),
Mat4::from_scale(Vec3::splat(0.02)),
false,
);
let model =
Model::add_to(&model_node, ResourceID::Direct(CURSOR_MODEL_PATH.into())).unwrap();
let model_part = model.get_model_part("Cursor".to_string()).unwrap();
let input = InputMethod::add_to(
&node,
tip,
Datamap::from_typed(ControllerDatamap::default())?,
)?;
Ok(OxrControllerInput {
object_handle,
input,
side,
model,
model_part,
capture_manager: CaptureManager::default(),
datamap: Default::default(),
tracked,
space: None,
_model_node: OwnedNode(model_node),
})
}
#[instrument(level = "debug", skip(self))]
pub fn set_enabled(&self, enabled: bool) {
if let Some(node) = self.input.spatial.node() {
node.set_enabled(enabled);
}
self.tracked.set_tracked(enabled);
}
fn update(
&mut self,
session: &OxrSession,
actions: &Actions,
time: openxr::Time,
ref_space: XrReferenceSpace,
) {
let Some(space) = self.space.as_ref() else {
return;
};
let _span = debug_span!("locate space").entered();
let Ok(location) = session
.locate_space(space, &ref_space, time)
.inspect_err(|err| error!("error while locating controller space: {err}"))
else {
return;
};
let enabled = location.location_flags.contains(
SpaceLocationFlags::POSITION_VALID
| SpaceLocationFlags::POSITION_TRACKED
| SpaceLocationFlags::ORIENTATION_VALID
| SpaceLocationFlags::ORIENTATION_TRACKED,
);
drop(_span);
self.set_enabled(enabled);
if enabled {
let world_transform = Mat4::from(Affine3A::from(location.pose.to_xr_pose()));
self.model_part
.set_material_parameter("roughness".to_string(), MaterialParameter::Float(1.0));
self.model_part.set_material_parameter(
"color".to_string(),
MaterialParameter::Color(stardust_xr::values::Color::new(
if self.capture_manager.capture.upgrade().is_none() {
Rgb::new(1.0, 1.0, 1.0)
} else {
Rgb::new(0.0, 1.0, 0.75)
},
1.0,
)),
);
self.input.spatial.set_local_transform(world_transform);
}
let path = session
.instance()
.string_to_path(match self.side {
HandSide::Left => "/user/hand/left",
HandSide::Right => "/user/hand/right",
})
.unwrap();
if let Ok(path) = session.current_interaction_profile(path)
&& let Ok(path) = session.instance().path_to_string(path)
&& path == "/interaction_profiles/khr/simple_controller"
{
self.set_enabled(false);
}
fn get<T: openxr::ActionInput + Default>(
session: &OxrSession,
path: openxr::Path,
action: &Action<T>,
) -> T {
action
.state(session, path)
.map(|v| v.current_state)
.unwrap_or_default()
}
let _span = debug_span!("apply datamap").entered();
self.datamap = ControllerDatamap {
select: get(session, path, &actions.trigger),
middle: get(session, path, &actions.stick_click) as u32 as f32,
context: get(session, path, &actions.button) as u32 as f32,
grab: get(session, path, &actions.grip),
scroll: get(session, path, &actions.stick).to_vec2(),
};
*self.input.datamap.lock() = Datamap::from_typed(&self.datamap).unwrap();
drop(_span);
let distance_calculator = |space: &Arc<Spatial>, _data: &InputDataType, field: &Field| {
Some(field.distance(space, [0.0; 3].into()).abs())
};
self.capture_manager.update_capture(&self.input);
self.capture_manager
.set_new_capture(&self.input, distance_calculator);
self.capture_manager.apply_capture(&self.input);
if self.capture_manager.capture.upgrade().is_some() {
return;
}
let sorted_handlers = get_sorted_handlers(&self.input, distance_calculator);
self.input
.set_handler_order(sorted_handlers.iter().map(|(handler, _)| handler));
}
}

View File

@@ -0,0 +1,360 @@
use crate::core::client::INTERNAL_CLIENT;
use crate::nodes::OwnedNode;
use crate::nodes::fields::{Field, FieldTrait};
use crate::nodes::input::{Finger, INPUT_HANDLER_REGISTRY, InputDataType, InputHandler, Thumb};
use crate::nodes::{
Node,
input::{Hand, InputMethod, Joint},
spatial::Spatial,
};
use crate::objects::{AsyncTracked, ObjectHandle, SpatialRef, Tracked};
use crate::{BevyMaterial, DbusConnection, ObjectRegistryRes, PreFrameWait, get_time};
use bevy::prelude::Transform as BevyTransform;
use bevy::prelude::*;
use bevy_mod_openxr::helper_traits::{ToQuat, ToVec3};
use bevy_mod_openxr::resources::{OxrFrameState, Pipelined};
use bevy_mod_openxr::session::OxrSession;
use bevy_mod_xr::hands::{HandBone, HandSide, XrHandBoneEntities, XrHandBoneRadius};
use bevy_mod_xr::session::{XrPreDestroySession, XrSessionCreated, session_available};
use bevy_mod_xr::spaces::{XrPrimaryReferenceSpace, XrSpaceLocationFlags};
use bevy_sk::hand::GRADIENT_TEXTURE_HANDLE;
use color_eyre::eyre::Result;
use glam::{Mat4, Quat, Vec3};
use openxr::{HandJointLocation, SpaceLocationFlags};
use serde::{Deserialize, Serialize};
use stardust_xr::values::Datamap;
use std::sync::Arc;
use zbus::Connection;
use super::{CaptureManager, get_sorted_handlers};
pub struct HandPlugin;
impl Plugin for HandPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PreFrameWait, update_hands.run_if(resource_exists::<Hands>));
app.add_systems(XrSessionCreated, create_trackers);
app.add_systems(XrPreDestroySession, destroy_trackers);
app.add_systems(
PostUpdate,
update_hand_material.run_if(resource_exists::<Hands>),
);
app.add_systems(Startup, setup.run_if(session_available));
}
}
fn update_hands(
mut hands: ResMut<Hands>,
session: Option<Res<OxrSession>>,
state: Option<Res<OxrFrameState>>,
ref_space: Option<Res<XrPrimaryReferenceSpace>>,
mut materials: ResMut<Assets<BevyMaterial>>,
mut joint_query: Query<(
&mut BevyTransform,
&mut XrSpaceLocationFlags,
&mut XrHandBoneRadius,
)>,
joints_query: Query<&XrHandBoneEntities>,
pipelined: Option<Res<Pipelined>>,
) {
let (Some(session), Some(state), Some(ref_space)) = (session, state, ref_space) else {
hands.left.tracked.set_tracked(false);
hands.right.tracked.set_tracked(false);
return;
};
let get_joints = |hand: &mut OxrHandInput| -> Option<openxr::HandJointLocations> {
let Some(tracker) = hand.tracker.as_ref() else {
hand.input.spatial.node().unwrap().set_enabled(false);
hand.tracked.set_tracked(false);
return None;
};
let time = get_time(pipelined.is_some(), &state);
session
.locate_hand_joints(tracker, &ref_space, time)
.inspect_err(|err| error!("Error while locating hand joints"))
.ok()
.flatten()
};
let joints_left = get_joints(&mut hands.left);
let joints_right = get_joints(&mut hands.right);
hands.left.update(joints_left.as_ref(), &mut materials);
hands.right.update(joints_right.as_ref(), &mut materials);
}
fn pinch_between(joint_1: &Joint, joint_2: &Joint) -> f32 {
const PINCH_MAX: f32 = 0.11;
const PINCH_ACTIVACTION_DISTANCE: f32 = 0.01;
let combined_radius = joint_1.radius + joint_2.radius;
let pinch_dist =
Vec3::from(joint_1.position).distance(Vec3::from(joint_2.position)) - combined_radius;
(1.0 - ((pinch_dist - PINCH_ACTIVACTION_DISTANCE) / (PINCH_MAX - PINCH_ACTIVACTION_DISTANCE)))
.clamp(0.0, 1.0)
}
fn create_trackers(session: Res<OxrSession>, mut hands: ResMut<Hands>) {
hands.left.tracker = session
.create_hand_tracker(openxr::HandEXT::LEFT)
.inspect_err(|err| error!("failed to create left hand tracker"))
.ok();
hands.right.tracker = session
.create_hand_tracker(openxr::HandEXT::RIGHT)
.inspect_err(|err| error!("failed to create right hand tracker"))
.ok();
}
fn destroy_trackers(mut hands: ResMut<Hands>) {
hands.left.tracker.take();
hands.right.tracker.take();
}
#[derive(Component)]
struct CorrectHandMaterial;
fn update_hand_material(
query: Query<
(Entity, &HandSide),
(
With<XrHandBoneEntities>,
With<MeshMaterial3d<BevyMaterial>>,
Without<CorrectHandMaterial>,
),
>,
mut cmds: Commands,
hands: Res<Hands>,
) {
for (entity, side) in &query {
let handle = match side {
HandSide::Left => hands.left.material.clone(),
HandSide::Right => hands.right.material.clone(),
};
cmds.entity(entity)
.insert(MeshMaterial3d(handle))
.insert(CorrectHandMaterial);
}
}
fn setup(
connection: Res<DbusConnection>,
mut cmds: Commands,
mut materials: ResMut<Assets<BevyMaterial>>,
) {
tokio::task::spawn({
let connection = connection.clone();
async move {
connection
.request_name("org.stardustxr.Hands")
.await
.unwrap();
}
});
cmds.insert_resource(Hands {
left: OxrHandInput::new(&connection, HandSide::Left, &mut materials).unwrap(),
right: OxrHandInput::new(&connection, HandSide::Right, &mut materials).unwrap(),
});
}
fn convert_joint(joint: HandJointLocation) -> Joint {
Joint {
position: joint.pose.position.to_vec3().into(),
rotation: joint.pose.orientation.to_quat().into(),
radius: joint.radius,
distance: 0.0,
}
}
#[derive(Resource)]
struct Hands {
left: OxrHandInput,
right: OxrHandInput,
}
#[derive(Default, Deserialize, Serialize)]
struct HandDatamap {
pinch_strength: f32,
grab_strength: f32,
}
pub struct OxrHandInput {
_node: OwnedNode,
palm_spatial: Arc<Spatial>,
palm_object: ObjectHandle<SpatialRef>,
side: HandSide,
input: Arc<InputMethod>,
capture_manager: CaptureManager,
datamap: HandDatamap,
tracked: AsyncTracked,
tracker: Option<openxr::HandTracker>,
captured: bool,
material: Handle<BevyMaterial>,
}
impl OxrHandInput {
pub fn new(
connection: &Connection,
side: HandSide,
materials: &mut Assets<BevyMaterial>,
) -> Result<Self> {
let (palm_spatial, palm_object) = SpatialRef::create(
connection,
&("/org/stardustxr/Hand/".to_string()
+ match side {
HandSide::Left => "left",
HandSide::Right => "right",
} + "/palm"),
);
let tracked = AsyncTracked::new(
connection,
&("/org/stardustxr/Hand/".to_string()
+ match side {
HandSide::Left => "left",
HandSide::Right => "right",
}),
);
let node = Node::generate(&INTERNAL_CLIENT, false).add_to_scenegraph_owned()?;
Spatial::add_to(&node.0, None, Mat4::IDENTITY, false);
let hand = InputDataType::Hand(Hand {
right: matches!(side, HandSide::Right),
..Default::default()
});
let datamap = Datamap::from_typed(HandDatamap::default())?;
let input = InputMethod::add_to(&node.0, hand, datamap)?;
let material = materials.add(BevyMaterial {
base_color: Srgba::new(1.0, 1.0, 1.0, 1.0).into(),
alpha_mode: AlphaMode::Blend,
base_color_texture: Some(GRADIENT_TEXTURE_HANDLE),
perceptual_roughness: 1.0,
..default()
});
Ok(OxrHandInput {
_node: node,
palm_spatial,
palm_object,
side,
input,
tracked,
capture_manager: CaptureManager::default(),
datamap: Default::default(),
tracker: None,
material,
captured: false,
})
}
pub fn set_enabled(&self, enabled: bool) {
if let Some(node) = self.input.spatial.node() {
node.set_enabled(enabled);
}
self.tracked.set_tracked(enabled);
}
fn update(
&mut self,
joints: Option<&openxr::HandJointLocations>,
materials: &mut ResMut<Assets<BevyMaterial>>,
) {
// TODO: use the hand data source ext
let real_hand = true;
let input_node = self.input.spatial.node().unwrap();
let is_tracked = real_hand
&& joints.is_some_and(|v| {
v.iter().all(|v| {
v.location_flags.contains(
SpaceLocationFlags::POSITION_VALID | SpaceLocationFlags::POSITION_TRACKED,
) || v.location_flags.contains(
SpaceLocationFlags::ORIENTATION_VALID
| SpaceLocationFlags::ORIENTATION_TRACKED,
)
})
});
self.set_enabled(is_tracked);
if is_tracked {
// cannot ever crash, is_tracked is only true of joints is some
let joints = joints.unwrap();
let new_hand = Hand {
right: matches!(self.side, HandSide::Right),
thumb: Thumb {
tip: convert_joint(joints[HandBone::ThumbTip as usize]),
distal: convert_joint(joints[HandBone::ThumbDistal as usize]),
proximal: convert_joint(joints[HandBone::ThumbProximal as usize]),
metacarpal: convert_joint(joints[HandBone::ThumbMetacarpal as usize]),
},
index: Finger {
tip: convert_joint(joints[HandBone::IndexTip as usize]),
distal: convert_joint(joints[HandBone::IndexDistal as usize]),
intermediate: convert_joint(joints[HandBone::IndexIntermediate as usize]),
proximal: convert_joint(joints[HandBone::IndexProximal as usize]),
metacarpal: convert_joint(joints[HandBone::IndexMetacarpal as usize]),
},
middle: Finger {
tip: convert_joint(joints[HandBone::MiddleTip as usize]),
distal: convert_joint(joints[HandBone::MiddleDistal as usize]),
intermediate: convert_joint(joints[HandBone::MiddleIntermediate as usize]),
proximal: convert_joint(joints[HandBone::MiddleProximal as usize]),
metacarpal: convert_joint(joints[HandBone::MiddleMetacarpal as usize]),
},
ring: Finger {
tip: convert_joint(joints[HandBone::RingTip as usize]),
distal: convert_joint(joints[HandBone::RingDistal as usize]),
intermediate: convert_joint(joints[HandBone::RingIntermediate as usize]),
proximal: convert_joint(joints[HandBone::RingProximal as usize]),
metacarpal: convert_joint(joints[HandBone::RingMetacarpal as usize]),
},
little: Finger {
tip: convert_joint(joints[HandBone::LittleTip as usize]),
distal: convert_joint(joints[HandBone::LittleDistal as usize]),
intermediate: convert_joint(joints[HandBone::LittleIntermediate as usize]),
proximal: convert_joint(joints[HandBone::LittleProximal as usize]),
metacarpal: convert_joint(joints[HandBone::LittleMetacarpal as usize]),
},
palm: convert_joint(joints[HandBone::Palm as usize]),
wrist: convert_joint(joints[HandBone::Wrist as usize]),
elbow: None,
};
self.palm_spatial
.set_local_transform(Mat4::from_rotation_translation(
new_hand.palm.rotation.into(),
new_hand.palm.position.into(),
));
self.datamap.pinch_strength = pinch_between(&new_hand.thumb.tip, &new_hand.index.tip);
// this is how stereokit calculates grab
self.datamap.grab_strength =
pinch_between(&new_hand.ring.tip, &new_hand.ring.metacarpal);
*self.input.data.lock() = InputDataType::Hand(new_hand);
*self.input.datamap.lock() = Datamap::from_typed(&self.datamap).unwrap();
let captured = self.capture_manager.capture.upgrade().is_some();
if captured && !self.captured {
materials.get_mut(&self.material).unwrap().base_color =
Srgba::rgb(0., 1., 0.75).into();
} else if self.captured && !captured {
materials.get_mut(&self.material).unwrap().base_color =
Srgba::rgb(1., 1.0, 1.0).into();
}
self.captured = captured;
}
let distance_calculator = |space: &Arc<Spatial>, data: &InputDataType, field: &Field| {
let InputDataType::Hand(hand) = data else {
return None;
};
let thumb_tip_distance = field.distance(space, hand.thumb.tip.position.into());
let index_tip_distance = field.distance(space, hand.index.tip.position.into());
let middle_tip_distance = field.distance(space, hand.middle.tip.position.into());
let ring_tip_distance = field.distance(space, hand.ring.tip.position.into());
Some(
(thumb_tip_distance * 0.3)
+ (index_tip_distance * 0.4)
+ (middle_tip_distance * 0.15)
+ (ring_tip_distance * 0.15),
)
};
self.capture_manager.update_capture(&self.input);
self.capture_manager
.set_new_capture(&self.input, distance_calculator);
self.capture_manager.apply_capture(&self.input);
if self.capture_manager.capture.upgrade().is_some() {
return;
}
let sorted_handlers = get_sorted_handlers(&self.input, distance_calculator);
self.input
.set_handler_order(sorted_handlers.iter().map(|(handler, _)| handler));
}
}

View File

@@ -1,79 +0,0 @@
use crate::{
core::client::INTERNAL_CLIENT,
nodes::{
input::{tip::Tip, InputMethod, InputType},
spatial::Spatial,
Node,
},
};
use color_eyre::eyre::Result;
use glam::{Mat4, Vec2, Vec3};
use serde::{Deserialize, Serialize};
use stardust_xr::values::Datamap;
use std::sync::Arc;
use stereokit::{
named_colors::WHITE, ButtonState, Handed, Model, RenderLayer, StereoKitDraw,
StereoKitMultiThread,
};
#[derive(Default, Deserialize, Serialize)]
struct ControllerDatamap {
select: f32,
grab: f32,
scroll: Vec2,
}
pub struct SkController {
_node: Arc<Node>,
input: Arc<InputMethod>,
model: Model,
handed: Handed,
datamap: ControllerDatamap,
}
impl SkController {
pub fn new(sk: &impl StereoKitMultiThread, handed: Handed) -> Result<Self> {
let _node = Node::create_parent_name(
&INTERNAL_CLIENT,
"",
if handed == Handed::Left {
"controller_left"
} else {
"controller_right"
},
false,
)
.add_to_scenegraph()?;
Spatial::add_to(&_node, None, Mat4::IDENTITY, false);
let model = sk.model_create_mem("cursor.glb", include_bytes!("cursor.glb"), None)?;
let tip = InputType::Tip(Tip::default());
let input = InputMethod::add_to(&_node, tip, None)?;
Ok(SkController {
_node,
input,
handed,
model,
datamap: Default::default(),
})
}
pub fn update(&mut self, sk: &impl StereoKitDraw) {
let controller = sk.input_controller(self.handed);
*self.input.enabled.lock() = controller.tracked.contains(ButtonState::ACTIVE);
if *self.input.enabled.lock() {
let world_transform = Mat4::from_rotation_translation(
controller.aim.orientation,
controller.aim.position,
);
sk.model_draw(
&self.model,
world_transform * Mat4::from_scale(Vec3::ONE * 0.02),
WHITE,
RenderLayer::LAYER0,
);
self.input.spatial.set_local_transform(world_transform);
}
self.datamap.select = controller.trigger;
self.datamap.grab = controller.grip;
self.datamap.scroll = controller.stick;
*self.input.datamap.lock() = Datamap::from_typed(&self.datamap).ok();
}
}

View File

@@ -1,107 +0,0 @@
use crate::{
core::client::INTERNAL_CLIENT,
nodes::{
input::{hand::Hand, InputMethod, InputType},
spatial::Spatial,
Node,
},
};
use color_eyre::eyre::Result;
use glam::Mat4;
use nanoid::nanoid;
use serde::{Deserialize, Serialize};
use stardust_xr::{
schemas::flat::{Hand as FlatHand, Joint},
values::Datamap,
};
use std::sync::Arc;
use stereokit::{ButtonState, HandJoint, Handed, StereoKitMultiThread};
fn convert_joint(joint: HandJoint) -> Joint {
Joint {
position: joint.position.into(),
rotation: joint.orientation.into(),
radius: joint.radius,
distance: 0.0,
}
}
#[derive(Default, Deserialize, Serialize)]
struct HandDatamap {
pinch_strength: f32,
grab_strength: f32,
}
pub struct SkHand {
_node: Arc<Node>,
input: Arc<InputMethod>,
handed: Handed,
datamap: HandDatamap,
}
impl SkHand {
pub fn new(handed: Handed) -> Result<Self> {
let _node = Node::create_parent_name(&INTERNAL_CLIENT, "", &nanoid!(), false)
.add_to_scenegraph()?;
Spatial::add_to(&_node, None, Mat4::IDENTITY, false);
let hand = InputType::Hand(Box::new(Hand {
base: FlatHand {
right: handed == Handed::Right,
..Default::default()
},
}));
let input = InputMethod::add_to(&_node, hand, None)?;
Ok(SkHand {
_node,
input,
handed,
datamap: Default::default(),
})
}
pub fn update(&mut self, controller_enabled: bool, sk: &impl StereoKitMultiThread) {
let sk_hand = sk.input_hand(self.handed);
if let InputType::Hand(hand) = &mut *self.input.specialization.lock() {
let controller_active = controller_enabled
&& sk
.input_controller(self.handed)
.tracked
.contains(ButtonState::ACTIVE);
*self.input.enabled.lock() =
!controller_active && sk_hand.tracked_state.contains(ButtonState::ACTIVE);
sk.input_hand_visible(self.handed, *self.input.enabled.lock());
if *self.input.enabled.lock() {
hand.base.thumb.tip = convert_joint(sk_hand.fingers[0][4]);
hand.base.thumb.distal = convert_joint(sk_hand.fingers[0][3]);
hand.base.thumb.proximal = convert_joint(sk_hand.fingers[0][2]);
hand.base.thumb.metacarpal = convert_joint(sk_hand.fingers[0][1]);
for (finger, sk_finger) in [
(&mut hand.base.index, sk_hand.fingers[1]),
(&mut hand.base.middle, sk_hand.fingers[2]),
(&mut hand.base.ring, sk_hand.fingers[3]),
(&mut hand.base.little, sk_hand.fingers[4]),
] {
finger.tip = convert_joint(sk_finger[4]);
finger.distal = convert_joint(sk_finger[3]);
finger.intermediate = convert_joint(sk_finger[2]);
finger.proximal = convert_joint(sk_finger[1]);
finger.metacarpal = convert_joint(sk_finger[0]);
}
hand.base.palm.position = sk_hand.palm.position.into();
hand.base.palm.rotation = sk_hand.palm.orientation.into();
hand.base.palm.radius =
(sk_hand.fingers[2][0].radius + sk_hand.fingers[2][1].radius) * 0.5;
hand.base.wrist.position = sk_hand.wrist.position.into();
hand.base.wrist.rotation = sk_hand.wrist.orientation.into();
hand.base.wrist.radius =
(sk_hand.fingers[0][0].radius + sk_hand.fingers[4][0].radius) * 0.5;
hand.base.elbow = None;
}
}
self.datamap.pinch_strength = sk_hand.pinch_activation;
self.datamap.grab_strength = sk_hand.grip_activation;
*self.input.datamap.lock() = Datamap::from_typed(&self.datamap).ok();
}
}

View File

@@ -1,2 +1,214 @@
#![allow(unused)]
use crate::{
core::client::INTERNAL_CLIENT,
nodes::{
Node, OwnedNode,
fields::{EXPORTED_FIELDS, Field, Shape},
spatial::{EXPORTED_SPATIALS, Spatial},
},
};
use glam::{Mat4, vec3};
use input::{
eye_pointer::EyePointer, mouse_pointer::MousePointer, oxr_controller::OxrControllerInput,
oxr_hand::OxrHandInput,
};
use parking_lot::RwLock;
use play_space::PlaySpaceBounds;
use stardust_xr::schemas::dbus::object_registry::ObjectRegistry;
use std::{
marker::PhantomData,
sync::{Arc, atomic::Ordering},
};
use tokio::{sync::mpsc, task::AbortHandle};
use zbus::{Connection, interface, object_server::Interface, zvariant::OwnedObjectPath};
pub mod hmd;
pub mod input;
pub mod play_space;
pub struct ObjectHandle<I: Interface>(Connection, OwnedObjectPath, PhantomData<I>);
impl<I: Interface> Clone for ObjectHandle<I> {
fn clone(&self) -> Self {
Self(self.0.clone(), self.1.clone(), PhantomData)
}
}
impl<I: Interface> Drop for ObjectHandle<I> {
fn drop(&mut self) {
let connection = self.0.clone();
let object_path = self.1.clone();
tokio::task::spawn(async move {
connection.object_server().remove::<I, _>(object_path);
});
}
}
/// A wrapper around ObjectHandle<Tracked> that batches async updates
/// instead of spawning a tokio task for each state change
pub struct AsyncTracked {
pub sender: mpsc::UnboundedSender<bool>,
pub _handle: ObjectHandle<Tracked>,
pub _abort_handle: AbortHandle,
}
impl AsyncTracked {
pub fn new(connection: &Connection, path: &str) -> Self {
let handle = Tracked::new(connection, path);
let (sender, mut receiver) = mpsc::unbounded_channel::<bool>();
// Spawn a single long-running task that processes state updates
let task = tokio::task::spawn({
let handle = handle.clone();
async move {
while let Some(is_tracked) = receiver.recv().await {
let _ = handle.set_tracked(is_tracked).await;
}
}
});
Self {
sender,
_handle: handle,
_abort_handle: task.abort_handle(),
}
}
pub fn set_tracked(&self, is_tracked: bool) {
// Just send over channel instead of spawning a task
let _ = self.sender.send(is_tracked);
}
}
impl Drop for AsyncTracked {
fn drop(&mut self) {
self._abort_handle.abort();
}
}
pub struct SpatialRef(u64, OwnedNode);
impl SpatialRef {
pub fn create(connection: &Connection, path: &str) -> (Arc<Spatial>, ObjectHandle<SpatialRef>) {
let node = OwnedNode(Arc::new(Node::generate(&INTERNAL_CLIENT, false)));
let spatial = Spatial::add_to(&node.0, None, Mat4::IDENTITY, false);
let uid: u64 = rand::random();
EXPORTED_SPATIALS
.lock()
.insert(uid, Arc::downgrade(&node.0));
tokio::task::spawn({
let connection = connection.clone();
let path = path.to_string();
async move {
connection
.object_server()
.at(path, Self(uid, node))
.await
.unwrap();
}
});
(
spatial,
ObjectHandle(
connection.clone(),
OwnedObjectPath::try_from(path.to_string()).unwrap(),
PhantomData,
),
)
}
}
#[interface(name = "org.stardustxr.SpatialRef")]
impl SpatialRef {
#[zbus(property)]
fn uid(&self) -> u64 {
self.0
}
}
pub struct Tracked(bool);
impl Tracked {
pub fn new(connection: &Connection, path: &str) -> ObjectHandle<Tracked> {
tokio::task::spawn({
let connection = connection.clone();
let path = path.to_string();
async move {
connection
.object_server()
.at(path, Self(false))
.await
.unwrap();
}
});
ObjectHandle(
connection.clone(),
OwnedObjectPath::try_from(path.to_string()).unwrap(),
PhantomData,
)
}
}
impl ObjectHandle<Tracked> {
pub async fn set_tracked(&self, is_tracked: bool) -> zbus::Result<()> {
let tracked_ref = self
.0
.object_server()
.interface::<_, Tracked>(self.1.as_ref())
.await?;
let mut tracked = tracked_ref.get_mut().await;
if tracked.0 != is_tracked {
tracked.0 = is_tracked;
tracked
.is_tracked_changed(tracked_ref.signal_emitter())
.await;
}
Ok(())
}
}
#[interface(name = "org.stardustxr.Tracked")]
impl Tracked {
#[zbus(property)]
fn is_tracked(&self) -> bool {
self.0
}
}
pub struct FieldRef(u64, OwnedNode);
impl FieldRef {
pub fn create(
connection: &Connection,
path: &str,
shape: Shape,
) -> (Arc<Field>, ObjectHandle<FieldRef>) {
let node = OwnedNode(Arc::new(Node::generate(&INTERNAL_CLIENT, false)));
Spatial::add_to(&node.0, None, Mat4::IDENTITY, false);
let field = Field::add_to(&node.0, shape).unwrap();
let uid: u64 = rand::random();
EXPORTED_FIELDS.lock().insert(uid, Arc::downgrade(&node.0));
tokio::task::spawn({
let connection = connection.clone();
let path = path.to_string();
async move {
connection
.object_server()
.at(path, Self(uid, node))
.await
.unwrap();
}
});
(
field,
ObjectHandle(
connection.clone(),
OwnedObjectPath::try_from(path.to_string()).unwrap(),
PhantomData,
),
)
}
}
#[interface(name = "org.stardustxr.FieldRef")]
impl FieldRef {
#[zbus(property)]
fn uid(&self) -> u64 {
self.0
}
}

View File

@@ -1,81 +1,153 @@
use std::sync::Arc;
use color_eyre::eyre::Result;
use glam::Mat4;
use mint::Vector2;
use nanoid::nanoid;
use serde::{Deserialize, Serialize};
use stardust_xr::values::Datamap;
use stereokit::StereoKitMultiThread;
use crate::{
core::client::INTERNAL_CLIENT,
nodes::{
data::PulseReceiver,
fields::{r#box::BoxField, Field},
spatial::Spatial,
Node,
},
use bevy::prelude::*;
use bevy_mod_openxr::{
helper_traits::{ToQuat, ToVec3},
resources::{OxrFrameState, Pipelined},
session::OxrSession,
};
use bevy_mod_xr::{
session::{XrPreDestroySession, XrSessionCreated},
spaces::{XrPrimaryReferenceSpace, XrReferenceSpace, XrSpace},
};
use openxr::SpaceLocationFlags;
use parking_lot::RwLock;
use zbus::{Connection, ObjectServer, interface};
#[derive(Debug, Deserialize, Serialize)]
struct PlaySpaceMap {
play_space: (),
size: Vector2<f32>,
use crate::{DbusConnection, PreFrameWait, get_time, nodes::spatial::Spatial};
use super::{AsyncTracked, ObjectHandle, SpatialRef, Tracked};
pub struct PlaySpacePlugin;
impl Plugin for PlaySpacePlugin {
fn build(&self, app: &mut App) {
app.add_systems(XrPreDestroySession, destroy_stage_space);
app.add_systems(XrSessionCreated, create_stage_space);
app.add_systems(PreFrameWait, update);
app.add_systems(Startup, setup);
}
}
impl Default for PlaySpaceMap {
fn default() -> Self {
Self {
play_space: (),
size: [0.0; 2].into(),
fn setup(connection: Res<DbusConnection>, mut cmds: Commands) {
let (spatial, spatial_handle) = SpatialRef::create(&connection, "/org/stardustxr/PlaySpace");
// the OpenXR session might not exist quite yet
let tracked = AsyncTracked::new(&connection, "/org/stardustxr/PlaySpace");
let dbus_connection = connection.clone();
let play_space_data = Arc::new(RwLock::default());
tokio::task::spawn({
let data = play_space_data.clone();
async move {
PlaySpaceBounds::create(&dbus_connection, data).await;
dbus_connection
.request_name("org.stardustxr.PlaySpace")
.await
.unwrap();
}
});
cmds.insert_resource(PlaySpace {
spatial,
_spatial_handle: spatial_handle,
tracked_handle: tracked,
bounds: play_space_data,
});
}
#[derive(Resource)]
struct StageSpace(XrSpace);
fn create_stage_space(session: Res<OxrSession>, mut cmds: Commands) {
let space = session
.create_reference_space(openxr::ReferenceSpaceType::STAGE, Transform::IDENTITY)
.inspect_err(|err| error!("failed to create Stage XrSpace"))
.ok();
if let Some(space) = space {
cmds.insert_resource(StageSpace(space.0));
}
}
fn destroy_stage_space(session: Res<OxrSession>, mut cmds: Commands, stage: Res<StageSpace>) {
session.destroy_space(stage.0);
cmds.remove_resource::<StageSpace>();
}
/// TODO: impl this
fn update(
session: Option<Res<OxrSession>>,
stage: Option<Res<StageSpace>>,
ref_space: Option<Res<XrPrimaryReferenceSpace>>,
play_space: Res<PlaySpace>,
state: Option<Res<OxrFrameState>>,
pipelined: Option<Res<Pipelined>>,
) {
let (Some(session), Some(stage), Some(ref_space), Some(state)) =
(session, stage, ref_space, state)
else {
play_space.bounds.write().drain(..);
play_space.tracked_handle.set_tracked(false);
play_space
.spatial
.set_local_transform(Mat4::from_translation(vec3(0.0, -1.65, 0.0)));
return;
};
let time = get_time(pipelined.is_some(), &state);
let location = session
.locate_space(&stage.0, &ref_space, time)
.inspect_err(|err| error!("Error while Locating OpenXR Stage Space {err}"));
if let Ok(location) = location {
let is_tracked = location.location_flags.contains(
SpaceLocationFlags::POSITION_VALID
| SpaceLocationFlags::POSITION_TRACKED
| SpaceLocationFlags::ORIENTATION_VALID
| SpaceLocationFlags::ORIENTATION_TRACKED,
);
play_space.tracked_handle.set_tracked(is_tracked);
if is_tracked {
play_space
.spatial
.set_local_transform(Mat4::from_rotation_translation(
location.pose.orientation.to_quat(),
location.pose.position.to_vec3(),
));
}
}
// session.reference_space_bounds_rect(openxr::ReferenceSpaceType::STAGE);
// if (World::has_bounds()
// && World::get_bounds_size().x != 0.0
// && World::get_bounds_size().y != 0.0)
// {
// let bounds = World::get_bounds_size();
// vec![
// ((bounds.x).into(), (bounds.y).into()),
// ((bounds.x).into(), (-bounds.y).into()),
// ((-bounds.x).into(), (-bounds.y).into()),
// ((-bounds.x).into(), (bounds.y).into()),
// ]
// } else {
// vec![]
// }
}
#[derive(Resource)]
pub struct PlaySpace {
_node: Arc<Node>,
spatial: Arc<Spatial>,
field: Arc<Field>,
_pulse_rx: Arc<PulseReceiver>,
_spatial_handle: ObjectHandle<SpatialRef>,
tracked_handle: AsyncTracked,
bounds: Arc<RwLock<Vec<(f64, f64)>>>,
}
impl PlaySpace {
pub fn new() -> Result<Self> {
let node = Node::create_parent_name(&INTERNAL_CLIENT, "", &nanoid!(), false)
.add_to_scenegraph()?;
let spatial = Spatial::add_to(&node, None, Mat4::IDENTITY, false);
BoxField::add_to(&node, [0.0; 3].into());
let field = node.get_aspect::<Field>()?.clone();
let pulse_rx = PulseReceiver::add_to(
&node,
node.clone(),
Datamap::from_typed(PlaySpaceMap::default())?,
)?;
Ok(PlaySpace {
_node: node,
spatial,
field,
_pulse_rx: pulse_rx,
})
}
pub fn update(&self, sk: &impl StereoKitMultiThread) {
let pose = sk.world_get_bounds_pose();
self.spatial
.set_local_transform(Mat4::from_rotation_translation(
pose.orientation,
pose.position,
));
let Field::Box(box_field) = self.field.as_ref() else {
return;
};
box_field.set_size(
[
sk.world_get_bounds_size().x,
0.0,
sk.world_get_bounds_size().y,
]
.into(),
);
pub struct PlaySpaceBounds(Arc<RwLock<Vec<(f64, f64)>>>);
impl PlaySpaceBounds {
pub async fn create(connection: &Connection, data: Arc<RwLock<Vec<(f64, f64)>>>) {
connection
.object_server()
.at("/org/stardustxr/PlaySpace", Self(data))
.await
.unwrap();
}
}
#[interface(name = "org.stardustxr.PlaySpace")]
impl PlaySpaceBounds {
#[zbus(property)]
fn bounds(&self) -> Vec<(f64, f64)> {
self.0.read().clone()
}
}

112
src/session.rs Normal file
View File

@@ -0,0 +1,112 @@
use crate::core::client::CLIENTS;
use crate::core::client_state::ClientStateParsed;
#[cfg(feature = "wayland")]
use crate::wayland::WAYLAND_DISPLAY;
use crate::{CliArgs, STARDUST_INSTANCE};
use directories::ProjectDirs;
use rustc_hash::FxHashMap;
use std::ffi::OsStr;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::time::Duration;
use tokio::task::LocalSet;
use tracing::info;
pub async fn save_session(project_dirs: &ProjectDirs) {
let session_id = nanoid::nanoid!();
let state_dir = project_dirs.state_dir().unwrap();
let session_dir = state_dir.join(&session_id);
std::fs::create_dir_all(&session_dir).unwrap();
let _ = std::fs::remove_dir_all(state_dir.join("latest"));
std::os::unix::fs::symlink(&session_dir, state_dir.join("latest")).unwrap();
let local_set = LocalSet::new();
for client in CLIENTS.get_vec() {
let session_dir = session_dir.clone();
local_set.spawn_local(async move {
tokio::select! {
biased;
s = client.save_state() => {if let Some(s) = s { s.to_file(&session_dir) }},
_ = tokio::time::sleep(Duration::from_millis(100)) => (),
}
});
}
local_set.await;
info!("Session ID for restore is {session_id}");
}
pub fn launch_start(cli_args: &CliArgs, project_dirs: &ProjectDirs) -> Vec<Child> {
match (&cli_args.restore, &cli_args.startup_script) {
(Some(session_id), _) => restore_session(
&project_dirs.state_dir().unwrap().join(session_id),
cli_args.debug_launched_clients,
),
(None, Some(startup_script)) => run_script(
&startup_script.clone().canonicalize().unwrap_or_default(),
cli_args.debug_launched_clients,
),
(None, None) => run_script(
&project_dirs.config_dir().join("startup"),
cli_args.debug_launched_clients,
),
}
}
pub fn restore_session(session_dir: &Path, debug_launched_clients: bool) -> Vec<Child> {
let Ok(clients) = session_dir.read_dir() else {
return Vec::new();
};
clients
.filter_map(Result::ok)
.filter(|c| c.path().extension() == Some(OsStr::new("toml")))
.filter_map(|c| ClientStateParsed::from_file(&c.path()))
.filter_map(ClientStateParsed::launch_command)
.filter_map(|c| run_client(c, debug_launched_clients))
.collect()
}
pub fn run_script(script_path: &Path, debug_launched_clients: bool) -> Vec<Child> {
let _ = std::fs::set_permissions(script_path, std::fs::Permissions::from_mode(0o755));
let startup_command = Command::new(script_path);
run_client(startup_command, debug_launched_clients)
.map(|c| vec![c])
.unwrap_or_default()
}
pub fn run_client(mut command: Command, debug_launched_clients: bool) -> Option<Child> {
command.stdin(Stdio::null());
if !debug_launched_clients {
command.stdout(Stdio::null());
command.stderr(Stdio::null());
}
for (var, value) in connection_env() {
command.env(var, value);
}
let child = command.spawn().ok()?;
Some(child)
}
pub fn connection_env() -> FxHashMap<String, String> {
let mut env: FxHashMap<String, String> = FxHashMap::default();
env.insert(
stringify!(STARDUST_INSTANCE).to_string(),
STARDUST_INSTANCE.get().unwrap().clone(),
);
if let Some(flat_wayland_display) = std::env::var_os("WAYLAND_DISPLAY") {
env.insert(
"FLAT_WAYLAND_DISPLAY".to_string(),
flat_wayland_display.to_string_lossy().into_owned(),
);
}
#[cfg(feature = "wayland")]
{
env.insert(
stringify!(WAYLAND_DISPLAY).to_string(),
WAYLAND_DISPLAY.get().unwrap().to_string_lossy().to_string(),
);
env.insert("XDG_SESSION_TYPE".to_string(), "wayland".to_string());
}
env
}

39
src/spectator_cam.rs Normal file
View File

@@ -0,0 +1,39 @@
use bevy::{
app::Plugin,
core_pipeline::core_3d::Camera3d,
ecs::{
component::Component,
system::{Commands, Res},
},
render::camera::{PerspectiveProjection, Projection},
transform::components::Transform,
};
use bevy_mod_openxr::session::OxrSession;
use bevy_mod_xr::session::XrSessionCreated;
use openxr::ReferenceSpaceType;
pub struct SpectatorCameraPlugin;
impl Plugin for SpectatorCameraPlugin {
fn build(&self, app: &mut bevy::app::App) {
app.add_systems(XrSessionCreated, create);
}
}
#[derive(Component)]
#[require(Camera3d)]
struct SpectatorCam;
fn create(session: Res<OxrSession>, mut cmds: Commands) {
cmds.spawn((
SpectatorCam,
session
.create_reference_space(ReferenceSpaceType::VIEW, Transform::IDENTITY)
.unwrap()
.0,
Projection::Perspective(PerspectiveProjection {
fov: 100f32.to_radians(),
..Default::default()
}),
));
}

82
src/tracking_offset.rs Normal file
View File

@@ -0,0 +1,82 @@
use bevy::{
app::{Plugin, Update},
ecs::{
resource::Resource,
schedule::{Condition, IntoScheduleConfigs, common_conditions::resource_exists},
system::{Commands, Res, ResMut},
},
transform::components::Transform,
};
use bevy_mod_openxr::{
helper_traits::ToTransform as _,
poll_events::{OxrEventHandlerExt, OxrEventIn},
resources::OxrFrameState,
session::OxrSession,
};
use bevy_mod_xr::{session::XrSessionCreated, spaces::XrPrimaryReferenceSpace};
use glam::{Quat, Vec3};
use openxr::ReferenceSpaceType;
pub struct TrackingOffsetPlugin;
impl Plugin for TrackingOffsetPlugin {
fn build(&self, app: &mut bevy::app::App) {
app.add_oxr_event_handler(reset_offset);
app.add_systems(XrSessionCreated, |mut cmds: Commands| {
cmds.insert_resource(OffsetTag);
});
app.add_systems(
Update,
offset.run_if(resource_exists::<OffsetTag>.and(resource_exists::<OxrFrameState>)),
);
}
}
#[derive(Resource)]
struct OffsetTag;
fn reset_offset(
oxr_event: OxrEventIn,
mut ref_space: ResMut<XrPrimaryReferenceSpace>,
session: Res<OxrSession>,
) {
if let openxr::Event::ReferenceSpaceChangePending(v) = *oxr_event
&& v.reference_space_type() == ReferenceSpaceType::LOCAL
{
let space = session
.create_reference_space(ReferenceSpaceType::LOCAL, Transform::IDENTITY)
.unwrap();
session.destroy_space(ref_space.0.0).unwrap();
ref_space.0 = space;
}
}
fn offset(
session: Res<OxrSession>,
state: Res<OxrFrameState>,
mut primary_ref_space: ResMut<XrPrimaryReferenceSpace>,
mut cmds: Commands,
) {
cmds.remove_resource::<OffsetTag>();
let local = session
.create_reference_space(ReferenceSpaceType::LOCAL, Transform::IDENTITY)
.unwrap();
let view = session
.create_reference_space(ReferenceSpaceType::VIEW, Transform::IDENTITY)
.unwrap();
let view_pose = session
.locate_space(&view, &local, state.predicted_display_time)
.unwrap()
.pose
.to_transform();
let offset = view_pose.with_rotation(Quat::from_axis_angle(
Vec3::Y,
view_pose.rotation.to_euler(glam::EulerRot::XYZ).1,
));
let offset = Transform::from_matrix(offset.compute_matrix());
let local_offset = session
.create_reference_space(ReferenceSpaceType::LOCAL, offset)
.unwrap();
session.destroy_space(primary_ref_space.0.0).unwrap();
primary_ref_space.0 = local_offset;
}

View File

@@ -1,53 +0,0 @@
use crate::wayland::surface::CoreSurface;
use super::state::{ClientState, WaylandState};
use portable_atomic::{AtomicU32, Ordering};
#[cfg(feature = "xwayland")]
use smithay::xwayland::XWaylandClientData;
use smithay::{
delegate_compositor,
reexports::wayland_server::{protocol::wl_surface::WlSurface, Client},
wayland::compositor::{self, CompositorClientState, CompositorHandler, CompositorState},
};
use std::sync::Arc;
use tracing::debug;
impl CompositorHandler for WaylandState {
fn compositor_state(&mut self) -> &mut CompositorState {
&mut self.compositor_state
}
fn commit(&mut self, surface: &WlSurface) {
debug!(?surface, "Surface commit");
let mut count = 0;
let core_surface = compositor::with_states(surface, |data| {
let count_new = data
.data_map
.insert_if_missing_threadsafe(|| AtomicU32::new(0));
if !count_new {
if let Some(stored_count) = data.data_map.get::<AtomicU32>() {
count = stored_count.fetch_add(1, Ordering::Relaxed);
}
}
data.data_map.get::<Arc<CoreSurface>>().cloned()
});
if let Some(core_surface) = core_surface {
core_surface.commit(count);
}
}
fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
if let Some(client_state) = client.get_data::<ClientState>() {
&client_state.compositor_state
} else {
#[cfg(feature = "xwayland")]
if let Some(xwayland_client_data) = client.get_data::<XWaylandClientData>() {
return &xwayland_client_data.compositor_state;
}
unimplemented!()
}
}
}
delegate_compositor!(WaylandState);

112
src/wayland/core/buffer.rs Normal file
View File

@@ -0,0 +1,112 @@
use crate::wayland::dmabuf::buffer_backing::DmabufBacking;
use crate::wayland::{Client, Message, WaylandResult};
use crate::wayland::{MessageSink, core::shm_buffer_backing::ShmBufferBacking, util::ClientExt};
use bevy::{
asset::{Assets, Handle},
image::Image,
};
use bevy_dmabuf::import::ImportedDmatexs;
use mint::Vector2;
use std::sync::Arc;
use waynest::ObjectId;
pub use waynest_protocols::server::core::wayland::wl_buffer::*;
use waynest_server::{Client as _, RequestDispatcher};
#[derive(Debug)]
pub struct BufferUsage {
pub buffer: Arc<Buffer>,
message_sink: MessageSink,
}
impl BufferUsage {
pub fn new(client: &Client, buffer: &Arc<Buffer>) -> Arc<Self> {
Arc::new(Self {
buffer: buffer.clone(),
message_sink: client.message_sink(),
})
}
}
impl Drop for BufferUsage {
fn drop(&mut self) {
let _ = self
.message_sink
.send(Message::ReleaseBuffer(self.buffer.clone()));
}
}
#[derive(Debug)]
pub enum BufferBacking {
Shm(ShmBufferBacking),
Dmabuf(DmabufBacking),
}
#[derive(Debug, RequestDispatcher)]
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
pub struct Buffer {
pub id: ObjectId,
backing: BufferBacking,
}
impl Buffer {
#[tracing::instrument(level = "debug", skip_all)]
pub fn new(
client: &mut Client,
id: ObjectId,
backing: BufferBacking,
) -> WaylandResult<Arc<Self>> {
Ok(client.insert(id, Self { id, backing })?)
}
/// Returns the tex if it was updated
#[tracing::instrument(level = "debug", skip_all)]
pub fn update_tex(
&self,
dmatexes: &ImportedDmatexs,
images: &mut Assets<Image>,
) -> Option<Handle<Image>> {
tracing::debug!("Updating texture for buffer {:?}", self.id);
match &self.backing {
BufferBacking::Shm(backing) => backing.update_tex(dmatexes, images),
BufferBacking::Dmabuf(backing) => backing.update_tex(dmatexes, images),
}
}
#[tracing::instrument(level = "debug", skip_all)]
pub fn on_commit(&self) {
tracing::debug!("running on_commit for buffer {:?}", self.id);
match &self.backing {
BufferBacking::Shm(backing) => backing.on_commit(),
BufferBacking::Dmabuf(_backing) => {}
}
}
pub fn is_transparent(&self) -> bool {
match &self.backing {
BufferBacking::Shm(backing) => backing.is_transparent(),
BufferBacking::Dmabuf(backing) => backing.is_transparent(),
}
}
pub fn size(&self) -> Vector2<usize> {
match &self.backing {
BufferBacking::Shm(backing) => backing.size(),
BufferBacking::Dmabuf(backing) => backing.size(),
}
}
pub fn uses_buffer_usage(&self) -> bool {
matches!(
self.backing,
BufferBacking::Dmabuf(_) | BufferBacking::Shm(_)
)
}
}
impl WlBuffer for Buffer {
type Connection = crate::wayland::Client;
/// https://wayland.app/protocols/wayland#wl_buffer:request:destroy
async fn destroy(&self, client: &mut Client, _sender_id: ObjectId) -> WaylandResult<()> {
client.remove(self.id);
tracing::info!("Destroying buffer {:?}", self.id);
Ok(())
}
}

View File

@@ -0,0 +1,11 @@
use waynest::ObjectId;
pub use waynest_protocols::server::core::wayland::wl_callback::*;
use waynest_server::RequestDispatcher;
#[derive(Debug, RequestDispatcher, Clone)]
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
pub struct Callback(pub ObjectId);
/// https://wayland.app/protocols/wayland#wl_callback
impl WlCallback for Callback {
type Connection = crate::wayland::Client;
}

View File

@@ -0,0 +1,86 @@
use super::surface::WL_SURFACE_REGISTRY;
use crate::wayland::{WaylandError, WaylandResult};
use crate::wayland::{core::surface::Surface, util::ClientExt};
use waynest::ObjectId;
use waynest_protocols::server::core::wayland::wl_surface::WlSurface;
pub use waynest_protocols::server::core::wayland::{wl_compositor::*, wl_region::*};
use waynest_server::{Client as _, RequestDispatcher};
#[derive(Debug, waynest_server::RequestDispatcher, Default)]
#[waynest(error = WaylandError, connection = crate::wayland::Client)]
pub struct Compositor;
impl WlCompositor for Compositor {
type Connection = crate::wayland::Client;
/// https://wayland.app/protocols/wayland#wl_compositor:request:create_surface
async fn create_surface(
&self,
client: &mut Self::Connection,
_sender_id: ObjectId,
id: ObjectId,
) -> WaylandResult<()> {
let surface = client.insert(id, Surface::new(client, id))?;
if let Some(output) = client.display().output.get() {
surface.enter(client, id, output.id).await?;
}
WL_SURFACE_REGISTRY.add_raw(&surface);
Ok(())
}
/// https://wayland.app/protocols/wayland#wl_compositor:request:create_region
async fn create_region(
&self,
client: &mut Self::Connection,
_sender_id: ObjectId,
id: ObjectId,
) -> WaylandResult<()> {
client.insert(id, Region { id })?;
Ok(())
}
}
#[derive(Debug, RequestDispatcher)]
#[waynest(error = WaylandError, connection = crate::wayland::Client)]
pub struct Region {
id: ObjectId,
}
impl WlRegion for Region {
type Connection = crate::wayland::Client;
/// https://wayland.app/protocols/wayland#wl_region:request:add
async fn add(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
_x: i32,
_y: i32,
_width: i32,
_height: i32,
) -> WaylandResult<()> {
Ok(())
}
/// https://wayland.app/protocols/wayland#wl_region:request:subtract
async fn subtract(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
_x: i32,
_y: i32,
_width: i32,
_height: i32,
) -> WaylandResult<()> {
Ok(())
}
/// https://wayland.app/protocols/wayland#wl_region:request:destroy
async fn destroy(
&self,
client: &mut Self::Connection,
_sender_id: ObjectId,
) -> WaylandResult<()> {
client.remove(self.id);
Ok(())
}
}

View File

@@ -0,0 +1,166 @@
use crate::wayland::{Client, WaylandResult};
use std::os::fd::OwnedFd;
use waynest::ObjectId;
use waynest_protocols::server::core::wayland::{
wl_data_device::*, wl_data_device_manager::*, wl_data_offer::WlDataOffer, wl_data_source::*,
};
use waynest_server::Client as _;
// TODO: actually implement this
#[derive(Debug, waynest_server::RequestDispatcher)]
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
pub struct DataDeviceManager;
impl WlDataDeviceManager for DataDeviceManager {
type Connection = Client;
async fn create_data_source(
&self,
client: &mut Self::Connection,
_sender_id: ObjectId,
id: ObjectId,
) -> WaylandResult<()> {
client.insert(id, DataSource { id })?;
Ok(())
}
async fn get_data_device(
&self,
client: &mut Client,
_sender_id: ObjectId,
id: ObjectId,
_seat: ObjectId,
) -> WaylandResult<()> {
client.insert(id, DataDevice)?;
Ok(())
}
}
#[derive(Debug, waynest_server::RequestDispatcher)]
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
pub struct DataSource {
id: ObjectId,
}
impl WlDataSource for DataSource {
type Connection = Client;
async fn offer(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
_mime_type: String,
) -> WaylandResult<()> {
Ok(())
}
async fn destroy(
&self,
client: &mut Self::Connection,
_sender_id: ObjectId,
) -> WaylandResult<()> {
client.remove(self.id);
Ok(())
}
async fn set_actions(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
_dnd_actions: DndAction,
) -> WaylandResult<()> {
Ok(())
}
}
#[derive(Debug, waynest_server::RequestDispatcher)]
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
pub struct DataDevice;
impl WlDataDevice for DataDevice {
type Connection = Client;
async fn start_drag(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
_source: Option<ObjectId>,
_origin: ObjectId,
_icon: Option<ObjectId>,
_serial: u32,
) -> WaylandResult<()> {
Ok(())
}
async fn set_selection(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
_source: Option<ObjectId>,
_serial: u32,
) -> WaylandResult<()> {
Ok(())
}
async fn release(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
) -> WaylandResult<()> {
Ok(())
}
}
#[derive(Debug, waynest_server::RequestDispatcher)]
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
pub struct DataOffer {
id: ObjectId,
}
impl WlDataOffer for DataOffer {
type Connection = Client;
async fn accept(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
_serial: u32,
_mime_type: Option<String>,
) -> WaylandResult<()> {
Ok(())
}
async fn receive(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
_mime_type: String,
_fd: OwnedFd,
) -> WaylandResult<()> {
Ok(())
}
async fn destroy(
&self,
client: &mut Self::Connection,
_sender_id: ObjectId,
) -> WaylandResult<()> {
client.remove(self.id);
Ok(())
}
async fn finish(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
) -> WaylandResult<()> {
Ok(())
}
async fn set_actions(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
_dnd_actions: DndAction,
_preferred_action: DndAction,
) -> WaylandResult<()> {
Ok(())
}
}

View File

@@ -0,0 +1,258 @@
use crate::{
nodes::items::panel::KEYMAPS,
wayland::{Client, WaylandResult, core::surface::Surface, util::ClientExt},
};
use dashmap::{DashMap, DashSet};
use memfd::MemfdOptions;
use slotmap::{DefaultKey, KeyData};
use std::{
collections::HashSet,
io::Write,
os::{
fd::{AsFd, IntoRawFd},
unix::io::{FromRawFd, OwnedFd},
},
sync::{Arc, Weak},
};
use tokio::sync::Mutex;
use waynest::ObjectId;
pub use waynest_protocols::server::core::wayland::wl_keyboard::*;
#[derive(Default)]
struct ModifierState {
pressed_keys: HashSet<u32>,
mods_depressed: u32,
mods_latched: u32,
mods_locked: u32,
group: u32,
}
impl ModifierState {
fn update_key(&mut self, key: u32, pressed: bool) -> bool {
let changed = if pressed {
self.pressed_keys.insert(key)
} else {
self.pressed_keys.remove(&key)
};
if changed {
self.update_modifiers();
}
changed
}
fn update_modifiers(&mut self) {
let mut mods = 0;
// Update modifier state based on currently pressed keys
for key in &self.pressed_keys {
match *key {
input_event_codes::KEY_LEFTSHIFT!() | input_event_codes::KEY_RIGHTSHIFT!() => {
mods |= 1
}
input_event_codes::KEY_LEFTCTRL!() | input_event_codes::KEY_RIGHTCTRL!() => {
mods |= 4
}
input_event_codes::KEY_LEFTALT!() => mods |= 8,
input_event_codes::KEY_RIGHTALT!() => mods |= 128,
input_event_codes::KEY_LEFTMETA!() | input_event_codes::KEY_RIGHTMETA!() => {
mods |= 64
}
input_event_codes::KEY_CAPSLOCK!() => self.mods_locked ^= 1,
_ => {}
}
}
self.mods_depressed = mods;
}
}
#[derive(waynest_server::RequestDispatcher)]
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
pub struct Keyboard {
pub id: ObjectId,
focused_surface: Mutex<Weak<Surface>>,
modifier_state: Mutex<ModifierState>,
pressed_keys: DashMap<ObjectId, DashSet<u32>>,
current_keymap_id: Mutex<u64>,
}
impl Keyboard {
pub fn new(id: ObjectId) -> Self {
Self {
id,
focused_surface: Mutex::new(Weak::new()),
modifier_state: Mutex::new(ModifierState::default()),
pressed_keys: DashMap::default(),
current_keymap_id: Mutex::new(0),
}
}
async fn send_keymap(&self, client: &mut Client, keymap: &[u8]) -> WaylandResult<()> {
let mut file = MemfdOptions::default()
.create("stardust-keymap")?
.into_file();
file.set_len(keymap.len() as u64)?;
file.write_all(keymap)?;
file.flush()?;
let fd = unsafe { OwnedFd::from_raw_fd(file.into_raw_fd()) };
// Send keymap to client
self.keymap(
client,
self.id,
KeymapFormat::XkbV1,
fd.as_fd(),
keymap.len() as u32,
)
.await?;
Ok(())
}
/// has to be the wayland key, so -8 or whatever
pub async fn handle_keyboard_key(
&self,
client: &mut Client,
surface: Arc<Surface>,
keymap_id: u64,
key: u32,
pressed: bool,
) -> WaylandResult<()> {
// KEYMAP UPDATES
{
let mut old_keymap_id = self.current_keymap_id.lock().await;
if *old_keymap_id != keymap_id {
let keymap_key = DefaultKey::from(KeyData::from_ffi(keymap_id));
// Get keymap data and drop the lock immediately
let keymap_data = {
let keymap_lock = KEYMAPS.lock();
keymap_lock
.get(keymap_key)
.map(|s| s.as_bytes().to_vec())
.unwrap_or_default()
};
// Now we can safely await
self.send_keymap(client, &keymap_data).await?;
};
*old_keymap_id = keymap_id;
drop(old_keymap_id);
}
// PRESSED KEYS UPDATE
let pressed_keys = self.pressed_keys.entry(surface.id).or_default();
if pressed {
pressed_keys.insert(key);
} else {
pressed_keys.remove(&key);
}
// println!("pressed keys: {:?}", &*pressed_keys);
// FOCUS UPDATES
let mut focused = self.focused_surface.lock().await;
let mut modifier_state = self.modifier_state.lock().await;
let refocus = focused.as_ptr() != Arc::as_ptr(&surface);
// If we're entering a new surface
if refocus {
// Send leave to old surface if it exists and is still alive
if let Some(old_surface) = focused.upgrade() {
let serial = client.next_event_serial();
self.leave(client, old_surface.id, serial, self.id).await?;
// println!("Left surface {}", old_surface.id);
}
// Send enter to new surface
let serial = client.next_event_serial();
self.enter(
client,
self.id,
serial,
surface.id,
pressed_keys.iter().flat_map(|k| k.to_ne_bytes()).collect(),
)
.await?;
// println!("Entered new surface {}", surface.id);
// Update focused surface
*focused = Arc::downgrade(&surface);
}
// KEY EVENT SENDING
let serial = client.next_event_serial();
// println!(
// "Sent key {key} {}",
// if pressed { "pressed" } else { "released" }
// );
self.key(
client,
self.id,
serial,
client.display().creation_time.elapsed().as_millis() as u32, // time
key,
if pressed {
KeyState::Pressed
} else {
KeyState::Released
},
)
.await?;
// MODIFIER UPDATES
// Update modifier state and send modifiers event if changed
if refocus || modifier_state.update_key(key, pressed) {
// println!("Update modifiers");
let serial = client.next_event_serial();
self.modifiers(
client,
self.id,
serial,
modifier_state.mods_depressed,
modifier_state.mods_latched,
modifier_state.mods_locked,
modifier_state.group,
)
.await?;
}
Ok(())
}
pub async fn reset(&self, client: &mut Client) -> WaylandResult<()> {
let mut modifier_state = self.modifier_state.lock().await;
modifier_state.pressed_keys.clear();
modifier_state.mods_depressed = 0;
modifier_state.mods_latched = 0;
modifier_state.mods_locked = 0;
modifier_state.group = 0;
let serial = client.next_event_serial();
self.modifiers(
client,
self.id,
serial,
modifier_state.mods_depressed,
modifier_state.mods_latched,
modifier_state.mods_locked,
modifier_state.group,
)
.await
}
}
impl WlKeyboard for Keyboard {
type Connection = Client;
/// https://wayland.app/protocols/wayland#wl_keyboard:request:release
async fn release(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
) -> WaylandResult<()> {
Ok(())
}
}

13
src/wayland/core/mod.rs Normal file
View File

@@ -0,0 +1,13 @@
pub mod buffer;
pub mod callback;
pub mod compositor;
pub mod data_device;
pub mod keyboard;
pub mod output;
pub mod pointer;
pub mod seat;
pub mod shm;
pub mod shm_buffer_backing;
pub mod shm_pool;
pub mod surface;
pub mod touch;

View File

@@ -0,0 +1,57 @@
use crate::wayland::{Client, WaylandResult};
use waynest::ObjectId;
pub use waynest_protocols::server::core::wayland::wl_output::*;
#[derive(Debug, waynest_server::RequestDispatcher)]
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
pub struct Output {
pub id: ObjectId,
pub version: u32,
}
impl Output {
pub async fn advertise_outputs(&self, client: &mut Client) -> WaylandResult<()> {
self.geometry(
client,
self.id,
2048,
2048,
0,
0,
Subpixel::None,
"Stardust Virtual Display".to_string(),
"Stardust Virtual Display".to_string(),
Transform::Normal,
)
.await?;
if self.version >= 4 {
self.name(client, self.id, "Stardust Virtual Display".to_string())
.await?;
self.description(
client,
self.id,
"I needed this to account for dumb clients".to_string(),
)
.await?;
}
self.mode(client, self.id, Mode::Current, 2048, 2048, i32::MAX)
.await?;
if self.version >= 2 {
self.done(client, self.id).await?;
}
Ok(())
}
}
impl WlOutput for Output {
type Connection = Client;
/// https://wayland.app/protocols/wayland#wl_output:request:release
async fn release(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
) -> WaylandResult<()> {
Ok(())
}
}

252
src/wayland/core/pointer.rs Normal file
View File

@@ -0,0 +1,252 @@
use super::surface::SurfaceRole;
use crate::nodes::items::panel::Geometry;
use crate::wayland::core::surface::Surface;
use crate::wayland::{Client, WaylandResult};
use mint::Vector2;
use std::sync::Arc;
use std::sync::Weak;
use tokio::sync::Mutex;
use tracing;
use waynest::ObjectId;
use waynest_server::Client as _;
pub use waynest_protocols::server::core::wayland::wl_pointer::*;
#[derive(waynest_server::RequestDispatcher)]
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
pub struct Pointer {
pub id: ObjectId,
version: u32,
focused_surface: Mutex<Weak<Surface>>,
cursor_surface: Mutex<Option<Arc<Surface>>>,
}
impl Pointer {
pub fn new(id: ObjectId, version: u32) -> Self {
Self {
id,
version,
focused_surface: Mutex::new(Weak::new()),
cursor_surface: Mutex::new(None),
}
}
pub async fn handle_pointer_motion(
&self,
client: &mut Client,
surface: Arc<Surface>,
position: Vector2<f32>,
) -> WaylandResult<()> {
tracing::debug!(
"Handling pointer motion at ({}, {})",
position.x,
position.y
);
let mut focused = self.focused_surface.lock().await;
// If we're entering a new surface
if focused.as_ptr() != Arc::as_ptr(&surface) {
tracing::debug!("Surface transition detected");
// Send leave to old surface if it exists and is still alive
if let Some(old_surface) = focused.upgrade() {
let serial = client.next_event_serial();
tracing::debug!("Sending leave event with serial {}", serial);
self.leave(client, self.id, serial, old_surface.id).await?;
}
// Send enter to new surface
let serial = client.next_event_serial();
tracing::debug!(
"Sending enter event with serial {} to surface {:?}",
serial,
surface.id
);
self.enter(
client,
self.id,
serial,
surface.id,
(position.x as f64).into(),
(position.y as f64).into(),
)
.await?;
// Update focused surface
*focused = Arc::downgrade(&surface);
}
// Send motion event to current surface
tracing::debug!("Sending motion event to surface");
self.motion(
client,
self.id,
0, // time
(position.x as f64).into(),
(position.y as f64).into(),
)
.await?;
if self.version >= 5 {
self.frame(client, self.id).await?;
}
Ok(())
}
pub async fn handle_pointer_button(
&self,
client: &mut Client,
surface: Arc<Surface>,
button: u32,
pressed: bool,
) -> WaylandResult<()> {
tracing::debug!(
"Handling pointer button {} {} on surface {:?}",
button,
if pressed { "pressed" } else { "released" },
surface.id
);
let serial = client.next_event_serial();
self.button(
client,
self.id,
serial,
0, // time
button,
if pressed {
ButtonState::Pressed
} else {
ButtonState::Released
},
)
.await?;
self.frame(client, self.id).await
}
pub async fn handle_pointer_scroll(
&self,
client: &mut Client,
_surface: Arc<Surface>,
scroll_distance: Option<Vector2<f32>>,
scroll_steps: Option<Vector2<f32>>,
) -> WaylandResult<()> {
tracing::debug!(
"Handling pointer scroll: distance={:?}, steps={:?}",
scroll_distance,
scroll_steps
);
if let Some(distance) = scroll_distance {
self.axis(
client,
self.id,
0, // time
Axis::HorizontalScroll,
(distance.x as f64).into(),
)
.await?;
self.axis(
client,
self.id,
0, // time
Axis::VerticalScroll,
(distance.y as f64).into(),
)
.await?;
}
if self.version < 8
&& self.version >= 5
&& let Some(steps) = scroll_steps
{
self.axis_discrete(client, self.id, Axis::HorizontalScroll, steps.x as i32)
.await?;
self.axis_discrete(client, self.id, Axis::VerticalScroll, steps.y as i32)
.await?;
}
if self.version >= 8
&& let Some(steps) = scroll_steps
{
self.axis_value120(
client,
self.id,
Axis::HorizontalScroll,
(steps.x * 120.) as i32,
)
.await?;
self.axis_value120(
client,
self.id,
Axis::VerticalScroll,
(steps.y * 120.) as i32,
)
.await?;
}
if self.version >= 5 {
self.frame(client, self.id).await?;
}
Ok(())
}
pub async fn reset(&self, client: &mut Client) -> WaylandResult<()> {
let mut focused = self.focused_surface.lock().await;
if let Some(old_surface) = focused.upgrade() {
let serial = client.next_event_serial();
self.leave(client, self.id, serial, old_surface.id).await?;
self.frame(client, self.id).await?;
}
*focused = Weak::new();
Ok(())
}
pub async fn cursor_surface(&self) -> Option<Arc<Surface>> {
self.cursor_surface.lock().await.clone()
}
}
impl WlPointer for Pointer {
type Connection = crate::wayland::Client;
/// https://wayland.app/protocols/wayland#wl_pointer:request:set_cursor
async fn set_cursor(
&self,
client: &mut Self::Connection,
_sender_id: ObjectId,
_serial: u32,
surface: Option<ObjectId>,
hotspot_x: i32,
hotspot_y: i32,
) -> WaylandResult<()> {
if let Some(focused_surface) = self.focused_surface.lock().await.upgrade()
&& let Some(panel_item) = focused_surface.panel_item.lock().upgrade()
{
panel_item.set_cursor(surface.and_then(|s| client.get::<Surface>(s)).map(|s| {
let size = s
.current_state()
.buffer
.map(|b| b.buffer.size())
.unwrap_or([16; 2].into());
Geometry {
origin: [hotspot_x, hotspot_y].into(),
size: [size.x as u32, size.y as u32].into(),
}
}));
}
let Some(surface) = surface else {
return Ok(());
};
let Some(surface) = client.get::<Surface>(surface) else {
return Ok(());
};
surface
.try_set_role(SurfaceRole::Cursor, Error::Role)
.await?;
self.cursor_surface.lock().await.replace(surface);
Ok(())
}
/// https://wayland.app/protocols/wayland#wl_pointer:request:release
async fn release(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
) -> WaylandResult<()> {
Ok(())
}
}

213
src/wayland/core/seat.rs Normal file
View File

@@ -0,0 +1,213 @@
use crate::wayland::Client;
use crate::wayland::WaylandResult;
use crate::wayland::core::{keyboard::Keyboard, pointer::Pointer, surface::Surface, touch::Touch};
use mint::Vector2;
use std::sync::Arc;
use std::sync::OnceLock;
use waynest::ObjectId;
pub use waynest_protocols::server::core::wayland::wl_seat::*;
use waynest_server::Client as _;
#[derive(Debug)]
pub enum SeatMessage {
PointerMotion {
surface: Arc<Surface>,
position: Vector2<f32>,
},
PointerButton {
surface: Arc<Surface>,
button: u32,
pressed: bool,
},
PointerScroll {
surface: Arc<Surface>,
scroll_distance: Option<Vector2<f32>>,
scroll_steps: Option<Vector2<f32>>,
},
KeyboardKey {
surface: Arc<Surface>,
keymap_id: u64,
key: u32,
pressed: bool,
},
TouchDown {
surface: Arc<Surface>,
id: u32,
position: Vector2<f32>,
},
TouchMove {
id: u32,
position: Vector2<f32>,
},
TouchUp {
id: u32,
},
Reset,
}
#[derive(Default, waynest_server::RequestDispatcher)]
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
pub struct Seat {
version: u32,
pointer: OnceLock<Arc<Pointer>>,
keyboard: OnceLock<Arc<Keyboard>>,
touch: OnceLock<Arc<Touch>>,
}
impl Seat {
pub async fn new(client: &mut Client, id: ObjectId, version: u32) -> WaylandResult<Self> {
let seat = Self {
version,
pointer: OnceLock::new(),
keyboard: OnceLock::new(),
touch: OnceLock::new(),
};
if version >= 2 {
seat.name(client, id, "theonlyseat".into()).await?;
}
tracing::debug!("Advertising seat capabilities with id {}", id);
let capabilities = Capability::Pointer | Capability::Keyboard | Capability::Touch;
WlSeat::capabilities(&seat, client, id, capabilities).await?;
tracing::debug!("Capabilities advertised: {:?}", capabilities);
Ok(seat)
}
pub async fn handle_message(
&self,
client: &mut Client,
message: SeatMessage,
) -> WaylandResult<()> {
match message {
SeatMessage::PointerMotion { surface, position } => {
if let Some(pointer) = self.pointer.get() {
pointer
.handle_pointer_motion(client, surface, position)
.await?;
}
}
SeatMessage::PointerButton {
surface,
button,
pressed,
} => {
if let Some(pointer) = self.pointer.get() {
pointer
.handle_pointer_button(client, surface, button, pressed)
.await?;
}
}
SeatMessage::PointerScroll {
surface,
scroll_distance,
scroll_steps,
} => {
if let Some(pointer) = self.pointer.get() {
pointer
.handle_pointer_scroll(client, surface, scroll_distance, scroll_steps)
.await?;
}
}
SeatMessage::KeyboardKey {
surface,
keymap_id,
key,
pressed,
} => {
if let Some(keyboard) = self.keyboard.get() {
keyboard
.handle_keyboard_key(client, surface, keymap_id, key - 8, pressed)
.await?;
}
}
SeatMessage::TouchDown {
surface,
id,
position,
} => {
if let Some(touch) = self.touch.get() {
touch
.handle_touch_down(client, surface, id, position)
.await?;
}
}
SeatMessage::TouchMove { id, position } => {
if let Some(touch) = self.touch.get() {
touch.handle_touch_move(client, id, position).await?;
}
}
SeatMessage::TouchUp { id } => {
if let Some(touch) = self.touch.get() {
touch.handle_touch_up(client, id).await?;
}
}
SeatMessage::Reset => {
if let Some(pointer) = self.pointer.get() {
pointer.reset(client).await?;
}
if let Some(keyboard) = self.keyboard.get() {
keyboard.reset(client).await?;
}
if let Some(touch) = self.touch.get() {
touch.reset(client).await?;
}
}
}
Ok(())
}
pub async fn cursor_surface(&self) -> Option<Arc<Surface>> {
self.pointer.get()?.cursor_surface().await
}
}
impl WlSeat for Seat {
type Connection = crate::wayland::Client;
/// https://wayland.app/protocols/wayland#wl_seat:request:get_pointer
async fn get_pointer(
&self,
client: &mut Self::Connection,
_sender_id: ObjectId,
id: ObjectId,
) -> WaylandResult<()> {
let pointer = client.insert(id, Pointer::new(id, self.version))?;
let _ = self.pointer.set(pointer);
Ok(())
}
/// https://wayland.app/protocols/wayland#wl_seat:request:get_keyboard
async fn get_keyboard(
&self,
client: &mut Self::Connection,
_sender_id: ObjectId,
id: ObjectId,
) -> WaylandResult<()> {
tracing::info!("Getting keyboard");
let keyboard = client.insert(id, Keyboard::new(id))?;
let _ = self.keyboard.set(keyboard);
Ok(())
}
/// https://wayland.app/protocols/wayland#wl_seat:request:get_touch
async fn get_touch(
&self,
client: &mut Self::Connection,
_sender_id: ObjectId,
id: ObjectId,
) -> WaylandResult<()> {
let touch = client.insert(id, Touch(id))?;
let _ = self.touch.set(touch);
Ok(())
}
/// https://wayland.app/protocols/wayland#wl_seat:request:release
async fn release(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
) -> WaylandResult<()> {
Ok(())
}
}

47
src/wayland/core/shm.rs Normal file
View File

@@ -0,0 +1,47 @@
use crate::wayland::{Client, WaylandResult, core::shm_pool::ShmPool};
use std::os::fd::OwnedFd;
use waynest::ObjectId;
pub use waynest_protocols::server::core::wayland::wl_shm::*;
use waynest_server::Client as _;
#[derive(Debug, waynest_server::RequestDispatcher, Default)]
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
pub struct Shm;
impl Shm {
pub async fn advertise_formats(
&self,
client: &mut Client,
sender_id: ObjectId,
) -> WaylandResult<()> {
self.format(client, sender_id, Format::Argb8888).await?;
self.format(client, sender_id, Format::Xrgb8888).await?;
Ok(())
}
}
impl WlShm for Shm {
type Connection = crate::wayland::Client;
/// https://wayland.app/protocols/wayland#wl_shm:request:create_pool
async fn create_pool(
&self,
client: &mut Self::Connection,
_sender_id: ObjectId,
pool_id: ObjectId,
fd: OwnedFd,
size: i32,
) -> WaylandResult<()> {
client.insert(pool_id, ShmPool::new(fd, size, pool_id)?)?;
Ok(())
}
/// https://wayland.app/protocols/wayland#wl_shm:request:release
async fn release(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
) -> WaylandResult<()> {
Ok(())
}
}

View File

@@ -0,0 +1,276 @@
use super::shm_pool::ShmPool;
use crate::wayland::{RENDER_DEVICE, vulkano_data::VULKANO_CONTEXT};
use bevy::{
asset::{Assets, Handle},
image::Image,
};
use bevy_dmabuf::{
dmatex::{Dmatex, DmatexPlane, Resolution},
import::{DmatexUsage, DropCallback, ImportedDmatexs, ImportedTexture, import_texture},
};
use drm_fourcc::DrmFourcc;
use mint::Vector2;
use parking_lot::Mutex;
use std::{
os::fd::OwnedFd,
sync::{Arc, OnceLock},
};
use tracing::debug_span;
use vulkano::{
buffer::BufferUsage,
command_buffer::{
AutoCommandBufferBuilder, CommandBufferUsage, CopyBufferToImageInfo,
PrimaryCommandBufferAbstract,
},
image::{
ImageAspect, ImageCreateFlags, ImageCreateInfo, ImageMemory, ImageTiling, ImageUsage,
sys::RawImage,
},
memory::{
DedicatedAllocation, DeviceMemory, ExternalMemoryHandleType, MemoryAllocateInfo,
ResourceMemory,
allocator::{AllocationCreateInfo, MemoryTypeFilter},
},
sync::GpuFuture,
};
use waynest_protocols::server::core::wayland::wl_shm::Format;
/// Parameters for a shared memory buffer
pub struct ShmBufferBacking {
pool: Arc<ShmPool>,
offset: usize,
stride: usize,
size: Vector2<usize>,
wl_format: Format,
image: Arc<vulkano::image::Image>,
tex: OnceLock<Handle<Image>>,
pending_imported_dmatex: Mutex<Option<ImportedTexture>>,
}
impl std::fmt::Debug for ShmBufferBacking {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShmBufferBacking")
.field("pool", &self.pool)
.field("offset", &self.offset)
.field("stride", &self.stride)
.field("size", &self.size)
.field("wl_format", &self.wl_format)
.field("image", &self.image)
.field("tex", &self.tex)
.finish()
}
}
impl ShmBufferBacking {
pub fn new(
pool: Arc<ShmPool>,
offset: usize,
stride: usize,
size: Vector2<usize>,
wl_format: Format,
) -> Self {
let vk = VULKANO_CONTEXT.wait();
let format = match wl_format {
Format::Argb8888 | Format::Xrgb8888 => vulkano::format::Format::B8G8R8A8_SRGB,
_ => unimplemented!(),
};
let modifiers = vk
.phys_dev
.format_properties(format)
.unwrap()
.drm_format_modifier_properties
.into_iter()
.filter_map(|v| {
(v.drm_format_modifier_plane_count == 1).then_some(v.drm_format_modifier)
})
.collect();
let raw_image = RawImage::new(
vk.dev.clone(),
ImageCreateInfo {
flags: ImageCreateFlags::empty(),
image_type: vulkano::image::ImageType::Dim2d,
format,
extent: [size.x as u32, size.y as u32, 1],
tiling: ImageTiling::DrmFormatModifier,
usage: ImageUsage::TRANSFER_DST,
drm_format_modifiers: modifiers,
external_memory_handle_types: ExternalMemoryHandleType::DmaBuf.into(),
..Default::default()
},
)
.unwrap();
let (modifier, num_planes) = raw_image.drm_format_modifier().unwrap();
let mem_reqs = raw_image.memory_requirements()[0];
let index = vk
.phys_dev
.memory_properties()
.memory_types
.iter()
.enumerate()
.map(|(i, _v)| i as u32)
.find(|i| mem_reqs.memory_type_bits & (1 << i) != 0)
.expect("no valid memory type");
let mem = ResourceMemory::new_dedicated(
DeviceMemory::allocate(
vk.dev.clone(),
MemoryAllocateInfo {
allocation_size: mem_reqs.layout.size(),
memory_type_index: index,
dedicated_allocation: Some(DedicatedAllocation::Image(&raw_image)),
export_handle_types: ExternalMemoryHandleType::DmaBuf.into(),
..Default::default()
},
)
.unwrap(),
);
let Ok(image) = raw_image.bind_memory([mem]) else {
panic!("unable to bind memory")
};
let image = Arc::new(image);
let ImageMemory::Normal(mem) = image.memory() else {
unreachable!()
};
let [mem] = mem.as_slice() else {
unreachable!()
};
let fd = OwnedFd::from(
mem.device_memory()
.export_fd(ExternalMemoryHandleType::DmaBuf)
.unwrap(),
);
let planes = (0..num_planes)
.filter_map(|i| {
Some(match i {
0 => ImageAspect::MemoryPlane0,
1 => ImageAspect::MemoryPlane1,
2 => ImageAspect::MemoryPlane2,
3 => ImageAspect::MemoryPlane3,
_ => return None,
})
})
.map(|aspect| {
let plane_layout = image.subresource_layout(aspect, 0, 0).unwrap();
DmatexPlane {
dmabuf_fd: fd.try_clone().unwrap().into(),
modifier,
offset: plane_layout.offset as u32,
stride: plane_layout.row_pitch as i32,
}
})
.collect::<Vec<_>>();
let dmatex = Dmatex {
planes,
res: Resolution {
x: size.x as u32,
y: size.y as u32,
},
format: DrmFourcc::Argb8888 as u32,
flip_y: false,
srgb: true,
};
let imported_dmatex = import_texture(
RENDER_DEVICE.wait(),
dmatex,
DropCallback(None),
DmatexUsage::Sampling,
)
.unwrap();
Self {
pool,
offset,
stride,
size,
wl_format,
image,
pending_imported_dmatex: Mutex::new(Some(imported_dmatex)),
tex: OnceLock::new(),
}
}
pub fn on_commit(&self) {
let vk = VULKANO_CONTEXT.wait();
let data_len = self.size.x * self.size.y * 4;
let gpu_buffer = vulkano::buffer::Buffer::new_slice::<u8>(
vk.alloc.clone(),
vulkano::buffer::BufferCreateInfo {
usage: BufferUsage::TRANSFER_SRC,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
..Default::default()
},
data_len as u64,
)
.unwrap();
{
let _span = debug_span!("copy to gpu buffer").entered();
let shm_data_lock = self.pool.data_lock();
let mut gpu_slice = gpu_buffer.write().unwrap();
for (shm_offset, gpu_offset) in
(0..self.size.y).map(|v| (self.offset + (v * self.stride), (v * (self.size.x * 4))))
{
let line_slice = &shm_data_lock[shm_offset..(shm_offset + (self.size.x * 4))];
let gpu_subslice = &mut gpu_slice[gpu_offset..(gpu_offset + (self.size.x * 4))];
gpu_subslice.copy_from_slice(line_slice);
}
}
let mut command_buffer = AutoCommandBufferBuilder::primary(
vk.command_buffer_alloc.clone(),
vk.queue.queue_family_index(),
CommandBufferUsage::OneTimeSubmit,
)
.unwrap();
command_buffer
.copy_buffer_to_image(CopyBufferToImageInfo::buffer_image(
gpu_buffer.clone(),
self.image.clone(),
))
.unwrap();
let command_buffer = command_buffer.build().unwrap();
command_buffer
.execute(vk.queue.clone())
.unwrap()
.then_signal_fence_and_flush()
.unwrap()
.wait(None)
.unwrap();
}
#[tracing::instrument("debug", skip_all)]
pub fn update_tex(
&self,
dmatexes: &ImportedDmatexs,
images: &mut Assets<Image>,
) -> Option<Handle<Image>> {
self.pending_imported_dmatex
.lock()
.take()
.map(|tex| dmatexes.insert_imported_dmatex(images, tex))
.inspect(|handle| {
_ = self.tex.set(handle.clone());
});
self.tex.get().cloned()
}
pub fn is_transparent(&self) -> bool {
match self.wl_format {
Format::Xrgb8888 => false,
Format::Argb8888 => true,
_ => true,
}
}
pub fn size(&self) -> Vector2<usize> {
self.size
}
}

View File

@@ -0,0 +1,93 @@
use super::shm_buffer_backing::ShmBufferBacking;
use crate::wayland::{
Client, WaylandResult,
core::buffer::{Buffer, BufferBacking},
};
use memmap2::{MmapOptions, RemapOptions};
use parking_lot::{Mutex, MutexGuard, RawMutex, lock_api::MappedMutexGuard};
use std::os::fd::{AsRawFd, OwnedFd};
use waynest::ObjectId;
use waynest_protocols::server::core::wayland::wl_shm::Format;
pub use waynest_protocols::server::core::wayland::wl_shm_pool::*;
use waynest_server::Client as _;
#[derive(Debug, waynest_server::RequestDispatcher)]
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
pub struct ShmPool {
inner: Mutex<memmap2::MmapMut>,
id: ObjectId,
}
impl ShmPool {
#[tracing::instrument(level = "debug", skip_all)]
pub fn new(fd: OwnedFd, size: i32, id: ObjectId) -> WaylandResult<Self> {
let map = unsafe {
MmapOptions::new()
.len(size as usize)
.map_mut(fd.as_raw_fd())?
};
Ok(Self {
inner: Mutex::new(map),
id,
})
}
#[tracing::instrument(level = "debug", skip_all)]
pub fn data_lock(&self) -> MappedMutexGuard<'_, RawMutex, [u8]> {
MutexGuard::map(self.inner.lock(), |i| i.as_mut())
}
}
impl WlShmPool for ShmPool {
type Connection = Client;
/// https://wayland.app/protocols/wayland#wl_shm_pool:request:create_buffer
#[tracing::instrument(level = "debug", skip_all)]
async fn create_buffer(
&self,
client: &mut Self::Connection,
sender_id: ObjectId,
id: ObjectId,
offset: i32,
width: i32,
height: i32,
stride: i32,
format: Format,
) -> WaylandResult<()> {
let params = ShmBufferBacking::new(
client.get::<ShmPool>(sender_id).unwrap(),
offset as usize,
stride as usize,
[width as usize, height as usize].into(),
format,
);
Buffer::new(client, id, BufferBacking::Shm(params))?;
Ok(())
}
/// https://wayland.app/protocols/wayland#wl_shm_pool:request:resize
#[tracing::instrument(level = "debug", skip_all)]
async fn resize(
&self,
_client: &mut Self::Connection,
_sender_id: ObjectId,
size: i32,
) -> WaylandResult<()> {
let mut inner = self.inner.lock();
unsafe { inner.remap(size as usize, RemapOptions::new().may_move(true))? };
Ok(())
}
/// https://wayland.app/protocols/wayland#wl_shm_pool:request:destroy
#[tracing::instrument(level = "debug", skip_all)]
async fn destroy(
&self,
client: &mut Self::Connection,
_sender_id: ObjectId,
) -> WaylandResult<()> {
client.remove(self.id);
Ok(())
}
}

Some files were not shown because too many files have changed in this diff Show More