Doing Raytracer in Crystal

Absolutely!

The thing is, I thought LLVM was smarter. Because semantically they are equivalent. Maybe because it’s a union, so a struct, something about load/store things aren’t well optimized on our side. So doing that might take care of things in a better way (there’s a lot of redundant load/store in the LLVM code that we generate.)

1 Like

OK, I’m finished with this now.

For Crystal newbies, and people coming from other languages, do the following to compile and run the source code.

  1. To use stumpy_png shard, create file (shown below): shard.yml
  2. Place it in the same dir|folder as the source filename.cr file.
  3. Run (need internet connection): $ shards init then $ shards install
  4. Compile *.cr source file: $ crystal build filename.cr --release
  5. Run compiled code (no *.cr): $ ./filename
  6. A filename.png file will be created in dir, which you can view.

Now you can run any source file that needs the stumpy_png shard from this dir.
Run the original code, and compare with this version, to see performance increase.
To check if images are the same do: $ diff file1.png file2.png

It was fun, interesting, and I learned a few things too.

I’ll probably write an article to share the details of what|why I did what I did to create the code. Particularly for people coming from Ruby, et al dynamic languages, they really need to understand how to optimize performance using static typing and how to make the compiler your friend, and work for you, and how|what|why to test, test, test!

shard.yml

name: shards
version: 1.2.3
crystal: '>= 0.34.0'

dependencies:
  stumpy_png:
    github: stumpycr/stumpy_png
    version: "~> 5.0"

crystal-raytracer-opt.cr

require "stumpy_png"

struct Vector
  getter x, y, z

  def initialize(@x : Float64, @y : Float64, @z : Float64) end

  def self.minus(v1, v2) Vector.new(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z) end

  def self.plus(v1, v2)  Vector.new(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z) end
      
  def self.scale(k, v)   Vector.new(k * v.x, k * v.y, k * v.z) end

  def self.dot(v1, v2) v1.x * v2.x + v1.y * v2.y + v1.z * v2.z end
      
  def self.mag(v) Math.sqrt(Vector.dot(v, v)) end

  def self.norm(v)
    mag = Vector.mag(v)
    div = (mag == 0) ? Float64::INFINITY : 1.0 / mag
    Vector.scale(div, v)
  end

  def self.cross(v1, v2)
    Vector.new(v1.y * v2.z - v1.z * v2.y, v1.z * v2.x - v1.x * v2.z, v1.x * v2.y - v1.y * v2.x)
  end
end

struct Color
  getter r, g, b

  def initialize(@r : Float64, @g : Float64, @b : Float64)   end

  def self.scale(k, v)  Color.new(k * v.r, k * v.g, k * v.b) end

  def self.plus(v1, v2) Color.new(v1.r + v2.r, v1.g + v2.g, v1.b + v2.b) end

  def self.mult(v1, v2) Color.new(v1.r * v2.r, v1.g * v2.g, v1.b * v2.b) end
      
  def self.toDrawingColor(c)
    r = (c.r.clamp(0.0, 1.0)*255).floor
    g = (c.g.clamp(0.0, 1.0)*255).floor
    b = (c.b.clamp(0.0, 1.0)*255).floor
    {r, g, b}
  end
end

Color_white        = Color.new(1.0, 1.0, 1.0)
Color_grey         = Color.new(0.5, 0.5, 0.5)
Color_black        = Color.new(0.0, 0.0, 0.0)
Color_background   = Color_black
Color_defaultColor = Color_black

class Camera
  getter pos, forward, right, up

  def initialize(pos : Vector, lookAt)
    down = Vector.new(0.0, -1.0, 0.0)
    @pos = pos
    @forward = Vector.norm(Vector.minus(lookAt, @pos))
    @right = Vector.scale(1.5, Vector.norm(Vector.cross(@forward, down)))
    @up = Vector.scale(1.5, Vector.norm(Vector.cross(@forward, @right)))
  end
end

record Ray, start : Vector, dir : Vector

record Intersection, thing : Thing, ray : Ray, dist : Float64

record Light, pos : Vector, color : Color

abstract class Thing end

