Compare commits

...
Sign in to create a new pull request.

161 commits

Author SHA1 Message Date
e223bf669b
lua: set expose_table_wrapper to pub, add requries in some lua type defs
Some checks failed
CI / test (ubuntu-latest, nightly) (push) Failing after 4m51s
CI / build (ubuntu-latest, nightly) (push) Successful in 4m52s
2025-02-10 20:57:19 -05:00
092fcb4b40
lua: create type defs for Rect, animation sheet, and tilemap related APIs
Some checks failed
CI / test (ubuntu-latest, nightly) (push) Failing after 2m36s
CI / build (ubuntu-latest, nightly) (push) Successful in 2m43s
2025-01-29 20:03:31 -05:00
bb120e67f6
lua: fix Pivot.CUSTOM enum, create ImageHandle type def
Some checks failed
CI / build (ubuntu-latest, nightly) (push) Successful in 2m57s
CI / test (ubuntu-latest, nightly) (push) Failing after 3m22s
2025-01-28 21:28:04 -05:00
a8f6e5e380
engine: custom controls for TopDown2dCamera
Some checks failed
CI / test (ubuntu-latest, nightly) (push) Failing after 2m38s
CI / build (ubuntu-latest, nightly) (push) Successful in 2m57s
2025-01-27 21:38:39 -05:00
f468abc597
scripting: implement Default for any T for ScriptContexts<T>
Some checks failed
CI / test (ubuntu-latest, nightly) (push) Failing after 2m23s
CI / build (ubuntu-latest, nightly) (push) Successful in 2m39s
2025-01-25 09:52:02 -05:00
2d1bcb0b3b lua: add macro command for skipping reflect implementation
Some checks failed
CI / build (ubuntu-latest, nightly) (push) Successful in 4m5s
CI / test (ubuntu-latest, nightly) (push) Failing after 4m12s
2025-01-23 17:46:33 -05:00
173d957f25
asset: implement Reflect for the ResourceManager
Some checks failed
CI / build (ubuntu-latest, nightly) (push) Successful in 3m5s
CI / test (ubuntu-latest, nightly) (push) Failing after 3m14s
2025-01-11 20:47:36 -05:00
9fc3d1eaaa
lua: implement adding lua functions to types that use the LuaConvert derive macro
Some checks failed
CI / build (ubuntu-latest, nightly) (push) Successful in 2m46s
CI / test (ubuntu-latest, nightly) (push) Failing after 2m55s
2025-01-11 18:20:37 -05:00
d0f337b113
lua: export VecN::distance 2025-01-11 18:20:07 -05:00
2ecb48c89e
lua: create LuaConvert derive macro
Some checks failed
CI / test (ubuntu-latest, nightly) (push) Failing after 2m36s
CI / build (ubuntu-latest, nightly) (push) Successful in 2m50s
2025-01-10 11:18:14 -05:00
8248c4918a
lua: improve World:view so the user doesn't have to manually create a view
Some checks failed
CI / test (ubuntu-latest, nightly) (push) Failing after 2m56s
CI / build (ubuntu-latest, nightly) (push) Successful in 3m7s
2025-01-07 21:10:35 -05:00
7cc1ff262a
lua: replace LuaViewOneResult with a get_one method on LuaViewREsult
Some checks failed
CI / build (ubuntu-latest, nightly) (push) Successful in 2m48s
CI / test (ubuntu-latest, nightly) (push) Failing after 4m27s
2025-01-07 20:39:13 -05:00
ba4acba638
lua: remove ReflectBranch::as_component_unchecked and improve error handling
Some checks failed
CI / test (ubuntu-latest, nightly) (push) Failing after 2m32s
CI / build (ubuntu-latest, nightly) (push) Successful in 2m44s
2025-01-07 19:18:00 -05:00
b2b19c9ccc
Use full paths for items used in wrapper proc-macros 2025-01-05 21:22:15 -05:00
4fd4bbe574
expose sprite related things to lua, improve wrapper macros
Some checks failed
CI / build (ubuntu-latest, nightly) (push) Successful in 2m48s
CI / test (ubuntu-latest, nightly) (push) Failing after 2m48s
2025-01-05 19:20:02 -05:00
ac4f00568a
lua: create LuaSprite and lua type bindings
Some checks failed
CI / build (ubuntu-latest, nightly) (push) Successful in 2m38s
CI / test (ubuntu-latest, nightly) (push) Failing after 2m38s
2025-01-04 16:09:26 -05:00
ca7be93505
engine: remove lifetimes from EventReader, fixes issues for lua scripting
Some checks failed
CI / test (ubuntu-latest, nightly) (push) Failing after 2m40s
CI / build (ubuntu-latest, nightly) (push) Successful in 2m41s
2025-01-03 22:41:35 -05:00
467bdd6d13
scripting: fix forgotten rename of LuaFreeFly3dCamera 2025-01-03 22:40:32 -05:00
6f65e66bf2
ci: remove last version of nightly, try to fix cache
Some checks failed
CI / test (ubuntu-latest, nightly) (push) Failing after 3m42s
CI / build (ubuntu-latest, nightly) (push) Successful in 4m33s
2025-01-02 23:51:28 -05:00
3756c2f466
Add last nightly version to ci, fix tons of compiler time errors in tests
Some checks failed
CI / build (ubuntu-latest, 1.84.0-nightly) (push) Failing after 2m41s
CI / build (ubuntu-latest, nightly) (push) Successful in 4m22s
CI / test (ubuntu-latest, 1.84.0-nightly) (push) Has been skipped
CI / test (ubuntu-latest, nightly) (push) Has been skipped
2025-01-02 23:32:59 -05:00
c04b11985a
Fix CI
Some checks failed
CI / build (ubuntu-latest, nightly) (push) Successful in 3m18s
CI / test (ubuntu-latest, nightly) (push) Failing after 2m36s
2025-01-02 13:58:48 -05:00
f9c077bdef
sprite: update tile sprites when their atlas index changes
Some checks failed
CI / build (push) Failing after 2m57s
2025-01-01 21:17:36 -05:00
3cbc1933c3
ecs: implement removing components from entity 2024-12-31 22:40:37 -05:00
f9e42b1aaa
increase sprite limit in sprite pass, use TransformBudle in TileMap impl
Some checks failed
CI / build (push) Failing after 2m43s
2024-12-31 11:59:26 -05:00
8629433211
render: make EitherTransform ecs query public
Some checks failed
CI / build (push) Failing after 3m2s
2024-12-26 19:42:12 -05:00
32f16e4d10
render: sort transparent sprites to render last 2024-12-23 19:07:04 -05:00
4ecf09f0b3
render: sprite sorting with optional offset
Some checks failed
CI / build (push) Failing after 3m4s
2024-12-21 22:25:11 -05:00
3298d17e61
Create related_by query, remove tracing feature from examples
Some checks failed
CI / build (push) Failing after 6s
2024-12-21 14:31:10 -05:00
c3330c044b
scene: create TransformBundle
Some checks failed
CI / build (push) Failing after 4m16s
2024-12-20 21:35:33 -05:00
661f91625c
scene: use new Without ecs query instead of Not<With<T>>
Some checks failed
CI / build (push) Failing after 3m26s
2024-12-19 18:08:28 -05:00
9eb19f2e2f
Remove tracing subscriber from game crate, rename Has<C> to With<C> create Without<C> ecs filters
Some checks failed
CI / build (push) Failing after 11m8s
2024-12-18 22:08:58 -05:00
e99153a53d
game: fix latency of input events and repeated events
Some checks failed
CI / build (push) Failing after 3m14s
2024-12-17 21:42:35 -05:00
d4fc3000f1
Implement optional sprite animation auto looping
Some checks failed
CI / build (push) Failing after 2m56s
2024-12-01 18:15:27 -05:00
275fdbc8d8
Implement padding and offset for TextureAtlas::from_grid, specify sprite pivot for atlas animations
Some checks failed
CI / build (push) Failing after 2m52s
2024-12-01 11:36:50 -05:00
caf410a4c6 Merge branch 'feat/rendering-2d-31' into 'main'
Some checks failed
CI / build (push) Failing after 2m51s
2024-11-30 21:39:21 -05:00
d2062fab99
ecs: create DynTypeId::as_unknown
Some checks failed
CI / build (push) Failing after 3m8s
Signed-off-by: SeanOMik <seanomik@gmail.com>
2024-11-30 20:01:38 -05:00
286cd9a914 remove InterpTransform, create EitherTransform query, some code cleanup
Some checks failed
CI / build (pull_request) Failing after 4m21s
2024-11-30 22:12:17 +00:00
863a1d0cbf ecs: add doc comments 2024-11-30 22:12:17 +00:00
4a70b3c026 ecs: rewrite Or to be a Filter and Query 2024-11-30 22:12:17 +00:00
5a0e06f94d Fix Changed<T> by rewriting ecs Filters, position TileMapPos entities with ChildOf relations 2024-11-30 22:12:17 +00:00
418765d595 render: use depth buffer 2024-11-30 22:12:17 +00:00
fa6511bff1 render: create TileMap with a 'RelativeToTile' component to position entities along the grid 2024-11-30 22:12:17 +00:00
4afd518f45 render: remove unused field from SpriteInstance struct on gpu 2024-11-30 22:12:17 +00:00
e1f48d525a render: implement sprite pivot, fix sprite centering in ortho projection 2024-11-30 22:12:17 +00:00
3c3025668a Make sprite texture atlas more generic 2024-11-30 22:12:17 +00:00
7e9ece83a2 Format some code, update wgsl-preprocessor crate 2024-11-30 22:12:17 +00:00
05ed1f1135 2d: support animating from ResHandle<AtlasAnimations>, not just non-asset handles 2024-11-30 22:12:17 +00:00
af2d7b4980 2d: create an animation sheet component 2024-11-30 22:12:17 +00:00
e3b0b1de8f implement texture atlases for sprites, allow storage of assets not from a loader 2024-11-30 22:12:17 +00:00
b78101718e game: create TopDown2dCamera controller, fix warning 2024-11-30 22:12:17 +00:00
c73ba3b943 rename FreeFlyCamera to FreeFly3dCamera 2024-11-30 22:12:17 +00:00
b2f0abf34d render: add more option for scaling modes of orthographic projection 2024-11-30 22:12:17 +00:00
c14c46f75d Return Result for asset handle wait_for_load, create shader asset loader that uses the preprocessor 2024-11-30 22:12:17 +00:00
865fbf9b91 update examples to use new camera bundles 2024-11-30 22:12:17 +00:00
6a09b64902 ecs: implement Bundle traits for structs 2024-11-30 22:12:17 +00:00
c4e5147967 render: fix 2d camera 2024-11-30 22:12:17 +00:00
6b9561d9bd render: implement 2d sprite rendering 2024-11-30 22:12:17 +00:00
62adcf2b50 ecs: create DynTypeId::as_unknown
Some checks failed
CI / build (push) Failing after 6m11s
2024-11-06 21:23:01 -05:00
4816b7333e
Make system_update_world_transforms also update world transforms for SceneGraphs
Some checks failed
CI / build (push) Failing after 2m53s
Before this couldn't be done since lyra-scene could not depend on lyra-resource, it would have caused a cyclic dependency. The last commit, where I created lyra-gltf from code inside lyra-resource, made this possible
2024-11-01 19:45:29 -04:00
5542467d7e
separate GLTF loader to its own crate 2024-11-01 12:09:01 -04:00
3ce9ab6fb3
move crates into 'crates' folder 2024-11-01 11:17:36 -04:00
f02d3c6b2f
render: create a transform pass for sending transforms to the GPU
Some checks failed
CI / build (push) Failing after 3m33s
2024-11-01 11:05:51 -04:00
7ae0eae6ac Merge pull request 'Improve Lua ECS' (#30) from feat/improve-lua-ecs-29 into main
Some checks failed
CI / build (push) Failing after 3m52s
Reviewed-on: #30
2024-10-29 23:22:19 -04:00
fae2cdfadc
lua: remove old implementation of world:view, replacing it with the new version
Some checks failed
CI / build (pull_request) Failing after 4m7s
2024-10-29 23:20:52 -04:00
076676e486
lua: write lua annotations for all queries and view one
Some checks failed
CI / build (pull_request) Failing after 3m6s
2024-10-29 23:04:00 -04:00
0e613bd216
lua: implement world:view_one for lua 2024-10-29 21:56:07 -04:00
964c4ec423
lua: create LuaOptionalQuery 2024-10-29 14:22:03 -04:00
23a215ba46
lua: create LuaTickOfQuery 2024-10-29 09:32:32 -04:00
42112c2cf1
lua: create LuaOrQuery 2024-10-29 09:19:54 -04:00
f2ff2a9855
lua: start using LuaQueryResult in all lua ecs queries 2024-10-29 09:10:14 -04:00
7c2efe3c6f
lua: create LuaNotQuery 2024-10-29 08:48:30 -04:00
cb3c3a601f
lua: create LuaQueryResult for simplying implementation of queries and filters 2024-10-23 16:47:45 -04:00
8072ec1c7e
lua: create LuaHasQuery 2024-10-23 16:31:47 -04:00
4dbd96832f
lua: add world:get_tick() to lua, write some missing type annotations
I tried to use generics to improve existing annotations, but I don't think it changed anything since the annotations for generics are kind of garbage
2024-10-21 21:58:22 -04:00
2e33de5da2
lua: implement Changed query that supports components and resources 2024-10-20 21:20:43 -04:00
74465ce614
lua: code cleanup 2024-10-19 20:51:54 -04:00
380b15e560
lua: implement ecs queries that work with the new Views 2024-10-19 20:42:28 -04:00
2ffdd4085b
lua: create Views that can query from the world 2024-10-19 17:45:59 -04:00
156cbf25a4
fix ci by ignoring tracy tsc check
Some checks failed
CI / build (push) Failing after 3m13s
2024-10-19 11:48:24 -04:00
b2d259ac71 Merge pull request 'Expose structs to Lua and write Lua type annotations' (#28) from feat/lua-type-defs into main
Some checks failed
CI / build (push) Failing after 2m43s
Reviewed-on: #28
2024-10-19 11:16:33 -04:00
d001e136d0
lua: expose WorldTransform
Some checks failed
CI / build (pull_request) Failing after 3m20s
2024-10-17 17:11:46 -04:00
d0e6fc6ecd
lua: make it easier to expose events and asset handle wrappers 2024-10-13 12:30:06 -04:00
6a47cd2671
lua: expose DeviceEvent 2024-10-13 11:43:49 -04:00
8e56ee1f0f
lua: start exposing events 2024-10-11 20:49:00 -04:00
9e9478966b
lua: cleanup 2024-10-09 12:06:08 -04:00
624cd5362f
lua: change lyra-scripting path in lyra-engine crate 2024-10-09 11:08:21 -04:00
eff6b221e0
remove unused code, cleanup some warnings 2024-10-09 10:56:54 -04:00
77ec620adb
lua: remove unused fields in FreeFlyCamera 2024-10-09 10:30:45 -04:00
6f65e2ce35
lua: add lua type defs for FreeFlyCamera and change name of field 2024-10-08 20:49:57 -04:00
b90e19161d
lua: expose FreeFlyCamera 2024-10-07 16:28:38 -04:00
e9cbb48653
lua: expose camera, support ToLua and FromLua structs in World:view 2024-10-07 15:20:13 -04:00
49dfb38da3
lua: expose fields on some types from lyra_resource 2024-10-05 13:46:53 -04:00
140ca506d6
lua: create type defs for World, Entity, ActionHandler, all asset handlers, add globals file 2024-10-04 23:48:58 -04:00
06a4301c23
lua: create type defs for Vec2, Vec3, Vec4, Quat, Transform, and DeltaTime 2024-10-04 15:07:42 -04:00
de14b6211b
lua: create type defs for Window and start on Vec2 2024-10-03 19:07:11 -04:00
a2c52a0bb8
ecs: fix Changed query; lua: lock and hide mouse in window
Some checks failed
CI / build (push) Failing after 3m10s
2024-10-02 21:29:13 -04:00
76b7cac699
lua: expose most fields for window component 2024-10-02 20:54:54 -04:00
64099f598c
fix ci test step
Some checks failed
CI / build (push) Failing after 2m35s
2024-09-30 19:59:26 -04:00
958c86cf73 Merge pull request 'Fix #19: Lua crashes when spawning entities in optimized builds' (#27) from fix/scripting-switch-to-mlua into main
Some checks failed
CI / build (push) Failing after 3m44s
Reviewed-on: #27
2024-09-29 21:39:20 -04:00
ef2b0bf326
ecs,scripting: fix invalid resources being passed to lua
Some checks failed
CI / build (pull_request) Failing after 3m35s
The issue was World::get_resource_ptr, it was returning a pointer to the AtomicRefCell instead of the actual resource data
2024-09-29 14:35:24 -04:00
fa22a0310c
scripting: switch to latest mlua, create custom impl of lua's getmetatable 2024-09-29 15:59:48 -04:00
02f0c93aa2
game: fix some unhandled device events causing panics 2024-09-28 22:05:57 -04:00
8fb686b7fe
scripting: switch to mlua, scripts need to be tested and fixed
currently the lua-scripting example doesnt work. For some reason the userdata's metatable is a boolean...
2024-09-28 12:32:37 -04:00
798719a7a2
game: remove unused enum InputEvent, remove some warnings 2024-09-27 21:09:33 -04:00
d6d6b2df72
game: improve event system 2024-09-27 21:03:57 -04:00
f5aca87ede
ecs: don't automatically tick the world, use Res and ResMut anywhere ecs resources are requested to track changes better
now the user must manually tick the world. The engine will do this before every update
2024-09-27 21:03:27 -04:00
9b1cc8c364
game: improve event handling, update input systems to use new event handling
Some checks failed
CI / build (push) Failing after 12m37s
2024-09-24 20:43:08 -04:00
9125b91977
ecs: add WorldTick query, implement IntoSystem and FnSystem for funcs with 11 args
accidentially missed the macro call for 11 arguments
2024-09-24 20:30:37 -04:00
eb43fad6c7 Merge pull request 'Update wgpu to 0.20.1 and winit to 0.30.3' (#26) from chore/winit-wgpu-update into main
Some checks failed
CI / build (push) Failing after 14m58s
Reviewed-on: #26
2024-09-23 19:06:26 -04:00
7219013593
delete Cargo.lock.old
Some checks failed
CI / build (pull_request) Failing after 3m18s
2024-09-23 18:53:32 -04:00
33ddf689be
game: create sync window system, handle more window events in the winit plugin 2024-09-22 21:17:40 -04:00
393b4206d3
game: start on updated WindowOptions component and window_sync_system 2024-09-21 14:09:24 -04:00
782d64f6cf
ecs: implement an actual Filter trait, create a Changed filter 2024-09-21 14:06:21 -04:00
2107b8f7b0
engine: move winit ApplicationHandler to winit plugin 2024-09-19 17:30:30 -04:00
8b1077cab7
engine: get a window showing and things rendered 2024-09-18 21:45:15 -04:00
45fd190409
update wgpu and winit to latest versions
need to make a WinitPlugin though, so no window currently
2024-09-18 19:47:55 -04:00
2b44d7f354 render: implement wgsl-preprocessor, split shaders
Some checks failed
CI / build (push) Failing after 7m56s
2024-09-14 20:08:18 -04:00
60c139f9b2 ecs: create DynamicViewOne
Some checks failed
CI / build (push) Failing after 11m20s
2024-09-02 20:34:02 -04:00
256025849e Merge pull request 'Implement Shadows' (#24) from feat/shadow-maps into main
All checks were successful
CI / build (push) Successful in 3m38s
Reviewed-on: #24
2024-08-09 23:10:28 -04:00
8545e7e27d
render: rewrite PCF for spot lights to somehow fix PCSS directional lights
All checks were successful
CI / build (pull_request) Successful in 9m48s
2024-08-09 22:01:57 -04:00
a85178eeea Revert "render: shadow maps and PCF for spot lights"
This reverts commit 8c1738334c.
2024-08-09 21:51:56 -04:00
8c1738334c
render: shadow maps and PCF for spot lights 2024-07-24 20:10:32 -04:00
fefcf58765
render: make shadow depth bias configurable per light source 2024-07-21 21:53:02 -04:00
b0a6d30afc
render: fix directional light shadows 2024-07-21 21:09:29 -04:00
fef709d5f1
render: implement PCF for point lights, support per-light shadow settings 2024-07-21 12:02:35 -04:00
c91ee67961
render: improve shadow settings to make it possible to switch between PCF, PCSS, hardware 2x2 PCF, or disable filtering all together 2024-07-19 17:56:27 -04:00
c961568b96
render: update the shadow filting poisson disc when shadow settings are modified 2024-07-19 16:07:40 -04:00
54b47c2178
ecs: implement change tracking for world resources 2024-07-19 16:07:03 -04:00
4449172c2b
render: implement PCSS for directional lights 2024-07-18 23:43:08 -04:00
4c6c6c4dd5
render: PCF with poisson disc on directional lights 2024-07-14 22:14:08 -04:00
27bc88c5a7
render: pass shadow settings to gpu 2024-07-14 19:46:15 -04:00
ff06bd55f3
render: simple PCF 2024-07-14 19:06:38 -04:00
d02258224a
render: fix bug with texture atlas not packing textures in last column 2024-07-14 12:24:13 -04:00
b45c2f4fab
render: point light shadows in texture atlas, fix bug with unaligned GpuSlotBuffer 2024-07-13 00:56:09 -04:00
40fa9c09da
render: fix shadow map atlas packing by writing my own skyline packer 2024-07-12 14:58:18 -04:00
87aa440691
render: create a GpuSlotBuffer for stable indices in a gpu buffer 2024-07-11 20:00:46 -04:00
cc1c482c40
render: provide shadow texture atlas frame for each shadow casting light 2024-07-11 18:27:26 -04:00
a4ce4cb432
render: implement packed texture atlas for shadow maps 2024-07-10 20:16:21 -04:00
e2b554b4ef
render: implement simple texture atlas for the shadow maps 2024-07-05 17:29:38 -04:00
6d57b40629
render: cull back faces, code cleanup to fix warnings 2024-07-04 23:28:21 -04:00
fd65f754cf
render: get simple directional shadow maps working 2024-07-04 13:43:36 -04:00
6c6893149a
render: bind direction light projection matrix to meshes shader 2024-06-30 21:58:08 -04:00
1c649b2eb6
render: bind the shadow map atlas to the meshes shaders 2024-06-30 21:42:08 -04:00
7b2d2424a3
render: start moving to a shadow map atlas texture and expose the resources as slots 2024-06-30 20:56:41 -04:00
e8974bbd44
render: create a depth map for the directional light 2024-06-30 19:33:51 -04:00
3a80c069c9
render: move most of the mesh processing to a MeshPrepare node
Moving that out of the MeshesPass makes the rendering meshes accessible to other passes/nodes. The shadow pass will need access to them which is why this was done now
2024-06-29 22:23:49 -04:00
7ff67a194b
create an example for testing shadow maps 2024-06-28 16:15:21 -04:00
c4aebdb25d
render: implement fxaa (#8)
All checks were successful
CI / build (push) Successful in 3m33s
2024-06-28 15:26:14 -04:00
5ebbec8cf9
ci: switch to ForgeJo actions
All checks were successful
CI / build (push) Successful in 3m33s
2024-06-28 13:50:26 -04:00
2eeca335e2
game: run clippy 2024-06-28 13:25:48 -04:00
96dea5b1f9
render: code cleanup 2024-06-28 13:16:47 -04:00
007b1047ef
render: implement view target chains for post processing steps 2024-06-27 23:48:24 -04:00
545da71cda
render: create ViewTarget to make it easier to render to a non-surface texture and chain together postprocess steps 2024-06-26 21:33:39 -04:00
0e71c5734f
render: make it easier to get Frame from RenderTarget 2024-06-26 17:14:31 -04:00
6c1bff5768
render: get sub render graphs working and create a simple test of them 2024-06-25 21:32:29 -04:00
5f1a61ef52
render: create RenderTarget and Frame, making it easier to render to a non-surface texture 2024-06-23 20:25:57 -04:00
f755a4c53b
render: create a node for sub render graphs, create a graph init node 2024-06-22 17:00:32 -04:00
9d3de88c50
render: cleanup 2024-06-21 22:10:35 -04:00
f440f306be
render: impl Node for RenderGraph in prep for sub-graphs 2024-06-21 22:06:58 -04:00
f17bf40c77
render: rename RenderGraph::add_pass to add_node 2024-06-21 21:58:40 -04:00
5fc1a0f134
Add todo panic in code when window is resized 2024-06-16 20:05:04 -04:00
372 changed files with 27888 additions and 10535 deletions

View file

@ -0,0 +1,76 @@
name: CI
env:
# Runners don't expose the TSC but we want to make sure these tests work, so we
# can ignore it.
TRACY_NO_INVARIANT_CHECK: 1
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ${{ matrix.os }}
#container: git.seanomik.net/seanomik/rust-nightly:2023-11-21-bookworm
strategy:
matrix:
os: [ubuntu-latest]
rust: [nightly]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-build-${{ matrix.rust }}-${{ hashFiles('**/Cargo.toml') }}
- name: Install system dependencies
run: |
apt update
apt install libudev-dev lua5.4 liblua5.4-dev -y
- uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
- name: Build
run: |
cargo build
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
rust: [nightly]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-test-${{ matrix.rust }}-${{ hashFiles('**/Cargo.toml') }}
- name: Install system dependencies
run: |
apt update
apt install libudev-dev lua5.4 liblua5.4-dev -y
- uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
- name: Build and run tests
run: |
cargo test --all

5
.gitmodules vendored
View file

@ -1,3 +1,6 @@
[submodule "lyra-scripting/elua"]
path = lyra-scripting/elua
path = crates/lyra-scripting/elua
url = ../elua.git # git@git.seanomik.net:SeanOMik/elua.git
[submodule "wgsl-preprocessor"]
path = crates/wgsl-preprocessor
url = git@git.seanomik.net:SeanOMik/wgsl-preprocessor.git

54
.vscode/launch.json vendored
View file

@ -4,6 +4,42 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug lyra dim_2d example",
"cargo": {
"args": [
"build",
"--manifest-path", "${workspaceFolder}/examples/2d/Cargo.toml"
//"--bin=testbed",
],
"filter": {
"name": "dim_2d",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}/examples/2d"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug lyra lua-scripting",
"cargo": {
"args": [
"build",
"--manifest-path", "${workspaceFolder}/examples/lua-scripting/Cargo.toml"
//"--bin=testbed",
],
"filter": {
"name": "lua-scripting",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}/examples/lua-scripting"
},
{
"type": "lldb",
"request": "launch",
@ -22,6 +58,24 @@
"args": [],
"cwd": "${workspaceFolder}/examples/testbed"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug lyra shadows",
"cargo": {
"args": [
"build",
"--manifest-path", "${workspaceFolder}/examples/shadows/Cargo.toml"
//"--bin=shadows",
],
"filter": {
"name": "shadows",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}/examples/shadows"
},
{
"type": "lldb",
"request": "launch",

View file

@ -1,21 +0,0 @@
variables:
- &rust_image 'git.seanomik.net/seanomik/rust-nightly:2023-11-21-bookworm'
when:
event: [push, manual, pull_request]
branch: main
steps:
Build - Debug:
image: *rust_image
commands:
- apt update
- apt install libudev-dev lua5.4 liblua5.4-dev -y
- cargo build
Test - Debug:
image: *rust_image
commands:
- apt update
- apt install libudev-dev lua5.4 liblua5.4-dev -y
- cargo test --all

View file

@ -1,20 +0,0 @@
variables:
- &rust_image 'git.seanomik.net/seanomik/rust-nightly:2023-11-21-bookworm'
when:
event: [release, pull_request, manual]
steps:
Build - Release:
image: *rust_image
commands:
- apt update
- apt install libudev-dev lua5.4 liblua5.4-dev -y
- cargo build --release
Test - Release:
image: *rust_image
commands:
- apt update
- apt install libudev-dev lua5.4 liblua5.4-dev -y
- cargo test --all --release

3459
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,32 +5,27 @@ edition = "2021"
[workspace]
members = [
"examples/testbed",
"lyra-resource",
"lyra-ecs",
"lyra-reflect",
"lyra-scripting",
"lyra-game",
"lyra-math",
"lyra-scene",
"examples/many-lights",
"crates/*",
"examples/2d",
"examples/fixed-timestep-rotating-model",
"examples/lua-scripting",
"examples/simple_scene"
"examples/many-lights",
"examples/shadows",
"examples/simple_scene",
"examples/testbed",
]
[features]
scripting = ["dep:lyra-scripting"]
lua_scripting = ["scripting", "lyra-scripting/lua"]
tracy = ["lyra-game/tracy"]
[dependencies]
lyra-game = { path = "lyra-game" }
lyra-scripting = { path = "lyra-scripting", optional = true }
lyra-game = { path = "crates/lyra-game" }
lyra-scripting = { path = "crates/lyra-scripting", optional = true }
[profile.dev]
opt-level = 1
#[profile.dev]
#opt-level = 1
[profile.release]
debug = true

View file

@ -0,0 +1,69 @@
use quote::quote;
use syn::{parse_macro_input, spanned::Spanned, DeriveInput};
#[proc_macro_derive(Component)]
pub fn derive_component(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let type_ident = &input.ident;
let type_name = type_ident.to_string();
proc_macro::TokenStream::from(quote! {
impl #impl_generics lyra_engine::ecs::Component for #type_ident #ty_generics #where_clause {
fn name() -> &'static str {
#type_name
}
}
})
}
#[proc_macro_derive(Bundle)]
pub fn derive_bundle(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let type_ident = &input.ident;
let s = match &input.data {
syn::Data::Struct(s) => {
s
},
_ => {
return syn::Error::new(input.span(), "Bundle derive macro only supports Structs")
.into_compile_error().into();
}
};
let field_names: Vec<_> = s.fields.iter().map(|f| f.ident.as_ref().unwrap()).collect();
proc_macro::TokenStream::from(quote! {
impl #impl_generics lyra_engine::ecs::Bundle for #type_ident #ty_generics #where_clause {
fn type_ids(&self) -> Vec<lyra_engine::ecs::DynTypeId> {
let mut v = vec![];
#(
v.extend(self.#field_names.type_ids().into_iter());
)*
v
}
fn info(&self) -> Vec<lyra_engine::ecs::ComponentInfo> {
let mut v = vec![];
#(
v.extend(self.#field_names.info().into_iter());
)*
v
}
fn take(self, f: &mut impl FnMut(std::ptr::NonNull<u8>, lyra_engine::ecs::DynTypeId, lyra_engine::ecs::ComponentInfo)) {
#(
self.#field_names.take(f);
)*
}
fn is_dynamic(&self) -> bool {
false
}
}
})
}

View file

@ -1,6 +1,16 @@
use std::{ptr::{NonNull, self}, alloc::{self, Layout, alloc, dealloc}, mem, collections::HashMap, cell::{RefCell, Ref, RefMut, BorrowError, BorrowMutError}, ops::DerefMut};
use std::{
alloc::{self, alloc, dealloc, Layout},
cell::{BorrowError, BorrowMutError, Ref, RefCell, RefMut},
collections::HashMap,
mem,
ops::DerefMut,
ptr::{self, NonNull},
};
use crate::{bundle::Bundle, component_info::ComponentInfo, world::ArchetypeEntityId, DynTypeId, Entity, Tick};
use crate::{
bundle::Bundle, component_info::ComponentInfo, world::ArchetypeEntityId, DynTypeId, Entity,
Tick,
};
#[derive(Clone)]
pub struct ComponentColumn {
@ -32,8 +42,9 @@ impl ComponentColumn {
pub unsafe fn alloc(component_layout: Layout, capacity: usize) -> NonNull<u8> {
let new_layout = Layout::from_size_align(
component_layout.size().checked_mul(capacity).unwrap(),
component_layout.align()
).unwrap();
component_layout.align(),
)
.unwrap();
if let Some(data) = NonNull::new(alloc(new_layout)) {
data
@ -44,18 +55,21 @@ impl ComponentColumn {
pub unsafe fn new(info: ComponentInfo, capacity: usize) -> Self {
let data = ComponentColumn::alloc(info.layout(), capacity);
let mut ticks = Vec::with_capacity(capacity);
ticks.resize(capacity, Tick::from(0));
Self {
data: RefCell::new(data),
capacity,
info,
len: 0,
entity_ticks: Vec::new(),
entity_ticks: ticks,
}
}
/// Set a component from pointer at an entity index.
///
///
/// # Safety
/// This column must have space to fit the component, if it does not have room it will panic.
pub unsafe fn set_at(&mut self, entity_index: usize, comp_src: NonNull<u8>, tick: Tick) {
@ -63,7 +77,7 @@ impl ComponentColumn {
let mut data = self.data.borrow_mut();
let data = data.deref_mut();
let size = self.info.layout().size();
let dest = NonNull::new_unchecked(data.as_ptr().add(entity_index * size));
ptr::copy_nonoverlapping(comp_src.as_ptr(), dest.as_ptr(), size);
@ -83,37 +97,35 @@ impl ComponentColumn {
let mut data = self.data.borrow_mut();
let data = data.deref_mut();
let size = self.info.layout().size();
let dest = NonNull::new_unchecked(data.as_ptr().add(entity_index * size));
ptr::copy_nonoverlapping(comp_src.as_ptr(), dest.as_ptr(), size);
// check if a component spot is being set twice and that the entity's tick is
// already stored
self.entity_ticks.push(tick);
self.entity_ticks[entity_index] = tick;
self.len += 1;
}
/// Get a component at an entities index.
///
///
/// # Safety
///
///
/// This column MUST have the entity. If it does not, it WILL NOT panic and will cause UB.
pub unsafe fn get<T>(&self, entity_index: usize) -> Ref<T> {
let data = self.data.borrow();
Ref::map(data, |data| {
let ptr = NonNull::new_unchecked(data.as_ptr()
.add(entity_index * self.info.layout().size()))
.cast();
let ptr =
NonNull::new_unchecked(data.as_ptr().add(entity_index * self.info.layout().size()))
.cast();
&*ptr.as_ptr()
})
}
/// Get a mutable borrow to the component at an entities index, ticking the entity.
///
///
/// # Safety
///
///
/// This column must have the entity.
pub unsafe fn get_mut<T>(&mut self, entity_index: usize, tick: &Tick) -> RefMut<T> {
self.entity_ticks[entity_index].tick_to(tick);
@ -121,22 +133,22 @@ impl ComponentColumn {
let data = self.data.borrow_mut();
RefMut::map(data, |data| {
let ptr = NonNull::new_unchecked(data.as_ptr()
.add(entity_index * self.info.layout().size()))
.cast();
let ptr =
NonNull::new_unchecked(data.as_ptr().add(entity_index * self.info.layout().size()))
.cast();
&mut *ptr.as_ptr()
})
}
/// Grow the column to fit `new_capacity` amount of components.
///
///
/// Parameters:
/// * `new_capacity` - The new capacity of components that can fit in this column.
///
///
/// Note: This does not modify the Tick of this column, since no components were actually modified.
///
///
/// # Safety
///
///
/// Will panic if `new_capacity` is less than the current capacity of the column.
pub unsafe fn grow(&mut self, new_capacity: usize) {
assert!(new_capacity > self.capacity);
@ -155,7 +167,7 @@ impl ComponentColumn {
// create a layout with the same alignment, but expand the size of the buffer.
let old_layout = Layout::from_size_align_unchecked(
layout.size().checked_mul(self.capacity).unwrap(),
layout.align()
layout.align(),
);
mem::swap(data.deref_mut(), &mut new_ptr);
@ -163,7 +175,8 @@ impl ComponentColumn {
} else {
*data = new_ptr;
}
self.entity_ticks.resize(new_capacity, Tick::from(0));
self.capacity = new_capacity;
}
@ -171,19 +184,20 @@ impl ComponentColumn {
pub unsafe fn remove_component(&mut self, entity_index: usize, tick: &Tick) -> Option<usize> {
let _ = tick; // may be used at some point
debug_assert!(self.len > 0, "There are no entities in the Archetype to remove from!");
debug_assert!(
self.len > 0,
"There are no entities in the Archetype to remove from!"
);
let mut data = self.data.borrow_mut();
let data = data.deref_mut();
let size = self.info.layout().size();
let mut old_comp_ptr = NonNull::new_unchecked(data.as_ptr()
.add(entity_index * size));
let mut old_comp_ptr = NonNull::new_unchecked(data.as_ptr().add(entity_index * size));
let moved_index = if entity_index != self.len - 1 {
let moved_index = self.len - 1;
let mut new_comp_ptr = NonNull::new_unchecked(data.as_ptr()
.add(moved_index * size));
let mut new_comp_ptr = NonNull::new_unchecked(data.as_ptr().add(moved_index * size));
ptr::copy_nonoverlapping(new_comp_ptr.as_ptr(), old_comp_ptr.as_ptr(), size);
@ -193,13 +207,43 @@ impl ComponentColumn {
self.entity_ticks.swap_remove(entity_index);
Some(moved_index)
} else { None };
} else {
None
};
self.len -= 1;
moved_index
}
/// Get the pointer of the component for an entity.
///
/// It is assumed that the component will be mutated, meaning the component's tick will be
/// updated.
pub fn component_ptr(&mut self, entity_index: usize, tick: &Tick) -> NonNull<u8> {
self.entity_ticks[entity_index] = *tick;
let size = self.info.layout().size();
unsafe { NonNull::new_unchecked(self.borrow_ptr().as_ptr().add(entity_index * size)) }
}
/// Get the pointer of the component for an entity without ticking.
///
/// Since this does not tick, only use this if you know the pointer will not be mutated.
pub fn component_ptr_non_tick(&self, entity_index: usize) -> NonNull<u8> {
let size = self.info.layout().size();
unsafe { NonNull::new_unchecked(self.borrow_ptr().as_ptr().add(entity_index * size)) }
}
/// Get the tick of a component for an entity.
pub fn component_tick(&self, entity_index: usize) -> Option<Tick> {
self.entity_ticks.get(entity_index).cloned()
}
pub fn component_has_changed(&self, entity_index: usize, world_tick: Tick) -> Option<bool> {
self.component_tick(entity_index)
.map(|tick| *tick >= *world_tick - 1)
}
pub fn borrow_ptr(&self) -> Ref<NonNull<u8>> {
self.data.borrow()
}
@ -226,13 +270,13 @@ impl ArchetypeId {
pub(crate) fn increment(&mut self) -> Self {
let v = self.0;
self.0 += 1;
ArchetypeId(v)
}
}
/// Stores a group of entities with matching components.
///
///
/// An Archetype can be thought of as a table, with entities as the rows and the entity's
/// components as each column. This means you can have tightly packed components of entities and
/// quickly iterate through entities with the same components.
@ -248,7 +292,7 @@ pub struct Archetype {
/// Can be used to map `ArchetypeEntityId` to an Entity since `ArchetypeEntityId` has
/// the index that the entity is stored at.
pub(crate) entities: Vec<Entity>,
pub(crate) columns: Vec<ComponentColumn>,
pub columns: Vec<ComponentColumn>,
capacity: usize,
}
@ -267,9 +311,10 @@ impl Archetype {
}
pub fn from_bundle_info(new_id: ArchetypeId, bundle_info: Vec<ComponentInfo>) -> Archetype {
let columns = bundle_info.into_iter().map(|i| {
unsafe { ComponentColumn::new(i, DEFAULT_CAPACITY) }
}).collect();
let columns = bundle_info
.into_iter()
.map(|i| unsafe { ComponentColumn::new(i, DEFAULT_CAPACITY) })
.collect();
Archetype {
id: new_id,
@ -281,13 +326,18 @@ impl Archetype {
}
/// Add an entity and its component bundle to the Archetype
///
///
/// # Safety:
///
///
/// Archetype must contain all of the components
pub(crate) fn add_entity<B>(&mut self, entity: Entity, bundle: B, tick: &Tick) -> ArchetypeEntityId
pub(crate) fn add_entity<B>(
&mut self,
entity: Entity,
bundle: B,
tick: &Tick,
) -> ArchetypeEntityId
where
B: Bundle
B: Bundle,
{
if self.capacity == self.entity_ids.len() {
let new_cap = self.capacity * 2;
@ -300,29 +350,50 @@ impl Archetype {
self.entity_ids.insert(entity, entity_index);
self.entities.push(entity);
bundle.take(|data, type_id, info| {
self.put_component_at(tick, data, type_id, info.layout().size(), entity_index.0 as _);
bundle.take(&mut |data, type_id, info| {
self.put_component_at(
tick,
data,
type_id,
info.layout().size(),
entity_index.0 as _,
);
});
entity_index
}
pub(crate) fn put_component_at(&mut self, tick: &Tick, ptr: NonNull<u8>, type_id: DynTypeId, size: usize, index: usize) {
pub(crate) fn put_component_at(
&mut self,
tick: &Tick,
ptr: NonNull<u8>,
type_id: DynTypeId,
size: usize,
index: usize,
) {
let _ = size;
let col = self.get_column_mut(type_id).unwrap();
//unsafe { col.set_at(index, ptr, *tick) };
unsafe { col.insert_entity(index, ptr, *tick); }
unsafe {
col.insert_entity(index, ptr, *tick);
}
}
/// Removes an entity from the Archetype and frees its components.
///
///
/// Inside the component columns, the entities are swap-removed. Meaning that the last
/// entity in the column is moved in the position of the entity that was removed.
/// If there was an entity that was swapped, this function returns the entity, and its
/// new index in the archetype that was put in place of the removed entity.
pub(crate) fn remove_entity(&mut self, entity: Entity, tick: &Tick) -> Option<(Entity, ArchetypeEntityId)> {
let entity_index = self.entity_ids.remove(&entity)
pub(crate) fn remove_entity(
&mut self,
entity: Entity,
tick: &Tick,
) -> Option<(Entity, ArchetypeEntityId)> {
let entity_index = self
.entity_ids
.remove(&entity)
.expect("The entity is not in this Archetype!");
let mut removed_entity: Option<(Entity, ArchetypeEntityId)> = None;
@ -339,19 +410,19 @@ impl Archetype {
removed_entity = Some((just_removed, ArchetypeEntityId(moved_idx as u64)));
}
} else {
// If there wasn't a moved entity, make sure no other columns moved something.
// If there wasn't a moved entity, make sure no other columns moved something.
assert!(removed_entity.is_none());
}
}
// safe from the .expect at the start of this method.
//self.entity_ids.remove(&entity).unwrap();
// update the archetype index of the moved entity
if let Some((moved, _old_idx)) = removed_entity {
self.entity_ids.insert(moved, entity_index);
}
let removed = self.entities.swap_remove(entity_index.0 as _);
assert_eq!(removed, entity);
@ -362,8 +433,12 @@ impl Archetype {
/// Returns a boolean indicating whether this archetype can store the TypeIds given
pub fn is_archetype_for(&self, types: &Vec<DynTypeId>) -> bool {
if types.len() == self.columns.len() {
self.columns.iter().all(|c| types.contains(&c.info.type_id()))
} else { false }
self.columns
.iter()
.all(|c| types.contains(&c.info.type_id()))
} else {
false
}
}
/// Returns a boolean indicating whether this archetype has a column for `comp_type`
@ -383,18 +458,20 @@ impl Archetype {
}
/// Grows columns in the archetype
///
///
/// Parameters:
/// * `new_capacity` - The new capacity of components that can fit in this column.
///
///
/// # Safety
///
///
/// Will panic if new_capacity is less than the current capacity
fn grow_columns(&mut self, new_capacity: usize) {
assert!(new_capacity > self.capacity);
for c in self.columns.iter_mut() {
unsafe { c.grow(new_capacity); }
unsafe {
c.grow(new_capacity);
}
}
self.capacity = new_capacity;
@ -412,11 +489,16 @@ impl Archetype {
}
/// Returns a mutable borrow to a component column for `type_id`.
///
///
/// Note: This does not modify the tick for the column!
pub fn get_column_mut<I: Into<DynTypeId>>(&mut self, type_id: I) -> Option<&mut ComponentColumn> {
pub fn get_column_mut<I: Into<DynTypeId>>(
&mut self,
type_id: I,
) -> Option<&mut ComponentColumn> {
let type_id = type_id.into();
self.columns.iter_mut().find(|c| c.info.type_id() == type_id)
self.columns
.iter_mut()
.find(|c| c.info.type_id() == type_id)
}
/// Reserves a slot in the columns for an entity and returns the index of that reserved spot
@ -427,8 +509,11 @@ impl Archetype {
self.capacity = new_cap;
}
debug_assert_eq!(self.entity_ids.len(), self.entities.len(),
"Somehow the Archetype's entity storage got unsynced");
debug_assert_eq!(
self.entity_ids.len(),
self.entities.len(),
"Somehow the Archetype's entity storage got unsynced"
);
let entity_index = ArchetypeEntityId(self.entity_ids.len() as u64);
self.entity_ids.insert(entity, entity_index);
self.entities.push(entity);
@ -442,12 +527,15 @@ impl Archetype {
/// Ensure that the internal entity lists are synced in length
pub(crate) fn ensure_synced(&self) {
debug_assert_eq!(self.entity_ids.len(), self.entities.len(),
"Somehow the Archetype's entity storage got unsynced");
debug_assert_eq!(
self.entity_ids.len(),
self.entities.len(),
"Somehow the Archetype's entity storage got unsynced"
);
}
/// Moves the entity from this archetype into another one.
///
///
/// # Safety
/// The entity IS NOT removed from the old archetype. You must manually call [`Archetype::remove_entity`](crate::Archetype).
/// It was done this way because I had some borrow check issues when writing [`World::insert`](crate::World)
@ -463,7 +551,7 @@ impl Archetype {
// move the existing components into the new archetype
for col in self.columns.iter() {
let into_col = into_arch.get_column_mut(col.info.type_id()).unwrap();
// copy from the old column into the new column, then remove it from the old one
unsafe {
let ptr = col.borrow_ptr();
@ -514,19 +602,20 @@ impl Archetype {
}
/// Extend the Archetype by adding more columns.
///
///
/// In order to extend the Archetype, the archetype needs the components for the entities
/// it already has. These are provided through the `new_columns` parameter. **If the Vec
/// does not have the same amount of bundles in it as the amount of entities in the
/// Archetype, it will panic!**
pub fn extend<B: Bundle>(&mut self, tick: &Tick, new_columns: Vec<B>) {
debug_assert_eq!(new_columns.len(), self.len(), "The amount of provided column does not \
match the amount of entities");
let column_info = new_columns.iter()
.next()
.unwrap()
.info();
debug_assert_eq!(
new_columns.len(),
self.len(),
"The amount of provided column does not \
match the amount of entities"
);
let column_info = new_columns.iter().next().unwrap().info();
for coli in column_info.into_iter() {
let col = unsafe { ComponentColumn::new(coli, self.capacity) };
@ -534,14 +623,18 @@ impl Archetype {
}
for (eid, bundle) in new_columns.into_iter().enumerate() {
bundle.take(|ptr, tyid, _size| {
unsafe {
let col = self.get_column_mut(tyid).unwrap();
col.insert_entity(eid, ptr, tick.clone());
}
bundle.take(&mut |ptr, tyid, _size| unsafe {
let col = self.get_column_mut(tyid).unwrap();
col.insert_entity(eid, ptr, tick.clone());
});
}
}
/// Remove a column that stores components with `component_id`.
pub fn remove_column<I: Into<DynTypeId>>(&mut self, component_id: I) {
let id = component_id.into();
self.columns.retain(|c| c.info.type_id() != id);
}
}
#[cfg(test)]
@ -550,7 +643,11 @@ mod tests {
use rand::Rng;
use crate::{bundle::Bundle, tests::{Vec2, Vec3}, ComponentInfo, DynTypeId, DynamicBundle, Entity, EntityId, Tick};
use crate::{
bundle::Bundle,
tests::{Vec2, Vec3},
ComponentInfo, DynTypeId, DynamicBundle, Entity, EntityId, Tick,
};
use super::Archetype;
@ -559,7 +656,7 @@ mod tests {
let bundle = (Vec2::new(10.0, 20.0),);
let entity = Entity {
id: EntityId(0),
generation: 0
generation: 0,
};
let mut a = Archetype::from_bundle_info(super::ArchetypeId(0), bundle.info());
@ -572,10 +669,10 @@ mod tests {
#[test]
fn one_entity_two_component() {
let bundle = (Vec2::new(10.0, 20.0),Vec3::new(15.0, 54.0, 84.0));
let bundle = (Vec2::new(10.0, 20.0), Vec3::new(15.0, 54.0, 84.0));
let entity = Entity {
id: EntityId(0),
generation: 0
generation: 0,
};
let mut a = Archetype::from_bundle_info(super::ArchetypeId(0), bundle.info());
@ -595,12 +692,12 @@ mod tests {
let b1 = (Vec2::new(10.0, 20.0),);
let e1 = Entity {
id: EntityId(0),
generation: 0
generation: 0,
};
let b2 = (Vec2::new(19.0, 43.0),);
let e2 = Entity {
id: EntityId(1),
generation: 0
generation: 0,
};
let mut a = Archetype::from_bundle_info(super::ArchetypeId(0), b1.info());
@ -619,12 +716,12 @@ mod tests {
let b1 = (Vec2::new(10.0, 20.0), Vec3::new(84.0, 283.0, 28.0));
let e1 = Entity {
id: EntityId(0),
generation: 0
generation: 0,
};
let b2 = (Vec2::new(19.0, 43.0), Vec3::new(74.0, 28.0, 93.0));
let e2 = Entity {
id: EntityId(1),
generation: 0
generation: 0,
};
let mut a = Archetype::from_bundle_info(super::ArchetypeId(0), b1.info());
@ -659,7 +756,7 @@ mod tests {
fn auto_archetype_growth() {
let mut rng = rand::thread_rng();
let bundle_count = rng.gen_range(50..150);
let mut bundles: Vec<(Vec2,)> = vec![];
bundles.reserve(bundle_count);
@ -669,14 +766,14 @@ mod tests {
for i in 0..bundle_count {
let c = (Vec2::rand(),);
bundles.push(c);
a.add_entity(
Entity {
id: EntityId(i as u64),
generation: 0
generation: 0,
},
c,
&Tick::default()
&Tick::default(),
);
}
println!("Inserted {} entities", bundle_count);
@ -701,25 +798,27 @@ mod tests {
a.add_entity(
Entity {
id: EntityId(i as u64),
generation: 0
generation: 0,
},
(bundles[i],),
&Tick::default()
&Tick::default(),
);
}
// Remove the 'middle' entity in the column
let moved_entity = a.remove_entity(
Entity {
id: EntityId(1u64),
generation: 0,
},
&Tick::default()
).expect("No entity was moved");
let moved_entity = a
.remove_entity(
Entity {
id: EntityId(1u64),
generation: 0,
},
&Tick::default(),
)
.expect("No entity was moved");
// The last entity in the column should have been moved
assert!(moved_entity.0.id.0 == 2);
assert!(moved_entity.1.0 == 1);
assert!(moved_entity.1 .0 == 1);
// make sure that the entities' component was actually moved in the column
let col = &a.columns[0];
@ -731,7 +830,8 @@ mod tests {
#[test]
fn dynamic_archetype() {
let layout = Layout::new::<u32>();
let info = ComponentInfo::new_unknown(Some("u32".to_string()), DynTypeId::Unknown(100), layout);
let info =
ComponentInfo::new_unknown(Some("u32".to_string()), DynTypeId::Unknown(100), layout);
let infos = vec![info.clone()];
let mut a = Archetype::from_bundle_info(super::ArchetypeId(0), infos);
@ -744,10 +844,10 @@ mod tests {
a.add_entity(
Entity {
id: EntityId(0),
generation: 0
generation: 0,
},
dynamic_bundle,
&Tick::default()
&Tick::default(),
);
let col = a.columns.iter().next().unwrap();
@ -764,15 +864,11 @@ mod tests {
let ae = Entity {
id: EntityId(0),
generation: 0
generation: 0,
};
a.add_entity(
ae,
Vec2::new(10.0, 50.0),
&Tick::default()
);
a.add_entity(ae, Vec2::new(10.0, 50.0), &Tick::default());
a.remove_entity(ae, &Tick::default());
}
}
}

View file

@ -1,4 +1,4 @@
use std::{ptr::NonNull, alloc::Layout};
use std::{alloc::Layout, ptr::NonNull};
use crate::{component::Component, component_info::ComponentInfo, DynTypeId};
@ -11,12 +11,17 @@ pub trait Bundle {
/// Take the bundle by calling the closure with pointers to each component, its type and size.
/// The closure is expected to take ownership of the pointer.
fn take(self, f: impl FnMut(NonNull<u8>, DynTypeId, ComponentInfo));
fn take(self, f: &mut impl FnMut(NonNull<u8>, DynTypeId, ComponentInfo));
/// Returns a boolean indicating if this Bundle is dynamic. See [`DynamicBundle`]
fn is_dynamic(&self) -> bool;
}
/// A bundle with its components known at compile time.
pub trait FixedBundle: Bundle {
fn fixed_type_ids() -> Vec<DynTypeId>;
}
impl Bundle for () {
fn type_ids(&self) -> Vec<DynTypeId> {
vec![DynTypeId::of::<()>()]
@ -26,8 +31,12 @@ impl Bundle for () {
vec![ComponentInfo::new::<()>()]
}
fn take(self, mut f: impl FnMut(NonNull<u8>, DynTypeId, ComponentInfo)) {
f(NonNull::from(&self).cast(), DynTypeId::of::<()>(), ComponentInfo::new::<()>());
fn take(self, f: &mut impl FnMut(NonNull<u8>, DynTypeId, ComponentInfo)) {
f(
NonNull::from(&self).cast(),
DynTypeId::of::<()>(),
ComponentInfo::new::<()>(),
);
}
fn is_dynamic(&self) -> bool {
@ -35,17 +44,27 @@ impl Bundle for () {
}
}
impl FixedBundle for () {
fn fixed_type_ids() -> Vec<DynTypeId> {
vec![DynTypeId::of::<()>()]
}
}
impl<C: Component> Bundle for C {
fn type_ids(&self) -> Vec<DynTypeId> {
vec![DynTypeId::of::<C>()]
C::fixed_type_ids()
}
fn info(&self) -> Vec<ComponentInfo> {
vec![ComponentInfo::new::<C>()]
}
fn take(self, mut f: impl FnMut(NonNull<u8>, DynTypeId, ComponentInfo)) {
f(NonNull::from(&self).cast(), DynTypeId::of::<C>(), ComponentInfo::new::<C>());
fn take(self, f: &mut impl FnMut(NonNull<u8>, DynTypeId, ComponentInfo)) {
f(
NonNull::from(&self).cast(),
DynTypeId::of::<C>(),
ComponentInfo::new::<C>(),
);
// this must be done to avoid calling drop on heap memory that the component
// may manage. So something like a Vec, or HashMap, etc.
@ -57,35 +76,65 @@ impl<C: Component> Bundle for C {
}
}
impl<C: Component> FixedBundle for C {
fn fixed_type_ids() -> Vec<DynTypeId> {
vec![DynTypeId::of::<C>()]
}
}
macro_rules! impl_bundle_tuple {
( $($name: ident),+ ) => (
// these names wont follow rust convention, but its a macro so deal with it
#[allow(non_snake_case)]
impl<$($name: Component),+> Bundle for ($($name,)+) {
impl<$($name: Bundle),+> Bundle for ($($name,)+) {
#[inline(always)]
fn type_ids(&self) -> Vec<DynTypeId> {
// these names wont follow rust convention, but its a macro so deal with it
vec![$(DynTypeId::of::<$name>()),+]
let ($($name,)+) = self;
let mut v = vec![];
$(
v.extend($name.type_ids().into_iter());
)+
v
}
#[inline(always)]
fn info(&self) -> Vec<ComponentInfo> {
vec![$(ComponentInfo::new::<$name>()),+]
let ($($name,)+) = self;
let mut v = vec![];
$(
v.extend($name.info().into_iter());
)+
v
}
fn take(self, mut f: impl FnMut(NonNull<u8>, DynTypeId, ComponentInfo)) {
// these names wont follow rust convention, but its a macro so deal with it
#[inline(always)]
fn take(self, f: &mut impl FnMut(NonNull<u8>, DynTypeId, ComponentInfo)) {
let ($($name,)+) = self;
$(
f(NonNull::from(&$name).cast(), DynTypeId::of::<$name>(), ComponentInfo::new::<$name>());
// this must be done to avoid calling drop on heap memory that the component
// may manage. So something like a Vec, or HashMap, etc.
std::mem::forget($name);
$name.take(f);
)+
}
#[inline(always)]
fn is_dynamic(&self) -> bool {
false
}
}
#[allow(non_snake_case)]
impl<$($name: FixedBundle),+> FixedBundle for ($($name,)+) {
#[inline(always)]
fn fixed_type_ids() -> Vec<DynTypeId> {
let mut v = vec![];
$(
v.extend($name::fixed_type_ids().into_iter());
)+
v
}
}
);
}
@ -108,7 +157,7 @@ impl_bundle_tuple! { C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11, C12, C13, C14
impl_bundle_tuple! { C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11, C12, C13, C14, C15, C16 }
/// A bundle of a dynamic number of components. The types of the components may not be known to Rust.
///
///
/// # Safety
/// Do not drop this without inserting it into an archetype. It WILL cause a memory leak
#[derive(Default, Clone)]
@ -134,7 +183,7 @@ impl DynamicBundle {
/// Push a type known to rust to this bundle
pub fn push<C>(&mut self, comp: C)
where
C: Component
C: Component,
{
// an owned pointer must be created from the provided component since comp would drop
// out of scope and the data would become invalid
@ -144,7 +193,7 @@ impl DynamicBundle {
let layout = Layout::new::<C>();
let alloc_ptr = NonNull::new_unchecked(std::alloc::alloc(layout)).cast::<C>();
std::ptr::copy_nonoverlapping(data.as_ptr(), alloc_ptr.as_ptr(), 1);
// this must be done to avoid calling drop on heap memory that the component
// may manage. So something like a Vec, or HashMap, etc.
std::mem::forget(comp);
@ -164,21 +213,24 @@ impl DynamicBundle {
/// Push a bundle to the end of this dynamic bundle.
pub fn push_bundle<B>(&mut self, bundle: B)
where
B: Bundle
B: Bundle,
{
bundle.take(|ptr, _, info| {
bundle.take(&mut |ptr, _, info| {
// unfortunately the components in the bundle must be copied since there is no guarantee that
// `bundle` lasts for as long as the `DynamicBundle`. If the data wasn't copied, the pointers
// could be invalid later.
let p = unsafe {
let alloc_ptr = NonNull::new_unchecked(std::alloc::alloc(info.layout()));
std::ptr::copy_nonoverlapping(ptr.as_ptr(), alloc_ptr.as_ptr(), info.layout().size());
std::ptr::copy_nonoverlapping(
ptr.as_ptr(),
alloc_ptr.as_ptr(),
info.layout().size(),
);
alloc_ptr
};
self.push_unknown(p, info);
});
}
}
@ -192,7 +244,7 @@ impl Bundle for DynamicBundle {
self.bundle.iter().map(|b| b.1).collect()
}
fn take(self, mut f: impl FnMut(NonNull<u8>, DynTypeId, ComponentInfo)) {
fn take(self, f: &mut impl FnMut(NonNull<u8>, DynTypeId, ComponentInfo)) {
for (data, info) in self.bundle.into_iter() {
f(data, info.type_id(), info);
}
@ -201,4 +253,74 @@ impl Bundle for DynamicBundle {
fn is_dynamic(&self) -> bool {
true
}
}
}
#[cfg(test)]
mod tests {
use crate::{lyra_engine, ComponentInfo, World};
use lyra_ecs_derive::{Bundle, Component};
use super::Bundle;
#[allow(dead_code)]
#[derive(Component, PartialEq, Clone, Copy, Debug)]
struct Vec2 {
x: f32,
y: f32,
}
#[allow(dead_code)]
#[derive(Component, Debug, PartialEq, Eq, Clone, Copy)]
enum SomeFlag {
SomethingA,
SomethingB,
}
#[derive(Bundle)]
struct CompBundle {
pos: Vec2,
flag: SomeFlag,
}
#[test]
fn check_bundle_order() {
let b = CompBundle {
pos: Vec2 { x: 10.0, y: 10.0 },
flag: SomeFlag::SomethingA,
};
let info = b.info();
let mut info = info.into_iter();
assert_eq!(info.next().unwrap(), ComponentInfo::new::<Vec2>());
assert_eq!(info.next().unwrap(), ComponentInfo::new::<SomeFlag>());
}
#[test]
fn check_bundle_spawn() {
let b_pos = Vec2 { x: 10.0, y: 10.0 };
let b_flag = SomeFlag::SomethingA;
let b = CompBundle {
pos: b_pos,
flag: b_flag,
};
let mut world = World::new();
let e = world.spawn(b);
let pos = world
.view_one::<&Vec2>(e)
.expect("failed to find spawned Vec2 from Bundle on Entity");
assert!(
pos.x == b_pos.x && pos.y == b_pos.y,
"Spawned Vec2 values were not correct, got: {:?}, expected: {:?}",
*pos,
b_pos
);
let flag = world
.view_one::<&SomeFlag>(e)
.expect("failed to find spawned SomeFlag from Bundle on Entity");
assert_eq!(*flag, b_flag);
}
}

View file

@ -1,6 +1,6 @@
use std::{any::Any, cell::RefMut, mem::{self, MaybeUninit}, ptr::{self, NonNull}};
use crate::{system::FnArgFetcher, Access, Bundle, Entities, Entity, World};
use crate::{system::FnArgFetcher, Access, Bundle, Entities, Entity, Relation, World};
/// A Command be used to delay mutation of the world until after this system is ran.
pub trait Command: Any {
@ -166,6 +166,21 @@ impl<'a, 'b> Commands<'a, 'b> {
e
}
/// Insert or update existing components into an Entity.
///
/// See [`World::insert`].
pub fn insert<B: Bundle + 'static>(&mut self, entity: Entity, bundle: B) {
self.add(move | world: &mut World| {
world.insert(entity, bundle);
});
}
pub fn add_relation<R: Relation>(&mut self, origin: Entity, relation: R, target: Entity){
self.add(move | world: &mut World| {
world.add_relation(origin, relation, target);
});
}
/// Execute all commands in the queue, in order of insertion
pub fn execute(&mut self, world: &mut World) {
self.queue.execute(Some(world));
@ -189,11 +204,11 @@ impl FnArgFetcher for Commands<'_, '_> {
CommandQueue::default()
}
fn apply_deferred<'a>(mut state: Self::State, mut world_ptr: NonNull<World>) {
fn apply_deferred<'a, 'state>(state: &'state mut Self::State, mut world_ptr: NonNull<World>) {
let world = unsafe { world_ptr.as_mut() };
let mut cmds = Commands::new(&mut state, world);
// safety: Commands has a mut borrow only to entities in the world
let mut cmds = Commands::new(state, world);
// SAFETY: Commands has a mut borrow only to entities in the world
let world = unsafe { world_ptr.as_mut() };
cmds.execute(world);
}

View file

@ -42,13 +42,20 @@ impl DynTypeId {
*self == id
}
/// Force self into a rust TypeId, will panic if this type is not a Rust type.
/// Force self into a rust TypeId, will panic if this type is not a Rust type.
pub fn as_rust(&self) -> TypeId {
match self {
DynTypeId::Rust(t) => *t,
DynTypeId::Unknown(_) => panic!("This type is unknown to rust, cannot construct a TypeId from it!"),
}
}
pub fn as_unknown(&self) -> Option<u128> {
match self {
DynTypeId::Rust(_) => None,
DynTypeId::Unknown(id) => Some(*id),
}
}
}
/// Some information about a component.

View file

@ -12,6 +12,13 @@ pub struct Entity {
}
impl Entity {
pub fn new(id: EntityId, gen: u64) -> Self {
Self {
id,
generation: gen,
}
}
pub fn id(&self) -> EntityId {
self.id
}
@ -70,4 +77,4 @@ impl Entities {
pub(crate) fn insert_entity_record(&mut self, entity: Entity, record: Record) {
self.arch_index.insert(entity.id, record);
}
}
}

View file

@ -1,3 +1,5 @@
#![feature(associated_type_defaults)]
extern crate self as lyra_ecs;
#[allow(unused_imports)]

View file

@ -5,6 +5,9 @@ use crate::{World, ComponentColumn, ComponentInfo};
mod view;
pub use view::*;
mod view_one;
pub use view_one::*;
use super::Fetch;
/// Data that rust does not know the type of

View file

@ -24,6 +24,10 @@ impl DynamicViewState {
}
}
pub fn queries_num(&self) -> usize {
self.queries.len()
}
pub fn push(&mut self, dyn_query: QueryDynamicType) {
self.queries.push(dyn_query);
}
@ -57,11 +61,17 @@ pub struct DynamicViewStateIter {
}
impl DynamicViewStateIter {
pub fn next(&mut self, world: &World) -> Option<DynamicViewItem> {
pub fn next(&mut self, world: &World) -> Option<(Entity, Vec<DynamicType>)> {
let archetypes = world.archetypes.values().collect::<Vec<_>>();
loop {
if let Some(entity_index) = self.component_indices.next() {
let entity = {
let arch_id = self.next_archetype - 1;
let arch = unsafe { archetypes.get_unchecked(arch_id) };
arch.entity_at_index(ArchetypeEntityId(entity_index)).unwrap()
};
let mut fetch_res = vec![];
for fetcher in self.fetchers.iter_mut() {
@ -78,11 +88,7 @@ impl DynamicViewStateIter {
continue;
}
let arch = archetypes.get(self.next_archetype-1).unwrap();
return Some(DynamicViewItem {
row: fetch_res,
entity: arch.entity_at_index(ArchetypeEntityId(entity_index)).unwrap()
})
return Some((entity, fetch_res));
} else {
if self.next_archetype >= archetypes.len() {
return None; // ran out of archetypes to go through
@ -138,7 +144,7 @@ impl<'a> DynamicView<'a> {
/// This works great for a embedding with a scripting language (*cough* *cough* WASM) since
/// Rust doesn't actually need to know the types of what its iterating over.
impl<'a> IntoIterator for DynamicView<'a> {
type Item = DynamicViewItem;
type Item = (Entity, Vec<DynamicType>);
type IntoIter = DynamicViewIter<'a>;
@ -160,7 +166,7 @@ pub struct DynamicViewIter<'a> {
}
impl<'a> Iterator for DynamicViewIter<'a> {
type Item = DynamicViewItem;
type Item = (Entity, Vec<DynamicType>);
fn next(&mut self) -> Option<Self::Item> {
self.inner.next(&self.world)
@ -193,13 +199,11 @@ mod tests {
view.push(query);
let mut view_iter = view.into_iter();
while let Some(view_row) = view_iter.next(&world) {
assert_eq!(view_row.row.len(), 1);
let mut row_iter = view_row.row.iter();
while let Some((_e, view_row)) = view_iter.next(&world) {
assert_eq!(view_row.len(), 1);
let mut row_iter = view_row.iter();
let dynamic_type = row_iter.next().unwrap();
let component_data = unsafe { dynamic_type.ptr.cast::<u32>().as_ref() };
assert_eq!(*component_data, 50);
}
@ -222,13 +226,11 @@ mod tests {
let mut view = DynamicView::new(&world);
view.push(query);
for view_row in view.into_iter() {
assert_eq!(view_row.row.len(), 1);
for (_e, view_row) in view.into_iter() {
assert_eq!(view_row.len(), 1);
let mut row_iter = view_row.iter();
let mut row_iter = view_row.row.iter();
let dynamic_type = row_iter.next().unwrap();
let dynamic_type = row_iter.next().unwrap();
let component_data = unsafe { dynamic_type.ptr.cast::<u32>().as_ref() };
assert_eq!(*component_data, 50);
}

View file

@ -0,0 +1,168 @@
use std::ops::{Deref, DerefMut};
use crate::{query::Fetch, Entity, World};
use super::{DynamicType, FetchDynamicTypeUnsafe, QueryDynamicType};
/// A view of dynamic types (types that are not known to Rust).
///
/// This view gives you the ability to iterate over types that are unknown to Rust, which we call
/// dynamic types. This is great for embedding with a scripting language (*cough* *cough* WASM)
/// since Rust doesn't actually need to know the types of what its iterating over.
pub struct DynamicViewOne<'a> {
world: &'a World,
inner: DynamicViewOneOwned,
}
impl<'a> Deref for DynamicViewOne<'a> {
type Target = DynamicViewOneOwned;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<'a> DerefMut for DynamicViewOne<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl<'a> DynamicViewOne<'a> {
pub fn new(world: &'a World, entity: Entity) -> Self {
Self {
world,
inner: DynamicViewOneOwned::new(entity)
}
}
/// Create a new [`DynamicViewOne`] with queries.
pub fn new_with(world: &'a World, entity: Entity, queries: Vec<QueryDynamicType>) -> Self {
Self {
world,
inner: DynamicViewOneOwned::new_with(entity, queries)
}
}
pub fn get(self) -> Option<Vec<DynamicType>> {
self.inner.get(&self.world)
}
}
/// A variant of [`DynamicViewOne`] that doesn't store a borrow of the world.
#[derive(Clone)]
pub struct DynamicViewOneOwned {
pub entity: Entity,
pub queries: Vec<QueryDynamicType>
}
impl DynamicViewOneOwned {
pub fn new(entity: Entity) -> Self {
Self {
entity,
queries: vec![],
}
}
/// Create a new [`DynamicViewOne`] with queries.
pub fn new_with(entity: Entity, queries: Vec<QueryDynamicType>) -> Self {
Self {
entity,
queries
}
}
pub fn get(self, world: &World) -> Option<Vec<DynamicType>> {
dynamic_view_one_get_impl(world, &self.queries, self.entity)
}
}
fn dynamic_view_one_get_impl(world: &World, queries: &Vec<QueryDynamicType>, entity: Entity) -> Option<Vec<DynamicType>> {
let arch = world.entity_archetype(entity)?;
let aid = arch.entity_indexes().get(&entity)?;
// get all fetchers for the queries
let mut fetchers: Vec<FetchDynamicTypeUnsafe> = queries.iter()
.map(|q| unsafe { q.fetch(world, arch.id(), arch) } )
.collect();
let mut fetch_res = vec![];
for fetcher in fetchers.iter_mut() {
if !fetcher.can_visit_item(*aid) {
return None;
} else {
let i = unsafe { fetcher.get_item(*aid) };
fetch_res.push(i);
}
}
if fetch_res.is_empty() {
None
} else {
Some(fetch_res)
}
}
#[cfg(test)]
mod tests {
use std::{alloc::Layout, ptr::NonNull};
use crate::{World, ComponentInfo, DynTypeId, DynamicBundle, query::dynamic::QueryDynamicType};
use super::DynamicViewOne;
#[test]
fn single_dynamic_view_one_state() {
let comp_layout = Layout::new::<u32>();
let comp_info = ComponentInfo::new_unknown(Some("u32".to_string()), DynTypeId::Unknown(100), comp_layout);
let mut dynamic_bundle = DynamicBundle::default();
let comp = 50u32;
let ptr = NonNull::from(&comp).cast::<u8>();
dynamic_bundle.push_unknown(ptr, comp_info.clone());
let mut world = World::new();
let e = world.spawn(dynamic_bundle);
let query = QueryDynamicType::from_info(comp_info);
let view = DynamicViewOne::new_with(&world, e, vec![query]);
let view_row = view.get()
.expect("failed to get entity row");
assert_eq!(view_row.len(), 1);
let mut row_iter = view_row.iter();
let dynamic_type = row_iter.next().unwrap();
let component_data = unsafe { dynamic_type.ptr.cast::<u32>().as_ref() };
assert_eq!(*component_data, 50);
}
#[test]
fn single_dynamic_view_one() {
let comp_layout = Layout::new::<u32>();
let comp_info = ComponentInfo::new_unknown(Some("u32".to_string()), DynTypeId::Unknown(100), comp_layout);
let mut dynamic_bundle = DynamicBundle::default();
let comp = 50u32;
let ptr = NonNull::from(&comp).cast::<u8>();
dynamic_bundle.push_unknown(ptr, comp_info.clone());
let mut world = World::new();
let e = world.spawn(dynamic_bundle);
let query = QueryDynamicType::from_info(comp_info);
let view = DynamicViewOne::new_with(&world, e, vec![query]);
let view_row = view.get()
.expect("failed to get entity row");
assert_eq!(view_row.len(), 1);
let mut row_iter = view_row.iter();
let dynamic_type = row_iter.next().unwrap();
let component_data = unsafe { dynamic_type.ptr.cast::<u32>().as_ref() };
assert_eq!(*component_data, 50);
}
}

View file

@ -0,0 +1,94 @@
use std::marker::PhantomData;
use crate::{query::{AsQuery, Fetch, Query}, Component, ComponentColumn, DynTypeId, Tick, World};
pub struct ChangedFetcher<'a, T> {
col: &'a ComponentColumn,
tick: Tick,
_phantom: PhantomData<&'a T>,
}
impl<'a, T> Fetch<'a> for ChangedFetcher<'a, T>
where
T: 'a,
{
type Item = bool;
fn dangling() -> Self {
unreachable!()
}
unsafe fn get_item(&mut self, entity: crate::world::ArchetypeEntityId) -> Self::Item {
let tick = self.col.entity_ticks[entity.0 as usize];
if *tick > 50 {
//debug!("tick: {}, world tick: {}", *tick, *self.tick);
}
*tick >= *self.tick
}
}
/// A filter that fetches components that have changed.
///
/// Since [`AsQuery`] is implemented for `&T`, you can use this query like this:
/// ```nobuild
/// for ts in world.view::<&T>() {
/// println!("Got a &T!");
/// }
/// ```
pub struct Changed<T> {
type_id: DynTypeId,
_phantom: PhantomData<T>
}
impl<T: Component> Default for Changed<T> {
fn default() -> Self {
Self {
type_id: DynTypeId::of::<T>(),
_phantom: PhantomData,
}
}
}
// manually implemented to avoid a Copy bound on T
impl<T> Copy for Changed<T> {}
// manually implemented to avoid a Clone bound on T
impl<T> Clone for Changed<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: Component> Changed<T> {
pub fn new() -> Self {
Self::default()
}
}
impl<T: Component> Query for Changed<T>
where
T: 'static
{
type Item<'a> = bool;
type Fetch<'a> = ChangedFetcher<'a, T>;
fn new() -> Self {
Changed::<T>::new()
}
fn can_visit_archetype(&self, archetype: &crate::archetype::Archetype) -> bool {
archetype.has_column(self.type_id)
}
unsafe fn fetch<'a>(&self, _: &'a World, a: &'a crate::archetype::Archetype, tick: crate::Tick) -> Self::Fetch<'a> {
ChangedFetcher {
col: a.get_column(self.type_id).unwrap(),
tick,
_phantom: PhantomData::<&T>,
}
}
}
impl<T: Component> AsQuery for Changed<T> {
type Query = Self;
}

View file

@ -2,30 +2,33 @@ use std::marker::PhantomData;
use crate::{query::{AsQuery, Query}, Archetype, Component, DynTypeId, World};
use super::StaticFetcher;
/// A filter query that fetches when the entity has the component `C`.
///
/// This does not return a reference to the component, it returns `()` if the entity has
/// the component. This query is great when its used with [`Or`](super::Or).
///
/// See [`Without`].
#[derive(Default)]
pub struct Has<C: Component> {
pub struct With<C: Component> {
_marker: PhantomData<C>
}
impl<C: Component> Copy for Has<C> {}
impl<C: Component> Copy for With<C> {}
impl<C: Component> Clone for Has<C> {
impl<C: Component> Clone for With<C> {
fn clone(&self) -> Self {
Self { _marker: self._marker.clone() }
}
}
impl<C: Component> Query for Has<C> {
type Item<'a> = ();
type Fetch<'a> = ();
impl<C: Component> Query for With<C> {
type Item<'a> = bool;
type Fetch<'a> = StaticFetcher<bool>;
fn new() -> Self {
Has {
With {
_marker: PhantomData
}
}
@ -35,10 +38,17 @@ impl<C: Component> Query for Has<C> {
}
unsafe fn fetch<'a>(&self, _world: &'a World, _: &'a Archetype, _: crate::Tick) -> Self::Fetch<'a> {
()
// if fetch is called, it means that 'can_visit_archetype' returned true
StaticFetcher::new(true)
}
}
impl<C: Component> AsQuery for Has<C> {
impl<C: Component> AsQuery for With<C> {
type Query = Self;
}
}
//impl<C: Component> Filter for Has<C> { }
/* impl<C: Component> AsFilter for Has<C> {
type Filter = Self;
} */

View file

@ -0,0 +1,111 @@
mod has;
pub use has::*;
mod or;
pub use or::*;
mod not;
pub use not::*;
mod changed;
pub use changed::*;
mod without;
pub use without::*;
use crate::{Archetype, ArchetypeEntityId, Tick, World};
use super::{Fetch, Query};
pub trait FilterFetch<'a> {
/// Returns true if the entity should be visited or skipped.
fn can_visit(&mut self, entity: ArchetypeEntityId) -> bool;
}
pub trait Filter: Copy {
/// The fetcher used for the filter
type Fetch<'a>: FilterFetch<'a>;
fn new() -> Self;
/// Returns true if the archetype should be visited or skipped.
fn can_visit_archetype(&self, archetype: &Archetype) -> bool;
unsafe fn fetch<'a>(&self, world: &'a World, archetype: &'a Archetype, tick: Tick) -> Self::Fetch<'a>;
/// Returns a fetcher that doesn't fetch from an archetype.
unsafe fn fetch_world<'a>(&self, world: &'a World) -> Option<Self::Fetch<'a>> {
let _ = world;
None
}
}
/// A trait for getting the filter of a type.
pub trait AsFilter {
/// The query for this type
type Filter: Filter;
}
impl<Q> Filter for Q
where
Q: for <'a> Query<Item<'a> = bool>,
{
type Fetch<'a> = Q::Fetch<'a>;
fn new() -> Self {
Query::new()
}
fn can_visit_archetype(&self, archetype: &Archetype) -> bool {
Query::can_visit_archetype(self, archetype)
}
unsafe fn fetch<'a>(&self, world: &'a World, archetype: &'a Archetype, tick: Tick) -> Self::Fetch<'a> {
Query::fetch(self, world, archetype, tick)
}
}
impl<'a, F> FilterFetch<'a> for F
where
F: Fetch<'a, Item = bool>
{
fn can_visit(&mut self, entity: ArchetypeEntityId) -> bool {
Fetch::can_visit_item(self, entity) && unsafe { Fetch::get_item(self, entity) }
}
}
impl<Q> AsFilter for Q
where
Q: for <'a> Query<Item<'a> = bool>
{
type Filter = Q;
}
/// A fetcher that just returns a provided value
pub struct StaticFetcher<T: Clone> {
value: T,
}
impl<'a, T: Clone> StaticFetcher<T> {
pub fn new(value: T) -> Self {
Self {
value
}
}
}
impl<'a, T> Fetch<'a> for StaticFetcher<T>
where
T: Clone + 'a,
{
type Item = T;
fn dangling() -> Self {
unreachable!()
}
unsafe fn get_item(&mut self, _: crate::world::ArchetypeEntityId) -> Self::Item {
self.value.clone()
}
}

View file

@ -1,5 +1,7 @@
use crate::{query::{AsQuery, Query}, Archetype, World};
use super::StaticFetcher;
/// A filter query that fetches the inverse of `Q`.
///
/// This means that entities that `Q` fetches are skipped, and entities that
@ -20,9 +22,8 @@ pub struct Not<Q: Query> {
}
impl<Q: Query> Query for Not<Q> {
type Item<'a> = ();
type Fetch<'a> = ();
type Item<'a> = bool;
type Fetch<'a> = StaticFetcher<bool>;
fn new() -> Self {
Not {
@ -35,7 +36,8 @@ impl<Q: Query> Query for Not<Q> {
}
unsafe fn fetch<'a>(&self, _world: &'a World, _: &'a Archetype, _: crate::Tick) -> Self::Fetch<'a> {
()
// if fetch is called, it means that 'can_visit_archetype' returned true
StaticFetcher::new(true)
}
}

View file

@ -0,0 +1,218 @@
use std::marker::PhantomData;
use crate::{query::{AsQuery, Fetch, Query}, Archetype, World};
use super::{AsFilter, Filter, FilterFetch};
/// The fetcher for [`OrQuery`].
pub struct OrFetch<'a, Q1: Query, Q2: Query> {
left: Option<Q1::Fetch<'a>>,
right: Option<Q2::Fetch<'a>>,
}
impl<'a, Q1: Query, Q2: Query> Fetch<'a> for OrFetch<'a, Q1, Q2> {
type Item = (Option<Q1::Item<'a>>, Option<Q2::Item<'a>>);
fn dangling() -> Self {
Self {
left: None,
right: None,
}
}
unsafe fn get_item(&mut self, entity: crate::ArchetypeEntityId) -> Self::Item {
let mut res = (None, None);
if let Some(left) = self.left.as_mut() {
let i = left.get_item(entity);
res.0 = Some(i);
}
if let Some(right) = self.right.as_mut() {
let i = right.get_item(entity);
res.1 = Some(i);
}
res
}
}
/// The fetcher for [`OrFilter`].
pub struct OrFilterFetch<'a, F1: Filter, F2: Filter> {
left: Option<F1::Fetch<'a>>,
right: Option<F2::Fetch<'a>>,
}
impl<'a, F1: Filter, F2: Filter> FilterFetch<'a> for OrFilterFetch<'a, F1, F2> {
fn can_visit(&mut self, entity: crate::ArchetypeEntityId) -> bool {
if let Some(left) = self.left.as_mut() {
left.can_visit(entity)
} else if let Some(right) = self.right.as_mut() {
right.can_visit(entity)
} else {
false
}
}
}
/// The [`Query`] implementation of [`Or`].
#[derive(Default)]
pub struct OrQuery<Q1: Clone + Copy, Q2: Clone + Copy, C> {
left: Q1,
right: Q2,
can_visit_left: bool,
can_visit_right: bool,
_marker: PhantomData<C>,
}
impl<Q1: AsQuery, Q2: AsQuery> Copy for OrQuery<Q1::Query, Q2::Query, (Q1, Q2)> {}
impl<Q1: AsQuery, Q2: AsQuery> Clone for OrQuery<Q1::Query, Q2::Query, (Q1, Q2)> {
fn clone(&self) -> Self {
Self {
left: self.left.clone(),
right: self.right.clone(),
can_visit_left: self.can_visit_left,
can_visit_right: self.can_visit_right,
_marker: self._marker,
}
}
}
impl<Q1: AsQuery, Q2: AsQuery> Query for OrQuery<Q1::Query, Q2::Query, (Q1, Q2)> {
type Item<'a> = (Option<<Q1::Query as Query>::Item<'a>>, Option<<Q2::Query as Query>::Item<'a>>);
type Fetch<'a> = OrFetch<'a, Q1::Query, Q2::Query>;
fn new() -> Self {
OrQuery {
left: Q1::Query::new(),
right: Q2::Query::new(),
can_visit_left: false,
can_visit_right: false,
_marker: PhantomData
}
}
fn can_visit_archetype(&self, archetype: &Archetype) -> bool {
self.left.can_visit_archetype(archetype) || self.right.can_visit_archetype(archetype)
}
unsafe fn fetch<'a>(&self, world: &'a World, archetype: &'a Archetype, tick: crate::Tick) -> Self::Fetch<'a> {
let mut f = OrFetch::<Q1::Query, Q2::Query>::dangling();
// TODO: store the result of Self::can_visit_archetype so this isn't ran twice
if self.left.can_visit_archetype(archetype) {
f.left = Some(self.left.fetch(world, archetype, tick));
}
if self.right.can_visit_archetype(archetype) {
f.right = Some(self.right.fetch(world, archetype, tick));
}
f
}
}
/// The [`Filter`] implementation of [`Or`].
#[derive(Default)]
pub struct OrFilter<F1: AsFilter, F2: AsFilter> {
left: F1::Filter,
right: F2::Filter,
can_visit_left: bool,
can_visit_right: bool,
}
impl<F1: AsFilter, F2: AsFilter> Copy for OrFilter<F1, F2> {}
impl<F1: AsFilter, F2: AsFilter> Clone for OrFilter<F1, F2> {
fn clone(&self) -> Self {
Self {
left: self.left.clone(),
right: self.right.clone(),
can_visit_left: self.can_visit_left,
can_visit_right: self.can_visit_right,
}
}
}
impl<F1: AsFilter, F2: AsFilter> Filter for OrFilter<F1, F2> {
type Fetch<'a> = OrFilterFetch<'a, F1::Filter, F2::Filter>;
fn new() -> Self {
OrFilter {
left: F1::Filter::new(),
right: F2::Filter::new(),
can_visit_left: false,
can_visit_right: false,
}
}
fn can_visit_archetype(&self, archetype: &Archetype) -> bool {
self.left.can_visit_archetype(archetype) || self.right.can_visit_archetype(archetype)
}
unsafe fn fetch<'a>(&self, world: &'a World, archetype: &'a Archetype, tick: crate::Tick) -> Self::Fetch<'a> {
let left = if self.left.can_visit_archetype(archetype) {
Some(self.left.fetch(world, archetype, tick))
} else { None };
let right = if self.right.can_visit_archetype(archetype) {
Some(self.right.fetch(world, archetype, tick))
} else { None };
OrFilterFetch {
left,
right,
}
}
}
/// A query and/or filter returning when either `Q1` or `Q2` returns.
///
/// This checks if `Q1` can fetch before checking `Q2`.
///
/// It can be used as a query:
/// ```nobuild
/// for (en, pos, mesh_or_scene) in world
/// .view::<(Entities, &Transform, Or<&Mesh, &Scene>)>()
/// .iter()
/// {
/// // do some things with the position of the entities
///
/// let (mesh, scene) = mesh_or_scene;
///
/// // now handle do things with the Mesh or Scene that the entity could have
/// if let Some(mesh) = mesh {
/// // do mesh things
/// }
///
/// if let Some(scene) = scene {
/// // do scene things
/// }
/// }
/// ```
///
/// Or as a filter
/// ```nobuild
/// for (en, pos) in world
/// .filtered_view::<
/// (Entities, &Transform),
/// Or<Has<Mesh>, Has<Scene>>
/// >()
/// .iter()
/// {
/// // this entity has a `Transform`, and either a Mesh or Scene, or both!
/// }
/// ```
pub struct Or<Q1, Q2> {
_marker: PhantomData<(Q1, Q2)>
}
impl<Q1: AsQuery, Q2: AsQuery> AsQuery for Or<Q1, Q2> {
type Query = OrQuery<Q1::Query, Q2::Query, (Q1, Q2)>;
}
impl<F1: AsFilter, F2: AsFilter> AsFilter for Or<F1, F2> {
type Filter = OrFilter<F1, F2>;
}

View file

@ -0,0 +1,45 @@
use std::marker::PhantomData;
use crate::{query::{AsQuery, Query}, Archetype, Component, DynTypeId, World};
use super::StaticFetcher;
/// A filter query for entities that do not have the component `C`.
///
/// See [`With`].
#[derive(Default)]
pub struct Without<C: Component> {
_marker: PhantomData<C>
}
impl<C: Component> Copy for Without<C> {}
impl<C: Component> Clone for Without<C> {
fn clone(&self) -> Self {
Self { _marker: self._marker.clone() }
}
}
impl<C: Component> Query for Without<C> {
type Item<'a> = bool;
type Fetch<'a> = StaticFetcher<bool>;
fn new() -> Self {
Without {
_marker: PhantomData
}
}
fn can_visit_archetype(&self, archetype: &Archetype) -> bool {
!archetype.has_column(DynTypeId::of::<C>())
}
unsafe fn fetch<'a>(&self, _world: &'a World, _: &'a Archetype, _: crate::Tick) -> Self::Fetch<'a> {
// if fetch is called, it means that 'can_visit_archetype' returned true
StaticFetcher::new(true)
}
}
impl<C: Component> AsQuery for Without<C> {
type Query = Self;
}

View file

@ -31,9 +31,14 @@ mod optional;
#[allow(unused_imports)]
pub use optional::*;
mod world_tick;
#[allow(unused_imports)]
pub use world_tick::*;
pub mod dynamic;
pub mod filter;
pub use filter::{Filter, AsFilter};
/// A [`Fetch`]er implementation gets data out of an archetype.
pub trait Fetch<'a> {
@ -94,19 +99,19 @@ pub trait IntoQuery {
}
impl<'a> Fetch<'a> for () {
type Item = ();
type Item = bool;
fn dangling() -> Self {
unreachable!()
}
unsafe fn get_item(&mut self, _: ArchetypeEntityId) -> Self::Item {
()
true
}
}
impl Query for () {
type Item<'a> = ();
type Item<'a> = bool;
type Fetch<'a> = ();

View file

@ -2,7 +2,7 @@ use std::marker::PhantomData;
use atomic_refcell::{AtomicRef, AtomicRefMut};
use crate::{World, resource::ResourceObject};
use crate::{resource::ResourceObject, Tick, TrackedResource, World};
use super::{Query, Fetch, AsQuery};
@ -22,12 +22,18 @@ impl<'a, T: ResourceObject + 'a> Fetch<'a> for FetchResource<'a, T> {
}
fn can_visit_item(&mut self, _entity: crate::world::ArchetypeEntityId) -> bool {
true
let w = self.world.unwrap();
w.has_resource::<T>()
}
unsafe fn get_item(&mut self, _entity: crate::world::ArchetypeEntityId) -> Self::Item {
let w = self.world.unwrap();
Res(w.get_resource::<T>())
Res {
// this unwrap is safe since `can_visit_item` ensures the resource exists
inner: w.get_tracked_resource::<T>().unwrap(),
world_tick: w.current_tick(),
_marker: PhantomData::<T>,
}
}
}
@ -81,13 +87,37 @@ impl<R: ResourceObject> AsQuery for QueryResource<R> {
}
/// A struct used for querying resources from the World.
pub struct Res<'a, T: ResourceObject>(pub(crate) AtomicRef<'a, T>);
pub struct Res<'a, T: ResourceObject> {
pub(crate) inner: AtomicRef<'a, TrackedResource<dyn ResourceObject>>,
pub(crate) world_tick: Tick,
pub(crate) _marker: PhantomData<T>,
}
impl<'a, T: ResourceObject> std::ops::Deref for Res<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.0.deref()
self.inner.res.as_any().downcast_ref::<T>().unwrap()
}
}
impl<'a, T: ResourceObject> Res<'a, T> {
/// Get the inner [`AtomicRef`].
///
/// This inner type does not have change tracking. If you `DerefMut` it, the change will not be tracked!
#[inline]
pub fn get_inner(self) -> atomic_refcell::AtomicRef<'a, T> {
atomic_refcell::AtomicRef::map(self.inner, |r| r.res.as_any().downcast_ref().unwrap())
}
/// Returns a boolean indicating if the resource changed.
pub fn changed(&self) -> bool {
self.inner.changed(self.world_tick)
}
/// The tick that this resource was last modified at
pub fn tick(&self) -> Tick {
self.inner.tick
}
}
@ -111,12 +141,18 @@ impl<'a, T: ResourceObject + 'a> Fetch<'a> for FetchResourceMut<'a, T> {
}
fn can_visit_item(&mut self, _entity: crate::world::ArchetypeEntityId) -> bool {
true
let w = self.world.unwrap();
w.has_resource::<T>()
}
unsafe fn get_item(&mut self, _entity: crate::world::ArchetypeEntityId) -> Self::Item {
let w = self.world.unwrap();
ResMut(w.get_resource_mut::<T>())
ResMut {
// this is safe since `can_visit_item` ensures that the resource exists.
inner: w.get_tracked_resource_mut::<T>().unwrap(),
world_tick: w.current_tick(),
_marker: PhantomData::<T>,
}
}
}
@ -169,20 +205,51 @@ impl<R: ResourceObject> AsQuery for QueryResourceMut<R> {
}
/// A struct used for querying resources from the World.
pub struct ResMut<'a, T: ResourceObject>(pub(crate) AtomicRefMut<'a, T>);
//pub struct ResMut<'a, T: ResourceObject>(pub(crate) AtomicRefMut<'a, T>);
pub struct ResMut<'a, T: ResourceObject> {
pub(crate) inner: AtomicRefMut<'a, TrackedResource<dyn ResourceObject>>,
pub(crate) world_tick: Tick,
pub(crate) _marker: PhantomData<T>,
}
impl<'a, T: ResourceObject> std::ops::Deref for ResMut<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.0.deref()
self.inner.res.as_any().downcast_ref::<T>().unwrap()
}
}
impl<'a, T: ResourceObject> std::ops::DerefMut for ResMut<'a, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.deref_mut()
self.mark_changed();
self.inner.res.as_any_mut().downcast_mut::<T>().unwrap()
}
}
impl<'a, T: ResourceObject> ResMut<'a, T> {
/// Get the inner [`AtomicRefMut`].
///
/// This inner type does not have change tracking. If you `DerefMut` it, the change will not be tracked!
pub fn get_inner(self) -> atomic_refcell::AtomicRefMut<'a, T> {
atomic_refcell::AtomicRefMut::map(self.inner, |r| r.res.as_any_mut().downcast_mut().unwrap())
}
pub fn changed(&self) -> bool {
self.inner.changed(self.world_tick)
}
/// The tick that this resource was last modified at
pub fn tick(&self) -> Tick {
self.inner.tick
}
/// Manually mark the resource as changed.
///
/// This is useful if you have a resource with interior mutability and you want to
/// mark this resource as changed.
pub fn mark_changed(&mut self) {
self.inner.tick = self.world_tick
}
}

View file

@ -1,8 +1,8 @@
use crate::World;
use super::{Query, Fetch, AsQuery};
use super::{Query, Fetch, AsQuery, Filter, AsFilter, filter::FilterFetch, ArchetypeEntityId};
impl<'a, F1> Fetch<'a> for (F1,)
/* impl<'a, F1> Fetch<'a> for (F1,)
where
F1: Fetch<'a>,
{
@ -102,7 +102,7 @@ where
Q2: AsQuery,
{
type Query = (Q1::Query, Q2::Query);
}
} */
macro_rules! impl_bundle_tuple {
( $($name: ident),+ ) => (
@ -139,10 +139,7 @@ macro_rules! impl_bundle_tuple {
fn can_visit_archetype(&self, archetype: &crate::archetype::Archetype) -> bool {
let ( $($name,)+ ) = self;
// this is the only way I could figure out how to do an 'and'
let bools = vec![$($name.can_visit_archetype(archetype),)+];
bools.iter().all(|b| *b)
$($name.can_visit_archetype(archetype)) &&+
}
unsafe fn fetch<'a>(&self, world: &'a World, archetype: &'a crate::archetype::Archetype, tick: crate::Tick) -> Self::Fetch<'a> {
@ -154,10 +151,43 @@ macro_rules! impl_bundle_tuple {
impl<$($name: AsQuery),+> AsQuery for ($($name,)+) {
type Query = ($($name::Query,)+);
}
#[allow(non_snake_case)]
impl<'a, $($name: FilterFetch<'a>),+> FilterFetch<'a> for ($($name,)+) {
fn can_visit(&mut self, entity: ArchetypeEntityId) -> bool {
let ( $($name,)+ ) = self;
$($name.can_visit(entity)) &&+
}
}
#[allow(non_snake_case)]
impl<$($name: Filter),+> Filter for ($($name,)+) {
type Fetch<'a> = ($(<$name as Filter>::Fetch<'a>,)+);
fn new() -> Self {
( $($name::new(),)+ )
}
fn can_visit_archetype(&self, archetype: &crate::archetype::Archetype) -> bool {
let ( $($name,)+ ) = self;
$($name.can_visit_archetype(archetype)) &&+
}
unsafe fn fetch<'a>(&self, world: &'a World, archetype: &'a crate::archetype::Archetype, tick: crate::Tick) -> Self::Fetch<'a> {
let ( $($name,)+ ) = self;
( $($name.fetch(world, archetype, tick),)+ )
}
}
impl<$($name: AsFilter),+> AsFilter for ($($name,)+) {
type Filter = ($($name::Filter,)+);
}
);
}
// Hopefully up to 16 queries in a SINGLE view is enough
impl_bundle_tuple! { Q1 }
impl_bundle_tuple! { Q1, Q2 }
impl_bundle_tuple! { Q1, Q2, Q3 }
impl_bundle_tuple! { Q1, Q2, Q3, Q4 }
impl_bundle_tuple! { Q1, Q2, Q3, Q4, Q5 }

View file

@ -1,12 +1,21 @@
use std::ops::Range;
use std::{ops::Range, ptr::NonNull};
use crate::{archetype::Archetype, world::{ArchetypeEntityId, World}, EntityId, Tick};
use crate::{
archetype::Archetype,
system::FnArgFetcher,
world::{ArchetypeEntityId, World},
Entity, Tick,
};
use super::{Query, Fetch, AsQuery};
use super::{
filter::{AsFilter, Filter, FilterFetch},
AsQuery, Fetch, Query,
};
pub type View<'a, Q, F = ()> = ViewState<'a, <Q as AsQuery>::Query, <F as AsQuery>::Query>;
pub struct ViewState<'a, Q: Query, F: Query> {
#[derive(Clone)]
pub struct ViewState<'a, Q: Query, F: Filter> {
world: &'a World,
query: Q,
filter: F,
@ -16,7 +25,7 @@ pub struct ViewState<'a, Q: Query, F: Query> {
impl<'a, Q, F> ViewState<'a, Q, F>
where
Q: Query,
F: Query,
F: Filter,
{
pub fn new(world: &'a World, query: Q, filter: F, archetypes: Vec<&'a Archetype>) -> Self {
Self {
@ -32,32 +41,46 @@ where
self.into_iter()
}
pub fn get_one(self) -> Option<Q::Item<'a>> {
self.into_iter().next()
}
/// Consumes `self`, adding a query to the view.
pub fn expand<U: AsQuery>(self, query: U::Query) -> ViewState<'a, (Q, U::Query), F> {
ViewState::new(self.world, (self.query, query), self.filter, self.archetypes)
ViewState::new(
self.world,
(self.query, query),
self.filter,
self.archetypes,
)
}
/// Consumes `self`, adding a filter to the view.
pub fn with<U: AsQuery>(self, filter: U::Query) -> ViewState<'a, Q, (F, U::Query)> {
ViewState::new(self.world, self.query, (self.filter, filter), self.archetypes)
pub fn with<U: AsFilter>(self, filter: U::Filter) -> ViewState<'a, Q, (F, U::Filter)> {
ViewState::new(
self.world,
self.query,
(self.filter, filter),
self.archetypes,
)
}
}
impl<'a, Q, F> IntoIterator for ViewState<'a, Q, F>
where
Q: Query,
F: Query,
F: Filter,
{
type Item = Q::Item<'a>;
type IntoIter = ViewIter<'a, Q, F>;
fn into_iter(self) -> Self::IntoIter {
let tick = self.world.tick_tracker().tick_when(Q::MUTATES);
//let tick = self.world.tick_tracker().tick_when(Q::MUTATES);
ViewIter {
world: self.world,
tick,
tick: self.world.current_tick(),
query: self.query,
filter: self.filter,
fetcher: None,
@ -69,7 +92,7 @@ where
}
}
pub struct ViewIter<'a, Q: Query, F: Query> {
pub struct ViewIter<'a, Q: Query, F: Filter> {
world: &'a World,
tick: Tick,
query: Q,
@ -84,7 +107,7 @@ pub struct ViewIter<'a, Q: Query, F: Query> {
impl<'a, Q, F> Iterator for ViewIter<'a, Q, F>
where
Q: Query,
F: Query,
F: Filter,
{
type Item = Q::Item<'a>;
@ -95,7 +118,7 @@ where
// fetcher gets set to Some after this `next` call.
if self.fetcher.is_none() {
if let Some(mut fetch) = unsafe { self.query.fetch_world(self.world) } {
let res = unsafe { Some(fetch.get_item(ArchetypeEntityId(0))) };
let res = unsafe { Some(fetch.get_item(ArchetypeEntityId(0))) };
self.fetcher = Some(fetch);
return res;
}
@ -109,7 +132,7 @@ where
let filter_fetcher = self.filter_fetcher.as_mut().unwrap();
let entity_index = ArchetypeEntityId(entity_index);
if fetcher.can_visit_item(entity_index) && filter_fetcher.can_visit_item(entity_index) {
if fetcher.can_visit_item(entity_index) && filter_fetcher.can_visit(entity_index) {
let i = unsafe { fetcher.get_item(entity_index) };
return Some(i);
}
@ -138,28 +161,31 @@ where
}
}
pub struct ViewOne<'a, Q: Query> {
pub type ViewOne<'a, Q> = ViewOneState<'a, <Q as AsQuery>::Query>;
pub struct ViewOneState<'a, Q: Query> {
world: &'a World,
tick: Tick,
entity: EntityId,
query: Q,
}
impl<'a, Q: Query> ViewOne<'a, Q> {
pub fn new(world: &'a World, entity: EntityId, query: Q) -> Self {
let tick = world.tick_tracker().tick_when(Q::MUTATES);
impl<'a, Q: Query> ViewOneState<'a, Q> {
pub fn new(world: &'a World, query: Q) -> Self {
//let tick = world.tick_tracker().tick_when(Q::MUTATES);
Self {
world,
tick,
entity,
query
tick: world.current_tick(),
query,
}
}
pub fn get(&self) -> Option<Q::Item<'a>> {
if let Some(record) = self.world.entities.arch_index.get(&self.entity) {
let arch = self.world.archetypes.get(&record.id)
pub fn get(&self, entity: Entity) -> Option<Q::Item<'a>> {
if let Some(record) = self.world.entities.arch_index.get(&entity.id()) {
let arch = self
.world
.archetypes
.get(&record.id)
.expect("An invalid record was specified for an entity");
if self.query.can_visit_archetype(arch) {
@ -169,7 +195,36 @@ impl<'a, Q: Query> ViewOne<'a, Q> {
}
}
}
None
}
}
/// Consumes `self`, adding a query to the view.
pub fn expand<U: AsQuery>(self, query: U::Query) -> ViewOneState<'a, (Q, U::Query)> {
//ViewOneState::new(self.world, (self.query, query), self.filter, self.archetypes)
ViewOneState {
world: &self.world,
tick: self.tick,
query: (self.query, query),
}
}
}
impl<Q: Query> FnArgFetcher for ViewOneState<'_, Q> {
type State = ();
type Arg<'a, 'state> = ViewOneState<'a, Q>;
unsafe fn get<'a, 'state>(
_: &'state mut Self::State,
world: NonNull<World>,
) -> Self::Arg<'a, 'state> {
let world = world.as_ref();
ViewOneState::<Q>::new(world, Q::new())
}
fn apply_deferred<'state>(_: &'state mut Self::State, _: NonNull<World>) {}
fn create_state(_: NonNull<World>) -> Self::State {
()
}
}

View file

@ -0,0 +1,102 @@
use std::ops::Deref;
use crate::{system::FnArgFetcher, Tick, World};
use super::{Fetch, Query, AsQuery};
/// Fetcher used to fetch the current tick of the world.
pub struct FetchWorldTick {
tick: Tick
}
impl<'a> Fetch<'a> for FetchWorldTick {
type Item = WorldTick;
fn dangling() -> Self {
unreachable!()
}
fn can_visit_item(&mut self, _entity: crate::ArchetypeEntityId) -> bool {
true
}
unsafe fn get_item(&mut self, _entity: crate::world::ArchetypeEntityId) -> Self::Item {
WorldTick(self.tick)
}
}
/// Query used to query the current tick of the world.
#[derive(Clone, Copy)]
pub struct QueryWorldTick;
impl Default for QueryWorldTick {
fn default() -> Self {
Self
}
}
impl Query for QueryWorldTick {
type Item<'a> = WorldTick;
type Fetch<'a> = FetchWorldTick;
const ALWAYS_FETCHES: bool = true;
fn new() -> Self {
QueryWorldTick
}
fn can_visit_archetype(&self, _archetype: &crate::archetype::Archetype) -> bool {
true
}
unsafe fn fetch<'a>(&self, world: &'a World, _archetype: &'a crate::archetype::Archetype, _tick: crate::Tick) -> Self::Fetch<'a> {
FetchWorldTick {
tick: world.current_tick()
}
}
unsafe fn fetch_world<'a>(&self, world: &'a World) -> Option<Self::Fetch<'a>> {
Some(FetchWorldTick {
tick: world.current_tick()
})
}
}
impl AsQuery for QueryWorldTick {
type Query = Self;
}
/// Type that can be used in an fn system for fetching the current world tick.
#[derive(Debug, Clone, Copy)]
pub struct WorldTick(Tick);
impl Deref for WorldTick {
type Target = Tick;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsQuery for WorldTick {
type Query = QueryWorldTick;
}
impl FnArgFetcher for WorldTick {
type State = ();
type Arg<'a, 'state> = WorldTick;
fn create_state(_: std::ptr::NonNull<World>) -> Self::State {
()
}
unsafe fn get<'a, 'state>(_: &'state mut Self::State, world: std::ptr::NonNull<World>) -> Self::Arg<'a, 'state> {
let world = world.as_ref();
WorldTick(world.current_tick())
}
fn apply_deferred<'state>(_: &'state mut Self::State, _: std::ptr::NonNull<World>) {
}
}

View file

@ -2,7 +2,9 @@ use std::marker::PhantomData;
use lyra_ecs_derive::Component;
use crate::query::Filter;
use crate::query::Query;
use crate::query::ViewOneState;
use crate::query::ViewState;
use crate::Entity;
@ -15,6 +17,9 @@ pub use relates_to::*;
mod relate_pair;
pub use relate_pair::*;
mod related_by;
pub use related_by::*;
mod child_of;
pub use child_of::*;
@ -95,10 +100,11 @@ impl World {
self.insert(origin, comp);
}
}
impl<'a, Q, F> ViewState<'a, Q, F>
where
Q: Query,
F: Query,
F: Filter,
{
/// Consumes `self` to return a view that fetches the relation to a specific target entity.
pub fn relates_to<R>(self, target: Entity) -> ViewState<'a, (Q, QueryRelatesTo<R>), F>
@ -109,6 +115,15 @@ where
self.expand::<RelatesTo<R>>(rel)
}
pub fn related_by<R>(self, target: Entity) -> ViewState<'a, Q, (F, QueryRelatedBy<R>)>
where
R: Relation,
{
let rel = QueryRelatedBy::new(Some(target));
self.with::<QueryRelatedBy<R>>(rel)
}
/// Consumes `self` to return a view that fetches the origin, target, and a reference to
/// the relation that the entities have together.
pub fn relate_pair<R>(self) -> ViewState<'a, (Q, QueryRelatePair<R>), F>
@ -120,6 +135,30 @@ where
}
}
impl<'a, Q> ViewOneState<'a, Q>
where
Q: Query,
{
/// Consumes `self` to return a view that fetches the relation to a specific target entity.
pub fn relates_to<R>(self, target: Entity) -> ViewOneState<'a, (Q, QueryRelatesTo<R>)>
where
R: Relation,
{
let rel = QueryRelatesTo::new(Some(target));
self.expand::<RelatesTo<R>>(rel)
}
/// Consumes `self` to return a view that fetches the origin, target, and a reference to
/// the relation that the entities have together.
pub fn relate_pair<R>(self) -> ViewOneState<'a, (Q, QueryRelatePair<R>)>
where
R: Relation,
{
let rel = QueryRelatePair::<R>::new();
self.expand::<RelatePair<R>>(rel)
}
}
#[cfg(test)]
mod tests {
use crate::{query::{Entities, ViewState}, relation::QueryRelatesTo, tests::Vec2, World};

View file

@ -0,0 +1,94 @@
use std::{any::TypeId, cell::Ref, marker::PhantomData};
use crate::{query::{AsQuery, Fetch, Query}, ComponentColumn, Entity, World};
use super::{Relation, RelationTargetComponent};
pub struct FetchRelatedBy<'a, T> {
col: &'a ComponentColumn,
origin: Entity,
_phantom: PhantomData<&'a T>
}
impl<'a, R> Fetch<'a> for FetchRelatedBy<'a, R>
where
R: Relation,
{
type Item = bool;
fn dangling() -> Self {
unreachable!()
}
fn can_visit_item(&mut self, entity: crate::ArchetypeEntityId) -> bool {
unsafe {
let comp: Ref<RelationTargetComponent<R>> = self.col.get(entity.0 as usize);
comp.origin == self.origin
}
}
unsafe fn get_item(&mut self, _: crate::world::ArchetypeEntityId) -> Self::Item {
true
}
}
pub struct QueryRelatedBy<R> {
target: Option<Entity>,
_marker: PhantomData<R>,
}
impl<R> Copy for QueryRelatedBy<R> {}
impl<R> Clone for QueryRelatedBy<R> {
fn clone(&self) -> Self {
*self
}
}
impl<R> QueryRelatedBy<R> {
pub fn new(target: Option<Entity>) -> Self {
Self {
target,
_marker: PhantomData
}
}
}
impl<R> Query for QueryRelatedBy<R>
where
R: Relation + 'static
{
type Item<'a> = bool;
type Fetch<'a> = FetchRelatedBy<'a, R>;
fn new() -> Self {
panic!("RelatedBy MUST be made with View::related_by since it requires State provided by \
that function.")
}
fn can_visit_archetype(&self, archetype: &crate::archetype::Archetype) -> bool {
archetype.has_column(TypeId::of::<RelationTargetComponent<R>>())
}
unsafe fn fetch<'a>(&self, _world: &'a World, archetype: &'a crate::archetype::Archetype, tick: crate::Tick) -> Self::Fetch<'a> {
let _ = tick;
let col = archetype.get_column(TypeId::of::<RelationTargetComponent<R>>())
.expect("You ignored 'can_visit_archetype'!");
FetchRelatedBy {
col,
origin: self.target.expect("Filter not initialized"),
_phantom: PhantomData,
}
}
}
/// A filter that returns entities that are the target of a relation.
pub struct RelatedBy<R: Relation> {
_marker: PhantomData<R>,
}
impl<R: Relation> AsQuery for RelatedBy<R> {
type Query = QueryRelatedBy<R>;
}

View file

@ -2,6 +2,8 @@ use std::{any::{Any, TypeId}, sync::Arc};
use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut};
use crate::Tick;
/// Shorthand for `Send + Sync + 'static`, so it never needs to be implemented manually.
pub trait ResourceObject: Send + Sync + Any {
fn as_any(&self) -> &dyn Any;
@ -18,18 +20,31 @@ impl<T: Send + Sync + Any> ResourceObject for T {
}
}
pub struct TrackedResource<T: ?Sized> {
pub tick: Tick,
pub res: T,
}
impl<T: ?Sized> TrackedResource<T> {
pub fn changed(&self, tick: Tick) -> bool {
let tick = tick.checked_sub(1).unwrap_or(0);
//println!("self: {}, world: {}", *self.tick, tick);
*self.tick >= tick
}
}
/// A type erased storage for a Resource.
#[derive(Clone)]
pub struct ResourceData {
pub(crate) data: Arc<AtomicRefCell<dyn ResourceObject>>,
pub(crate) data: Arc<AtomicRefCell<TrackedResource<dyn ResourceObject>>>,
type_id: TypeId,
}
impl ResourceData {
pub fn new<T: ResourceObject>(data: T) -> Self {
pub fn new<T: ResourceObject>(data: T, tick: Tick) -> Self {
Self {
data: Arc::new(AtomicRefCell::new(data)),
data: Arc::new(AtomicRefCell::new(TrackedResource { tick, res: data })),
type_id: TypeId::of::<T>(),
}
}
@ -46,7 +61,7 @@ impl ResourceData {
/// * If the data is already borrowed mutably, this will panic.
/// * If the type of `T` is not the same as the resource type.
pub fn get<T: ResourceObject>(&self) -> AtomicRef<T> {
AtomicRef::map(self.data.borrow(), |a| a.as_any().downcast_ref().unwrap())
AtomicRef::map(self.data.borrow(), |a| a.res.as_any().downcast_ref().unwrap())
}
/// Mutably borrow the data inside of the resource.
@ -56,7 +71,7 @@ impl ResourceData {
/// * If the data is already borrowed mutably, this will panic.
/// * If the type of `T` is not the same as the resource type.
pub fn get_mut<T: ResourceObject>(&self) -> AtomicRefMut<T> {
AtomicRefMut::map(self.data.borrow_mut(), |a| a.as_any_mut().downcast_mut().unwrap())
AtomicRefMut::map(self.data.borrow_mut(), |a| a.res.as_any_mut().downcast_mut().unwrap())
}
/// Borrow the data inside of the resource.
@ -66,7 +81,7 @@ impl ResourceData {
/// * If the type of `T` is not the same as the resource type.
pub fn try_get<T: ResourceObject>(&self) -> Option<AtomicRef<T>> {
self.data.try_borrow()
.map(|r| AtomicRef::map(r, |a| a.as_any().downcast_ref().unwrap()))
.map(|r| AtomicRef::map(r, |a| a.res.as_any().downcast_ref().unwrap()))
.ok()
}
@ -77,7 +92,11 @@ impl ResourceData {
/// * If the type of `T` is not the same as the resource type.
pub fn try_get_mut<T: ResourceObject>(&self) -> Option<AtomicRefMut<T>> {
self.data.try_borrow_mut()
.map(|r| AtomicRefMut::map(r, |a| a.as_any_mut().downcast_mut().unwrap()))
.map(|r| AtomicRefMut::map(r, |a| a.res.as_any_mut().downcast_mut().unwrap()))
.ok()
}
pub fn changed(&self, tick: Tick) -> bool {
self.data.borrow().changed(tick)
}
}

View file

@ -1,7 +1,7 @@
use std::{any::Any, marker::PhantomData, ptr::NonNull};
use paste::paste;
use crate::{World, Access, ResourceObject, query::{Query, ViewState, ResMut, Res}};
use crate::{World, Access, ResourceObject, query::{Query, Filter, ViewState, ResMut, Res}};
use super::{System, IntoSystem};
@ -20,6 +20,7 @@ pub trait FnArgFetcher {
Access::Read
}
// TODO: check if the fetcher can fetch before getting.
/// Get the arg from the world
///
/// # Safety
@ -28,7 +29,7 @@ pub trait FnArgFetcher {
unsafe fn get<'a, 'state>(state: &'state mut Self::State, world: NonNull<World>) -> Self::Arg<'a, 'state>;
/// Apply some action after the system was ran.
fn apply_deferred(state: Self::State, world: NonNull<World>);
fn apply_deferred<'state>(state: &'state mut Self::State, world: NonNull<World>);
}
/// A system that is implemented as a function.
@ -63,22 +64,32 @@ macro_rules! impl_fn_system_tuple {
fn execute(&mut self, world: NonNull<World>) -> anyhow::Result<()> {
unsafe {
paste! {
// initialize state if it hasn't been
if self.arg_state.is_none() {
self.arg_state = Some(vec![
$(
Box::new($name::create_state(world))
),+
]);
}
// safe unwrap since state was just initialized
let mut state_iter = self.arg_state.as_mut().unwrap().iter_mut();
$(
// get the arg fetcher, create its state, and get the arg
let mut [<state_ $name:lower>]: $name::State = $name::create_state(world);
let mut [<state_ $name:lower>]: &mut $name::State = state_iter.next()
// Safe since state vec is initialized to expected size
//.unwrap()
.expect("state was not initialized to correct size")
.downcast_mut()
// Safe due to the ordering of the arguments ensuring the correct
// types.
.unwrap_or_else(|| panic!("Failed to downcast state '{}' from 'Box<dyn Any>'",
std::any::type_name::<$name::State>()));
let [<$name:lower>] = $name::get(&mut [<state_ $name:lower>], world);
)+
(self.inner)($( [<$name:lower>] ),+)?;
let mut state = Vec::new();
$(
// type erase the now modified state, and store it
let boxed = Box::new([<state_ $name:lower>]) as Box<dyn Any>;
state.push(boxed);
)+
self.arg_state = Some(state);
}
Ok(())
@ -86,14 +97,16 @@ macro_rules! impl_fn_system_tuple {
}
fn execute_deferred(&mut self, world: NonNull<World>) -> anyhow::Result<()> {
let state = self.arg_state.as_mut().expect("Somehow there was no state");
state.reverse();
let mut state = self.arg_state.as_mut()
.expect("State was never initialized")
.iter_mut();
$(
let arg_state_box = state.pop()
.expect("Missing expected arg state");
let arg_state = *arg_state_box.downcast::<$name::State>()
.expect("Somehow the state cannot be downcasted from boxed Any");
let arg_state_box = state.next()
.expect("Invalid number of state was initialized");
let arg_state = arg_state_box.downcast_mut::<$name::State>()
.unwrap_or_else(|| panic!("Failed to downcast state '{}' from 'Box<dyn Any>'",
std::any::type_name::<$name::State>()));
$name::apply_deferred(arg_state, world);
)+
@ -130,6 +143,7 @@ impl_fn_system_tuple!{ A, B, C, D, E, F2, G }
impl_fn_system_tuple!{ A, B, C, D, E, F2, G, H }
impl_fn_system_tuple!{ A, B, C, D, E, F2, G, H, I }
impl_fn_system_tuple!{ A, B, C, D, E, F2, G, H, I, J }
impl_fn_system_tuple!{ A, B, C, D, E, F2, G, H, I, J, K }
impl_fn_system_tuple!{ A, B, C, D, E, F2, G, H, I, J, K, L }
impl_fn_system_tuple!{ A, B, C, D, E, F2, G, H, I, J, K, L, M }
impl_fn_system_tuple!{ A, B, C, D, E, F2, G, H, I, J, K, L, M, N }
@ -140,7 +154,7 @@ impl_fn_system_tuple!{ A, B, C, D, E, F2, G, H, I, J, K, L, M, N, O, P }
impl<'c, Q, F> FnArgFetcher for ViewState<'c, Q, F>
where
Q: Query + 'static,
F: Query + 'static,
F: Filter + 'static,
{
type State = (Q, F);
type Arg<'a, 'state> = ViewState<'a, Q, F>;
@ -158,7 +172,7 @@ where
v
}
fn apply_deferred(_: Self::State, _: NonNull<World>) { }
fn apply_deferred<'state>(_: &'state mut Self::State, _: NonNull<World>) { }
fn create_state(_: NonNull<World>) -> Self::State {
(Q::new(), F::new())
@ -177,7 +191,7 @@ impl FnArgFetcher for &'_ World {
&*world.as_ptr()
}
fn apply_deferred(_: Self::State, _: NonNull<World>) { }
fn apply_deferred<'state>(_: &'state mut Self::State, _: NonNull<World>) { }
fn create_state(_: NonNull<World>) -> Self::State { () }
}
@ -194,7 +208,7 @@ impl FnArgFetcher for &'_ mut World {
&mut *world.as_ptr()
}
fn apply_deferred(_: Self::State, _: NonNull<World>) { }
fn apply_deferred<'state>(_: &'state mut Self::State, _: NonNull<World>) { }
fn create_state(_: NonNull<World>) -> Self::State { () }
}
@ -213,10 +227,12 @@ impl<R: ResourceObject> FnArgFetcher for Res<'_, R> {
unsafe fn get<'a, 'state>(_: &'state mut Self::State, world: NonNull<World>) -> Self::Arg<'a, 'state> {
let world = world.as_ref();
Res(world.get_resource::<R>())
world.get_resource::<R>()
// TODO: check if the resource exists before attempting to fetch
.unwrap_or_else(|| panic!("world is missing resource: {}", std::any::type_name::<R>()))
}
fn apply_deferred(_: Self::State, _: NonNull<World>) { }
fn apply_deferred<'state>(_: &'state mut Self::State, _: NonNull<World>) { }
fn create_state(_: NonNull<World>) -> Self::State { () }
}
@ -227,10 +243,12 @@ impl<R: ResourceObject> FnArgFetcher for ResMut<'_, R> {
unsafe fn get<'a, 'state>(_: &'state mut Self::State, world: NonNull<World>) -> Self::Arg<'a, 'state> {
let world = world.as_ref();
ResMut(world.get_resource_mut::<R>())
world.get_resource_mut::<R>()
// TODO: check if the resource exists before attempting to fetch
.unwrap_or_else(|| panic!("world is missing resource: {}", std::any::type_name::<R>()))
}
fn apply_deferred(_: Self::State, _: NonNull<World>) { }
fn apply_deferred<'state>(_: &'state mut Self::State, _: NonNull<World>) { }
fn create_state(_: NonNull<World>) -> Self::State { () }
}
@ -306,7 +324,8 @@ mod tests {
world.add_resource(SomeCounter(0));
let test_system = |world: &World| -> anyhow::Result<()> {
let mut counter = world.get_resource_mut::<SomeCounter>();
let mut counter = world.get_resource_mut::<SomeCounter>()
.expect("Counter resource is missing");
counter.0 += 10;
Ok(())
@ -315,7 +334,8 @@ mod tests {
test_system.into_system().execute(NonNull::from(&world)).unwrap();
let test_system = |world: &mut World| -> anyhow::Result<()> {
let mut counter = world.get_resource_mut::<SomeCounter>();
let mut counter = world.get_resource_mut::<SomeCounter>()
.expect("Counter resource is missing");
counter.0 += 10;
Ok(())
@ -323,7 +343,8 @@ mod tests {
test_system.into_system().execute(NonNull::from(&world)).unwrap();
let counter = world.get_resource::<SomeCounter>();
let counter = world.get_resource::<SomeCounter>()
.expect("Counter resource is missing");
assert_eq!(counter.0, 20);
}
@ -335,7 +356,8 @@ mod tests {
world.add_resource(SomeCounter(0));
let test_system = |world: &World| -> anyhow::Result<()> {
let mut counter = world.get_resource_mut::<SomeCounter>();
let mut counter = world.get_resource_mut::<SomeCounter>()
.expect("Counter resource is missing");
counter.0 += 10;
Ok(())
@ -345,7 +367,8 @@ mod tests {
#[allow(dead_code)]
fn test_system(world: &mut World) -> anyhow::Result<()> {
let mut counter = world.get_resource_mut::<SomeCounter>();
let mut counter = world.get_resource_mut::<SomeCounter>()
.expect("Counter resource is missing");
counter.0 += 10;
Ok(())
@ -353,7 +376,8 @@ mod tests {
test_system.into_system().execute(NonNull::from(&world)).unwrap();
let counter = world.get_resource::<SomeCounter>();
let counter = world.get_resource::<SomeCounter>()
.expect("Counter resource is missing");
assert_eq!(counter.0, 20);
}
@ -365,16 +389,15 @@ mod tests {
world.add_resource(SomeCounter(0));
let test_system = |mut counter: ResMut<SomeCounter>| -> anyhow::Result<()> {
// .0 is twice here since ResMut's tuple field is pub(crate).
// Users wont need to do this
counter.0.0 += 10;
counter.0 += 10;
Ok(())
};
test_system.into_system().execute(NonNull::from(&world)).unwrap();
let counter = world.get_resource::<SomeCounter>();
let counter = world.get_resource::<SomeCounter>()
.expect("Counter resource is missing");
assert_eq!(counter.0, 10);
}
@ -388,9 +411,7 @@ mod tests {
let test_system = |mut counter: ResMut<SomeCounter>, view: ViewState<QueryBorrow<Vec2>, ()>| -> anyhow::Result<()> {
for v2 in view.into_iter() {
println!("Got v2 at '{:?}'", v2);
// .0 is twice here since ResMut's tuple field is pub(crate).
// Users wont need to do this
counter.0.0 += 1;
counter.0 += 1;
}
Ok(())
@ -398,7 +419,8 @@ mod tests {
test_system.into_system().execute(NonNull::from(&world)).unwrap();
let counter = world.get_resource::<SomeCounter>();
let counter = world.get_resource::<SomeCounter>()
.expect("Counter resource is missing");
assert_eq!(counter.0, 2);
}
}

View file

@ -103,7 +103,7 @@ impl GraphExecutor {
}
let world = unsafe { world_ptr.as_mut() };
if let Some(mut queue) = world.try_get_resource_mut::<CommandQueue>() {
if let Some(mut queue) = world.get_resource_mut::<CommandQueue>() {
// Safety: Commands only borrows world.entities when adding commands
let world = unsafe { world_ptr.as_mut() };
let mut commands = Commands::new(&mut queue, world);
@ -189,7 +189,8 @@ mod tests {
exec.execute(NonNull::from(&world), true).unwrap();
println!("Executed systems");
let order = world.get_resource::<Vec<String>>();
let order = world.get_resource::<Vec<String>>()
.expect("missing Vec<String> resource");
let mut order_iter = order.iter();
// ... but still executed in order

View file

@ -3,7 +3,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
/// TickTracker is used for tracking changes of [`Component`](crate::Component)s and entities.
///
/// TickTracker stores an [`AtomicU64`], making all operations on `TickTracker`, atomic as well.
/// Note that [`Tick::clone`] only clones the inner value of atomic, and not the atomic itself.
/// Note that [`TickTracker::clone`] only clones the inner value of atomic, and not the atomic itself.
#[derive(Debug, Default)]
pub struct TickTracker {
tick: AtomicU64,
@ -72,6 +72,12 @@ impl TickTracker {
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, PartialOrd, Ord)]
pub struct Tick(u64);
impl From<u64> for Tick {
fn from(value: u64) -> Self {
Self(value)
}
}
impl std::ops::Deref for Tick {
type Target = u64;

1085
crates/lyra-ecs/src/world.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -10,34 +10,32 @@ lyra-ecs = { path = "../lyra-ecs", features = [ "math" ] }
lyra-reflect = { path = "../lyra-reflect", features = [ "math" ] }
lyra-math = { path = "../lyra-math" }
lyra-scene = { path = "../lyra-scene" }
lyra-gltf = { path = "../lyra-gltf" }
wgsl_preprocessor = { path = "../wgsl-preprocessor" }
winit = "0.28.1"
wgpu = "0.15.1"
winit = "0.30.5"
wgpu = { version = "22.1.0" }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = [ "tracing-log" ] }
tracing-log = "0.1.3"
tracing-appender = "0.2.2"
tracing-tracy = { version = "0.11.0", optional = true }
async-std = { version = "1.12.0", features = [ "unstable", "attributes" ] }
cfg-if = "1"
bytemuck = { version = "1.12", features = [ "derive", "min_const_generics" ] }
image = { version = "0.24", default-features = false, features = ["png", "jpeg"] }
image = "0.25.2"
anyhow = "1.0"
instant = "0.1"
async-trait = "0.1.65"
glam = { version = "0.24.0", features = ["bytemuck", "debug-glam-assert"] }
gilrs-core = "0.5.6"
glam = { version = "0.29.0", features = ["bytemuck", "debug-glam-assert"] }
syn = "2.0.26"
quote = "1.0.29"
uuid = { version = "1.5.0", features = ["v4", "fast-rng"] }
itertools = "0.11.0"
itertools = "0.13.0"
thiserror = "1.0.56"
unique = "0.9.1"
rustc-hash = "1.1.0"
rustc-hash = "2.0.0"
petgraph = { version = "0.6.5", features = ["matrix_graph"] }
bind_match = "0.1.2"
[features]
tracy = ["dep:tracing-tracy"]
round_mult = "0.1.3"
fast_poisson = { version = "1.0.0", features = ["single_precision"] }
atomic_refcell = "0.1.13"
rand = "0.8.5"

View file

@ -1,5 +1,5 @@
use instant::Instant;
use lyra_ecs::{Component, World};
use lyra_ecs::{query::ResMut, Component};
use lyra_reflect::Reflect;
use crate::{plugin::Plugin, game::GameStages};
@ -30,9 +30,8 @@ impl std::ops::DerefMut for DeltaTime {
/// A system that updates the [`DeltaTime``] resource.
///
/// The resource is updated in the [`GameStages::First`] stage.
pub fn delta_time_system(world: &mut World) -> anyhow::Result<()> {
pub fn delta_time_system(mut delta: ResMut<DeltaTime>) -> anyhow::Result<()> {
let now = Instant::now();
let mut delta = world.get_resource_mut::<DeltaTime>();
delta.0 = delta.1.unwrap_or(now).elapsed().as_secs_f32();
delta.1 = Some(now);
@ -42,8 +41,8 @@ pub fn delta_time_system(world: &mut World) -> anyhow::Result<()> {
pub struct DeltaTimePlugin;
impl Plugin for DeltaTimePlugin {
fn setup(&self, game: &mut crate::game::Game) {
game.world_mut().add_resource(DeltaTime(0.0, None));
game.add_system_to_stage(GameStages::First, "delta_time", delta_time_system, &[]);
fn setup(&mut self, app: &mut crate::game::App) {
app.world.add_resource(DeltaTime(0.0, None));
app.add_system_to_stage(GameStages::First, "delta_time", delta_time_system, &[]);
}
}

View file

@ -0,0 +1,260 @@
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
use atomic_refcell::AtomicRefCell;
use lyra_ecs::{
query::{ResMut, WorldTick},
system::FnArgFetcher,
Tick,
};
pub trait Event: Clone + Send + Sync + 'static {}
impl<T: Clone + Send + Sync + 'static> Event for T {}
/// A Vec with other Vecs in it to track relative age of items.
///
/// The vec has 3 levels, a `newest`, `medium` and `old`. Items are pushed to the `newest`
/// internal vec. When [`WaterfallVec::waterfall`] is called the items in `newest` are
/// put into `medium`, and items in `medium` goes to `old`.
///
/// By checking the items in each internal vec, you can see a relative age between the items.
/// The event system uses this to clear the `old` vec to ensure keep events for only two
/// frames at a time.
struct WaterfallVec<T> {
newest: Vec<T>,
medium: Vec<T>,
old: Vec<T>,
}
impl<T> Default for WaterfallVec<T> {
fn default() -> Self {
Self {
newest: Default::default(),
medium: Default::default(),
old: Default::default(),
}
}
}
impl<T> WaterfallVec<T> {
fn total_len(&self) -> usize {
self.newest.len() + self.medium.len() + self.old.len()
}
fn get(&self, mut i: usize) -> Option<&T> {
if i >= self.old.len() {
i -= self.old.len();
if i >= self.medium.len() {
i -= self.medium.len();
self.newest.get(i)
} else {
self.medium.get(i)
}
} else {
self.old.get(i)
}
}
/// Age elements.
///
/// This moves elements in `newest` to `medium` and elements in `medium` to `old`.
/// This is what drives the relative age of the [`WaterfallVec`].
fn waterfall(&mut self) {
self.old.append(&mut self.medium);
self.medium.append(&mut self.newest);
}
/// Push a new element to the newest queue.
fn push(&mut self, event: T) {
self.newest.push(event);
}
/// Clear oldest items.
fn clear_oldest(&mut self) {
self.old.clear();
}
}
pub struct Events<T: Event> {
events: Arc<AtomicRefCell<WaterfallVec<T>>>,
/// Used to track when the old events were last cleared.
last_cleared_at: Tick,
/// Used to indicate when the cursor in readers should be reset to zero.
/// This becomes true after the old events are cleared.
//reset_cursor: bool,
/// This is set to the amount of elements that were deleted.
///
/// Its used to decrease the cursor by the amount of elements deleted, instead of resetting
/// to zero to avoid rereading events that are in other levels of the vec.
decrease_cursor_by: Option<usize>,
}
impl<T: Event> Default for Events<T> {
fn default() -> Self {
Self {
events: Default::default(),
last_cleared_at: Default::default(),
decrease_cursor_by: None,
}
}
}
impl<T: Event> Events<T> {
pub fn new() -> Self {
Self::default()
}
pub fn push_event(&mut self, event: T) {
let mut events = self.events.borrow_mut();
events.push(event);
}
pub fn reader(&self) -> EventReader<T> {
EventReader {
events: self.events.clone(),
cursor: Arc::new(AtomicUsize::new(0)),
}
}
pub fn writer(&self) -> EventWriter<T> {
EventWriter {
events: self.events.clone(),
}
}
}
pub struct EventReader<T: Event> {
events: Arc<AtomicRefCell<WaterfallVec<T>>>,
cursor: Arc<AtomicUsize>,
}
impl<T: Event> EventReader<T> {
pub fn read(&self) -> Option<T> {
let events = self.events.borrow();
let cursor = self.cursor.load(Ordering::Acquire);
if cursor >= events.total_len() {
None
} else {
let e = events.get(cursor).unwrap();
self.cursor.store(cursor + 1, Ordering::Release);
Some(e.clone())
}
}
}
pub struct EventWriter<T: Event> {
events: Arc<AtomicRefCell<WaterfallVec<T>>>,
}
impl<T: Event> EventWriter<T> {
pub fn write(&self, event: T) {
let mut events = self.events.borrow_mut();
events.push(event);
}
}
/// Clean events of event type `T` every 2 ticks.
pub fn event_cleaner_system<T>(tick: WorldTick, mut events: ResMut<Events<T>>) -> anyhow::Result<()>
where
T: Event,
{
let last_tick = *events.last_cleared_at;
let world_tick = **tick;
if last_tick + 2 < world_tick {
events.last_cleared_at = *tick;
let mut events_fall = events.events.borrow_mut();
// Since oldest will be cleared, we need to decrease the cursor by the removed amount
// store the amount it needs to decrease by.
let old_len = events_fall.old.len();
events_fall.clear_oldest();
drop(events_fall);
events.decrease_cursor_by = Some(old_len);
} else {
events.decrease_cursor_by = None;
}
let mut events = events.events.borrow_mut();
events.waterfall();
Ok(())
}
impl<T: Event> FnArgFetcher for EventReader<T> {
type State = Arc<AtomicUsize>;
type Arg<'b, 'state> = EventReader<T>;
fn create_state(_: std::ptr::NonNull<lyra_ecs::World>) -> Self::State {
Arc::new(AtomicUsize::new(0))
}
unsafe fn get<'b, 'state>(
state: &'state mut Self::State,
world: std::ptr::NonNull<lyra_ecs::World>,
) -> Self::Arg<'b, 'state> {
let world = world.as_ref();
let events = world.get_resource::<Events<T>>().unwrap_or_else(|| {
panic!(
"world missing Events<{}> resource",
std::any::type_name::<T>()
)
});
if let Some(dec_by) = events.decrease_cursor_by {
// The waterfall vec had its oldest events deleted.
// The cursor needs to be decreased by the amount of elements deleted, instead of resetting
// to zero to avoid rereading events that are in other levels of the vec.
let mut s = state.load(Ordering::Acquire);
s = s.checked_sub(dec_by).unwrap_or_default();
state.store(s, Ordering::Release);
//*state = state.checked_sub(dec_by).unwrap_or_default();
}
let reader = EventReader {
events: events.events.clone(),
cursor: state.clone(),
};
reader
}
fn apply_deferred<'state>(_: &'state mut Self::State, _: std::ptr::NonNull<lyra_ecs::World>) {}
}
impl<T: Event> FnArgFetcher for EventWriter<T> {
type State = ();
type Arg<'a, 'state> = EventWriter<T>;
fn create_state(_: std::ptr::NonNull<lyra_ecs::World>) -> Self::State {
()
}
unsafe fn get<'a, 'state>(
_: &'state mut Self::State,
world: std::ptr::NonNull<lyra_ecs::World>,
) -> Self::Arg<'a, 'state> {
let world = world.as_ref();
let events = world.get_resource::<Events<T>>().unwrap_or_else(|| {
panic!(
"world missing Events<{}> resource",
std::any::type_name::<T>()
)
});
EventWriter {
events: events.events.clone(),
}
}
fn apply_deferred<'state>(_: &'state mut Self::State, _: std::ptr::NonNull<lyra_ecs::World>) {}
}

208
crates/lyra-game/src/game.rs Executable file
View file

@ -0,0 +1,208 @@
use std::{cell::OnceCell, collections::VecDeque, ptr::NonNull};
use lyra_ecs::{system::{IntoSystem, System}, ResourceObject, World};
use lyra_math::IVec2;
use tracing::{error, info};
use crate::{event_cleaner_system, plugin::Plugin, render::renderer::Renderer, Event, Events, Stage, StagedExecutor};
#[derive(Clone, Copy, Hash, Debug)]
pub enum GameStages {
/// This stage runs before all other stages.
First,
/// This stage runs before `Update`.
PreUpdate,
/// This stage is where most game logic would be.
Update,
/// This stage is ran after `Update`.
PostUpdate,
/// This stage runs after all other stages.
Last,
}
impl Stage for GameStages {}
pub struct Controls<'a> {
pub world: &'a mut World,
}
#[derive(Clone, Default)]
pub struct WindowState {
/// Indicates if the window is currently focused.
pub focused: bool,
/// Indicates if the window is currently occluded.
pub occluded: bool,
/// Indicates if the cursor is inside of the window.
pub cursor_inside_window: bool,
pub position: IVec2,
}
impl WindowState {
pub fn new() -> Self {
Self::default()
}
}
pub struct App {
pub(crate) renderer: OnceCell<Box<dyn Renderer>>,
pub world: World,
plugins: VecDeque<Box<dyn Plugin>>,
startup_systems: VecDeque<Box<dyn System>>,
staged_exec: StagedExecutor,
run_fn: OnceCell<Box<dyn FnOnce(App)>>,
}
impl App {
pub fn new() -> Self {
let world = World::new();
// initialize ecs system stages
let mut staged = StagedExecutor::new();
staged.add_stage(GameStages::First);
staged.add_stage_after(GameStages::First, GameStages::PreUpdate);
staged.add_stage_after(GameStages::PreUpdate, GameStages::Update);
staged.add_stage_after(GameStages::Update, GameStages::PostUpdate);
staged.add_stage_after(GameStages::PostUpdate, GameStages::Last);
Self {
renderer: OnceCell::new(),
world,
plugins: Default::default(),
startup_systems: Default::default(),
staged_exec: staged,
run_fn: OnceCell::new(),
}
}
pub fn update(&mut self) {
self.world.tick();
let wptr = NonNull::from(&self.world);
if let Err(e) = self.staged_exec.execute(wptr, true) {
error!("Error when executing staged systems: '{}'", e);
}
}
pub(crate) fn on_resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
self.renderer.get_mut()
.expect("renderer was not initialized")
.on_resize(&mut self.world, new_size);
}
pub(crate) fn on_exit(&mut self) {
info!("On exit!");
}
pub fn add_resource<T: ResourceObject>(&mut self, data: T) {
self.world.add_resource(data);
}
/// Add a system to the ecs world
pub fn with_system<S, A>(&mut self, name: &str, system: S, depends: &[&str]) -> &mut Self
where
S: IntoSystem<A>,
<S as IntoSystem<A>>::System: 'static
{
self.staged_exec.add_system_to_stage(GameStages::Update, name, system.into_system(), depends);
self
}
/// Add a stage.
///
/// This stage could run at any moment if nothing is dependent on it.
pub fn add_stage<T: Stage>(&mut self, stage: T) -> &mut Self {
self.staged_exec.add_stage(stage);
self
}
/// Add a stage that executes after another one.
///
/// Parameters:
/// * `before` - The stage that will run before `after`.
/// * `after` - The stage that will run after `before`.
pub fn add_stage_after<T: Stage, U: Stage>(&mut self, before: T, after: U) -> &mut Self {
self.staged_exec.add_stage_after(before, after);
self
}
/// Add a system to an already existing stage.
///
/// # Panics
/// Panics if the stage was not already added to the executor
pub fn add_system_to_stage<T, S, A>(&mut self, stage: T,
name: &str, system: S, depends: &[&str]) -> &mut Self
where
T: Stage,
S: IntoSystem<A>,
<S as IntoSystem<A>>::System: 'static
{
self.staged_exec.add_system_to_stage(stage, name, system.into_system(), depends);
self
}
/// Add a startup system that will be ran right after plugins are setup.
/// They will only be ran once
pub fn with_startup_system<S>(&mut self, system: S) -> &mut Self
where
S: System + 'static
{
self.startup_systems.push_back(Box::new(system));
self
}
/// Add a plugin to the game. These are executed as they are added.
pub fn with_plugin<P>(&mut self, mut plugin: P) -> &mut Self
where
P: Plugin + 'static
{
plugin.setup(self);
let plugin = Box::new(plugin);
self.plugins.push_back(plugin);
self
}
/// Override the default (empty) world
///
/// This isn't recommended, you should create a startup system and add it to `with_startup_system`
pub fn with_world(&mut self, world: World) -> &mut Self {
self.world = world;
self
}
pub fn set_run_fn<F>(&self, f: F)
where
F: FnOnce(App) + 'static
{
// ignore if a runner function was already set
let _ = self.run_fn.set(Box::new(f));
}
pub fn run(mut self) {
let f = self.run_fn.take()
.expect("No run function set");
f(self);
}
pub fn register_event<T: Event>(&mut self) {
let world = &mut self.world;
// only register the event if it isn't already registered.
if !world.has_resource::<Events<T>>() {
world.add_resource_default::<Events<T>>();
let sys_name = format!("{}_event_cleaner_system", std::any::type_name::<T>().to_lowercase());
self.add_system_to_stage(GameStages::First, &sys_name, event_cleaner_system::<T>, &[]);
}
}
pub fn push_event<T: Event>(&mut self, event: T) {
let world = &mut self.world;
let mut events = world.get_resource_mut::<Events<T>>()
.expect("missing events for event type! Must use `App::register_event` first");
events.push_event(event);
}
}

View file

@ -1,19 +1,23 @@
use std::{collections::HashMap, ops::Deref, hash::{Hash, DefaultHasher, Hasher}, fmt::Debug};
use std::{
collections::HashMap,
fmt::Debug,
hash::{DefaultHasher, Hash, Hasher},
};
use glam::Vec2;
use lyra_ecs::World;
use lyra_ecs::query::{Res, ResMut};
use lyra_reflect::Reflect;
use crate::{plugin::Plugin, game::GameStages, EventQueue};
use crate::{game::GameStages, plugin::Plugin, EventReader};
use super::{Button, InputButtons, KeyCode, MouseButton, MouseMotion};
pub trait ActionLabel: Debug {
pub trait ActionLabel: Debug + Clone {
/// Returns a unique hash of the label.
fn label_hash(&self) -> u64;
}
impl<T: Hash + Debug> ActionLabel for T {
impl<T: Hash + Debug + Clone> ActionLabel for T {
fn label_hash(&self) -> u64 {
let mut s = DefaultHasher::new();
self.hash(&mut s);
@ -21,6 +25,24 @@ impl<T: Hash + Debug> ActionLabel for T {
}
}
/// An [`ActionLabel`]'s value.
///
/// This is used to store the [`label_hash`](ActionLabel::label_hash) of the label.
#[derive(Debug, Clone, Copy, Reflect)]
pub struct ActionLabelValue(pub u64);
impl ActionLabelValue {
pub fn new<T: ActionLabel>(label: T) -> Self {
Self(label.label_hash())
}
}
impl ActionLabel for ActionLabelValue {
fn label_hash(&self) -> u64 {
self.0
}
}
#[derive(Clone)]
pub struct ActionLabelWrapper(String, u64);
@ -44,7 +66,7 @@ impl<A: ActionLabel + Hash> From<A> for ActionLabelWrapper {
}
/// Some commonly used action labels.
///
///
/// The built-in systems uses these labels
#[derive(Clone, Copy, Hash, Debug)]
pub enum CommonActionLabel {
@ -125,7 +147,7 @@ pub enum MouseInput {
pub enum ActionSource {
Keyboard(KeyCode),
Gamepad(GamepadFormat, GamepadInput),
Mouse(MouseInput)
Mouse(MouseInput),
}
impl ActionSource {
@ -158,10 +180,7 @@ impl Binding {
/// Create a binding from a Source, with a modifier
pub fn from_source_modifier(source: ActionSource, modifier: f32) -> Self {
Self {
source,
modifier,
}
Self { source, modifier }
}
}
@ -175,7 +194,7 @@ pub enum ActionState {
JustPressed(f32),
/// The button was just released
JustReleased,
//Released,
/// The axis moved
Axis(f32),
@ -186,7 +205,7 @@ pub enum ActionState {
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum ActionKind {
Button,
Axis
Axis,
}
#[derive(Clone, Debug)]
@ -261,15 +280,15 @@ impl ActionMapping {
}
/// Creates a binding for the action.
///
///
/// If the action is not in this layout, this will panic!
///
///
/// Parameters:
/// * `action` - The label corresponding to the action in this Layout.
/// * `bind` - The Binding to add to the Action.
pub fn bind<L>(&mut self, action: L, bindings: &[Binding]) -> &mut Self
where
L: ActionLabel
L: ActionLabel,
{
let mut bindings = bindings.to_vec();
@ -290,18 +309,20 @@ pub struct ActionMappingBuilder {
impl ActionMappingBuilder {
fn new(mapping: ActionMapping) -> Self {
Self {
mapping,
}
Self { mapping }
}
pub fn bind<L>(mut self, action: L, bindings: &[Binding]) -> Self
where
L: ActionLabel
L: ActionLabel,
{
let mut bindings = bindings.to_vec();
let action_binds = self.mapping.action_binds.entry(action.label_hash()).or_default();
let action_binds = self
.mapping
.action_binds
.entry(action.label_hash())
.or_default();
action_binds.append(&mut bindings);
self
@ -339,14 +360,14 @@ impl ActionHandler {
pub fn action<L>(&self, label: L) -> Option<&Action>
where
L: ActionLabel
L: ActionLabel,
{
self.actions.get(&label.label_hash())
}
pub fn add_action<L>(&mut self, label: L, action: Action)
where
L: ActionLabel
L: ActionLabel,
{
self.actions.insert(label.label_hash(), action);
}
@ -357,67 +378,65 @@ impl ActionHandler {
}
/// Returns true if the action is pressed (or was just pressed).
///
/// This will panic if the action name does not correspond to an action.
pub fn is_action_pressed<L>(&self, action: L) -> bool
///
/// Returns `None` if the action was not found.
pub fn is_action_pressed<L>(&self, action: L) -> Option<bool>
where
L: ActionLabel
L: ActionLabel,
{
let action = self.actions.get(&action.label_hash())
.unwrap_or_else(|| panic!("Action {action:?} was not found"));
let action = self.actions.get(&action.label_hash())?;
matches!(action.state, ActionState::Pressed(_) | ActionState::JustPressed(_))
Some(matches!(
action.state,
ActionState::Pressed(_) | ActionState::JustPressed(_)
))
}
/// Returns true if the action was just pressed.
///
/// This will panic if the action name does not correspond to an action.
pub fn was_action_just_pressed<L>(&self, action: L) -> bool
///
/// Returns `None` if the action was not found.
pub fn was_action_just_pressed<L>(&self, action: L) -> Option<bool>
where
L: ActionLabel
L: ActionLabel,
{
let action = self.actions.get(&action.label_hash())
.unwrap_or_else(|| panic!("Action {action:?} was not found"));
let action = self.actions.get(&action.label_hash())?;
matches!(action.state, ActionState::JustPressed(_))
Some(matches!(action.state, ActionState::JustPressed(_)))
}
/// Returns true if the action was just released.
///
/// This will panic if the action name does not correspond to an action.
pub fn was_action_just_released<L>(&self, action: L) -> bool
///
/// Returns `None` if the action was not found.
pub fn was_action_just_released<L>(&self, action: L) -> Option<bool>
where
L: ActionLabel
L: ActionLabel,
{
let action = self.actions.get(&action.label_hash())
.unwrap_or_else(|| panic!("Action {action:?} was not found"));
let action = self.actions.get(&action.label_hash())?;
matches!(action.state, ActionState::JustReleased)
Some(matches!(action.state, ActionState::JustReleased))
}
/// Returns an action's state.
///
/// This will panic if the action name does not correspond to an action.
pub fn get_action_state<L>(&self, action: L) -> ActionState
///
/// Returns `None` if the action was not found.
pub fn get_action_state<L>(&self, action: L) -> Option<ActionState>
where
L: ActionLabel
L: ActionLabel,
{
let action = self.actions.get(&action.label_hash())
.unwrap_or_else(|| panic!("Action {action:?} was not found"));
let action = self.actions.get(&action.label_hash())?;
action.state
Some(action.state)
}
/// Returns the action's modifier if it is pressed (or was just pressed).
/// Returns `None` if the action's state is not `ActionState::Pressed` or `ActionState::JustPressed`.
///
/// This will panic if the action name does not correspond to an action.
///
/// Returns `None` if the action's state is not `ActionState::Pressed`, `ActionState::JustPressed`,
/// or if the action was not found.
pub fn get_pressed_modifier<L>(&self, action: L) -> Option<f32>
where
L: ActionLabel
L: ActionLabel,
{
let action = self.actions.get(&action.label_hash())
.unwrap_or_else(|| panic!("Action {action:?} was not found"));
let action = self.actions.get(&action.label_hash())?;
match action.state {
ActionState::Pressed(v) | ActionState::JustPressed(v) => Some(v),
@ -426,32 +445,30 @@ impl ActionHandler {
}
/// Returns the action's modifier if it was just pressed.
/// Returns `None` if the action's state is not `ActionState::JustPressed`.
///
/// This will panic if the action name does not correspond to an action.
///
/// Returns `None` if the action's state is not `ActionState::JustPressed`,
/// or if the action was not found.
pub fn get_just_pressed_modifier<L>(&self, action: L) -> Option<f32>
where
L: ActionLabel
L: ActionLabel,
{
let action = self.actions.get(&action.label_hash())
.unwrap_or_else(|| panic!("Action {action:?} was not found"));
let action = self.actions.get(&action.label_hash())?;
match action.state {
ActionState::JustPressed(v) => Some(v),
_ => None,
}
}
/// Returns the action's modifier if its an updated axis.
/// Returns `None` if the action's state is not `ActionState::Axis`.
///
/// This will panic if the action name does not correspond to an action.
///
/// Returns `None` if the action's state is not `ActionState::Axis`,
/// or if the action was not found.
pub fn get_axis_modifier<L>(&self, action: L) -> Option<f32>
where
L: ActionLabel
L: ActionLabel,
{
let action = self.actions.get(&action.label_hash())
.unwrap_or_else(|| panic!("Action {action:?} was not found"));
let action = self.actions.get(&action.label_hash())?;
match action.state {
ActionState::Axis(v) => Some(v),
@ -478,7 +495,7 @@ impl ActionHandlerBuilder {
pub fn add_action<L>(mut self, label: L, action: Action) -> Self
where
L: ActionLabel
L: ActionLabel,
{
self.handler.actions.insert(label.label_hash(), action);
@ -497,83 +514,100 @@ impl ActionHandlerBuilder {
}
}
fn actions_system(world: &mut World) -> anyhow::Result<()> {
let keys = world.try_get_resource::<InputButtons<KeyCode>>()
.map(|r| r.deref().clone());
let mouse_events = world
.try_get_resource::<EventQueue>()
.and_then(|q| q.read_events::<MouseMotion>());
//let mouse = world.try_get_resource()
//fn actions_system(world: &mut World) -> anyhow::Result<()> {
fn actions_system(
input_btns: Res<InputButtons<KeyCode>>,
mouse_ev: EventReader<MouseMotion>,
mut handler: ResMut<ActionHandler>,
) -> anyhow::Result<()> {
// clear the states of all axises each frame
{
let mut handler = world.try_get_resource_mut::<ActionHandler>()
.expect("No Input Action handler was created in the world!");
let layout = handler.layouts.get(&handler.current_layout).expect("No active layout");
let mapping = layout.mappings.get(&layout.active_mapping).expect("No active mapping");
for (action, _) in mapping.action_binds.clone().iter() {
let action = handler.actions.get_mut(action).expect("Action name for binding is invalid!");
if action.kind == ActionKind::Axis {
action.state = ActionState::Axis(0.0);
}
let layout = handler
.layouts
.get(&handler.current_layout)
.expect("No active layout");
let mapping = layout
.mappings
.get(&layout.active_mapping)
.expect("No active mapping");
for (action, _) in mapping.action_binds.clone().iter() {
let action = handler
.actions
.get_mut(action)
.expect("Action name for binding is invalid!");
if action.kind == ActionKind::Axis {
action.state = ActionState::Axis(0.0);
}
}
let motion_avg = if let Some(mouse_events) = mouse_events {
// collect all events to a list
let mouse_events = {
let mut evs = vec![];
while let Some(ev) = mouse_ev.read() {
evs.push(ev.clone());
}
evs
};
// get the average motion from the events that were collected last frame
let motion_avg = if !mouse_events.is_empty() {
let count = mouse_events.len();
let mut sum = Vec2::ZERO;
for mm in mouse_events {
sum += mm.delta;
}
Some(sum / count as f32)
} else { None };
let mut handler = world.try_get_resource_mut::<ActionHandler>()
.expect("No Input Action handler was created in the world!");
let layout = handler.layouts.get(&handler.current_layout).expect("No active layout");
let mapping = layout.mappings.get(&layout.active_mapping).expect("No active mapping");
sum / count as f32
} else {
Vec2::new(0.0, 0.0)
};
let layout = handler
.layouts
.get(&handler.current_layout)
.expect("No active layout");
let mapping = layout
.mappings
.get(&layout.active_mapping)
.expect("No active mapping");
for (action_lbl, binds) in mapping.action_binds.clone().iter() {
let action = handler.actions.get_mut(action_lbl).expect("Action name for binding is invalid!");
let action = handler
.actions
.get_mut(action_lbl)
.expect("Action name for binding is invalid!");
let mut new_state = None;
for bind in binds.iter() {
match bind.source {
ActionSource::Keyboard(key) => if let Some(keys) = &keys {
ActionSource::Keyboard(key) => {
// JustPressed needs to be first, since is_pressed includes buttons that
// were just pressed.
match action.kind {
ActionKind::Button => {
if keys.was_just_pressed(key) {
if input_btns.was_just_pressed(key) {
new_state = Some(ActionState::JustPressed(bind.modifier));
} else if keys.is_pressed(key) {
} else if input_btns.is_pressed(key) {
new_state = Some(ActionState::Pressed(bind.modifier));
}
},
}
ActionKind::Axis => {
if keys.is_pressed(key) {
if input_btns.is_pressed(key) {
new_state = Some(ActionState::Axis(bind.modifier));
}
}
}
},
}
ActionSource::Gamepad(_, _) => todo!(),
ActionSource::Mouse(m) => match m {
MouseInput::Button(_) => todo!(),
MouseInput::Axis(a) => if let Some(motion_avg) = motion_avg {
match a {
MouseAxis::X => {
new_state = Some(ActionState::Axis(motion_avg.x));
},
MouseAxis::Y => {
new_state = Some(ActionState::Axis(motion_avg.y));
},
MouseAxis::ScrollWheel => todo!(),
MouseInput::Axis(a) => match a {
MouseAxis::X => {
new_state = Some(ActionState::Axis(motion_avg.x));
}
MouseAxis::Y => {
new_state = Some(ActionState::Axis(motion_avg.y));
}
MouseAxis::ScrollWheel => todo!(),
},
},
}
@ -583,7 +617,7 @@ fn actions_system(world: &mut World) -> anyhow::Result<()> {
action.state = new_state;
} else if action.kind == ActionKind::Button {
match action.state {
ActionState::Idle => {},
ActionState::Idle => {}
ActionState::JustReleased => action.state = ActionState::Idle,
_ => action.state = ActionState::JustReleased,
}
@ -597,7 +631,7 @@ fn actions_system(world: &mut World) -> anyhow::Result<()> {
pub struct InputActionPlugin;
impl Plugin for InputActionPlugin {
fn setup(&self, game: &mut crate::game::Game) {
game.add_system_to_stage(GameStages::PreUpdate, "input_actions", actions_system, &[]);
fn setup(&mut self, app: &mut crate::game::App) {
app.add_system_to_stage(GameStages::PreUpdate, "input_actions", actions_system, &[]);
}
}
}

View file

@ -92,10 +92,7 @@ impl<T: Button> InputButtons<T> {
pub fn was_just_pressed(&self, button: T) -> bool {
let hash = Self::get_button_hash(&button);
match self.button_events.get(&hash) {
Some(button_event) => match button_event {
ButtonEvent::JustPressed(b) if button == *b => true,
_ => false,
},
Some(button_event) => matches!(button_event, ButtonEvent::JustPressed(b) if button == *b),
None => false
}
}
@ -105,11 +102,8 @@ impl<T: Button> InputButtons<T> {
/// This must be done so that a key does not stay as JustPressed between multiple ticks
pub fn update(&mut self) {
for bev in self.button_events.values_mut() {
match bev {
ButtonEvent::JustPressed(btn) => {
*bev = ButtonEvent::Pressed(btn.clone());
},
_ => {},
if let ButtonEvent::JustPressed(btn) = bev {
*bev = ButtonEvent::Pressed(btn.clone());
}
}
}

View file

@ -88,6 +88,8 @@ pub enum MouseButton {
Left,
Right,
Middle,
Back,
Forward,
Other(u16),
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,143 @@
use glam::Vec2;
use lyra_ecs::query::ResMut;
use winit::{
event::{MouseScrollDelta, WindowEvent},
keyboard::PhysicalKey,
};
use crate::{game::GameStages, plugin::Plugin, winit::DeviceEventPair, EventReader, EventWriter};
use super::{events::*, InputButtons, KeyCode};
fn write_scroll_delta(mouse_scroll_ev: &mut EventWriter<MouseScroll>, delta: &MouseScrollDelta) {
let event = match delta {
MouseScrollDelta::LineDelta(x, y) => MouseScroll {
unit: MouseScrollUnit::Line(Vec2::new(*x, *y)),
},
MouseScrollDelta::PixelDelta(delta) => MouseScroll {
unit: MouseScrollUnit::Pixel(Vec2::new(delta.x as f32, delta.y as f32)),
},
};
mouse_scroll_ev.write(event);
}
fn write_key_event(
key_buttons: &mut ResMut<InputButtons<KeyCode>>,
physical_key: PhysicalKey,
state: winit::event::ElementState,
) {
if let PhysicalKey::Code(code) = physical_key {
key_buttons.add_input_from_winit(KeyCode::from(code), state);
}
}
pub fn input_system(
mut key_code_res: ResMut<InputButtons<KeyCode>>,
mut mouse_btn_res: ResMut<InputButtons<MouseButton>>,
mut touches_res: ResMut<Touches>,
window_ev: EventReader<WindowEvent>,
device_ev: EventReader<DeviceEventPair>,
mut mouse_scroll_ev: EventWriter<MouseScroll>,
mouse_btn_ev: EventWriter<MouseButton>,
mouse_exact_ev: EventWriter<MouseExact>,
mouse_entered_ev: EventWriter<CursorEnteredWindow>,
mouse_left_ev: EventWriter<CursorLeftWindow>,
mouse_motion_ev: EventWriter<MouseMotion>,
) -> anyhow::Result<()> {
while let Some(event) = window_ev.read() {
match event {
WindowEvent::KeyboardInput { event, .. } => {
write_key_event(&mut key_code_res, event.physical_key, event.state);
}
WindowEvent::CursorMoved { position, .. } => {
let exact = MouseExact {
pos: Vec2::new(position.x as f32, position.y as f32),
};
mouse_exact_ev.write(exact);
}
WindowEvent::CursorEntered { .. } => {
mouse_entered_ev.write(CursorEnteredWindow);
}
WindowEvent::CursorLeft { .. } => {
mouse_left_ev.write(CursorLeftWindow);
}
WindowEvent::MouseWheel { delta, .. } => {
write_scroll_delta(&mut mouse_scroll_ev, &delta);
}
WindowEvent::MouseInput { button, state, .. } => {
let button_event = match button {
winit::event::MouseButton::Left => MouseButton::Left,
winit::event::MouseButton::Right => MouseButton::Right,
winit::event::MouseButton::Middle => MouseButton::Middle,
winit::event::MouseButton::Back => MouseButton::Back,
winit::event::MouseButton::Forward => MouseButton::Forward,
winit::event::MouseButton::Other(v) => MouseButton::Other(v),
};
mouse_btn_ev.write(button_event);
mouse_btn_res.add_input_from_winit(button_event, state);
}
WindowEvent::Touch(t) => {
let touch = Touch {
phase: TouchPhase::from(t.phase),
location: Vec2::new(t.location.x as f32, t.location.y as f32),
force: t.force.map(Force::from),
finger_id: t.id,
};
touches_res.touches.push(touch);
}
_ => {}
}
}
while let Some(device) = device_ev.read() {
match &device.event {
winit::event::DeviceEvent::Motion { .. } => {
// TODO: handle device motion events
// A todo! isn't used since these are triggered alongside MouseMotion events
}
winit::event::DeviceEvent::MouseMotion { delta } => {
let delta = MouseMotion {
delta: Vec2::new(delta.0 as f32, delta.1 as f32),
};
mouse_motion_ev.write(delta);
}
winit::event::DeviceEvent::MouseWheel { delta } => {
write_scroll_delta(&mut mouse_scroll_ev, delta);
}
winit::event::DeviceEvent::Key(key) => {
write_key_event(&mut key_code_res, key.physical_key, key.state);
}
_ => {
todo!("unhandled device event: {:?}", device.event);
}
}
}
Ok(())
}
/// Plugin that runs InputSystem
#[derive(Default)]
pub struct InputPlugin;
impl Plugin for InputPlugin {
fn setup(&mut self, app: &mut crate::game::App) {
app.add_resource(InputButtons::<KeyCode>::default());
app.add_resource(InputButtons::<MouseButton>::default());
app.add_resource(Touches::default());
app.register_event::<MouseScroll>();
app.register_event::<MouseButton>();
app.register_event::<MouseMotion>();
app.register_event::<MouseExact>();
app.register_event::<CursorEnteredWindow>();
app.register_event::<CursorLeftWindow>();
app.add_system_to_stage(GameStages::PreUpdate, "input", input_system, &[]);
}
}

View file

@ -1,5 +1,4 @@
#![feature(hash_extract_if)]
#![feature(lint_reasons)]
#![feature(trait_alias)]
#![feature(map_many_mut)]
@ -9,12 +8,14 @@ pub mod game;
pub mod render;
pub mod resources;
pub mod input;
pub mod winit;
pub mod as_any;
pub mod plugin;
pub mod change_tracker;
pub mod events;
pub use events::*;
pub mod sprite;
mod event;
pub use event::*;
pub mod stage;
pub use stage::*;
@ -28,8 +29,6 @@ pub use lyra_resource as assets;
pub use lyra_ecs as ecs;
pub use lyra_math as math;
pub use lyra_reflect as reflect;
#[cfg(feature = "scripting")]
pub use lyra_scripting as script;
pub use lyra_gltf as gltf;
pub use plugin::DefaultPlugins;

View file

@ -1,35 +1,38 @@
use lyra_ecs::query::ResMut;
use lyra_ecs::CommandQueue;
use lyra_gltf::GltfLoader;
use lyra_resource::ResourceManager;
use crate::EventsPlugin;
use crate::game::App;
use crate::render::PreprocessShaderLoader;
use crate::winit::{WinitPlugin, WindowPlugin};
use crate::DeltaTimePlugin;
use crate::game::Game;
use crate::input::InputPlugin;
use crate::render::window::WindowPlugin;
/// A Plugin is something you can add to a `Game` that can be used to define systems, or spawn initial entities.
pub trait Plugin {
/// Setup this plugin. This runs before the game has started
fn setup(&self, game: &mut Game);
/// Setup this plugin. This runs before the app has started
fn setup(&mut self, app: &mut App);
fn is_ready(&self, _game: &mut Game) -> bool {
fn is_ready(&self, app: &mut App) -> bool {
let _ = app;
true
}
fn complete(&self, _game: &mut Game) {
fn complete(&self, app: &mut App) {
let _ = app;
}
fn cleanup(&self, _game: &mut Game) {
fn cleanup(&self, app: &mut App) {
let _ = app;
}
}
impl<P> Plugin for P
where P: Fn(&mut Game)
where P: Fn(&mut App)
{
fn setup(&self, game: &mut Game) {
self(game);
fn setup(&mut self, app: &mut App) {
self(app);
}
}
@ -56,9 +59,9 @@ impl PluginSet {
}
impl Plugin for PluginSet {
fn setup(&self, game: &mut Game) {
for plugin in self.plugins.iter() {
plugin.setup(game);
fn setup(&mut self, app: &mut App) {
for plugin in self.plugins.iter_mut() {
plugin.setup(app);
}
}
}
@ -98,8 +101,11 @@ impl_tuple_plugin_set! { (C0, 0) (C1, 1) (C2, 2) (C3, 3) (C4, 4) (C5, 5) (C6, 6)
pub struct ResourceManagerPlugin;
impl Plugin for ResourceManagerPlugin {
fn setup(&self, game: &mut Game) {
game.world_mut().add_resource(ResourceManager::new());
fn setup(&mut self, app: &mut App) {
let rm = ResourceManager::new();
rm.register_loader::<PreprocessShaderLoader>();
app.world.add_resource(rm);
}
}
@ -108,13 +114,14 @@ impl Plugin for ResourceManagerPlugin {
pub struct DefaultPlugins;
impl Plugin for DefaultPlugins {
fn setup(&self, game: &mut Game) {
CommandQueuePlugin.setup(game);
EventsPlugin.setup(game);
InputPlugin.setup(game);
ResourceManagerPlugin.setup(game);
WindowPlugin::default().setup(game);
DeltaTimePlugin.setup(game);
fn setup(&mut self, app: &mut App) {
ResourceManagerPlugin.setup(app);
GltfPlugin.setup(app);
WinitPlugin::default().setup(app);
WindowPlugin::default().setup(app);
CommandQueuePlugin.setup(app);
InputPlugin.setup(app);
DeltaTimePlugin.setup(app);
}
}
@ -124,7 +131,17 @@ impl Plugin for DefaultPlugins {
pub struct CommandQueuePlugin;
impl Plugin for CommandQueuePlugin {
fn setup(&self, game: &mut Game) {
game.world_mut().add_resource(CommandQueue::default());
fn setup(&mut self, app: &mut App) {
app.world.add_resource(CommandQueue::default());
}
}
#[derive(Default)]
pub struct GltfPlugin;
impl Plugin for GltfPlugin {
fn setup(&mut self, app: &mut App) {
let man: ResMut<ResourceManager> = app.world.get_resource_or_default();
man.register_loader::<GltfLoader>();
}
}

View file

@ -67,7 +67,7 @@ impl<T> AVec<T> {
#[inline(always)]
fn slot_size(&self) -> usize {
let a = self.align - 1;
mem::align_of::<T>() + (a) & !a
(mem::align_of::<T>() + (a)) & !a
}
/// # Panics
@ -240,6 +240,11 @@ impl<T> AVec<T> {
self.len
}
#[inline(always)]
pub fn is_empty(&self) -> bool {
self.len == 0
}
/// Returns the capacity of the vector.
///
/// The capacity is the amount of elements that the vector can store without reallocating.

View file

@ -0,0 +1,70 @@
use glam::Vec2;
use lyra_math::Transform;
use crate::scene::CameraProjection;
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct CameraUniform {
/// The view matrix of the camera
pub view: glam::Mat4,
/// The inverse of the projection matrix of the camera
pub inverse_projection: glam::Mat4,
/// The view projection matrix
pub view_projection: glam::Mat4,
pub projection: glam::Mat4,
/// The position of the camera
pub position: glam::Vec3,
_padding: u32,
}
impl Default for CameraUniform {
fn default() -> Self {
Self {
view: glam::Mat4::IDENTITY,
inverse_projection: glam::Mat4::IDENTITY,
view_projection: glam::Mat4::IDENTITY,
projection: glam::Mat4::IDENTITY,
position: Default::default(),
_padding: 0,
}
}
}
impl CameraUniform {
pub fn new(
view: glam::Mat4,
inverse_projection: glam::Mat4,
view_projection: glam::Mat4,
projection: glam::Mat4,
position: glam::Vec3,
) -> Self {
Self {
view,
inverse_projection,
view_projection,
projection,
position,
_padding: 0,
}
}
pub fn from_component(transform: Transform, projection: CameraProjection, viewport_size: Vec2) -> Self {
let position = transform.translation;
let forward = transform.forward();
let up = transform.up();
let view = glam::Mat4::look_to_rh(position, forward, up);
let projection = projection.to_mat4(viewport_size);
let view_projection = projection * view;
Self {
view,
inverse_projection: projection.inverse(),
view_projection,
projection,
position,
_padding: 0,
}
}
}

View file

Before

Width:  |  Height:  |  Size: 545 B

After

Width:  |  Height:  |  Size: 545 B

View file

@ -0,0 +1,68 @@
use std::{any::TypeId, cell::Ref};
use lyra_ecs::{query::{AsQuery, Fetch, Query}, Archetype, ArchetypeEntityId, ComponentColumn, Tick, World};
use lyra_math::Transform;
use lyra_scene::WorldTransform;
/// Fetcher for [`EitherTransform`].
pub struct FetchEitherTransform<'a> {
col: &'a ComponentColumn,
is_world: bool,
}
impl<'a> Fetch<'a> for FetchEitherTransform<'a> {
type Item = Ref<'a, Transform>;
fn dangling() -> Self {
unreachable!()
}
unsafe fn get_item(&mut self, entity: ArchetypeEntityId) -> Self::Item {
if self.is_world {
let wt = self.col.get::<WorldTransform>(entity.0 as _);
Ref::map(wt, |wt| &**wt)
} else {
self.col.get(entity.0 as _)
}
}
}
/// ECS query that retrieves the Transform from an Entity's [`WorldTransform`], or [`Transform`] component.
///
/// [`WorldTransform`] is preferred over [`Transform`].
#[derive(Default, Clone, Copy)]
pub struct EitherTransform;
impl Query for EitherTransform {
type Item<'a> = Ref<'a, Transform>;
type Fetch<'a> = FetchEitherTransform<'a>;
fn new() -> Self {
EitherTransform
}
fn can_visit_archetype(&self, archetype: &Archetype) -> bool {
archetype.has_column(TypeId::of::<Transform>()) ||
archetype.has_column(TypeId::of::<WorldTransform>())
}
unsafe fn fetch<'a>(&self, _: &'a World, archetype: &'a Archetype, _: Tick) -> Self::Fetch<'a> {
if let Some(col) = archetype.get_column(TypeId::of::<WorldTransform>()) {
FetchEitherTransform {
col,
is_world: true,
}
} else if let Some(col) = archetype.get_column(TypeId::of::<Transform>()) {
FetchEitherTransform {
col,
is_world: false,
}
} else { unreachable!() }
}
}
impl AsQuery for EitherTransform {
type Query = Self;
}

View file

@ -0,0 +1,101 @@
use std::{collections::VecDeque, sync::Arc};
use tracing::instrument;
use super::{RenderGraphLabel, RenderGraphLabelValue};
/// A queued write to a GPU buffer targeting a graph slot.
pub(crate) struct GraphBufferWrite {
/// The name of the slot that has the resource that will be written
pub(crate) target_slot: RenderGraphLabelValue,
pub(crate) offset: u64,
pub(crate) bytes: Vec<u8>,
}
#[allow(dead_code)]
pub struct RenderGraphContext<'a> {
/// The [`wgpu::CommandEncoder`] used to encode GPU operations.
///
/// This is `None` during the `prepare` stage.
pub encoder: Option<wgpu::CommandEncoder>,
/// The gpu device that is being used.
pub device: Arc<wgpu::Device>,
pub queue: Arc<wgpu::Queue>,
pub(crate) buffer_writes: VecDeque<GraphBufferWrite>,
renderpass_desc: Vec<wgpu::RenderPassDescriptor<'a>>,
/// The label of this Node.
pub label: RenderGraphLabelValue,
}
impl<'a> RenderGraphContext<'a> {
pub(crate) fn new(device: Arc<wgpu::Device>, queue: Arc<wgpu::Queue>, encoder: Option<wgpu::CommandEncoder>, label: RenderGraphLabelValue) -> Self {
Self {
encoder,
device,
queue,
buffer_writes: Default::default(),
renderpass_desc: vec![],
label,
}
}
pub fn begin_render_pass(
&'a mut self,
desc: wgpu::RenderPassDescriptor<'a>,
) -> wgpu::RenderPass<'a> {
self.encoder
.as_mut()
.expect(
"RenderGraphContext is missing a command encoder. This is likely \
because you are trying to run render commands in the prepare stage.",
)
.begin_render_pass(&desc)
}
pub fn begin_compute_pass(&mut self, desc: &wgpu::ComputePassDescriptor) -> wgpu::ComputePass {
self.encoder
.as_mut()
.expect(
"RenderGraphContext is missing a command encoder. This is likely \
because you are trying to run render commands in the prepare stage.",
)
.begin_compute_pass(desc)
}
/// Queue a data write to a buffer at that is contained in `target_slot`.
///
/// This does not submit the data to the GPU immediately, or add it to the `wgpu::Queue`. The
/// data will be submitted to the GPU queue right after the prepare stage for all passes
/// is ran.
#[instrument(skip(self, bytes), level="trace", fields(size = bytes.len()))]
pub fn queue_buffer_write(&mut self, target_slot: impl RenderGraphLabel, offset: u64, bytes: &[u8]) {
self.buffer_writes.push_back(GraphBufferWrite {
target_slot: target_slot.into(),
offset,
bytes: bytes.to_vec(),
})
}
/// Queue a data write of a type that to a buffer at that is contained in `target_slot`.
#[instrument(skip(self, bytes), level="trace", fields(size = std::mem::size_of::<T>()))]
pub fn queue_buffer_write_with<T: bytemuck::NoUninit>(
&mut self,
target_slot: impl RenderGraphLabel,
offset: u64,
bytes: T,
) {
self.queue_buffer_write(target_slot, offset, bytemuck::bytes_of(&bytes));
}
/// Submit the encoder to the gpu queue.
///
/// The `encoder` of this context will be `None` until the next node is executed, then another
/// one will be made. You likely don't need to run this yourself until you are manually
/// presenting a surface texture.
pub fn submit_encoder(&mut self) {
let en = self.encoder.take()
.unwrap()
.finish();
self.queue.submit(std::iter::once(en));
}
}

View file

@ -0,0 +1,596 @@
mod node;
use std::{
cell::{Ref, RefCell, RefMut}, collections::VecDeque, fmt::Debug, hash::Hash, rc::Rc, sync::Arc
};
use lyra_ecs::World;
use lyra_resource::{RequestError, ResHandle, ResourceManager};
pub use node::*;
mod passes;
pub use passes::*;
mod slot_desc;
pub use slot_desc::*;
mod context;
pub use context::*;
mod render_target;
pub use render_target::*;
use rustc_hash::FxHashMap;
use tracing::{debug_span, instrument, trace, warn};
use wgpu::{util::DeviceExt, BufferUsages, CommandEncoder};
use super::{resource::{ComputePipeline, Pass, Pipeline, RenderPipeline}, Shader};
/// A trait that represents the label of a resource, slot, or node in the [`RenderGraph`].
pub trait RenderGraphLabel: Debug + 'static {
fn rc_clone(&self) -> Rc<dyn RenderGraphLabel>;
fn as_label_hash(&self) -> u64;
fn label_eq_rc(&self, other: &Rc<dyn RenderGraphLabel>) -> bool {
self.as_label_hash() == other.as_label_hash()
}
fn label_eq(&self, other: &dyn RenderGraphLabel) -> bool {
self.as_label_hash() == other.as_label_hash()
}
}
/// An owned [`RenderGraphLabel`].
#[derive(Clone)]
pub struct RenderGraphLabelValue(Rc<dyn RenderGraphLabel>);
impl Debug for RenderGraphLabelValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl<L: RenderGraphLabel> From<L> for RenderGraphLabelValue {
fn from(value: L) -> Self {
Self(Rc::new(value))
}
}
impl From<Rc<dyn RenderGraphLabel>> for RenderGraphLabelValue {
fn from(value: Rc<dyn RenderGraphLabel>) -> Self {
Self(value)
}
}
impl From<&Rc<dyn RenderGraphLabel>> for RenderGraphLabelValue {
fn from(value: &Rc<dyn RenderGraphLabel>) -> Self {
Self(value.clone())
}
}
impl Hash for RenderGraphLabelValue {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write_u64(self.0.as_label_hash());
}
}
impl PartialEq for RenderGraphLabelValue {
fn eq(&self, other: &Self) -> bool {
self.0.label_eq_rc(&other.0)
}
}
impl Eq for RenderGraphLabelValue {}
struct NodeEntry {
/// The Node
inner: Arc<RefCell<dyn Node>>,
/// The Node descriptor
desc: Rc<RefCell<NodeDesc>>,
/// The index of the node in the execution graph
graph_index: petgraph::matrix_graph::NodeIndex<usize>,
/// The Node's optional pipeline
pipeline: Rc<RefCell<Option<Pipeline>>>,
}
#[derive(Clone)]
struct BindGroupEntry {
label: RenderGraphLabelValue,
/// BindGroup
bg: Arc<wgpu::BindGroup>,
/// BindGroupLayout
layout: Option<Arc<wgpu::BindGroupLayout>>,
}
#[allow(dead_code)]
#[derive(Clone)]
struct ResourceSlot {
label: RenderGraphLabelValue,
ty: SlotType,
value: SlotValue,
}
pub struct RenderGraph {
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
slots: FxHashMap<RenderGraphLabelValue, ResourceSlot>,
nodes: FxHashMap<RenderGraphLabelValue, NodeEntry>,
sub_graphs: FxHashMap<RenderGraphLabelValue, RenderGraph>,
bind_groups: FxHashMap<RenderGraphLabelValue, BindGroupEntry>,
/// A directed graph used to determine dependencies of nodes.
node_graph: petgraph::matrix_graph::DiMatrix<RenderGraphLabelValue, (), Option<()>, usize>,
view_target: Rc<RefCell<ViewTarget>>,
/// Type erased data of ResourceManager ecs resource
resource_manager: lyra_ecs::ResourceData,
}
impl RenderGraph {
pub fn new(device: Arc<wgpu::Device>, queue: Arc<wgpu::Queue>, view_target: Rc<RefCell<ViewTarget>>, world: &World) -> Self {
let rm = world.get_resource_data::<ResourceManager>()
.expect("RenderGraph requires ResourceManager ECS resource");
Self {
device,
queue,
slots: Default::default(),
nodes: Default::default(),
sub_graphs: Default::default(),
bind_groups: Default::default(),
node_graph: Default::default(),
view_target,
resource_manager: rm,
}
}
pub fn device(&self) -> &wgpu::Device {
&self.device
}
/// Add a [`Node`] to the RenderGraph.
///
/// When the node is added, its [`Node::desc`] method will be executed.
///
/// Additionally, all [`Slot`](node::NodeSlot)s of the node will be iterated,
/// 1. Ensuring that there are no two slots of the same name, with different value types
/// 2. Changing the id of insert slots to match the id of the output slot of the same name.
/// * This means that the id of insert slots **ARE NOT STABLE**. **DO NOT** rely on them to
/// not change. The IDs of output slots do stay the same.
/// 3. Ensuring that no two slots share the same ID when the names do not match.
#[instrument(skip(self, node), level = "debug")]
pub fn add_node<P: Node>(&mut self, label: impl RenderGraphLabel, mut node: P) {
let mut desc = node.desc(self);
// collect all the slots of the node
for slot in &mut desc.slots {
if let Some(other) = self
.slots
.get_mut(&slot.label)
{
debug_assert_eq!(
slot.ty, other.ty,
"slot {:?} in node {:?} does not match existing slot of same name",
slot.label, label
);
} else {
debug_assert!(!self.slots.contains_key(&slot.label),
"Reuse of id detected in render graph! Node: {:?}, slot: {:?}",
label, slot.label,
);
let res_slot = ResourceSlot {
label: slot.label.clone(),
ty: slot.ty,
value: slot.value.clone().unwrap_or(SlotValue::None),
};
self.slots.insert(slot.label.clone(), res_slot);
}
}
// get clones of the bind groups and layouts
for (label, bg, bgl) in &desc.bind_groups {
self.bind_groups.insert(label.clone(), BindGroupEntry {
label: label.clone(),
bg: bg.clone(),
layout: bgl.clone(),
});
}
let label: RenderGraphLabelValue = label.into();
let index = self.node_graph.add_node(label.clone());
self.nodes.insert(
label,
NodeEntry {
inner: Arc::new(RefCell::new(node)),
desc: Rc::new(RefCell::new(desc)),
graph_index: index,
pipeline: Rc::new(RefCell::new(None)),
},
);
}
/// Creates all buffers required for the nodes.
///
/// This only needs to be ran when the [`Node`]s in the graph change, or they are removed or
/// added.
#[instrument(skip(self, device))]
pub fn setup(&mut self, device: &wgpu::Device) {
// For all nodes, create their pipelines
for node in self.nodes.values_mut() {
let desc = (*node.desc).borrow();
if let Some(pipeline_desc) = &desc.pipeline_desc {
let pipeline = match desc.ty {
NodeType::Render => Pipeline::Render(RenderPipeline::create(
device,
pipeline_desc
.as_render_pipeline_descriptor()
.expect("got compute pipeline descriptor in a render node"),
)),
NodeType::Compute => Pipeline::Compute(ComputePipeline::create(
device,
pipeline_desc
.as_compute_pipeline_descriptor()
.expect("got render pipeline descriptor in a compute node"),
)),
NodeType::Presenter | NodeType::Node | NodeType::Graph => {
panic!("Present or Node RenderGraph nodes should not have a pipeline descriptor!");
},
};
drop(desc);
let mut node_pipeline = node.pipeline.borrow_mut();
*node_pipeline = Some(pipeline);
}
}
for sub in self.sub_graphs.values_mut() {
sub.setup(device);
}
}
#[instrument(skip(self, world))]
pub fn prepare(&mut self, world: &mut World) {
let mut buffer_writes = VecDeque::<GraphBufferWrite>::new();
// reserve some buffer writes. not all nodes write so half the amount of them is probably
// fine.
buffer_writes.reserve(self.nodes.len() / 2);
let mut sorted: VecDeque<RenderGraphLabelValue> = petgraph::algo::toposort(&self.node_graph, None)
.expect("RenderGraph had cycled!")
.iter()
.map(|i| self.node_graph[*i].clone())
.collect();
while let Some(node_label) = sorted.pop_front() {
let node = self.nodes.get(&node_label).unwrap();
let device = self.device.clone();
let queue = self.queue.clone();
let inner = node.inner.clone();
let mut inner = inner.borrow_mut();
let mut context = RenderGraphContext::new(device, queue, None, node_label.clone());
inner.prepare(self, world, &mut context);
buffer_writes.append(&mut context.buffer_writes);
}
{
// Queue all buffer writes to the gpu
let s = debug_span!("queue_buffer_writes");
let _e = s.enter();
while let Some(bufwr) = buffer_writes.pop_front() {
let slot = self
.slots
.get(&bufwr.target_slot)
.unwrap_or_else(|| panic!("Failed to find slot '{:?}' for buffer write",
bufwr.target_slot));
let buf = slot
.value
.as_buffer()
.unwrap_or_else(|| panic!("Slot '{:?}' is not a buffer", bufwr.target_slot));
self.queue.write_buffer(buf, bufwr.offset, &bufwr.bytes);
}
}
}
fn create_encoder(&self) -> CommandEncoder {
self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("graph encoder"),
})
}
#[instrument(skip(self))]
pub fn render(&mut self) {
let mut sorted: VecDeque<RenderGraphLabelValue> = petgraph::algo::toposort(&self.node_graph, None)
.expect("RenderGraph had cycled!")
.iter()
.map(|i| self.node_graph[*i].clone())
.collect();
// A bit of 'encoder hot potato' is played using this.
// Although the encoder is an option, its only an option so ownership of it can be given
// to the context for the time of the node execution.
// After the node is executed, the encoder is taken back. If the node is a presenter node,
// the encoder will be submitted and a new one will be made.
let mut encoder = Some(self.create_encoder());
while let Some(node_label) = sorted.pop_front() {
let node = self.nodes.get(&node_label).unwrap();
let node_inn = node.inner.clone();
let node_desc = node.desc.clone();
let node_desc = (*node_desc).borrow();
// clone of the Rc's is required to appease the borrow checker
let device = self.device.clone();
let queue = self.queue.clone();
// create a new encoder if the last one was submitted
if encoder.is_none() {
encoder = Some(self.create_encoder());
}
let mut context = RenderGraphContext::new(device, queue, encoder.take(), node_label.clone());
trace!("Executing {:?}", node_label.0);
let mut inner = node_inn.borrow_mut();
inner.execute(self, &node_desc, &mut context);
// take back the encoder from the context
encoder = context.encoder;
}
if let Some(encoder) = encoder {
self.queue.submit(std::iter::once(encoder.finish()));
}
}
pub fn slot_value<L: Into<RenderGraphLabelValue>>(&self, label: L) -> Option<&SlotValue> {
self.slots.get(&label.into()).map(|s| &s.value)
}
pub fn slot_value_mut<L: Into<RenderGraphLabelValue>>(&mut self, label: L) -> Option<&mut SlotValue> {
self.slots.get_mut(&label.into()).map(|s| &mut s.value)
}
pub fn node_desc<L: Into<RenderGraphLabelValue>>(&self, label: L) -> Option<Ref<NodeDesc>> {
self.nodes.get(&label.into()).map(|s| (*s.desc).borrow())
}
#[inline(always)]
pub fn pipeline<L: Into<RenderGraphLabelValue>>(&self, label: L) -> Option<Ref<Pipeline>> {
self.nodes.get(&label.into())
.and_then(|p| {
let v = p.pipeline.borrow();
#[allow(clippy::manual_map)]
match &*v {
Some(_) => Some(Ref::map(v, |p| p.as_ref().unwrap())),
None => None,
}
})
}
#[inline(always)]
pub fn try_bind_group<L: Into<RenderGraphLabelValue>>(&self, label: L) -> Option<&Arc<wgpu::BindGroup>> {
self.bind_groups.get(&label.into()).map(|e| &e.bg)
}
#[inline(always)]
pub fn bind_group<L: Into<RenderGraphLabelValue>>(&self, label: L) -> &Arc<wgpu::BindGroup> {
let l = label.into();
self.try_bind_group(l.clone()).unwrap_or_else(|| panic!("Unknown label '{:?}' for bind group layout", l.clone()))
}
#[inline(always)]
pub fn try_bind_group_layout<L: Into<RenderGraphLabelValue>>(&self, label: L) -> Option<&Arc<wgpu::BindGroupLayout>> {
self.bind_groups.get(&label.into()).and_then(|e| e.layout.as_ref())
}
#[inline(always)]
pub fn bind_group_layout<L: Into<RenderGraphLabelValue>>(&self, label: L) -> &Arc<wgpu::BindGroupLayout> {
let l = label.into();
self.try_bind_group_layout(l.clone())
.unwrap_or_else(|| panic!("Unknown label '{:?}' for bind group layout", l.clone()))
}
pub fn add_edge(&mut self, from: impl RenderGraphLabel, to: impl RenderGraphLabel)
{
let from = RenderGraphLabelValue::from(from);
let to = RenderGraphLabelValue::from(to);
let from_idx = self
.nodes
.iter()
.find(|p| *p.0 == from)
.map(|p| p.1.graph_index)
.expect("Failed to find from node");
let to_idx = self
.nodes
.iter()
.find(|p| *p.0 == to)
.map(|p| p.1.graph_index)
.expect("Failed to find to node");
debug_assert_ne!(from_idx, to_idx, "cannot add edges between the same node");
self.node_graph.add_edge(from_idx, to_idx, ());
}
/// Utility method for setting the bind groups for a node.
///
/// The parameter `bind_groups` can be used to specify the labels of a bind group, and the
/// index of the bind group in the pipeline for the node. If a bind group of the provided
/// name is not found in the graph, a panic will occur.
///
/// # Example:
/// ```nobuild
/// graph.set_bind_groups(
/// &mut pass,
/// &[
/// // retrieves the `BasePassSlots::DepthTexture` bind group and sets the index 0 in the
/// // node to it.
/// (&BaseNodeSlots::DepthTexture, 0),
/// (&BaseNodeSlots::Camera, 1),
/// (&LightBaseNodeSlots::Lights, 2),
/// (&LightCullComputeNodeSlots::LightIndicesGridGroup, 3),
/// (&BaseNodeSlots::ScreenSize, 4),
/// ],
/// );
/// ```
///
/// # Panics
/// Panics if a bind group of a provided name is not found.
pub fn set_bind_groups<'a, P: Pass<'a>>(
&'a self,
pass: &mut P,
bind_groups: &[(&dyn RenderGraphLabel, u32)],
) {
for (label, index) in bind_groups {
let bg = self
.bind_group(label.rc_clone());
pass.set_bind_group(*index, bg, &[]);
}
}
pub fn sub_graph_mut<L: Into<RenderGraphLabelValue>>(&mut self, label: L) -> Option<&mut RenderGraph> {
self.sub_graphs.get_mut(&label.into())
}
/// Add a sub graph.
///
/// > Note: the sub graph is not ran unless you add a node that executes it. See [`SubGraphNode`].
pub fn add_sub_graph<L: Into<RenderGraphLabelValue>>(&mut self, label: L, sub: RenderGraph) {
self.sub_graphs.insert(label.into(), sub);
}
/// Clone rendering resources (slots, bind groups, etc.) to a sub graph.
fn clone_resources_to_sub(&mut self, sub_graph: RenderGraphLabelValue, slots: Vec<RenderGraphLabelValue>) {
// instead of inserting the slots to the sub graph as they are extracted from the parent graph,
// they are done separately to make the borrow checker happy. If this is not done,
// the borrow checker complains about multiple mutable borrows (or an inmutable borrow
// while mutable borrowing) to self; caused by borrowing the sub graph from self, and
// self.slots.
let mut collected_slots = VecDeque::new();
let mut collected_bind_groups = VecDeque::new();
for slot in slots.iter() {
let mut found_res = false;
// Since slots and bind groups may go by the same label,
// there must be a way to collect both of them. A flag variable is used to detect
// if neither was found.
if let Some(slot_res) = self.slots.get(slot) {
collected_slots.push_back(slot_res.clone());
found_res = true;
}
if let Some(bg_res) = self.bind_groups.get(slot) {
collected_bind_groups.push_back(bg_res.clone());
found_res = true;
}
if !found_res {
panic!("sub graph is missing {:?} input slot or bind group", slot);
}
}
let sg = self.sub_graph_mut(sub_graph.clone()).unwrap();
while let Some(res) = collected_slots.pop_front() {
sg.slots.insert(res.label.clone(), res);
}
while let Some(bg) = collected_bind_groups.pop_front() {
sg.bind_groups.insert(bg.label.clone(), bg);
}
}
pub fn view_target(&self) -> Ref<ViewTarget> {
self.view_target.borrow()
}
pub fn view_target_mut(&self) -> RefMut<ViewTarget> {
self.view_target.borrow_mut()
}
/// Register a shader with the preprocessor.
///
/// This step also parses the shader and will return errors if it failed to parse.
///
/// Returns: The shader module import path if the module specified one.
/* #[inline(always)]
pub fn register_shader(&mut self, shader_src: &str) -> Result<Option<String>, wgsl_preprocessor::Error> {
self.shader_prepoc.parse_module(shader_src)
}
/// Preprocess a shader, returning the source.
#[inline(always)]
pub fn preprocess_shader(&mut self, shader_path: &str) -> Result<String, wgsl_preprocessor::Error> {
self.shader_prepoc.preprocess_module(shader_path)
} */
/// Load a shader from a `str`
///
/// This will also wait until the shader is loaded before returning.
pub fn load_shader_str(&self, shader_name: &str, shader_src: &str) -> Result<ResHandle<Shader>, RequestError> {
let rm = self.resource_manager.get::<ResourceManager>();
let shader = rm.load_str(shader_name, "text/wgsl", shader_src)?;
shader.wait_for_load()?;
Ok(shader)
}
/// Create a buffer with a single item inside of it
pub fn create_buffer_with_data<T: bytemuck::NoUninit>(&self, label: Option<&'static str>, usage: BufferUsages, data: &T) -> wgpu::Buffer {
self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label,
usage,
contents: bytemuck::bytes_of(data),
})
}
}
pub struct SubGraphNode {
subg: RenderGraphLabelValue,
slots: Vec<RenderGraphLabelValue>,
}
impl SubGraphNode {
pub fn new<L: Into<RenderGraphLabelValue>>(sub_label: L, slot_labels: Vec<RenderGraphLabelValue>) -> Self {
Self {
subg: sub_label.into(),
slots: slot_labels,
}
}
}
impl Node for SubGraphNode {
fn desc(&mut self, _: &mut RenderGraph) -> NodeDesc {
NodeDesc::new(NodeType::Graph, None, vec![])
}
fn prepare(&mut self, graph: &mut RenderGraph, world: &mut World, _: &mut RenderGraphContext) {
graph.clone_resources_to_sub(self.subg.clone(), self.slots.clone());
let sg = graph.sub_graph_mut(self.subg.clone())
.unwrap_or_else(|| panic!("failed to find sub graph for SubGraphNode: {:?}", self.subg));
sg.prepare(world);
}
fn execute(
&mut self,
graph: &mut RenderGraph,
_: &NodeDesc,
_: &mut RenderGraphContext,
) {
graph.clone_resources_to_sub(self.subg.clone(), self.slots.clone());
let sg = graph.sub_graph_mut(self.subg.clone())
.unwrap_or_else(|| panic!("failed to find sub graph for SubGraphNode: {:?}", self.subg));
sg.render();
}
}

View file

@ -1,11 +1,11 @@
use std::{cell::{Ref, RefCell, RefMut}, num::NonZeroU32, rc::Rc};
use std::{cell::{Ref, RefCell, RefMut}, num::NonZeroU32, rc::Rc, sync::Arc};
use bind_match::bind_match;
use lyra_ecs::World;
use crate::render::resource::PipelineDescriptor;
use super::{RenderGraph, RenderGraphContext, RenderGraphLabel, RenderGraphLabelValue, RenderTarget};
use super::{Frame, RenderGraph, RenderGraphContext, RenderGraphLabel, RenderGraphLabelValue, RenderTarget};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum NodeType {
@ -18,6 +18,8 @@ pub enum NodeType {
Render,
/// A node that presents render results to a render target.
Presenter,
/// A node that represents a sub-graph.
Graph,
}
impl NodeType {
@ -28,6 +30,7 @@ impl NodeType {
NodeType::Compute => true,
NodeType::Render => true,
NodeType::Presenter => false,
NodeType::Graph => false,
}
}
}
@ -40,25 +43,35 @@ pub enum SlotType {
Texture,
Buffer,
RenderTarget,
Frame,
}
/// The value of a slot in a [`Node`].
#[derive(Debug, Clone)]
#[derive(Clone)]
pub enum SlotValue {
/// This slot doesn't have any value
None,
/// The value will be set during a later phase of the render graph. To see the type of value
/// this will be set to, see the slots type.
Lazy,
TextureView(Rc<wgpu::TextureView>),
TextureView(Arc<wgpu::TextureView>),
Sampler(Rc<wgpu::Sampler>),
Texture(Rc<wgpu::Texture>),
Buffer(Rc<wgpu::Buffer>),
Texture(Arc<wgpu::Texture>),
Buffer(Arc<wgpu::Buffer>),
RenderTarget(Rc<RefCell<RenderTarget>>),
Frame(Rc<RefCell<Option<Frame>>>),
}
impl SlotValue {
pub fn as_texture_view(&self) -> Option<&Rc<wgpu::TextureView>> {
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
pub fn is_lazy(&self) -> bool {
matches!(self, Self::Lazy)
}
pub fn as_texture_view(&self) -> Option<&Arc<wgpu::TextureView>> {
bind_match!(self, Self::TextureView(v) => v)
}
@ -66,11 +79,11 @@ impl SlotValue {
bind_match!(self, Self::Sampler(v) => v)
}
pub fn as_texture(&self) -> Option<&Rc<wgpu::Texture>> {
pub fn as_texture(&self) -> Option<&Arc<wgpu::Texture>> {
bind_match!(self, Self::Texture(v) => v)
}
pub fn as_buffer(&self) -> Option<&Rc<wgpu::Buffer>> {
pub fn as_buffer(&self) -> Option<&Arc<wgpu::Buffer>> {
bind_match!(self, Self::Buffer(v) => v)
}
@ -81,6 +94,14 @@ impl SlotValue {
pub fn as_render_target_mut(&mut self) -> Option<RefMut<RenderTarget>> {
bind_match!(self, Self::RenderTarget(v) => v.borrow_mut())
}
pub fn as_frame(&self) -> Option<Ref<Option<Frame>>> {
bind_match!(self, Self::Frame(v) => v.borrow())
}
pub fn as_frame_mut(&mut self) -> Option<RefMut<Option<Frame>>> {
bind_match!(self, Self::Frame(v) => v.borrow_mut())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SlotAttribute {
@ -132,6 +153,7 @@ pub struct RenderGraphPipelineInfo {
pub multiview: Option<NonZeroU32>,
}
#[allow(clippy::too_many_arguments)]
impl RenderGraphPipelineInfo {
pub fn new(
label: &str,
@ -147,7 +169,7 @@ impl RenderGraphPipelineInfo {
label: Some(label.to_string()),
bind_group_layouts: bind_group_layouts
.into_iter()
.map(|bgl| Rc::new(bgl))
.map(Rc::new)
.collect(),
vertex,
primitive,
@ -175,8 +197,8 @@ pub struct NodeDesc {
/// This makes the bind groups accessible to other Nodes.
pub bind_groups: Vec<(
RenderGraphLabelValue,
Rc<wgpu::BindGroup>,
Option<Rc<wgpu::BindGroupLayout>>,
Arc<wgpu::BindGroup>,
Option<Arc<wgpu::BindGroupLayout>>,
)>,
}
@ -185,7 +207,7 @@ impl NodeDesc {
pub fn new(
pass_type: NodeType,
pipeline_desc: Option<PipelineDescriptor>,
bind_groups: Vec<(&dyn RenderGraphLabel, Rc<wgpu::BindGroup>, Option<Rc<wgpu::BindGroupLayout>>)>,
bind_groups: Vec<(&dyn RenderGraphLabel, Arc<wgpu::BindGroup>, Option<Arc<wgpu::BindGroupLayout>>)>,
) -> Self {
Self {
ty: pass_type,
@ -342,13 +364,13 @@ impl NodeDesc {
/// describes all resources the node requires for execution during the `execute` phase.
pub trait Node: 'static {
/// Retrieve a descriptor of the Node.
fn desc<'a, 'b>(&'a mut self, graph: &'b mut RenderGraph) -> NodeDesc;
fn desc(&mut self, graph: &mut RenderGraph) -> NodeDesc;
/// Prepare the node for rendering.
///
/// This phase runs before `execute` and is meant to be used to collect data from the World
/// and write to GPU buffers.
fn prepare(&mut self, world: &mut World, context: &mut RenderGraphContext);
fn prepare(&mut self, graph: &mut RenderGraph, world: &mut World, context: &mut RenderGraphContext);
/// Execute the node.
///

View file

@ -0,0 +1,155 @@
use std::sync::Arc;
use glam::UVec2;
use lyra_game_derive::RenderGraphLabel;
use lyra_math::Transform;
use tracing::warn;
use crate::{
render::{
camera::CameraUniform,
graph::{
Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext, SlotAttribute, SlotValue
},
render_buffer::BufferWrapper, texture::RenderTexture,
}, scene::{Camera, CameraProjection},
};
#[derive(Debug, Hash, Clone, Default, PartialEq, RenderGraphLabel)]
pub struct BasePassLabel;
#[derive(Debug, Hash, Clone, PartialEq, RenderGraphLabel)]
pub enum BasePassSlots {
DepthTexture,
ScreenSize,
Camera,
DepthTextureView,
}
/// Supplies some basic things other passes needs.
///
/// screen size buffer, camera buffer,
#[derive(Default)]
pub struct BasePass {
screen_size: UVec2,
}
impl BasePass {
pub fn new() -> Self {
Self::default()
}
}
impl Node for BasePass {
fn desc(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
let vt = graph.view_target();
self.screen_size = vt.size();
let (screen_size_bgl, screen_size_bg, screen_size_buf, _) = BufferWrapper::builder()
.buffer_usage(wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST)
.label_prefix("ScreenSize")
.visibility(wgpu::ShaderStages::COMPUTE)
.buffer_dynamic_offset(false)
.contents(&[self.screen_size])
.finish_parts(graph.device());
let screen_size_bgl = Arc::new(screen_size_bgl);
let screen_size_bg = Arc::new(screen_size_bg);
let (camera_bgl, camera_bg, camera_buf, _) = BufferWrapper::builder()
.buffer_usage(wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST)
.label_prefix("camera")
.visibility(wgpu::ShaderStages::all())
.buffer_dynamic_offset(false)
.contents(&[CameraUniform::default()])
.finish_parts(graph.device());
let camera_bgl = Arc::new(camera_bgl);
let camera_bg = Arc::new(camera_bg);
// create the depth texture using the utility struct, then take all the required fields
let mut depth_texture = RenderTexture::create_depth_texture(graph.device(), self.screen_size, "depth_texture");
depth_texture.create_bind_group(&graph.device);
let dt_bg_pair = depth_texture.bindgroup_pair.unwrap();
let depth_texture_bg = Arc::new(dt_bg_pair.bindgroup);
let depth_texture_bgl = dt_bg_pair.layout;
let depth_texture_view = Arc::new(depth_texture.view);
let mut desc = NodeDesc::new(
NodeType::Node,
None,
vec![
// TODO: Make this a trait maybe?
// Could impl it for (RenderGraphLabel, wgpu::BindGroup) and also
// (RenderGraphLabel, wgpu::BindGroup, wgpu::BindGroupLabel) AND
// (RenderGraphLabel, wgpu::BindGroup, Option<wgpu::BindGroupLabel>)
//
// This could make it slightly easier to create this
(&BasePassSlots::DepthTexture, depth_texture_bg, Some(depth_texture_bgl)),
(&BasePassSlots::ScreenSize, screen_size_bg, Some(screen_size_bgl)),
(&BasePassSlots::Camera, camera_bg, Some(camera_bgl)),
],
);
desc.add_texture_view_slot(
BasePassSlots::DepthTextureView,
SlotAttribute::Output,
Some(SlotValue::TextureView(depth_texture_view)),
);
desc.add_buffer_slot(
BasePassSlots::ScreenSize,
SlotAttribute::Output,
Some(SlotValue::Buffer(Arc::new(screen_size_buf))),
);
desc.add_buffer_slot(
BasePassSlots::Camera,
SlotAttribute::Output,
Some(SlotValue::Buffer(Arc::new(camera_buf))),
);
desc
}
fn prepare(&mut self, graph: &mut RenderGraph, world: &mut lyra_ecs::World, context: &mut RenderGraphContext) {
let mut found_camera = false;
for (camera, projection, transform) in world.view_iter::<(&Camera, &CameraProjection, &Transform)>() {
if camera.is_active {
let screen_size = graph.view_target().size();
let uniform = CameraUniform::from_component(*transform, *projection, screen_size.as_vec2());
context.queue_buffer_write_with(BasePassSlots::Camera, 0, uniform);
found_camera = true;
break;
}
}
if !found_camera {
warn!("Missing camera!");
}
}
fn execute(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
_desc: &crate::render::graph::NodeDesc,
context: &mut crate::render::graph::RenderGraphContext,
) {
let mut vt = graph.view_target_mut();
vt.primary.create_frame();
vt.primary.create_frame_view();
/* debug_assert!(
!rt.current_texture.is_some(),
"main render target surface was not presented!"
); */
// update the screen size buffer if the size changed.
let rt_size = vt.size();
if rt_size != self.screen_size {
self.screen_size = rt_size;
context.queue_buffer_write_with(BasePassSlots::ScreenSize, 0, self.screen_size)
}
}
}

View file

@ -0,0 +1,171 @@
use std::{collections::HashMap, sync::Arc};
use lyra_game_derive::RenderGraphLabel;
use crate::render::{
graph::{Node, NodeDesc, NodeType},
resource::{FragmentState, PipelineDescriptor, RenderPipelineDescriptor, VertexState},
};
#[derive(Default, Debug, Clone, Copy, Hash, RenderGraphLabel)]
pub struct FxaaPassLabel;
#[derive(Debug, Default)]
pub struct FxaaPass {
target_sampler: Option<wgpu::Sampler>,
bgl: Option<Arc<wgpu::BindGroupLayout>>,
/// Store bind groups for the input textures.
/// The texture may change due to resizes, or changes to the view target chain
/// from other nodes.
bg_cache: HashMap<wgpu::Id<wgpu::TextureView>, wgpu::BindGroup>,
}
impl FxaaPass {
pub fn new() -> Self {
Self::default()
}
}
impl Node for FxaaPass {
fn desc(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
let device = &graph.device;
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("fxaa_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let bgl = Arc::new(bgl);
self.bgl = Some(bgl.clone());
self.target_sampler = Some(device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("fxaa sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
..Default::default()
}));
let shader = graph.load_shader_str("fxaa_shader", include_str!("../../shaders/fxaa.wgsl"))
.expect("failed to load wgsl shader from manager");
let vt = graph.view_target();
NodeDesc::new(
NodeType::Render,
Some(PipelineDescriptor::Render(RenderPipelineDescriptor {
label: Some("fxaa_pass".into()),
layouts: vec![bgl.clone()],
push_constant_ranges: vec![],
vertex: VertexState {
module: shader.clone(),
entry_point: "vs_main".into(),
buffers: vec![],
},
fragment: Some(FragmentState {
module: shader,
entry_point: "fs_main".into(),
targets: vec![Some(wgpu::ColorTargetState {
format: vt.format(),
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
depth_stencil: None,
primitive: wgpu::PrimitiveState::default(),
multisample: wgpu::MultisampleState::default(),
multiview: None,
})),
vec![],
)
}
fn prepare(
&mut self,
_: &mut crate::render::graph::RenderGraph,
_: &mut lyra_ecs::World,
_: &mut crate::render::graph::RenderGraphContext,
) {
//todo!()
}
fn execute(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
_: &crate::render::graph::NodeDesc,
context: &mut crate::render::graph::RenderGraphContext,
) {
let pipeline = graph
.pipeline(context.label.clone())
.expect("Failed to find pipeline for FxaaPass");
let mut vt = graph.view_target_mut();
let chain = vt.get_chain();
let source_view = chain.source.frame_view.as_ref().unwrap();
let dest_view = chain.dest.frame_view.as_ref().unwrap();
let bg = self
.bg_cache
.entry(source_view.global_id())
.or_insert_with(|| {
graph
.device()
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("fxaa_bg"),
layout: self.bgl.as_ref().unwrap(),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(source_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(
self.target_sampler.as_ref().unwrap(),
),
},
],
})
});
{
let encoder = context.encoder.as_mut().unwrap();
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("fxaa_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: dest_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None, // TODO: occlusion queries
});
pass.set_pipeline(pipeline.as_render());
pass.set_bind_group(0, bg, &[]);
pass.draw(0..3, 0..1);
}
}
}

View file

@ -0,0 +1,33 @@
use lyra_game_derive::RenderGraphLabel;
use crate::render::graph::{Node, NodeDesc, NodeSlot, NodeType};
#[derive(Debug, Default, Clone, Copy, Hash, RenderGraphLabel)]
pub struct InitNodeLabel;
pub struct InitNode {
slots: Vec<NodeSlot>,
}
impl Node for InitNode {
fn desc(&mut self, _: &mut crate::render::graph::RenderGraph) -> crate::render::graph::NodeDesc {
let mut desc = NodeDesc::new(NodeType::Node, None, vec![]);
// the slots can just be cloned since the slot attribute doesn't really matter much.
desc.slots = self.slots.clone();
desc
}
fn prepare(&mut self, _: &mut crate::render::graph::RenderGraph, _: &mut lyra_ecs::World, _: &mut crate::render::graph::RenderGraphContext) {
}
fn execute(
&mut self,
_: &mut crate::render::graph::RenderGraph,
_: &crate::render::graph::NodeDesc,
_: &mut crate::render::graph::RenderGraphContext,
) {
}
}

View file

@ -2,8 +2,7 @@ use lyra_game_derive::RenderGraphLabel;
use crate::render::{
graph::{
RenderGraphContext, Node, NodeDesc, NodeType, SlotAttribute,
SlotValue,
Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext, SlotAttribute, SlotValue
},
light::LightUniformBuffers,
};
@ -58,10 +57,10 @@ impl Node for LightBasePass {
desc
}
fn prepare(&mut self, world: &mut lyra_ecs::World, context: &mut RenderGraphContext) {
fn prepare(&mut self, _graph: &mut RenderGraph, world: &mut lyra_ecs::World, context: &mut RenderGraphContext) {
let tick = world.current_tick();
let lights = self.light_buffers.as_mut().unwrap();
lights.update_lights(context.queue, tick, world);
lights.update_lights(&context.queue, tick, world);
}
fn execute(

View file

@ -1,15 +1,14 @@
use std::{mem, rc::Rc};
use std::{mem, sync::Arc};
use glam::Vec2Swizzles;
use lyra_ecs::World;
use lyra_game_derive::RenderGraphLabel;
use wgpu::util::DeviceExt;
use crate::render::{
graph::{
RenderGraphContext, Node, NodeDesc, NodeType, SlotAttribute,
SlotValue,
},
resource::{ComputePipelineDescriptor, PipelineDescriptor, Shader},
Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext, SlotAttribute, SlotValue
}, renderer::ScreenSize, resource::{ComputePipeline, ComputePipelineDescriptor}
};
use super::{BasePassSlots, LightBasePassSlots};
@ -27,12 +26,14 @@ pub enum LightCullComputePassSlots {
pub struct LightCullComputePass {
workgroup_size: glam::UVec2,
pipeline: Option<ComputePipeline>,
}
impl LightCullComputePass {
pub fn new(screen_size: winit::dpi::PhysicalSize<u32>) -> Self {
Self {
workgroup_size: glam::UVec2::new(screen_size.width, screen_size.height),
pipeline: None,
}
}
}
@ -42,19 +43,6 @@ impl Node for LightCullComputePass {
&mut self,
graph: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
let shader = Rc::new(Shader {
label: Some("light_cull_comp_shader".into()),
source: include_str!("../../shaders/light_cull.comp.wgsl").to_string(),
});
// get the size of the work group for the grid
let main_rt = graph
.slot_value(BasePassSlots::MainRenderTarget)
.and_then(|s| s.as_render_target())
.expect("missing main render target");
self.workgroup_size =
glam::UVec2::new(main_rt.surface_config.width, main_rt.surface_config.height);
// initialize some buffers with empty data
let mut contents = Vec::<u8>::new();
let contents_len =
@ -71,11 +59,11 @@ impl Node for LightCullComputePass {
let light_index_counter_buffer =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("light_index_counter_buffer"),
contents: &bytemuck::cast_slice(&[0]),
contents: bytemuck::cast_slice(&[0]),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
let light_indices_bg_layout = Rc::new(device.create_bind_group_layout(
let light_indices_bg_layout = Arc::new(device.create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
@ -140,7 +128,7 @@ impl Node for LightCullComputePass {
array_layer_count: None,
});
let light_indices_bg = Rc::new(device.create_bind_group(&wgpu::BindGroupDescriptor {
let light_indices_bg = Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &light_indices_bg_layout,
entries: &[
wgpu::BindGroupEntry {
@ -167,16 +155,16 @@ impl Node for LightCullComputePass {
label: Some("light_indices_grid_bind_group"),
}));
drop(main_rt);
//drop(main_rt);
let depth_tex_bgl = graph.bind_group_layout(BasePassSlots::DepthTexture);
/* let depth_tex_bgl = graph.bind_group_layout(BasePassSlots::DepthTexture);
let camera_bgl = graph.bind_group_layout(BasePassSlots::Camera);
let lights_bgl = graph.bind_group_layout(LightBasePassSlots::Lights);
let screen_size_bgl = graph.bind_group_layout(BasePassSlots::ScreenSize);
let screen_size_bgl = graph.bind_group_layout(BasePassSlots::ScreenSize); */
let mut desc = NodeDesc::new(
NodeType::Compute,
Some(PipelineDescriptor::Compute(ComputePipelineDescriptor {
/* Some(PipelineDescriptor::Compute(ComputePipelineDescriptor {
label: Some("light_cull_pipeline".into()),
push_constant_ranges: vec![],
layouts: vec![
@ -188,7 +176,8 @@ impl Node for LightCullComputePass {
],
shader,
shader_entry_point: "cs_main".into(),
})),
})), */
None,
vec![(
&LightCullComputePassSlots::LightIndicesGridGroup,
light_indices_bg,
@ -196,11 +185,6 @@ impl Node for LightCullComputePass {
)],
);
desc.add_texture_view_slot(
BasePassSlots::WindowTextureView,
SlotAttribute::Input,
None,
);
desc.add_buffer_slot(
BasePassSlots::ScreenSize,
SlotAttribute::Input,
@ -210,14 +194,52 @@ impl Node for LightCullComputePass {
desc.add_buffer_slot(
LightCullComputePassSlots::IndexCounterBuffer,
SlotAttribute::Output,
Some(SlotValue::Buffer(Rc::new(light_index_counter_buffer))),
Some(SlotValue::Buffer(Arc::new(light_index_counter_buffer))),
);
desc
}
fn prepare(&mut self, _world: &mut World, context: &mut RenderGraphContext) {
fn prepare(&mut self, graph: &mut RenderGraph, world: &mut World, context: &mut RenderGraphContext) {
context.queue_buffer_write_with(LightCullComputePassSlots::IndexCounterBuffer, 0, 0);
let screen_size = world.get_resource::<ScreenSize>()
.expect("world missing ScreenSize resource");
if screen_size.xy() != self.workgroup_size {
self.workgroup_size = screen_size.xy();
todo!("Resize buffers and other resources");
}
if self.pipeline.is_none() {
let device = graph.device();
let depth_tex_bgl = graph.bind_group_layout(BasePassSlots::DepthTexture);
let camera_bgl = graph.bind_group_layout(BasePassSlots::Camera);
let lights_bgl = graph.bind_group_layout(LightBasePassSlots::Lights);
let screen_size_bgl = graph.bind_group_layout(BasePassSlots::ScreenSize);
let light_indices_bg_layout = graph.bind_group_layout(LightCullComputePassSlots::LightIndicesGridGroup);
let shader = graph.load_shader_str("light_cull_comp_shader", include_str!("../../shaders/light_cull.comp.wgsl"))
.expect("failed to load light cull compute shader");
let pipeline = ComputePipeline::create(device, &ComputePipelineDescriptor {
label: Some("light_cull_pipeline".into()),
push_constant_ranges: vec![],
layouts: vec![
depth_tex_bgl.clone(),
camera_bgl.clone(),
lights_bgl.clone(),
light_indices_bg_layout.clone(),
screen_size_bgl.clone(),
],
shader,
shader_entry_point: "cs_main".into(),
cache: None,
compilation_options: Default::default(),
});
self.pipeline = Some(pipeline);
}
}
fn execute(
@ -226,14 +248,11 @@ impl Node for LightCullComputePass {
_: &crate::render::graph::NodeDesc,
context: &mut RenderGraphContext,
) {
let label = context.label.clone();
let pipeline = graph.pipeline(label)
.expect("Failed to find Pipeline for LightCullComputePass");
let pipeline = pipeline.as_compute();
let pipeline = self.pipeline.as_mut().unwrap();
let mut pass = context.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("light_cull_pass"),
..Default::default()
});
pass.set_pipeline(pipeline);

View file

@ -0,0 +1,667 @@
use std::{
collections::{HashSet, VecDeque},
ops::{Deref, DerefMut},
sync::Arc,
};
use glam::{UVec2, Vec3};
use image::GenericImageView;
use itertools::izip;
use lyra_ecs::{
query::{filter::Or, Entities, ResMut, TickOf},
Entity, ResourceObject, World,
};
use lyra_game_derive::RenderGraphLabel;
use lyra_resource::ResHandle;
use lyra_gltf::Mesh;
use lyra_scene::SceneGraph;
use rustc_hash::FxHashMap;
use tracing::{debug, instrument};
use uuid::Uuid;
use wgpu::util::DeviceExt;
use crate::render::{
graph::{Node, NodeDesc, NodeType},
render_buffer::BufferStorage,
render_job::RenderJob,
texture::{res_filter_to_wgpu, res_wrap_to_wgpu},
transform_buffer_storage::TransformIndex,
vertex::Vertex,
};
type MeshHandle = ResHandle<Mesh>;
type SceneHandle = ResHandle<SceneGraph>;
pub struct MeshBufferStorage {
pub buffer_vertex: BufferStorage,
pub buffer_indices: Option<(wgpu::IndexFormat, BufferStorage)>,
// maybe this should just be a Uuid and the material can be retrieved though
// MeshPass's `material_buffers` field?
pub material: Option<Arc<GpuMaterial>>,
}
#[derive(Default, Debug, Clone, Copy, Hash, RenderGraphLabel)]
pub struct MeshPrepNodeLabel;
#[derive(Debug)]
pub struct MeshPrepNode {
pub material_bgl: Arc<wgpu::BindGroupLayout>,
}
impl MeshPrepNode {
pub fn new(device: &wgpu::Device) -> Self {
let bgl = GpuMaterial::create_bind_group_layout(device);
Self { material_bgl: bgl }
}
/// Checks if the mesh buffers in the GPU need to be updated.
#[instrument(skip(self, device, mesh_buffers, queue, mesh_han))]
fn check_mesh_buffers(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
mesh_buffers: &mut FxHashMap<uuid::Uuid, MeshBufferStorage>,
mesh_han: &ResHandle<Mesh>,
) {
let mesh_uuid = mesh_han.uuid();
if let (Some(mesh), Some(buffers)) = (mesh_han.data_ref(), mesh_buffers.get_mut(&mesh_uuid))
{
// check if the buffer sizes dont match. If they dont, completely remake the buffers
let vertices = mesh.position().unwrap();
if buffers.buffer_vertex.count() != vertices.len() {
debug!("Recreating buffers for mesh {}", mesh_uuid.to_string());
let (vert, idx) = self.create_vertex_index_buffers(device, &mesh);
// have to re-get buffers because of borrow checker
let buffers = mesh_buffers.get_mut(&mesh_uuid).unwrap();
buffers.buffer_indices = idx;
buffers.buffer_vertex = vert;
return;
}
// update vertices
let vertex_buffer = buffers.buffer_vertex.buffer();
let vertices = vertices.as_slice();
// align the vertices to 4 bytes (u32 is 4 bytes, which is wgpu::COPY_BUFFER_ALIGNMENT)
let (_, vertices, _) = bytemuck::pod_align_to::<Vec3, u32>(vertices);
queue.write_buffer(vertex_buffer, 0, bytemuck::cast_slice(vertices));
// update the indices if they're given
if let Some(index_buffer) = buffers.buffer_indices.as_ref() {
let aligned_indices = match mesh.indices.as_ref().unwrap() {
// U16 indices need to be aligned to u32, for wpgu, which are 4-bytes in size.
lyra_gltf::MeshIndices::U16(v) => {
bytemuck::pod_align_to::<u16, u32>(v).1
}
lyra_gltf::MeshIndices::U32(v) => {
bytemuck::pod_align_to::<u32, u32>(v).1
}
};
let index_buffer = index_buffer.1.buffer();
queue.write_buffer(index_buffer, 0, bytemuck::cast_slice(aligned_indices));
}
}
}
#[instrument(skip(self, device, mesh))]
fn create_vertex_index_buffers(
&mut self,
device: &wgpu::Device,
mesh: &Mesh,
) -> (BufferStorage, Option<(wgpu::IndexFormat, BufferStorage)>) {
let positions = mesh.position().unwrap();
let tex_coords: Vec<glam::Vec2> = mesh
.tex_coords()
.cloned()
.unwrap_or_else(|| vec![glam::Vec2::new(0.0, 0.0); positions.len()]);
let normals = mesh.normals().unwrap();
assert!(positions.len() == tex_coords.len() && positions.len() == normals.len());
let mut vertex_inputs = vec![];
for (v, t, n) in izip!(positions.iter(), tex_coords.iter(), normals.iter()) {
vertex_inputs.push(Vertex::new(*v, *t, *n));
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Vertex Buffer"),
contents: bytemuck::cast_slice(vertex_inputs.as_slice()), //vertex_combined.as_slice(),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
let vertex_buffer = BufferStorage::new(vertex_buffer, 0, vertex_inputs.len());
let indices = match mesh.indices.as_ref() {
Some(indices) => {
let (idx_type, len, contents) = match indices {
lyra_gltf::MeshIndices::U16(v) => {
(wgpu::IndexFormat::Uint16, v.len(), bytemuck::cast_slice(v))
}
lyra_gltf::MeshIndices::U32(v) => {
(wgpu::IndexFormat::Uint32, v.len(), bytemuck::cast_slice(v))
}
};
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Index Buffer"),
contents,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
});
let buffer_indices = BufferStorage::new(index_buffer, 0, len);
Some((idx_type, buffer_indices))
}
None => None,
};
(vertex_buffer, indices)
}
#[instrument(skip(self, device, queue, material_buffers, mesh))]
fn create_mesh_buffers(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
material_buffers: &mut RenderAssets<Arc<GpuMaterial>>,
mesh: &Mesh,
) -> MeshBufferStorage {
let (vertex_buffer, buffer_indices) = self.create_vertex_index_buffers(device, mesh);
let material = mesh
.material
.as_ref()
.expect("Material resource not loaded yet");
let material_ref = material.data_ref().unwrap();
let material = material_buffers.entry(material.uuid()).or_insert_with(|| {
debug!(
uuid = material.uuid().to_string(),
"Sending material to gpu"
);
Arc::new(GpuMaterial::from_resource(
device,
queue,
&self.material_bgl,
&material_ref,
))
});
MeshBufferStorage {
buffer_vertex: vertex_buffer,
buffer_indices,
material: Some(material.clone()),
}
}
/// Processes the mesh for the renderer, storing and creating buffers as needed. Returns true if a new mesh was processed.
#[instrument(skip(
self,
device,
queue,
mesh_buffers,
material_buffers,
entity_meshes,
mesh,
entity
))]
fn process_mesh(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
mesh_buffers: &mut RenderAssets<MeshBufferStorage>,
material_buffers: &mut RenderAssets<Arc<GpuMaterial>>,
entity_meshes: &mut FxHashMap<Entity, uuid::Uuid>,
entity: Entity,
mesh: &Mesh,
mesh_uuid: Uuid,
) -> bool {
#[allow(clippy::map_entry)]
if !mesh_buffers.contains_key(&mesh_uuid) {
// create the mesh's buffers
let buffers = self.create_mesh_buffers(device, queue, material_buffers, mesh);
mesh_buffers.insert(mesh_uuid, buffers);
entity_meshes.insert(entity, mesh_uuid);
true
} else {
false
}
}
/// If the resource does not exist in the world, add the default
fn try_init_resource<T: ResourceObject + Default>(world: &mut World) {
if !world.has_resource::<T>() {
world.add_resource_default::<T>();
}
}
}
impl Node for MeshPrepNode {
fn desc(
&mut self,
_: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
NodeDesc::new(NodeType::Node, None, vec![])
}
fn prepare(
&mut self,
_: &mut crate::render::graph::RenderGraph,
world: &mut lyra_ecs::World,
context: &mut crate::render::graph::RenderGraphContext,
) {
let device = &context.device;
let queue = &context.queue;
let last_epoch = world.current_tick();
let mut alive_entities = HashSet::new();
{
// prepare the world with resources
Self::try_init_resource::<RenderMeshes>(world);
Self::try_init_resource::<RenderAssets<MeshBufferStorage>>(world);
Self::try_init_resource::<RenderAssets<Arc<GpuMaterial>>>(world);
Self::try_init_resource::<FxHashMap<Entity, uuid::Uuid>>(world);
let mut render_meshes = world
.get_resource_mut::<RenderMeshes>()
.expect("world missing RenderMeshes resource");
render_meshes.clear();
}
let view = world.view_iter::<(
Entities,
&TransformIndex,
Or<(&MeshHandle, TickOf<MeshHandle>), (&SceneHandle, TickOf<SceneHandle>)>,
ResMut<RenderMeshes>,
ResMut<RenderAssets<MeshBufferStorage>>,
ResMut<RenderAssets<Arc<GpuMaterial>>>,
ResMut<FxHashMap<Entity, uuid::Uuid>>,
)>();
// used to store InterpTransform components to add to entities later
for (
entity,
transform_index,
(mesh_pair, scene_pair),
mut render_meshes,
mut mesh_buffers,
mut material_buffers,
mut entity_meshes,
) in view
{
alive_entities.insert(entity);
if let Some((mesh_han, mesh_epoch)) = mesh_pair {
if let Some(mesh) = mesh_han.data_ref() {
// if process mesh did not just create a new mesh, and the epoch
// shows that the scene has changed, verify that the mesh buffers
// dont need to be resent to the gpu.
if !self.process_mesh(
device,
queue,
&mut mesh_buffers,
&mut material_buffers,
&mut entity_meshes,
entity,
&mesh,
mesh_han.uuid(),
) && mesh_epoch == last_epoch
{
self.check_mesh_buffers(device, queue, &mut mesh_buffers, &mesh_han);
}
let material = mesh.material.as_ref().unwrap().data_ref().unwrap();
let shader = material.shader_uuid.unwrap_or(0);
let job = RenderJob::new(entity, shader, mesh_han.uuid(), *transform_index);
render_meshes.push_back(job);
}
}
if let Some((scene_han, scene_epoch)) = scene_pair {
if let Some(scene) = scene_han.data_ref() {
for (mesh_han, transform_index) in
scene.world().view_iter::<(&MeshHandle, &TransformIndex)>()
{
if let Some(mesh) = mesh_han.data_ref() {
// if process mesh did not just create a new mesh, and the epoch
// shows that the scene has changed, verify that the mesh buffers
// dont need to be resent to the gpu.
if !self.process_mesh(
device,
queue,
&mut mesh_buffers,
&mut material_buffers,
&mut entity_meshes,
entity,
&mesh,
mesh_han.uuid(),
) && scene_epoch == last_epoch
{
self.check_mesh_buffers(
device,
queue,
&mut mesh_buffers,
&mesh_han,
);
}
let material = mesh.material.as_ref().unwrap().data_ref().unwrap();
let shader = material.shader_uuid.unwrap_or(0);
let job =
RenderJob::new(entity, shader, mesh_han.uuid(), *transform_index);
render_meshes.push_back(job);
}
}
}
}
}
}
fn execute(
&mut self,
_: &mut crate::render::graph::RenderGraph,
_: &crate::render::graph::NodeDesc,
_: &mut crate::render::graph::RenderGraphContext,
) {
}
}
#[repr(transparent)]
pub struct RenderAssets<T>(FxHashMap<Uuid, T>);
impl<T> Deref for RenderAssets<T> {
type Target = FxHashMap<Uuid, T>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for RenderAssets<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T> Default for RenderAssets<T> {
fn default() -> Self {
Self(Default::default())
}
}
impl<T> RenderAssets<T> {
pub fn new() -> Self {
Self::default()
}
}
#[allow(dead_code)]
pub struct GpuMaterial {
pub bind_group: Arc<wgpu::BindGroup>,
bind_group_layout: Arc<wgpu::BindGroupLayout>,
material_properties_buffer: wgpu::Buffer,
diffuse_texture: wgpu::Texture,
diffuse_texture_sampler: wgpu::Sampler,
/* specular_texture: wgpu::Texture,
specular_texture_sampler: wgpu::Sampler, */
}
impl GpuMaterial {
fn create_bind_group_layout(device: &wgpu::Device) -> Arc<wgpu::BindGroupLayout> {
Arc::new(
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("bgl_material"),
entries: &[
// material properties
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None, /* Some(
NonZeroU64::new(mem::size_of::<MaterialPropertiesUniform>() as _)
.unwrap(),
) */
},
count: None,
},
// diffuse texture
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// diffuse texture sampler
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
// specular texture
/* wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// specular texture sampler
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
}, */
],
}),
)
}
fn texture_desc(label: &str, size: UVec2) -> wgpu::TextureDescriptor {
//debug!("Texture desc size: {:?}", size);
wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
mip_level_count: 1, // TODO
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
}
}
fn write_texture(queue: &wgpu::Queue, texture: &wgpu::Texture, img: &lyra_resource::Image) {
let dim = img.dimensions();
//debug!("Write texture size: {:?}", dim);
queue.write_texture(
wgpu::ImageCopyTexture {
aspect: wgpu::TextureAspect::All,
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
},
&img.to_rgba8(),
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(4 * dim.0),
rows_per_image: Some(dim.1),
},
wgpu::Extent3d {
width: dim.0,
height: dim.1,
depth_or_array_layers: 1,
},
);
}
fn from_resource(
device: &wgpu::Device,
queue: &wgpu::Queue,
layout: &Arc<wgpu::BindGroupLayout>,
mat: &lyra_gltf::Material,
) -> Self {
//let specular = mat.specular.as_ref().unwrap_or_default();
//let specular_
let prop = MaterialPropertiesUniform {
ambient: Vec3::ONE,
_padding1: 0,
diffuse: Vec3::ONE,
shininess: 32.0,
specular_factor: 0.0,
_padding2: [0; 3],
specular_color_factor: Vec3::ZERO,
_padding3: 0,
};
let properties_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("buffer_material"),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
contents: bytemuck::bytes_of(&prop),
});
let diffuse_tex = mat.base_color_texture.as_ref().unwrap();
let diffuse_tex = diffuse_tex.data_ref().unwrap();
let diffuse_tex_img = diffuse_tex.image.data_ref().unwrap();
let diffuse_tex_dim = diffuse_tex_img.dimensions();
let diffuse_texture = device.create_texture(&Self::texture_desc(
"material_diffuse_texture",
UVec2::new(diffuse_tex_dim.0, diffuse_tex_dim.1),
));
let diffuse_tex_view = diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default());
let sampler_desc = match &diffuse_tex.sampler {
Some(sampler) => {
let magf = res_filter_to_wgpu(
sampler
.mag_filter
.unwrap_or(lyra_resource::FilterMode::Linear),
);
let minf = res_filter_to_wgpu(
sampler
.min_filter
.unwrap_or(lyra_resource::FilterMode::Nearest),
);
let mipf = res_filter_to_wgpu(
sampler
.mipmap_filter
.unwrap_or(lyra_resource::FilterMode::Nearest),
);
let wrap_u = res_wrap_to_wgpu(sampler.wrap_u);
let wrap_v = res_wrap_to_wgpu(sampler.wrap_v);
let wrap_w = res_wrap_to_wgpu(sampler.wrap_w);
wgpu::SamplerDescriptor {
address_mode_u: wrap_u,
address_mode_v: wrap_v,
address_mode_w: wrap_w,
mag_filter: magf,
min_filter: minf,
mipmap_filter: mipf,
..Default::default()
}
}
None => wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
},
};
let diffuse_sampler = device.create_sampler(&sampler_desc);
Self::write_texture(queue, &diffuse_texture, &diffuse_tex_img);
debug!("TODO: specular texture");
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bg_material"),
layout: &layout,
entries: &[
// material properties
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &properties_buffer,
offset: 0,
size: None,
}),
},
// diffuse texture
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&diffuse_tex_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&diffuse_sampler),
},
// TODO: specular textures
],
});
Self {
bind_group: Arc::new(bg),
bind_group_layout: layout.clone(),
material_properties_buffer: properties_buffer,
diffuse_texture,
diffuse_texture_sampler: diffuse_sampler,
}
}
}
/// Uniform for MaterialProperties in a shader
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct MaterialPropertiesUniform {
ambient: glam::Vec3,
_padding1: u32,
diffuse: glam::Vec3,
shininess: f32,
specular_factor: f32,
_padding2: [u32; 3],
specular_color_factor: glam::Vec3,
_padding3: u32,
}
#[derive(Default)]
pub struct RenderMeshes(VecDeque<RenderJob>);
impl Deref for RenderMeshes {
type Target = VecDeque<RenderJob>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for RenderMeshes {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

View file

@ -0,0 +1,500 @@
use std::sync::Arc;
use lyra_ecs::{AtomicRef, ResourceData};
use lyra_game_derive::RenderGraphLabel;
use tracing::{instrument, warn};
use crate::render::{
desc_buf_lay::DescVertexBufferLayout,
graph::{Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext},
resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, VertexState},
texture::RenderTexture,
transform_buffer_storage::TransformBuffers,
vertex::Vertex,
};
use super::{
BasePassSlots, LightBasePassSlots, LightCullComputePassSlots, MeshBufferStorage, RenderAssets, RenderMeshes, ShadowMapsPassSlots
};
#[derive(Debug, Hash, Clone, Default, PartialEq, RenderGraphLabel)]
pub struct MeshesPassLabel;
#[derive(Debug, Hash, Clone, PartialEq, RenderGraphLabel)]
pub enum MeshesPassSlots {
Material,
}
/// Stores the bind group and bind group layout for the shadow atlas texture
struct ShadowsAtlasBgPair {
layout: Arc<wgpu::BindGroupLayout>,
bg: Arc<wgpu::BindGroup>,
}
//#[derive(Default)]
#[allow(dead_code)]
pub struct MeshPass {
default_texture: Option<RenderTexture>,
pipeline: Option<RenderPipeline>,
material_bgl: Arc<wgpu::BindGroupLayout>,
// TODO: find a better way to extract these resources from the main world to be used in the
// render stage.
transform_buffers: Option<ResourceData>,
render_meshes: Option<ResourceData>,
mesh_buffers: Option<ResourceData>,
shadows_atlas: Option<ShadowsAtlasBgPair>,
}
impl MeshPass {
pub fn new(material_bgl: Arc<wgpu::BindGroupLayout>) -> Self {
Self {
default_texture: None,
pipeline: None,
material_bgl,
transform_buffers: None,
render_meshes: None,
mesh_buffers: None,
shadows_atlas: None,
}
}
fn transform_buffers(&self) -> AtomicRef<TransformBuffers> {
self.transform_buffers.as_ref().unwrap().get()
}
fn render_meshes(&self) -> AtomicRef<RenderMeshes> {
self.render_meshes.as_ref().unwrap().get()
}
fn mesh_buffers(&self) -> AtomicRef<RenderAssets<MeshBufferStorage>> {
self.mesh_buffers.as_ref().unwrap().get()
}
}
impl Node for MeshPass {
fn desc(
&mut self,
_: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
// load the default texture
//let bytes = include_bytes!("../../default_texture.png");
//self.default_texture = Some(RenderTexture::from_bytes(device, &graph.queue, texture_bind_group_layout.clone(), bytes, "default_texture").unwrap());
NodeDesc::new(
NodeType::Render,
None,
vec![
//(&MeshesPassSlots::Material, material_bg, Some(material_bgl)),
],
)
}
#[instrument(skip(self, graph, world))]
fn prepare(
&mut self,
graph: &mut RenderGraph,
world: &mut lyra_ecs::World,
_: &mut RenderGraphContext,
) {
if self.pipeline.is_none() {
let shader = graph.load_shader_str("mesh_base", include_str!("../../shaders/base.wgsl"))
.expect("failed to load base mesh shader");
let device = graph.device();
let surface_config_format = graph.view_target().format();
let atlas_view = graph
.slot_value(ShadowMapsPassSlots::ShadowAtlasTextureView)
.expect("missing ShadowMapsPassSlots::ShadowAtlasTextureView")
.as_texture_view()
.unwrap();
let atlas_sampler = graph
.slot_value(ShadowMapsPassSlots::ShadowAtlasSampler)
.expect("missing ShadowMapsPassSlots::ShadowAtlasSampler")
.as_sampler()
.unwrap();
let atlas_sampler_compare = graph
.slot_value(ShadowMapsPassSlots::ShadowAtlasSamplerComparison)
.expect("missing ShadowMapsPassSlots::ShadowAtlasSamplerComparison")
.as_sampler()
.unwrap();
let shadow_settings_buf = graph
.slot_value(ShadowMapsPassSlots::ShadowSettingsUniform)
.expect("missing ShadowMapsPassSlots::ShadowSettingsUniform")
.as_buffer()
.unwrap();
let light_uniform_buf = graph
.slot_value(ShadowMapsPassSlots::ShadowLightUniformsBuffer)
.expect("missing ShadowMapsPassSlots::ShadowLightUniformsBuffer")
.as_buffer()
.unwrap();
let pcf_poisson_disc = graph
.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer)
.expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer")
.as_buffer()
.unwrap();
let pcf_poisson_disc_3d = graph
.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer3d)
.expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer3d")
.as_buffer()
.unwrap();
let pcss_poisson_disc = graph
.slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer)
.expect("missing ShadowMapsPassSlots::PcssPoissonDiscBuffer")
.as_buffer()
.unwrap();
let atlas_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("bgl_shadows_atlas"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 5,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 6,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 7,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let atlas_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bg_shadows_atlas"),
layout: &atlas_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(atlas_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(atlas_sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(atlas_sampler_compare),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: shadow_settings_buf,
offset: 0,
size: None,
}),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: light_uniform_buf,
offset: 0,
size: None,
}),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: pcf_poisson_disc,
offset: 0,
size: None,
}),
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: pcf_poisson_disc_3d,
offset: 0,
size: None,
}),
},
wgpu::BindGroupEntry {
binding: 7,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: pcss_poisson_disc,
offset: 0,
size: None,
}),
},
],
});
self.shadows_atlas = Some(ShadowsAtlasBgPair {
layout: Arc::new(atlas_layout),
bg: Arc::new(atlas_bg),
});
let camera_bgl = graph.bind_group_layout(BasePassSlots::Camera);
let lights_bgl = graph.bind_group_layout(LightBasePassSlots::Lights);
let light_grid_bgl =
graph.bind_group_layout(LightCullComputePassSlots::LightIndicesGridGroup);
let atlas_bgl = self.shadows_atlas.as_ref().unwrap().layout.clone();
let transforms = world
.get_resource_data::<TransformBuffers>()
.expect("Missing transform buffers");
self.transform_buffers = Some(transforms.clone());
let render_meshes = world
.get_resource_data::<RenderMeshes>()
.expect("Missing transform buffers");
self.render_meshes = Some(render_meshes.clone());
let mesh_buffers = world
.get_resource_data::<RenderAssets<MeshBufferStorage>>()
.expect("Missing render meshes");
self.mesh_buffers = Some(mesh_buffers.clone());
let transforms = transforms.get::<TransformBuffers>();
self.pipeline = Some(RenderPipeline::create(
device,
&RenderPipelineDescriptor {
label: Some("meshes".into()),
layouts: vec![
self.material_bgl.clone(),
transforms.bindgroup_layout.clone(),
camera_bgl.clone(),
lights_bgl.clone(),
light_grid_bgl.clone(),
atlas_bgl,
],
push_constant_ranges: vec![],
vertex: VertexState {
module: shader.clone(),
entry_point: "vs_main".into(),
buffers: vec![Vertex::desc().into()],
},
fragment: Some(FragmentState {
module: shader,
entry_point: "fs_main".into(),
targets: vec![Some(wgpu::ColorTargetState {
format: surface_config_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
depth_stencil: Some(wgpu::DepthStencilState {
format: RenderTexture::DEPTH_FORMAT,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Less,
stencil: wgpu::StencilState::default(), // TODO: stencil buffer
bias: wgpu::DepthBiasState::default(),
}),
primitive: wgpu::PrimitiveState {
cull_mode: Some(wgpu::Face::Back),
..Default::default()
},
multisample: wgpu::MultisampleState::default(),
multiview: None,
},
));
}
}
fn execute(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
_: &crate::render::graph::NodeDesc,
context: &mut crate::render::graph::RenderGraphContext,
) {
let encoder = context.encoder.as_mut().unwrap();
/* let view = graph
.slot_value(BasePassSlots::WindowTextureView)
.unwrap()
.as_texture_view()
.expect("BasePassSlots::WindowTextureView was not a TextureView slot"); */
let vt = graph.view_target();
let view = vt.render_view();
let depth_view = graph
.slot_value(BasePassSlots::DepthTextureView)
.unwrap()
.as_texture_view()
.expect("BasePassSlots::DepthTextureView was not a TextureView slot");
let camera_bg = graph.bind_group(BasePassSlots::Camera);
let lights_bg = graph.bind_group(LightBasePassSlots::Lights);
let light_grid_bg = graph.bind_group(LightCullComputePassSlots::LightIndicesGridGroup);
let shadows_atlas_bg = &self.shadows_atlas.as_ref().unwrap().bg;
//let material_bg = graph.bind_group(MeshesPassSlots::Material);
/* let pipeline = graph.pipeline(context.label.clone())
.expect("Failed to find pipeline for MeshPass"); */
let pipeline = self.pipeline.as_ref().unwrap();
let transforms = self.transform_buffers();
let render_meshes = self.render_meshes();
let mesh_buffers = self.mesh_buffers();
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,/* wgpu::LoadOp::Clear(wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
}), */
store: wgpu::StoreOp::Store,
},
})],
// enable depth buffer
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
..Default::default()
});
pass.set_pipeline(pipeline);
//let default_texture = self.default_texture.as_ref().unwrap();
for job in render_meshes.iter() {
// get the mesh (containing vertices) and the buffers from storage
let buffers = mesh_buffers.get(&job.asset_uuid);
if buffers.is_none() {
warn!("Skipping job since its mesh is missing {:?}", job.asset_uuid);
continue;
}
let buffers = buffers.unwrap();
// Bind the optional texture
/* if let Some(tex) = buffers.material.as_ref()
.and_then(|m| m.diffuse_texture.as_ref()) {
pass.set_bind_group(0, tex.bind_group(), &[]);
} else {
pass.set_bind_group(0, default_texture.bind_group(), &[]);
}
if let Some(tex) = buffers.material.as_ref()
.and_then(|m| m.specular.as_ref())
.and_then(|s| s.texture.as_ref().or(s.color_texture.as_ref())) {
pass.set_bind_group(5, tex.bind_group(), &[]);
} else {
pass.set_bind_group(5, default_texture.bind_group(), &[]);
} */
if let Some(mat) = buffers.material.as_ref() {
pass.set_bind_group(0, &mat.bind_group, &[]);
} else {
todo!("cannot render mesh without material");
}
// Get the bindgroup for job's transform and bind to it using an offset.
let bindgroup = transforms.bind_group(job.transform_id);
let offset = transforms.buffer_offset(job.transform_id);
pass.set_bind_group(1, bindgroup, &[offset]);
pass.set_bind_group(2, camera_bg, &[]);
pass.set_bind_group(3, lights_bg, &[]);
//pass.set_bind_group(4, material_bg, &[]);
pass.set_bind_group(4, light_grid_bg, &[]);
pass.set_bind_group(5, shadows_atlas_bg, &[]);
// if this mesh uses indices, use them to draw the mesh
if let Some((idx_type, indices)) = buffers.buffer_indices.as_ref() {
let indices_len = indices.count() as u32;
pass.set_vertex_buffer(
buffers.buffer_vertex.slot(),
buffers.buffer_vertex.buffer().slice(..),
);
pass.set_index_buffer(indices.buffer().slice(..), *idx_type);
pass.draw_indexed(0..indices_len, 0, 0..1);
} else {
let vertex_count = buffers.buffer_vertex.count();
pass.set_vertex_buffer(
buffers.buffer_vertex.slot(),
buffers.buffer_vertex.buffer().slice(..),
);
pass.draw(0..vertex_count as u32, 0..1);
}
}
}
}
}

View file

@ -0,0 +1,35 @@
mod light_cull_compute;
pub use light_cull_compute::*;
mod base;
pub use base::*;
mod meshes;
pub use meshes::*;
mod light_base;
pub use light_base::*;
mod present_pass;
pub use present_pass::*;
mod init;
pub use init::*;
mod tint;
pub use tint::*;
mod fxaa;
pub use fxaa::*;
mod shadows;
pub use shadows::*;
mod mesh_prepare;
pub use mesh_prepare::*;
mod transform;
pub use transform::*;
mod sprite;
pub use sprite::*;

View file

@ -0,0 +1,44 @@
use std::hash::Hash;
use lyra_game_derive::RenderGraphLabel;
use crate::render::graph::{Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext};
#[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)]
pub struct PresentPassLabel;
/// Supplies some basic things other passes needs.
///
/// screen size buffer, camera buffer,
#[derive(Default, Debug)]
pub struct PresentPass;
impl PresentPass {
pub fn new() -> Self {
Self
}
}
impl Node for PresentPass {
fn desc(&mut self, _graph: &mut crate::render::graph::RenderGraph) -> crate::render::graph::NodeDesc {
NodeDesc::new(
NodeType::Presenter,
None,
vec![],
)
}
fn prepare(&mut self, _graph: &mut RenderGraph, _world: &mut lyra_ecs::World, _context: &mut RenderGraphContext) {
}
fn execute(&mut self, graph: &mut crate::render::graph::RenderGraph, _desc: &crate::render::graph::NodeDesc, context: &mut crate::render::graph::RenderGraphContext) {
let mut vt = graph.view_target_mut();
vt.copy_to_primary(context.encoder.as_mut().unwrap());
context.submit_encoder();
let frame = vt.primary.frame.take()
.expect("ViewTarget.primary was already presented");
frame.present();
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,558 @@
use std::{collections::VecDeque, sync::Arc};
use glam::{UVec2, Vec2, Vec3};
use image::GenericImageView;
use lyra_ecs::{
query::{filter::Or, Entities, ResMut, TickOf},
AtomicRef, Entity, ResourceData,
};
use lyra_game_derive::RenderGraphLabel;
use lyra_math::URect;
use lyra_resource::Image;
use rustc_hash::FxHashMap;
use tracing::{debug, instrument, warn};
use uuid::Uuid;
use wgpu::util::DeviceExt;
use crate::{
render::{
graph::{Node, NodeDesc, NodeType, SlotAttribute},
resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, VertexState},
texture::RenderTexture,
vertex::Vertex2D,
EitherTransform,
},
scene::{CameraSortingAxis, SortingOffset},
sprite::{AtlasSprite, Sprite},
};
use super::{BasePassSlots, RenderAssets};
#[derive(Clone)]
pub struct RenderJob {
pub entity: Entity,
pub shader_id: u64,
pub asset_uuid: uuid::Uuid,
pub atlas_frame_id: u64,
pub position: Vec3,
pub sort_offset: Vec3,
pub is_transparent: bool,
}
#[derive(Default, Debug, Clone, Copy, Hash, RenderGraphLabel)]
pub struct SpritePassLabel;
#[derive(Debug, Hash, Clone, PartialEq, RenderGraphLabel)]
pub enum SpritePassSlots {
SpriteTexture,
SpriteTextureView,
SpriteTextureSampler,
}
struct SpriteTexture {
// this field is actually read, its given to the bind group
#[allow(dead_code)]
texture: wgpu::Texture,
// this field is actually read, its given to the bind group
#[allow(dead_code)]
sampler: wgpu::Sampler,
texture_bg: Arc<wgpu::BindGroup>,
}
struct SpriteBuffers {
vertex_buffers: wgpu::Buffer,
index_buffers: wgpu::Buffer,
}
#[derive(Default)]
pub struct SpritePass {
pipeline: Option<RenderPipeline>,
texture_bgl: Option<Arc<wgpu::BindGroupLayout>>,
jobs: VecDeque<RenderJob>,
sprite_instances_buf: Option<Arc<wgpu::Buffer>>,
texture_store: Option<ResourceData>,
buffer_store: Option<ResourceData>,
}
impl SpritePass {
pub fn new() -> Self {
Self::default()
}
#[instrument(skip(self, device))]
fn create_vertex_index_buffers(
&mut self,
device: &wgpu::Device,
dimensions: UVec2,
) -> (wgpu::Buffer, wgpu::Buffer) {
let vertices = vec![
// top left
Vertex2D::new(Vec3::new(0.0, 0.0, 0.0), Vec2::new(0.0, 1.0)),
// bottom left
Vertex2D::new(
Vec3::new(0.0, dimensions.y as f32, 0.0),
Vec2::new(0.0, 0.0),
),
// top right
Vertex2D::new(
Vec3::new(dimensions.x as f32, 0.0, 0.0),
Vec2::new(1.0, 1.0),
),
// bottom right
Vertex2D::new(
Vec3::new(dimensions.x as f32, dimensions.y as f32, 0.0),
Vec2::new(1.0, 0.0),
),
];
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Vertex Buffer"),
contents: bytemuck::cast_slice(vertices.as_slice()),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
let contents: [u32; 6] = [
//3, 1, 0,
//0, 2, 3
3, 1, 0, // second tri
0, 2, 3, // first tri
//0, 2, 3, // second tri
];
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Index Buffer"),
contents: bytemuck::cast_slice(&contents),
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
});
(vertex_buffer, index_buffer)
}
fn load_sprite_texture(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
uuid: &Uuid,
image: &Image,
) -> Option<(wgpu::Texture, wgpu::Sampler, wgpu::BindGroup)> {
let uuid_str = uuid.to_string();
let image_dim = image.dimensions();
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some(&format!("sprite_texture_{}", uuid_str)),
size: wgpu::Extent3d {
width: image_dim.0,
height: image_dim.1,
depth_or_array_layers: 1,
},
mip_level_count: 1, // TODO
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let tex_view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
queue.write_texture(
wgpu::ImageCopyTexture {
aspect: wgpu::TextureAspect::All,
texture: &tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
},
&image.to_rgba8(),
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(4 * image_dim.0),
rows_per_image: Some(image_dim.1),
},
wgpu::Extent3d {
width: image_dim.0,
height: image_dim.1,
depth_or_array_layers: 1,
},
);
let sprite_instances = self.sprite_instances_buf.as_ref().unwrap();
let bgl = self.texture_bgl.as_ref().unwrap();
let tex_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&format!("sprite_texture_bg_{}", uuid_str)),
layout: bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&tex_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &sprite_instances,
offset: 0,
size: None,
}),
},
],
});
Some((tex, sampler, tex_bg))
}
}
impl Node for SpritePass {
fn desc(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
let device = &graph.device;
let sprite_instances = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("sprite_instances"),
size: std::mem::size_of::<SpriteInstance>() as u64 * 5000,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.sprite_instances_buf = Some(Arc::new(sprite_instances));
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("bgl_sprite_main"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
self.texture_bgl = Some(Arc::new(bgl));
let mut desc = NodeDesc::new(NodeType::Render, None, vec![]);
desc.add_buffer_slot(BasePassSlots::Camera, SlotAttribute::Input, None);
desc
}
fn prepare(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
world: &mut lyra_ecs::World,
_: &mut crate::render::graph::RenderGraphContext,
) {
let device = graph.device();
let vt = graph.view_target();
if self.pipeline.is_none() {
let shader = graph
.load_shader_str(
"sprite_shader",
include_str!("../../shaders/2d/sprite_main.wgsl"),
)
.expect("failed to load wgsl shader from manager");
let diffuse_bgl = self.texture_bgl.clone().unwrap();
let camera_bgl = graph.bind_group_layout(BasePassSlots::Camera).clone();
self.pipeline = Some(RenderPipeline::create(
device,
&RenderPipelineDescriptor {
label: Some("sprite_pass".into()),
layouts: vec![diffuse_bgl, camera_bgl],
push_constant_ranges: vec![],
vertex: VertexState {
module: shader.clone(),
entry_point: "vs_main".into(),
buffers: vec![Vertex2D::desc().into()],
},
fragment: Some(FragmentState {
module: shader,
entry_point: "fs_main".into(),
targets: vec![Some(wgpu::ColorTargetState {
format: vt.format(),
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
depth_stencil: Some(wgpu::DepthStencilState {
format: RenderTexture::DEPTH_FORMAT,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Less,
stencil: wgpu::StencilState::default(), // TODO: stencil buffer
bias: wgpu::DepthBiasState::default(),
}),
primitive: wgpu::PrimitiveState {
cull_mode: Some(wgpu::Face::Back),
..Default::default()
},
multisample: wgpu::MultisampleState::default(),
multiview: None,
},
));
world.add_resource_default_if_absent::<RenderAssets<SpriteTexture>>();
let texture_store = world
.get_resource_data::<RenderAssets<SpriteTexture>>()
.unwrap();
self.texture_store = Some(texture_store.clone());
world.add_resource_default_if_absent::<FxHashMap<Entity, SpriteBuffers>>();
let buffer_store = world
.get_resource_data::<FxHashMap<Entity, SpriteBuffers>>()
.unwrap();
self.buffer_store = Some(buffer_store.clone());
}
let mut sprite_instances = Vec::with_capacity(500);
let world_tick = world.current_tick();
let queue = &graph.queue;
for (
entity,
(sprite, atlas_sprite),
transform,
sorting_offset,
mut texture_store,
mut buffer_store,
) in world
.view::<(
Entities,
Or<&Sprite, (&AtlasSprite, TickOf<AtlasSprite>)>,
EitherTransform,
Option<&SortingOffset>,
ResMut<RenderAssets<SpriteTexture>>,
ResMut<FxHashMap<Entity, SpriteBuffers>>,
)>()
.iter()
{
let tex = if let Some(sprite) = &sprite {
sprite.texture.clone() //.data_ref()
} else if let Some((a, _)) = &atlas_sprite {
a.atlas.data_ref().unwrap().texture.clone()
} else {
continue;
};
let rect = atlas_sprite.as_ref().map(|(a, _)| a.sprite);
if let Some(image) = tex.data_ref() {
let texture_uuid = tex.uuid();
if !texture_store.contains_key(&texture_uuid) {
// returns `None` if the Texture image is not loaded.
if let Some((texture, sampler, tex_bg)) =
self.load_sprite_texture(device, queue, &texture_uuid, &image)
{
texture_store.insert(
texture_uuid,
SpriteTexture {
texture,
sampler,
texture_bg: Arc::new(tex_bg),
},
);
}
}
let dim = rect.map(|r| r.dimensions()).unwrap_or_else(|| {
let i = image.dimensions();
UVec2::new(i.0, i.1)
});
if !buffer_store.contains_key(&entity) {
let (vertex, index) = self.create_vertex_index_buffers(device, dim);
buffer_store.insert(
entity,
SpriteBuffers {
vertex_buffers: vertex,
index_buffers: index,
},
);
} else if let Some((ats, tick)) = &atlas_sprite {
// detect a change for the vertex and index buffers of the sprite
if tick.checked_sub(1).unwrap_or(0) >= *world_tick {
debug!("Updating buffer for entity after change detected in atlas sprite");
let dim = ats.sprite.dimensions();
let (vertex, index) = self.create_vertex_index_buffers(device, dim);
buffer_store.insert(
entity,
SpriteBuffers {
vertex_buffers: vertex,
index_buffers: index,
},
);
}
}
let pivot = atlas_sprite
.map(|ats| ats.0.pivot)
// unwrap is safe since its either AtlasSprite or Sprite.
.unwrap_or_else(|| sprite.unwrap().pivot)
.as_vec();
let pivot_pos = dim.as_vec2() * (pivot - Vec2::splat(0.5));
let transform =
*transform + lyra_math::Transform::from_translation(pivot_pos.extend(0.0));
let inst = SpriteInstance {
atlas_frame: rect.unwrap_or(URect::ZERO),
transform: transform.calculate_mat4(),
};
sprite_instances.push(inst);
let inst_id = sprite_instances.len() as u64 - 1;
self.jobs.push_back(RenderJob {
entity,
shader_id: 0,
asset_uuid: texture_uuid,
atlas_frame_id: inst_id,
position: transform.translation,
sort_offset: sorting_offset.map(|o| o.0).unwrap_or_default(),
is_transparent: image.is_transparent,
});
};
}
// Sort the jobs be the sorting axis if one exists.
// If there is one, there should only be one, so `next` is fine.
if let Some(sort_order) = world.view_iter::<&CameraSortingAxis>().next() {
self.jobs
.make_contiguous()
.sort_by(|aj: &RenderJob, bj: &RenderJob| {
let a = (aj.position + aj.sort_offset) * sort_order.0;
let b = (bj.position + bj.sort_offset) * sort_order.0;
// If just one of the job is a transparent texture, only consider that.
// Else compare the Vec3
if !aj.is_transparent ^ !bj.is_transparent {
aj.is_transparent.cmp(&bj.is_transparent)
} else {
// Vec3 does not implement PartialOrd
a.x.partial_cmp(&b.x)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
.then(a.z.partial_cmp(&b.z).unwrap_or(std::cmp::Ordering::Equal))
}
});
}
let buf = self.sprite_instances_buf.as_ref().unwrap();
// skip default rect
queue.write_buffer(buf, 0, bytemuck::cast_slice(&sprite_instances));
}
fn execute(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
_: &crate::render::graph::NodeDesc,
context: &mut crate::render::graph::RenderGraphContext,
) {
let pipeline = self.pipeline.as_ref().unwrap();
let texture_store = self.texture_store.clone().unwrap();
let texture_store: AtomicRef<RenderAssets<SpriteTexture>> = texture_store.get();
let buffer_store = self.buffer_store.clone().unwrap();
let buffer_store: AtomicRef<FxHashMap<Entity, SpriteBuffers>> = buffer_store.get();
let vt = graph.view_target();
let view = vt.render_view();
let depth_view = graph
.slot_value(BasePassSlots::DepthTextureView)
.unwrap()
.as_texture_view()
.expect("BasePassSlots::DepthTextureView was not a TextureView slot");
let camera_bg = graph.bind_group(BasePassSlots::Camera);
{
let encoder = context.encoder.as_mut().unwrap();
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("sprite_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(pipeline);
while let Some(job) = self.jobs.pop_front() {
// bind texture
let tex = texture_store
.get(&job.asset_uuid)
.expect("failed to find SpriteTexture for job asset_uuid");
pass.set_bind_group(0, &tex.texture_bg, &[]);
pass.set_bind_group(1, camera_bg, &[]);
// set vertex and index buffers
let bufs = buffer_store
.get(&job.entity)
.expect("failed to find buffers for job entity");
pass.set_vertex_buffer(0, bufs.vertex_buffers.slice(..));
pass.set_index_buffer(bufs.index_buffers.slice(..), wgpu::IndexFormat::Uint32);
// use the atlas frame id as the instance
let inst = job.atlas_frame_id as u32;
pass.draw_indexed(0..6, 0, inst..inst + 1);
}
}
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct SpriteInstance {
atlas_frame: URect,
transform: glam::Mat4,
}

View file

@ -0,0 +1,164 @@
use std::{collections::HashMap, sync::Arc};
use lyra_game_derive::RenderGraphLabel;
use crate::render::{
graph::{Node, NodeDesc, NodeType},
resource::{FragmentState, PipelineDescriptor, RenderPipelineDescriptor, VertexState},
};
#[derive(Default, Debug, Clone, Copy, Hash, RenderGraphLabel)]
pub struct TintPassLabel;
#[derive(Debug, Default)]
pub struct TintPass {
target_sampler: Option<wgpu::Sampler>,
bgl: Option<Arc<wgpu::BindGroupLayout>>,
/// Store bind groups for the input textures.
/// The texture may change due to resizes, or changes to the view target chain
/// from other nodes.
bg_cache: HashMap<wgpu::Id<wgpu::TextureView>, wgpu::BindGroup>,
}
impl TintPass {
pub fn new() -> Self {
Self::default()
}
}
impl Node for TintPass {
fn desc(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
let device = &graph.device;
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("tint_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
},
],
});
let bgl = Arc::new(bgl);
self.bgl = Some(bgl.clone());
self.target_sampler = Some(device.create_sampler(&wgpu::SamplerDescriptor::default()));
let vt = graph.view_target();
let shader = graph.load_shader_str("tint_shader", include_str!("../../shaders/tint.wgsl"))
.expect("failed to load tint shader");
NodeDesc::new(
NodeType::Render,
Some(PipelineDescriptor::Render(RenderPipelineDescriptor {
label: Some("tint_pass".into()),
layouts: vec![bgl.clone()],
push_constant_ranges: vec![],
vertex: VertexState {
module: shader.clone(),
entry_point: "vs_main".into(),
buffers: vec![],
},
fragment: Some(FragmentState {
module: shader,
entry_point: "fs_main".into(),
targets: vec![Some(wgpu::ColorTargetState {
format: vt.format(),
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
depth_stencil: None,
primitive: wgpu::PrimitiveState::default(),
multisample: wgpu::MultisampleState::default(),
multiview: None,
})),
vec![],
)
}
fn prepare(
&mut self,
_: &mut crate::render::graph::RenderGraph,
_: &mut lyra_ecs::World,
_: &mut crate::render::graph::RenderGraphContext,
) {
//todo!()
}
fn execute(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
_: &crate::render::graph::NodeDesc,
context: &mut crate::render::graph::RenderGraphContext,
) {
let pipeline = graph
.pipeline(context.label.clone())
.expect("Failed to find pipeline for TintPass");
let mut vt = graph.view_target_mut();
let chain = vt.get_chain();
let source_view = chain.source.frame_view.as_ref().unwrap();
let dest_view = chain.dest.frame_view.as_ref().unwrap();
let bg = self
.bg_cache
.entry(source_view.global_id())
.or_insert_with(|| {
graph
.device()
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("tint_bg"),
layout: self.bgl.as_ref().unwrap(),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(source_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(
self.target_sampler.as_ref().unwrap(),
),
},
],
})
});
{
let encoder = context.encoder.as_mut().unwrap();
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("tint_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: dest_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(pipeline.as_render());
pass.set_bind_group(0, bg, &[]);
pass.draw(0..3, 0..1);
}
}
}

View file

@ -0,0 +1,165 @@
use lyra_ecs::{
query::{
filter::{Changed, Or},
Entities,
},
Entity,
};
use lyra_game_derive::RenderGraphLabel;
use lyra_math::Transform;
use lyra_resource::ResHandle;
use lyra_scene::{SceneGraph, WorldTransform};
use tracing::debug;
use crate::{
render::{
graph::{Node, NodeDesc, NodeType},
transform_buffer_storage::{TransformBuffers, TransformIndex},
EitherTransform,
},
DeltaTime,
};
#[derive(Default, Debug, Clone, Copy, Hash, RenderGraphLabel)]
pub struct TransformsNodeLabel;
#[derive(Debug, Default)]
pub struct TransformsNode;
impl TransformsNode {
pub fn new() -> Self {
Self
}
}
fn process_component_queue(
world: &mut lyra_ecs::World,
component_queue: Vec<(Entity, Option<TransformIndex>)>,
) {
for (en, index) in component_queue {
if let Some(index) = index {
world.insert(en, index);
}
}
}
fn update_transforms(
device: &wgpu::Device,
queue: &wgpu::Queue,
limits: &wgpu::Limits,
world: &mut lyra_ecs::World,
delta_time: DeltaTime,
buffers: &mut TransformBuffers,
parent_transform: Transform,
) {
let mut component_queue = vec![];
let view = world.filtered_view_iter::<(
Entities,
EitherTransform,
Option<&TransformIndex>,
Option<&ResHandle<SceneGraph>>,
), Or<Changed<WorldTransform>, Changed<Transform>>>();
for (entity, transform, transform_index, scene_graph) in view {
// expand the transform buffers if they need to be.
if buffers.needs_expand() {
debug!("Expanding transform buffers");
buffers.expand_buffers(device);
}
// offset this transform by its parent
let transform = *transform + parent_transform;
// Get the TransformIndex from the entity, or reserve a new one if the entity doesn't have
// the component.
let index = match transform_index {
Some(i) => *i,
None => {
let i = buffers.reserve_transform(&device);
component_queue.push((entity, Some(i)));
i
}
};
// TODO: only update if the transform changed.
buffers.update(
&queue,
index,
transform.calculate_mat4(),
glam::Mat3::from_quat(transform.rotation),
);
if let Some(scene) = scene_graph {
if let Some(mut scene) = scene.data_mut() {
update_transforms(
device,
queue,
limits,
scene.world_mut(),
delta_time,
buffers,
transform,
);
}
}
}
process_component_queue(world, component_queue);
}
impl Node for TransformsNode {
fn desc(
&mut self,
_: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
NodeDesc::new(NodeType::Node, None, vec![])
}
fn prepare(
&mut self,
_: &mut crate::render::graph::RenderGraph,
world: &mut lyra_ecs::World,
context: &mut crate::render::graph::RenderGraphContext,
) {
let device = &context.device;
let queue = &context.queue;
let render_limits = device.limits();
// prepare the world with resources
if !world.has_resource::<TransformBuffers>() {
let buffers = TransformBuffers::new(device);
world.add_resource(buffers);
}
// I have to do this weird garbage to borrow the `TransformBuffers`
// without running into a borrow checker error from passing `world` as mutable.
// This is safe since I know that the recursive function isn't accessing this
// TransformBuffers, or any other ones in other worlds.
let buffers = world
.get_resource_data::<TransformBuffers>()
.map(|r| r.clone())
.unwrap();
let mut buffers = buffers.get_mut();
let dt = world.get_resource::<DeltaTime>().unwrap().clone();
update_transforms(
&device,
&queue,
&render_limits,
world,
dt,
&mut buffers,
Transform::default(),
);
}
fn execute(
&mut self,
_: &mut crate::render::graph::RenderGraph,
_: &crate::render::graph::NodeDesc,
_: &mut crate::render::graph::RenderGraphContext,
) {
}
}

View file

@ -0,0 +1,355 @@
use std::sync::Arc;
use tracing::debug;
use crate::math;
enum RenderTargetInner {
Surface {
/// The surface that will be rendered to.
///
/// You can create a new surface with a `'static` lifetime if you have an `Arc<Window>`:
/// ```nobuild
/// let window = Arc::new(window);
/// let surface = instance.create_surface(Arc::clone(&window))?;
/// ```
surface: wgpu::Surface<'static>,
/// the configuration of the surface render target..
config: wgpu::SurfaceConfiguration,
},
Texture {
/// The texture that will be rendered to.
texture: Arc<wgpu::Texture>,
}
}
/// A render target that is a surface or a texture.
#[repr(transparent)]
pub struct RenderTarget(RenderTargetInner);
impl From<wgpu::Texture> for RenderTarget {
fn from(value: wgpu::Texture) -> Self {
Self(RenderTargetInner::Texture { texture: Arc::new(value) })
}
}
impl RenderTarget {
pub fn from_surface(surface: wgpu::Surface<'static>, config: wgpu::SurfaceConfiguration) -> Self {
Self(RenderTargetInner::Surface { surface, config })
}
pub fn new_texture(device: &wgpu::Device, format: wgpu::TextureFormat, size: math::UVec2) -> Self {
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
Self(RenderTargetInner::Texture { texture: Arc::new(tex) })
}
pub fn format(&self) -> wgpu::TextureFormat {
match &self.0 {
RenderTargetInner::Surface { config, .. } => config.format,
RenderTargetInner::Texture { texture } => texture.format(),
}
}
pub fn size(&self) -> math::UVec2 {
match &self.0 {
RenderTargetInner::Surface { config, .. } => math::UVec2::new(config.width, config.height),
RenderTargetInner::Texture { texture } => {
let s = texture.size();
math::UVec2::new(s.width, s.height)
},
}
}
/// Get the frame texture of the [`RenderTarget`]
///
/// If this is target is a surface and the frame texture was already retrieved from the swap
/// chain, a [`wgpu::SurfaceError`] error will be returned.
pub fn frame_texture(&self) -> Result<FrameTexture, wgpu::SurfaceError> {
match &self.0 {
RenderTargetInner::Surface { surface, .. } => Ok(FrameTexture::Surface(surface.get_current_texture()?)),
RenderTargetInner::Texture { texture } => Ok(FrameTexture::Texture(texture.clone())),
}
}
pub fn resize(&mut self, device: &wgpu::Device, new_size: math::UVec2) {
match &mut self.0 {
RenderTargetInner::Surface { surface, config } => {
config.width = new_size.x;
config.height = new_size.y;
surface.configure(device, config);
},
RenderTargetInner::Texture { texture } => {
let format = texture.format();
let size = self.size();
*self = Self::new_texture(device, format, size);
},
}
}
/// Create the frame of the RenderTarget.
///
/// If this is target is a surface and the frame texture was already retrieved from the
/// swap chain, a [`wgpu::SurfaceError`] error will be returned.
pub fn create_frame(&self) -> Frame {
let texture = self.frame_texture()
.expect("failed to create frame texture"); // TODO: should be returned to the user
let size = self.size();
Frame {
size,
texture,
}
}
}
pub enum FrameTexture {
Surface(wgpu::SurfaceTexture),
Texture(Arc<wgpu::Texture>),
}
/// Represents the current frame that is being rendered to.
//#[allow(dead_code)]
pub struct Frame {
pub(crate) size: math::UVec2,
pub(crate) texture: FrameTexture,
}
impl Frame {
pub fn texture(&self) -> &wgpu::Texture {
match &self.texture {
FrameTexture::Surface(s) => &s.texture,
FrameTexture::Texture(t) => t,
}
}
/// Present the frame
///
/// If this frame is from a surface, it will be present, else nothing will happen.
pub fn present(self) {
match self.texture {
FrameTexture::Surface(s) => s.present(),
FrameTexture::Texture(_) => {},
}
}
/// The size of the frame
pub fn size(&self) -> math::UVec2 {
self.size
}
}
/// Stores the current frame, and the render target it came from.
pub struct FrameTarget {
pub render_target: RenderTarget,
/// None when a frame has not been created yet
pub frame: Option<Frame>,
/// The view to use to render to the frame.
pub frame_view: Option<wgpu::TextureView>,
}
impl FrameTarget {
pub fn new(render_target: RenderTarget) -> Self {
Self {
render_target,
frame: None,
frame_view: None,
}
}
/// Returns the size of the [`RenderTarget`].
pub fn size(&self) -> math::UVec2 {
self.render_target.size()
}
/// Returns the [`wgpu::TextureFormat`] of the [`RenderTarget`].
pub fn format(&self) -> wgpu::TextureFormat {
self.render_target.format()
}
/// Create the frame using the inner [`RenderTarget`].
pub fn create_frame(&mut self) -> &mut Frame {
self.frame = Some(self.render_target.create_frame());
self.frame.as_mut().unwrap()
}
/// Create the [`wgpu::TextureView`] for the [`Frame`], storing it in self and returning a reference to it.
pub fn create_frame_view(&mut self) -> &wgpu::TextureView {
let frame = self.frame.as_ref().expect("frame was not created, cannot create view");
self.frame_view = Some(frame.texture().create_view(&wgpu::TextureViewDescriptor::default()));
self.frame_view.as_ref().unwrap()
}
}
pub struct TargetViewChain<'a> {
pub source: &'a mut FrameTarget,
pub dest: &'a mut FrameTarget,
}
struct ViewChain {
source: FrameTarget,
dest: FrameTarget,
/// tracks the target that is currently being presented
active: u8,
}
impl ViewChain {
/// Returns the currently active [`FrameTarget`].
fn active(&self) -> &FrameTarget {
if self.active == 0 {
&self.source
} else if self.active == 1 {
&self.dest
} else {
panic!("active chain index became invalid! ({})", self.active);
}
}
}
pub struct ViewTarget {
device: Arc<wgpu::Device>,
/// The primary RenderTarget, likely a Surface
pub primary: FrameTarget,
chain: Option<ViewChain>,
}
impl ViewTarget {
pub fn new(device: Arc<wgpu::Device>, primary: RenderTarget) -> Self {
let mut s = Self {
device,
primary: FrameTarget::new(primary),
chain: None,
};
s.create_chain(s.primary.format(), s.primary.size());
s
}
/// Returns the size of the target.
pub fn size(&self) -> math::UVec2 {
self.primary.size()
}
/// Returns the [`wgpu::TextureFormat`]
pub fn format(&self) -> wgpu::TextureFormat {
self.primary.format()
}
/// Resize all the targets, causes the chain to be recreated.
pub fn resize(&mut self, device: &wgpu::Device, size: math::UVec2) {
if size != self.primary.size() {
self.primary.render_target.resize(device, size);
self.create_chain(self.primary.format(), size);
}
}
fn create_chain(&mut self, format: wgpu::TextureFormat, size: math::UVec2) {
debug!("Creating chain with {:?} format and {:?} size", format, size);
let mut source = FrameTarget::new(RenderTarget::new_texture(&self.device, format, size));
source.create_frame();
source.create_frame_view();
let mut dest = FrameTarget::new(RenderTarget::new_texture(&self.device, format, size));
dest.create_frame();
dest.create_frame_view();
self.chain = Some(ViewChain {
source,
dest,
active: 0,
});
}
/// Cycle the target view chain, storing it in self, and returning a mutable borrow to it.
pub fn get_chain(&mut self) -> TargetViewChain {
let format = self.primary.format();
let size = self.primary.size();
if let Some(chain) = &self.chain {
// check if the chain needs to be recreated
if chain.source.format() != format || chain.source.size() != size {
self.create_chain(format, size);
}
} else {
self.create_chain(format, size);
}
let chain = self.chain.as_mut().unwrap();
if chain.active == 0 {
chain.active = 1;
TargetViewChain {
source: &mut chain.source,
dest: &mut chain.dest,
}
} else if chain.active == 1 {
chain.active = 0;
TargetViewChain {
source: &mut chain.dest,
dest: &mut chain.source,
}
} else {
panic!("active chain index became invalid! ({})", chain.active);
}
}
/// Get the [`wgpu::TextureView`] to render to.
pub fn render_view(&self) -> &wgpu::TextureView {
let chain = self.chain.as_ref().unwrap();
chain.active().frame_view.as_ref().unwrap()
}
/// Copy the chain target to the primary target
///
/// The primary target must have `wgpu::TextureUsages::COPY_DST`. This also resets the active
/// chain texture.
pub fn copy_to_primary(&mut self, encoder: &mut wgpu::CommandEncoder) {
let chain = self.chain.as_mut().unwrap();
let active_tex = chain.active().frame.as_ref().unwrap().texture();
let active_copy = wgpu::ImageCopyTexture {
texture: active_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
};
let dest_tex = self.primary.frame.as_ref().unwrap().texture();
let dest_copy = wgpu::ImageCopyTexture {
texture: dest_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
};
let size = self.primary.size();
let size = wgpu::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
};
encoder.copy_texture_to_texture(active_copy, dest_copy, size);
// reset active texture after a render
// must get the chain again because of the borrow checker
let chain = self.chain.as_mut().unwrap();
chain.active = 0;
}
}

View file

@ -1,4 +1,4 @@
use std::{mem, num::{NonZeroU32, NonZeroU8}};
use std::mem;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TextureViewDescriptor {
@ -16,13 +16,13 @@ pub struct TextureViewDescriptor {
/// Mip level count.
/// If `Some(count)`, `base_mip_level + count` must be less or equal to underlying texture mip count.
/// If `None`, considered to include the rest of the mipmap levels, but at least 1 in total.
pub mip_level_count: Option<NonZeroU32>,
pub mip_level_count: Option<u32>,
/// Base array layer.
pub base_array_layer: u32,
/// Layer count.
/// If `Some(count)`, `base_array_layer + count` must be less or equal to the underlying array count.
/// If `None`, considered to include the rest of the array layers, but at least 1 in total.
pub array_layer_count: Option<NonZeroU32>,
pub array_layer_count: Option<u32>,
}
impl TextureViewDescriptor {
@ -79,7 +79,7 @@ pub struct SamplerDescriptor {
/// If this is enabled, this is a comparison sampler using the given comparison function.
pub compare: Option<wgpu::CompareFunction>,
/// Valid values: 1, 2, 4, 8, and 16.
pub anisotropy_clamp: Option<NonZeroU8>,
pub anisotropy_clamp: u16,
/// Border color to use when address_mode is [`AddressMode::ClampToBorder`]
pub border_color: Option<wgpu::SamplerBorderColor>,
}

View file

@ -1,17 +1,24 @@
pub mod point;
pub mod directional;
pub mod point;
pub mod spotlight;
use lyra_ecs::{Entity, Tick, World};
pub use point::*;
pub use spotlight::*;
use std::{collections::{HashMap, VecDeque}, marker::PhantomData, mem, rc::Rc};
use std::{
collections::{HashMap, VecDeque},
marker::PhantomData,
mem,
sync::Arc,
};
use crate::math::Transform;
use self::directional::DirectionalLight;
use super::graph::LightShadowMapId;
const MAX_LIGHT_COUNT: usize = 16;
/// A struct that stores a list of lights in a wgpu::Buffer.
@ -20,7 +27,7 @@ pub struct LightBuffer<U: Default + bytemuck::Pod + bytemuck::Zeroable> {
/// The max amount of light casters that could fit in this buffer.
pub max_count: usize,
/// The amount of light casters that are taking up space in the buffer.
///
///
/// This means that a light may be inactive in the buffer, by being replaced
/// with a default caster as to not affect lighting. Its easier this way than
/// to recreate the array and remove the gaps.
@ -47,15 +54,27 @@ impl<U: Default + bytemuck::Pod + bytemuck::Zeroable> LightBuffer<U> {
}
/// Update an existing light in the light buffer.
pub fn update_light(&mut self, lights_buffer: &mut [U; MAX_LIGHT_COUNT], entity: Entity, light: U) {
let buffer_idx = *self.used_indexes.get(&entity)
pub fn update_light(
&mut self,
lights_buffer: &mut [U; MAX_LIGHT_COUNT],
entity: Entity,
light: U,
) {
let buffer_idx = *self
.used_indexes
.get(&entity)
.expect("Entity for Light is not in buffer!");
lights_buffer[buffer_idx] = light;
}
/// Add a new light to the light buffer.
pub fn add_light(&mut self, lights_buffer: &mut [U; MAX_LIGHT_COUNT], entity: Entity, light: U) {
pub fn add_light(
&mut self,
lights_buffer: &mut [U; MAX_LIGHT_COUNT],
entity: Entity,
light: U,
) {
let buffer_idx = match self.dead_indexes.pop_front() {
Some(i) => i,
None => {
@ -67,15 +86,20 @@ impl<U: Default + bytemuck::Pod + bytemuck::Zeroable> LightBuffer<U> {
assert!(self.buffer_count <= self.max_count);
i
},
}
};
self.used_indexes.insert(entity, buffer_idx);
self.update_light(lights_buffer, entity, light);
}
/// Update, or add a new caster, to the light buffer.
pub fn update_or_add(&mut self, lights_buffer: &mut [U; MAX_LIGHT_COUNT], entity: Entity, light: U) {
pub fn update_or_add(
&mut self,
lights_buffer: &mut [U; MAX_LIGHT_COUNT],
entity: Entity,
light: U,
) {
if self.used_indexes.contains_key(&entity) {
self.update_light(lights_buffer, entity, light);
} else {
@ -84,7 +108,11 @@ impl<U: Default + bytemuck::Pod + bytemuck::Zeroable> LightBuffer<U> {
}
/// Remove a caster from the buffer, returns true if it was removed.
pub fn remove_light(&mut self, lights_buffer: &mut [U; MAX_LIGHT_COUNT], entity: Entity) -> bool {
pub fn remove_light(
&mut self,
lights_buffer: &mut [U; MAX_LIGHT_COUNT],
entity: Entity,
) -> bool {
if let Some(removed_idx) = self.used_indexes.remove(&entity) {
self.dead_indexes.push_back(removed_idx);
//self.current_count -= 1;
@ -98,9 +126,9 @@ impl<U: Default + bytemuck::Pod + bytemuck::Zeroable> LightBuffer<U> {
}
pub(crate) struct LightUniformBuffers {
pub buffer: Rc<wgpu::Buffer>,
pub bind_group: Rc<wgpu::BindGroup>,
pub bind_group_layout: Rc<wgpu::BindGroupLayout>,
pub buffer: Arc<wgpu::Buffer>,
pub bind_group: Arc<wgpu::BindGroup>,
pub bind_group_layout: Arc<wgpu::BindGroupLayout>,
max_light_count: u64,
}
@ -110,54 +138,44 @@ impl LightUniformBuffers {
// TODO: ensure we dont write over this limit
let max_buffer_sizes = (limits.max_uniform_buffer_binding_size as u64) / 2;
let buffer = device.create_buffer(
&wgpu::BufferDescriptor {
label: Some("UBO_Lights"),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
size: max_buffer_sizes,
mapped_at_creation: false,
}
);
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("UBO_Lights"),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
size: max_buffer_sizes,
mapped_at_creation: false,
});
let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage {
read_only: true
},
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
],
count: None,
}],
label: Some("BGL_Lights"),
});
let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &bindgroup_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(
wgpu::BufferBinding {
buffer: &buffer,
offset: 0,
size: None, // use the full buffer
}
)
},
],
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &buffer,
offset: 0,
size: None, // use the full buffer
}),
}],
label: Some("BG_Lights"),
});
Self {
buffer: Rc::new(buffer),
bind_group: Rc::new(bindgroup),
bind_group_layout: Rc::new(bindgroup_layout),
buffer: Arc::new(buffer),
bind_group: Arc::new(bindgroup),
bind_group_layout: Arc::new(bindgroup_layout),
max_light_count: max_buffer_sizes / mem::size_of::<LightUniform>() as u64,
}
}
@ -166,27 +184,43 @@ impl LightUniformBuffers {
let _ = world_tick;
let mut lights = vec![];
for (point_light, transform) in world.view_iter::<(&PointLight, &Transform)>() {
let uniform = LightUniform::from_point_light_bundle(&point_light, &transform);
for (point_light, transform, shadow_map_id) in
world.view_iter::<(&PointLight, &Transform, Option<&LightShadowMapId>)>()
{
let shadow_map_id = shadow_map_id.map(|m| m.clone());
let uniform =
LightUniform::from_point_light_bundle(&point_light, &transform, shadow_map_id);
lights.push(uniform);
}
for (spot_light, transform) in world.view_iter::<(&SpotLight, &Transform)>() {
let uniform = LightUniform::from_spot_light_bundle(&spot_light, &transform);
for (spot_light, transform, shadow_map_id) in
world.view_iter::<(&SpotLight, &Transform, Option<&LightShadowMapId>)>()
{
let shadow_map_id = shadow_map_id.map(|m| m.clone());
let uniform =
LightUniform::from_spot_light_bundle(&spot_light, &transform, shadow_map_id);
lights.push(uniform);
}
for (dir_light, transform) in world.view_iter::<(&DirectionalLight, &Transform)>() {
let uniform = LightUniform::from_directional_bundle(&dir_light, &transform);
for (dir_light, transform, shadow_map_id) in
world.view_iter::<(&DirectionalLight, &Transform, Option<&LightShadowMapId>)>()
{
let shadow_map_id = shadow_map_id.map(|m| m.clone());
let uniform =
LightUniform::from_directional_bundle(&dir_light, &transform, shadow_map_id);
lights.push(uniform);
}
assert!(lights.len() < self.max_light_count as _); // ensure we dont overwrite the buffer
assert!(lights.len() < self.max_light_count as usize); // ensure we dont overwrite the buffer
// write the amount of lights to the buffer, and right after that the list of lights.
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(&[lights.len()]));
// the size of u32 is multiplied by 4 because of gpu alignment requirements
queue.write_buffer(&self.buffer, mem::size_of::<u32>() as u64 * 4, bytemuck::cast_slice(lights.as_slice()));
queue.write_buffer(
&self.buffer,
mem::size_of::<u32>() as u64 * 4,
bytemuck::cast_slice(lights.as_slice()),
);
}
}
@ -209,17 +243,22 @@ pub(crate) struct LightUniform {
pub color: glam::Vec3,
// no padding is needed here since range acts as the padding
// that would usually be needed for the vec3
pub range: f32,
pub intensity: f32,
pub smoothness: f32,
pub spot_cutoff_rad: f32,
pub spot_outer_cutoff_rad: f32,
pub light_shadow_uniform_index: [i32; 6],
_padding: [u32; 2],
}
impl LightUniform {
pub fn from_point_light_bundle(light: &PointLight, transform: &Transform) -> Self {
pub fn from_point_light_bundle(
light: &PointLight,
transform: &Transform,
map_id: Option<LightShadowMapId>,
) -> Self {
Self {
light_type: LightType::Point as u32,
enabled: light.enabled as u32,
@ -233,11 +272,27 @@ impl LightUniform {
spot_cutoff_rad: 0.0,
spot_outer_cutoff_rad: 0.0,
light_shadow_uniform_index: map_id
.map(|m| {
[
m.uniform_index(0) as i32,
m.uniform_index(1) as i32,
m.uniform_index(2) as i32,
m.uniform_index(3) as i32,
m.uniform_index(4) as i32,
m.uniform_index(5) as i32,
]
})
.unwrap_or([-1; 6]),
_padding: [0; 2],
}
}
pub fn from_directional_bundle(light: &DirectionalLight, transform: &Transform) -> Self {
pub fn from_directional_bundle(
light: &DirectionalLight,
transform: &Transform,
map_id: Option<LightShadowMapId>,
) -> Self {
Self {
light_type: LightType::Directional as u32,
enabled: light.enabled as u32,
@ -251,11 +306,28 @@ impl LightUniform {
spot_cutoff_rad: 0.0,
spot_outer_cutoff_rad: 0.0,
light_shadow_uniform_index: map_id
.map(|m| {
[
m.uniform_index(0) as i32,
m.uniform_index(1) as i32,
m.uniform_index(2) as i32,
m.uniform_index(3) as i32,
m.uniform_index(4) as i32,
m.uniform_index(5) as i32,
]
})
.unwrap_or([-1; 6]),
_padding: [0; 2],
}
}
// Create the SpotLightUniform from an ECS bundle
pub fn from_spot_light_bundle(light: &SpotLight, transform: &Transform) -> Self {
pub fn from_spot_light_bundle(
light: &SpotLight,
transform: &Transform,
map_id: Option<LightShadowMapId>,
) -> Self {
Self {
light_type: LightType::Spotlight as u32,
enabled: light.enabled as u32,
@ -269,7 +341,19 @@ impl LightUniform {
spot_cutoff_rad: light.cutoff.to_radians(),
spot_outer_cutoff_rad: light.outer_cutoff.to_radians(),
light_shadow_uniform_index: map_id
.map(|m| {
[
m.uniform_index(0) as i32,
m.uniform_index(1) as i32,
m.uniform_index(2) as i32,
m.uniform_index(3) as i32,
m.uniform_index(4) as i32,
m.uniform_index(5) as i32,
]
})
.unwrap_or([-1; 6]),
_padding: [0; 2],
}
}
}

View file

@ -17,8 +17,8 @@ pub(crate) struct LightIndicesGridBuffer {
}
pub(crate) struct LightCullCompute {
device: Rc<wgpu::Device>,
queue: Rc<wgpu::Queue>,
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
pipeline: ComputePipeline,
pub light_indices_grid: LightIndicesGridBuffer,
screen_size_buffer: BufferWrapper,
@ -153,7 +153,7 @@ impl LightCullCompute {
}
}
pub fn new(device: Rc<wgpu::Device>, queue: Rc<wgpu::Queue>, screen_size: PhysicalSize<u32>, lights_buffers: &LightUniformBuffers, camera_buffers: &BufferWrapper, depth_texture: &mut RenderTexture) -> Self {
pub fn new(device: Arc<wgpu::Device>, queue: Arc<wgpu::Queue>, screen_size: PhysicalSize<u32>, lights_buffers: &LightUniformBuffers, camera_buffers: &BufferWrapper, depth_texture: &mut RenderTexture) -> Self {
let screen_size_buffer = BufferWrapper::builder()
.buffer_usage(wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST)
.label_prefix("ScreenSize")

View file

@ -1,9 +1,10 @@
use std::rc::Rc;
use std::sync::Arc;
use lyra_resource::{ResHandle, Texture};
use super::texture::RenderTexture;
#[derive(Default)]
pub struct MaterialSpecular {
pub factor: f32,
pub color_factor: glam::Vec3,
@ -11,7 +12,7 @@ pub struct MaterialSpecular {
pub color_texture: Option<RenderTexture>,
}
fn texture_to_render(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: &Rc<wgpu::BindGroupLayout>, i: &Option<ResHandle<Texture>>) -> Option<RenderTexture> {
fn texture_to_render(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: &Arc<wgpu::BindGroupLayout>, i: &Option<ResHandle<Texture>>) -> Option<RenderTexture> {
if let Some(tex) = i {
RenderTexture::from_resource(device, queue, bg_layout.clone(), tex, None).ok()
} else {
@ -20,7 +21,7 @@ fn texture_to_render(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: &Rc<
}
impl MaterialSpecular {
pub fn from_resource(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Rc<wgpu::BindGroupLayout>, value: &lyra_resource::gltf::Specular) -> Self {
pub fn from_resource(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Arc<wgpu::BindGroupLayout>, value: &lyra_gltf::Specular) -> Self {
let tex = texture_to_render(device, queue, &bg_layout, &value.texture);
let color_tex = texture_to_render(device, queue, &bg_layout, &value.color_texture);
@ -45,7 +46,7 @@ pub struct Material {
}
impl Material {
pub fn from_resource(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Rc<wgpu::BindGroupLayout>, value: &lyra_resource::gltf::Material) -> Self {
pub fn from_resource(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Arc<wgpu::BindGroupLayout>, value: &lyra_gltf::Material) -> Self {
let diffuse_texture = texture_to_render(device, queue, &bg_layout, &value.base_color_texture);
let specular = value.specular.as_ref().map(|s| MaterialSpecular::from_resource(device, queue, bg_layout.clone(), s));
@ -57,7 +58,7 @@ impl Material {
//diffuse: glam::Vec3::new(value.base_color.x, value.base_color.y, value.base_color.z),
//diffuse: glam::Vec3::new(1.0, 0.5, 0.31),
//specular: glam::Vec3::new(0.5, 0.5, 0.5),
ambient: glam::Vec3::new(1.0, 1.0, 1.0),
ambient: glam::Vec3::new(1.0, 1.0, 1.0) * 0.5,
diffuse: glam::Vec3::new(1.0, 1.0, 1.0),
shininess: 32.0,

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