using System; using Silk.NET.OpenGL; namespace AcDream.App.Rendering; /// /// Two persistent GL sampler objects (Repeat + ClampToEdge) created once /// per GL context. Renderers the appropriate /// one to a texture unit instead of mutating per-texture /// GL_TEXTURE_WRAP_S/T state — sampler state overrides the /// texture's own wrap parameters, so two renderers can share the same /// texture handle but sample it with different wrap modes safely. /// /// /// Ported from /// references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs:115-132. /// Filter modes match 's upload defaults /// (Linear / Linear, no mipmaps) so binding either sampler doesn't /// change the visual filtering behavior — only the wrap behavior at /// UVs outside [0, 1]. /// /// /// /// Lifetime: created once at GL init, disposed with the GL context. /// Anything that binds a sampler MUST unbind it (BindSampler(unit, 0)) /// before yielding to a renderer that doesn't use samplers, otherwise /// the bound sampler's wrap mode will silently override that renderer's /// per-texture wrap state. /// /// public sealed class SamplerCache : IDisposable { private readonly GL _gl; private readonly ResourceCleanupGroup _resources; /// Sampler with WrapS = WrapT = Repeat. The default for textures uploaded by . public uint Wrap { get; } /// Sampler with WrapS = WrapT = ClampToEdge. Used by sky meshes whose authored UVs are strictly in [0, 1] to avoid bilinear-filter bleed at seam edges. public uint Clamp { get; } public SamplerCache(GL gl) { _gl = gl ?? throw new ArgumentNullException(nameof(gl)); var resources = new ResourceCleanupGroup(); uint wrap = 0; uint clamp = 0; try { wrap = GlResourceCommand.CreateName( _gl, "repeat sampler", _gl.GenSampler, _gl.DeleteSampler); uint ownedWrap = wrap; resources.Add( "repeat sampler", () => GlResourceCommand.Execute( _gl, $"delete repeat sampler {ownedWrap}", () => _gl.DeleteSampler(ownedWrap))); Configure(wrap, TextureWrapMode.Repeat, "repeat sampler"); clamp = GlResourceCommand.CreateName( _gl, "clamp sampler", _gl.GenSampler, _gl.DeleteSampler); uint ownedClamp = clamp; resources.Add( "clamp sampler", () => GlResourceCommand.Execute( _gl, $"delete clamp sampler {ownedClamp}", () => _gl.DeleteSampler(ownedClamp))); Configure(clamp, TextureWrapMode.ClampToEdge, "clamp sampler"); } catch (Exception constructionFailure) { resources.RollbackConstructionAndThrow( "SamplerCache construction failed and its sampler prefix did not cleanly roll back.", constructionFailure); } Wrap = wrap; Clamp = clamp; _resources = resources; } private void Configure(uint sampler, TextureWrapMode wrap, string name) => GlResourceCommand.Execute(_gl, $"configure {name}", () => { _gl.SamplerParameter(sampler, SamplerParameterI.WrapS, (int)wrap); _gl.SamplerParameter(sampler, SamplerParameterI.WrapT, (int)wrap); _gl.SamplerParameter(sampler, SamplerParameterI.MinFilter, (int)TextureMinFilter.Linear); _gl.SamplerParameter(sampler, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear); }); public void Dispose() { _resources.RetryCleanup(); } }