All three plugins M1 called for now exist over Silk.NET, and the milestone's actual claim is proven against a live GL context, not just argued: edit a plugin's code, rebuild just it, reload it while a real window stays open, see the change with no app restart. Verified directly this time — two PNGs of the same running window, before and after a live reload (orange triangle -> green, same process, same window, same GL context throughout) — not by analogy to the earlier clear-color version. engine.render, upgraded from clear-color to real geometry: - Vertex/fragment shaders compiled and linked at Configure() time, with real error checking (GetShader/GetProgram *Status + InfoLog on failure) rather than trusting hand-typed GLSL to just work. - VAO/VBO for a hardcoded triangle; Shutdown() deletes all three GL objects rather than leaking them across reloads. - unsafe confined to Configure() (VertexAttribPointer takes a raw offset pointer) via a narrow, documented override of Directory.Build.props' default — native graphics interop, not the kernel data structures docs/kernel-contract.md §7 was written against. engine.input: publishes IEngineInput (keyboard state) via Silk.NET.Input. Reuses Silk.NET's own Key enum rather than inventing one, same call as IEngineWindow.Native. Loads and constructs cleanly against a real window; nothing reacts to it yet since there's no gameplay code to. IScreenCapture (new, engine.render): reads the frame back via ReadPixels and writes a PNG. No SixLabors.ImageSharp — checked its license first and it isn't MIT/Apache (revenue-gated), which would have been a real surprise for downstream users of an MIT engine. PngWriter is a from-scratch encoder instead: ZLibStream (BCL, .NET 6+) for the one genuinely hard part, a correctly zlib-wrapped DEFLATE stream; chunk framing and CRC32 are small enough to get right and to verify by actually decoding files this wrote (done repeatedly, by hand, across this session). Engine.Host: "screenshot <path>" joins "r <plugin-id>" as a live stdin command, plus a non-interactive --screenshot/--screenshot-after- frames pair that captures once and exits — for scripts and agents that can't easily hold a pipe open into a long-running process. Real bug found and fixed in PluginHost, not specific to any one environment: loading a Contracts assembly into the Default ALC never set up resolution for ITS OWN dependencies. engine.windowing's and engine.render's Contracts both need Silk.NET packages and loaded fine anyway, by accident — Engine.Host references those two Contracts projects directly (to drive the windowed loop and screenshot capture), so their dependencies were already sitting in Engine.Host's own output directory. engine.input's Contracts has no such lucky coincidence and failed with a real FileNotFoundException. Fixed by hooking AssemblyLoadContext.Default.Resolving with an AssemblyDependencyResolver per loaded Contracts path — mirroring what PluginLoadContext already does for collectible ALCs, applied to the one path that never had it. Hooked once per process via a static list/flag, not per PluginHost instance, specifically to avoid a PluginHost instance becoming unreclaimable through its own event subscription. Not covered by automated tests, deliberately, same reasoning as the windowing/render pass: opening a real window and reading back a real framebuffer both need a real display. Verified by hand instead, documented above and in commit history rather than asserted. README status: M1 done, not "in progress." Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
156 lines
4.9 KiB
C#
156 lines
4.9 KiB
C#
using Engine.Kernel.Plugins;
|
|
using Engine.Kernel.Scheduling;
|
|
using Engine.Kernel.World;
|
|
using Engine.Render.Contracts;
|
|
using Engine.Windowing.Contracts;
|
|
using Silk.NET.OpenGL;
|
|
|
|
namespace Engine.Render;
|
|
|
|
/// <summary>
|
|
/// M1's render pipeline: one hardcoded triangle, drawn every Render stage.
|
|
/// See M1 in docs/kernel-contract.md §8.
|
|
///
|
|
/// This is the whole point of M1's "done when": change TriangleColor,
|
|
/// rebuild just this plugin, and reload it while the window from
|
|
/// engine.windowing stays open — the color changes with no app restart.
|
|
/// Verified by hand against a real window and a live GL context, and via
|
|
/// IScreenCapture — no external image library; see the note on PngWriter
|
|
/// for why.
|
|
///
|
|
/// engine.windowing alone produces a window that never becomes visible on
|
|
/// Wayland — unlike X11, a Wayland surface with no committed buffer simply
|
|
/// isn't shown by the compositor, so an "empty" window isn't even a black
|
|
/// rectangle, it's nothing at all. This plugin's first Clear+SwapBuffers is
|
|
/// what actually makes the window appear.
|
|
/// </summary>
|
|
public sealed class RenderPlugin : IPlugin
|
|
{
|
|
private const string VertexShaderSource = """
|
|
#version 330 core
|
|
layout (location = 0) in vec2 aPosition;
|
|
|
|
void main()
|
|
{
|
|
gl_Position = vec4(aPosition, 0.0, 1.0);
|
|
}
|
|
""";
|
|
|
|
private const string FragmentShaderSource = """
|
|
#version 330 core
|
|
out vec4 FragColor;
|
|
uniform vec4 uColor;
|
|
|
|
void main()
|
|
{
|
|
FragColor = uColor;
|
|
}
|
|
""";
|
|
|
|
private static readonly float[] TriangleColor = [0.2f, 0.8f, 0.4f, 1f];
|
|
|
|
private static readonly float[] Vertices =
|
|
[
|
|
0.0f, 0.6f,
|
|
-0.6f, -0.6f,
|
|
0.6f, -0.6f,
|
|
];
|
|
|
|
private GL? _gl;
|
|
private IEngineWindow? _window;
|
|
private uint _vao;
|
|
private uint _vbo;
|
|
private uint _program;
|
|
private int _colorLocation;
|
|
|
|
public unsafe void Configure(IPluginContext ctx)
|
|
{
|
|
_window = ctx.Services.Require<IEngineWindow>();
|
|
_window.Native.GLContext!.MakeCurrent();
|
|
_gl = _window.Native.CreateOpenGL();
|
|
|
|
_program = LinkProgram(_gl, VertexShaderSource, FragmentShaderSource);
|
|
_colorLocation = _gl.GetUniformLocation(_program, "uColor");
|
|
|
|
_vao = _gl.GenVertexArray();
|
|
_gl.BindVertexArray(_vao);
|
|
|
|
_vbo = _gl.GenBuffer();
|
|
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
|
_gl.BufferData<float>(BufferTargetARB.ArrayBuffer, Vertices, BufferUsageARB.StaticDraw);
|
|
|
|
_gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, 2 * sizeof(float), (void*)0);
|
|
_gl.EnableVertexAttribArray(0);
|
|
_gl.BindVertexArray(0);
|
|
|
|
ctx.Services.Provide<IScreenCapture>(new GlScreenCapture(_gl, _window));
|
|
ctx.Schedule.Add(Stage.Render, Draw);
|
|
ctx.Log.Info("GL context created, triangle ready");
|
|
}
|
|
|
|
public void Shutdown(IPluginContext ctx)
|
|
{
|
|
ctx.Schedule.RemoveAllFrom("engine.render");
|
|
ctx.Services.Revoke<IScreenCapture>();
|
|
|
|
if (_gl is not null)
|
|
{
|
|
_gl.DeleteVertexArray(_vao);
|
|
_gl.DeleteBuffer(_vbo);
|
|
_gl.DeleteProgram(_program);
|
|
_gl.Dispose();
|
|
}
|
|
|
|
_gl = null;
|
|
_window = null;
|
|
}
|
|
|
|
private void Draw(IWorld world)
|
|
{
|
|
_gl!.ClearColor(0.05f, 0.05f, 0.08f, 1f);
|
|
_gl.Clear(ClearBufferMask.ColorBufferBit);
|
|
|
|
_gl.UseProgram(_program);
|
|
_gl.Uniform4(_colorLocation, TriangleColor[0], TriangleColor[1], TriangleColor[2], TriangleColor[3]);
|
|
_gl.BindVertexArray(_vao);
|
|
_gl.DrawArrays(PrimitiveType.Triangles, 0, 3);
|
|
|
|
_window!.Native.GLContext!.SwapBuffers();
|
|
}
|
|
|
|
private static uint LinkProgram(GL gl, string vertexSource, string fragmentSource)
|
|
{
|
|
var vertex = CompileShader(gl, ShaderType.VertexShader, vertexSource);
|
|
var fragment = CompileShader(gl, ShaderType.FragmentShader, fragmentSource);
|
|
|
|
var program = gl.CreateProgram();
|
|
gl.AttachShader(program, vertex);
|
|
gl.AttachShader(program, fragment);
|
|
gl.LinkProgram(program);
|
|
|
|
gl.GetProgram(program, GLEnum.LinkStatus, out var linked);
|
|
if (linked == 0)
|
|
throw new InvalidOperationException($"Shader program failed to link: {gl.GetProgramInfoLog(program)}");
|
|
|
|
gl.DetachShader(program, vertex);
|
|
gl.DetachShader(program, fragment);
|
|
gl.DeleteShader(vertex);
|
|
gl.DeleteShader(fragment);
|
|
|
|
return program;
|
|
}
|
|
|
|
private static uint CompileShader(GL gl, ShaderType type, string source)
|
|
{
|
|
var shader = gl.CreateShader(type);
|
|
gl.ShaderSource(shader, source);
|
|
gl.CompileShader(shader);
|
|
|
|
gl.GetShader(shader, GLEnum.CompileStatus, out var compiled);
|
|
if (compiled == 0)
|
|
throw new InvalidOperationException($"{type} failed to compile: {gl.GetShaderInfoLog(shader)}");
|
|
|
|
return shader;
|
|
}
|
|
}
|