class Sphere < Thing
  @radius2 : Float64

  def initialize(@center : Vector, radius : Float64, @_surface : Surface)
    @radius2 = radius*radius
  end

  def normal(pos) Vector.norm(Vector.minus(pos, @center)) end

  def surface; @_surface end

  def intersect(ray)
    eo = Vector.minus(@center, ray.start)
    v  = Vector.dot(eo, ray.dir)
    dist = 0.0
    if (v >= 0)
      disc = @radius2 - (Vector.dot(eo, eo) - v * v)
      dist = v - Math.sqrt(disc) if (disc >= 0)    
    end
    
    (dist == 0) ? nil : Intersection.new(self, ray, dist)
  end
end

class Plane < Thing
  def initialize(@_norm : Vector, @offset : Float64, @_surface : Surface) end

  def normal(pos) @_norm end

  def surface; @_surface end

  def intersect(ray)
    denom = Vector.dot(@_norm, ray.dir)
    return nil if denom > 0
    dist = (Vector.dot(@_norm, ray.start) + @offset) / (-denom)
    Intersection.new(self, ray, dist)
  end
end

abstract class Surface end

class ShinySurface < Surface
  def diffuse(pos) Color_white end

  def specular(pos) Color_grey end

  def reflect(pos) 0.7 end

  def roughness; 250 end
end

class CheckerboardSurface < Surface
  def diffuse(pos) ((pos.z).floor + (pos.x).floor).to_i.odd? ? Color_white : Color_black end
  
  def reflect(pos) ((pos.z).floor + (pos.x).floor).to_i.odd? ? 0.1 : 0.7 end

  def specular(pos) Color_white end

  def roughness; 250 end
end

Surface_shiny        = ShinySurface.new
Surface_checkerboard = CheckerboardSurface.new

class RayTracer
  MaxDepth = 5

  def intersections(ray, scene)
    closest = Float64::INFINITY
    closestInter = nil
    scene.things.each do |item|
      inter = item.intersect(ray)
      if inter && inter.dist < closest
        closestInter = inter
        closest = inter.dist
      end 
    end
    closestInter
  end

  def testRay(ray, scene)
    isect = self.intersections(ray, scene)
    isect && isect.dist
  end

  def traceRay(ray, scene, depth)
    isect = self.intersections(ray, scene)
    isect.nil? ? Color_background : self.shade(isect, scene, depth)
  end

  def shade(isect : Intersection, scene, depth)
    d = isect.ray.dir
    pos = Vector.plus(Vector.scale(isect.dist, d), isect.ray.start)
    normal = isect.thing.normal(pos)
    reflectDir = Vector.minus(d, Vector.scale(2, Vector.scale(Vector.dot(normal, d), normal)))
    naturalColor = Color.plus(Color_background, self.getNaturalColor(isect.thing, pos, normal, reflectDir, scene))

    reflectedColor = (depth >= MaxDepth) ? Color_grey :
                     self.getReflectionColor(isect.thing, pos, normal, reflectDir, scene, depth)

    Color.plus(naturalColor, reflectedColor)
  end

  def getReflectionColor(thing, pos, normal, rd, scene, depth)
    Color.scale(thing.surface.reflect(pos), self.traceRay(Ray.new(pos, rd), scene, depth + 1))
  end

  def getNaturalColor(thing, pos, norm, rd, scene)
    color = Color_defaultColor
    scene.lights.each { |light| color = self.addLight(color, light, pos, norm, scene, thing, rd) }
    color
  end

  def addLight(col, light, pos, norm, scene, thing, rd)
    ldis  = Vector.minus(light.pos, pos)
    livec = Vector.norm(ldis)
    neatIsect = self.testRay(Ray.new(pos, livec), scene)

    isInShadow = neatIsect && neatIsect <= Vector.mag(ldis)
    return col if isInShadow

    illum  = Vector.dot(livec, norm)
    lcolor = (illum > 0) ? Color.scale(illum, light.color) : Color_defaultColor

    specular = Vector.dot(livec, Vector.norm(rd))
    scolor = (specular > 0) ? Color.scale(specular ** thing.surface.roughness, light.color) : Color_defaultColor

    Color.plus(col, Color.plus(Color.mult(thing.surface.diffuse(pos), lcolor), Color.mult(thing.surface.specular(pos), scolor)))
  end

  def getPoint(x : Int32, y : Int32, screenWidth : Int32, screenHeight : Int32, camera)
    recenterX =  (x - (screenWidth  >> 1)) / (screenWidth  << 1)
    recenterY = -(y - (screenHeight >> 1)) / (screenHeight << 1)
    Vector.norm(Vector.plus(camera.forward, Vector.plus(Vector.scale(recenterX, camera.right), Vector.scale(recenterY, camera.up))))
  end

  def render(scene, image, screenWidth, screenHeight)
    screenHeight.times do |y|
      screenWidth.times do |x|
        color = self.traceRay(Ray.new(scene.camera.pos, self.getPoint(x, y, screenWidth, screenHeight, scene.camera)), scene, 0)
        r, g, b = Color.toDrawingColor(color)
        image.set(x, y, StumpyCore::RGBA.from_rgb(r, g, b))
  end end end
