Compare commits

..

No commits in common. "dcb48e9acfa4864228f769928e7fc3ec54164a6f" and "4deed47b3d3c2e958b7a2e7d0eacfd3a4bc22aca" have entirely different histories.

38 changed files with 407 additions and 1100 deletions

580
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
workspace = { members = [ "./guests/rust/common-api", "./guests/rust/witguest"] } workspace = { members = [ "common-api", "witguest"] }
[package] [package]
name = "wittest" name = "wittest"
@ -8,13 +8,13 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
async-trait = "0.1.83" async-trait = "0.1.80"
tokio = { version = "1.41.1", features = ["full"] } tokio = { version = "1.37.0", features = ["full"] }
wasmtime = "26.0.1" wasmtime = "23.0.1"
wasmtime-wasi = "26.0.1" wasmtime-wasi = "23.0.1"
lyra-ecs = { path = "./lyra-engine/crates/lyra-ecs" } lyra-ecs = { path = "./lyra-engine/lyra-ecs" }
slab = "0.4.9" slab = "0.4.9"
thiserror = "2.0.0" thiserror = "1.0.58"
common-api = { path = "./guests/rust/common-api" } common-api = { path = "./common-api" }
anyhow = "1.0.93" anyhow = "1.0.86"

View File

@ -26,7 +26,7 @@ use lyra::api::ecs::{EcsDynamicView, EcsWorld, Entity, WasmTypeId};
impl WasmTypeId { impl WasmTypeId {
pub fn of<T: 'static>() -> Self { pub fn of<T: 'static>() -> Self {
Self { Self {
inner: unsafe { mem::transmute(TypeId::of::<T>()) }, inner: unsafe { mem::transmute(TypeId::of::<T>()) }
} }
} }
} }
@ -74,53 +74,16 @@ impl<'a> World<'a> {
} }
} }
/// Create a new [`Entity`] in the world with a component [`Bundle`].
///
///
/// ```nobuild
/// let new_entity = world.spawn((Vec3::new(10.0, 1.0, 10.0), PlayerMovement::new()));
/// ```
pub fn spawn<B: Bundle>(&self, bundle: B) -> Result<Entity, ComponentSerializationError> { pub fn spawn<B: Bundle>(&self, bundle: B) -> Result<Entity, ComponentSerializationError> {
let infos = B::component_info(); let infos = B::component_info();
let bundle = bundle.to_bytes()?; let bundle = bundle.to_bytes()?;
Ok(self.inner.spawn(&bundle, &infos)) Ok(self.inner.spawn(&bundle, &infos))
} }
/// Insert a bundle into an existing entity.
///
/// If the components already exist 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: Bundle>(&self, en: Entity, bundle: B) -> Result<(), ComponentSerializationError> {
let infos = B::component_info();
let bundle = bundle.to_bytes()?;
Ok(self.inner.insert(en, &bundle, &infos))
}
/// Query components from entities.
///
/// ```rust
/// # use common_api::{math::{Vec3, Vec4}, World};
/// # let world = World::new();
///
/// let view = world.view::<(Vec3, Vec4)>();
/// for row in view {
/// let (en, (p3, p4)) = row.unwrap();
/// println!("Entity: {}", en.id.id);
/// println!(" vec3: {:?}", p3);
/// println!(" vec4: {:?}", p4);
/// }
/// ```
pub fn view<B: Bundle>(&self) -> View<B> { pub fn view<B: Bundle>(&self) -> View<B> {
let infos = B::component_info(); let infos = B::component_info();
View::from(self.inner.view(&infos)) View::from(self.inner.view(&infos))
} }
pub fn view_one<B: Bundle>(&self, en: Entity) -> Option<Result<B, ComponentSerializationError>> {
let infos = B::component_info();
self.inner.view_one(en, &infos)
.map(|bytes| B::from_bytes(bytes))
}
} }
pub struct View<B: Bundle> { pub struct View<B: Bundle> {
@ -132,7 +95,7 @@ impl<B: Bundle> From<EcsDynamicView> for View<B> {
fn from(value: EcsDynamicView) -> Self { fn from(value: EcsDynamicView) -> Self {
Self { Self {
inner: value, inner: value,
_marker: PhantomData, _marker: PhantomData
} }
} }
} }
@ -148,11 +111,11 @@ impl<B: Bundle> View<B> {
} }
impl<B: Bundle> Iterator for View<B> { impl<B: Bundle> Iterator for View<B> {
type Item = Result<(Entity, B), ComponentSerializationError>; type Item = Result<B, ComponentSerializationError>;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
if let Some((en, row)) = self.inner.next() { if let Some(row) = self.inner.next() {
Some(B::from_bytes(row).map(|b| (en, b))) Some(B::from_bytes(row))
} else { } else {
None None
} }

View File

@ -34,7 +34,7 @@ interface ecs {
/// Get the bytes of the next row in the view. /// Get the bytes of the next row in the view.
/// ///
/// A row contains multiple component serialized as bytes. The buffer is tighly packed. /// A row contains multiple component serialized as bytes. The buffer is tighly packed.
next: func() -> option<tuple<entity, list<u8>>>; next: func() -> option<list<u8>>;
} }
resource ecs-world { resource ecs-world {
@ -49,33 +49,14 @@ interface ecs {
/// and specify the layouts of them. /// and specify the layouts of them.
spawn: func(components: list<u8>, component-infos: list<component-info>) -> entity; spawn: func(components: list<u8>, component-infos: list<component-info>) -> entity;
/// Insert components into an existing entity.
///
/// Parameters:
/// * `en`: The entity to insert into.
/// * `components`: A tightly packed byte buffer containing the components to spawn
/// with the entity. This expects the components in the same order of `component-infos`.
/// * `component-infos`: A list of `component-infos` uses to identify the components
/// and specify the layouts of them.
insert: func(en: entity, components: list<u8>, component-infos: list<component-info>);
/// Query for a list of entities and their components. /// Query for a list of entities and their components.
/// ///
/// Parameters: /// Parameters:
/// * `component-infos`: The `component-info`'s of the components that you are querying. /// * `component-infos`: The `component-info`'s of the components that you are querying.
/// ///
/// Returns: an iterator that returns the byte buffers of each row. /// Returns: an iterator that returns the byte buffers of each row.
view: func(component-infos: list<component-info>) -> ecs-dynamic-view; view: func(component-infos: list<component-info>) -> ecs-dynamic-view;
/// Query for components from a single entity.
///
/// Parameters:
/// * `en`: The entity to query components from.
/// * `component-infos`: The `component-info`'s of the components that you are querying.
///
/// Returns: A row of components serialized as bytes. The buffer is tighly packed.
view-one: func(en: entity, component-infos: list<component-info>) -> option<list<u8>>;
//with_system: func(stage: string, component-infos: list<component-info>, system: func(components: list<u8>)); //with_system: func(stage: string, component-infos: list<component-info>, system: func(components: list<u8>));
} }
} }

