Compare commits

...

144 Commits

Author SHA1 Message Date
SeanOMik 2b44d7f354 render: implement wgsl-preprocessor, split shaders
CI / build (push) Failing after 7m56s Details
2024-09-14 20:08:18 -04:00
SeanOMik 60c139f9b2 ecs: create DynamicViewOne
CI / build (push) Failing after 11m20s Details
2024-09-02 20:34:02 -04:00
SeanOMik 256025849e Merge pull request 'Implement Shadows' (#24) from feat/shadow-maps into main
CI / build (push) Successful in 3m38s Details
Reviewed-on: #24
2024-08-09 23:10:28 -04:00
SeanOMik 8545e7e27d
render: rewrite PCF for spot lights to somehow fix PCSS directional lights
CI / build (pull_request) Successful in 9m48s Details
2024-08-09 22:01:57 -04:00
SeanOMik a85178eeea Revert "render: shadow maps and PCF for spot lights"
This reverts commit 8c1738334c.
2024-08-09 21:51:56 -04:00
SeanOMik 8c1738334c
render: shadow maps and PCF for spot lights 2024-07-24 20:10:32 -04:00
SeanOMik fefcf58765
render: make shadow depth bias configurable per light source 2024-07-21 21:53:02 -04:00
SeanOMik b0a6d30afc
render: fix directional light shadows 2024-07-21 21:09:29 -04:00
SeanOMik fef709d5f1
render: implement PCF for point lights, support per-light shadow settings 2024-07-21 12:02:35 -04:00
SeanOMik 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
SeanOMik c961568b96
render: update the shadow filting poisson disc when shadow settings are modified 2024-07-19 16:07:40 -04:00
SeanOMik 54b47c2178
ecs: implement change tracking for world resources 2024-07-19 16:07:03 -04:00
SeanOMik 4449172c2b
render: implement PCSS for directional lights 2024-07-18 23:43:08 -04:00
SeanOMik 4c6c6c4dd5
render: PCF with poisson disc on directional lights 2024-07-14 22:14:08 -04:00
SeanOMik 27bc88c5a7
render: pass shadow settings to gpu 2024-07-14 19:46:15 -04:00
SeanOMik ff06bd55f3
render: simple PCF 2024-07-14 19:06:38 -04:00
SeanOMik d02258224a
render: fix bug with texture atlas not packing textures in last column 2024-07-14 12:24:13 -04:00
SeanOMik b45c2f4fab
render: point light shadows in texture atlas, fix bug with unaligned GpuSlotBuffer 2024-07-13 00:56:09 -04:00
SeanOMik 40fa9c09da
render: fix shadow map atlas packing by writing my own skyline packer 2024-07-12 14:58:18 -04:00
SeanOMik 87aa440691
render: create a GpuSlotBuffer for stable indices in a gpu buffer 2024-07-11 20:00:46 -04:00
SeanOMik cc1c482c40
render: provide shadow texture atlas frame for each shadow casting light 2024-07-11 18:27:26 -04:00
SeanOMik a4ce4cb432
render: implement packed texture atlas for shadow maps 2024-07-10 20:16:21 -04:00
SeanOMik e2b554b4ef
render: implement simple texture atlas for the shadow maps 2024-07-05 17:29:38 -04:00
SeanOMik 6d57b40629
render: cull back faces, code cleanup to fix warnings 2024-07-04 23:28:21 -04:00
SeanOMik fd65f754cf
render: get simple directional shadow maps working 2024-07-04 13:43:36 -04:00
SeanOMik 6c6893149a
render: bind direction light projection matrix to meshes shader 2024-06-30 21:58:08 -04:00
SeanOMik 1c649b2eb6
render: bind the shadow map atlas to the meshes shaders 2024-06-30 21:42:08 -04:00
SeanOMik 7b2d2424a3
render: start moving to a shadow map atlas texture and expose the resources as slots 2024-06-30 20:56:41 -04:00
SeanOMik e8974bbd44
render: create a depth map for the directional light 2024-06-30 19:33:51 -04:00
SeanOMik 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
SeanOMik 7ff67a194b
create an example for testing shadow maps 2024-06-28 16:15:21 -04:00
SeanOMik c4aebdb25d
render: implement fxaa (#8)
CI / build (push) Successful in 3m33s Details
2024-06-28 15:26:14 -04:00
SeanOMik 5ebbec8cf9
ci: switch to ForgeJo actions
CI / build (push) Successful in 3m33s Details
2024-06-28 13:50:26 -04:00
SeanOMik 2eeca335e2
game: run clippy 2024-06-28 13:25:48 -04:00
SeanOMik 96dea5b1f9
render: code cleanup 2024-06-28 13:16:47 -04:00
SeanOMik 007b1047ef
render: implement view target chains for post processing steps 2024-06-27 23:48:24 -04:00
SeanOMik 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
SeanOMik 0e71c5734f
render: make it easier to get Frame from RenderTarget 2024-06-26 17:14:31 -04:00
SeanOMik 6c1bff5768
render: get sub render graphs working and create a simple test of them 2024-06-25 21:32:29 -04:00
SeanOMik 5f1a61ef52
render: create RenderTarget and Frame, making it easier to render to a non-surface texture 2024-06-23 20:25:57 -04:00
SeanOMik f755a4c53b
render: create a node for sub render graphs, create a graph init node 2024-06-22 17:00:32 -04:00
SeanOMik 9d3de88c50
render: cleanup 2024-06-21 22:10:35 -04:00
SeanOMik f440f306be
render: impl Node for RenderGraph in prep for sub-graphs 2024-06-21 22:06:58 -04:00
SeanOMik f17bf40c77
render: rename RenderGraph::add_pass to add_node 2024-06-21 21:58:40 -04:00
SeanOMik 5fc1a0f134
Add todo panic in code when window is resized 2024-06-16 20:05:04 -04:00
SeanOMik 33358662e2
fix docstest errors in render graph 2024-06-16 00:15:54 -04:00
SeanOMik 0edba69b02
fix warnings, remove some commented code from base shader
ci/woodpecker/push/debug Pipeline failed Details
2024-06-16 00:11:21 -04:00
SeanOMik 6182a4b9c8
render: simplify light buffer updating 2024-06-15 23:52:46 -04:00
SeanOMik 7576979797
render: fix lighting with more than one light 2024-06-15 23:52:10 -04:00
SeanOMik acb58a15ff Merge pull request 'Implement a Render Graph' (#16) from feature/render-graph into main
ci/woodpecker/push/debug Pipeline failed Details
Reviewed-on: #16
2024-06-15 18:54:46 -04:00
SeanOMik d5348ec172
render: a tiny bit of code cleanup
ci/woodpecker/pr/debug Pipeline failed Details
ci/woodpecker/pr/release Pipeline failed Details
2024-06-12 21:30:09 -04:00
SeanOMik 9ce79e6b29
resource: fix tests, render: remove warning
ci/woodpecker/pr/debug Pipeline failed Details
ci/woodpecker/pr/release Pipeline failed Details
2024-06-12 21:23:27 -04:00
SeanOMik 6d7932f6a5
render: remove IDs for everything, use only labels to identify things
ci/woodpecker/pr/debug Pipeline failed Details
ci/woodpecker/pr/release Pipeline failed Details
2024-06-11 20:59:55 -04:00
SeanOMik 28b9604189
render: rename RenderGraphPass to Node to better represent what it actually is in the RenderGraph
A node wont always render or compute, so it wouldn't actually be a pass. Calling it a node is a better representation of what it actually is
2024-06-06 19:37:25 -04:00
SeanOMik a0a2acfec0
render: add todo in code 2024-06-03 19:06:07 -04:00
SeanOMik ef68b2a4c5
render: create a RenderGraphLabel trait for graph labels instead of strings
ci/woodpecker/pr/debug Pipeline failed Details
ci/woodpecker/pr/release Pipeline failed Details
2024-06-02 21:35:59 -04:00
SeanOMik 41d77c5687
render: a tiny bit of code cleanup in mesh pass 2024-06-01 23:05:43 -04:00
SeanOMik bb21805278
render: add a debug_assert to ensure the developer doesn't reuse ids for slots 2024-06-01 22:55:50 -04:00
SeanOMik c846d52b0d
render: finally get meshes and entities rendering again with the render graph!
ci/woodpecker/pr/debug Pipeline failed Details
ci/woodpecker/pr/release Pipeline failed Details
2024-05-31 20:11:35 -04:00
SeanOMik 7f5a1cd953
render: a bit of code cleanup 2024-05-25 19:37:43 -04:00
SeanOMik 9a48075f07
render: change to manual creation of render graph exeuction path, rewrite light cull compute pass into the render graph 2024-05-25 19:27:36 -04:00
SeanOMik fc57777a45
render: move render targets to be graph slots, create present passes and base passes
ci/woodpecker/pr/debug Pipeline failed Details
ci/woodpecker/pr/release Pipeline failed Details
Since the render graph no longer has default slots, base passes must be created that supply things like render targets. This also makes it easier to render offscreen to some other surface that is not the window, or just some other texture
2024-05-19 12:56:03 -04:00
SeanOMik 8c3446389c
render: code cleanup
ci/woodpecker/pr/debug Pipeline failed Details
ci/woodpecker/pr/release Pipeline failed Details
2024-05-18 11:02:07 -04:00
SeanOMik 64e6e4a942
render: make it easier to share bind groups and bind group layouts between passes
ci/woodpecker/pr/debug Pipeline failed Details
ci/woodpecker/pr/release Pipeline failed Details
2024-05-17 17:43:46 -04:00
SeanOMik cee6e44d61
render: support creating bindgroups for passes and updating buffers every frame
ci/woodpecker/pr/debug Pipeline failed Details
ci/woodpecker/pr/release Pipeline failed Details
2024-05-14 18:46:35 -04:00
SeanOMik b94a8e3cd3
Make it possible to create a complete pipeline descriptor for a pass
ci/woodpecker/pr/debug Pipeline failed Details
ci/woodpecker/pr/release Pipeline failed Details
2024-05-11 09:19:58 -04:00
SeanOMik bccf6287c0
render: get first image from RenderGraph, just a simple hard coded triangle
ci/woodpecker/pr/debug Pipeline failed Details
ci/woodpecker/pr/release Pipeline failed Details
2024-05-08 18:27:10 -04:00
SeanOMik daa6fc3d4b
move profiles to root workspace Cargo.toml so they aren't ignored 2024-05-08 18:25:12 -04:00
SeanOMik a4e80d4fec
render: continue work of render graph, expanding RenderGraphPass and removing things from the renderer
ci/woodpecker/pr/debug Pipeline failed Details
ci/woodpecker/pr/release Pipeline failed Details
2024-05-04 10:02:50 -04:00
SeanOMik 4c2ed6ca80
render: create foundation of render graph, also add super simple topological sort for pass execution path 2024-04-28 17:51:35 -04:00
SeanOMik 669cc7590c
examples: remove some warnings
ci/woodpecker/push/debug Pipeline failed Details
2024-04-27 19:45:59 -04:00
SeanOMik d1f1e03cbb
scripting: improve macros to make it easier to create wrappers 2024-04-27 19:43:45 -04:00
SeanOMik 29c68abbbb
scripting: fix lua scripting (#13), create an example for it
ci/woodpecker/push/debug Pipeline failed Details
2024-04-27 00:52:47 -04:00
SeanOMik 1b08482ef7
resource: fix wait_for_load haning when using handles from 'request_raw' 2024-04-27 00:31:20 -04:00
SeanOMik 15807a3dc1
resource: impl Reflect for a bunch of types, add some utility methods to handles 2024-04-27 00:28:49 -04:00
SeanOMik f0d36e7b56
ecs: impl Clone for World and return entity in dynamic views 2024-04-27 00:21:26 -04:00
SeanOMik 6a11f7cbb7 Merge pull request 'Improve Performance in Scenes With Many Lights' (#14) from bugfix/many-lights-poor-performance into main
ci/woodpecker/push/debug Pipeline failed Details
Reviewed-on: #14
2024-04-24 19:55:15 -04:00
SeanOMik db501015d0
Create an example project to test transform interpolation 2024-04-24 00:30:30 -04:00
SeanOMik 53837d469b
ecs: fix BatchedSystem, implement ways for `Criteria`s to modify the world before and after execution 2024-04-24 00:28:01 -04:00
SeanOMik e2c6b557bb
render: improve performance of transform interpolation by using ecs components 2024-04-22 01:07:35 -04:00
SeanOMik 337ce18e8c
ecs: update existing components on entity in World::insert 2024-04-22 00:20:42 -04:00
SeanOMik 8eac563229
render: significantly improve performance of TransformBuffers
Before the changes, a release build of 'many-lights' was running at about 130fps, now its 430fps
2024-04-21 00:54:45 -04:00
SeanOMik 24e1c0281e
Make tracy profiling an optional feature, create 'many-lights' example 2024-04-20 00:08:25 -04:00
SeanOMik 246705b80b
game: some profiling improvements 2024-04-19 23:37:08 -04:00
SeanOMik 3c73e1d7e2
render: only run system_update_world_transforms for scenes that were modified 2024-04-18 22:38:15 -04:00
SeanOMik 25aa902e02
render: use WorldTransforms in the renderer
ci/woodpecker/push/debug Pipeline failed Details
2024-04-17 20:46:46 -04:00
SeanOMik 12c8ece418
ecs: create a DynamicViewState that can be used to create a dynamic view without dealing with lifetimes
ci/woodpecker/push/debug Pipeline failed Details
2024-04-13 02:10:25 -04:00
SeanOMik 60ec62c558
scene: improve docs and some code cleanup
ci/woodpecker/push/debug Pipeline failed Details
2024-04-10 23:55:48 -04:00
SeanOMik 2daf617ba3
scene: implement WorldTransform struct to simplify getting the world transform of scene nodes 2024-04-10 23:45:25 -04:00
SeanOMik 0668be06e2
ecs: documentation improvements for filter queries 2024-04-10 23:18:11 -04:00
SeanOMik 347427a841
ecs: remove compiler warning 2024-04-10 22:27:44 -04:00
SeanOMik 4162150c5f
ecs: fix issue with Entities query returning the incorrect entity ids 2024-04-10 22:27:23 -04:00
SeanOMik 4a0d003181
ecs: add not filter, improve the code for inserting components into entity, bundle cleanup and improvements 2024-04-10 22:26:49 -04:00
SeanOMik 01a74ab9a6
Rewrite nix-shell, use mold for the linker
ci/woodpecker/push/debug Pipeline failed Details
2024-04-04 23:58:59 -04:00
SeanOMik 3dfb2520ce
ecs: add some spans around the system executors
ci/woodpecker/push/debug Pipeline failed Details
2024-04-01 12:02:16 -04:00
SeanOMik f3b5106073
reflect: fix warnings
ci/woodpecker/push/debug Pipeline failed Details
2024-04-01 11:17:19 -04:00
SeanOMik 7ae38476fa Merge pull request 'Fix #6: Rendering Shared 3D Models' (#10) from bug/6-rendering-shared-models into main
ci/woodpecker/push/debug Pipeline failed Details
Reviewed-on: #10
2024-04-01 11:03:00 -04:00
SeanOMik 0a9e5ebcdb
render: improve fix for rendering shared 3d modules 2024-04-01 10:50:17 -04:00
SeanOMik dd61e8e66c
render: hack to get rendering shared 3d modules working 2024-03-31 23:02:18 -04:00
SeanOMik a3118f32e2
resource: implement retrieving loaded SceneGraph dependencies 2024-03-31 13:37:25 -04:00
SeanOMik aa8d94851c
game: rewrite EventQueue due to new ecs requirement of Send + Sync for resources, use new SceneGraph in renderer 2024-03-31 13:24:32 -04:00
SeanOMik a39d259bb4
Switch nix-shell to use oxalica overlay to get miri working, fix memory leak in archetypes 2024-03-31 10:56:04 -04:00
SeanOMik a17c035c05
resource: use a SceneGraph for loading gltf nodes, make resources Send + Sync 2024-03-31 00:32:31 -04:00
SeanOMik a2aac25249
ecs, reflect: implement Bundle for (), use `nobuild` instead of `compile_fail` for reflect rustdocs 2024-03-31 00:29:12 -04:00
SeanOMik e5018c8258
reflect: fix type registry from changes with ecs resources 2024-03-30 22:42:41 -04:00
SeanOMik e00d0d71d1
examples: move assets outside of testbed for other examples 2024-03-30 22:20:53 -04:00
SeanOMik 46cdcfdd3b
ecs: make resources Send + Sync, rewrite Commands, CommandsQueue so that they are Send + Sync 2024-03-30 22:20:52 -04:00
SeanOMik 61efc358ce
scene: make scenes own its own world, no references 2024-03-24 22:40:38 -04:00
SeanOMik 763d51ae36
move some stuff out of testbed into lyra-game 2024-03-22 22:55:22 -04:00
SeanOMik 0f11fe2e6d
render: fix spot light culling 2024-03-22 10:46:52 -04:00
SeanOMik e2844a11a6
render: create toggleable debug light cull view 2024-03-20 19:03:39 -04:00
SeanOMik f0b413d9ae
render: resize light grid with window, improve light buffer, add spot lights to the light cull compute
Spot lights are buggy. They get culled when they shouldn't be, maybe still an issue with the light grid :(
2024-03-20 11:41:40 -04:00
SeanOMik 65ff7c4f23
render: retrieve light properties from components 2024-03-19 22:40:15 -04:00
SeanOMik f63a7ae86a
include correct rust install in nix-shell 2024-03-19 21:40:08 -04:00
SeanOMik 834a864544
render: get forward+ rendering working with multiple light sources 2024-03-19 21:08:24 -04:00
SeanOMik 014abcf7e6
render: fix the tile frustum used for culling lights 2024-03-19 21:07:39 -04:00
SeanOMik 76ec9606ec
render: add some fields to the camera uniform 2024-03-17 16:07:24 -04:00
SeanOMik 5c1ce809ff
render: get some lights showing up with tiled forward rendering
For some reason there's weird square in the light source, and the dynamic light is only applied to the top left tile
2024-03-17 15:20:17 -04:00
SeanOMik c73c1a7f43
render: fix segfault in LightCullCompute 2024-03-16 22:58:38 -04:00
SeanOMik 4ce21d4db0
render: dont send the same material to the gpu multiple times, speeding up gpu texture loading 2024-03-16 19:12:32 -04:00
SeanOMik 1818a0b48b
position the camera in a good position in the scene 2024-03-16 18:50:22 -04:00
SeanOMik cfd5cabfbb
render: create light cull compute shader, bind buffers, etc. 2024-03-16 18:39:07 -04:00
SeanOMik 22c08ba66e
render: improve the render buffer wrapper, use it for the camera uniform 2024-03-14 23:08:21 -04:00
SeanOMik f345f065c1
reflect: create ReflectedMap 2024-03-10 00:17:09 -05:00
SeanOMik aa3a4a17d7
resource: implement waiting for resource dependencies to be loaded 2024-03-10 00:11:15 -05:00
SeanOMik 4a285e5866
Merge branch 'feature/async-resource-loading' into main 2024-03-09 00:48:42 -05:00
SeanOMik 1c29e6fa72
cleanup some simple compiler warnings 2024-03-09 00:48:23 -05:00
SeanOMik de64b06e46
ecs: fix warning 2024-03-09 00:48:22 -05:00
SeanOMik dead32dbab
resource: asyncronous loading of resources 2024-03-09 00:48:22 -05:00
SeanOMik 1d7d13eb7b Merge pull request 'Some gltf improvements' (#4) from feature/gltf-scene-fixes into main
Reviewed-on: #4
2024-03-09 00:46:42 -05:00
SeanOMik cd27c9602c Inline a few things 2024-03-09 00:46:42 -05:00
SeanOMik 5331cfc2c4 resource, render: load in texture sampler from gltf and use them in the renderer 2024-03-09 00:46:42 -05:00
SeanOMik b941fa2fe0 cleanup some simple compiler warnings 2024-03-09 00:46:42 -05:00
SeanOMik e36307eef7 render: fix the TransformBuffers that could only store a single Transform for an entity
This caused Scenes to be rendered poorly since all meshes would use the Transform of the last processed mesh
2024-03-09 00:46:42 -05:00
SeanOMik fba925512b render: process GltfScenes and Node local transforms 2024-03-09 00:46:42 -05:00
SeanOMik c1b5ca768f resource: Create Gltf object, expand GltfScene to collect all node transforms, other qol changes 2024-03-09 00:46:42 -05:00
SeanOMik c3de9e77db ecs: fix filters 2024-03-09 00:46:42 -05:00
SeanOMik 7db913d15b resource: load multiple gltf scenes 2024-03-09 00:46:42 -05:00
SeanOMik 556b603f83 resource: improve gltf loader to show scene hierarchy and node local transform 2024-03-09 00:46:42 -05:00
SeanOMik ad40621f7c scene: some cleanup 2024-03-09 00:46:42 -05:00
SeanOMik bcc035ab91 Some cleanup 2024-03-09 00:46:42 -05:00
SeanOMik 8aae479df3 Create a new crate! lyra-scene for representing a SceneGraph in an ECS world 2024-03-09 00:46:42 -05:00
SeanOMik b76832ec05 ecs: fix World::insert, finish a TODO related to it
The TODO was that if the archetype has a single entity, add a component column for the new component instead of moving the entity to a brand new archetype
2024-03-09 00:46:42 -05:00
SeanOMik ad82f61cf4 Merge pull request 'Implement relationships in the ECS' (#3) from feature/ecs-relations into main
Reviewed-on: #3
2024-03-03 16:22:39 -05:00
202 changed files with 16654 additions and 3974 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use_nix

View File

@ -0,0 +1,31 @@
name: CI
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: docker
container: git.seanomik.net/seanomik/rust-nightly:2023-11-21-bookworm
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Install system dependencies
run: |
apt update
apt install libudev-dev lua5.4 liblua5.4-dev -y
- name: Build
run: |
cargo build
- name: Test
run: |
cargo test --all

3
.gitmodules vendored
View File

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

29
.lapce/run.toml Normal file
View File

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

58
.vscode/launch.json vendored
View File

@ -22,6 +22,42 @@
"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",
"name": "Debug example simple_scene",
"cargo": {
"args": [
"build",
"--manifest-path", "${workspaceFolder}/examples/simple_scene/Cargo.toml"
//"--bin=testbed",
],
"filter": {
"name": "simple_scene",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}/examples/simple_scene"
},
{
"type": "lldb",
"request": "launch",
@ -83,6 +119,28 @@
},
"args": [],
"cwd": "${workspaceFolder}/lyra-ecs"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'lyra-scene'",
"cargo": {
"args": [
"test",
"--no-run",
"--lib",
"--package=lyra-scene",
//"command::tests::deferred_commands",
//"--",
//"--exact --nocapture"
],
"filter": {
"name": "lyra-scene",
"kind": "lib"
}
},
"args": [],
"cwd": "${workspaceFolder}/lyra-scene"
}
]
}

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

517
Cargo.lock generated
View File

@ -56,6 +56,33 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "aligned"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "377e4c0ba83e4431b10df45c1d4666f178ea9c552cac93e60c3a88bf32785923"
dependencies = [
"as-slice",
]
[[package]]
name = "aligned-array"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c92d086290f52938013f6242ac62bf7d401fab8ad36798a609faa65c3fd2c"
dependencies = [
"generic-array",
]
[[package]]
name = "allocator-api2"
version = "0.2.16"
@ -97,9 +124,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.79"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
[[package]]
name = "arrayref"
@ -113,6 +140,15 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "as-slice"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
dependencies = [
"stable_deref_trait",
]
[[package]]
name = "ash"
version = "0.37.3+1.3.251"
@ -330,12 +366,24 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "atomic_refcell"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "az"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973"
[[package]]
name = "backtrace"
version = "0.3.69"
@ -369,6 +417,12 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bind_match"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "171f0236f66c7be99f32060539c2bade94033ded356ecf4c9dc9b1e6198326cd"
[[package]]
name = "bit-set"
version = "0.5.3"
@ -708,12 +762,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.18"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c"
dependencies = [
"cfg-if",
]
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "crunchy"
@ -768,6 +819,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
name = "divrem"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69dde51e8fef5e12c1d65e0929b03d66e4c0c18282bc30ed2ca050ad6f44dd82"
[[package]]
name = "dlib"
version = "0.5.2"
@ -777,6 +834,12 @@ dependencies = [
"libloading 0.8.1",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "downcast-rs"
version = "1.2.0"
@ -789,6 +852,12 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "elapsed"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f4e5af126dafd0741c2ad62d47f68b28602550102e5f0dd45c8a97fc8b49c29"
[[package]]
name = "elua"
version = "0.1.0"
@ -890,6 +959,18 @@ dependencies = [
"zune-inflate",
]
[[package]]
name = "fast_poisson"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2472baa9796d2ee497bd61690e3093a26935390d8ce0dd0ddc2db9b47a65898f"
dependencies = [
"kiddo",
"rand 0.8.5",
"rand_distr",
"rand_xoshiro",
]
[[package]]
name = "fastrand"
version = "1.9.0"
@ -935,6 +1016,37 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "fixed"
version = "1.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fc715d38bea7b5bf487fcd79bcf8c209f0b58014f3018a7a19c2b855f472048"
dependencies = [
"az",
"bytemuck",
"half",
"num-traits",
"typenum",
]
[[package]]
name = "fixed-timestep-rotating-model"
version = "0.1.0"
dependencies = [
"anyhow",
"async-std",
"fps_counter",
"lyra-engine",
"rand 0.8.5",
"tracing",
]
[[package]]
name = "fixedbitset"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flate2"
version = "1.0.28"
@ -986,9 +1098,12 @@ dependencies = [
[[package]]
name = "fps_counter"
version = "2.0.0"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3aaba7ff514ee9d802b562927f80b1e94e93d8e74c31b134c9c3762dabf1a36b"
checksum = "ff23a4d90ba4b859f370ee3c12ca3b1ca80d8ee144b279e135f6852cdadd6dd6"
dependencies = [
"instant",
]
[[package]]
name = "fsevent-sys"
@ -1090,6 +1205,19 @@ dependencies = [
"byteorder",
]
[[package]]
name = "generator"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
dependencies = [
"cc",
"libc",
"log",
"rustversion",
"windows 0.48.0",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@ -1631,6 +1759,26 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "kiddo"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1c5ea778d68eacd5c33f29537ba0b7b6c2595e74ee013a69cedc20ab4d3177"
dependencies = [
"aligned",
"aligned-array",
"az",
"divrem",
"doc-comment",
"elapsed",
"fixed",
"log",
"min-max-heap",
"num-traits",
"rand 0.8.5",
"rayon",
]
[[package]]
name = "kqueue"
version = "1.0.8"
@ -1698,6 +1846,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "libm"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
name = "libredox"
version = "0.0.2"
@ -1750,16 +1904,43 @@ dependencies = [
"value-bag",
]
[[package]]
name = "loom"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e045d70ddfbc984eacfa964ded019534e8f6cbf36f6410aee0ed5cefa5a9175"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "lua-scripting"
version = "0.1.0"
dependencies = [
"anyhow",
"async-std",
"fps_counter",
"lyra-engine",
"rand 0.8.5",
"tracing",
]
[[package]]
name = "lyra-ecs"
version = "0.1.0"
dependencies = [
"anyhow",
"atomic_refcell",
"lyra-ecs-derive",
"lyra-math",
"paste",
"rand 0.8.5",
"thiserror",
"tracing",
]
[[package]]
@ -1786,29 +1967,48 @@ dependencies = [
"anyhow",
"async-std",
"async-trait",
"bind_match",
"bytemuck",
"cfg-if",
"fast_poisson",
"gilrs-core",
"glam",
"image",
"instant",
"itertools 0.11.0",
"lyra-ecs",
"lyra-game-derive",
"lyra-math",
"lyra-reflect",
"lyra-resource",
"lyra-scene",
"petgraph",
"quote",
"round_mult",
"rustc-hash",
"syn 2.0.51",
"thiserror",
"tracing",
"tracing-appender",
"tracing-log 0.1.4",
"tracing-subscriber",
"tracing-tracy",
"unique",
"uuid",
"wgpu",
"wgsl_preprocessor",
"winit",
]
[[package]]
name = "lyra-game-derive"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.51",
]
[[package]]
name = "lyra-math"
version = "0.1.0"
@ -1839,27 +2039,44 @@ name = "lyra-resource"
version = "0.0.1"
dependencies = [
"anyhow",
"async-std",
"base64 0.21.5",
"crossbeam",
"glam",
"gltf",
"image",
"infer",
"instant",
"lyra-ecs",
"lyra-math",
"lyra-reflect",
"lyra-scene",
"mime",
"notify",
"notify-debouncer-full",
"percent-encoding",
"rand 0.8.5",
"thiserror",
"tracing",
"uuid",
]
[[package]]
name = "lyra-scene"
version = "0.1.0"
dependencies = [
"anyhow",
"lyra-ecs",
"lyra-math",
"lyra-reflect",
]
[[package]]
name = "lyra-scripting"
version = "0.1.0"
dependencies = [
"anyhow",
"atomic_refcell",
"elua",
"itertools 0.12.0",
"lyra-ecs",
@ -1867,6 +2084,7 @@ dependencies = [
"lyra-reflect",
"lyra-resource",
"lyra-scripting-derive",
"paste",
"thiserror",
"tracing",
"tracing-subscriber",
@ -1876,6 +2094,7 @@ dependencies = [
name = "lyra-scripting-derive"
version = "0.1.0"
dependencies = [
"paste",
"proc-macro2",
"quote",
"syn 2.0.51",
@ -1899,6 +2118,27 @@ dependencies = [
"libc",
]
[[package]]
name = "many-lights"
version = "0.1.0"
dependencies = [
"anyhow",
"async-std",
"fps_counter",
"lyra-engine",
"rand 0.8.5",
"tracing",
]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]]
name = "memchr"
version = "2.7.1"
@ -1943,6 +2183,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "min-max-heap"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2687e6cf9c00f48e9284cf9fd15f2ef341d03cc7743abf9df4c5f07fdee50b18"
[[package]]
name = "miniz_oxide"
version = "0.7.1"
@ -2150,6 +2396,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@ -2396,6 +2643,61 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pest"
version = "2.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95"
dependencies = [
"memchr",
"thiserror",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.51",
]
[[package]]
name = "pest_meta"
version = "2.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f"
dependencies = [
"once_cell",
"pest",
"sha2",
]
[[package]]
name = "petgraph"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
dependencies = [
"fixedbitset",
"indexmap 2.1.0",
]
[[package]]
name = "pin-project-lite"
version = "0.2.13"
@ -2581,6 +2883,25 @@ dependencies = [
"getrandom",
]
[[package]]
name = "rand_distr"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
dependencies = [
"num-traits",
"rand 0.8.5",
]
[[package]]
name = "rand_xoshiro"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa"
dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "range-alloc"
version = "0.1.3"
@ -2640,6 +2961,50 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "regex"
version = "1.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.6",
"regex-syntax 0.8.3",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
name = "regex-automata"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.3",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
@ -2695,6 +3060,15 @@ dependencies = [
"winreg",
]
[[package]]
name = "round_mult"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74bc7d5286c4d36f09aa6ae93f76acf6aa068cd62bc02970a9deb24763655dee"
dependencies = [
"rustc_version",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
@ -2707,6 +3081,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.37.27"
@ -2743,6 +3126,12 @@ dependencies = [
"base64 0.21.5",
]
[[package]]
name = "rustversion"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47"
[[package]]
name = "ryu"
version = "1.0.16"
@ -2815,6 +3204,12 @@ dependencies = [
"libc",
]
[[package]]
name = "semver"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.194"
@ -2880,6 +3275,16 @@ dependencies = [
"digest",
]
[[package]]
name = "shadows"
version = "0.1.0"
dependencies = [
"anyhow",
"async-std",
"lyra-engine",
"tracing",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@ -2904,6 +3309,16 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simple_scene"
version = "0.1.0"
dependencies = [
"anyhow",
"async-std",
"lyra-engine",
"tracing",
]
[[package]]
name = "slab"
version = "0.4.9"
@ -2986,6 +3401,12 @@ dependencies = [
"num-traits",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "static_assertions"
version = "1.1.0"
@ -3091,26 +3512,24 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-std",
"fps_counter",
"lyra-engine",
"lyra-scripting",
"tracing",
]
[[package]]
name = "thiserror"
version = "1.0.56"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.56"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2",
"quote",
@ -3342,14 +3761,49 @@ version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log 0.2.0",
]
[[package]]
name = "tracing-tracy"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6024d04f84a69fd0d1dc1eee3a2b070bd246530a0582f9982ae487cb6c703614"
dependencies = [
"tracing-core",
"tracing-subscriber",
"tracy-client",
]
[[package]]
name = "tracy-client"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59fb931a64ff88984f86d3e9bcd1ae8843aa7fe44dd0f8097527bc172351741d"
dependencies = [
"loom",
"once_cell",
"tracy-client-sys",
]
[[package]]
name = "tracy-client-sys"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d104d610dfa9dd154535102cc9c6164ae1fa37842bc2d9e83f9ac82b0ae0882"
dependencies = [
"cc",
]
[[package]]
name = "try-lock"
version = "0.2.5"
@ -3368,6 +3822,12 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ucd-trie"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
[[package]]
name = "unicode-bidi"
version = "0.3.15"
@ -3401,6 +3861,12 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "unique"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d360722e1f3884f5b14d332185f02ff111f771f0c76a313268fe6af1409aba96"
[[package]]
name = "url"
version = "2.5.0"
@ -3744,6 +4210,18 @@ dependencies = [
"web-sys",
]
[[package]]
name = "wgsl_preprocessor"
version = "0.1.0"
dependencies = [
"pest",
"pest_derive",
"regex",
"thiserror",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "widestring"
version = "0.5.1"
@ -3790,6 +4268,15 @@ dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows"
version = "0.52.0"

View File

@ -5,17 +5,33 @@ edition = "2021"
[workspace]
members = [
"examples/testbed",
"lyra-resource",
"lyra-ecs",
"lyra-reflect",
"lyra-scripting",
"lyra-game", "lyra-math"]
"lyra-game",
"lyra-math",
"lyra-scene",
"examples/testbed",
"examples/many-lights",
"examples/fixed-timestep-rotating-model",
"examples/lua-scripting",
"examples/simple_scene",
"examples/shadows",
]
[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 }
#[profile.dev]
#opt-level = 1
[profile.release]
debug = true

View File

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

Before

Width:  |  Height:  |  Size: 457 KiB

After

Width:  |  Height:  |  Size: 457 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Binary file not shown.

2
examples/assets/sponza/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
Sponza*
Textures

View File

@ -0,0 +1 @@
To keep the size of this repository down, the Sponza scene is omitted from this repo. The files were downloaded from https://github.com/toji/sponza-optimized

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,142 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v4.1.63",
"version":"2.0"
},
"scene":0,
"scenes":[
{
"name":"Scene",
"nodes":[
0
]
}
],
"nodes":[
{
"mesh":0,
"name":"Cube",
"scale":[
10,
0.25,
10
]
}
],
"materials":[
{
"doubleSided":true,
"name":"Material.001",
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":0
},
"metallicFactor":0,
"roughnessFactor":0.5
}
}
],
"meshes":[
{
"name":"Cube.001",
"primitives":[
{
"attributes":{
"POSITION":0,
"NORMAL":1,
"TEXCOORD_0":2
},
"indices":3,
"material":0
}
]
}
],
"textures":[
{
"sampler":0,
"source":0
}
],
"images":[
{
"mimeType":"image/jpeg",
"name":"wood1",
"uri":"wood1.jpg"
}
],
"accessors":[
{
"bufferView":0,
"componentType":5126,
"count":24,
"max":[
1,
1,
1
],
"min":[
-1,
-1,
-1
],
"type":"VEC3"
},
{
"bufferView":1,
"componentType":5126,
"count":24,
"type":"VEC3"
},
{
"bufferView":2,
"componentType":5126,
"count":24,
"type":"VEC2"
},
{
"bufferView":3,
"componentType":5123,
"count":36,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":288,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":288,
"byteOffset":288,
"target":34962
},
{
"buffer":0,
"byteLength":192,
"byteOffset":576,
"target":34962
},
{
"buffer":0,
"byteLength":72,
"byteOffset":768,
"target":34963
}
],
"samplers":[
{
"magFilter":9729,
"minFilter":9987
}
],
"buffers":[
{
"byteLength":840,
"uri":"model.bin"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -0,0 +1,12 @@
[package]
name = "fixed-timestep-rotating-model"
version = "0.1.0"
edition = "2021"
[dependencies]
lyra-engine = { path = "../../", features = ["tracy"] }
anyhow = "1.0.75"
async-std = "1.12.0"
tracing = "0.1.37"
rand = "0.8.5"
fps_counter = "3.0.0"

View File

@ -0,0 +1,241 @@
use std::ptr::NonNull;
use lyra_engine::{
assets::{gltf::Gltf, ResourceManager},
ecs::{
query::{Res, ResMut, View},
system::{BatchedSystem, Criteria, CriteriaSchedule, IntoSystem},
World,
},
game::Game,
input::{
Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource,
InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput,
},
math::{self, Transform, Vec3},
render::light::directional::DirectionalLight,
scene::{
CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform,
ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN,
ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN,
},
DeltaTime,
};
use tracing::info;
#[async_std::main]
async fn main() {
let action_handler_plugin = |game: &mut Game| {
let action_handler = ActionHandler::builder()
.add_layout(LayoutId::from(0))
.add_action(ACTLBL_MOVE_FORWARD_BACKWARD, Action::new(ActionKind::Axis))
.add_action(ACTLBL_MOVE_LEFT_RIGHT, Action::new(ActionKind::Axis))
.add_action(ACTLBL_MOVE_UP_DOWN, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_LEFT_RIGHT, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_UP_DOWN, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_ROLL, Action::new(ActionKind::Axis))
.add_action("Debug", Action::new(ActionKind::Button))
.add_mapping(
ActionMapping::builder(LayoutId::from(0), ActionMappingId::from(0))
.bind(
ACTLBL_MOVE_FORWARD_BACKWARD,
&[
ActionSource::Keyboard(KeyCode::W).into_binding_modifier(1.0),
ActionSource::Keyboard(KeyCode::S).into_binding_modifier(-1.0),
],
)
.bind(
ACTLBL_MOVE_LEFT_RIGHT,
&[
ActionSource::Keyboard(KeyCode::A).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::D).into_binding_modifier(1.0),
],
)
.bind(
ACTLBL_MOVE_UP_DOWN,
&[
ActionSource::Keyboard(KeyCode::C).into_binding_modifier(1.0),
ActionSource::Keyboard(KeyCode::Z).into_binding_modifier(-1.0),
],
)
.bind(
ACTLBL_LOOK_LEFT_RIGHT,
&[
ActionSource::Mouse(MouseInput::Axis(MouseAxis::X)).into_binding(),
ActionSource::Keyboard(KeyCode::Left).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Right).into_binding_modifier(1.0),
],
)
.bind(
ACTLBL_LOOK_UP_DOWN,
&[
ActionSource::Mouse(MouseInput::Axis(MouseAxis::Y)).into_binding(),
ActionSource::Keyboard(KeyCode::Up).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Down).into_binding_modifier(1.0),
],
)
.bind(
ACTLBL_LOOK_ROLL,
&[
ActionSource::Keyboard(KeyCode::E).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Q).into_binding_modifier(1.0),
],
)
.bind(
"Debug",
&[ActionSource::Keyboard(KeyCode::B).into_binding()],
)
.finish(),
)
.finish();
let world = game.world_mut();
world.add_resource(action_handler);
game.with_plugin(InputActionPlugin);
};
Game::initialize()
.await
.with_plugin(lyra_engine::DefaultPlugins)
.with_plugin(setup_scene_plugin)
.with_plugin(action_handler_plugin)
//.with_plugin(camera_debug_plugin)
.with_plugin(FreeFlyCameraPlugin)
.run()
.await;
}
fn setup_scene_plugin(game: &mut Game) {
let world = game.world_mut();
let resman = world.get_resource_mut::<ResourceManager>();
let camera_gltf = resman
.request::<Gltf>("../assets/AntiqueCamera.glb")
.unwrap();
camera_gltf.wait_recurse_dependencies_load();
let camera_mesh = &camera_gltf.data_ref().unwrap().scenes[0];
drop(resman);
world.spawn((
camera_mesh.clone(),
WorldTransform::default(),
Transform::from_xyz(0.0, -5.0, 0.0),
));
{
let mut light_tran = Transform::from_xyz(1.5, 2.5, 0.0);
light_tran.scale = Vec3::new(0.5, 0.5, 0.5);
light_tran.rotate_x(math::Angle::Degrees(-45.0));
light_tran.rotate_y(math::Angle::Degrees(25.0));
world.spawn((
DirectionalLight {
enabled: true,
color: Vec3::ONE,
intensity: 0.15, //..Default::default()
},
light_tran,
));
}
let mut camera = CameraComponent::new_3d();
camera.transform.translation += math::Vec3::new(0.0, 0.0, 1.5);
world.spawn((camera, FreeFlyCamera::default()));
let fps_counter = |mut counter: ResMut<fps_counter::FPSCounter>,
delta: Res<DeltaTime>| -> anyhow::Result<()> {
let tick = counter.tick();
info!("FPS: {}, frame time: {}", tick, **delta);
Ok(())
};
world.add_resource(fps_counter::FPSCounter::new());
let rotate_system = |dt: Res<DeltaTime>, view: View<&mut Transform>| -> anyhow::Result<()> {
const SPEED: f32 = 4.0;
let dt = **dt;
for mut transform in view.iter() {
info!("rotation: {:?}", transform.rotation);
transform.rotate_y(math::Angle::Degrees(SPEED * dt));
}
Ok(())
};
let mut sys = BatchedSystem::new();
sys.with_criteria(FixedTimestep::new(60));
sys.with_system(rotate_system.into_system());
sys.with_system(fps_counter.into_system());
game.with_system("fixed_timestep", sys, &[]);
}
struct FixedTimestep {
max_tps: u32,
fixed_time: f32,
accumulator: f32,
old_dt: Option<DeltaTime>,
}
#[allow(dead_code)]
impl FixedTimestep {
pub fn new(max_tps: u32) -> Self {
Self {
max_tps,
fixed_time: Self::calc_fixed_time(max_tps),
accumulator: 0.0,
old_dt: None,
}
}
fn calc_fixed_time(max_tps: u32) -> f32 {
1.0 / max_tps as f32
}
fn set_tps(&mut self, tps: u32) {
self.max_tps = tps;
self.fixed_time = Self::calc_fixed_time(tps);
}
fn tps(&self) -> u32 {
self.max_tps
}
fn fixed_time(&self) -> f32 {
self.fixed_time
}
}
impl Criteria for FixedTimestep {
fn can_run(&mut self, mut world: NonNull<World>, check_count: u32) -> CriteriaSchedule {
let world = unsafe { world.as_mut() };
if check_count == 0 {
let delta_time = world.get_resource::<DeltaTime>();
self.accumulator += **delta_time;
}
if self.accumulator >= self.fixed_time {
self.accumulator -= self.fixed_time;
return CriteriaSchedule::YesAndLoop;
}
CriteriaSchedule::No
}
fn modify_world(&mut self, mut world: NonNull<World>) {
let world = unsafe { world.as_mut() };
self.old_dt = world.try_get_resource().map(|r| *r);
world.add_resource(DeltaTime::from(self.fixed_time));
}
fn undo_world_modifications(&mut self, mut world: NonNull<World>) {
let world = unsafe { world.as_mut() };
world.add_resource(
self.old_dt
.expect("DeltaTime resource was somehow never got from the world"),
);
}
}

View File

@ -0,0 +1,12 @@
[package]
name = "lua-scripting"
version = "0.1.0"
edition = "2021"
[dependencies]
lyra-engine = { path = "../../", features = ["tracy", "lua_scripting"] }
anyhow = "1.0.75"
async-std = "1.12.0"
tracing = "0.1.37"
rand = "0.8.5"
fps_counter = "3.0.0"

View File

@ -0,0 +1,59 @@
---Return the userdata's name from its metatable
---@param val userdata
---@return string
function udname(val)
return getmetatable(val).__name
end
function on_init()
local cube = world:request_res("../assets/cube-texture-embedded.gltf")
print("Loaded textured cube (" .. udname(cube) .. ")")
cube:wait_until_loaded()
local scenes = cube:scenes()
local cube_scene = scenes[1]
local pos = Transform.from_translation(Vec3.new(0, 0, -8.0))
local e = world:spawn(pos, cube_scene)
print("spawned entity " .. tostring(e))
end
--[[ function on_first()
print("Lua's first function was called")
end
function on_pre_update()
print("Lua's pre-update function was called")
end ]]
function on_update()
--[[ ---@type number
local dt = world:resource(DeltaTime)
local act = world:resource(ActionHandler)
---@type number
local move_objs = act:get_axis("ObjectsMoveUpDown")
world:view(function (t)
if move_objs ~= nil then
t:translate(0, move_objs * 0.35 * dt, 0)
return t
end
end, Transform) ]]
---@type number
local dt = world:resource(DeltaTime)
world:view(function (t)
t:translate(0, 0.15 * dt, 0)
return t
end, Transform)
end
--[[ function on_post_update()
print("Lua's post-update function was called")
end
function on_last()
print("Lua's last function was called")
end ]]

View File

@ -0,0 +1,151 @@
use lyra_engine::{
assets::{gltf::Gltf, ResourceManager},
game::Game,
input::{
Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource,
InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput,
},
lua::{LuaScript, LuaScriptingPlugin},
math::{self, Transform, Vec3},
render::light::directional::DirectionalLight,
scene::{
CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform,
ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN,
ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN,
},
Script, ScriptList,
};
#[async_std::main]
async fn main() {
let action_handler_plugin = |game: &mut Game| {
let action_handler = ActionHandler::builder()
.add_layout(LayoutId::from(0))
.add_action(ACTLBL_MOVE_FORWARD_BACKWARD, Action::new(ActionKind::Axis))
.add_action(ACTLBL_MOVE_LEFT_RIGHT, Action::new(ActionKind::Axis))
.add_action(ACTLBL_MOVE_UP_DOWN, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_LEFT_RIGHT, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_UP_DOWN, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_ROLL, Action::new(ActionKind::Axis))
.add_action("Debug", Action::new(ActionKind::Button))
.add_mapping(
ActionMapping::builder(LayoutId::from(0), ActionMappingId::from(0))
.bind(
ACTLBL_MOVE_FORWARD_BACKWARD,
&[
ActionSource::Keyboard(KeyCode::W).into_binding_modifier(1.0),
ActionSource::Keyboard(KeyCode::S).into_binding_modifier(-1.0),
],
)
.bind(
ACTLBL_MOVE_LEFT_RIGHT,
&[
ActionSource::Keyboard(KeyCode::A).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::D).into_binding_modifier(1.0),
],
)
.bind(
ACTLBL_MOVE_UP_DOWN,
&[
ActionSource::Keyboard(KeyCode::C).into_binding_modifier(1.0),
ActionSource::Keyboard(KeyCode::Z).into_binding_modifier(-1.0),
],
)
.bind(
ACTLBL_LOOK_LEFT_RIGHT,
&[
ActionSource::Mouse(MouseInput::Axis(MouseAxis::X)).into_binding(),
ActionSource::Keyboard(KeyCode::Left).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Right).into_binding_modifier(1.0),
],
)
.bind(
ACTLBL_LOOK_UP_DOWN,
&[
ActionSource::Mouse(MouseInput::Axis(MouseAxis::Y)).into_binding(),
ActionSource::Keyboard(KeyCode::Up).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Down).into_binding_modifier(1.0),
],
)
.bind(
ACTLBL_LOOK_ROLL,
&[
ActionSource::Keyboard(KeyCode::E).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Q).into_binding_modifier(1.0),
],
)
.bind(
"Debug",
&[ActionSource::Keyboard(KeyCode::B).into_binding()],
)
.finish(),
)
.finish();
let world = game.world_mut();
world.add_resource(action_handler);
game.with_plugin(InputActionPlugin);
};
Game::initialize()
.await
.with_plugin(lyra_engine::DefaultPlugins)
.with_plugin(setup_scene_plugin)
.with_plugin(action_handler_plugin)
.with_plugin(setup_script_plugin)
//.with_plugin(camera_debug_plugin)
.with_plugin(FreeFlyCameraPlugin)
.run()
.await;
}
fn setup_scene_plugin(game: &mut Game) {
let world = game.world_mut();
let resman = world.get_resource_mut::<ResourceManager>();
let camera_gltf = resman
.request::<Gltf>("../assets/AntiqueCamera.glb")
.unwrap();
camera_gltf.wait_recurse_dependencies_load();
let camera_mesh = &camera_gltf.data_ref().unwrap().scenes[0];
drop(resman);
world.spawn((
camera_mesh.clone(),
WorldTransform::default(),
Transform::from_xyz(0.0, -5.0, -2.0),
));
{
let mut light_tran = Transform::from_xyz(1.5, 2.5, 0.0);
light_tran.scale = Vec3::new(0.5, 0.5, 0.5);
light_tran.rotate_x(math::Angle::Degrees(-45.0));
light_tran.rotate_y(math::Angle::Degrees(25.0));
world.spawn((
DirectionalLight {
enabled: true,
color: Vec3::ONE,
intensity: 0.15, //..Default::default()
},
light_tran,
));
}
let mut camera = CameraComponent::new_3d();
camera.transform.translation += math::Vec3::new(0.0, 0.0, 5.5);
world.spawn((camera, FreeFlyCamera::default()));
}
fn setup_script_plugin(game: &mut Game) {
game.with_plugin(LuaScriptingPlugin);
let world = game.world_mut();
let res_man = world.get_resource_mut::<ResourceManager>();
let script = res_man.request::<LuaScript>("scripts/test.lua").unwrap();
res_man.watch("scripts/test.lua", false).unwrap();
drop(res_man);
let script = Script::new("test.lua", script);
let scripts = ScriptList::new(vec![script]);
world.spawn((scripts,));
}

View File

@ -0,0 +1,12 @@
[package]
name = "many-lights"
version = "0.1.0"
edition = "2021"
[dependencies]
lyra-engine = { path = "../../", features = ["tracy"] }
anyhow = "1.0.75"
async-std = "1.12.0"
tracing = "0.1.37"
rand = "0.8.5"
fps_counter = "3.0.0"

View File

@ -0,0 +1,189 @@
use lyra_engine::{assets::{gltf::Gltf, ResourceManager}, ecs::query::{Res, ResMut, View}, game::Game, input::{Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput}, math::{self, Quat, Transform, Vec3}, render::light::{directional::DirectionalLight, PointLight}, scene::{CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN}, DeltaTime};
use rand::Rng;
use tracing::info;
const MAX_POINT_LIGHT_RANGE: f32 = 1.0;
const MIN_POINT_LIGHT_RANGE: f32 = 0.5;
const POINT_LIGHT_MAX_INTENSITY: f32 = 1.0;
const POINT_LIGHT_MIN_INTENSITY: f32 = 0.3;
const POINT_LIGHT_CUBE_SCALE: f32 = 0.2;
const POINT_LIGHT_NUM: u32 = 500;
const POINT_LIGHT_MAX_X: f32 = 9.0;
const POINT_LIGHT_MIN_X: f32 = -9.0;
const POINT_LIGHT_MAX_Y: f32 = 3.0;
const POINT_LIGHT_MIN_Y: f32 = 0.5;
const POINT_LIGHT_MAX_Z: f32 = 4.0;
const POINT_LIGHT_MIN_Z: f32 = -5.0;
#[async_std::main]
async fn main() {
let action_handler_plugin = |game: &mut Game| {
let action_handler = ActionHandler::builder()
.add_layout(LayoutId::from(0))
.add_action(ACTLBL_MOVE_FORWARD_BACKWARD, Action::new(ActionKind::Axis))
.add_action(ACTLBL_MOVE_LEFT_RIGHT, Action::new(ActionKind::Axis))
.add_action(ACTLBL_MOVE_UP_DOWN, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_LEFT_RIGHT, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_UP_DOWN, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_ROLL, Action::new(ActionKind::Axis))
.add_action("Debug", Action::new(ActionKind::Button))
.add_mapping(ActionMapping::builder(LayoutId::from(0), ActionMappingId::from(0))
.bind(ACTLBL_MOVE_FORWARD_BACKWARD, &[
ActionSource::Keyboard(KeyCode::W).into_binding_modifier(1.0),
ActionSource::Keyboard(KeyCode::S).into_binding_modifier(-1.0)
])
.bind(ACTLBL_MOVE_LEFT_RIGHT, &[
ActionSource::Keyboard(KeyCode::A).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::D).into_binding_modifier(1.0)
])
.bind(ACTLBL_MOVE_UP_DOWN, &[
ActionSource::Keyboard(KeyCode::C).into_binding_modifier(1.0),
ActionSource::Keyboard(KeyCode::Z).into_binding_modifier(-1.0)
])
.bind(ACTLBL_LOOK_LEFT_RIGHT, &[
ActionSource::Mouse(MouseInput::Axis(MouseAxis::X)).into_binding(),
ActionSource::Keyboard(KeyCode::Left).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Right).into_binding_modifier(1.0),
])
.bind(ACTLBL_LOOK_UP_DOWN, &[
ActionSource::Mouse(MouseInput::Axis(MouseAxis::Y)).into_binding(),
ActionSource::Keyboard(KeyCode::Up).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Down).into_binding_modifier(1.0),
])
.bind(ACTLBL_LOOK_ROLL, &[
ActionSource::Keyboard(KeyCode::E).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Q).into_binding_modifier(1.0),
])
.bind("Debug", &[
ActionSource::Keyboard(KeyCode::B).into_binding(),
])
.finish()
).finish();
let world = game.world_mut();
world.add_resource(action_handler);
game.with_plugin(InputActionPlugin);
};
Game::initialize().await
.with_plugin(lyra_engine::DefaultPlugins)
.with_plugin(setup_scene_plugin)
.with_plugin(action_handler_plugin)
.with_plugin(camera_debug_plugin)
.with_plugin(FreeFlyCameraPlugin)
.run().await;
}
fn setup_scene_plugin(game: &mut Game) {
let fps_counter = |mut counter: ResMut<fps_counter::FPSCounter>, delta: Res<DeltaTime>| -> anyhow::Result<()> {
let tick = counter.tick();
info!("FPS: {}, frame time: {}", tick, **delta);
Ok(())
};
game.with_system("fps_counter", fps_counter, &[]);
let world = game.world_mut();
world.add_resource(fps_counter::FPSCounter::new());
let resman = world.get_resource_mut::<ResourceManager>();
let cube_gltf = resman.request::<Gltf>("../assets/texture-sep/texture-sep.gltf").unwrap();
cube_gltf.wait_recurse_dependencies_load();
let cube_mesh = &cube_gltf.data_ref()
.unwrap().meshes[0];
let sponza_model = resman.request::<Gltf>("../assets/sponza/Sponza.gltf").unwrap();
drop(resman);
sponza_model.wait_recurse_dependencies_load();
let sponza_scene = &sponza_model.data_ref()
.unwrap().scenes[0];
world.spawn((
sponza_scene.clone(),
Transform::from_xyz(0.0, 0.0, 0.0),
));
{
let mut light_tran = Transform::from_xyz(1.5, 2.5, 0.0);
light_tran.scale = Vec3::new(0.5, 0.5, 0.5);
light_tran.rotate_x(math::Angle::Degrees(-45.0));
light_tran.rotate_y(math::Angle::Degrees(25.0));
world.spawn((
DirectionalLight {
enabled: true,
color: Vec3::ONE,
intensity: 0.15
//..Default::default()
},
light_tran,
));
}
let x_range = POINT_LIGHT_MIN_X..POINT_LIGHT_MAX_X;
let y_range = POINT_LIGHT_MIN_Y..POINT_LIGHT_MAX_Y;
let z_range = POINT_LIGHT_MIN_Z..POINT_LIGHT_MAX_Z;
let mut rand = rand::thread_rng();
let mut rand_vec3 = || -> Vec3 {
let x = rand.gen_range(x_range.clone());
let y = rand.gen_range(y_range.clone());
let z = rand.gen_range(z_range.clone());
Vec3::new(x, y, z)
};
let mut rand = rand::thread_rng();
for _ in 0..POINT_LIGHT_NUM {
let range = rand.gen_range(MIN_POINT_LIGHT_RANGE..MAX_POINT_LIGHT_RANGE);
let intensity = rand.gen_range(POINT_LIGHT_MIN_INTENSITY..POINT_LIGHT_MAX_INTENSITY);
let color = rand_vec3().normalize();
world.spawn((
PointLight {
enabled: true,
color,
intensity,
range,
..Default::default()
},
Transform::new(
rand_vec3(),
Quat::IDENTITY,
Vec3::new(POINT_LIGHT_CUBE_SCALE, POINT_LIGHT_CUBE_SCALE, POINT_LIGHT_CUBE_SCALE),
),
cube_mesh.clone(),
));
}
let mut camera = CameraComponent::new_3d();
// these values were taken by manually positioning the camera in the scene.
camera.transform = Transform::new(
Vec3::new(-10.0, 0.94, -0.28),
Quat::from_xyzw(0.03375484, -0.7116095, 0.0342693, 0.70092666),
Vec3::ONE
);
world.spawn(( camera, FreeFlyCamera::default() ));
}
fn camera_debug_plugin(game: &mut Game) {
let sys = |handler: Res<ActionHandler>, view: View<&mut CameraComponent>| -> anyhow::Result<()> {
if handler.was_action_just_pressed("Debug") {
for mut cam in view.into_iter() {
cam.debug = !cam.debug;
}
}
Ok(())
};
game.with_system("camera_debug_trigger", sys, &[]);
}