end

class DefaultScene
  getter :things, :lights, :camera

  def initialize
    @things = [
      Plane.new(Vector.new(0.0, 1.0, 0.0), 0.0, Surface_checkerboard),
      Sphere.new(Vector.new(0.0, 1.0, -0.25), 1.0, Surface_shiny),
      Sphere.new(Vector.new(-1.0, 0.5, 1.5), 0.5, Surface_shiny),
    ]
    @lights = [
      Light.new(Vector.new(-2.0, 2.5, 0.0), Color.new(0.49, 0.07, 0.07)),
      Light.new(Vector.new(1.5, 2.5, 1.5),  Color.new(0.07, 0.07, 0.49)),
      Light.new(Vector.new(1.5, 2.5, -1.5), Color.new(0.07, 0.49, 0.071)),
      Light.new(Vector.new(0.0, 3.5, 0.0),  Color.new(0.21, 0.21, 0.35)),
    ]
    @camera = Camera.new(Vector.new(3.0, 2.0, 4.0), Vector.new(-1.0, 0.5, 0.0))
  end
end

width, height = 500, 500
image = StumpyCore::Canvas.new(width, height)

n = 1000
t1 = Time.monotonic
n.times do
rayTracer = RayTracer.new
scene = DefaultScene.new
rayTracer.render(scene, image, width, height)
end
t2 = (Time.monotonic - t1).total_milliseconds

puts "total time for #{n} iterations = #{t2} ms, avg time = #{t2/n} ms"

#puts "Completed in #{t2} ms"

StumpyPNG.write(image, "crystal-raytracer-opt.png")

It would be nice for people to run the original code and this version and post times here (with system specs). I would really like to see how this performs on newer|different hardware systems, than what I have.

Using the timing process here, I’ll redo the other languages and post their results later.

It may also be best if one of the devs submits this to the website, to reformat it as
“standard” Crystal code, if they want.

3 Likes
$ crystal build --release crystal-raytracer-orig.cr
$ ./crystal-raytracer-orig
total time for 1000 iterations = 179669.659491 ms, avg time = 179.669659491 ms
$ crystal build --release crystal-raytracer-opt.cr
$ ./crystal-raytracer-opt
total time for 1000 iterations = 149492.357071 ms, avg time = 149.49235707100001 ms
$ crystal --version
Crystal 0.35.1 (2020-06-19)

LLVM: 10.0.0
Default target: x86_64-apple-macosx
$ sysctl -n machdep.cpu.brand_string
Intel(R) Core(TM) i7-3615QM CPU @ 2.30GHz

Thanks @vlazar, that ratio is consistent with what I’m seeing with 1.0.0 on my i7 and i5 laptops.

Anybody with AMD Ryzen or ARM (Raspberry PI) systems?

And I fixed typos in instructions: shard -> shards and -- release -> --release.

ADDITION:
Found another tweak (maybe compiler already does equivalent?).

def addLight(col, light, pos, norm, scene, thing, rd)
    ---
    
    # from
    isInShadow = (neatIsect) ? neatIsect <= Vector.mag(ldis) : false
    return col if isInShadow

    # to
    isInShadow = neatIsect && neatIsect <= Vector.mag(ldis)
    return col if isInShadow

also

  def testRay(ray, scene)
    isect = self.intersections(ray, scene)
    #isect ? isect.dist : nil
    isect && isect.dist
  end

Here is one with the AMD Ryzen
(WSL2 Win10)

$ ./crystal-raytracer-orig
total time for 1000 iterations = 81132.2264 ms, avg time = 81.1322264 ms

$ ./crystal-raytracer-opt
total time for 1000 iterations = 63051.6238 ms, avg time = 63.0516238 ms

$ crystal --version
Crystal 1.0.0 [dd40a2442] (2021-03-22)

LLVM: 10.0.0
Default target: x86_64-unknown-linux-gnu

$ sudo lshw -C CPU
    *-cpu
        product: AMD Ryzen 7 3700X 8-Core Processor
2 Likes

Nice! That’s even faster. (63/81 = 0.777) < (149/179 = 0.832)

Here is the optimized Ruby version that mimics the Crystal version.
The performance differences aren’t as large because Ruby (being dynamic)
can’t optimally compile code, which is first translated to C (for CRuby).

I also posted this in the Ruby-Forum to show the Ruby community the
benefits of using Crystal (vs C, Rust, etc) to speedup their critical
code segments, now that Crystal has hit 1.0.0.

ruby-raytracer-opt.rb

# Following libs are required:
# $ gem install imageruby imageruby-bmp

#require "rubygems"
require "imageruby"

class Vector
  attr_accessor :x , :y , :z
  def initialize(x, y, z)
    @x, @y, @z = x, y, z
  end

  def self.scale(k, v)
    Vector.new(k * v.x, k * v.y, k * v.z)
  end

  def self.minus(v1, v2)
    Vector.new(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z)
  end

  def self.plus(v1, v2)
    Vector.new(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z)
  end

  def self.dot(v1, v2)
    v1.x * v2.x + v1.y * v2.y + v1.z * v2.z
  end

  def self.mag(v)
    Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
  end

  def self.norm(v)
    mag = Vector.mag(v)
    div = (mag == 0) ? Float::INFINITY : 1.0 / mag
    Vector.scale(div, v)
  end

  def self.cross(v1, v2)
    Vector.new(v1.y * v2.z - v1.z * v2.y, v1.z * v2.x - v1.x * v2.z, v1.x * v2.y - v1.y * v2.x)
  end
end

class Color
  attr_accessor :r, :g, :b

  def initialize(r, g, b)
    @r, @g, @b = r, g, b
  end

  def self.scale(k, v)
    Color.new(k * v.r, k * v.g, k * v.b)
  end

  def self.plus(v1, v2)
    Color.new(v1.r + v2.r, v1.g + v2.g, v1.b + v2.b)
  end

  def self.mult(v1, v2)
    Color.new(v1.r * v2.r, v1.g * v2.g, v1.b * v2.b)
  end

  def self.toDrawingColor(c)
    r = (c.r.clamp(0.0, 1.0)*255).floor
    g = (c.g.clamp(0.0, 1.0)*255).floor
    b = (c.b.clamp(0.0, 1.0)*255).floor
    return r, g ,b
  end
end

Color_white        = Color.new(1.0, 1.0, 1.0)
Color_grey         = Color.new(0.5, 0.5, 0.5)
Color_black        = Color.new(0.0, 0.0, 0.0)
Color_background   = Color_black
Color_defaultColor = Color_black

class Camera
  attr_accessor :pos, :forward, :right, :up
  def initialize(pos, lookAt)
    down     = Vector.new(0.0, -1.0, 0.0)
    @pos     = pos
    @forward = Vector.norm(Vector.minus(lookAt, @pos))
    @right   = Vector.scale(1.5, Vector.norm(Vector.cross(@forward, down)))
    @up      = Vector.scale(1.5, Vector.norm(Vector.cross(@forward, @right)))
  end
end

class Ray
  attr_accessor :start, :dir
  def initialize(start, dir)
    @start, @dir = start, dir
  end
end

class Intersection
  attr_accessor :thing, :ray, :dist
  def initialize(thing, ray, dist)
    @thing, @ray, @dist = thing, ray, dist
  end
end

class Light
  attr_accessor :pos, :color
  def initialize(pos, color)
    @pos, @color = pos, color
  end
end

class Sphere
  def initialize(center, radius, surface)
    @radius2, @_surface, @center = radius*radius, surface, center
  end

  def normal(pos)
    Vector.norm(Vector.minus(pos, @center))
  end

  def surface()
    @_surface
  end

  def intersect(ray)
    eo = Vector.minus(@center, ray.start)
    v  = Vector.dot(eo, ray.dir)
    dist = 0
    if (v >= 0)
      disc = @radius2 - (Vector.dot(eo, eo) - v * v)
      dist = v - Math.sqrt(disc) if (disc >= 0)
    end
    (dist == 0) ? nil : Intersection.new(self, ray, dist)
  end
end

class Plane
  def initialize(norm, offset, surface)
    @_norm, @_surface, @offset = norm, surface, offset
  end

  def normal(pos)
    @_norm
  end

  def intersect(ray)
    denom = Vector.dot(@_norm, ray.dir)
    return nil if (denom > 0)
    dist = (Vector.dot(@_norm, ray.start) + @offset) / (-denom)
    Intersection.new(self, ray, dist)
  end

  def surface()
    @_surface
  end
end

class ShinySurface
  def diffuse(pos)
    Color_white
  end

  def specular(pos)
    Color_grey
  end

  def reflect(pos)
    0.7
  end

  def roughness()
    250
  end
end

class CheckerboardSurface
  def diffuse(pos)
    ((pos.z).floor + (pos.x).floor).to_i.odd? ? Color_white : Color_black
  end

  def reflect(pos)
    ((pos.z).floor + (pos.x).floor).to_i.odd? ? 0.1 : 0.7
  end

  def specular(pos)
    Color_white
  end

  def roughness()
    250
  end
end

Surface_shiny        = ShinySurface.new
Surface_checkerboard = CheckerboardSurface.new

class RayTracer
  MaxDepth = 5

  def intersections(ray, scene)
    closest = Float::INFINITY
    closestInter = nil
    scene.things.each do |item|
      inter = item.intersect(ray)
      if inter && inter.dist < closest
        closestInter = inter
        closest = inter.dist
      end
    end
    closestInter
  end

  def testRay(ray, scene)
    isect = self.intersections(ray, scene)
    isect && isect.dist
  end

  def traceRay(ray, scene, depth)
    isect = self.intersections(ray, scene)
    isect == nil ? Color_background : self.shade(isect, scene, depth)
  end

  def shade(isect, scene, depth)
    d = isect.ray.dir
    pos = Vector.plus(Vector.scale(isect.dist, d), isect.ray.start)
    normal = isect.thing.normal(pos)
    reflectDir   = Vector.minus(d, Vector.scale(2, Vector.scale(Vector.dot(normal, d), normal)))
    naturalColor = Color.plus(Color_background,self.getNaturalColor(isect.thing, pos, normal, reflectDir, scene))
    reflectedColor = (depth >= MaxDepth) ? Color_grey : self.getReflectionColor(isect.thing, pos, normal, reflectDir, scene, depth)
    Color.plus(naturalColor, reflectedColor)
  end

  def getReflectionColor(thing, pos, normal, rd, scene, depth)
    Color.scale(thing.surface().reflect(pos), self.traceRay(Ray.new(pos, rd), scene, depth + 1))
  end

  def getNaturalColor(thing, pos, norm, rd, scene)
    color = Color_defaultColor
    scene.lights.each { |light| color = self.addLight(color, light, pos, norm, scene, thing, rd) }
    color
  end

  def addLight(col, light, pos, norm, scene, thing, rd)
    ldis  = Vector.minus(light.pos, pos)
    livec = Vector.norm(ldis)
    neatIsect = self.testRay(Ray.new(pos, livec), scene)

    isInShadow = neatIsect && neatIsect <= Vector.mag(ldis)
    return col if isInShadow

    illum  = Vector.dot(livec, norm)
    lcolor = (illum > 0) ? Color.scale(illum, light.color) : Color_defaultColor

    specular = Vector.dot(livec, Vector.norm(rd))
    scolor = (specular > 0) ? Color.scale(specular ** thing.surface().roughness(), light.color) : Color_defaultColor

    Color.plus(col, Color.plus(Color.mult(thing.surface().diffuse(pos), lcolor), Color.mult(thing.surface().specular(pos), scolor)))
  end

  def getPoint(x, y, screenWidth, screenHeight, camera)
    recenterX =  (x - (screenWidth  * 0.5)) / (screenWidth  * 2.0)
    recenterY = -(y - (screenHeight * 0.5)) / (screenHeight * 2.0)
    Vector.norm(Vector.plus(camera.forward, Vector.plus(Vector.scale(recenterX, camera.right), Vector.scale(recenterY, camera.up))))
  end

  def render(scene, image, screenWidth, screenHeight)
    screenHeight.times do |y|
      screenWidth.times do |x|
        color = self.traceRay(Ray.new(scene.camera().pos, self.getPoint(x, y, screenWidth, screenHeight, scene.camera())), scene, 0)
        r,g,b = Color.toDrawingColor(color)
        image.set_pixel(x,y, ImageRuby::Color.from_rgba(r,g,b, 255))
  end end end
end

class DefaultScene
  attr_accessor :things, :lights, :camera
  def initialize
    @things = [
      Plane.new(Vector.new(0.0, 1.0, 0.0), 0.0, Surface_checkerboard),
      Sphere.new(Vector.new(0.0, 1.0, -0.25), 1.0, Surface_shiny),
      Sphere.new(Vector.new(-1.0, 0.5, 1.5), 0.5, Surface_shiny)
    ]
    @lights = [
      Light.new(Vector.new(-2.0, 2.5, 0.0), Color.new(0.49, 0.07, 0.07)),
      Light.new(Vector.new(1.5, 2.5, 1.5),  Color.new(0.07, 0.07, 0.49)),
      Light.new(Vector.new(1.5, 2.5, -1.5), Color.new(0.07, 0.49, 0.071)),
      Light.new(Vector.new(0.0, 3.5, 0.0),  Color.new(0.21, 0.21, 0.35))
    ]
    @camera = Camera.new(Vector.new(3.0, 2.0, 4.0), Vector.new(-1.0, 0.5, 0.0))
  end
end

width, height  = 500, 500
image = ImageRuby::Image.new(width, height, ImageRuby::Color.black)

t1 = Time.now
rayTracer = RayTracer.new()
scene     = DefaultScene.new()
rayTracer.render(scene, image, width, height)
t2 = (Time.now - t1) * 1000

puts "Completed in #{t2} ms"

image.save("ruby-raytracer-opt.bmp", :bmp
1 Like

Nice article, @jzakiya ! (Ruby Raytracer in Crystal - Ruby-Forum) :slight_smile: I didn’t see a run times comparison table in Ruby Raytracer in Crystal - Ruby-Forum . Did you plan to add that or just submit updated times for/to GitHub - edin/raytracer: Performance comparison of various compilers?

I do see JRuby is fastest, which might make some readers wonder if you’re saying that JRuby is faster than Crystal also. I assume you didn’t mean that, but figured you might want to clarify.

Remember, I’m posting this in the Ruby forum.

I was saying running the Ruby code with JRuby was faster than the other Ruby versions I listed (Ruby 2.6.7 and 2.7.3). I had already said the Crystal version was orders of magnitude faster. Hopefully the Ruby readers were clear on that.

What I hope is Rubyist will at least compare the runtimes of both Ruby versions to see that difference, and then be curious (compelled) enough to compile the Crystal version and confirm for themselves the orders of magnitude performance increase.

Because my rvm is broken (and they haven’t bothered to fix the issue I posted there) I can’t install the needed gems for Ruby so I can’t run it on Ruby 3.0.1 and Truffleruby. Both of those also have jits to optimize speed. Hopefully, someone will be curious enough to do that too.

I have on my TODO to write up a proper article on the Ruby->Crystal translation process, but it will have to wait for now, until I can take the time.

But feel free to post in the Ruby-Forum suggestions, issues, etc, to help promote Rubyist get in their heads, hey, maybe we need to check out Crystal now!

If the Rails people pick up on how easy it is to write code to run on Crystal, and get an immediate X-times performance increase (and concurrency), that could be a killer application for Crystal, especially in certain gems.

4 Likes

I’ve checked several MRI versions against Crystal 1.0

I didn’t wanted to install Java and haven’t tested JRuby for that reason.

I’ve tried to run benchmark with Truffleruby too, but imageruby gem is not working:

% asdf shell ruby truffleruby-21.0.0

% ruby ruby-raytracer-opt.rb
[ruby] WARNING OutOfMemoryError
<internal:core> core/string.rb:1059:in `assign_index': failed to allocate memory (NoMemoryError)
	from <internal:core> core/string.rb:1039:in `[]='
	from /Users/xxxxxx/.asdf/installs/ruby/truffleruby-21.0.0/lib/gems/gems/imageruby-0.2.5/lib/imageruby/bitmap/rbbitmap.rb:71:in `set_pixel'
	from ruby-raytracer-opt.rb:268:in `block (2 levels) in render'
	from <internal:core> core/integer.rb:140:in `times'
	from ruby-raytracer-opt.rb:265:in `block in render'
	from <internal:core> core/integer.rb:140:in `times'
	from ruby-raytracer-opt.rb:264:in `render'
	from ruby-raytracer-opt.rb:298:in `block in <main>'
	from <internal:core> core/integer.rb:140:in `times'
	from ruby-raytracer-opt.rb:295:in `<main>'
1 Like

@jzakiya , I noticed your code was added via crystal-raytracer.cr by jzakiya · Pull Request #9 · edin/raytracer · GitHub ; Yay! But, you might want to rename it to something like RayTracer.cr and put it into a ‘crystal’ or ‘crystal-lang’ sub-folder, along with other applicable file(s), probably at least README.md and shards.yml files. Also, an update for the top-level README.md noting values for Crystal would be great, but for fair comparison, we’d need it run on a similar system. ( AMD FX-8120 CPU is noted in the current README.md ; but it doesn’t specify the ram, os, etc.) I don’t have that system, but if you want I can help with the file reorg/rename/add stuff. See also crystal init --help .

Thanks @drhuffman12.

Yes, I could use some help, as I’m not really familiar with using git[hub].
I thought he would just put things in the correct format, as I also sent him the shard.yml file to install the shard to create the png image.

Take a look at the closed PRs and see the responses to mine, and let me know what needs to be done to create a folder like all the rest, and placing the source and shard.yml and install instructions in the right places.

I also recommend running crystal tool format file.cr to make the file follow the recommend file formatting.

Maybe also following the style guide: Coding style - Crystal

For instance, method names are snake_case, never camelCase.

@jzakiya , I’m thinking we start with creating a shard that has most of the pieces of your code (so others can re-use it), along with an ‘examples’ folder containing the code that will later be re-submitted to GitHub - edin/raytracer: Performance comparison of various compilers. This way, we can work thru the steps of splitting it up, formatting, adding tests, etc. so that we make sure we (or I) didn’t break things along the way. I started a repo at: https://github.com/drhuffman12/crystal_ray_tracer/; send me your github handle and I’ll add you as a contributor/editor.

For the first PR (drhuffman12_based_on_jzakiya by drhuffman12 · Pull Request #1 · drhuffman12/crystal_ray_tracer · GitHub), I basically just set up the repo and pulled in your code basically as-is; and I made sure I could run it and generate the example image. See the README.md file for instructions.

For the next PR (drhuffman12_split_code_and_add_tests by drhuffman12 · Pull Request #2 · drhuffman12/crystal_ray_tracer · GitHub), I started reorganizing your code into smaller pieces under the src folder, add tests, etc. The PR has a couple simple specs (all of which pass), but before I add more tests, I need to look into Windows Compatibility for both Ameba (Windows compatibility? · Issue #230 · crystal-ameba/ameba · GitHub) and Spectator (Windows compatibility? (#58) · Issues · Mike Miller / Spectator · GitLab), which will affect how I might deal with linting/etc and how I might code the tests.

I noticed you use the same handle for your github account, so I sent you an invite for this repo. I’d be glad to collaborate with you on this.

1 Like

FYI, the Crystal version has now been merged and configured into its own folder.
I did a PR, and raised issue, to add using --release for compilation (he has done this consistently with other languages too, leaving off opt compile flags).

@drhuffman12 I accepted your invite, so hopefully that’s all good now.

@asterite I requested one of the devs submit this, and reformat it if desired, but got no response, so I submitted it to get the process going, since other languages were being added, and existing ones upgraded. I’m going to be busy for awhile and didn’t want this to linger.

FYI, the Crystal version has now been merged and configured into its own folder.
I did a PR, and raised issue, to add using --release for compilation

Cool! I definitely would like them to add the --release flag! (And update the associated run times in the readme.md page. I’d love to see them add some wrapper scripts to build/run/time each of the language implementations, with some warm-up runs, and auto-generate updated comparison tables. I might look into that later if no one else does.)

If you want to leave it like that, that’s fine with me. If you want to proceed with further mods to GitHub - drhuffman12/crystal_ray_tracer: Based on code from jzakiya, for adding to https://github.com/edin/raytracer, let me know.

1 Like

The site updated the build file to include the --release option, and added a build README.md too.

2 Likes

See comments near bottom on Crystal performance on his system.

The site author was impressed Crystal only 30ms more than C.

He’s creating a better way to get truer|consistent timings across languages, and also check images, which I had found were different|bad in some instances.

1 Like

Cool! :slight_smile:

It might be a fun exercise for someone to do a Crystal version that creates a *.bmp image to see coding and timing differences. In the process, might also get a shard to create bmps out of it. :relaxed: