using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Numerics; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading.Tasks; using Raylib_cs; using SkiaSharp; Main(); const string programsProsperoVm = "Programs/prospero.vm"; // STAThread is required if you deploy using NativeAOT on Windows // See https://github.com/raylib-cs/raylib-cs/issues/301 [STAThread] void Main() { string fragmentShader = """ #version 330 in vec2 fragTexCoord; in vec4 fragColor; uniform sampler2D texture0; out vec4 outputColor; void main() { float v = texture(texture0, fragTexCoord).r < 0 ? 1.0 : 0.0; outputColor = vec4(v, v, v, 1.0f) * fragColor; } """; int currentOutputImageSize = 1024; Raylib.InitWindow(currentOutputImageSize, currentOutputImageSize, "Sharpero"); Texture2D currentOutputTexture; Shader currentShader = Raylib.LoadShaderFromMemory(null, fragmentShader); float[] currentOutputImageData = new float[currentOutputImageSize * currentOutputImageSize]; unsafe { fixed (float* currentOutputImageDataPtr = currentOutputImageData) { Image currentOutputImage = new Image() { Format = PixelFormat.UncompressedR32, Data = currentOutputImageDataPtr, Width = currentOutputImageSize, Height = currentOutputImageSize, Mipmaps = 1 }; currentOutputTexture = Raylib.LoadTextureFromImage(currentOutputImage); } } // options?? bool shouldUseCompiler = false; bool shouldUseParallelism = true; bool shouldUseSimd = true; // compilation specific bool shouldRecompile = false; bool isEvaluating = false; bool shouldEvaluate = false; bool shouldCancelUpdateTexture = false; bool shouldUpdateTexture = false; float lastEvaluationTimeTook = 0.0f; float lastCompilationTimeTook = 0.0f; // cache the instructions, the program doesn't change after all EvaluationInstructions instructions = Parsing.Parse(programsProsperoVm); while (!Raylib.WindowShouldClose()) { Raylib.BeginDrawing(); Raylib.ClearBackground(Color.White); InterpreterOptions interpreterOptions = (shouldUseParallelism ? InterpreterOptions.Parallelism : default) | (shouldUseSimd ? InterpreterOptions.Simd : default) | (shouldUseCompiler ? InterpreterOptions.CompileInnerLoop : default); if (shouldEvaluate && isEvaluating) { shouldEvaluate = false; } if (shouldRecompile && !isEvaluating) { Compiler.CompilerCache>.CachedPrograms.Clear(); Compiler.CompilerCache.CachedPrograms.Clear(); Stopwatch sw = Stopwatch.StartNew(); if ((interpreterOptions & InterpreterOptions.Simd) != 0) { Compiler.Compile>(instructions); } else { Compiler.Compile(instructions); } lastCompilationTimeTook = (float)sw.Elapsed.TotalSeconds; shouldRecompile = false; } if (shouldEvaluate && !isEvaluating) { isEvaluating = true; shouldUpdateTexture = true; Task.Run(() => { Stopwatch sw = Stopwatch.StartNew(); currentOutputImageData.AsSpan()[..currentOutputImageData.Length].Clear(); if ((interpreterOptions & InterpreterOptions.Simd) != 0) { Interpreter.Evaluate>(instructions, imageSize: currentOutputImageSize, interpreterOptions, currentOutputImageData); } else { Interpreter.Evaluate(instructions, imageSize: currentOutputImageSize, interpreterOptions, currentOutputImageData); } lastEvaluationTimeTook = (float)sw.Elapsed.TotalSeconds; Raylib.UpdateTexture(currentOutputTexture, currentOutputImageData); shouldCancelUpdateTexture = true; isEvaluating = false; }); shouldEvaluate = false; } if (shouldUpdateTexture) { Raylib.UpdateTexture(currentOutputTexture, currentOutputImageData); if (shouldCancelUpdateTexture) { shouldCancelUpdateTexture = false; shouldUpdateTexture = false; } } Raylib.BeginShaderMode(currentShader); Raylib.DrawTexture(currentOutputTexture, 0, 0, Color.White); Raylib.EndShaderMode(); Raylib.DrawText("Sharpero (press R to evaluate, O to output to file)", 12, 12, 20, Color.RayWhite); Raylib.DrawText($" - parallelism {(shouldUseParallelism ? "enabled" : "disabled")} (P to toggle), simd {(shouldUseSimd ? "enabled" : "disabled")} (S to toggle), compile {(shouldUseCompiler? "enabled" : "disabled")} (C to toggle)", 12, 32, 20, Color.RayWhite); if (lastEvaluationTimeTook != 0.0f) { double evaluationTimeNanoSeconds = (lastEvaluationTimeTook * 1000.0 * 1000.0 * 1000.0); double nanoSecondsPerPixel = evaluationTimeNanoSeconds / (currentOutputImageSize * currentOutputImageSize); Raylib.DrawText( shouldUseCompiler ? $" - evaluation took: {lastEvaluationTimeTook:0.0} s ({nanoSecondsPerPixel:0.0} ns / pixel) (compilation: {lastCompilationTimeTook:0.0} s{(shouldRecompile ? " (pending)" : string.Empty)})" : $" - evaluation took: {lastEvaluationTimeTook:0.0} s ({nanoSecondsPerPixel:0.0} ns / pixel)", 12, 52, 20, Color.White); } if (Raylib.IsKeyPressed(KeyboardKey.R)) { shouldEvaluate = true; } if (Raylib.IsKeyPressed(KeyboardKey.O)) { Task.Run(() => { GenerateOutputImage(currentImageSize: currentOutputImageSize); }); } if (Raylib.IsKeyPressed(KeyboardKey.P)) { shouldUseParallelism = !shouldUseParallelism; } if (Raylib.IsKeyPressed(KeyboardKey.S)) { shouldUseSimd = !shouldUseSimd; if (shouldUseCompiler) { shouldRecompile = true; } } if (Raylib.IsKeyPressed(KeyboardKey.C)) { shouldUseCompiler = !shouldUseCompiler; shouldRecompile = shouldUseCompiler; if (shouldUseCompiler) { shouldRecompile = true; } } Raylib.EndDrawing(); } Raylib.UnloadShader(currentShader); Raylib.UnloadTexture(currentOutputTexture); Raylib.CloseWindow(); } void GenerateOutputImage(int currentImageSize) { string outputImagePath = "prospero.jpg"; InterpreterOptions interpreterOptions = InterpreterOptions.Parallelism | InterpreterOptions.Simd; (bool result, double timeTakenInSeconds) = BenchmarkFunction(() => { EvaluationInstructions instructions = Parsing.Parse(programsProsperoVm); float[] result = Interpreter.Evaluate>(instructions, imageSize: currentImageSize, interpreterOptions); WriteOutputImage(currentImageSize, result, outputImagePath); void WriteOutputImage(int imageSize, float[] imageData, string imageOutputPath) { byte[] imageDataBytes = imageData.Select(p => (byte)(p < 0 ? 255 : 0)).ToArray(); using var image = SKImage.FromPixelCopy(new SKImageInfo(imageSize, imageSize, SKColorType.Gray8), imageDataBytes); using var data = image.Encode(SKEncodedImageFormat.Jpeg, 100); using var stream = File.OpenWrite(imageOutputPath); data.SaveTo(stream); } return true; }); Console.WriteLine($"Sharpero wrote {currentImageSize}x{currentImageSize} to {outputImagePath} in {timeTakenInSeconds} seconds ({interpreterOptions})"); } (T, double) BenchmarkFunction(Func benchmarkedFunction) { var sw = Stopwatch.StartNew(); T benchmarkedResult = benchmarkedFunction(); return (benchmarkedResult, sw.Elapsed.TotalSeconds); } internal enum OpCode { VarX, VarY, Const, Add, Sub, Mul, Max, Min, Neg, Square, Sqrt } internal readonly record struct Operand { private readonly int _value = -1; public bool IsConstant => ((_value >> 31) & 1) != 0; public int Value => _value & 0x7FFFFFFF; public Operand(int value, bool isConstant = false) { _value = (isConstant ? 1 : 0) << 31 | value & 0x7FFFFFFF; } public static implicit operator int(Operand o) => o.Value; public override string ToString() => $"{(IsConstant ? Value : $"_{Value}")}"; } internal readonly record struct Instruction( Operand Out, OpCode OpCode, Operand A = default, Operand B = default, float C = 0.0f) { public override string ToString() { return OpCode switch { OpCode.VarX => $"_{Out} var-x", OpCode.VarY => $"_{Out} var-y", OpCode.Const => $"_{Out} const", OpCode.Add => $"_{Out} add {A} {B}", OpCode.Sub => $"_{Out} sub {A} {B}", OpCode.Mul => $"_{Out} mul {A} {B}", OpCode.Max => $"_{Out} max {A} {B}", OpCode.Min => $"_{Out} min {A} {B}", OpCode.Neg => $"_{Out} neg {A}", OpCode.Square => $"_{Out} square {A}", OpCode.Sqrt => $"_{Out} sqrt {A}", _ => string.Empty }; } } internal record EvaluationInstructions(Instruction[] Instructions); internal static class Parsing { // 1D <-> 2D coordinate helpers for square grids public static (int x, int y) IndexToCoord(int idx, int width) => (idx % width, idx / width); public static int CoordToIndex(int x, int y, int width) => x + (y * width); // the identifiers are hexadecimal, stripping off the leading _ is enough :), public static Operand ParseIdentifier(Dictionary identifiers, string id, bool isConstant = false) { if (identifiers.TryGetValue(id, out var value)) { return value; } return identifiers[id] = new Operand(Convert.ToInt32(id[1..], 16), isConstant); } private static float EvaluateExpression(OpCode opCode, float a, float b) { return opCode switch { OpCode.Add => a + b, OpCode.Sub => a - b, OpCode.Mul => a * b, _ => 0.0f }; } public static EvaluationInstructions Parse(string filename) { // parse all identifiers first, so we know which are directly associated with constant values Dictionary identifiers = ParseIdentifiers(filename); // now parse instructions, correctly tagging operands that operate directly on constants List instructions = ParseInstructions(filename, identifiers); // eliminate any constants from the tape, folding them into the instruction operands instead (int totalNumberOfInstructions, int numberOfRemovedInstructions) = EliminateConstants(instructions); Console.WriteLine($"Sharpero eliminated {numberOfRemovedInstructions} constants from tape, {(float)numberOfRemovedInstructions / totalNumberOfInstructions * 100.0f:0.0} % of total"); return new EvaluationInstructions(instructions.ToArray()); } private static Dictionary ParseIdentifiers(string filename) { Dictionary identifiers = []; foreach (string line in File.ReadAllLines(filename)) { if (line.StartsWith('#')) { continue; } switch (line.Split(" ")) { case [{ } @out, "const", not null]: ParseIdentifier(identifiers, @out, isConstant: true); break; case [{ } @out, not null, { } a, { } b]: ParseIdentifier(identifiers, @out); ParseIdentifier(identifiers, a); ParseIdentifier(identifiers, b); break; case [{ } @out, not null, { } a]: ParseIdentifier(identifiers, @out); ParseIdentifier(identifiers, a); break; case [{ } @out, not null]: ParseIdentifier(identifiers, @out); break; } } return identifiers; } private static List ParseInstructions(string filename, Dictionary identifiers) { List instructions = []; foreach (string line in File.ReadAllLines(filename)) { if (line.StartsWith('#')) { continue; } Instruction? parsedInstruction = line.Split(" ") switch { [{ } @out, "var-x"] => new Instruction(identifiers[@out], OpCode.VarX), [{ } @out, "var-y"] => new Instruction(identifiers[@out], OpCode.VarY), [{ } @out, "const", { } v] => new Instruction(identifiers[@out], OpCode.Const, C: float.Parse(v, CultureInfo.InvariantCulture)), [{ } @out, "add", { } a, { } b] => new Instruction(identifiers[@out], OpCode.Add, identifiers[a], identifiers[b]), [{ } @out, "sub", { } a, { } b] => new Instruction(identifiers[@out], OpCode.Sub, identifiers[a], identifiers[b]), [{ } @out, "mul", { } a, { } b] => new Instruction(identifiers[@out], OpCode.Mul, identifiers[a], identifiers[b]), [{ } @out, "max", { } a, { } b] => new Instruction(identifiers[@out], OpCode.Max, identifiers[a], identifiers[b]), [{ } @out, "min", { } a, { } b] => new Instruction(identifiers[@out], OpCode.Min, identifiers[a], identifiers[b]), [{ } @out, "neg", { } a] => new Instruction(identifiers[@out], OpCode.Neg, identifiers[a]), [{ } @out, "square", { } a] => new Instruction(identifiers[@out], OpCode.Square, identifiers[a]), [{ } @out, "sqrt", { } a] => new Instruction(identifiers[@out], OpCode.Sqrt, identifiers[a]), _ => null }; if (parsedInstruction is { } instruction) { instructions.Add(instruction); } } return instructions; } private static (int TotalInstructions, int NumberOfRemovedInstructions) EliminateConstants(List instructions) { // handle constant propagation, eliminating all constants from the instruction stream and merging them foreach (ref Instruction instruction in CollectionsMarshal.AsSpan(instructions)) { switch (instruction.OpCode) { case OpCode.Add or OpCode.Sub or OpCode.Mul when instruction.A.IsConstant || instruction.B.IsConstant: switch (instruction) { case { A: { IsConstant: true } a, B.IsConstant: false }: instruction = instruction with { C = instructions[a].C }; break; case { A.IsConstant: false, B: { IsConstant: true } b }: instruction = instruction with { C = instructions[b].C }; break; case { OpCode: var opCode, A: { IsConstant: true } a, B: { IsConstant: true } b }: instruction = instruction with { C = EvaluateExpression(opCode, instructions[a].C, instructions[b].C) }; break; case { A.IsConstant: false, B.IsConstant: false }: break; } break; } } // relocate all offsets, now that we've nuked all the constants foreach (ref Instruction instruction in CollectionsMarshal.AsSpan(instructions)) { switch (instruction.OpCode) { case OpCode.Const: int offset = instruction.Out; foreach (ref Instruction otherInstruction in CollectionsMarshal.AsSpan(instructions)) { int newOperandOut = otherInstruction.Out >= offset ? otherInstruction.Out - 1 : otherInstruction.Out; int newOperandA = otherInstruction.A >= offset ? otherInstruction.A - 1 : otherInstruction.A; int newOperandB = otherInstruction.B >= offset ? otherInstruction.B - 1 : otherInstruction.B; if (newOperandOut == otherInstruction.Out && newOperandA == otherInstruction.A && newOperandB == otherInstruction.B) { continue; } otherInstruction = otherInstruction with { Out = new Operand(newOperandOut), A = new Operand(newOperandA, isConstant: otherInstruction.A.IsConstant), B = new Operand(newOperandB, isConstant: otherInstruction.B.IsConstant), }; } break; } } int totalNumberOfInstructions = instructions.Count; int numberOfRemovedInstructions = instructions.RemoveAll(i => i.OpCode == OpCode.Const); return (TotalInstructions: totalNumberOfInstructions, NumberOfRemovedInstructions: numberOfRemovedInstructions); } } [Flags] internal enum InterpreterOptions { CompileInnerLoop = 0x1, Parallelism = 0x2, Simd = 0x4 } internal static class Evaluation { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Write(Span variables, T value, int offset) where T : unmanaged { Unsafe.Add(ref MemoryMarshal.GetReference(variables), offset) = value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T Read(Span variables, int offset) where T : unmanaged { return Unsafe.Add(ref MemoryMarshal.GetReference(variables), offset); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T Add(T a, T b) where T : unmanaged { if (typeof(T) == typeof(float)) { return Unsafe.BitCast(Unsafe.As(ref a) + Unsafe.As(ref b)); } else if (typeof(T) == typeof(Vector)) { return Unsafe.BitCast, T>(Unsafe.As>(ref a) + Unsafe.As>(ref b)); } else { throw new InvalidOperationException(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T Sub(T a, T b) where T : unmanaged { if (typeof(T) == typeof(float)) { return Unsafe.BitCast(Unsafe.As(ref a) - Unsafe.As(ref b)); } else if (typeof(T) == typeof(Vector)) { return Unsafe.BitCast, T>(Unsafe.As>(ref a) - Unsafe.As>(ref b)); } else { throw new InvalidOperationException(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T Mul(T a, T b) where T : unmanaged { if (typeof(T) == typeof(float)) { return Unsafe.BitCast(Unsafe.As(ref a) * Unsafe.As(ref b)); } else if (typeof(T) == typeof(Vector)) { return Unsafe.BitCast, T>(Unsafe.As>(ref a) * Unsafe.As>(ref b)); } else { throw new InvalidOperationException(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T Neg(T v) where T : unmanaged { if (typeof(T) == typeof(float)) { return Unsafe.BitCast(-Unsafe.As(ref v)); } else if (typeof(T) == typeof(Vector)) { return Unsafe.BitCast, T>(-Unsafe.As>(ref v)); } else { throw new InvalidOperationException(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T Max(T a, T b) where T : unmanaged { if (typeof(T) == typeof(float)) { return Unsafe.BitCast(MathF.Max(Unsafe.As(ref a), Unsafe.As(ref b))); } else if (typeof(T) == typeof(Vector)) { return Unsafe.BitCast, T>(Vector.Max(Unsafe.As>(ref a), Unsafe.As>(ref b))); } else { throw new InvalidOperationException(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T Min(T a, T b) where T : unmanaged { if (typeof(T) == typeof(float)) { return Unsafe.BitCast(MathF.Min(Unsafe.As(ref a), Unsafe.As(ref b))); } else if (typeof(T) == typeof(Vector)) { return Unsafe.BitCast, T>(Vector.Min(Unsafe.As>(ref a), Unsafe.As>(ref b))); } else { throw new InvalidOperationException(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T SquareRoot(T v) where T : unmanaged { if (typeof(T) == typeof(float)) { return Unsafe.BitCast(MathF.Sqrt(Unsafe.As(ref v))); } else if (typeof(T) == typeof(Vector)) { return Unsafe.BitCast, T>(Vector.SquareRoot(Unsafe.As>(ref v))); } else { throw new InvalidOperationException(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T Square(T v) where T : unmanaged { if (typeof(T) == typeof(float)) { return Unsafe.BitCast(Unsafe.As(ref v) * Unsafe.As(ref v)); } else if (typeof(T) == typeof(Vector)) { return Unsafe.BitCast, T>(Vector.Multiply(Unsafe.As>(ref v), Unsafe.As>(ref v))); } else { throw new InvalidOperationException(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T EvaluateConstant(float c) where T : unmanaged { if (typeof(T) == typeof(float)) { return Unsafe.BitCast(c); } else if (typeof(T) == typeof(Vector)) { return Unsafe.BitCast, T>(new Vector(c)); } else { throw new InvalidOperationException(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T One() where T : unmanaged { if (typeof(T) == typeof(float)) { return Unsafe.BitCast(1.0f); } else if (typeof(T) == typeof(Vector)) { return Unsafe.BitCast, T>(Vector.One); } else { throw new InvalidOperationException(); } } } internal static class Delegates where T : unmanaged { public delegate void EvaluateDelegate(Span variables, T xs, T ys); } internal static class Compiler { internal static class CompilerCache where T : unmanaged { internal static readonly ConcurrentDictionary.EvaluateDelegate> CachedPrograms = new ConcurrentDictionary.EvaluateDelegate>(); } public static Delegates.EvaluateDelegate Compile(EvaluationInstructions evaluationInstructions) where T : unmanaged { if (CompilerCache.CachedPrograms.TryGetValue(evaluationInstructions, out var program)) { return program; } lock (CompilerCache.CachedPrograms) { // this number is derived very scientifically, on our sample program, this gives us about a kilobyte of IL // for each function that we output into our function-of-functions that we eventually evaluate, // and the JIT generally seems quite a lot happier to deal with smaller functions int maximumInstructionsPerChunk = 32; Stopwatch totalSw = Stopwatch.StartNew(); var chunkDelegates = evaluationInstructions.Instructions .Chunk(maximumInstructionsPerChunk) .AsParallel() .Select((instructionsChunk, instructionChunkIndex) => { Stopwatch subTotalSw = Stopwatch.StartNew(); var evaluateDynamicMethod = new DynamicMethod( $"EvaluateChunk_{instructionChunkIndex}", typeof(void), [typeof(Span), typeof(T), typeof(T)], typeof(Compiler).Module, skipVisibility: true ); var methodGenerator = evaluateDynamicMethod.GetILGenerator(); methodGenerator.DeclareLocal(typeof(T)); foreach (Instruction instruction in instructionsChunk) { CompileInstruction(methodGenerator, in instruction); } // finalize method, we need a return as our final bit methodGenerator.Emit(OpCodes.Ret); // create the delegate, but also force the JIT to compile our function :D var newChunkDelegate = evaluateDynamicMethod.CreateDelegate.EvaluateDelegate>(); RuntimeHelpers.PrepareDelegate(newChunkDelegate); Console.WriteLine($" - took: {subTotalSw.Elapsed.Milliseconds:0.0} ms to compile sub program ({instructionChunkIndex}) ({methodGenerator.ILOffset} bytes of instructions)!"); return newChunkDelegate; }).ToList(); Console.WriteLine($"Sharpero took: {totalSw.Elapsed.Milliseconds:0.0} ms to compile program with {chunkDelegates.Count} parts (max instructions per chunk = {maximumInstructionsPerChunk})!"); CompilerCache.CachedPrograms[evaluationInstructions] = (variables, xs, ys) => { foreach (Delegates.EvaluateDelegate chunkDelegate in chunkDelegates) { chunkDelegate(variables, xs, ys); } }; // force the JIT to compile our function :D RuntimeHelpers.PrepareDelegate(CompilerCache.CachedPrograms[evaluationInstructions]); return CompilerCache.CachedPrograms[evaluationInstructions]; } } private static void CompileInstruction(ILGenerator methodGenerator, in Instruction instruction) where T : unmanaged { void EmitReadConstant(float constant) { methodGenerator.Emit(OpCodes.Ldc_R4, constant); // constant methodGenerator.Emit(OpCodes.Call, typeof(Evaluation).GetMethod(nameof(Evaluation.EvaluateConstant))!.MakeGenericMethod(typeof(T))); } void EmitWriteConstant(int offset, float constant) { // write, but with constant value methodGenerator.Emit(OpCodes.Ldarg_0); // Span methodGenerator.Emit(OpCodes.Ldc_R4, constant); // result of call methodGenerator.Emit(OpCodes.Ldc_I4, offset); // offset methodGenerator.Emit(OpCodes.Call, typeof(Evaluation).GetMethod(nameof(Evaluation.Write))!.MakeGenericMethod(typeof(T))); } void EmitWrite(int offset) { // write, previous result will be at loc 0 if all is well methodGenerator.Emit(OpCodes.Ldarg_0); // Span methodGenerator.Emit(OpCodes.Ldloc_0); // result methodGenerator.Emit(OpCodes.Ldc_I4, offset); // offset methodGenerator.Emit(OpCodes.Call, typeof(Evaluation).GetMethod(nameof(Evaluation.Write))!.MakeGenericMethod(typeof(T))); } void EmitWriteXs(int offset) { // write, but xs specifically methodGenerator.Emit(OpCodes.Ldarg_0); // Span methodGenerator.Emit(OpCodes.Ldarg_1); // xs methodGenerator.Emit(OpCodes.Ldc_I4, offset); // offset methodGenerator.Emit(OpCodes.Call, typeof(Evaluation).GetMethod(nameof(Evaluation.Write))!.MakeGenericMethod(typeof(T))); } void EmitWriteYs(int offset) { // write, but ys specifically methodGenerator.Emit(OpCodes.Ldarg_0); // Span methodGenerator.Emit(OpCodes.Ldarg_2); // ys methodGenerator.Emit(OpCodes.Ldc_I4, offset); // offset methodGenerator.Emit(OpCodes.Call, typeof(Evaluation).GetMethod(nameof(Evaluation.Write))!.MakeGenericMethod(typeof(T))); } void EmitInvokeUnaryOperation(string methodName) { // call, but we're expecting only a single argument, this would be a good place to put validation methodGenerator.Emit(OpCodes.Call, typeof(Evaluation).GetMethod(methodName)!.MakeGenericMethod(typeof(T))); methodGenerator.Emit(OpCodes.Stloc_0); } void EmitInvokeBinaryOperation(string methodName) { // call, but we're expecting two arguments, this would be a good place to put validation methodGenerator.Emit(OpCodes.Call, typeof(Evaluation).GetMethod(methodName)!.MakeGenericMethod(typeof(T))); methodGenerator.Emit(OpCodes.Stloc_0); } void EmitReadVariable(Operand v) { methodGenerator.Emit(OpCodes.Ldarg_0); // Span methodGenerator.Emit(OpCodes.Ldc_I4, v); // variable offset methodGenerator.Emit(OpCodes.Call, typeof(Evaluation).GetMethod(nameof(Evaluation.Read))!.MakeGenericMethod(typeof(T))); } switch (instruction) { case { OpCode: OpCode.VarX }: // => xs, EmitWriteXs(instruction.Out); break; case { OpCode: OpCode.VarY }: // => ys, EmitWriteYs(instruction.Out); break; case { OpCode: OpCode.Add, A: { IsConstant: false } a, B: { IsConstant: false } b }: // => Add(variables[a], variables[b]), EmitReadVariable(a); EmitReadVariable(b); EmitInvokeBinaryOperation(nameof(Evaluation.Add)); EmitWrite(instruction.Out); break; case { OpCode: OpCode.Add, A.IsConstant: true, B: { IsConstant: false } b }: // => Add(EvaluateConstant(instruction.C), variables[b]), EmitReadConstant(instruction.C); EmitReadVariable(b); EmitInvokeBinaryOperation(nameof(Evaluation.Add)); EmitWrite(instruction.Out); break; case { OpCode: OpCode.Add, A: { IsConstant: false } a, B.IsConstant: true }: // => Add(variables[a], EvaluateConstant(instruction.C)), EmitReadVariable(a); EmitReadConstant(instruction.C); EmitInvokeBinaryOperation(nameof(Evaluation.Add)); EmitWrite(instruction.Out); break; case { OpCode: OpCode.Sub, A: { IsConstant: false } a, B: { IsConstant: false } b }: // => Sub(variables[a], variables[b]), EmitReadVariable(a); EmitReadVariable(b); EmitInvokeBinaryOperation(nameof(Evaluation.Sub)); EmitWrite(instruction.Out); break; case { OpCode: OpCode.Sub, A.IsConstant: true, B: { IsConstant: false } b }: // => Sub(EvaluateConstant(instruction.C), variables[b]), EmitReadConstant(instruction.C); EmitReadVariable(b); EmitInvokeBinaryOperation(nameof(Evaluation.Sub)); EmitWrite(instruction.Out); break; case { OpCode: OpCode.Sub, A: { IsConstant: false } a, B.IsConstant: true }: // => Sub(variables[a], EvaluateConstant(instruction.C)), EmitReadVariable(a); EmitReadConstant(instruction.C); EmitInvokeBinaryOperation(nameof(Evaluation.Sub)); EmitWrite(instruction.Out); break; case { OpCode: OpCode.Mul, A: { IsConstant: false } a, B: { IsConstant: false } b }: // => Mul(variables[a], variables[b]), EmitReadVariable(a); EmitReadVariable(b); EmitInvokeBinaryOperation(nameof(Evaluation.Mul)); EmitWrite(instruction.Out); break; case { OpCode: OpCode.Mul, A.IsConstant: true, B: { IsConstant: false } b }: // => Mul(EvaluateConstant(instruction.C), variables[b]), EmitReadConstant(instruction.C); EmitReadVariable(b); EmitInvokeBinaryOperation(nameof(Evaluation.Mul)); EmitWrite(instruction.Out); break; case { OpCode: OpCode.Mul, A: { IsConstant: false } a, B.IsConstant: true }: // => Mul(variables[a], EvaluateConstant(instruction.C)), EmitReadVariable(a); EmitReadConstant(instruction.C); EmitInvokeBinaryOperation(nameof(Evaluation.Mul)); EmitWrite(instruction.Out); break; case { OpCode: OpCode.Max, A: var a, B: var b }: // => Max(variables[a], variables[b]), EmitReadVariable(a); EmitReadVariable(b); EmitInvokeBinaryOperation(nameof(Evaluation.Max)); EmitWrite(instruction.Out); break; case { OpCode: OpCode.Min, A: var a, B: var b }: // => Min(variables[a], variables[b]), EmitReadVariable(a); EmitReadVariable(b); EmitInvokeBinaryOperation(nameof(Evaluation.Min)); EmitWrite(instruction.Out); break; case { OpCode: OpCode.Neg, A: var a }: // => Neg(variables[a]), EmitReadVariable(a); EmitInvokeUnaryOperation(nameof(Evaluation.Neg)); EmitWrite(instruction.Out); break; case { OpCode: OpCode.Sqrt, A: var a }: // => SquareRoot(variables[a]), EmitReadVariable(a); EmitInvokeUnaryOperation(nameof(Evaluation.SquareRoot)); EmitWrite(instruction.Out); break; case { OpCode: OpCode.Square, A: var a }: // => Square(variables[a]), EmitReadVariable(a); EmitInvokeUnaryOperation(nameof(Evaluation.Square)); EmitWrite(instruction.Out); break; case { OpCode: OpCode.Const, C: var v }: // => EvaluateConstant(v), EmitWriteConstant(instruction.Out, v); break; } } } internal static class Interpreter { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T GetValues(Span values) { if (typeof(T) == typeof(float)) { return Unsafe.BitCast(values[0]); } else if (typeof(T) == typeof(Vector)) { return Unsafe.BitCast, T>(new Vector(values)); } else { throw new InvalidOperationException(); } } [SkipLocalsInit] public static float[] Evaluate(EvaluationInstructions evaluationInstructions, int imageSize, InterpreterOptions options = default, float[]? result = null) where T : unmanaged { result ??= new float[imageSize * imageSize]; ParallelOptions parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = (options & InterpreterOptions.Parallelism) != 0 ? -1 : 1 }; bool shouldCompileInnerLoop = (options & InterpreterOptions.CompileInnerLoop) != 0; if (shouldCompileInnerLoop) { // invoke the compilation before we go into the parallel context, if we haven't already Compiler.Compile(evaluationInstructions); } int chunkSize = typeof(T) == typeof(Vector) ? Vector.Count : 1; Parallel.For(0, (imageSize * imageSize) / chunkSize, parallelOptions, chunkIdx => { Span xs = stackalloc float[chunkSize]; Span ys = stackalloc float[chunkSize]; for (int idx = 0; idx < chunkSize; ++idx) { int currentIdx = chunkIdx * chunkSize + idx; (int x, int y) = Parsing.IndexToCoord(currentIdx, width: imageSize); // fix up the coordinate space, our space is actually more like [-imageSize * 0.5f, imageSize * 0.5f], // ... rather than [0, imageSize] in x/y, so this gives us the expected result float vx = (x / (imageSize * 0.5f)) - 1.0f; float vy = 1.0f - (y / (imageSize * 0.5f)); (xs[idx], ys[idx]) = (vx, vy); } T results = shouldCompileInnerLoop ? EvaluateCompiled(evaluationInstructions, GetValues(xs), GetValues(ys)) : EvaluateInterpreted(evaluationInstructions, GetValues(xs), GetValues(ys)); for (int idx = 0; idx < chunkSize; ++idx) { int currentIdx = chunkIdx * chunkSize + idx; (int x, int y) = Parsing.IndexToCoord(currentIdx, width: imageSize); result[Parsing.CoordToIndex(x, y, width: imageSize)] = Unsafe.Add(ref Unsafe.As(ref results), idx); } }); return result; } [SkipLocalsInit] public static T EvaluateCompiled(EvaluationInstructions evaluationInstructions, T xs, T ys) where T : unmanaged { Span variables = stackalloc T[evaluationInstructions.Instructions.Length]; // compile if we've not already cached this instruction set, and evaluate! Compiler.Compile(evaluationInstructions)?.Invoke(variables, xs, ys); return variables[evaluationInstructions.Instructions.Length - 1]; } [SkipLocalsInit] public static T EvaluateInterpreted(EvaluationInstructions evaluationInstructions, T xs, T ys) where T : unmanaged { Span variables = stackalloc T[evaluationInstructions.Instructions.Length]; foreach (ref Instruction instruction in evaluationInstructions.Instructions.AsSpan()) { variables[instruction.Out] = instruction switch { { OpCode: OpCode.VarX } => xs, { OpCode: OpCode.VarY } => ys, { OpCode: OpCode.Add, A: { IsConstant: false } a, B: { IsConstant: false } b } => Evaluation.Add(variables[a], variables[b]), { OpCode: OpCode.Add, A.IsConstant: true, B: { IsConstant: false } b } => Evaluation.Add(Evaluation.EvaluateConstant(instruction.C), variables[b]), { OpCode: OpCode.Add, A: { IsConstant: false } a, B.IsConstant: true } => Evaluation.Add(variables[a], Evaluation.EvaluateConstant(instruction.C)), { OpCode: OpCode.Sub, A: { IsConstant: false } a, B: { IsConstant: false } b } => Evaluation.Sub(variables[a], variables[b]), { OpCode: OpCode.Sub, A.IsConstant: true, B: { IsConstant: false } b } => Evaluation.Sub(Evaluation.EvaluateConstant(instruction.C), variables[b]), { OpCode: OpCode.Sub, A: { IsConstant: false } a, B.IsConstant: true } => Evaluation.Sub(variables[a], Evaluation.EvaluateConstant(instruction.C)), { OpCode: OpCode.Mul, A: { IsConstant: false } a, B: { IsConstant: false } b } => Evaluation.Mul(variables[a], variables[b]), { OpCode: OpCode.Mul, A.IsConstant: true, B: { IsConstant: false } b } => Evaluation.Mul(Evaluation.EvaluateConstant(instruction.C), variables[b]), { OpCode: OpCode.Mul, A: { IsConstant: false } a, B.IsConstant: true } => Evaluation.Mul(variables[a], Evaluation.EvaluateConstant(instruction.C)), { OpCode: OpCode.Max, A: var a, B: var b } => Evaluation.Max(variables[a], variables[b]), { OpCode: OpCode.Min, A: var a, B: var b } => Evaluation.Min(variables[a], variables[b]), { OpCode: OpCode.Neg, A: var a } => Evaluation.Neg(variables[a]), { OpCode: OpCode.Sqrt, A: var a } => Evaluation.SquareRoot(variables[a]), { OpCode: OpCode.Square, A: var a } => Evaluation.Square(variables[a]), { OpCode: OpCode.Const, C: var v } => Evaluation.EvaluateConstant(v), _ => variables[instruction.Out] }; } return variables[evaluationInstructions.Instructions.Length - 1]; } }