View File

@ -1,26 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-guest-test", "guests/csharp/dotnet-guest-test\dotnet-guest-test.csproj", "{68F96E6F-472F-409E-B36E-9C5E7206CCDE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A4003DF0-C8FF-4A50-A66C-9777E900ED21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4003DF0-C8FF-4A50-A66C-9777E900ED21}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4003DF0-C8FF-4A50-A66C-9777E900ED21}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4003DF0-C8FF-4A50-A66C-9777E900ED21}.Release|Any CPU.Build.0 = Release|Any CPU
{68F96E6F-472F-409E-B36E-9C5E7206CCDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{68F96E6F-472F-409E-B36E-9C5E7206CCDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{68F96E6F-472F-409E-B36E-9C5E7206CCDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{68F96E6F-472F-409E-B36E-9C5E7206CCDE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -1,2 +0,0 @@
bin
obj

View File

@ -1,56 +0,0 @@
namespace ExampleWorld;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ExampleWorld.wit.imports.lyra.api;
using LyraApi;
using LyraApi.Ecs;
[StructLayout(LayoutKind.Sequential)]
struct Vec3(float x, float y, float z) : IComponent
{
/// <summary>
/// The X component.
/// </summary>
public float X { get; set; } = x;
/// <summary>
/// The Y component
/// </summary>
public float Y { get; set; } = y;
/// <summary>
/// The Z component
/// </summary>
public float Z { get; set; } = z;
public static string HostName => "Vec3";
public static ulong HostSize => (ulong)Marshal.SizeOf<Vec3>();
public static ulong HostAlignment => (ulong)MarshalUtils.AlignmentOf<Vec3>();
public static ulong TypeId => 4124409524;
public static object? TakeFromBytes(byte[] bytes)
{
byte[] taken = bytes.Take((int)HostSize).ToArray();
return MarshalUtils.FromBytes<Vec3>(taken);
}
}
public class ExampleWorldImpl : IExampleWorld
{
public static void OnInit(IEcs.EcsWorld gameWorld, IEcs.Entity owningEntity)
{
var world = new World(gameWorld);
Entity entity = world.Spawn(new Vec3(7.0f, 30.0f, 18.0f));
Console.WriteLine("C#: Spawned entity with id {0}", entity.Id);
foreach ((Entity en, Vec3 comp) in world.View<Vec3>())
{
Console.WriteLine("C#: Found entity at ({0}, {1}, {2})", comp.X, comp.Y, comp.Z);
}
}
public static void OnUpdate(IEcs.EcsWorld gameWorld, IEcs.Entity owningEntity)
{
throw new NotImplementedException();
}
}

View File

@ -1,21 +0,0 @@
using System.Reflection;
using ExampleWorld.wit.imports.lyra.api;
using LyraApi.Ecs;
public class Component(string hostName, ulong hostSize, ulong hostAlignment, ulong typeId)
{
public string HostName { get; } = hostName;
public ulong HostSize { get; } = hostSize;
public ulong HostAlignment { get; } = hostAlignment;
public ulong TypeId { get; } = typeId;
public static Component FromComponent<T>() where T : IComponent
{
return new Component(T.HostName, T.HostSize, T.HostAlignment, T.TypeId);
}
public ComponentInfo GetComponentInfo()
{
return new ComponentInfo(HostName, HostSize, HostAlignment, TypeId);
}
}

View File

@ -1,31 +0,0 @@
using ExampleWorld.wit.imports.lyra.api;
namespace LyraApi.Ecs;
public readonly struct ComponentInfo
{
internal readonly IEcs.ComponentInfo info;
public readonly string? HostName => info.hostName;
public readonly ulong Size => info.size;
public readonly ulong Alignment => info.alignment;
public readonly ulong TypeId => info.typeId.inner.Item1;
public ComponentInfo(IEcs.ComponentInfo info)
{
this.info = info;
}
public ComponentInfo(string hostName, ulong size, ulong alignment, ulong typeId)
{
var wasmTypeId = new IEcs.WasmTypeId((typeId, 0));
this.info = new IEcs.ComponentInfo(hostName, size, alignment, wasmTypeId);
}
public static ComponentInfo FromType<T>() where T : IComponent
{
var typeId = new IEcs.WasmTypeId((T.TypeId, 0));
var info = new IEcs.ComponentInfo(T.HostName, T.HostSize, T.HostAlignment, typeId);
return new ComponentInfo(info);
}
}

View File

@ -1,11 +0,0 @@
namespace LyraApi.Ecs;
using ExampleWorld.wit.imports.lyra.api;
public class Entity(IEcs.Entity entity)
{
internal IEcs.Entity Inner { get; set; } = entity;
public ulong Id { get => Inner.id.id; }
public ulong Generation { get => Inner.generation; }
}

View File

@ -1,14 +0,0 @@
using System.Data.SqlTypes;
using ExampleWorld.wit.imports.lyra.api;
namespace LyraApi.Ecs;
public interface IComponent
{
public abstract static string HostName { get; }
public abstract static ulong HostSize { get; }
public abstract static ulong HostAlignment { get; }
public abstract static ulong TypeId { get; }
public abstract static object? TakeFromBytes(byte[] bytes);
}

View File

@ -1,20 +0,0 @@
using System.ComponentModel;
using ExampleWorld;
namespace LyraApi.Ecs;
public class View
{
internal List<ComponentInfo> infos;
private View(List<ComponentInfo> infos)
{
this.infos = infos;
}
public static View Create<T1>() where T1 : IComponent
{
List<ComponentInfo> infos = [ComponentInfo.FromType<T1>()];
return new View(infos);
}
}

View File

@ -1,32 +0,0 @@
using System.Runtime.InteropServices.Swift;
using ExampleWorld.wit.imports.lyra.api;
namespace LyraApi.Ecs;
public class ViewResult
{
private List<Component> components;
private IEcs.EcsDynamicView inner;
internal ViewResult(List<Component> components, IEcs.EcsDynamicView inner)
{
this.components = components;
this.inner = inner;
}
public IEnumerable<(Entity, T1)> Get<T1>() where T1 : IComponent
{
(IEcs.Entity, byte[])? row = inner.Next();
if (row is (var entity, byte[] bytes))
{
byte[] compBytes = bytes.Take((int)T1.HostSize).ToArray();
var t1 = (T1?)T1.TakeFromBytes(compBytes);
if (t1 != null)
{
yield return (new Entity(entity), t1);
}
}
}
}

View File

@ -1,30 +0,0 @@
namespace LyraApi.Ecs;
using ExampleWorld.wit.imports.lyra.api;
using LyraApi;
public class World(IEcs.EcsWorld world)
{
private IEcs.EcsWorld Inner { get; set; } = world;
private Entity Spawn(List<IEcs.ComponentInfo> infos, params object[] comps)
{
byte[] bytes = comps.SelectMany(c => MarshalUtils.GetBytes(c)).ToArray();
return new Entity(Inner.Spawn(bytes, infos));
}
public Entity Spawn<T1>(T1 c1) where T1 : IComponent
{
List<IEcs.ComponentInfo> infos = [ComponentInfo.FromType<T1>().info];
return Spawn(infos, c1);
}
public IEnumerable<(Entity, T1)> View<T1>() where T1 : IComponent
{
List<Component> comps = [Component.FromComponent<T1>()];
List<IEcs.ComponentInfo> infos = comps.Select(c => c.GetComponentInfo().info).ToList();
IEcs.EcsDynamicView dynamicView = Inner.View(infos);
return new ViewResult(comps, dynamicView).Get<T1>();
}
}

View File

@ -1,55 +0,0 @@
namespace LyraApi;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
public static class MarshalUtils
{
internal struct AlignmentHelper<T> where T : unmanaged
{
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value 0
public byte Padding;
public T Target;
#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value 0
}
public static int AlignmentOf<T>() where T : unmanaged
{
return (int)Marshal.OffsetOf<AlignmentHelper<T>>(nameof(AlignmentHelper<T>.Target));
}
public static byte[] GetBytes<T>([DisallowNull] T data)
{
int size = Marshal.SizeOf(data);
byte[] arr = new byte[size];
IntPtr ptr = IntPtr.Zero;
try
{
ptr = Marshal.AllocHGlobal(size);
Marshal.StructureToPtr(data, ptr, true);
Marshal.Copy(ptr, arr, 0, size);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
return arr;
}
public static T? FromBytes<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(byte[] data)
{
var ptr = GCHandle.Alloc(data, GCHandleType.Pinned);
try
{
//var res = (T?)Marshal.PtrToStructure(ptr.AddrOfPinnedObject(), typeof(T));
var res = Marshal.PtrToStructure<T>(ptr.AddrOfPinnedObject());
return res;
}
finally
{
ptr.Free();
}
}
}

View File

@ -1,58 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
<UseAppHost>false</UseAppHost>
<PublishTrimmed>true</PublishTrimmed>
<InvariantGlobalization>true</InvariantGlobalization>
<SelfContained>true</SelfContained>
<IlcExportUnmanagedEntrypoints>true</IlcExportUnmanagedEntrypoints>
<!-- Ignore warning of multiple `PackageReference` items for the `ILCompiler.LLVM` package. -->
<NoWarn>$(NoWarn);NU1504</NoWarn>
</PropertyGroup>
<!-- Copy WIT files from LyraApi to lyra-api wit folder. -->
<!-- <Target Name="CopyFolderOnBuild" BeforeTargets="WitCompile_InvokeTool">
<ItemGroup>
<MyFiles Include="..\LyraApi\wit\**\*.wit" />
</ItemGroup>
<Copy SourceFiles="@(MyFiles)" DestinationFolder="wit\deps\lyraapi\%(RecursiveDir)" />
</Target> -->
<!-- Remove bindgen of LyraApi WIT .-->
<!-- <Target Name="RemoveBindgenLyraApi" AfterTargets="WitCompile_InvokeTool">
<ItemGroup>
<FilesToDelete Include="obj\Debug\net*\wasi-wasm\wit_bindgen\*.lyra.api.*.cs"/>
</ItemGroup>
<Delete Files="@(FilesToDelete)" />
</Target> -->
<!-- Reuse bindgen from LyraApi package instead of using local bindgen. -->
<!-- <Target Name="UpdateLyraApiReferences" AfterTargets="WitCompile_InvokeTool; RemoveBindgenLyraApi">
<Exec Condition="'$(OS)' == 'Unix'" Command="find obj/Debug -type f -wholename '*wit_bindgen/*.cs' -exec sed -i 's/ExampleWorld\.wit\.imports\.lyra\.api/ImportsWorld.wit.imports.lyra.api/g' {} \;" />
<Exec Condition="'$(OS)' == 'Windows_NT'" Command="powershell -Command &quot;Get-ChildItem -Path 'obj\Debug' -Recurse -Filter '*.cs' ^
| Where-Object { $_.FullName -match 'wit_bindgen' } ^
| ForEach-Object { (Get-Content -Path $_.FullName) -replace 'ExampleWorld\.wit\.imports\.lyra\.api', 'ImportsWorld.wit.imports.lyra.api' ^
| Set-Content -Path $_.FullName }&quot;" />
</Target> -->
<ItemGroup>
<PackageReference Include="BytecodeAlliance.Componentize.DotNet.Wasm.SDK" Version="0.4.0-preview00007" />
<PackageReference Include="runtime.linux-x64.Microsoft.DotNet.ILCompiler.LLVM" Version="10.0.0-alpha.1.24531.4" />
</ItemGroup>
<ItemGroup>
<Wit Remove="**\*.wit" />
<Wit Include="wit" World="example" />
</ItemGroup>
<!-- <ItemGroup>
<ProjectReference Include="..\LyraApi\LyraApi.csproj" />
</ItemGroup> -->
</Project>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
<add key="dotnet-experimental" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json" />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>

View File

@ -1,81 +0,0 @@
package lyra:api;
interface ecs {
record entity-id {
id: u64,
}
record entity {
id: entity-id,
generation: u64,
}
record wasm-type-id {
// represents a u128, can be converted into that with mem::transmute
inner: tuple<u64, u64>,
}
record component-info {
host-name: option<string>,
/// The size of the component in memory.
size: u64,
/// The alignment of the component in memory.
alignment: u64,
/// The type id of the component.
///
/// This must be unique between component types since its used to identify the components
/// in spawning and querying.
type-id: wasm-type-id,
}
resource ecs-dynamic-view {
constructor(wrld: borrow<ecs-world>, component-infos: list<component-info>);
/// Get the bytes of the next row in the view.
///
/// A row contains multiple component serialized as bytes. The buffer is tighly packed.
next: func() -> option<tuple<entity, list<u8>>>;
}
resource ecs-world {
constructor();
/// Spawn an entity.
///
/// Parameters:
/// * `components`: A tightly packed byte buffer containing the components to spawn
/// with the entity. This expects the components in the same order of `component-infos`.
/// * `component-infos`: A list of `component-infos` uses to identify the components
/// and specify the layouts of them.
spawn: func(components: list<u8>, component-infos: list<component-info>) -> entity;
/// Insert components into an existing entity.
///
/// Parameters:
/// * `en`: The entity to insert into.
/// * `components`: A tightly packed byte buffer containing the components to spawn
/// with the entity. This expects the components in the same order of `component-infos`.
/// * `component-infos`: A list of `component-infos` uses to identify the components
/// and specify the layouts of them.
insert: func(en: entity, components: list<u8>, component-infos: list<component-info>);
/// Query for a list of entities and their components.
///
/// Parameters:
/// * `component-infos`: The `component-info`'s of the components that you are querying.
///
/// Returns: an iterator that returns the byte buffers of each row.
view: func(component-infos: list<component-info>) -> ecs-dynamic-view;
/// Query for components from a single entity.
///
/// Parameters:
/// * `en`: The entity to query components from.
/// * `component-infos`: The `component-info`'s of the components that you are querying.
///
/// Returns: A row of components serialized as bytes. The buffer is tighly packed.
view-one: func(en: entity, component-infos: list<component-info>) -> option<list<u8>>;
//with_system: func(stage: string, component-infos: list<component-info>, system: func(components: list<u8>));
}
}

View File

@ -1,5 +0,0 @@
package lyra:api;
world imports {
import ecs;
}

View File

@ -1,20 +0,0 @@
package component:witguest;
interface math {
record vec3 {
x: f32,
y: f32,
z: f32,
}
}
/// An example world for the component to target.
world example {
import math;
use lyra:api/ecs.{ecs-world, entity};
import host-print: func(msg: string);
export on-init: func(game-world: borrow<ecs-world>, owning-entity: entity) -> result;
export on-update: func(game-world: borrow<ecs-world>, owning-entity: entity) -> result;
}

@ -1 +1 @@
Subproject commit 62adcf2b50a458314f573425061530d3dc3d25da Subproject commit 12c8ece4183f117864bac362d181ea5906141c6f

View File

@ -1,2 +0,0 @@
[toolchain]
channel = "nightly"

View File

@ -1,13 +1,12 @@
use std::alloc::Layout; use std::alloc::Layout;
use std::collections::HashMap; use std::borrow::Borrow;
use std::ptr::NonNull; use std::ptr::NonNull;
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use async_trait::async_trait; use async_trait::async_trait;
use common_api::math::Vec3;
use component::witguest::math; use component::witguest::math;
use ecs::query::dynamic::{DynamicViewState, DynamicViewStateIter, QueryDynamicType}; use ecs::query::dynamic::{DynamicViewState, DynamicViewStateIter, QueryDynamicType};
use lyra_ecs::{query::dynamic::DynamicViewOne, DynTypeId, World}; use lyra_ecs::{Component, World};
use lyra_ecs as ecs; use lyra_ecs as ecs;
@ -15,16 +14,50 @@ use slab::Slab;
use thiserror::Error; use thiserror::Error;
use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiView}; use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiView};
use wasmtime::component::Resource as WasmResource; use wasmtime::component::{Resource as WasmResource, ResourceAny};
use wasmtime::component::Val as ComponentVal;
#[allow(unused_imports)] #[allow(unused_imports)]
pub(crate) mod lyra_engine { pub(crate) mod lyra_engine {
pub use lyra_ecs as ecs; pub use lyra_ecs as ecs;
} }
/* mod bindings {
wasmtime::component::bindgen!({
world: "imports",
path: "common-api/wit",
//tracing: true,
async: true,
});
mod component {
wasmtime::component::bindgen!({
world: "example",
path: "witguest/wit",
//tracing: true,
async: true,
with: {
"lyra:api": super::lyra::api,
}
});
}
}
use bindings::lyra::api::ecs as wasm_ecs; */
/* wasmtime::component::bindgen!({
world: "imports",
path: "common-api/wit",
//tracing: true,
async: true,
}); */
wasmtime::component::bindgen!({ wasmtime::component::bindgen!({
world: "example", world: "example",
path: "guests/rust/witguest/wit", path: "witguest/wit",
//tracing: true,
async: true, async: true,
with: { with: {
@ -63,6 +96,14 @@ impl Into<ecs::Entity> for wasm_ecs::Entity {
} }
} }
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Component)]
struct Vec3 {
x: f32,
y: f32,
z: f32,
}
struct WorldEntry { struct WorldEntry {
world: lyra_ecs::World, world: lyra_ecs::World,
/// Indices of dynamic views that are still alive /// Indices of dynamic views that are still alive
@ -131,41 +172,6 @@ impl wasm_ecs::HostEcsWorld for Imports {
.ok_or(WasmError::InvalidResourceHandle("EcsWorld")) .ok_or(WasmError::InvalidResourceHandle("EcsWorld"))
.unwrap(); .unwrap();
let world = &mut world_entry.world; let world = &mut world_entry.world;
let lookup = world.get_resource::<GuestTypeLookup>().unwrap();
// add the components in the tightly packed `components` buffer into the dynamic bundle
let mut bundle = ecs::DynamicBundle::new();
let mut offset = 0;
for info in infos {
// Try to get the Native type's ComponentInfo.
let info = lookup.lookup_info(info.into());
// SAFETY: The components are tightly packed. Adding the offset to the pointer will
// get the next component in the buffer.
let data_ptr = unsafe { NonNull::new_unchecked(components.as_mut_ptr().add(offset)) };
bundle.push_unknown(data_ptr, info.clone().into());
offset += info.layout().size() as usize;
}
drop(lookup);
let en = world.spawn(bundle);
en.into()
}
async fn insert(
&mut self,
this: WasmResource<wasm_ecs::EcsWorld>,
entity: Entity,
mut components: Vec<u8>,
infos: Vec<wasm_ecs::ComponentInfo>,
) {
let world_entry = self
.world_slab
.get_mut(this.rep() as _)
.ok_or(WasmError::InvalidResourceHandle("EcsWorld"))
.unwrap();
let world = &mut world_entry.world;
// add the components in the tightly packed `components` buffer into the dynamic bundle // add the components in the tightly packed `components` buffer into the dynamic bundle
let mut bundle = ecs::DynamicBundle::new(); let mut bundle = ecs::DynamicBundle::new();
@ -179,7 +185,8 @@ impl wasm_ecs::HostEcsWorld for Imports {
offset += info.size as usize; offset += info.size as usize;
} }
world.insert(entity.into(), bundle); let en = world.spawn(bundle);
en.into()
} }
async fn view( async fn view(
@ -190,27 +197,64 @@ impl wasm_ecs::HostEcsWorld for Imports {
<Self as wasm_ecs::HostEcsDynamicView>::new(self, this, infos).await <Self as wasm_ecs::HostEcsDynamicView>::new(self, this, infos).await
} }
async fn view_one( fn drop(
&mut self, &mut self,
this: wasmtime::component::Resource<wasm_ecs::EcsWorld>, this: wasmtime::component::Resource<wasm_ecs::EcsWorld>,
entity: Entity, ) -> wasmtime::Result<()> {
let mut world_entry = self
.world_slab
.try_remove(this.rep() as _)
.ok_or(WasmError::InvalidResourceHandle("EcsWorld"))?;
// make sure that all the world views are now dead
for view in world_entry.views.drain() {
self.world_views_slab.remove(view);
}
Ok(())
}
}
impl math::Host for Imports {}
#[async_trait]
impl wasm_ecs::HostEcsDynamicView for Imports {
async fn new(
&mut self,
world: WasmResource<wasm_ecs::EcsWorld>,
infos: Vec<wasm_ecs::ComponentInfo>, infos: Vec<wasm_ecs::ComponentInfo>,
) -> Option<Vec<u8>> { ) -> wasmtime::component::Resource<wasm_ecs::EcsDynamicView> {
// Create a dynamic view for querying for the components
let mut view = DynamicViewState::new();
for info in infos.clone() {
view.push(QueryDynamicType::from_info(info.into()));
}
let world_rep = world.rep();
let entry = DynamicViewEntry {
world_index: world_rep as _,
view: view.into_iter(),
};
let view_index = self.world_views_slab.insert(entry);
// link the dynamic view to the world that its a part of.
let world_entry = self let world_entry = self
.world_slab .world_slab
.get_mut(this.rep() as _) .get_mut(world.rep() as _)
.ok_or(WasmError::InvalidResourceHandle("EcsWorld")) .ok_or(WasmError::InvalidResourceHandle("EcsWorld"))
.unwrap(); .unwrap();
let world = &mut world_entry.world; world_entry.views.insert(view_index);
WasmResource::new_own(view_index as _)
}
//world.view_one(entity.into()); async fn next(&mut self, view: WasmResource<wasm_ecs::EcsDynamicView>) -> Option<Vec<u8>> {
let queries = infos.into_iter() let view_entry = self.world_views_slab.get_mut(view.rep() as _).unwrap();
.map(|i| QueryDynamicType::from_info(i.into())) let world_entry = self.world_slab.get(view_entry.world_index).unwrap();
.collect(); let view = &mut view_entry.view;
let v = DynamicViewOne::new_with(&world, entity.into(), queries);
if let Some(row) = v.get() { // get the next row in the view and copy the returned components into a tightly packed
if let Some(row) = view.next(&world_entry.world) {
let row_len = row.iter().map(|d| d.info.layout().size()).sum(); let row_len = row.iter().map(|d| d.info.layout().size()).sum();
let mut row_buf = Vec::<u8>::with_capacity(row_len); let mut row_buf = Vec::<u8>::with_capacity(row_len);
@ -241,110 +285,7 @@ impl wasm_ecs::HostEcsWorld for Imports {
} }
} }
fn drop(
async fn drop(
&mut self,
this: wasmtime::component::Resource<EcsWorld>,
) -> wasmtime::Result<()> {
let mut world_entry = self
.world_slab
.try_remove(this.rep() as _)
.ok_or(WasmError::InvalidResourceHandle("EcsWorld"))?;
// make sure that all the world views are now dead
for view in world_entry.views.drain() {
self.world_views_slab.remove(view);
}
Ok(())
}
}
impl math::Host for Imports {}
#[async_trait]
impl wasm_ecs::HostEcsDynamicView for Imports {
async fn new(
&mut self,
world: WasmResource<wasm_ecs::EcsWorld>,
infos: Vec<wasm_ecs::ComponentInfo>,
) -> wasmtime::component::Resource<wasm_ecs::EcsDynamicView> {
// Create the view state
let view = {
let world_entry = self
.world_slab
.get_mut(world.rep() as _)
.ok_or(WasmError::InvalidResourceHandle("EcsWorld"))
.unwrap();
let lookup = world_entry.world.get_resource::<GuestTypeLookup>().unwrap();
// Create a dynamic view for querying for the components
let mut view = DynamicViewState::new();
for info in infos.clone() {
let info = lookup.lookup_info(info.into());
view.push(QueryDynamicType::from_info(info));
}
view
};
let world_rep = world.rep();
let entry = DynamicViewEntry {
world_index: world_rep as _,
view: view.into_iter(),
};
let view_index = self.world_views_slab.insert(entry);
// link the dynamic view to the world that its a part of.
let world_entry = self
.world_slab
.get_mut(world.rep() as _)
.ok_or(WasmError::InvalidResourceHandle("EcsWorld"))
.unwrap();
world_entry.views.insert(view_index);
WasmResource::new_own(view_index as _)
}
async fn next(&mut self, view: WasmResource<wasm_ecs::EcsDynamicView>) -> Option<(Entity, Vec<u8>)> {
let view_entry = self.world_views_slab.get_mut(view.rep() as _).unwrap();
let world_entry = self.world_slab.get(view_entry.world_index).unwrap();
let view = &mut view_entry.view;
// get the next row in the view and copy the returned components into a tightly packed
if let Some((en, row)) = view.next(&world_entry.world) {
let row_len = row.iter().map(|d| d.info.layout().size()).sum();
let mut row_buf = Vec::<u8>::with_capacity(row_len);
let mut offset = 0;
for comp in row {
let comp_size = comp.info.layout().size();
unsafe {
// SAFETY: the vec has the capacity to store this component because it was
// created with Vec::with_capacity.
let dst = row_buf.as_mut_ptr().add(offset);
std::ptr::copy_nonoverlapping(
comp.ptr.as_ptr(),
dst,
comp.info.layout().size(),
);
// make sure to tell the vec that it now has the bytes of this component
row_buf.set_len(row_buf.len() + comp_size);
}
offset += comp_size;
}
Some((en.into(), row_buf))
} else {
None
}
}
async fn drop(
&mut self, &mut self,
this: wasmtime::component::Resource<wasm_ecs::EcsDynamicView>, this: wasmtime::component::Resource<wasm_ecs::EcsDynamicView>,
) -> wasmtime::Result<()> { ) -> wasmtime::Result<()> {
@ -362,32 +303,6 @@ impl wasm_ecs::HostEcsDynamicView for Imports {
} }
} }
#[derive(Default, Clone)]
struct GuestTypeLookup(HashMap<u128, lyra_ecs::ComponentInfo>);
impl GuestTypeLookup {
/// Try to find the native type's component info.
///
/// If the wasm type id does not correspond to a native type's info, the provided `ComponentInfo`
/// will be returned. If info is found, the size and alignment will be verified to match using
/// asserts and the info of the native type will be returned.
fn lookup_info(&self, info: lyra_ecs::ComponentInfo) -> lyra_ecs::ComponentInfo {
match self.0.get(&info.type_id().as_unknown().unwrap()) {
Some(native) => {
let native_align = native.layout().align() as usize;
let native_size = native.layout().size() as usize;
let layout = info.layout();
assert_eq!(native_align, layout.align(), "Native type alignment is different then scripting type alignment!");
assert_eq!(native_size, layout.size(), "Native type size is different then scripting type size!");
native.clone()
},
None => info
}
}
}
#[async_trait] #[async_trait]
impl wasm_ecs::Host for Imports {} impl wasm_ecs::Host for Imports {}
@ -425,17 +340,11 @@ async fn main() -> wasmtime::Result<()> {
); );
// Load the component from disk // Load the component from disk
//let bytes = std::fs::read("target/wasm32-wasip1/debug/witguest-component.wasm")?; let bytes = std::fs::read("target/wasm32-wasip1/debug/witguest-component.wasm")?;
let bytes = std::fs::read("guests/csharp/dotnet-guest-test/bin/Debug/net9.0/wasi-wasm/native/dotnet-guest-test.wasm")?;
let component = wasmtime::component::Component::new(&engine, bytes)?; let component = wasmtime::component::Component::new(&engine, bytes)?;
let (script_en, (world_res_a, world_res_b)) = { let (script_en, (world_res_a, world_res_b)) = {
let mut world = World::new(); let mut world = World::new();
let mut lookup = GuestTypeLookup::default();
lookup.0.insert(4124409524, lyra_ecs::ComponentInfo::new::<Vec3>());
world.add_resource(lookup);
let script_en = world.spawn(()); let script_en = world.spawn(());
let data = store.data_mut(); let data = store.data_mut();
@ -450,41 +359,13 @@ async fn main() -> wasmtime::Result<()> {
}; };
// Instantiate the component // Instantiate the component
println!("RUST: Creating example component"); println!("creating example component");
let instance = linker.instantiate_async(&mut store, &component).await?; let instance = linker.instantiate_async(&mut store, &component).await?;
call_script_stage_async(&mut store, &instance, "init", world_res_a, script_en.into()).await?; call_script_stage_async(&mut store, &instance, "init", world_res_a, script_en.into()).await?;
//call_script_stage_async(&mut store, &instance, "update", world_res_b, script_en.into()).await?; call_script_stage_async(&mut store, &instance, "update", world_res_b, script_en.into()).await?;
println!("RUST: Guest is done");
let rep = world_res_b.rep(); println!("Guest is done");
let w = store.data().world_slab.get(rep as _).unwrap();
let w = &w.world;
println!("RUST: Got {} archetypes", w.archetype_count());
for a in w.archetypes.values() {
println!("RUST: Archetype {}", a.id().0);
for col in &a.columns {
println!("RUST: Column type id: {:?}", col.info.type_id());
if col.info.type_id().is_id(DynTypeId::Unknown(4124409524)) {
println!("RUST: Found C# Vec3");
let pos = unsafe { col.get::<Vec3>(0) };
println!("RUST: Entity 0 pos: {:?}", *pos);
}
}
}
let mut dv = w.dynamic_view();
dv.push(QueryDynamicType::from_info(lyra_ecs::ComponentInfo::new::<Vec3>()));
let iter = dv.into_iter();
for (_, comps) in iter {
let first = comps.first().unwrap();
unsafe {
let v: Vec3 = std::ptr::read(first.ptr.as_ptr() as _);
println!("RUST: Found native Vec3! {:?}", v);
}
}
Ok(()) Ok(())
} }

View File

@ -13,14 +13,9 @@ struct Component;
impl Guest for Component { impl Guest for Component {
fn on_init(world: &EcsWorld, _owning_entity: Entity) -> Result<(), ()> { fn on_init(world: &EcsWorld, _owning_entity: Entity) -> Result<(), ()> {
let world = World::from(world); let world = World::from(world);
let en = world.spawn((Vec3::new(7.0, 30.0, 18.0), Vec4::new(10.0, 332.35, 329.0, 95.0))) let en = world.spawn((Vec3::new(7.0, 30.0, 18.0), Vec4::new(10.0, 332.35, 329.0, 95.0)));
.unwrap();
println!("Spawned {:?}", en); println!("guest spawned {:?}", en);
let pos3 = world.view_one::<Vec3>(en)
.unwrap().unwrap();
println!("Found entity at {pos3:?}");
Ok(()) Ok(())
} }
@ -30,10 +25,9 @@ impl Guest for Component {
let view = world.view::<(Vec3, Vec4)>(); let view = world.view::<(Vec3, Vec4)>();
for pos in view { for pos in view {
let (en, (p3, p4)) = pos.unwrap(); let (p3, p4) = pos.unwrap();
println!("Entity: {}", en.id.id); println!("Retrieved vec3: {:?}", p3);
println!(" vec3: {:?}", p3); println!("Retrieved vec4: {:?}", p4);
println!(" vec4: {:?}", p4);
} }
Ok(()) Ok(())

View File

@ -34,7 +34,7 @@ interface ecs {
/// Get the bytes of the next row in the view. /// Get the bytes of the next row in the view.
/// ///
/// A row contains multiple component serialized as bytes. The buffer is tighly packed. /// A row contains multiple component serialized as bytes. The buffer is tighly packed.
next: func() -> option<tuple<entity, list<u8>>>; next: func() -> option<list<u8>>;
} }
resource ecs-world { resource ecs-world {
@ -49,33 +49,14 @@ interface ecs {
/// and specify the layouts of them. /// and specify the layouts of them.
spawn: func(components: list<u8>, component-infos: list<component-info>) -> entity; spawn: func(components: list<u8>, component-infos: list<component-info>) -> entity;
/// Insert components into an existing entity.
///
/// Parameters:
/// * `en`: The entity to insert into.
/// * `components`: A tightly packed byte buffer containing the components to spawn
/// with the entity. This expects the components in the same order of `component-infos`.
/// * `component-infos`: A list of `component-infos` uses to identify the components
/// and specify the layouts of them.
insert: func(en: entity, components: list<u8>, component-infos: list<component-info>);
/// Query for a list of entities and their components. /// Query for a list of entities and their components.
/// ///
/// Parameters: /// Parameters:
/// * `component-infos`: The `component-info`'s of the components that you are querying. /// * `component-infos`: The `component-info`'s of the components that you are querying.
/// ///
/// Returns: an iterator that returns the byte buffers of each row. /// Returns: an iterator that returns the byte buffers of each row.
view: func(component-infos: list<component-info>) -> ecs-dynamic-view; view: func(component-infos: list<component-info>) -> ecs-dynamic-view;
/// Query for components from a single entity.
///
/// Parameters:
/// * `en`: The entity to query components from.
/// * `component-infos`: The `component-info`'s of the components that you are querying.
///
/// Returns: A row of components serialized as bytes. The buffer is tighly packed.
view-one: func(en: entity, component-infos: list<component-info>) -> option<list<u8>>;
//with_system: func(stage: string, component-infos: list<component-info>, system: func(components: list<u8>)); //with_system: func(stage: string, component-infos: list<component-info>, system: func(components: list<u8>));
} }
} }