View File

@ -0,0 +1,10 @@
[package]
name = "shadows"
version = "0.1.0"
edition = "2021"
[dependencies]
lyra-engine = { path = "../../", features = ["tracy"] }
anyhow = "1.0.75"
async-std = "1.12.0"
tracing = "0.1.37"

View File

@ -0,0 +1,59 @@
---Return the userdata's name from its metatable
---@param val userdata
---@return string
function udname(val)
return getmetatable(val).__name
end
function on_init()
local cube = world:request_res("../assets/cube-texture-embedded.gltf")
print("Loaded textured cube (" .. udname(cube) .. ")")
cube:wait_until_loaded()
local scenes = cube:scenes()
local cube_scene = scenes[1]
local pos = Transform.from_translation(Vec3.new(0, 0, -8.0))
local e = world:spawn(pos, cube_scene)
print("spawned entity " .. tostring(e))
end
--[[ function on_first()
print("Lua's first function was called")
end
function on_pre_update()
print("Lua's pre-update function was called")
end ]]
function on_update()
--[[ ---@type number
local dt = world:resource(DeltaTime)
local act = world:resource(ActionHandler)
---@type number
local move_objs = act:get_axis("ObjectsMoveUpDown")
world:view(function (t)
if move_objs ~= nil then
t:translate(0, move_objs * 0.35 * dt, 0)
return t
end
end, Transform) ]]
---@type number
local dt = world:resource(DeltaTime)
world:view(function (t)
t:translate(0, 0.15 * dt, 0)
return t
end, Transform)
end
--[[ function on_post_update()
print("Lua's post-update function was called")
end
function on_last()
print("Lua's last function was called")
end ]]

View File

@ -0,0 +1,249 @@
use lyra_engine::{
assets::{gltf::Gltf, ResourceManager},
game::Game,
input::{
Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource,
InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput,
},
math::{self, Quat, Transform, Vec3},
render::{
graph::{ShadowCasterSettings, ShadowFilteringMode},
light::{directional::DirectionalLight, PointLight, SpotLight},
},
scene::{
CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform,
ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN,
ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN,
},
};
#[async_std::main]
async fn main() {
let action_handler_plugin = |game: &mut Game| {
let action_handler = ActionHandler::builder()
.add_layout(LayoutId::from(0))
.add_action(ACTLBL_MOVE_FORWARD_BACKWARD, Action::new(ActionKind::Axis))
.add_action(ACTLBL_MOVE_LEFT_RIGHT, Action::new(ActionKind::Axis))
.add_action(ACTLBL_MOVE_UP_DOWN, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_LEFT_RIGHT, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_UP_DOWN, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_ROLL, Action::new(ActionKind::Axis))
.add_action("Debug", Action::new(ActionKind::Button))
.add_mapping(
ActionMapping::builder(LayoutId::from(0), ActionMappingId::from(0))
.bind(
ACTLBL_MOVE_FORWARD_BACKWARD,
&[
ActionSource::Keyboard(KeyCode::W).into_binding_modifier(1.0),
ActionSource::Keyboard(KeyCode::S).into_binding_modifier(-1.0),
],
)
.bind(
ACTLBL_MOVE_LEFT_RIGHT,
&[
ActionSource::Keyboard(KeyCode::A).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::D).into_binding_modifier(1.0),
],
)
.bind(
ACTLBL_MOVE_UP_DOWN,
&[
ActionSource::Keyboard(KeyCode::C).into_binding_modifier(1.0),
ActionSource::Keyboard(KeyCode::Z).into_binding_modifier(-1.0),
],
)
.bind(
ACTLBL_LOOK_LEFT_RIGHT,
&[
ActionSource::Mouse(MouseInput::Axis(MouseAxis::X)).into_binding(),
ActionSource::Keyboard(KeyCode::Left).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Right).into_binding_modifier(1.0),
],
)
.bind(
ACTLBL_LOOK_UP_DOWN,
&[
ActionSource::Mouse(MouseInput::Axis(MouseAxis::Y)).into_binding(),
ActionSource::Keyboard(KeyCode::Up).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Down).into_binding_modifier(1.0),
],
)
.bind(
ACTLBL_LOOK_ROLL,
&[
ActionSource::Keyboard(KeyCode::E).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Q).into_binding_modifier(1.0),
],
)
.bind(
"Debug",
&[ActionSource::Keyboard(KeyCode::B).into_binding()],
)
.finish(),
)
.finish();
let world = game.world_mut();
world.add_resource(action_handler);
game.with_plugin(InputActionPlugin);
};
Game::initialize()
.await
.with_plugin(lyra_engine::DefaultPlugins)
.with_plugin(setup_scene_plugin)
.with_plugin(action_handler_plugin)
//.with_plugin(camera_debug_plugin)
.with_plugin(FreeFlyCameraPlugin)
.run()
.await;
}
fn setup_scene_plugin(game: &mut Game) {
let world = game.world_mut();
let resman = world.get_resource_mut::<ResourceManager>();
/* let camera_gltf = resman
.request::<Gltf>("../assets/AntiqueCamera.glb")
.unwrap();
camera_gltf.wait_recurse_dependencies_load();
let camera_mesh = &camera_gltf.data_ref().unwrap().scenes[0];
drop(resman);
world.spawn((
camera_mesh.clone(),
WorldTransform::default(),
Transform::from_xyz(0.0, -5.0, -2.0),
)); */
let cube_gltf = resman
.request::<Gltf>("../assets/cube-texture-embedded.gltf")
.unwrap();
cube_gltf.wait_recurse_dependencies_load();
let cube_mesh = &cube_gltf.data_ref().unwrap().scenes[0];
let palm_tree_platform_gltf = resman
.request::<Gltf>("../assets/shadows-platform-palmtree.glb")
.unwrap();
palm_tree_platform_gltf.wait_recurse_dependencies_load();
let palm_tree_platform_mesh = &palm_tree_platform_gltf.data_ref().unwrap().scenes[0];
drop(resman);
// cube in the air
/* world.spawn((
cube_mesh.clone(),
WorldTransform::default(),
Transform::from_xyz(0.0, -2.0, -5.0),
));
// cube really high in the air
world.spawn((
cube_mesh.clone(),
WorldTransform::default(),
Transform::from_xyz(-6.0, 0.0, -5.0),
));
// cube on the right, on the ground
world.spawn((
cube_mesh.clone(),
WorldTransform::default(),
Transform::from_xyz(3.0, -3.75, -5.0),
));
world.spawn((
platform_mesh.clone(),
WorldTransform::default(),
//Transform::from_xyz(0.0, -5.0, -5.0),
Transform::new(math::vec3(0.0, -5.0, -5.0), math::Quat::IDENTITY, math::vec3(5.0, 1.0, 5.0)),
)); */
world.spawn((
palm_tree_platform_mesh.clone(),
WorldTransform::default(),
Transform::from_xyz(5.0, -15.0, 0.0),
//Transform::new(math::vec3(0.0, -5.0, -5.0), math::Quat::IDENTITY, math::vec3(5.0, 1.0, 5.0)),
));
//shadows-platform-palmtree.glb
{
let mut light_tran = Transform::from_xyz(0.0, 0.0, 0.0);
light_tran.scale = Vec3::new(0.5, 0.5, 0.5);
light_tran.rotate_x(math::Angle::Degrees(-45.0));
light_tran.rotate_y(math::Angle::Degrees(-35.0));
world.spawn((
//cube_mesh.clone(),
DirectionalLight {
enabled: true,
color: Vec3::new(1.0, 0.95, 0.9),
intensity: 0.9,
},
ShadowCasterSettings {
filtering_mode: ShadowFilteringMode::Pcss,
pcf_samples_num: 64,
pcss_blocker_search_samples: 36,
constant_depth_bias_scale: 5.0,
..Default::default()
},
light_tran,
));
/* world.spawn((
cube_mesh.clone(),
PointLight {
enabled: true,
color: Vec3::new(0.133, 0.098, 0.91),
intensity: 2.0,
range: 10.0,
..Default::default()
},
ShadowCasterSettings {
filtering_mode: ShadowFilteringMode::Pcf,
..Default::default()
},
Transform::new(
Vec3::new(4.0 - 1.43, -13.0, 1.53),
Quat::IDENTITY,
Vec3::new(0.5, 0.5, 0.5),
),
)); */
let t = Transform::new(
Vec3::new(4.0 - 1.43, -13.0, 0.0),
//Vec3::new(-5.0, 1.0, -0.28),
//Vec3::new(-10.0, 0.94, -0.28),
Quat::from_euler(math::EulerRot::XYZ, 0.0, math::Angle::Degrees(-45.0).to_radians(), 0.0),
Vec3::new(0.15, 0.15, 0.15),
);
world.spawn((
SpotLight {
enabled: true,
color: Vec3::new(1.0, 0.0, 0.0),
intensity: 3.0,
range: 4.5,
//cutoff: math::Angle::Degrees(45.0),
..Default::default()
},
/* ShadowCasterSettings {
filtering_mode: ShadowFilteringMode::Pcf,
..Default::default()
}, */
WorldTransform::from(t),
t,
//cube_mesh.clone(),
));
}
let mut camera = CameraComponent::new_3d();
camera.transform.translation = math::Vec3::new(-1.0, -10.0, -1.5);
camera.transform.rotate_x(math::Angle::Degrees(-27.0));
camera.transform.rotate_y(math::Angle::Degrees(-90.0));
world.spawn((camera, FreeFlyCamera::default()));
}

View File

@ -0,0 +1,10 @@
[package]
name = "simple_scene"
version = "0.1.0"
edition = "2021"
[dependencies]
lyra-engine = { path = "../../", features = ["tracy"] }
anyhow = "1.0.75"
async-std = "1.12.0"
tracing = "0.1.37"

View File

@ -0,0 +1,59 @@
---Return the userdata's name from its metatable
---@param val userdata
---@return string
function udname(val)
return getmetatable(val).__name
end
function on_init()
local cube = world:request_res("../assets/cube-texture-embedded.gltf")
print("Loaded textured cube (" .. udname(cube) .. ")")
cube:wait_until_loaded()
local scenes = cube:scenes()
local cube_scene = scenes[1]
local pos = Transform.from_translation(Vec3.new(0, 0, -8.0))
local e = world:spawn(pos, cube_scene)
print("spawned entity " .. tostring(e))
end
--[[ function on_first()
print("Lua's first function was called")
end
function on_pre_update()
print("Lua's pre-update function was called")
end ]]
function on_update()
--[[ ---@type number
local dt = world:resource(DeltaTime)
local act = world:resource(ActionHandler)
---@type number
local move_objs = act:get_axis("ObjectsMoveUpDown")
world:view(function (t)
if move_objs ~= nil then
t:translate(0, move_objs * 0.35 * dt, 0)
return t
end
end, Transform) ]]
---@type number
local dt = world:resource(DeltaTime)
world:view(function (t)
t:translate(0, 0.15 * dt, 0)
return t
end, Transform)
end
--[[ function on_post_update()
print("Lua's post-update function was called")
end
function on_last()
print("Lua's last function was called")
end ]]

View File

@ -0,0 +1,149 @@
use lyra_engine::{
assets::{gltf::Gltf, ResourceManager},
game::Game,
input::{
Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource,
InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput,
},
math::{self, Transform, Vec3},
render::light::directional::DirectionalLight,
scene::{
CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform,
ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN,
ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN,
},
};
#[async_std::main]
async fn main() {
let action_handler_plugin = |game: &mut Game| {
let action_handler = ActionHandler::builder()
.add_layout(LayoutId::from(0))
.add_action(ACTLBL_MOVE_FORWARD_BACKWARD, Action::new(ActionKind::Axis))
.add_action(ACTLBL_MOVE_LEFT_RIGHT, Action::new(ActionKind::Axis))
.add_action(ACTLBL_MOVE_UP_DOWN, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_LEFT_RIGHT, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_UP_DOWN, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_ROLL, Action::new(ActionKind::Axis))
.add_action("Debug", Action::new(ActionKind::Button))
.add_mapping(
ActionMapping::builder(LayoutId::from(0), ActionMappingId::from(0))
.bind(
ACTLBL_MOVE_FORWARD_BACKWARD,
&[
ActionSource::Keyboard(KeyCode::W).into_binding_modifier(1.0),
ActionSource::Keyboard(KeyCode::S).into_binding_modifier(-1.0),
],
)
.bind(
ACTLBL_MOVE_LEFT_RIGHT,
&[
ActionSource::Keyboard(KeyCode::A).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::D).into_binding_modifier(1.0),
],
)
.bind(
ACTLBL_MOVE_UP_DOWN,
&[
ActionSource::Keyboard(KeyCode::C).into_binding_modifier(1.0),
ActionSource::Keyboard(KeyCode::Z).into_binding_modifier(-1.0),
],
)
.bind(
ACTLBL_LOOK_LEFT_RIGHT,
&[
ActionSource::Mouse(MouseInput::Axis(MouseAxis::X)).into_binding(),
ActionSource::Keyboard(KeyCode::Left).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Right).into_binding_modifier(1.0),
],
)
.bind(
ACTLBL_LOOK_UP_DOWN,
&[
ActionSource::Mouse(MouseInput::Axis(MouseAxis::Y)).into_binding(),
ActionSource::Keyboard(KeyCode::Up).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Down).into_binding_modifier(1.0),
],
)
.bind(
ACTLBL_LOOK_ROLL,
&[
ActionSource::Keyboard(KeyCode::E).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Q).into_binding_modifier(1.0),
],
)
.bind(
"Debug",
&[ActionSource::Keyboard(KeyCode::B).into_binding()],
)
.finish(),
)
.finish();
let world = game.world_mut();
world.add_resource(action_handler);
game.with_plugin(InputActionPlugin);
};
Game::initialize()
.await
.with_plugin(lyra_engine::DefaultPlugins)
.with_plugin(setup_scene_plugin)
.with_plugin(action_handler_plugin)
//.with_plugin(camera_debug_plugin)
.with_plugin(FreeFlyCameraPlugin)
.run()
.await;
}
fn setup_scene_plugin(game: &mut Game) {
let world = game.world_mut();
let resman = world.get_resource_mut::<ResourceManager>();
/* let camera_gltf = resman
.request::<Gltf>("../assets/AntiqueCamera.glb")
.unwrap();
camera_gltf.wait_recurse_dependencies_load();
let camera_mesh = &camera_gltf.data_ref().unwrap().scenes[0];
drop(resman);
world.spawn((
camera_mesh.clone(),
WorldTransform::default(),
Transform::from_xyz(0.0, -5.0, -2.0),
)); */
let cube_gltf = resman
.request::<Gltf>("../assets/cube-texture-embedded.gltf")
.unwrap();
cube_gltf.wait_recurse_dependencies_load();
let cube_mesh = &cube_gltf.data_ref().unwrap().scenes[0];
drop(resman);
world.spawn((
cube_mesh.clone(),
WorldTransform::default(),
Transform::from_xyz(0.0, 0.0, -2.0),
));
{
let mut light_tran = Transform::from_xyz(1.5, 2.5, 0.0);
light_tran.scale = Vec3::new(0.5, 0.5, 0.5);
light_tran.rotate_x(math::Angle::Degrees(-45.0));
light_tran.rotate_y(math::Angle::Degrees(25.0));
world.spawn((
DirectionalLight {
enabled: true,
color: Vec3::ONE,
intensity: 0.15, //..Default::default()
},
light_tran,
));
}
let mut camera = CameraComponent::new_3d();
camera.transform.translation += math::Vec3::new(0.0, 0.0, 5.5);
world.spawn((camera, FreeFlyCamera::default()));
}

View File

@ -6,10 +6,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
lyra-engine = { path = "../../", version = "0.0.1", features = ["lua_scripting"] }
lyra-scripting = { path = "../../lyra-scripting", features = ["lua", "teal"] }
lyra-engine = { path = "../../", version = "0.0.1" }
#lyra-engine = { path = "../../", version = "0.0.1", features = ["lua_scripting"] }
#lyra-scripting = { path = "../../lyra-scripting", features = ["lua", "teal"] }
#lyra-ecs = { path = "../../lyra-ecs"}
anyhow = "1.0.75"
async-std = "1.12.0"
tracing = "0.1.37"
fps_counter = "2.0.0"
tracing = "0.1.37"

View File

@ -1,102 +0,0 @@
use lyra_engine::{
ecs::{query::{Res, View}, Component}, game::Game, input::ActionHandler, math::{EulerRot, Quat, Vec3}, plugin::Plugin, scene::CameraComponent, DeltaTime
};
/* enum FreeFlyCameraActions {
MoveForwardBackward,
MoveLeftRight,
MoveUpDown,
LookLeftRight,
LookUpDown,
LookRoll,
} */
#[derive(Clone, Component)]
pub struct FreeFlyCamera {
pub speed: f32,
pub slow_speed_factor: f32,
pub look_speed: f32,
pub mouse_sensitivity: f32,
pub look_with_keys: bool,
}
impl Default for FreeFlyCamera {
fn default() -> Self {
Self {
speed: 4.0,
slow_speed_factor: 0.25,
look_speed: 0.3,
mouse_sensitivity: 1.0,
look_with_keys: false,
}
}
}
impl FreeFlyCamera {
#[allow(dead_code)]
pub fn new(speed: f32, slow_speed_factor: f32, look_speed: f32, mouse_sensitivity: f32, look_with_keys: bool) -> Self {
Self {
speed,
slow_speed_factor,
look_speed,
mouse_sensitivity,
look_with_keys,
}
}
}
pub fn free_fly_camera_controller(delta_time: Res<DeltaTime>, handler: Res<ActionHandler>, view: View<(&mut CameraComponent, &FreeFlyCamera)>) -> anyhow::Result<()> {
let delta_time = **delta_time;
for (mut cam, fly) in view.into_iter() {
let forward = cam.transform.forward();
let left = cam.transform.left();
let up = Vec3::Y;
let move_y = handler.get_axis_modifier("MoveUpDown").unwrap_or(0.0);
let move_x = handler.get_axis_modifier("MoveLeftRight").unwrap_or(0.0);
let move_z = handler.get_axis_modifier("MoveForwardBackward").unwrap_or(0.0);
let mut velocity = Vec3::ZERO;
velocity += move_y * up;
velocity += move_x * left;
velocity += move_z * forward;
if velocity != Vec3::ZERO {
cam.transform.translation += velocity.normalize() * fly.speed * delta_time; // TODO: speeding up
}
let motion_x = handler.get_axis_modifier("LookLeftRight").unwrap_or(0.0);
let motion_y = handler.get_axis_modifier("LookUpDown").unwrap_or(0.0);
let motion_z = handler.get_axis_modifier("LookRoll").unwrap_or(0.0);
let mut camera_rot = Vec3::ZERO;
camera_rot.y -= motion_x * fly.mouse_sensitivity;
camera_rot.x -= motion_y * fly.mouse_sensitivity;
camera_rot.z -= motion_z * fly.mouse_sensitivity;
if camera_rot != Vec3::ZERO {
let look_velocity = camera_rot * fly.look_speed * delta_time;
let (mut y, mut x, _) = cam.transform.rotation.to_euler(EulerRot::YXZ);
x += look_velocity.x;
y += look_velocity.y;
x = x.clamp(-1.54, 1.54);
// rotation is not commutative, keep this order to avoid unintended roll
cam.transform.rotation = Quat::from_axis_angle(Vec3::Y, y)
* Quat::from_axis_angle(Vec3::X, x);
}
}
Ok(())
}
/// A plugin that adds the free fly camera controller system to the world. It is expected that
/// there is a [`FreeFlyCamera`] in the world, if there isn't, the camera would not move.
pub struct FreeFlyCameraPlugin;
impl Plugin for FreeFlyCameraPlugin {
fn setup(&self, game: &mut Game) {
game.with_system("free_fly_camera_system", free_fly_camera_controller, &[]);
}
}

View File

@ -1,25 +1,13 @@
use std::ptr::NonNull;
use std::{ptr::NonNull, thread, time::Duration};
use lyra_engine::{math::{self, Vec3}, math::Transform, input::{KeyCode, ActionHandler, Action, ActionKind, LayoutId, ActionMapping, ActionSource, ActionMappingId, InputActionPlugin, MouseInput, MouseAxis, CommonActionLabel}, game::Game, render::{window::{CursorGrabMode, WindowOptions}, light::{PointLight, directional::DirectionalLight, SpotLight}}, change_tracker::Ct, ecs::{system::{Criteria, CriteriaSchedule, BatchedSystem, IntoSystem}, World, Component}, DeltaTime, scene::{ModelComponent, CameraComponent}, lua::{LuaScriptingPlugin, LuaScript}, Script, ScriptList};
use lyra_engine::assets::{ResourceManager, Model};
mod free_fly_camera;
use free_fly_camera::{FreeFlyCameraPlugin, FreeFlyCamera};
#[derive(Clone, Copy, Hash, Debug)]
pub enum ActionLabel {
MoveForwardBackward,
MoveLeftRight,
MoveUpDown,
LookLeftRight,
LookUpDown,
LookRoll,
}
use lyra_engine::{assets::gltf::Gltf, change_tracker::Ct, ecs::{query::{Res, View}, system::{Criteria, CriteriaSchedule, IntoSystem}, Component, World}, game::Game, input::{Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput}, math::{self, Quat, Transform, Vec3}, render::{light::{directional::DirectionalLight, PointLight, SpotLight}, window::{CursorGrabMode, WindowOptions}}, scene::{self, CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform, ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN}, DeltaTime};
use lyra_engine::assets::ResourceManager;
struct FixedTimestep {
max_tps: u32,
fixed_time: f32,
accumulator: f32,
old_dt: Option<DeltaTime>,
}
#[allow(dead_code)]
@ -29,6 +17,7 @@ impl FixedTimestep {
max_tps,
fixed_time: Self::calc_fixed_time(max_tps),
accumulator: 0.0,
old_dt: None,
}
}
@ -65,6 +54,21 @@ impl Criteria for FixedTimestep {
CriteriaSchedule::No
}
fn modify_world(&mut self, mut world: NonNull<World>) {
let world = unsafe { world.as_mut() };
self.old_dt = world.try_get_resource().map(|r| *r);
world.add_resource(DeltaTime::from(self.fixed_time));
}
fn undo_world_modifications(&mut self, mut world: NonNull<World>) {
let world = unsafe { world.as_mut() };
world.add_resource(
self.old_dt
.expect("DeltaTime resource was somehow never got from the world"),
);
}
}
#[derive(Clone)]
@ -82,34 +86,37 @@ async fn main() {
window_options.cursor_visible = false; */
}
let mut resman = world.get_resource_mut::<ResourceManager>();
let resman = world.get_resource_mut::<ResourceManager>();
//let diffuse_texture = resman.request::<Texture>("assets/happy-tree.png").unwrap();
//let antique_camera_model = resman.request::<Model>("assets/AntiqueCamera.glb").unwrap();
//let cube_model = resman.request::<Model>("assets/cube-texture-bin.glb").unwrap();
let cube_model = resman.request::<Model>("assets/texture-sep/texture-sep.gltf").unwrap();
let crate_model = resman.request::<Model>("assets/crate/crate.gltf").unwrap();
//let sponza_model = resman.request::<Model>("assets/sponza/Sponza.gltf").unwrap();
let cube_gltf = resman.request::<Gltf>("../assets/texture-sep/texture-sep.gltf").unwrap();
/*let crate_gltf = resman.request::<Gltf>("assets/crate/crate.gltf").unwrap();
let separate_gltf = resman.request::<Gltf>("assets/pos-testing/child-node-cubes.glb").unwrap(); */
//drop(resman);
cube_gltf.wait_recurse_dependencies_load();
let cube_mesh = &cube_gltf.data_ref()
.unwrap().meshes[0];
/* let crate_mesh = &crate_gltf.data_ref()
.unwrap().meshes[0];
let separate_scene = &separate_gltf.data_ref()
.unwrap().scenes[0]; */
let sponza_model = resman.request::<Gltf>("../assets/sponza/Sponza.gltf").unwrap();
drop(resman);
/* world.spawn((
ModelComponent(antique_camera_model),
Transform::from_xyz(0.0, -5.0, -10.0),
)); */
sponza_model.wait_recurse_dependencies_load();
let sponza_scene = &sponza_model.data_ref()
.unwrap().scenes[0];
/* world.spawn((
ModelComponent(sponza_model),
world.spawn((
sponza_scene.clone(),
WorldTransform::default(),
Transform::from_xyz(0.0, 0.0, 0.0),
)); */
{
let cube_tran = Transform::from_xyz(-3.5, 0.0, -8.0);
//cube_tran.rotate_y(math::Angle::Degrees(180.0));
world.spawn((
cube_tran,
ModelComponent(crate_model.clone()),
CubeFlag,
));
}
));
{
let mut light_tran = Transform::from_xyz(1.5, 2.5, 0.0);
@ -118,37 +125,105 @@ async fn main() {
light_tran.rotate_y(math::Angle::Degrees(25.0));
world.spawn((
DirectionalLight {
color: Vec3::new(1.0, 1.0, 1.0),
ambient: 0.3,
diffuse: 1.0,
specular: 1.3,
enabled: true,
color: Vec3::ONE,
intensity: 0.35
//..Default::default()
},
light_tran,
ModelComponent(cube_model.clone()),
));
}
{
let mut light_tran = Transform::from_xyz(-3.5, 0.2, -4.5);
light_tran.scale = Vec3::new(0.5, 0.5, 0.5);
let t = Transform::new(
//Vec3::new(-5.0, 1.0, -1.28),
Vec3::new(-5.0, 1.0, -0.0),
//Vec3::new(-10.0, 0.94, -0.28),
Quat::IDENTITY,
Vec3::new(0.25, 0.25, 0.25),
);
world.spawn((
PointLight {
enabled: true,
color: Vec3::new(0.0, 0.0, 1.0),
intensity: 1.0,
range: 2.0,
..Default::default()
},
WorldTransform::from(t),
t,
cube_mesh.clone(),
));
let t = Transform::new(
Vec3::new(-3.0, 0.2, -1.5),
//Vec3::new(-5.0, 1.0, -0.28),
//Vec3::new(-10.0, 0.94, -0.28),
Quat::IDENTITY,
Vec3::new(0.15, 0.15, 0.15),
);
world.spawn((
PointLight {
enabled: true,
color: Vec3::new(0.0, 0.5, 1.0),
intensity: 1.0,
range: 1.0,
..Default::default()
},
WorldTransform::from(t),
t,
cube_mesh.clone(),
));
let t = Transform::new(
Vec3::new(0.0, 0.2, -1.5),
//Vec3::new(-5.0, 1.0, -0.28),
//Vec3::new(-10.0, 0.94, -0.28),
Quat::IDENTITY,
Vec3::new(0.15, 0.15, 0.15),
);
world.spawn((
SpotLight {
color: Vec3::new(1.0, 0.2, 0.2),
cutoff: math::Angle::Degrees(12.5),
outer_cutoff: math::Angle::Degrees(17.5),
constant: 1.0,
linear: 0.007,
quadratic: 0.0002,
ambient: 0.0,
diffuse: 7.0,
specular: 1.0,
enabled: true,
color: Vec3::new(1.0, 0.0, 0.0),
intensity: 1.0,
range: 1.5,
//cutoff: math::Angle::Degrees(45.0),
..Default::default()
},
Transform::from(light_tran),
ModelComponent(cube_model.clone()),
WorldTransform::from(t),
t,
cube_mesh.clone(),
));
}
/* {
let mut light_tran = Transform::from_xyz(2.0, 2.5, -9.5);
light_tran.scale = Vec3::new(0.5, 0.5, 0.5);
world.spawn((
PointLight {
color: Vec3::new(0.0, 0.0, 1.0),
intensity: 3.3,
constant: 1.0,
linear: 0.09,
quadratic: 0.032,
ambient: 0.2,
diffuse: 1.0,
specular: 1.3,
},
Transform::from(light_tran),
cube_mesh.clone(),
));
} */
/* {
let mut light_tran = Transform::from_xyz(-5.0, 2.5, -9.5);
@ -172,128 +247,87 @@ async fn main() {
));
} */
{
let mut light_tran = Transform::from_xyz(2.0, 2.5, -9.5);
light_tran.scale = Vec3::new(0.5, 0.5, 0.5);
world.spawn((
PointLight {
color: Vec3::new(0.0, 0.0, 1.0),
intensity: 3.3,
constant: 1.0,
linear: 0.09,
quadratic: 0.032,
ambient: 0.2,
diffuse: 1.0,
specular: 1.3,
},
Transform::from(light_tran),
ModelComponent(cube_model),
));
}
let mut camera = CameraComponent::new_3d();
camera.transform.translation += math::Vec3::new(0.0, 0.0, 5.5);
// these values were taken by manually positioning the camera in the scene.
camera.transform = Transform::new(
Vec3::new(-10.0, 0.94, -0.28),
Quat::from_xyzw(0.03375484, -0.7116095, 0.0342693, 0.70092666),
Vec3::ONE
);
//camera.transform.translation += math::Vec3::new(0.0, 0.0, 5.5);
world.spawn(( camera, FreeFlyCamera::default() ));
Ok(())
};
#[allow(unused_variables)]
let fps_system = |world: &mut World| -> anyhow::Result<()> {
let mut counter = world.get_resource_mut::<fps_counter::FPSCounter>();
let camera_debug_plugin = move |game: &mut Game| {
let sys = |handler: Res<ActionHandler>, view: View<&mut CameraComponent>| -> anyhow::Result<()> {
if handler.was_action_just_pressed("Debug") {
for mut cam in view.into_iter() {
cam.debug = !cam.debug;
}
}
let fps = counter.tick();
Ok(())
};
println!("FPS: {fps}");
Ok(())
};
let fps_plugin = move |game: &mut Game| {
let world = game.world_mut();
world.add_resource(fps_counter::FPSCounter::new());
};
let spin_system = |world: &mut World| -> anyhow::Result<()> {
const SPEED: f32 = 4.0;
let delta_time = **world.get_resource::<DeltaTime>();
for (mut transform, _) in world.view_iter::<(&mut Transform, &CubeFlag)>() {
let t = &mut transform;
t.rotate_y(math::Angle::Degrees(SPEED * delta_time));
}
for (mut transform, _s) in world.view_iter::<(&mut Transform, &mut SpotLight)>() {
let t = &mut transform;
t.rotate_x(math::Angle::Degrees(SPEED * delta_time));
}
Ok(())
};
let jiggle_plugin = move |game: &mut Game| {
game.world_mut().add_resource(TpsAccumulator(0.0));
let mut sys = BatchedSystem::new();
sys.with_criteria(FixedTimestep::new(45));
sys.with_system(spin_system.into_system());
//sys.with_system(fps_system);
//game.with_system("fixed", sys, &[]);
//fps_plugin(game);
game.with_system("camera_debug_trigger", sys, &[]);
game.with_system("update_world_transforms", scene::system_update_world_transforms, &[]);
};
let action_handler_plugin = |game: &mut Game| {
/* let action_handler = ActionHandler::builder()
let action_handler = ActionHandler::builder()
.add_layout(LayoutId::from(0))
.add_action(CommonActionLabel::MoveForwardBackward, Action::new(ActionKind::Axis))
.add_action(CommonActionLabel::MoveLeftRight, Action::new(ActionKind::Axis))
.add_action(CommonActionLabel::MoveUpDown, Action::new(ActionKind::Axis))
.add_action(CommonActionLabel::LookLeftRight, Action::new(ActionKind::Axis))
.add_action(CommonActionLabel::LookUpDown, Action::new(ActionKind::Axis))
.add_action(CommonActionLabel::LookRoll, Action::new(ActionKind::Axis))
.add_action(ACTLBL_MOVE_FORWARD_BACKWARD, Action::new(ActionKind::Axis))
.add_action(ACTLBL_MOVE_LEFT_RIGHT, Action::new(ActionKind::Axis))
.add_action(ACTLBL_MOVE_UP_DOWN, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_LEFT_RIGHT, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_UP_DOWN, Action::new(ActionKind::Axis))
.add_action(ACTLBL_LOOK_ROLL, Action::new(ActionKind::Axis))
.add_action("Debug", Action::new(ActionKind::Button))
.add_mapping(ActionMapping::builder(LayoutId::from(0), ActionMappingId::from(0))
.bind(CommonActionLabel::MoveForwardBackward, &[
.bind(ACTLBL_MOVE_FORWARD_BACKWARD, &[
ActionSource::Keyboard(KeyCode::W).into_binding_modifier(1.0),
ActionSource::Keyboard(KeyCode::S).into_binding_modifier(-1.0)
])
.bind(CommonActionLabel::MoveLeftRight, &[
.bind(ACTLBL_MOVE_LEFT_RIGHT, &[
ActionSource::Keyboard(KeyCode::A).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::D).into_binding_modifier(1.0)
])
.bind(CommonActionLabel::MoveUpDown, &[
.bind(ACTLBL_MOVE_UP_DOWN, &[
ActionSource::Keyboard(KeyCode::C).into_binding_modifier(1.0),
ActionSource::Keyboard(KeyCode::Z).into_binding_modifier(-1.0)
])
.bind(CommonActionLabel::LookLeftRight, &[
.bind(ACTLBL_LOOK_LEFT_RIGHT, &[
ActionSource::Mouse(MouseInput::Axis(MouseAxis::X)).into_binding(),
ActionSource::Keyboard(KeyCode::Left).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Right).into_binding_modifier(1.0),
//ActionSource::Gamepad(GamepadFormat::DualAxis, GamepadInput::Axis(GamepadAxis::RThumbstickX)).into_binding(),
])
.bind(CommonActionLabel::LookUpDown, &[
.bind(ACTLBL_LOOK_UP_DOWN, &[
ActionSource::Mouse(MouseInput::Axis(MouseAxis::Y)).into_binding(),
ActionSource::Keyboard(KeyCode::Up).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Down).into_binding_modifier(1.0),
//ActionSource::Gamepad(GamepadFormat::DualAxis, GamepadInput::Axis(GamepadAxis::RThumbstickY)).into_binding(),
])
.bind(CommonActionLabel::LookRoll, &[
.bind(ACTLBL_LOOK_ROLL, &[
ActionSource::Keyboard(KeyCode::E).into_binding_modifier(-1.0),
ActionSource::Keyboard(KeyCode::Q).into_binding_modifier(1.0),
])
.bind("Debug", &[
ActionSource::Keyboard(KeyCode::B).into_binding(),
])
.finish()
);
).finish();
let world = game.world_mut();
world.add_resource(action_handler); */
world.add_resource(action_handler);
game.with_plugin(InputActionPlugin);
};
let script_test_plugin = |game: &mut Game| {
/* let script_test_plugin = |game: &mut Game| {
game.with_plugin(LuaScriptingPlugin);
let world = game.world_mut();
@ -306,15 +340,15 @@ async fn main() {
let scripts = ScriptList::new(vec![script]);
world.spawn((scripts,));
};
}; */
Game::initialize().await
.with_plugin(lyra_engine::DefaultPlugins)
.with_startup_system(setup_sys.into_system())
.with_plugin(action_handler_plugin)
.with_plugin(script_test_plugin)
//.with_plugin(script_test_plugin)
//.with_plugin(fps_plugin)
.with_plugin(jiggle_plugin)
.with_plugin(camera_debug_plugin)
.with_plugin(FreeFlyCameraPlugin)
.run().await;
}

View File

@ -1,13 +1,13 @@
{
"rdocCaptureSettings": 1,
"settings": {
"autoStart": true,
"autoStart": false,
"commandLine": "",
"environment": [
],
"executable": "/media/data_drive/Development/Rust/lyra-test/engine/target/debug/testbed",
"executable": "/media/data_drive/Development/Rust/lyra-engine/target/debug/testbed",
"inject": false,
"numQueuedFrames": 1,
"numQueuedFrames": 0,
"options": {
"allowFullscreen": true,
"allowVSync": true,
@ -22,7 +22,7 @@
"softMemoryLimit": 0,
"verifyBufferAccess": false
},
"queuedFrameCap": 5,
"workingDir": "/media/data_drive/Development/Rust/lyra-test/engine/examples/testbed"
"queuedFrameCap": 0,
"workingDir": "/media/data_drive/Development/Rust/lyra-engine/examples/testbed"
}
}

View File

@ -14,6 +14,8 @@ lyra-math = { path = "../lyra-math", optional = true }
anyhow = "1.0.75"
thiserror = "1.0.50"
paste = "1.0.14"
atomic_refcell = "0.1.13"
tracing = "0.1.37"
[dev-dependencies]
rand = "0.8.5" # used for tests

View File

@ -1,6 +1,6 @@
use std::{ptr::{NonNull, self}, alloc::{self, Layout, alloc, dealloc}, mem, collections::HashMap, cell::{RefCell, Ref, RefMut, BorrowError, BorrowMutError}, ops::DerefMut};
use crate::{world::ArchetypeEntityId, bundle::Bundle, component_info::ComponentInfo, DynTypeId, Tick, Entity};
use crate::{bundle::Bundle, component_info::ComponentInfo, world::ArchetypeEntityId, DynTypeId, Entity, Tick};
#[derive(Clone)]
pub struct ComponentColumn {
@ -19,7 +19,9 @@ impl Drop for ComponentColumn {
unsafe {
// TODO: trigger drop on the components
let layout = self.info.layout();
// SAFETY: The size of the data buffer is the capcity times the size of a component
let size = self.info.layout().size() * self.capacity;
let layout = Layout::from_size_align_unchecked(size, self.info.layout().align());
dealloc(data, layout);
}
}
@ -55,7 +57,6 @@ impl ComponentColumn {
/// 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) {
assert!(entity_index < self.capacity);
@ -76,6 +77,23 @@ impl ComponentColumn {
}
}
/// Inserts an entity and its component at a specific index.
pub unsafe fn insert_entity(&mut self, entity_index: usize, comp_src: NonNull<u8>, tick: Tick) {
assert!(entity_index < self.capacity);
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.len += 1;
}
/// Get a component at an entities index.
///
/// # Safety
@ -152,6 +170,8 @@ impl ComponentColumn {
/// Removes a component from the column, freeing it, and returning the old index of the entity that took its place in the column.
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!");
let mut data = self.data.borrow_mut();
let data = data.deref_mut();
@ -170,8 +190,7 @@ impl ComponentColumn {
mem::swap(&mut old_comp_ptr, &mut new_comp_ptr); // new_comp_ptr is now the old ptr
// make sure to keep entity indexes correct in the ticks list as well
self.entity_ticks.swap(moved_index, entity_index);
self.entity_ticks.pop();
self.entity_ticks.swap_remove(entity_index);
Some(moved_index)
} else { None };
@ -276,38 +295,48 @@ impl Archetype {
self.capacity = new_cap;
}
debug_assert_eq!(self.entity_ids.len(), self.entities.len(), "Somehow the Archetype's entity storage got unsynced");
self.ensure_synced();
let entity_index = ArchetypeEntityId(self.entity_ids.len() as u64);
self.entity_ids.insert(entity, entity_index);
self.entities.push(entity);
bundle.take(|data, type_id, _size| {
let col = self.get_column_mut(type_id).unwrap();
unsafe { col.set_at(entity_index.0 as usize, data, *tick); }
col.len += 1;
bundle.take(|data, type_id, info| {
self.put_component_at(tick, data, type_id, info.layout().size(), entity_index.0 as _);
});
entity_index
}
/// Removes an entity from the Archetype and frees its components. Returns the entity record
/// that took its place in the component column.
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); }
}
/// 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.get(&entity)
let entity_index = self.entity_ids.remove(&entity)
.expect("The entity is not in this Archetype!");
let mut removed_entity: Option<(Entity, ArchetypeEntityId)> = None;
for c in self.columns.iter_mut() {
let moved_entity = unsafe { c.remove_component(entity_index.0 as usize, tick) };
if let Some(res) = moved_entity {
if let Some(moved_idx) = moved_entity {
if let Some((_, aid)) = removed_entity {
// Make sure that the moved entity is the same as what was moved in other columns.
assert!(res as u64 == aid.0);
assert!(moved_idx as u64 == aid.0);
} else {
// This is the first move, so find the EntityId that points to the column index.
let just_removed = self.entities[res];
removed_entity = Some((just_removed, ArchetypeEntityId(res as u64)));
// This is the first move, so find the Entity that was moved into this index.
let just_removed = self.entities[moved_idx];
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.
@ -316,13 +345,16 @@ impl Archetype {
}
// safe from the .expect at the start of this method.
self.entity_ids.remove(&entity).unwrap();
if self.entities.len() > 1 {
let len = self.entities.len();
self.entities.swap(entity_index.0 as _, len - 1);
}
self.entities.pop().unwrap();
//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);
// now change the ArchetypeEntityId to be the index that the moved entity was moved into.
removed_entity.map(|(e, _a)| (e, entity_index))
}
@ -368,6 +400,11 @@ impl Archetype {
self.capacity = new_capacity;
}
/// Attempts to find the column storing components of `type_id`
pub fn get_column_at(&self, idx: usize) -> Option<&ComponentColumn> {
self.columns.get(idx)
}
/// Attempts to find the column storing components of `type_id`
pub fn get_column<I: Into<DynTypeId>>(&self, type_id: I) -> Option<&ComponentColumn> {
let type_id = type_id.into();
@ -390,7 +427,8 @@ 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);
@ -402,13 +440,19 @@ impl Archetype {
entity_index
}
/// 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");
}
/// 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)
/// related to borrowing mutably from self more than once.
/* pub fn move_into<B>(&self, into_arch: &mut Archetype, entity: Entity, new_components: B)
/* pub fn move_into<B>(&mut self, entities: &mut Entities, tick: &Tick, into_arch: &mut Archetype, entity: Entity, new_components: B)
where
B: Bundle
{
@ -418,25 +462,40 @@ 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();
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();
let ptr = NonNull::new_unchecked(ptr.as_ptr()
.add(new_index.0 as usize * col.info.layout.size));
into_col.set_at(new_index.0 as _, ptr);
.add(new_index.0 as usize * col.info.layout().size()));
into_col.set_at(new_index.0 as _, ptr, *tick);
//into_col.set_at(new_index.0 as _, ptr);
}
}
// now move the new components into the new archetype
new_components.take(|data, type_id, _size| {
let col = into_arch.get_column_mut(type_id).unwrap();
unsafe { col.set_at(new_index.0 as _, data); }
unsafe { col.set_at(new_index.0 as _, data, *tick); }
col.len += 1;
});
//self.remove_entity(entity);
if let Some((en, new_idx)) = self.remove_entity(entity, tick) {
let moved_rec = Record {
id: self.id,
index: new_idx,
};
entities.insert_entity_record(en, moved_rec);
}
let new_rec = Record {
id: into_arch.id,
index: new_index,
};
entities.insert_entity_record(entity, new_rec);
into_arch.ensure_synced();
self.ensure_synced();
} */
/// Returns a borrow to the map used to find the column indices of the entity.
@ -453,6 +512,36 @@ impl Archetype {
pub fn has_entity(&self, e: Entity) -> bool {
self.entity_ids.contains_key(&e)
}
/// 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();
for coli in column_info.into_iter() {
let col = unsafe { ComponentColumn::new(coli, self.capacity) };
self.columns.push(col);
}
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());
}
});
}
}
}
#[cfg(test)]
@ -642,7 +731,7 @@ mod tests {
#[test]
fn dynamic_archetype() {
let layout = Layout::new::<u32>();
let info = ComponentInfo::new_unknown(DynTypeId::Unknown(100), "u32", 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);
@ -666,4 +755,24 @@ mod tests {
assert_eq!(unsafe { *ptr.cast::<u32>().as_ref() }, comp);
assert_eq!(col.info, info);
}
/// Tests removing an entity from the Archetype when it is the only entity in it.
#[test]
fn remove_single_entity() {
let info = (Vec2::new(0.0, 0.0),).info();
let mut a = Archetype::from_bundle_info(super::ArchetypeId(0), info);
let ae = Entity {
id: EntityId(0),
generation: 0
};
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, mem::size_of, alloc::Layout};
use std::{ptr::NonNull, alloc::Layout};
use crate::{component::Component, component_info::ComponentInfo, DynTypeId};
@ -11,12 +11,30 @@ 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, usize));
fn take(self, f: impl FnMut(NonNull<u8>, DynTypeId, ComponentInfo));
/// Returns a boolean indicating if this Bundle is dynamic. See [`DynamicBundle`]
fn is_dynamic(&self) -> bool;
}
impl Bundle for () {
fn type_ids(&self) -> Vec<DynTypeId> {
vec![DynTypeId::of::<()>()]
}
fn info(&self) -> Vec<ComponentInfo> {
vec![ComponentInfo::new::<()>()]
}
fn take(self, mut f: impl FnMut(NonNull<u8>, DynTypeId, ComponentInfo)) {
f(NonNull::from(&self).cast(), DynTypeId::of::<()>(), ComponentInfo::new::<()>());
}
fn is_dynamic(&self) -> bool {
false
}
}
impl<C: Component> Bundle for C {
fn type_ids(&self) -> Vec<DynTypeId> {
vec![DynTypeId::of::<C>()]
@ -26,8 +44,8 @@ impl<C: Component> Bundle for C {
vec![ComponentInfo::new::<C>()]
}
fn take(self, mut f: impl FnMut(NonNull<u8>, DynTypeId, usize)) {
f(NonNull::from(&self).cast(), DynTypeId::of::<C>(), size_of::<C>());
fn take(self, mut f: 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.
@ -52,12 +70,12 @@ macro_rules! impl_bundle_tuple {
vec![$(ComponentInfo::new::<$name>()),+]
}
fn take(self, mut f: impl FnMut(NonNull<u8>, DynTypeId, usize)) {
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
let ($($name,)+) = self;
$(
f(NonNull::from(&$name).cast(), DynTypeId::of::<$name>(), size_of::<$name>());
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);
@ -118,8 +136,6 @@ impl DynamicBundle {
where
C: Component
{
let info = ComponentInfo::new::<C>();
// an owned pointer must be created from the provided component since comp would drop
// out of scope and the data would become invalid
let ptr = unsafe {
@ -128,17 +144,43 @@ 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);
alloc_ptr.cast()
};
let info = ComponentInfo::new::<C>();
self.bundle.push((ptr, info));
}
/// Push an unknown type to the bundle
/// Push an unknown type to the end of the bundle.
pub fn push_unknown(&mut self, data: NonNull<u8>, info: ComponentInfo) {
self.bundle.push((data, info));
}
/// Push a bundle to the end of this dynamic bundle.
pub fn push_bundle<B>(&mut self, bundle: B)
where
B: Bundle
{
bundle.take(|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());
alloc_ptr
};
self.push_unknown(p, info);
});
}
}
impl Bundle for DynamicBundle {
@ -150,9 +192,9 @@ impl Bundle for DynamicBundle {
self.bundle.iter().map(|b| b.1).collect()
}
fn take(self, mut f: impl FnMut(NonNull<u8>, DynTypeId, usize)) {
for (data, info) in self.bundle.iter() {
f(*data, info.type_id(), info.layout().size());
fn take(self, mut f: impl FnMut(NonNull<u8>, DynTypeId, ComponentInfo)) {
for (data, info) in self.bundle.into_iter() {
f(data, info.type_id(), info);
}
}

View File

@ -1,4 +1,4 @@
use std::{any::Any, cell::RefMut, collections::VecDeque, ptr::{self, NonNull}};
use std::{any::Any, cell::RefMut, mem::{self, MaybeUninit}, ptr::{self, NonNull}};
use crate::{system::FnArgFetcher, Access, Bundle, Entities, Entity, World};
@ -23,24 +23,73 @@ where
}
}
type RunCommand = unsafe fn(cmd: Box<dyn Command>, world: &mut World);
type RunCommand = unsafe fn(cmd: *mut (), world: Option<&mut World>) -> usize;
#[repr(C, packed)]
struct PackedCommand<T: Command> {
run: RunCommand,
cmd: T,
}
/// Stores a queue of commands that will get executed after the system is ran.
///
/// This struct can be inserted as a resource into the world, and the commands will be
/// executed by the [`GraphExecutor`](crate::system::GraphExecutor) after the system is executed.
#[derive(Default)]
pub struct CommandQueue(VecDeque<(RunCommand, Box<dyn Command>)>);
pub struct CommandQueue {
data: Vec<MaybeUninit<u8>>,
}
impl CommandQueue {
/// Execute the commands in the queue.
///
/// If `world` is `None`, the commands will just be dropped and the memory freed.
fn execute(&mut self, mut world: Option<&mut World>) {
let range = self.data.as_mut_ptr_range();
let mut current = range.start;
let end = range.end;
while current < end {
// Retrieve the runner for the command.
// Safety: current pointer will either be the start of the buffer, or at the start of a new PackedCommand
let run_fn = unsafe { current.cast::<RunCommand>().read_unaligned() };
// Retrieves the pointer to the command which is just after RunCommand due to PackedCommand.
// Safety: PackedCommand is repr C and packed, so it will be right after the RunCommand.
current = unsafe { current.add(mem::size_of::<RunCommand>()) };
// Now run the command, providing the type erased pointer to the command.
let read_size = unsafe { run_fn(current.cast(), world.as_deref_mut()) };
// The pointer is added to so that it is just after the command that was ran.
// Safety: the RunCommand returns the size of the command
current = unsafe { current.add(read_size) };
}
// Safety: all of the commands were just read from the pointers.
unsafe { self.data.set_len(0) };
}
}
impl Drop for CommandQueue {
fn drop(&mut self) {
if !self.data.is_empty() {
println!("CommandQueue has commands but is being dropped");
}
self.execute(None);
}
}
/// Used in a system to queue up commands that will run right after this system.
///
/// This can be used to delay the mutation of the world until after the system is ran. These
/// must be used if you're mutating the world inside a [`ViewState`](crate::query::ViewState).
/// must be used if you're mutating the world inside a [`View`](crate::query::View).
///
/// ```nobuild
/// fn particle_spawner_system(
/// commands: Commands,
/// view: ViewState<(&Campfire, &Transform)>
/// view: View<(&Campfire, &Transform)>
/// ) -> anyhow::Result<()> {
/// for (campfire, pos) in view.iter() {
/// // If you do not use commands to spawn this, the next iteration
@ -66,17 +115,44 @@ impl<'a, 'b> Commands<'a, 'b> {
/// Add a command to the end of the command queue
pub fn add<C: Command>(&mut self, cmd: C) {
let cmd = Box::new(cmd);
let run_fn = |cmd_ptr: *mut (), world: Option<&mut World>| {
// Safety: the pointer is a type-erased pointer to the command. The pointer is read
// then dropped out of scope, this closure will not be ran again so no use-after-free
// will occur.
let cmd: C = unsafe { ptr::read_unaligned(cmd_ptr.cast::<C>()) };
match world {
Some(world) => cmd.run(world),
None => {} // cmd just gets dropped
}
let run_fn = |cmd: Box<dyn Command>, world: &mut World| {
let cmd = cmd.as_any_boxed()
.downcast::<C>()
.unwrap();
cmd.run(world);
// the size of the command must be returned to increment the pointer when applying
// the command queue.
mem::size_of::<C>()
};
self.queue.0.push_back((run_fn, cmd));
let data = &mut self.queue.data;
// Reserve enough bytes from the vec to store the packed command and its run fn.
let old_len = data.len();
data.reserve(mem::size_of::<PackedCommand<C>>());
// Get a pointer to the end of the packed data. Safe since we just reserved enough memory
// to store this command.
let end_ptr = unsafe { data.as_mut_ptr().add(old_len) };
unsafe {
// write the command and its runner into the buffer
end_ptr.cast::<PackedCommand<C>>()
// written unaligned to keep everything packed
.write_unaligned(PackedCommand {
run: run_fn,
cmd,
});
// we wrote to the vec's buffer without using its api, so we need manually
// set the length of the vec.
data.set_len(old_len + mem::size_of::<PackedCommand<C>>());
}
}
/// Spawn an entity into the World. See [`World::spawn`]
@ -91,14 +167,8 @@ impl<'a, 'b> Commands<'a, 'b> {
}
/// Execute all commands in the queue, in order of insertion
pub fn execute(&mut self, world: &mut World) -> anyhow::Result<()> {
while let Some((cmd_fn, cmd_ptr)) = self.queue.0.pop_front() {
unsafe {
cmd_fn(cmd_ptr, world);
}
}
Ok(())
pub fn execute(&mut self, world: &mut World) {
self.queue.execute(Some(world));
}
}
@ -125,21 +195,26 @@ impl FnArgFetcher for Commands<'_, '_> {
let mut cmds = Commands::new(&mut state, world);
// safety: Commands has a mut borrow only to entities in the world
let world = unsafe { world_ptr.as_mut() };
cmds.execute(world).unwrap()
cmds.execute(world);
}
}
/// A system for executing deferred commands that are stored in a [`World`] as a Resource.
///
/// Commands are usually added inside a system from a [`Commands`] object created just for it
/// as an fn argument. However, there may be cases that commands cannot be added that way, so
/// they can also be added as a resource and executed later in this system.
pub fn execute_deferred_commands(world: &mut World, mut commands: RefMut<Commands>) -> anyhow::Result<()> {
commands.execute(world)?;
commands.execute(world);
Ok(())
}
#[cfg(test)]
mod tests {
use std::{cell::Ref, ptr::NonNull};
use std::{cell::Ref, ptr::NonNull, sync::{atomic::{AtomicU32, Ordering}, Arc}};
use crate::{system::{GraphExecutor, IntoSystem}, tests::Vec2, Commands, DynTypeId, World};
use crate::{system::{GraphExecutor, IntoSystem}, tests::Vec2, CommandQueue, Commands, DynTypeId, World};
#[test]
fn deferred_commands() {
@ -170,4 +245,28 @@ mod tests {
let vec2: Ref<Vec2> = unsafe { col.get(3) };
assert_eq!(vec2.clone(), spawned_vec);
}
/// A test that ensures a command in a command queue will only ever run once.
#[test]
fn commands_only_one_exec() {
let mut world = World::new();
let counter = Arc::new(AtomicU32::new(0));
let mut queue = CommandQueue::default();
let mut commands = Commands::new(&mut queue, &mut world);
let counter_cl = counter.clone();
commands.add(move |_world: &mut World| {
counter_cl.fetch_add(1, Ordering::AcqRel);
});
queue.execute(Some(&mut world));
assert_eq!(1, counter.load(Ordering::Acquire));
queue.execute(Some(&mut world));
// If its not one, the command somehow was executed.
// I would be surprised it wouldn't cause some segfault but still increment the counter
assert_eq!(1, counter.load(Ordering::Acquire));
}
}

View File

@ -67,7 +67,7 @@ impl ComponentInfo {
}
/// Create ComponentInfo from a type that is not known to rust
pub fn new_unknown<D>(type_id: D, name: &str, layout: Layout) -> Self
pub fn new_unknown<D>(name: Option<String>, type_id: D, layout: Layout) -> Self
where
D: Into<DynTypeId>,
{

View File

@ -11,6 +11,24 @@ pub struct Entity {
pub(crate) generation: u64,
}
impl Entity {
pub fn new(id: EntityId, gen: u64) -> Self {
Self {
id,
generation: gen,
}
}
pub fn id(&self) -> EntityId {
self.id
}
pub fn generation(&self) -> u64 {
self.generation
}
}
#[derive(Clone)]
pub struct Entities {
pub(crate) arch_index: HashMap<EntityId, Record>,
dead: VecDeque<Entity>,

View File

@ -30,7 +30,7 @@ pub use component::*;
pub mod query;
//pub use query::*;
mod relation;
pub mod relation;
pub use relation::Relation;
mod component_info;
@ -51,6 +51,9 @@ pub mod math;
pub use lyra_ecs_derive::*;
pub use atomic_refcell::AtomicRef;
pub use atomic_refcell::AtomicRefMut;
#[cfg(test)]
mod tests;

View File

@ -177,10 +177,10 @@ where
unsafe fn fetch<'a>(&self, _world: &'a World, archetype: &'a crate::archetype::Archetype, tick: crate::Tick) -> Self::Fetch<'a> {
let col = archetype.get_column(self.type_id)
.expect("You ignored 'can_visit_archetype'!");
let col = NonNull::from(col);
// TODO: find a way to get the component column mutable with a borrowed archetype so its tick can be updated.
// the fetcher needs to tick the entities tick in the archetype
let col = NonNull::from(col);
FetchBorrowMut {
col,

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
@ -19,16 +22,21 @@ impl DynamicType {
}
}
/// A struct that fetches some dynamic type.
/// A struct that fetches a dynamic type.
///
/// Currently it can only fetch from archetypes, later it will be able to fetch dynamic
/// resources as well. Its meant to be a single Fetcher for all dynamic types.
pub struct FetchDynamicType<'a> {
pub col: &'a ComponentColumn,
///
/// # Safety
/// Internally, this struct has a `NonNull<ComponentColumn>`. You must ensure that the column
/// is not dropped while this is still alive.
pub struct FetchDynamicTypeUnsafe {
pub col: NonNull<ComponentColumn>,
pub info: ComponentInfo,
}
impl<'a> Fetch<'a> for FetchDynamicType<'a> {
impl<'a> Fetch<'a> for FetchDynamicTypeUnsafe {
type Item = DynamicType;
fn dangling() -> Self {
@ -36,7 +44,7 @@ impl<'a> Fetch<'a> for FetchDynamicType<'a> {
}
unsafe fn get_item(&mut self, entity: crate::ArchetypeEntityId) -> Self::Item {
let ptr = self.col.borrow_ptr();
let ptr = unsafe { self.col.as_ref() }.borrow_ptr();
let ptr = NonNull::new_unchecked(ptr.as_ptr()
.add(entity.0 as usize * self.info.layout().size()));
@ -66,13 +74,13 @@ impl QueryDynamicType {
archetype.has_column(self.info.type_id())
}
pub unsafe fn fetch<'a>(&self, _world: &'a World, _arch_id: crate::archetype::ArchetypeId, archetype: &'a crate::archetype::Archetype) -> FetchDynamicType<'a> {
pub unsafe fn fetch<'a>(&self, _world: &'a World, _arch_id: crate::archetype::ArchetypeId, archetype: &'a crate::archetype::Archetype) -> FetchDynamicTypeUnsafe {
let col = archetype.get_column(self.info.type_id())
.expect("You ignored 'can_visit_archetype'!");
FetchDynamicType {
col,
info: self.info,
FetchDynamicTypeUnsafe {
col: NonNull::from(col),
info: self.info
}
}
}

View File

@ -1,18 +1,25 @@
use std::ops::Range;
use crate::{World, Archetype, ArchetypeEntityId, ArchetypeId, query::Fetch};
use crate::{query::Fetch, Archetype, ArchetypeEntityId, ArchetypeId, Entity, World};
use super::{QueryDynamicType, FetchDynamicType, DynamicType};
use super::{DynamicType, FetchDynamicTypeUnsafe, QueryDynamicType};
pub struct DynamicView<'a> {
world: &'a World,
queries: Vec<QueryDynamicType>
/// Stores the state of a dynamic view.
///
/// See [`DynamicView`].
///
/// This backs [`DynamicView`] which you should probably use. The only reason you would use this
/// instead is if you cant borrow the world when storing this type, and its iterators.
/// [`DynamicViewState`] provides an 'iterator' of [`DynamicViewStateIter`], which requires you to
/// provide a world borrow on each `next` of the iterator. View [`DynamicViewStateIter`] for more
/// info.
pub struct DynamicViewState {
pub(crate) queries: Vec<QueryDynamicType>
}
impl<'a> DynamicView<'a> {
pub fn new(world: &'a World) -> Self {
impl DynamicViewState {
pub fn new() -> Self {
Self {
world,
queries: Vec::new(),
}
}
@ -20,45 +27,49 @@ impl<'a> DynamicView<'a> {
pub fn push(&mut self, dyn_query: QueryDynamicType) {
self.queries.push(dyn_query);
}
}
impl<'a> IntoIterator for DynamicView<'a> {
type Item = Vec<DynamicType>;
type IntoIter = DynamicViewIter<'a>;
fn into_iter(self) -> Self::IntoIter {
let archetypes = self.world.archetypes.values().collect();
DynamicViewIter {
world: self.world,
pub fn into_iter(self) -> DynamicViewStateIter {
DynamicViewStateIter {
queries: self.queries,
fetchers: Vec::new(),
archetypes,
next_archetype: 0,
component_indices: 0..0,
}
}
}
pub struct DynamicViewIter<'a> {
pub world: &'a World,
pub queries: Vec<QueryDynamicType>,
pub fetchers: Vec<FetchDynamicType<'a>>,
pub archetypes: Vec<&'a Archetype>,
pub next_archetype: usize,
pub component_indices: Range<u64>,
pub struct DynamicViewItem {
pub row: Vec<DynamicType>,
pub entity: Entity,
}
impl<'a> Iterator for DynamicViewIter<'a> {
type Item = Vec<DynamicType>;
/// A view iterator on dynamic types.
///
/// You will likely want to use [`DynamicViewIter`] unless you need to store the iterator
/// without also borrowing from the world. [`DynamicViewStateIter`] doesn't
/// actually implement [`Iterator`] since it requires a `&World` to be provided to it
/// each time `next` is ran (see [`DynamicViewStateIter::next`]).
pub struct DynamicViewStateIter {
pub queries: Vec<QueryDynamicType>,
fetchers: Vec<FetchDynamicTypeUnsafe>,
next_archetype: usize,
component_indices: Range<u64>,
}
impl DynamicViewStateIter {
pub fn next(&mut self, world: &World) -> Option<(Entity, Vec<DynamicType>)> {
let archetypes = world.archetypes.values().collect::<Vec<_>>();
fn next(&mut self) -> Option<Self::Item> {
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![];
//let fetcher = self.fetcher.as_mut().unwrap();
for fetcher in self.fetchers.iter_mut() {
let entity_index = ArchetypeEntityId(entity_index);
if !fetcher.can_visit_item(entity_index) {
@ -73,15 +84,15 @@ impl<'a> Iterator for DynamicViewIter<'a> {
continue;
}
return Some(fetch_res);
return Some((entity, fetch_res));
} else {
if self.next_archetype >= self.archetypes.len() {
if self.next_archetype >= archetypes.len() {
return None; // ran out of archetypes to go through
}
let arch_id = self.next_archetype;
self.next_archetype += 1;
let arch = unsafe { self.archetypes.get_unchecked(arch_id) };
let arch = unsafe { archetypes.get_unchecked(arch_id) };
if arch.entity_ids.is_empty() {
continue;
@ -92,7 +103,7 @@ impl<'a> Iterator for DynamicViewIter<'a> {
}
self.fetchers = self.queries.iter()
.map(|q| unsafe { q.fetch(self.world, ArchetypeId(arch_id as u64), arch) } )
.map(|q| unsafe { q.fetch(world, ArchetypeId(arch_id as u64), arch) } )
.collect();
self.component_indices = 0..arch.entity_ids.len() as u64;
}
@ -100,18 +111,106 @@ impl<'a> Iterator for DynamicViewIter<'a> {
}
}
/// 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 DynamicView<'a> {
world: &'a World,
inner: DynamicViewState,
}
impl<'a> DynamicView<'a> {
pub fn new(world: &'a World) -> Self {
Self {
world,
inner: DynamicViewState::new(),
}
}
pub fn push(&mut self, dyn_query: QueryDynamicType) {
self.inner.queries.push(dyn_query);
}
}
/// A view iterator on dynamic types.
///
/// This view gives you the ability to iterate over types that are completely unknown to Rust.
/// 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 = (Entity, Vec<DynamicType>);
type IntoIter = DynamicViewIter<'a>;
fn into_iter(self) -> Self::IntoIter {
let archetypes = self.world.archetypes.values().collect();
DynamicViewIter {
world: self.world,
archetypes,
inner: self.inner.into_iter(),
}
}
}
pub struct DynamicViewIter<'a> {
pub world: &'a World,
pub archetypes: Vec<&'a Archetype>,
inner: DynamicViewStateIter,
}
impl<'a> Iterator for DynamicViewIter<'a> {
type Item = (Entity, Vec<DynamicType>);
fn next(&mut self) -> Option<Self::Item> {
self.inner.next(&self.world)
}
}
#[cfg(test)]
mod tests {
use std::{alloc::Layout, ptr::NonNull};
use crate::{World, ComponentInfo, DynTypeId, DynamicBundle, query::dynamic::QueryDynamicType};
use super::DynamicView;
use super::{DynamicView, DynamicViewState};
#[test]
fn single_dynamic_view_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();
world.spawn(dynamic_bundle);
let query = QueryDynamicType::from_info(comp_info);
let mut view = DynamicViewState::new();
view.push(query);
let mut view_iter = view.into_iter();
while let Some((_e, view_row)) = view_iter.next(&world) {
assert_eq!(view_row.len(), 1);
let mut row_iter = view_row.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() {
let comp_layout = Layout::new::<u32>();
let comp_info = ComponentInfo::new_unknown(DynTypeId::Unknown(100), "u32", comp_layout);
let comp_info = ComponentInfo::new_unknown(Some("u32".to_string()), DynTypeId::Unknown(100), comp_layout);
let mut dynamic_bundle = DynamicBundle::default();
let comp = 50u32;
@ -125,10 +224,10 @@ mod tests {
let mut view = DynamicView::new(&world);
view.push(query);
for view_row in view.into_iter() {
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();

View File

@ -0,0 +1,123 @@
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,
pub entity: Entity,
pub queries: Vec<QueryDynamicType>
}
impl<'a> DynamicViewOne<'a> {
pub fn new(world: &'a World, entity: Entity) -> Self {
Self {
world,
entity,
queries: vec![],
}
}
/// Create a new [`DynamicViewOne`] with queries.
pub fn new_with(world: &'a World, entity: Entity, queries: Vec<QueryDynamicType>) -> Self {
Self {
world,
entity,
queries
}
}
pub fn get(self) -> Option<Vec<DynamicType>> {
let arch = self.world.entity_archetype(self.entity)?;
let aid = arch.entity_indexes().get(&self.entity)?;
// get all fetchers for the queries
let mut fetchers: Vec<FetchDynamicTypeUnsafe> = self.queries.iter()
.map(|q| unsafe { q.fetch(self.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

@ -2,21 +2,20 @@ use crate::{archetype::Archetype, World, Entity};
use super::{Fetch, Query, AsQuery};
pub struct EntitiesFetch {
entities: Vec<Entity>,
pub struct EntitiesFetch<'a> {
archetype_entities: Option<&'a [Entity]>,
}
impl<'a> Fetch<'a> for EntitiesFetch {
impl<'a> Fetch<'a> for EntitiesFetch<'a> {
type Item = Entity;
unsafe fn get_item(&mut self, entity: crate::world::ArchetypeEntityId) -> Self::Item {
let e = *self.entities.get_unchecked(entity.0 as usize);
e
self.archetype_entities.unwrap()[entity.0 as usize]
}
fn dangling() -> Self {
Self {
entities: vec![],
archetype_entities: None,
}
}
}
@ -27,7 +26,7 @@ pub struct Entities;
impl Query for Entities {
type Item<'a> = Entity;
type Fetch<'a> = EntitiesFetch;
type Fetch<'a> = EntitiesFetch<'a>;
fn new() -> Self {
Entities
@ -41,7 +40,7 @@ impl Query for Entities {
unsafe fn fetch<'a>(&self, _world: &'a World, archetype: &'a Archetype, tick: crate::Tick) -> Self::Fetch<'a> {
let _ = tick; // ignore unused warnings
EntitiesFetch {
entities: archetype.entity_ids.keys().cloned().collect::<Vec<Entity>>(),
archetype_entities: Some(&archetype.entities),
}
}
}

View File

@ -0,0 +1,44 @@
use std::marker::PhantomData;
use crate::{query::{AsQuery, Query}, Archetype, Component, DynTypeId, World};
/// 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).
#[derive(Default)]
pub struct Has<C: Component> {
_marker: PhantomData<C>
}
impl<C: Component> Copy for Has<C> {}
impl<C: Component> Clone for Has<C> {
fn clone(&self) -> Self {
Self { _marker: self._marker.clone() }
}
}
impl<C: Component> Query for Has<C> {
type Item<'a> = ();
type Fetch<'a> = ();
fn new() -> Self {
Has {
_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> {
()
}
}
impl<C: Component> AsQuery for Has<C> {
type Query = Self;
}

View File

@ -0,0 +1,8 @@
mod has;
pub use has::*;
mod or;
pub use or::*;
mod not;
pub use not::*;

View File

@ -0,0 +1,44 @@
use crate::{query::{AsQuery, Query}, Archetype, World};
/// A filter query that fetches the inverse of `Q`.
///
/// This means that entities that `Q` fetches are skipped, and entities that
/// `Q` does not fetch are not skipped.
///
/// ```nobuild
/// // Iterate over entities that has a transform, and are not the origin of a `ChildOf` relationship.
/// for (en, pos, _) in world
/// .view::<(Entities, &Transform, Not<Has<RelationOriginComponent<ChildOf>>>)>()
/// .iter()
/// {
/// // ...
/// }
/// ```
#[derive(Default, Copy, Clone)]
pub struct Not<Q: Query> {
query: Q,
}
impl<Q: Query> Query for Not<Q> {
type Item<'a> = ();
type Fetch<'a> = ();
fn new() -> Self {
Not {
query: Q::new(),
}
}
fn can_visit_archetype(&self, archetype: &Archetype) -> bool {
!self.query.can_visit_archetype(archetype)
}
unsafe fn fetch<'a>(&self, _world: &'a World, _: &'a Archetype, _: crate::Tick) -> Self::Fetch<'a> {
()
}
}
impl<Q: Query> AsQuery for Not<Q> {
type Query = Self;
}

View File

@ -0,0 +1,113 @@
use crate::{query::{AsQuery, Fetch, Query}, Archetype, World};
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
}
}
/// A filter query returning when either `Q1` or `Q2` returns.
///
/// This checks if `Q1` can fetch before checking `Q2`.
///
/// ```nobuild
/// for (en, pos, _) in world
/// .view::<(Entities, &Transform, Or<Has<Mesh>, Has<Scene>>)>()
/// .iter()
/// {
/// // do some things with the position of the entities
///
/// // now handle do things with the Mesh or Scene that the entity could have
/// if let Some(mesh) = world.view_one::<&Mesh>(en).get() {
/// // do mesh things
/// }
///
/// if let Some(scene) = world.view_one::<&Scene>(en).get() {
/// // do scene things
/// }
/// }
/// ```
#[derive(Default)]
pub struct Or<Q1: AsQuery, Q2: AsQuery> {
left: Q1::Query,
right: Q2::Query,
can_visit_left: bool,
can_visit_right: bool,
}
impl<Q1: AsQuery, Q2: AsQuery> Copy for Or<Q1, Q2> {}
impl<Q1: AsQuery, Q2: AsQuery> Clone for Or<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,
}
}
}
impl<Q1: AsQuery, Q2: AsQuery> Query for Or<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 {
Or {
left: Q1::Query::new(),
right: Q2::Query::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 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
}
}
impl<Q1: AsQuery, Q2: AsQuery> AsQuery for Or<Q1, Q2> {
type Query = Self;
}

View File

@ -27,8 +27,14 @@ mod world;
#[allow(unused_imports)]
pub use world::*;
mod optional;
#[allow(unused_imports)]
pub use optional::*;
pub mod dynamic;
pub mod filter;
/// A [`Fetch`]er implementation gets data out of an archetype.
pub trait Fetch<'a> {
/// The type that this Fetch yields

View File

@ -0,0 +1,76 @@
use crate::{Archetype, World};
use super::{AsQuery, Fetch, Query};
#[derive(Default)]
pub struct OptionalFetcher<'a, Q: AsQuery> {
fetcher: Option<<Q::Query as Query>::Fetch<'a>>,
}
impl<'a, Q: AsQuery> Fetch<'a> for OptionalFetcher<'a, Q> {
type Item = Option<<Q::Query as Query>::Item<'a>>;
fn dangling() -> Self {
unreachable!()
}
unsafe fn get_item(&mut self, entity: crate::ArchetypeEntityId) -> Self::Item {
self.fetcher.as_mut()
.map(|f| f.get_item(entity))
}
fn can_visit_item(&mut self, entity: crate::ArchetypeEntityId) -> bool {
self.fetcher.as_mut()
.map(|f| f.can_visit_item(entity))
.unwrap_or(true)
}
}
#[derive(Default)]
pub struct Optional<Q: AsQuery> {
query: Q::Query,
}
impl<Q: AsQuery> Copy for Optional<Q> { }
impl<Q: AsQuery> Clone for Optional<Q> {
fn clone(&self) -> Self {
Self { query: self.query.clone() }
}
}
impl<Q: AsQuery> Query for Optional<Q> {
type Item<'a> = Option<<Q::Query as Query>::Item<'a>>;
type Fetch<'a> = OptionalFetcher<'a, Q>;
fn new() -> Self {
Optional {
query: Q::Query::new(),
}
}
fn can_visit_archetype(&self, _: &Archetype) -> bool {
true
}
unsafe fn fetch<'a>(&self, world: &'a World, arch: &'a Archetype, tick: crate::Tick) -> Self::Fetch<'a> {
let fetcher = if self.query.can_visit_archetype(arch) {
Some(self.query.fetch(world, arch, tick))
} else {
None
};
OptionalFetcher {
fetcher,
}
}
}
impl<Q: AsQuery> AsQuery for Optional<Q> {
type Query = Self;
}
impl<Q: AsQuery> AsQuery for Option<Q> {
type Query = Optional<Q>;
}

View File

@ -1,4 +1,6 @@
use std::{marker::PhantomData, cell::{Ref, RefMut}};
use std::marker::PhantomData;
use atomic_refcell::{AtomicRef, AtomicRefMut};
use crate::{World, resource::ResourceObject};
@ -9,7 +11,7 @@ pub struct FetchResource<'a, T> {
_phantom: PhantomData<T>,
}
impl<'a, T: 'a + 'static> Fetch<'a> for FetchResource<'a, T> {
impl<'a, T: ResourceObject + 'a> Fetch<'a> for FetchResource<'a, T> {
type Item = Res<'a, T>;
fn dangling() -> Self {
@ -79,7 +81,7 @@ impl<R: ResourceObject> AsQuery for QueryResource<R> {
}
/// A struct used for querying resources from the World.
pub struct Res<'a, T>(pub(crate) Ref<'a, T>);
pub struct Res<'a, T: ResourceObject>(pub(crate) AtomicRef<'a, T>);
impl<'a, T: ResourceObject> std::ops::Deref for Res<'a, T> {
type Target = T;
@ -98,7 +100,7 @@ pub struct FetchResourceMut<'a, T> {
_phantom: PhantomData<T>,
}
impl<'a, T: 'a + 'static> Fetch<'a> for FetchResourceMut<'a, T> {
impl<'a, T: ResourceObject + 'a> Fetch<'a> for FetchResourceMut<'a, T> {
type Item = ResMut<'a, T>;
fn dangling() -> Self {
@ -167,7 +169,7 @@ impl<R: ResourceObject> AsQuery for QueryResourceMut<R> {
}
/// A struct used for querying resources from the World.
pub struct ResMut<'a, T>(pub(crate) RefMut<'a, T>);
pub struct ResMut<'a, T: ResourceObject>(pub(crate) AtomicRefMut<'a, T>);
impl<'a, T: ResourceObject> std::ops::Deref for ResMut<'a, T> {
type Target = T;

View File

@ -90,7 +90,7 @@ where
fn next(&mut self) -> Option<Self::Item> {
loop {
if Q::ALWAYS_FETCHES && F::ALWAYS_FETCHES {
if Q::ALWAYS_FETCHES {
// only fetch this query once.
// fetcher gets set to Some after this `next` call.
if self.fetcher.is_none() {

View File

@ -0,0 +1,8 @@
use super::Relation;
// TODO: Delete child entities when the parent is deleted
pub struct ChildOf;
impl Relation for ChildOf {
}

View File

@ -10,19 +10,20 @@ use crate::lyra_engine;
use crate::World;
mod relates_to;
#[doc(hidden)]
pub use relates_to::*;
mod relate_pair;
#[doc(hidden)]
pub use relate_pair::*;
mod child_of;
pub use child_of::*;
#[allow(unused_variables)]
pub trait Relation: 'static {
/// called when a relation of this type is set on a target
fn relation_add(&self, origin: Entity, target: Entity) { }
fn relation_add(&mut self, world: &mut World, origin: Entity, target: Entity) { }
/// called when a relation is removed
fn relation_remove(&self, origin: Entity, target: Entity) { }
fn relation_remove(&mut self, world: &mut World, origin: Entity, target: Entity) { }
}
/// A component that stores the target of a relation.
@ -31,10 +32,16 @@ pub trait Relation: 'static {
/// entities that the relation targets.
#[derive(Component)]
pub struct RelationOriginComponent<R: Relation> {
pub(crate) relation: R,
pub relation: R,
target: Entity,
}
impl<R: Relation> RelationOriginComponent<R> {
pub fn target(&self) -> Entity {
self.target
}
}
/// A component that stores the origin of a relation.
///
/// This component is on the target of the relation and can be used to find the
@ -79,12 +86,12 @@ impl World {
};
self.insert(target, comp);
let comp = RelationOriginComponent {
let mut comp = RelationOriginComponent {
relation,
target,
};
comp.relation.relation_add(origin, target);
comp.relation.relation_add(self, origin, target);
self.insert(origin, comp);
}
}

View File

@ -1,4 +1,4 @@
use std::{any::{Any, TypeId}, cell::Ref, marker::PhantomData};
use std::{any::TypeId, cell::Ref, marker::PhantomData};
use crate::{query::{AsQuery, Fetch, Query}, Archetype, ComponentColumn, Entity, World};
@ -69,7 +69,7 @@ where
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(self.type_id())
let col = archetype.get_column(TypeId::of::<RelationOriginComponent<R>>())
.expect("You ignored 'can_visit_archetype'!");
FetchRelatePair {
@ -83,8 +83,10 @@ where
/// A query that fetches the origin, and target of a relation of type `R`.
///
/// It provides it as a tuple in the following format: `(origin, relation, target)`.
/// Similar to [`RelatesTo`](super::RelatesTo), you can use [`ViewState::relate_pair`] to get a view that fetches the
/// pair, or unlike [`RelatesTo`](super::RelatesTo), you can do the common procedure of using [`World::view`].
/// Similar to [`RelatesTo`](super::RelatesTo), you can use
/// [`ViewState::relate_pair`](crate::relation::ViewState::relate_pair) to get a view that
/// fetches the pair, or unlike [`RelatesTo`](super::RelatesTo), you can do the common
/// procedure of using [`World::view`].
pub struct RelatePair<R: Relation> {
_marker: PhantomData<R>,
}

View File

@ -1,26 +1,46 @@
use std::{any::{TypeId, Any}, cell::{RefCell, Ref, RefMut}};
use std::{any::{Any, TypeId}, sync::Arc};
use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut};
use crate::{Tick, TickTracker};
/// Shorthand for `Send + Sync + 'static`, so it never needs to be implemented manually.
pub trait ResourceObject: 'static {}
impl<T: 'static> ResourceObject for T {}
pub trait ResourceObject: Send + Sync + Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
impl<T: Send + Sync + Any> ResourceObject for T {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
/// A type erased storage for a Resource.
#[derive(Clone)]
pub struct ResourceData {
pub(crate) data: Box<RefCell<dyn Any>>,
pub(crate) data: Arc<AtomicRefCell<dyn ResourceObject>>,
type_id: TypeId,
// use a tick tracker which has interior mutability
pub(crate) tick: TickTracker,
}
impl ResourceData {
pub fn new<T: Any>(data: T) -> Self {
pub fn new<T: ResourceObject>(data: T, tick: Tick) -> Self {
Self {
data: Box::new(RefCell::new(data)),
data: Arc::new(AtomicRefCell::new(data)),
type_id: TypeId::of::<T>(),
tick: TickTracker::from(*tick),
}
}
/// Returns a boolean indicating whether or not `T`` is of the same type of the Resource
pub fn is<T: 'static>(&self) -> bool {
pub fn is<T: ResourceObject>(&self) -> bool {
self.type_id == TypeId::of::<T>()
}
@ -30,8 +50,8 @@ 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: 'static>(&self) -> Ref<T> {
Ref::map(self.data.borrow(), |a| a.downcast_ref().unwrap())
pub fn get<T: ResourceObject>(&self) -> AtomicRef<T> {
AtomicRef::map(self.data.borrow(), |a| a.as_any().downcast_ref().unwrap())
}
/// Mutably borrow the data inside of the resource.
@ -40,8 +60,8 @@ 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: 'static>(&self) -> RefMut<T> {
RefMut::map(self.data.borrow_mut(), |a| a.downcast_mut().unwrap())
pub fn get_mut<T: ResourceObject>(&self) -> AtomicRefMut<T> {
AtomicRefMut::map(self.data.borrow_mut(), |a| a.as_any_mut().downcast_mut().unwrap())
}
/// Borrow the data inside of the resource.
@ -49,10 +69,9 @@ impl ResourceData {
/// # Panics
///
/// * If the type of `T` is not the same as the resource type.
pub fn try_get<T: 'static>(&self) -> Option<Ref<T>> {
pub fn try_get<T: ResourceObject>(&self) -> Option<AtomicRef<T>> {
self.data.try_borrow()
.map(|r| Ref::map(r, |a| a.downcast_ref().unwrap()))
.map(|r| AtomicRef::map(r, |a| a.as_any().downcast_ref().unwrap()))
.ok()
}
@ -61,9 +80,13 @@ impl ResourceData {
/// # Panics
///
/// * If the type of `T` is not the same as the resource type.
pub fn try_get_mut<T: 'static>(&self) -> Option<RefMut<T>> {
pub fn try_get_mut<T: ResourceObject>(&self) -> Option<AtomicRefMut<T>> {
self.data.try_borrow_mut()
.map(|r| RefMut::map(r, |a| a.downcast_mut().unwrap()))
.map(|r| AtomicRefMut::map(r, |a| a.as_any_mut().downcast_mut().unwrap()))
.ok()
}
pub fn changed(&self, tick: Tick) -> bool {
self.tick.current() >= tick
}
}

View File

@ -1,8 +1,9 @@
use lyra_ecs::World;
use tracing::{debug_span, instrument};
use crate::Access;
use super::{System, Criteria, IntoSystem};
use super::{Criteria, GraphExecutorError, IntoSystem, System};
/// A system that executes a batch of systems in order that they were given.
/// You can optionally add criteria that must pass before the systems are
@ -12,6 +13,7 @@ pub struct BatchedSystem {
systems: Vec<Box<dyn System>>,
criteria: Vec<Box<dyn Criteria>>,
criteria_checks: u32,
did_run: bool,
}
impl BatchedSystem {
@ -46,11 +48,15 @@ impl System for BatchedSystem {
}
}
#[instrument(skip(self, world))]
fn execute(&mut self, world: std::ptr::NonNull<World>) -> anyhow::Result<()> {
let mut can_run = true;
let mut check_again = false;
for criteria in self.criteria.iter_mut() {
let crit_span = debug_span!("criteria");
let _e = crit_span.enter();
match criteria.can_run(world, self.criteria_checks) {
super::CriteriaSchedule::Yes => {},
super::CriteriaSchedule::No => can_run = false,
@ -65,9 +71,26 @@ impl System for BatchedSystem {
}
if can_run {
for system in self.systems.iter_mut() {
system.execute(world)?;
for criteria in self.criteria.iter_mut() {
criteria.modify_world(world);
}
for (idx, system) in self.systems.iter_mut().enumerate() {
let span = debug_span!("batch", system=idx);
let _e = span.enter();
system.execute(world)?;
/* let deferred_span = debug_span!("deferred_exec");
let _e = deferred_span.enter();
if let Err(e) = system.execute_deferred(world)
.map_err(|e| GraphExecutorError::Command(e)) {
return Err(e.into());
} */
}
self.did_run = true;
}
if check_again {
@ -79,9 +102,26 @@ impl System for BatchedSystem {
Ok(())
}
fn execute_deferred(&mut self, _: std::ptr::NonNull<World>) -> anyhow::Result<()> {
todo!()
#[instrument(skip(self, world))]
fn execute_deferred(&mut self, world: std::ptr::NonNull<World>) -> anyhow::Result<()> {
if self.did_run {
for (idx, system) in self.systems.iter_mut().enumerate() {
let span = debug_span!("batch", system=idx);
let _e = span.enter();
system.execute_deferred(world)
.map_err(|e| GraphExecutorError::Command(e))?;
}
for criteria in self.criteria.iter_mut() {
criteria.undo_world_modifications(world);
}
}
self.did_run = false;
Ok(())
}
}

View File

@ -26,13 +26,17 @@ pub trait Criteria {
/// * `world` - The ecs world.
/// * `check_count` - The amount of times the Criteria has been checked this tick.
fn can_run(&mut self, world: NonNull<World>, check_count: u32) -> CriteriaSchedule;
}
impl<F> Criteria for F
where F: FnMut(&mut World, u32) -> CriteriaSchedule
{
fn can_run(&mut self, mut world: NonNull<World>, check_count: u32) -> CriteriaSchedule {
let world_mut = unsafe { world.as_mut() };
self(world_mut, check_count)
}
/// Modify the world after the [`Criteria`] in the system batch allows the systems to run.
///
/// This can be great if this Criteria limits the execution of systems based off of resources.
/// A `FixedTimestep` criteria would use this to replace the [`DeltaTime`] resource in the
/// world to match the timestep time.
fn modify_world(&mut self, world: NonNull<World>);
/// Undo modifications to the world after the systems in the batch have been executed.
///
/// The `FixedTimestep` criteria (see docs for [`Criteria::modify_world`]) uses this
/// to replace the [`DeltaTime`] resource with its original value before it was replaced.
fn undo_world_modifications(&mut self, world: NonNull<World>);
}

View File

@ -1,5 +1,7 @@
use std::{collections::{HashMap, VecDeque, HashSet}, ptr::NonNull};
use tracing::{debug_span, info_span, instrument};
use super::System;
use crate::{World, CommandQueue, Commands};
@ -58,7 +60,9 @@ impl GraphExecutor {
}
/// Executes the systems in the graph
pub fn execute(&mut self, mut world_ptr: NonNull<World>, stop_on_error: bool) -> Result<Vec<GraphExecutorError>, GraphExecutorError> {
#[instrument(skip(self, world_ptr, stop_on_error))]
pub fn execute(&mut self, mut world_ptr: NonNull<World>, stop_on_error: bool)
-> Result<Vec<GraphExecutorError>, GraphExecutorError> {
let mut stack = VecDeque::new();
let mut visited = HashSet::new();
@ -71,6 +75,9 @@ impl GraphExecutor {
while let Some(node) = stack.pop_front() {
let system = self.systems.get_mut(node.as_str()).unwrap();
let span = info_span!("graph_exec", system=system.name.clone());
let _e = span.enter();
if let Err(e) = system.system.execute(world_ptr)
.map_err(|e| GraphExecutorError::SystemError(node, e)) {
if stop_on_error {
@ -81,6 +88,9 @@ impl GraphExecutor {
unimplemented!("Cannot resume topological execution from error"); // TODO: resume topological execution from error
}
let deferred_span = debug_span!("deferred_exec");
let _e = deferred_span.enter();
if let Err(e) = system.system.execute_deferred(world_ptr)
.map_err(|e| GraphExecutorError::Command(e)) {
@ -99,23 +109,15 @@ impl GraphExecutor {
let mut commands = Commands::new(&mut queue, world);
let world = unsafe { world_ptr.as_mut() };
if let Err(e) = commands.execute(world)
.map_err(|e| GraphExecutorError::Command(e)) {
if stop_on_error {
return Err(e);
}
possible_errors.push(e);
unimplemented!("Cannot resume topological execution from error"); // TODO: resume topological execution from error
}
commands.execute(world);
}
}
Ok(possible_errors)
}
fn topological_sort<'a>(&'a self, stack: &mut VecDeque<String>, visited: &mut HashSet<&'a str>, node: &'a GraphSystem) -> Result<(), GraphExecutorError> {
fn topological_sort<'a>(&'a self, stack: &mut VecDeque<String>,
visited: &mut HashSet<&'a str>, node: &'a GraphSystem) -> Result<(), GraphExecutorError> {
if !visited.contains(node.name.as_str()) {
visited.insert(&node.name);

View File

@ -1,6 +1,8 @@
use std::{any::TypeId, cell::{Ref, RefMut}, collections::HashMap, ptr::NonNull};
use std::{any::TypeId, collections::HashMap, ptr::NonNull};
use crate::{archetype::{Archetype, ArchetypeId}, bundle::Bundle, query::{dynamic::DynamicView, AsQuery, Query, ViewState, ViewIter, ViewOne}, resource::ResourceData, ComponentInfo, DynTypeId, Entities, Entity, ResourceObject, Tick, TickTracker};
use atomic_refcell::{AtomicRef, AtomicRefMut};
use crate::{archetype::{Archetype, ArchetypeId}, bundle::Bundle, query::{dynamic::DynamicView, AsQuery, Query, ViewIter, ViewOne, ViewState}, resource::ResourceData, ComponentInfo, DynTypeId, DynamicBundle, Entities, Entity, ResourceObject, Tick, TickTracker};
/// The id of the entity for the Archetype.
///
@ -14,6 +16,7 @@ pub struct Record {
pub index: ArchetypeEntityId,
}
#[derive(Clone)]
pub struct World {
pub(crate) archetypes: HashMap<ArchetypeId, Archetype>,
next_archetype_id: ArchetypeId,
@ -126,51 +129,66 @@ impl World {
}
}
/// Insert a bundle into an existing entity. If the components are already existing on the
/// entity, they will be updated, else the entity will be moved to a different Archetype
/// that can store the entity. That may involve creating a new Archetype.
/// Insert a component bundle into an existing entity.
///
/// If the components are already existing on the entity, they will be updated, else the
/// entity will be moved to a different Archetype that can store the entity. That may
/// involve creating a new Archetype.
pub fn insert<B>(&mut self, entity: Entity, bundle: B)
where
B: Bundle
{
// TODO: If the archetype has a single entity, add a component column for the new
// component instead of moving the entity to a brand new archetype.
// TODO: If the entity already has the components in `bundle`, update the values of the
// components with the bundle.
let tick = self.tick();
let record = self.entities.entity_record(entity).unwrap();
let current_arch = self.archetypes.get(&record.id).unwrap();
let mut col_types: Vec<DynTypeId> = current_arch.columns.iter().map(|c| c.info.type_id()).collect();
let orig_col = col_types.clone();
col_types.extend(bundle.type_ids());
let mut col_infos: Vec<ComponentInfo> = current_arch.columns.iter().map(|c| c.info).collect();
col_infos.extend(bundle.info());
let current_arch_len = current_arch.len();
let col_ptrs: Vec<(NonNull<u8>, ComponentInfo)> = current_arch.columns.iter().map(|c| unsafe { (NonNull::new_unchecked(c.borrow_ptr().as_ptr()), c.info) }).collect();
let mut contains_all = true;
for id in bundle.type_ids() {
contains_all = contains_all && current_arch.get_column(id).is_some();
}
if let Some(arch) = self.archetypes.values_mut().find(|a| a.is_archetype_for(&col_types)) {
let res_index = arch.reserve_one(entity);
if contains_all {
let current_arch = self.archetypes.get_mut(&record.id).unwrap();
let entry_idx = *current_arch.entity_indexes()
.get(&entity).unwrap();
for (col_type, (col_ptr, col_info)) in orig_col.into_iter().zip(col_ptrs.into_iter()) {
unsafe {
let ptr = NonNull::new_unchecked(col_ptr.as_ptr()
.add(res_index.0 as usize * col_info.layout().size()));
let col = arch.get_column_mut(col_type).unwrap();
col.set_at(res_index.0 as _, ptr, tick);
}
}
bundle.take(|data, type_id, _size| {
let col = arch.get_column_mut(type_id).unwrap();
unsafe { col.set_at(res_index.0 as _, data, tick); }
col.len += 1;
bundle.take(|ptr, id, _info| {
let col = current_arch.get_column_mut(id).unwrap();
unsafe { col.set_at(entry_idx.0 as _, ptr, tick) };
});
arch.entity_ids.insert(entity, res_index);
return;
}
// contains the type ids for the old component columns + the ids for the new components
let mut combined_column_types: Vec<DynTypeId> = current_arch.columns.iter().map(|c| c.info.type_id()).collect();
combined_column_types.extend(bundle.type_ids());
// contains the ComponentInfo for the old component columns + the info for the new components
let mut combined_column_infos: Vec<ComponentInfo> = current_arch.columns.iter().map(|c| c.info).collect();
combined_column_infos.extend(bundle.info());
// pointers only for the old columns
let old_columns: Vec<(NonNull<u8>, ComponentInfo)> = current_arch.columns.iter()
.map(|c| unsafe { (NonNull::new_unchecked(c.borrow_ptr().as_ptr()), c.info) })
.collect();
if let Some(arch) = self.archetypes.values_mut().find(|a| a.is_archetype_for(&combined_column_types)) {
let mut dbun = DynamicBundle::new();
// move old entity components into new archetype columns
for (col_ptr, col_info) in old_columns.into_iter() {
unsafe {
let ptr = NonNull::new_unchecked(col_ptr.as_ptr()
.add(record.index.0 as usize * col_info.layout().size()));
dbun.push_unknown(ptr, col_info);
}
}
dbun.push_bundle(bundle);
let res_index = arch.add_entity(entity, dbun, &tick);
arch.ensure_synced();
let new_record = Record {
id: arch.id(),
@ -178,9 +196,28 @@ impl World {
};
self.entities.insert_entity_record(entity, new_record);
} else {
if current_arch_len == 1 {
// if this entity is the only entity for this archetype, add more columns to it
let current_arch = self.archetypes.get_mut(&record.id).unwrap();
current_arch.extend(&tick, vec![bundle]);
return;
}
let new_arch_id = self.next_archetype_id.increment();
let mut archetype = Archetype::from_bundle_info(new_arch_id, col_infos);
let entity_arch_id = archetype.add_entity(entity, bundle, &tick);
let mut archetype = Archetype::from_bundle_info(new_arch_id, combined_column_infos);
let mut dbun = DynamicBundle::new();
for (column_ptr, column_info) in old_columns.into_iter() {
unsafe {
// ptr of component for the entity
let comp_ptr = NonNull::new_unchecked(column_ptr.as_ptr()
.add(record.index.0 as usize * column_info.layout().size()));
dbun.push_unknown(comp_ptr, column_info);
}
}
dbun.push_bundle(bundle);
let entity_arch_id = archetype.add_entity(entity, dbun, &tick);
self.archetypes.insert(new_arch_id, archetype);
@ -194,7 +231,104 @@ impl World {
}
let current_arch = self.archetypes.get_mut(&record.id).unwrap();
current_arch.remove_entity(entity, &tick);
if let Some((en, enar)) = current_arch.remove_entity(entity, &tick) {
let rec = Record {
id: current_arch.id(),
index: enar
};
self.entities.insert_entity_record(en, rec);
}
current_arch.ensure_synced();
}
/// A method used for debugging implementation details of the ECS.
///
/// Here's an example of the output:
/// ```nobuild
/// Entities
/// 1 in archetype 0 at 0
/// 0 in archetype 1 at 0
/// 2 in archetype 0 at 1
/// 3 in archetype 2 at 0
/// Arch 1 -- 1 entities
/// Col 175564825027445222460146453544114453753
/// 0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 128 63 0 0 128 63 0 0 128 63 0 0 128 63 78 86 0 0
/// Col 162279302565774655543278578489329315472
/// 0: 0 0 32 65 0 0 32 65 0 0 32 65 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 128 63 0 0 128 63 0 0 128 63 0 0 128 63 0 0 0 0
/// Col 24291284537013640759061027938209843602
/// 0: 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
/// Arch 2 -- 1 entities
/// Col 175564825027445222460146453544114453753
/// 0: 0 0 0 0 0 0 0 0 0 0 0 0 237 127 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 128 63 0 0 128 63 0 0 128 63 0 0 128 63 78 86 0 0
/// Col 162279302565774655543278578489329315472
/// 0: 0 0 76 66 0 0 170 66 0 0 136 65 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 128 63 0 0 128 63 0 0 128 63 0 0 128 63 0 0 0 0
/// Col 142862377085187052737282554588643015580
/// 0: 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
/// Arch 0 -- 2 entities
/// Col 175564825027445222460146453544114453753
/// 0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 32 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 128 63 0 0 128 63 0 0 128 63 0 0 128 63 0 0 0 0
/// 1: 0 0 0 0 0 0 0 0 0 0 0 0 237 127 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 128 63 0 0 128 63 0 0 128 63 0 0 128 63 78 86 0 0
/// Col 162279302565774655543278578489329315472
/// 0: 0 0 112 65 0 0 112 65 0 0 112 65 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 128 63 0 0 128 63 0 0 128 63 0 0 128 63 0 0 0 0
/// 1: 0 0 27 67 0 0 184 65 0 0 192 64 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 128 63 0 0 128 63 0 0 128 63 0 0 128 63 0 0 0 0
/// Col 142862377085187052737282554588643015580
/// 0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
/// 1: 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
/// Col 24291284537013640759061027938209843602
/// 0: 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
/// 1: 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
/// Arch 3 -- 0 entities
/// Col 175564825027445222460146453544114453753
/// Col 162279302565774655543278578489329315472
/// ```
///
/// This output prints all Entity ids, the archetype they're in, and the index that they're
/// in inside of the archetype. Additionally, the archetypes are printing, including their
/// columns type ids, and the contents of the type ids. This output can be used to debug
/// the contents of entities inside the archetypes.
///
/// Below is a template of the output:
/// ```nobuild
/// Entities
/// %ENTITY_ID% in archetype %ARCHETYPE_ID% at %INDEX%
/// Arch ID -- %ARCHETYPE_LEN% entities
/// %FOR EACH COL%
/// Col COLUMN_COMPONENT_TYPE_ID
/// %FOR EACH ENTITY%
/// %ENTITY_INDEX%: %COMPONENT_BYTES%
/// ```
/// If the template above doesn't help you in understanding the output, read the source code
/// of the function. The source code is pretty simple.
pub fn debug_print_world(&self) {
println!("Entities");
for (en, rec) in &self.entities.arch_index {
println!(" {} in archetype {} at {}", en.0, rec.id.0, rec.index.0);
}
for arch in self.archetypes.values() {
println!("Arch {} -- {} entities", arch.id().0, arch.len());
for col in &arch.columns {
// no clue if doing this is stable, but this is a debug function so :shrug:
let tyid: u128 = unsafe { std::mem::transmute(col.info.type_id().as_rust()) };
println!(" Col {}", tyid);
for en in 0..col.len {
// get the ptr starting at the component
let p = col.borrow_ptr();
let p = unsafe { p.as_ptr().add(en * col.info.layout().size()) };
print!(" {}: ", en);
// print each byte of the component
for i in 0..col.info.layout().size() {
let d = unsafe { *p.add(i) };
print!("{} ", d);
}
println!();
}
}
}
}
pub fn entity_archetype(&self, entity: Entity) -> Option<&Archetype> {
@ -225,6 +359,13 @@ impl World {
v.into_iter()
}
/// View into the world for a set of entities that satisfy the queries.
pub fn filtered_view_iter<Q: AsQuery, F: AsQuery>(&self) -> ViewIter<Q::Query, F::Query> {
let archetypes = self.archetypes.values().collect();
let v = ViewState::new(self, Q::Query::new(), F::Query::new(), archetypes);
v.into_iter()
}
pub fn dynamic_view(&self) -> DynamicView {
DynamicView::new(self)
}
@ -233,54 +374,102 @@ impl World {
ViewOne::new(self, entity.id, T::Query::new())
}
//pub fn view_one(&self, entity: EntityId) ->
pub fn add_resource<T: 'static>(&mut self, data: T) {
self.resources.insert(TypeId::of::<T>(), ResourceData::new(data));
/// Add a resource to the world.
///
/// Ticks the world.
pub fn add_resource<T: ResourceObject>(&mut self, data: T) {
let tick = self.tick();
self.resources.insert(TypeId::of::<T>(), ResourceData::new(data, tick));
}
pub fn add_resource_default<T: Default + 'static>(&mut self) {
self.resources.insert(TypeId::of::<T>(), ResourceData::new(T::default()));
/// Add the default value of a resource.
///
/// Ticks the world.
///
/// > Note: This will replace existing values.
pub fn add_resource_default<T: ResourceObject + Default>(&mut self) {
let tick = self.tick();
self.resources.insert(TypeId::of::<T>(), ResourceData::new(T::default(), tick));
}
/// Add the default value of a resource if it does not already exist.
///
/// Returns a boolean indicating if the resource was added. Ticks the world if the resource
/// was added.
pub fn add_resource_default_if_absent<T: ResourceObject + Default>(&mut self) -> bool {
let id = TypeId::of::<T>();
if !self.resources.contains_key(&id) {
let tick = self.tick();
self.resources.insert(id, ResourceData::new(T::default(), tick));
true
} else {
false
}
}
/// Get a resource from the world, or insert it into the world with the provided
/// `fn` and return it.
pub fn get_resource_or_else<T: 'static, F>(&mut self, f: F) -> RefMut<T>
///
/// Ticks the world.
pub fn get_resource_or_else<T: ResourceObject, F>(&mut self, f: F) -> AtomicRefMut<T>
where
F: Fn() -> T + 'static
{
self.resources.entry(TypeId::of::<T>())
.or_insert_with(|| ResourceData::new(f()))
.get_mut()
let tick = self.tick();
let res = self.resources.entry(TypeId::of::<T>())
.or_insert_with(|| ResourceData::new(f(), tick));
res.tick.tick_to(&tick);
res.get_mut()
}
/// Get a resource from the world, or insert it into the world as its default.
pub fn get_resource_or_default<T: Default + 'static>(&mut self) -> RefMut<T>
///
/// Ticks the world.
pub fn get_resource_or_default<T: ResourceObject + Default>(&mut self) -> AtomicRefMut<T>
{
self.resources.entry(TypeId::of::<T>())
.or_insert_with(|| ResourceData::new(T::default()))
.get_mut()
let tick = self.tick();
let res = self.resources.entry(TypeId::of::<T>())
.or_insert_with(|| ResourceData::new(T::default(), tick));
res.tick.tick_to(&tick);
res.get_mut()
}
/// Gets a resource from the World.
///
/// Will panic if the resource is not in the world. See [`World::try_get_resource`] for
/// a function that returns an option.
pub fn get_resource<T: 'static>(&self) -> Ref<T> {
pub fn get_resource<T: ResourceObject>(&self) -> AtomicRef<T> {
self.resources.get(&TypeId::of::<T>())
.expect(&format!("World is missing resource of type '{}'", std::any::type_name::<T>()))
.get()
}
/// Returns a boolean indicating if the resource changed.
///
/// This will return false if the resource doesn't exist.
pub fn has_resource_changed<T: ResourceObject>(&self) -> bool {
let tick = self.current_tick();
self.resources.get(&TypeId::of::<T>())
.map(|r| r.changed(tick))
.unwrap_or(false)
}
/// Returns the [`Tick`] that the resource was last modified at.
pub fn resource_tick<T: ResourceObject>(&self) -> Option<Tick> {
self.resources.get(&TypeId::of::<T>())
.map(|r| r.tick.current())
}
/// Returns boolean indicating if the World contains a resource of type `T`.
pub fn has_resource<T: 'static>(&self) -> bool {
pub fn has_resource<T: ResourceObject>(&self) -> bool {
self.resources.contains_key(&TypeId::of::<T>())
}
/// Attempts to get a resource from the World.
///
/// Returns `None` if the resource was not found.
pub fn try_get_resource<T: 'static>(&self) -> Option<Ref<T>> {
pub fn try_get_resource<T: ResourceObject>(&self) -> Option<AtomicRef<T>> {
self.resources.get(&TypeId::of::<T>())
.and_then(|r| r.try_get())
}
@ -289,18 +478,32 @@ impl World {
///
/// Will panic if the resource is not in the world. See [`World::try_get_resource_mut`] for
/// a function that returns an option.
pub fn get_resource_mut<T: 'static>(&self) -> RefMut<T> {
self.resources.get(&TypeId::of::<T>())
.expect(&format!("World is missing resource of type '{}'", std::any::type_name::<T>()))
.get_mut()
///
/// Ticks the world.
pub fn get_resource_mut<T: ResourceObject>(&self) -> AtomicRefMut<T> {
self.try_get_resource_mut::<T>()
.unwrap_or_else(|| panic!("World is missing resource of type '{}'", std::any::type_name::<T>()))
}
/// Attempts to get a mutable borrow of a resource from the World.
///
/// Returns `None` if the resource was not found.
pub fn try_get_resource_mut<T: 'static>(&self) -> Option<RefMut<T>> {
/// Returns `None` if the resource was not found. Ticks the world if the resource was found.
pub fn try_get_resource_mut<T: ResourceObject>(&self) -> Option<AtomicRefMut<T>> {
self.resources.get(&TypeId::of::<T>())
.and_then(|r| r.try_get_mut())
.and_then(|r| {
// now that the resource was retrieved, tick the world and the resource
let new_tick = self.tick();
r.tick.tick_to(&new_tick);
r.try_get_mut()
})
}
/// Get the corresponding [`ResourceData`].
///
/// > Note: If you borrow the resource mutably, the world and the resource will not be ticked.
pub fn try_get_resource_data<T: ResourceObject>(&self) -> Option<ResourceData> {
self.resources.get(&TypeId::of::<T>())
.map(|r| r.clone())
}
/// Increments the TickTracker which is used for tracking changes to components.
@ -326,11 +529,19 @@ impl World {
self.resources.get(&TypeId::of::<T>())
.map(|d| unsafe { NonNull::new_unchecked(d.data.as_ptr() as *mut T) })
}
pub fn archetype_count(&self) -> usize {
self.archetypes.len()
}
}
// TODO: Ensure that all non-send resources are only accessible on the main thread.
unsafe impl Send for World {}
unsafe impl Sync for World {}
#[cfg(test)]
mod tests {
use crate::{tests::{Vec2, Vec3}, query::TickOf};
use crate::{query::TickOf, tests::{Vec2, Vec3}, Entity};
use super::World;
@ -404,16 +615,14 @@ mod tests {
#[test]
fn resource_multi_borrow() {
let mut world = World::new();
{
let counter = SimpleCounter(4582);
world.add_resource(counter);
}
let counter = SimpleCounter(4582);
world.add_resource(counter);
// test multiple borrows at the same time
let counter = world.get_resource::<SimpleCounter>();
assert_eq!(counter.0, 4582);
let counter2 = world.get_resource::<SimpleCounter>();
assert_eq!(counter2.0, 4582);
assert_eq!(counter.0, 4582);
assert_eq!(counter2.0, 4582);
}
@ -425,7 +634,7 @@ mod tests {
world.add_resource(counter);
}
// test multiple borrows at the same time
// test that its only possible to get a single mutable borrow
let counter = world.get_resource_mut::<SimpleCounter>();
assert_eq!(counter.0, 4582);
assert!(world.try_get_resource_mut::<SimpleCounter>().is_none());
@ -453,6 +662,44 @@ mod tests {
assert!(world.view_one::<&Vec3>(e).get().is_some())
}
#[test]
fn insert_multiple_times() {
let v2s = &[Vec2::rand(), Vec2::rand(), Vec2::rand()];
let v3s = &[Vec3::rand(), Vec3::rand(), Vec3::rand()];
let mut world = World::new();
let e1 = world.spawn(v2s[0]);
let e2 = world.spawn(v2s[1]);
let e3 = world.spawn(v2s[2]);
println!("Spawned entities");
let ev2 = world.view_one::<&Vec2>(e2).get()
.expect("Failed to find Vec2 and Vec3 on inserted entity!");
assert_eq!(*ev2, v2s[1]);
drop(ev2);
let insert_and_assert = |world: &mut World, e: Entity, v2: Vec2, v3: Vec3| {
println!("inserting entity");
world.insert(e, (v3,));
println!("inserted entity");
let (ev2, ev3) = world.view_one::<(&Vec2, &Vec3)>(e).get()
.expect("Failed to find Vec2 and Vec3 on inserted entity!");
assert_eq!(*ev2, v2);
assert_eq!(*ev3, v3);
};
insert_and_assert(&mut world, e2, v2s[1], v3s[1]);
println!("Entity 2 is good");
insert_and_assert(&mut world, e3, v2s[2], v3s[2]);
println!("Entity 3 is good");
assert_eq!(world.archetypes.len(), 2);
println!("No extra archetypes were created");
insert_and_assert(&mut world, e1, v2s[0], v3s[0]);
println!("Entity 1 is good");
}
#[test]
fn view_one() {
let v = Vec2::rand();
@ -468,11 +715,6 @@ mod tests {
fn view_change_tracking() {
let mut world = World::new();
/* let v = Vec2::rand();
world.spawn((v,));
let v = Vec2::rand();
world.spawn((v,)); */
println!("spawning");
world.spawn((Vec2::new(10.0, 10.0),));
world.spawn((Vec2::new(5.0, 5.0),));
@ -491,4 +733,39 @@ mod tests {
assert!(tick >= world_tick);
}
}
/// Tests replacing components using World::insert
#[test]
fn entity_insert_replace() {
let mut world = World::new();
let first = world.spawn((Vec2::new(10.0, 10.0),));
let second = world.spawn((Vec2::new(5.0, 5.0),));
world.insert(first, Vec2::new(50.0, 50.0));
let pos = world.view_one::<&mut Vec2>(first).get().unwrap();
assert_eq!(*pos, Vec2::new(50.0, 50.0));
drop(pos);
let pos = world.view_one::<&mut Vec2>(second).get().unwrap();
assert_eq!(*pos, Vec2::new(5.0, 5.0));
}
/// Tests resource change checks
#[test]
fn resource_changed() {
let mut world = World::new();
world.add_resource(SimpleCounter(50));
assert!(world.has_resource_changed::<SimpleCounter>());
world.spawn(Vec2::new(50.0, 50.0));
assert!(!world.has_resource_changed::<SimpleCounter>());
let mut counter = world.get_resource_mut::<SimpleCounter>();
counter.0 += 100;
assert!(world.has_resource_changed::<SimpleCounter>());
}
}

View File

@ -4,20 +4,26 @@ version = "0.0.1"
edition = "2021"
[dependencies]
lyra-game-derive = { path = "./lyra-game-derive" }
lyra-resource = { path = "../lyra-resource" }
lyra-ecs = { path = "../lyra-ecs", features = [ "math" ] }
lyra-reflect = { path = "../lyra-reflect", features = [ "math" ] }
lyra-math = { path = "../lyra-math" }
lyra-scene = { path = "../lyra-scene" }
wgsl_preprocessor = { path = "../wgsl-preprocessor" }
winit = "0.28.1"
wgpu = { version = "0.15.1", features = [ "expose-ids"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = [ "tracing-log" ] }
tracing-log = "0.1.3"
tracing-appender = "0.2.2"
wgpu = "0.15.1"
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" ] }
bytemuck = { version = "1.12", features = [ "derive", "min_const_generics" ] }
image = { version = "0.24", default-features = false, features = ["png", "jpeg"] }
anyhow = "1.0"
instant = "0.1"
@ -29,3 +35,12 @@ quote = "1.0.29"
uuid = { version = "1.5.0", features = ["v4", "fast-rng"] }
itertools = "0.11.0"
thiserror = "1.0.56"
unique = "0.9.1"
rustc-hash = "1.1.0"
petgraph = { version = "0.6.5", features = ["matrix_graph"] }
bind_match = "0.1.2"
round_mult = "0.1.3"
fast_poisson = { version = "1.0.0", features = ["single_precision"] }
[features]
tracy = ["dep:tracing-tracy"]

View File

@ -0,0 +1,14 @@
[package]
name = "lyra-game-derive"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0.70"
quote = "1.0.33"
syn = "2.0.41"

View File

@ -0,0 +1,35 @@
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(RenderGraphLabel)]
pub fn derive_render_graph_label(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;
proc_macro::TokenStream::from(quote! {
impl #impl_generics crate::render::graph::RenderGraphLabel for #type_ident #ty_generics #where_clause {
fn rc_clone(&self) -> std::rc::Rc<dyn crate::render::graph::RenderGraphLabel> {
std::rc::Rc::new(self.clone())
}
/* fn as_dyn(&self) -> &dyn crate::render::graph::RenderGraphLabel {
&self
}
fn as_partial_eq(&self) -> &dyn PartialEq<dyn crate::render::graph::RenderGraphLabel> {
self
} */
fn as_label_hash(&self) -> u64 {
let tyid = ::std::any::TypeId::of::<Self>();
let mut s = ::std::hash::DefaultHasher::new();
::std::hash::Hash::hash(&tyid, &mut s);
::std::hash::Hash::hash(self, &mut s);
::std::hash::Hasher::finish(&s)
}
}
})
}

View File

@ -1,12 +1,13 @@
use std::any::Any;
pub trait CastableAny: Send + Sync + 'static {
/// A trait that is implemented for types that are `Send + Sync + 'static`.
pub trait AsSyncAny: Send + Sync + 'static {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
/// Implements this trait for anything that fits the type bounds
impl<T: Send + Sync + 'static> CastableAny for T {
impl<T: Send + Sync + 'static> AsSyncAny for T {
fn as_any(&self) -> &dyn Any {
self
}

View File

@ -4,9 +4,15 @@ use lyra_reflect::Reflect;
use crate::{plugin::Plugin, game::GameStages};
#[derive(Clone, Component, Default, Reflect)]
#[derive(Clone, Copy, Component, Default, Reflect)]
pub struct DeltaTime(f32, #[reflect(skip)] Option<Instant>);
impl From<f32> for DeltaTime {
fn from(value: f32) -> Self {
DeltaTime(value, None)
}
}
impl std::ops::Deref for DeltaTime {
type Target = f32;

View File

@ -1,15 +1,82 @@
use std::{collections::{HashMap, VecDeque}, any::{TypeId, Any}, cell::RefCell};
use std::{any::{Any, TypeId}, collections::{HashMap, VecDeque}, marker::PhantomData, mem::{self, MaybeUninit}, ops::Range};
use crate::{castable_any::CastableAny, plugin::Plugin};
use crate::plugin::Plugin;
pub trait Event: Clone + Send + Sync + 'static {}
impl<T: Clone + Send + Sync + 'static> Event for T {}
pub type Events<T> = VecDeque<T>;
#[derive(Clone)]
struct TypeErasedBuffer {
type_id: TypeId,
item_size: usize,
data: Vec<MaybeUninit<u8>>,
fn_drop_item: fn(item: *mut ()),
}
impl Drop for TypeErasedBuffer {
fn drop(&mut self) {
let range = self.data.as_mut_ptr_range();
let mut current = range.start;
let end = range.end;
while current < end {
// SAFETY: current pointer will either be the start of the buffer, or at the start of
// the next item
(self.fn_drop_item)(current.cast());
// SAFETY: the items are placed right after eachother
current = unsafe { current.add(self.item_size) };
}
// Safety: all of the events were just dropped
unsafe { self.data.set_len(0) };
}
}
impl TypeErasedBuffer {
pub fn new<T: 'static>() -> Self {
Self {
type_id: TypeId::of::<T>(),
item_size: mem::size_of::<T>(),
data: Default::default(),
// dropped immediately after the read
fn_drop_item: |item| unsafe { item.cast::<T>().drop_in_place() },
}
}
pub fn push<T: 'static>(&mut self, value: T) {
debug_assert!(TypeId::of::<T>() == self.type_id, "The type of the buffer does not match the type that was added to it");
// reserve enough bytes to store T
let old_len = self.data.len();
self.data.reserve(mem::size_of::<T>());
// Get a pointer to the end of the data.
// SAFETY: we just reserved enough memory to store the data.
let end_ptr = unsafe { self.data.as_mut_ptr().add(old_len) };
unsafe {
// write the command and its runner into the buffer
end_ptr.cast::<T>()
// written unaligned to keep everything packed
.write_unaligned(value);
// we wrote to the vec's buffer without using its api, so we need manually
// set the length of the vec.
self.data.set_len(old_len + mem::size_of::<T>());
}
}
pub fn len(&self) -> usize {
self.data.len() / self.item_size
}
}
pub struct EventQueue {
events: HashMap<TypeId, RefCell<Box<dyn CastableAny>>>,
event_write_queue: HashMap<TypeId, RefCell<Box<dyn CastableAny>>>
events: HashMap<TypeId, TypeErasedBuffer>,
event_write_queue: HashMap<TypeId, TypeErasedBuffer>,
}
impl Default for EventQueue {
@ -32,7 +99,7 @@ impl EventQueue {
E: Event
{
// the compiler wants me to explicit right here for some reason
let default = || RefCell::new(Box::<Events::<E>>::default() as Box<dyn CastableAny>);
/* let default = || RefCell::new(Box::<Events::<E>>::default() as Box<dyn AsSyncAny>);
// Get, or create, a list of events of this type
let type_id = event.type_id();
@ -42,7 +109,13 @@ impl EventQueue {
// downcast resource as an events storage
let e: &mut Events<E> = events.as_mut().as_any_mut().downcast_mut().unwrap();
e.push_back(event);
e.push_back(event); */
let type_id = event.type_id();
let events = self.event_write_queue.entry(type_id)
.or_insert_with(|| TypeErasedBuffer::new::<E>());
events.push(event);
}
// Clear events, this should happen at the start of every tick since events are cloned
@ -60,13 +133,59 @@ impl EventQueue {
}
}
pub fn read_events<E>(&self) -> Option<Events<E>>
pub fn read_events<E>(&self) -> Option<EventReader<E>>
where
E: Event
{
if let Some(event ) = self.events.get(&TypeId::of::<E>()) {
let eref = event.borrow();
Some(eref.as_ref().as_any().downcast_ref::<Events<E>>().unwrap().clone())
self.events.get(&TypeId::of::<E>())
.map(|event| EventReader::new(event.clone()))
}
}
pub struct EventReader<E: Event> {
buf: TypeErasedBuffer,
range: Option<Range<*mut MaybeUninit<u8>>>,
_marker: PhantomData<E>,
}
impl<E: Event> EventReader<E> {
fn new(buffer: TypeErasedBuffer) -> Self {
Self {
buf: buffer,
range: None,
_marker: PhantomData,
}
}
pub fn len(&self) -> usize {
self.buf.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl<E: Event> Iterator for EventReader<E> {
type Item = E;
fn next(&mut self) -> Option<Self::Item> {
if self.range.is_none() {
self.range = Some(self.buf.data.as_mut_ptr_range());
}
let range = self.range.as_mut().unwrap();
if range.start < range.end {
// Retrieve the event in the buffer.
// SAFETY: current pointer will either be the start of the buffer, or at the start
// of the next event.
let event = unsafe { range.start.cast::<E>().read_unaligned() };
// Advance the pointer to be after this event.
range.start = unsafe { range.start.add(mem::size_of::<E>()) };
Some(event)
} else {
None
}

View File

@ -3,7 +3,7 @@ use std::{sync::Arc, collections::VecDeque, ptr::NonNull};
use async_std::task::block_on;
use lyra_ecs::{World, system::{System, IntoSystem}};
use tracing::{info, error, Level};
use tracing::{error, info, Level};
use tracing_appender::non_blocking;
use tracing_subscriber::{
layer::SubscriberExt,
@ -57,10 +57,12 @@ struct GameLoop {
}
impl GameLoop {
pub async fn new(window: Arc<Window>, world: World, staged_exec: StagedExecutor) -> GameLoop {
pub async fn new(window: Arc<Window>, mut world: World, staged_exec: StagedExecutor) -> Self {
let renderer = BasicRenderer::create_with_window(&mut world, window.clone()).await;
Self {
window: Arc::clone(&window),
renderer: Box::new(BasicRenderer::create_with_window(window).await),
renderer: Box::new(renderer),
world,
staged_exec,
@ -68,7 +70,7 @@ impl GameLoop {
}
pub async fn on_resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
self.renderer.on_resize(new_size);
self.renderer.on_resize(&mut self.world, new_size);
}
pub async fn on_init(&mut self) {
@ -344,14 +346,23 @@ impl Game {
pub async fn run(&mut self) {
// init logging
let (stdout_layer, _stdout_nb) = non_blocking(std::io::stdout());
tracing_subscriber::registry()
.with(fmt::layer().with_writer(stdout_layer))
.with(filter::Targets::new()
// done by prefix, so it includes all lyra subpackages
.with_target("lyra", Level::DEBUG)
.with_target("wgpu", Level::WARN)
.with_default(Level::INFO))
.init();
{
let t = tracing_subscriber::registry()
.with(fmt::layer().with_writer(stdout_layer));
#[cfg(feature = "tracy")]
let t = t.with(tracing_tracy::TracyLayer::default());
t.with(filter::Targets::new()
// done by prefix, so it includes all lyra subpackages
.with_target("lyra", Level::DEBUG)
.with_target("wgsl_preprocessor", Level::DEBUG)
.with_target("wgpu", Level::WARN)
.with_target("winit", Level::DEBUG)
.with_default(Level::INFO))
.init();
}
let world = self.world.take().unwrap_or_default();

View File

@ -6,7 +6,7 @@ use lyra_reflect::Reflect;
use crate::{plugin::Plugin, game::GameStages, EventQueue};
use super::{Button, KeyCode, InputButtons, MouseMotion};
use super::{Button, InputButtons, KeyCode, MouseButton, MouseMotion};
pub trait ActionLabel: Debug {
/// Returns a unique hash of the label.
@ -105,15 +105,6 @@ pub enum GamepadInput {
Axis(GamepadAxis),
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum MouseButton {
Left,
Right,
Middle,
Other(u16),
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum MouseAxis {
@ -530,10 +521,10 @@ fn actions_system(world: &mut World) -> anyhow::Result<()> {
}
}
let motion_avg = if let Some(mut mouse_events) = mouse_events {
let motion_avg = if let Some(mouse_events) = mouse_events {
let count = mouse_events.len();
let mut sum = Vec2::ZERO;
while let Some(mm) = mouse_events.pop_front() {
for mm in mouse_events {
sum += mm.delta;
}
Some(sum / count as f32)

View File

@ -92,12 +92,19 @@ 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 {
// this if statement should always be true, but just in case ;)
ButtonEvent::JustPressed(b) if button == *b => true,
_ => false,
},
Some(button_event) => matches!(button_event, ButtonEvent::JustPressed(b) if button == *b),
None => false
}
}
/// Update any JustPressed events into Pressed events
///
/// 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() {
if let ButtonEvent::JustPressed(btn) = bev {
*bev = ButtonEvent::Pressed(btn.clone());
}
}
}
}

View File

@ -83,7 +83,7 @@ impl Touches {
/// A mouse button event
///
/// Translated `WindowEvent::MouseButton` from `winit` crate
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum MouseButton {
Left,
Right,

View File

@ -102,13 +102,16 @@ impl crate::ecs::system::System for InputSystem {
let queue = world.try_get_resource_mut::<EventQueue>()
.and_then(|q| q.read_events::<InputEvent>());
let mut e = world.get_resource_or_else(InputButtons::<winit::event::VirtualKeyCode>::new);
e.update();
drop(e);
if queue.is_none() {
return Ok(());
}
let mut events = queue.unwrap();
while let Some(event) = events.pop_front() {
let events = queue.unwrap();
for event in events {
self.process_event(world, &event);
}

View File

@ -1,6 +1,7 @@
#![feature(hash_extract_if)]
#![feature(lint_reasons)]
#![feature(trait_alias)]
#![feature(map_many_mut)]
extern crate self as lyra_engine;
@ -8,7 +9,7 @@ pub mod game;
pub mod render;
pub mod resources;
pub mod input;
pub mod castable_any;
pub mod as_any;
pub mod plugin;
pub mod change_tracker;

View File

@ -0,0 +1,297 @@
use std::{alloc::Layout, cmp, marker::PhantomData, mem};
use std::{alloc, ptr};
use unique::Unique;
/// A [`Vec`] with its elements aligned to a runtime alignment value.
pub struct AVec<T> {
buf: Unique<u8>,
cap: usize,
len: usize,
align: usize,
_marker: PhantomData<T>,
}
impl<T> AVec<T> {
// Tiny Vecs are dumb. Skip to:
// - 8 if the element size is 1, because any heap allocators are likely
// to round up a request of less than 8 bytes to at least 8 bytes.
// - 4 if elements are moderate-sized (<= 1 KiB).
// - 1 otherwise, to avoid wasting too much space for very short Vecs.
//
// Taken from Rust's standard library RawVec
pub(crate) const MIN_NON_ZERO_CAP: usize = if mem::size_of::<T>() == 1 {
8
} else if mem::size_of::<T>() <= 1024 {
4
} else {
1
};
#[inline]
pub fn new(alignment: usize) -> Self {
debug_assert!(mem::size_of::<T>() > 0, "ZSTs not yet supported");
Self {
buf: Unique::dangling(),
cap: 0,
len: 0,
align: alignment,
_marker: PhantomData
}
}
/// Constructs a new, empty `AVec` with at least the specified capacity.
///
/// The aligned vector will be able to hold at least `capacity` elements without reallocating.
/// This method may allocate for more elements than `capacity`. If `capacity` is zero,
/// the vector will not allocate.
///
/// # Panics
///
/// Panics if the capacity exceeds `usize::MAX` bytes.
#[inline]
pub fn with_capacity(alignment: usize, capacity: usize) -> Self {
let mut s = Self::new(alignment);
if capacity > 0 {
unsafe {
s.grow_amortized(0, capacity);
}
}
s
}
/// Calculates the size of the 'slot' for a single **aligned** item.
#[inline(always)]
fn slot_size(&self) -> usize {
let a = self.align - 1;
(mem::align_of::<T>() + (a)) & !a
}
/// # Panics
///
/// Panics if the new capacity exceeds `usize::MAX` bytes.
#[inline]
unsafe fn grow_amortized(&mut self, len: usize, additional: usize) {
debug_assert!(additional > 0);
let required_cap = len.checked_add(additional)
.expect("Capacity overflow");
let cap = cmp::max(self.cap * 2, required_cap);
let cap = cmp::max(Self::MIN_NON_ZERO_CAP, cap);
let new_layout = Layout::from_size_align_unchecked(cap * self.slot_size(), self.align);
let ptr = alloc::alloc(new_layout);
self.buf = Unique::new_unchecked(ptr);
self.cap = cap;
}
/// # Panics
///
/// Panics if the new capacity exceeds `usize::MAX` bytes.
#[inline]
unsafe fn grow_exact(&mut self, len: usize, additional: usize) {
debug_assert!(additional > 0);
let cap = len.checked_add(additional)
.expect("Capacity overflow");
let new_layout = Layout::from_size_align_unchecked(cap * self.slot_size(), self.align);
let ptr = alloc::alloc(new_layout);
self.buf = Unique::new_unchecked(ptr);
self.cap = cap;
}
/// Reserves capacity for at least `additional` more elements.
///
/// The collection may reserve more space to speculatively avoid frequent reallocations.
/// After calling `reserve`, capacity will be greater than or equal to
/// `self.len() + additional`. Does nothing if capacity is already sufficient.
///
/// # Panics
///
/// Panics if the new capacity exceeds `usize::MAX` bytes.
#[inline]
pub fn reserve(&mut self, additional: usize) {
debug_assert!(additional > 0);
let remaining = self.capacity().wrapping_sub(self.len);
if additional > remaining {
unsafe { self.grow_amortized(self.len, additional) };
}
}
/// Reserves capacity for `additional` more elements.
///
/// Unlike [`reserve`], this will not over-allocate to speculatively avoid frequent
/// reallocations. After calling `reserve_exact`, capacity will be equal to
/// `self.len() + additional`. Does nothing if the capacity is already sufficient.
///
/// Prefer [`reserve`] if future insertions are expected.
///
/// # Panics
///
/// Panics if the new capacity exceeds `usize::MAX` bytes.
#[inline]
pub fn reserve_exact(&mut self, additional: usize) {
let remaining = self.capacity().wrapping_sub(self.len);
if additional > remaining {
unsafe { self.grow_exact(self.len, additional) };
}
}
/// Appends an element to the back of the collection.
///
/// # Panics
///
/// Panics if the new capacity exceeds `usize::MAX` bytes.
#[inline]
pub fn push(&mut self, val: T) {
if self.len == self.cap {
self.reserve(self.slot_size());
}
unsafe {
// SAFETY: the length is ensured to be less than the capacity.
self.set_at_unchecked(self.len, val);
}
self.len += 1;
}
/// Sets an element at position `idx` within the vector to `val`.
///
/// # Unsafe
///
/// If `self.len > idx`, bytes past the length of the vector will be written to, potentially
/// also writing past the capacity of the vector.
#[inline(always)]
unsafe fn set_at_unchecked(&mut self, idx: usize, val: T) {
let ptr = self.buf
.as_ptr()
.add(idx * self.slot_size());
std::ptr::write(ptr.cast::<T>(), val);
}
/// Sets an element at position `idx` within the vector to `val`.
///
/// # Panics
///
/// Panics if `idx >= self.len`.
#[inline(always)]
pub fn set_at(&mut self, idx: usize, val: T) {
assert!(self.len > idx);
unsafe {
self.set_at_unchecked(idx, val);
}
}
/// Shortens the vector, keeping the first `len` elements and dropping the rest.
///
/// If `len` is greater or equal to the vectors current length, this has no effect.
#[inline]
pub fn truncate(&mut self, len: usize) {
if len > self.len {
return;
}
unsafe {
// drop each element past the new length
for i in len..self.len {
let ptr = self.buf.as_ptr()
.add(i * self.slot_size())
.cast::<T>();
ptr::drop_in_place(ptr);
}
}
self.len = len;
}
#[inline(always)]
pub fn as_ptr(&self) -> *const u8 {
self.buf.as_ptr()
}
#[inline(always)]
pub fn as_mut_ptr(&self) -> *mut u8 {
self.buf.as_ptr()
}
/// Returns the alignment of the elements in the vector.
#[inline(always)]
pub fn align(&self) -> usize {
self.align
}
/// Returns the length of the vector.
#[inline(always)]
pub fn len(&self) -> usize {
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.
#[inline(always)]
pub fn capacity(&self) -> usize {
self.cap
}
}
impl<T: Clone> AVec<T> {
/// Resized the `AVec` in-place so that `len` is equal to `new_len`.
///
/// If `new_len` is greater than `len`, the `AVec` is extended by the difference, and
/// each additional slot is filled with `value`. If `new_len` is less than `len`,
/// the `AVec` will be truncated by to be `new_len`
///
/// This method requires `T` to implement [`Clone`] in order to clone the passed value.
///
/// # Panics
///
/// Panics if the new capacity exceeds `usize::MAX` bytes.
#[inline]
pub fn resize(&mut self, new_len: usize, value: T) {
if new_len > self.len {
self.reserve(new_len - self.len);
unsafe {
let mut ptr = self.buf
.as_ptr().add(self.len * self.slot_size());
// write all elements besides the last one
for _ in 1..new_len {
std::ptr::write(ptr.cast::<T>(), value.clone());
ptr = ptr.add(self.slot_size());
self.len += 1;
}
if new_len > 0 {
// the last element can be written without cloning
std::ptr::write(ptr.cast::<T>(), value.clone());
self.len += 1;
}
self.len = new_len;
}
} else {
self.truncate(new_len);
}
}
}

View File

@ -38,16 +38,43 @@ impl Projection {
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct CameraUniform {
pub view_proj: glam::Mat4,
// vec4 is used because of the uniforms 16 byte spacing requirement
pub view_pos: glam::Vec4,
/// 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,
pub tile_debug: u32,
//_padding: [u8; 3],
}
impl Default for CameraUniform {
fn default() -> Self {
Self {
view_proj: glam::Mat4::IDENTITY,
view_pos: Default::default()
view: glam::Mat4::IDENTITY,
inverse_projection: glam::Mat4::IDENTITY,
view_projection: glam::Mat4::IDENTITY,
projection: glam::Mat4::IDENTITY,
position: Default::default(),
tile_debug: 0,
//_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,
tile_debug: 0
}
}
}
@ -79,26 +106,38 @@ impl RenderCamera {
self.aspect = size.width as f32 / size.height as f32;
}
pub fn update_view_projection(&mut self, camera: &CameraComponent) -> &glam::Mat4 {
/// Calculates the view projection, and the view
///
/// Returns: A tuple with the view projection as the first element, and the
/// view matrix as the second.
pub fn calc_view_projection(&mut self, camera: &CameraComponent) -> CameraUniform {
let position = camera.transform.translation;
let forward = camera.transform.forward();
let up = camera.transform.up();
let view = glam::Mat4::look_to_rh(
position,
forward,
up
);
match camera.mode {
CameraProjectionMode::Perspective => {
let position = camera.transform.translation;
let forward = camera.transform.forward();
let up = camera.transform.up();
let view = glam::Mat4::look_to_rh(
position,
forward,
up
);
let proj = glam::Mat4::perspective_rh_gl(camera.fov.to_radians(), self.aspect, self.znear, self.zfar);
self.view_proj = OPENGL_TO_WGPU_MATRIX * proj * view;
&self.view_proj
//(&self.view_proj, view)
CameraUniform {
view,
inverse_projection: proj.inverse(),
view_projection: self.view_proj,
projection: proj,
position,
tile_debug: camera.debug as u32,
}
},
CameraProjectionMode::Orthographic => {
let position = camera.transform.translation;
let target = camera.transform.rotation * glam::Vec3::new(0.0, 0.0, -1.0);
let target = target.normalize();
@ -111,7 +150,15 @@ impl RenderCamera {
let proj = glam::Mat4::orthographic_rh_gl(-size_x, size_x, -size_y, size_y, self.znear, self.zfar);
self.view_proj = OPENGL_TO_WGPU_MATRIX * proj;
&self.view_proj
CameraUniform {
view,
inverse_projection: proj.inverse(),
view_projection: self.view_proj,
projection: proj,
position,
tile_debug: camera.debug as u32,
}
},
}
}

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, '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, 'a>,
) -> wgpu::RenderPass {
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,572 @@
mod node;
use std::{
cell::{Ref, RefCell, RefMut}, collections::VecDeque, fmt::Debug, hash::Hash, rc::Rc, sync::Arc
};
use lyra_ecs::World;
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::CommandEncoder;
use super::resource::{ComputePipeline, Pass, Pipeline, RenderPipeline};
/// 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>>,
shader_prepoc: wgsl_preprocessor::Processor,
}
impl RenderGraph {
pub fn new(device: Arc<wgpu::Device>, queue: Arc<wgpu::Queue>, view_target: Rc<RefCell<ViewTarget>>) -> Self {
Self {
device,
queue,
slots: Default::default(),
nodes: Default::default(),
sub_graphs: Default::default(),
bind_groups: Default::default(),
node_graph: Default::default(),
view_target,
shader_prepoc: wgsl_preprocessor::Processor::new(),
}
}
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)
}
}
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

@ -0,0 +1,387 @@
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::{Frame, RenderGraph, RenderGraphContext, RenderGraphLabel, RenderGraphLabelValue, RenderTarget};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum NodeType {
/// A node doesn't render, compute, or present anything. This likely means it injects data into the graph.
#[default]
Node,
/// A Compute pass node type.
Compute,
/// A render pass node type.
Render,
/// A node that presents render results to a render target.
Presenter,
/// A node that represents a sub-graph.
Graph,
}
impl NodeType {
/// Returns a boolean indicating if the node should have a [`Pipeline`](crate::render::resource::Pipeline).
pub fn should_have_pipeline(&self) -> bool {
match self {
NodeType::Node => false,
NodeType::Compute => true,
NodeType::Render => true,
NodeType::Presenter => false,
NodeType::Graph => false,
}
}
}
/// The type of data that is stored in a [`Node`] slot.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SlotType {
TextureView,
Sampler,
Texture,
Buffer,
RenderTarget,
Frame,
}
/// The value of a slot in a [`Node`].
#[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(Arc<wgpu::TextureView>),
Sampler(Rc<wgpu::Sampler>),
Texture(Arc<wgpu::Texture>),
Buffer(Arc<wgpu::Buffer>),
RenderTarget(Rc<RefCell<RenderTarget>>),
Frame(Rc<RefCell<Option<Frame>>>),
}
impl SlotValue {
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)
}
pub fn as_sampler(&self) -> Option<&Rc<wgpu::Sampler>> {
bind_match!(self, Self::Sampler(v) => v)
}
pub fn as_texture(&self) -> Option<&Arc<wgpu::Texture>> {
bind_match!(self, Self::Texture(v) => v)
}
pub fn as_buffer(&self) -> Option<&Arc<wgpu::Buffer>> {
bind_match!(self, Self::Buffer(v) => v)
}
pub fn as_render_target(&self) -> Option<Ref<RenderTarget>> {
bind_match!(self, Self::RenderTarget(v) => v.borrow())
}
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 {
/// This slot inputs a value into the node, expecting another node to `Output` it.
Input,
/// This slot outputs a value from the node, providing the value to other nodes that
/// `Input`it.
Output,
}
#[derive(Clone)]
pub struct NodeSlot {
/// The type of the value that this slot inputs/outputs.
pub ty: SlotType,
/// The way this slot uses the value. Defines if this slot is an output or input.
pub attribute: SlotAttribute,
/// The identifying label of this slot.
pub label: RenderGraphLabelValue,
/// The value of the slot.
/// This is `None` if the slot is a `SlotAttribute::Input` type.
pub value: Option<SlotValue>,
}
#[derive(Clone)]
pub struct PipelineShaderDesc {
pub label: Option<String>,
pub source: String,
pub entry_point: String,
}
#[derive(Clone)]
pub struct RenderGraphPipelineInfo {
/// Debug label of the pipeline. This will show up in graphics debuggers for easy identification.
pub label: Option<String>,
/// The layout of bind groups for this pipeline.
pub bind_group_layouts: Vec<Rc<wgpu::BindGroupLayout>>,
/// The descriptor of the vertex shader.
pub vertex: PipelineShaderDesc,
/// The properties of the pipeline at the primitive assembly and rasterization level.
pub primitive: wgpu::PrimitiveState,
/// The effect of draw calls on the depth and stencil aspects of the output target, if any.
pub depth_stencil: Option<wgpu::DepthStencilState>,
/// The multi-sampling properties of the pipeline.
pub multisample: wgpu::MultisampleState,
/// The compiled fragment stage, its entry point, and the color targets.
pub fragment: Option<PipelineShaderDesc>,
/// If the pipeline will be used with a multiview render pass, this indicates how many array
/// layers the attachments will have.
pub multiview: Option<NonZeroU32>,
}
#[allow(clippy::too_many_arguments)]
impl RenderGraphPipelineInfo {
pub fn new(
label: &str,
bind_group_layouts: Vec<wgpu::BindGroupLayout>,
vertex: PipelineShaderDesc,
primitive: wgpu::PrimitiveState,
depth_stencil: Option<wgpu::DepthStencilState>,
multisample: wgpu::MultisampleState,
fragment: Option<PipelineShaderDesc>,
multiview: Option<NonZeroU32>,
) -> Self {
Self {
label: Some(label.to_string()),
bind_group_layouts: bind_group_layouts
.into_iter()
.map(Rc::new)
.collect(),
vertex,
primitive,
depth_stencil,
multisample,
fragment,
multiview,
}
}
}
/// Descriptor of a Node in a [`RenderGraph`].
pub struct NodeDesc {
/// The [`NodeType`] of the node.
pub ty: NodeType,
/// The slots that the Node uses.
/// This defines the resources that the node uses and creates in the graph.
pub slots: Vec<NodeSlot>,
//slot_label_lookup: HashMap<RenderGraphLabelValue, u64>,
/// An optional pipeline descriptor for the Node.
/// This is `None` if the Node type is not a node that requires a pipeline
/// (see [`NodeType::should_have_pipeline`]).
pub pipeline_desc: Option<PipelineDescriptor>,
/// The bind groups that this Node creates.
/// This makes the bind groups accessible to other Nodes.
pub bind_groups: Vec<(
RenderGraphLabelValue,
Arc<wgpu::BindGroup>,
Option<Arc<wgpu::BindGroupLayout>>,
)>,
}
impl NodeDesc {
/// Create a new node descriptor.
pub fn new(
pass_type: NodeType,
pipeline_desc: Option<PipelineDescriptor>,
bind_groups: Vec<(&dyn RenderGraphLabel, Arc<wgpu::BindGroup>, Option<Arc<wgpu::BindGroupLayout>>)>,
) -> Self {
Self {
ty: pass_type,
slots: vec![],
pipeline_desc,
bind_groups: bind_groups
.into_iter()
.map(|bg| (bg.0.rc_clone().into(), bg.1, bg.2))
.collect(),
}
}
/// Add a slot to the descriptor.
///
/// In debug builds, there is an assert that triggers if the slot is an input slot and has
/// a value set.
pub fn add_slot(&mut self, slot: NodeSlot) {
debug_assert!(
!(slot.attribute == SlotAttribute::Input && slot.value.is_some()),
"input slots should not have values"
);
self.slots.push(slot);
}
/// Add a buffer slot to the descriptor.
///
/// In debug builds, there is an assert that triggers if the slot is an input slot and has
/// a value set. There is also an assert that is triggered if this slot value is not `None`,
/// `SlotValue::Lazy` or a `Buffer`.
#[inline(always)]
pub fn add_buffer_slot<L: RenderGraphLabel>(
&mut self,
label: L,
attribute: SlotAttribute,
value: Option<SlotValue>,
) {
debug_assert!(
matches!(value, None | Some(SlotValue::Lazy) | Some(SlotValue::Buffer(_))),
"slot value is not a buffer"
);
let slot = NodeSlot {
label: label.into(),
ty: SlotType::Buffer,
attribute,
value,
};
self.add_slot(slot);
}
/// Add a slot that stores a [`wgpu::Texture`] to the descriptor.
///
/// In debug builds, there is an assert that triggers if the slot is an input slot and has
/// a value set. There is also an assert that is triggered if this slot value is not `None`,
/// `SlotValue::Lazy` or a `SlotValue::Texture`.
#[inline(always)]
pub fn add_texture_slot<L: RenderGraphLabel>(
&mut self,
label: L,
attribute: SlotAttribute,
value: Option<SlotValue>,
) {
debug_assert!(
matches!(value, None | Some(SlotValue::Lazy) | Some(SlotValue::Texture(_))),
"slot value is not a texture"
);
let slot = NodeSlot {
label: label.into(),
ty: SlotType::Texture,
attribute,
value,
};
self.add_slot(slot);
}
/// Add a slot that stores a [`wgpu::TextureView`] to the descriptor.
///
/// In debug builds, there is an assert that triggers if the slot is an input slot and has
/// a value set. There is also an assert that is triggered if this slot value is not `None`,
/// `SlotValue::Lazy` or a `SlotValue::TextureView`.
#[inline(always)]
pub fn add_texture_view_slot<L: RenderGraphLabel>(
&mut self,
label: L,
attribute: SlotAttribute,
value: Option<SlotValue>,
) {
debug_assert!(
matches!(value, None | Some(SlotValue::Lazy) | Some(SlotValue::TextureView(_))),
"slot value is not a texture view"
);
let slot = NodeSlot {
label: label.into(),
ty: SlotType::TextureView,
attribute,
value,
};
self.add_slot(slot);
}
/// Add a slot that stores a [`wgpu::Sampler`] to the descriptor.
///
/// In debug builds, there is an assert that triggers if the slot is an input slot and has
/// a value set. There is also an assert that is triggered if this slot value is not `None`,
/// `SlotValue::Lazy` or a `SlotValue::Sampler`.
#[inline(always)]
pub fn add_sampler_slot<L: RenderGraphLabel>(
&mut self,
label: L,
attribute: SlotAttribute,
value: Option<SlotValue>,
) {
debug_assert!(
matches!(value, None | Some(SlotValue::Lazy) | Some(SlotValue::Sampler(_))),
"slot value is not a sampler"
);
let slot = NodeSlot {
label: label.into(),
ty: SlotType::Sampler,
attribute,
value,
};
self.add_slot(slot);
}
/// Returns all input slots that the descriptor defines.
pub fn input_slots(&self) -> Vec<&NodeSlot> {
self.slots
.iter()
.filter(|s| s.attribute == SlotAttribute::Input)
.collect()
}
/// Returns all output slots that the descriptor defines.
pub fn output_slots(&self) -> Vec<&NodeSlot> {
self.slots
.iter()
.filter(|s| s.attribute == SlotAttribute::Output)
.collect()
}
}
/// A node that can be executed and scheduled in a [`RenderGraph`].
///
/// A node can be used for rendering, computing data on the GPU, collecting data from the main
/// world and writing it to GPU buffers, or presenting renders to a surface.
///
/// The [`RenderGraph`] is ran in phases. The first phase is `prepare`, then `execute`. When a node
/// is first added to a RenderGraph, its [`Node::desc`] function will be ran. The descriptor
/// describes all resources the node requires for execution during the `execute` phase.
pub trait Node: 'static {
/// Retrieve a descriptor of the Node.
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, graph: &mut RenderGraph, world: &mut World, context: &mut RenderGraphContext);
/// Execute the node.
///
/// Parameters:
/// * `graph` - The RenderGraph that this node is a part of. Can be used to get bind groups and bind to them.
/// * `desc` - The descriptor of this node.
/// * `context` - The rendering graph context.
fn execute(
&mut self,
graph: &mut RenderGraph,
desc: &NodeDesc,
context: &mut RenderGraphContext,
);
}

View File

@ -0,0 +1,151 @@
use std::sync::Arc;
use glam::UVec2;
use lyra_game_derive::RenderGraphLabel;
use tracing::warn;
use winit::dpi::PhysicalSize;
use crate::{
render::{
camera::{CameraUniform, RenderCamera},
graph::{
Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext, SlotAttribute, SlotValue
},
render_buffer::BufferWrapper, texture::RenderTexture,
},
scene::CameraComponent,
};
#[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) {
if let Some(camera) = world.view_iter::<&mut CameraComponent>().next() {
let screen_size = graph.view_target().size();
let mut render_cam =
RenderCamera::new(PhysicalSize::new(screen_size.x, screen_size.y));
let uniform = render_cam.calc_view_projection(&camera);
context.queue_buffer_write_with(BasePassSlots::Camera, 0, uniform)
} else {
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, rc::Rc, sync::Arc};
use lyra_game_derive::RenderGraphLabel;
use crate::render::{
graph::{Node, NodeDesc, NodeType},
resource::{FragmentState, PipelineDescriptor, RenderPipelineDescriptor, Shader, 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::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 = Rc::new(Shader {
label: Some("fxaa_shader".into()),
source: include_str!("../../shaders/fxaa.wgsl").to_string(),
});
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: true,
},
})],
depth_stencil_attachment: None,
});
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

@ -0,0 +1,73 @@
use lyra_game_derive::RenderGraphLabel;
use crate::render::{
graph::{
Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext, SlotAttribute, SlotValue
},
light::LightUniformBuffers,
};
#[derive(Debug, Hash, Clone, Default, PartialEq, RenderGraphLabel)]
pub struct LightBasePassLabel;
#[derive(Debug, Hash, Clone, PartialEq, RenderGraphLabel)]
pub enum LightBasePassSlots {
Lights
}
/// Supplies some basic things other passes needs.
///
/// screen size buffer, camera buffer,
#[derive(Default)]
pub struct LightBasePass {
light_buffers: Option<LightUniformBuffers>,
}
impl LightBasePass {
pub fn new() -> Self {
Self::default()
}
}
impl Node for LightBasePass {
fn desc(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
let device = &graph.device;
self.light_buffers = Some(LightUniformBuffers::new(device));
let light_buffers = self.light_buffers.as_ref().unwrap();
let mut desc = NodeDesc::new(
NodeType::Node,
None,
vec![(
&LightBasePassSlots::Lights,
light_buffers.bind_group.clone(),
Some(light_buffers.bind_group_layout.clone()),
)],
);
desc.add_buffer_slot(
LightBasePassSlots::Lights,
SlotAttribute::Output,
Some(SlotValue::Buffer(light_buffers.buffer.clone())),
);
desc
}
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);
}
fn execute(
&mut self,
_graph: &mut crate::render::graph::RenderGraph,
_desc: &crate::render::graph::NodeDesc,
_context: &mut crate::render::graph::RenderGraphContext,
) {
}
}

View File

@ -0,0 +1,271 @@
use std::{mem, rc::Rc, sync::Arc};
use glam::Vec2Swizzles;
use lyra_ecs::World;
use lyra_game_derive::RenderGraphLabel;
use wgpu::util::DeviceExt;
use crate::render::{
graph::{
Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext, SlotAttribute, SlotValue
}, renderer::ScreenSize, resource::{ComputePipeline, ComputePipelineDescriptor, Shader}
};
use super::{BasePassSlots, LightBasePassSlots};
#[derive(Debug, Hash, Clone, Default, PartialEq, RenderGraphLabel)]
pub struct LightCullComputePassLabel;
#[derive(Debug, Hash, Clone, PartialEq, RenderGraphLabel)]
pub enum LightCullComputePassSlots {
LightGridTexture,
LightGridTextureView,
IndexCounterBuffer,
LightIndicesGridGroup,
}
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,
}
}
}
impl Node for LightCullComputePass {
fn desc(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
// initialize some buffers with empty data
let mut contents = Vec::<u8>::new();
let contents_len =
self.workgroup_size.x * self.workgroup_size.y * mem::size_of::<u32>() as u32;
contents.resize(contents_len as _, 0);
let device = graph.device();
let light_indices_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("light_indices_buffer"),
contents: &contents,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
let light_index_counter_buffer =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("light_index_counter_buffer"),
contents: bytemuck::cast_slice(&[0]),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
let light_indices_bg_layout = Arc::new(device.create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::StorageTexture {
access: wgpu::StorageTextureAccess::ReadWrite,
format: wgpu::TextureFormat::Rg32Uint, // vec2<uint>
view_dimension: wgpu::TextureViewDimension::D2,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: Some("light_indices_grid_bgl"),
},
));
let size = wgpu::Extent3d {
width: self.workgroup_size.x,
height: self.workgroup_size.y,
depth_or_array_layers: 1,
};
let grid_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("light_grid_tex"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rg32Uint, // vec2<uint>
usage: wgpu::TextureUsages::STORAGE_BINDING,
view_formats: &[],
});
let grid_texture_view = grid_texture.create_view(&wgpu::TextureViewDescriptor {
label: Some("light_grid_texview"),
format: Some(wgpu::TextureFormat::Rg32Uint), // vec2<uint>
dimension: Some(wgpu::TextureViewDimension::D2),
aspect: wgpu::TextureAspect::All,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: 0,
array_layer_count: None,
});
let light_indices_bg = Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &light_indices_bg_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &light_indices_buffer,
offset: 0,
size: None, // the entire light buffer is needed
}),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&grid_texture_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &light_index_counter_buffer,
offset: 0,
size: None, // the entire light buffer is needed
}),
},
],
label: Some("light_indices_grid_bind_group"),
}));
//drop(main_rt);
/* 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 mut desc = NodeDesc::new(
NodeType::Compute,
/* Some(PipelineDescriptor::Compute(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(),
})), */
None,
vec![(
&LightCullComputePassSlots::LightIndicesGridGroup,
light_indices_bg,
Some(light_indices_bg_layout),
)],
);
desc.add_buffer_slot(
BasePassSlots::ScreenSize,
SlotAttribute::Input,
None,
);
desc.add_buffer_slot(BasePassSlots::Camera, SlotAttribute::Input, None);
desc.add_buffer_slot(
LightCullComputePassSlots::IndexCounterBuffer,
SlotAttribute::Output,
Some(SlotValue::Buffer(Arc::new(light_index_counter_buffer))),
);
desc
}
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>();
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 = Rc::new(Shader {
label: Some("light_cull_comp_shader".into()),
source: include_str!("../../shaders/light_cull.comp.wgsl").to_string(),
});
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(),
});
self.pipeline = Some(pipeline);
}
}
fn execute(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
_: &crate::render::graph::NodeDesc,
context: &mut RenderGraphContext,
) {
let pipeline = self.pipeline.as_mut().unwrap();
let mut pass = context.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("light_cull_pass"),
});
pass.set_pipeline(pipeline);
graph.set_bind_groups(
&mut pass,
&[
(&BasePassSlots::DepthTexture, 0),
(&BasePassSlots::Camera, 1),
(&LightBasePassSlots::Lights, 2),
(&LightCullComputePassSlots::LightIndicesGridGroup, 3),
(&BasePassSlots::ScreenSize, 4),
],
);
pass.dispatch_workgroups(self.workgroup_size.x, self.workgroup_size.y, 1);
}
}

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