Skip to content

Instantly share code, notes, and snippets.

@mattwarren
Last active May 18, 2021 08:08
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save mattwarren/d17a0c356bd6fdb9f596bee6b9a5e63c to your computer and use it in GitHub Desktop.
Save mattwarren/d17a0c356bd6fdb9f596bee6b9a5e63c to your computer and use it in GitHub Desktop.
using System;
using System.IO;
using System.Reflection;
using System.Runtime.Versioning;
using System.Text;
namespace PostcardSizedRaytracerDotNet
{
public struct Vec {
public float x, y, z;
public static implicit operator Vec(float v) { return new Vec(a: v, b: v, c: v); }
public Vec(float a, float b, float c = 0) {
x = a;
y = b;
z = c;
}
public static Vec operator +(Vec q, Vec r) { return new Vec(q.x + r.x, q.y + r.y, q.z + r.z); }
public static Vec operator *(Vec q, Vec r) { return new Vec(q.x * r.x, q.y * r.y, q.z * r.z); }
public static float operator %(Vec q, Vec r) { return q.x * r.x + q.y * r.y + q.z * r.z; }
#if NETSTANDARD2_1 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_0
// intnv square root
public static Vec operator !(Vec q) {
return q * (1.0f / MathF.Sqrt(q % q));
}
#else
public static Vec operator !(Vec q) {
return q * (1.0f / (float)Math.Sqrt(q % q));
}
#endif
public override string ToString() {
var format = ",10:N5"; // 5 decimal spaces, padded to 10 chars in total
return string.Format("{{ x:{0" + format + "}, y:{1" + format + "}, z:{2" + format + "} }}", x, y, z);
}
}
class Program
{
// Helper method to make porting easier, we can just write 'Vec(...)' instead of 'new Vec(..)'
public static Vec Vec(float a, float b, float c = 0) {
return new Vec(a, b, c);
}
private static float min(float l, float r) { return Math.Min(l, r); }
private static Random random = new Random();
private static float randomVal() {
// TODO NextDouble() - 'greater than or equal to 0.0, and less than 1.0. i.e [0,1)
return (float)random.NextDouble();
}
private static float fmodf(float x, float y) {
return x % y; // according to https://stackoverflow.com/a/2690516
}
private static float fabsf(float x) {
return Math.Abs(x);
}
#if NETSTANDARD2_1 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_0
private static float sqrtf(float x) {
return MathF.Sqrt(x);
}
private static float powf(float x, float y) {
return MathF.Pow(x, y);
}
private static float cosf(float x) {
return MathF.Cos(x);
}
private static float sinf(float x) {
return MathF.Sin(x);
}
#else
private static float sqrtf(float x) {
return (float)Math.Sqrt(x);
}
private static float powf(float x, float y) {
return (float)Math.Pow(x, y);
}
private static float cosf(float x) {
return (float)Math.Cos(x);
}
private static float sinf(float x) {
return (float)Math.Sin(x);
}
#endif
// Rectangle CSG equation. Returns minimum signed distance from
// space carved by
// lowerLeft vertex and opposite rectangle vertex upperRight.
//[MethodImpl(MethodImplOptions.AggressiveInlining)] // This causes .NET Core to be x3 slower!!
static float BoxTest(Vec position, Vec lowerLeft, Vec upperRight) {
lowerLeft = position + (lowerLeft * -1.0f);
upperRight = upperRight + (position * -1.0f);
return -min(
min(
min(lowerLeft.x, upperRight.x),
min(lowerLeft.y, upperRight.y)),
min(lowerLeft.z, upperRight.z));
}
const int HIT_NONE = 0;
const int HIT_LETTER = 1;
const int HIT_WALL = 2;
const int HIT_SUN = 3;
static char[] letters = // 15 two points lines
("5O5_"+"5W9W"+"5_9_"+ // P (without curve)
"AOEO"+"COC_"+"A_E_"+ // I
"IOQ_"+"I_QO"+ // X
"UOY_"+"Y_]O"+"WW[W"+ // A
"aOa_"+"aWeW"+"a_e_"+"cWiO") // R (without curve)
.ToCharArray();
// Two curves (for P and R in PixaR) with hard-coded locations.
static Vec[] curves = new[] { Vec(-11, 6), Vec(11, 6) };
// Sample the world using Signed Distance Fields.
//[MethodImpl(MethodImplOptions.AggressiveInlining)] // This causes .NET Core to be *way* (x5) slower!!
static float QueryDatabase(Vec position, ref int hitType) {
float distance = float.MaxValue; // 1e9;
Vec f = position; // Flattened position (z=0)
f.z = 0;
for (int i = 0; i < letters.Length; i += 4) {
Vec begin = Vec(letters[i] - 79, letters[i + 1] - 79) * .5f;
Vec e = Vec(letters[i + 2] - 79, letters[i + 3] - 79) * .5f + begin * -1f;
Vec o = f + (begin + e * min(-min((begin + f * -1) % e / (e % e),
0),
1)
) * -1;
distance = min(distance, o % o); // compare squared distance.
}
distance = sqrtf(distance); // Get real distance, not square distance.
for (int i = 1; i >= 0; i--) {
Vec o = f + curves[i] * -1;
// I *think* this equivalent to the C++ 'conditional expression', see https://stackoverflow.com/a/16676940
float temp = 0.0f;
if (o.x > 0) {
temp = fabsf(sqrtf(o % o) - 2);
} else {
o.y += o.y > 0 ? -2 : 2;
temp = sqrtf(o % o);
}
distance = min(distance, temp);
}
distance = powf(powf(distance, 8) + powf(position.z, 8), 0.125f) - 0.5f;
hitType = HIT_LETTER;
float roomDist ;
roomDist = min(// min(A,B) = Union with Constructive solid geometry
//-min carves an empty space
-min(// Lower room
BoxTest(position, Vec(-30, -0.5f, -30), Vec(30, 18, 30)),
// Upper room
BoxTest(position, Vec(-25, 17, -25), Vec(25, 20, 25))
),
BoxTest( // Ceiling "planks" spaced 8 units apart.
Vec(fmodf(fabsf(position.x), 8),
position.y,
position.z),
Vec(1.5f, 18.5f, -25),
Vec(6.5f, 20, 25)
)
);
if (roomDist < distance) { distance = roomDist; hitType = HIT_WALL; };
float sun = 19.9f - position.y ; // Everything above 19.9 is light source.
if (sun < distance) { distance = sun; hitType = HIT_SUN; }
return distance;
}
// Perform signed sphere marching
// Returns hitType 0, 1, 2, or 3 and update hit position/normal
static int RayMarching(Vec origin, Vec direction, ref Vec hitPos, ref Vec hitNorm) {
int hitType = HIT_NONE;
int noHitCount = 0;
float d; // distance from closest object in world.
// Signed distance marching
for (float total_d = 0; total_d < 100; total_d += d)
if ((d = QueryDatabase(hitPos = origin + direction * total_d, ref hitType)) < .01
|| ++noHitCount > 99)
{
hitNorm =
!Vec(QueryDatabase(hitPos + Vec(.01f, 0), ref noHitCount) - d,
QueryDatabase(hitPos + Vec(0, .01f), ref noHitCount) - d,
QueryDatabase(hitPos + Vec(0, 0, .01f), ref noHitCount) - d);
return hitType;
}
return 0;
}
static Vec Trace(Vec origin, Vec direction) {
Vec sampledPosition = 0, normal = 0, color = 0, attenuation = 1;
Vec lightDirection = (!Vec(.6f, .6f, 1f)); // Directional light
for (int bounceCount = 3; bounceCount > 0; bounceCount--) {
int hitType = RayMarching(origin, direction, ref sampledPosition, ref normal);
if (hitType == HIT_NONE) break; // No hit. This is over, return color.
if (hitType == HIT_LETTER) { // Specular bounce on a letter. No color acc.
direction = direction + normal * (normal % direction * -2);
origin = sampledPosition + direction * 0.1f;
attenuation = attenuation * 0.2f; // Attenuation via distance traveled.
}
if (hitType == HIT_WALL) { // Wall hit uses color yellow?
float incidence = normal % lightDirection;
float p = 6.283185f * randomVal();
float c = randomVal();
float s = sqrtf(1 - c);
float g = normal.z < 0 ? -1 : 1;
float u = -1 / (g + normal.z);
float v = normal.x * normal.y * u;
direction = Vec(v,
g + normal.y * normal.y * u,
-normal.y) * (cosf(p) * s)
+
Vec(1 + g * normal.x * normal.x * u,
g * v,
-g * normal.x) * (sinf(p) * s) + normal * sqrtf(c);
origin = sampledPosition + direction * .1f;
attenuation = attenuation * 0.2f;
if (incidence > 0 &&
RayMarching(sampledPosition + normal * .1f,
lightDirection,
ref sampledPosition,
ref normal) == HIT_SUN)
color = color + attenuation * Vec(500, 400, 100) * incidence;
}
if (hitType == HIT_SUN) { //
color = color + attenuation * Vec(50, 80, 100); break; // Sun Color
}
}
return color;
}
static void Main(string[] args) {
int w = 960, h = 540, samplesCount = 2; //8;
Vec position = Vec(-22, 5, 25);
Vec goal = !(Vec(-3, 4, 0) + position * -1);
Vec left = !Vec(goal.z, 0, -goal.x) * (1.0f / w);
var framework = Assembly
.GetEntryAssembly()?
.GetCustomAttribute<TargetFrameworkAttribute>()
?.FrameworkName;
Console.WriteLine($"C# Code - {framework ?? "Unknown"}");
// Cross-product to get the up vector
Vec up = Vec(
goal.y * left.z - goal.z * left.y,
goal.z * left.x - goal.x * left.z,
goal.x * left.y - goal.y * left.x);
string fileName = "output-C#.ppm";
Console.WriteLine($"Width = {w}, Height = {h}, Samples = {samplesCount}");
Console.WriteLine($"Writing data to {fileName}", fileName);
if (File.Exists(fileName))
File.Delete(fileName);
using (var fileStream = File.Open(fileName, FileMode.CreateNew, FileAccess.Write))
using (var writer = new BinaryWriter(fileStream, Encoding.ASCII)) {
writer.Write(Encoding.ASCII.GetBytes($"P6 {w} {h} 255 ")); // trailing space!!!
for (int y = (h-1); y >= 0; y--) {
for (int x = (w-1); x >= 0; x--) {
Vec color = 0;
for (int p = samplesCount - 1; p >= 0; p--) {
color = color + Trace(position, !(goal + left * (x - w / 2 + randomVal()) + up * (y - h / 2 + randomVal())));
}
// Reinhard tone mapping
color = color * (1.0f / samplesCount) + 14.0f / 241;
Vec o = color + 1;
color = Vec(color.x / o.x, color.y / o.y, color.z / o.z) * 255;
writer.Write(new byte [] { (byte)(int)color.x, (byte)(int)color.y, (byte)(int)color.z });
}
}
}
}
}
}
@sonnemaf
Copy link

sonnemaf commented Mar 1, 2019

Hi Matt,

I really like your example. I changed your 3 public fields (x,y,z) in the Vec class to AutoProperties. Like this

public struct Vec {
    //public float x, y, z;

    public float x { get; set; }
    public float y { get; set; }
    public float z { get; set; }

This made the code almost 3 times slower. Can you explain that? I can't find any difference in the Inlining result.

Fons

@pilotMike
Copy link

Hi Matt,

I really like your example. I changed your 3 public fields (x,y,z) in the Vec class to AutoProperties. Like this

public struct Vec {
    //public float x, y, z;

    public float x { get; set; }
    public float y { get; set; }
    public float z { get; set; }

This made the code almost 3 times slower. Can you explain that? I can't find any difference in the Inlining result.

Fons

The chsarp team did something similar with value tuples and they can explain it better than I can https://msdn.microsoft.com/en-us/magazine/mt846725.aspx

@sonnemaf
Copy link

sonnemaf commented Mar 4, 2019

Thanks @pilotMike, The MSDN article is a great read.

I have also tried a solution in which the Vec class is a readonly struct (using C# 7.2).

public readonly struct Vec {

    //public float x, y, z;
    public readonly float x, y, z;

The Vec class is not Mutable so we don't need to use public fields any more (see also https://blogs.msdn.microsoft.com/seteplia/2018/03/07/the-in-modifier-and-the-readonly-structs-in-c/). I expected the same performance as with public fields but it is still much slower (the same as before). It looks like the properties are not inlined correctly. Any Idea on that?

public readonly struct Vec {
    //public readonly float x, y, z;
    public float x { get; }
    public float y { get; }
    public float z { get; }

I modified the QueryDatabase() method a bit so it could cope with the fact that Vec is readonly now.

static float QueryDatabase(Vec position, ref int hitType) {
    float distance = float.MaxValue; // 1e9;
    Vec f = new Vec(position.x, position.y, 0); // Flattened position (z=0)
    //Vec f = position; // Flattened position (z=0)
    //f.z = 0;

    for (int i = 0; i < letters.Length; i += 4) {
        Vec begin = Vec(letters[i] - 79, letters[i + 1] - 79) * .5f;
        Vec e = Vec(letters[i + 2] - 79, letters[i + 3] - 79) * .5f + begin * -1f;
        Vec o = f + (begin + e * min(-min((begin + f * -1) % e / (e % e),
                                            0),
                                        1)
                    ) * -1;
        distance = min(distance, o % o); // compare squared distance.
    }
    distance = sqrtf(distance); // Get real distance, not square distance.

    for (int i = 1; i >= 0; i--) {
        Vec o = f + curves[i] * -1;
        // I *think* this equivalent to the C++ 'conditional expression', see https://stackoverflow.com/a/16676940
        float temp = 0.0f;
        if (o.x > 0) {
            temp = fabsf(sqrtf(o % o) - 2);
        } else {
            o = new Vec(o.x, o.y + (o.y > 0 ? -2 : 2), o.z);
            //o.y += (o.y > 0 ? -2 : 2);
            temp = sqrtf(o % o);
        }
        distance = min(distance, temp);
    }
    distance = powf(powf(distance, 8) + powf(position.z, 8), 0.125f) - 0.5f;
    hitType = HIT_LETTER;

    float roomDist;
    roomDist = min(// min(A,B) = Union with Constructive solid geometry
                    //-min carves an empty space
                    -min(// Lower room
                        BoxTest(position, Vec(-30, -0.5f, -30), Vec(30, 18, 30)),
                        // Upper room
                        BoxTest(position, Vec(-25, 17, -25), Vec(25, 20, 25))
                    ),
                    BoxTest( // Ceiling "planks" spaced 8 units apart.
                    Vec(fmodf(fabsf(position.x), 8),
                        position.y,
                        position.z),
                    Vec(1.5f, 18.5f, -25),
                    Vec(6.5f, 20, 25)
                    )
    );
    if (roomDist < distance) { distance = roomDist; hitType = HIT_WALL; };

    float sun = 19.9f - position.y; // Everything above 19.9 is light source.
    if (sun < distance) { distance = sun; hitType = HIT_SUN; }

    return distance;
}

@sonnemaf
Copy link

sonnemaf commented Mar 4, 2019

I have created a Benchmark wich only uses the * operator on a Vec class with Fields or Properties. The version with the Fields is more than 3 times faster.

BenchmarkDotNet=v0.11.4, OS=Windows 10.0.18348
Intel Core i7-2640M CPU 2.80GHz (Sandy Bridge), 1 CPU, 4 logical and 2 physical cores
.NET Core SDK=2.2.200
  [Host]     : .NET Core 2.2.2 (CoreCLR 4.6.27317.07, CoreFX 4.6.27318.02), 64bit RyuJIT
  DefaultJob : .NET Core 2.2.2 (CoreCLR 4.6.27317.07, CoreFX 4.6.27318.02), 64bit RyuJIT


|                Method |     Mean |    Error |   StdDev | Ratio | RatioSD |
|---------------------- |---------:|---------:|---------:|------:|--------:|
|     TestVecWithFields | 149.4 us | 2.194 us | 2.053 us |  1.00 |    0.00 |
| TestVecWithProperties | 506.7 us | 3.891 us | 3.450 us |  3.39 |    0.05 |

Code:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;

namespace ConsoleApp1 {
    class Program {
        static void Main(string[] args) {
            BenchmarkRunner.Run<BM>();
        }
    }

    public class BM {

        [Benchmark(Baseline = true)]
        public VecWithFields TestVecWithFields() {
            var p = new VecWithFields(12f, 5f, 1.5f);
            var r = new VecWithFields();
            for (float i = 0; i < 100000; i++) {
                r = p * r;
            }
            return r;
        }

        [Benchmark]
        public VecWithProperties TestVecWithProperties() {
            var p = new VecWithProperties(12f, 5f, 1.5f);
            var r = new VecWithProperties(); ;
            for (int i = 0; i < 100000; i++) {
                r = p * r;
            }
            return r;
        }
    }


    public struct VecWithFields {

        public float X;
        public float Y;
        public float Z;

        public VecWithFields(float x, float y, float z) {
            this.X = x;
            this.Y = y;
            this.Z = z;
        }

        public static VecWithFields operator *(VecWithFields q, VecWithFields r) {
            return new VecWithFields(q.X * r.X, q.Y * r.Y, q.Z * r.Z);
        }
    }


    public struct VecWithProperties {

        public float X { get; set; }
        public float Y { get; set; }
        public float Z { get; set; }

        public VecWithProperties(float x, float y, float z) {
            this.X = x;
            this.Y = y;
            this.Z = z;
        }

        public static VecWithProperties operator *(VecWithProperties q, VecWithProperties r) {
            return new VecWithProperties(q.X * r.X, q.Y * r.Y, q.Z * r.Z);
        }
    }
}

@sonnemaf
Copy link

sonnemaf commented Mar 4, 2019

I think I have found the cause and a (ugly) workaround. The * operator is not inlined and should not be used if you have auto properties.

I have added a TestVecWithPropertiesNoOperator() method to my BM and it has the same performance as TestVecWithFields(). It bypasses the * operator.

public class BM {

    [Benchmark(Baseline = true)]
    public VecWithFields TestVecWithFields() {
        var p = new VecWithFields(12f, 5f, 1.5f);
        var r = new VecWithFields();
        for (int i = 0; i < 100000; i++) {
            r = p * r;
            //r = new VecWithFields(i, i, i);
        }
        return r;
    }

    [Benchmark]
    public VecWithProperties TestVecWithProperties() {
        var p = new VecWithProperties(12f, 5f, 1.5f);
        var r = new VecWithProperties(); ;
        for (int i = 0; i < 100000; i++) {
            r = p * r;
            //r = new VecWithProperties(i, i, i);
        }
        return r;
    }

    [Benchmark]
    public VecWithProperties TestVecWithPropertiesNoOperator() {
        var p = new VecWithProperties(12f, 5f, 1.5f);
        var r = new VecWithProperties(); ;
        for (int i = 0; i < 100000; i++) {
            //r = p * r;
            r = new VecWithProperties(p.X * r.X, p.Y * r.Y, p.Z * r.Z);
        }
        return r;
    }
}

Summary

BenchmarkDotNet=v0.11.4, OS=Windows 10.0.18348
Intel Core i7-2640M CPU 2.80GHz (Sandy Bridge), 1 CPU, 4 logical and 2 physical cores
.NET Core SDK=2.2.200
  [Host]     : .NET Core 2.2.2 (CoreCLR 4.6.27317.07, CoreFX 4.6.27318.02), 64bit RyuJIT
  DefaultJob : .NET Core 2.2.2 (CoreCLR 4.6.27317.07, CoreFX 4.6.27318.02), 64bit RyuJIT


|                          Method |     Mean |    Error |   StdDev | Ratio | RatioSD |
|-------------------------------- |---------:|---------:|---------:|------:|--------:|
|               TestVecWithFields | 152.6 us | 2.106 us | 1.970 us |  1.00 |    0.00 |
|           TestVecWithProperties | 517.3 us | 7.359 us | 6.523 us |  3.39 |    0.08 |
| TestVecWithPropertiesNoOperator | 155.7 us | 3.092 us | 3.911 us |  1.02 |    0.03 |

I have created this coreclr issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment