diff --git a/source/Vignette/Content/ContentManager.cs b/source/Vignette/Content/ContentManager.cs new file mode 100644 index 0000000..cb3f855 --- /dev/null +++ b/source/Vignette/Content/ContentManager.cs @@ -0,0 +1,153 @@ +// Copyright (c) Cosyne +// Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Sekai.Storages; + +namespace Vignette.Content; + +/// +/// Manages content. +/// +public sealed class ContentManager +{ + private readonly Storage storage; + private readonly HashSet extensions = new(); + private readonly HashSet loaders = new(); + private readonly Dictionary content = new(); + + internal ContentManager(Storage storage) + { + this.storage = storage; + } + + /// + /// Reads a file from and loads it as . + /// + /// The type of content to load. + /// The path to the content. + /// The loaded content. + /// Thrown when invalid path has been passed. + public T Load([StringSyntax(StringSyntaxAttribute.Uri)] string path) + where T : class + { + string ext = Path.GetExtension(path); + + if (string.IsNullOrEmpty(ext)) + { + throw new ArgumentException($"Failed to determine file extension.", nameof(path)); + } + + if (!extensions.Contains(ext)) + { + throw new ArgumentException($"Cannot load unsupported file extension \"{ext}\"."); + } + + var key = new ContentKey(typeof(T), path); + + if (!content.TryGetValue(key, out var weak)) + { + weak = new WeakReference(null); + content.Add(key, weak); + } + + if (!weak.IsAlive) + { + weak.Target = Load(storage.Open(path, FileMode.Open, FileAccess.Read)); + } + + return (T)weak.Target!; + } + + /// + /// Loads a as . + /// + /// The type of content to load. + /// The stream to be read. + /// The loaded content. + /// Thrown when the stream can't be read. + public T Load(Stream stream) + where T : class + { + Span buffer = stackalloc byte[(int)stream.Length]; + + if (stream.Read(buffer) <= 0) + { + throw new InvalidOperationException("Failed to read stream."); + } + + return Load((ReadOnlySpan)buffer); + } + + /// + /// Loads a as . + /// + /// The type of content to load. + /// The byte data to be read. + /// The loaded content. + public T Load(byte[] bytes) + where T : class + { + return Load(bytes); + } + + /// + /// Loads a as . + /// + /// The type of content to load. + /// The byte data to be read. + /// The loaded content. + /// Thrown when no loader was able to load the content. + public T Load(ReadOnlySpan bytes) + where T : class + { + var result = default(T); + + foreach (var loader in loaders) + { + if (loader is not IContentLoader typedLoader) + { + continue; + } + + try + { + result = typedLoader.Load(bytes); + break; + } + catch + { + } + } + + if (result is null) + { + throw new InvalidOperationException("Failed to load content."); + } + + return result; + } + + /// + /// Adds a content loader to the content manager. + /// + /// The content loader to add. + /// The file extensions supported by this loader. + /// Thrown when + internal void Add(IContentLoader loader, params string[] extensions) + { + foreach (string extension in extensions) + { + string ext = extension.StartsWith(ext_separator) ? extension : ext_separator + extension; + this.loaders.Add(loader); + this.extensions.Add(ext); + } + } + + private const char ext_separator = '.'; + + private readonly record struct ContentKey(Type Type, string Path); +} diff --git a/source/Vignette/Content/IContentLoader.cs b/source/Vignette/Content/IContentLoader.cs new file mode 100644 index 0000000..9a73a15 --- /dev/null +++ b/source/Vignette/Content/IContentLoader.cs @@ -0,0 +1,28 @@ +// Copyright (c) Cosyne +// Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. + +using System; + +namespace Vignette.Content; + +/// +/// Defines a mechanism for loading content. +/// +public interface IContentLoader +{ +} + +/// +/// Defines a mechanism for loading . +/// +/// The type of content it loads. +public interface IContentLoader : IContentLoader + where T : class +{ + /// + /// Loads a as . + /// + /// The byte data to be read. + /// The loaded content. + T Load(ReadOnlySpan bytes); +} diff --git a/source/Vignette/Content/ShaderLoader.cs b/source/Vignette/Content/ShaderLoader.cs new file mode 100644 index 0000000..d075a66 --- /dev/null +++ b/source/Vignette/Content/ShaderLoader.cs @@ -0,0 +1,16 @@ +// Copyright (c) Cosyne +// Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. + +using System; +using System.Text; +using Vignette.Graphics; + +namespace Vignette.Content; + +internal sealed class ShaderLoader : IContentLoader +{ + public ShaderMaterial Load(ReadOnlySpan bytes) + { + return ShaderMaterial.Create(Encoding.UTF8.GetString(bytes)); + } +} diff --git a/source/Vignette/Content/TextureLoader.cs b/source/Vignette/Content/TextureLoader.cs new file mode 100644 index 0000000..c0b4268 --- /dev/null +++ b/source/Vignette/Content/TextureLoader.cs @@ -0,0 +1,37 @@ +// Copyright (c) Cosyne +// Licensed under GPL 3.0 with SDK Exception. See LICENSE for details. + +using System; +using Sekai.Graphics; +using StbiSharp; + +namespace Vignette.Content; + +internal sealed class TextureLoader : IContentLoader +{ + private readonly GraphicsDevice device; + + public TextureLoader(GraphicsDevice device) + { + this.device = device; + } + + public Texture Load(ReadOnlySpan bytes) + { + var image = Stbi.LoadFromMemory(bytes, 4); + + var texture = device.CreateTexture(new TextureDescription + ( + image.Width, + image.Height, + PixelFormat.R8G8B8A8_UNorm, + 1, + 1, + TextureUsage.Resource + )); + + texture.SetData(image.Data, 0, 0, 0, 0, 0, image.Width, image.Height, 0); + + return texture; + } +} diff --git a/source/Vignette/Graphics/UnlitMaterial.cs b/source/Vignette/Graphics/UnlitMaterial.cs index 297b52b..424723b 100644 --- a/source/Vignette/Graphics/UnlitMaterial.cs +++ b/source/Vignette/Graphics/UnlitMaterial.cs @@ -70,6 +70,19 @@ public UnlitMaterial() { } + public UnlitMaterial(Texture texture) + : this(false) + { + Texture = texture; + } + + public UnlitMaterial(Texture texture, Sampler sampler) + : this(false) + { + Texture = texture; + Sampler = sampler; + } + private UnlitMaterial(bool isDefault) { effect = Effect.From(shader, out layout, out properties); diff --git a/source/Vignette/Vignette.csproj b/source/Vignette/Vignette.csproj index 9054d8f..0bd80f1 100644 --- a/source/Vignette/Vignette.csproj +++ b/source/Vignette/Vignette.csproj @@ -7,6 +7,7 @@ + diff --git a/source/Vignette/VignetteGame.cs b/source/Vignette/VignetteGame.cs index 884fc58..8bb3f1b 100644 --- a/source/Vignette/VignetteGame.cs +++ b/source/Vignette/VignetteGame.cs @@ -3,18 +3,34 @@ using System; using Sekai; +using Vignette.Content; using Vignette.Graphics; namespace Vignette; public sealed class VignetteGame : Game { + private Window root = null!; + private Camera camera = null!; private Renderer renderer = null!; - private readonly Window root = new(); + private ContentManager content = null!; + private ServiceLocator services = null!; public override void Load() { + content = new(Storage); + content.Add(new ShaderLoader(), ".hlsl"); + content.Add(new TextureLoader(Graphics), ".png", ".jpg", ".jpeg", ".bmp", ".gif"); + renderer = new(Graphics); + + services = new(); + services.Add(content); + + root = new(services) + { + (camera = new Camera { ProjectionMode = CameraProjectionMode.OrthographicOffCenter }) + }; } public override void Draw() @@ -24,6 +40,7 @@ public override void Draw() public override void Update(TimeSpan elapsed) { + camera.ViewSize = Window.Size; root.Update(elapsed); }