Minecraft 4k ported to the D programming language

2013-10-23

A Minecraft 4k port to the D language from C++. Based directly on Anthony Hay's port, which is in turn based on Markus Persson's javascript mk4k. You can read more about all of this on Anthony's blog

Minecraft 4k screen shot written in D

The source can be downloaded here and includes a dub build package.

You will require:

  1. A D (version 2) compiler. It has been tested with DMD (Windows+Linux), and GDC (Linux). 

  2. The dub build tool for your platform (for ease of compilation.) Dub requires an internet connection to fetch the delelict library, which is a dependency used to provide OpenGL and SDL 2 support.

What's all this about then?

Well it's a direct C++ port of Anthony's work to D. It doesn't really show of any of D's features per se, except perhaps that it shows the syntax is quite C/C++ like, and in fact D is highly interoperable with C, so much so in fact that all the standard C library calls are available directly in the language, as well as the usual C features such as pointers.Have a look at the code and then I'll mention some things:

gem.d

import std.stdio;
import derelict.sdl2.sdl;
import derelict.opengl3.gl;
import std.random;
import std.math;
import std.datetime;
import std.functional;
import core.memory;

static import std.compiler;


class gem { // graphics environment manager
public:
    this(int width, int height, float scale)
    {
        width_ = width; height_ = height; scale_ = scale;
    
        DerelictSDL2.load();
        DerelictGL.load();
        SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER | SDL_INIT_JOYSTICK);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
       // SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);

        
        
        
        window_ = SDL_CreateWindow("mc4k-dmd2", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
                                   cast(int)(width * scale), cast(int)(height * scale), SDL_WINDOW_OPENGL|SDL_WINDOW_RESIZABLE);
        if (!window_) {
            const char * sdl_error = SDL_GetError();
            printf("%s", sdl_error);
            throw new Exception("Can't do sdl");
        }
        
        maincontext_ = SDL_GL_CreateContext(window_);
        
      //  DerelictGL.reload(); can't load point parameteri opengl 1.4

        assert(null != glClear);
        assert(null != glPixelZoom);
        assert(null != glDrawPixels);
                      
           
        frame_buf_ = new uint[width_ * height_];
    }
    
    ~this()
    {
        SDL_GL_DeleteContext(maincontext_);  
    
        // Close and destroy the window
        SDL_DestroyWindow(window_);

        // Clean up
        SDL_Quit();
    }
    
    alias void function(void * render_data, uint * frame_buf) renderer;
    
   // alias void delegate(void * render_data, uint * frame_buf) renderer;
    // repeatedly display frames until user quits
    void run(renderer render, void * private_renderer_data)
    {
        int frames = 0;
        
        bool quit = false;
        while (!quit) {
        
            SDL_Event event;
            while (SDL_PollEvent(&event)) {
                switch (event.type) {
                    case SDL_QUIT: 
                        writefln("Quitting.");
                        return; 
                    case SDL_KEYUP:
                        return;
                    default: break;
                }
            }
            // do sdl drawing
            render(private_renderer_data, frame_buf_.ptr);
    
            glClear(GL_COLOR_BUFFER_BIT);
            glTranslatef(0, height_, 0);
            glPixelZoom(scale_, scale_);
            glDrawPixels(width_, height_, GL_RGBA, GL_UNSIGNED_BYTE, frame_buf_.ptr);
            
            SDL_GL_SwapWindow(window_);
            
            ++fps_;
            if (Clock.currSystemTick.length - clock_.length > TickDuration.ticksPerSec) {
         
            writefln("%d", fps_);
                fps_ = 0;
                clock_ = Clock.currSystemTick();
            }
        } 
    }
    

    int width_;  // frame buffer width in pixels
    int height_; // frame buffer height in pixels
    float scale_;  // each pxl in frame buf displayed as scale_ x scale_ pxls on screen
    int fps_;    // frames per second
    TickDuration   clock_;
    uint[] frame_buf_; // width_ x height_ pixels
    SDL_Window * window_;
    SDL_GLContext maincontext_;
};

mk4ka.d

import std.stdio;
import derelict.sdl2.sdl;
import derelict.opengl3.gl;
import std.random;
import std.math;
import std.datetime;
import std.functional;
import core.memory;

static import std.compiler;

import gem;

// the block world map is stored in a cube of side mapdim; each map entry
// determines the colour of the corresponding block
immutable int mappow = 6;
immutable int mapdim = 1 << mappow;
immutable int mapmask = mapdim - 1;

// these are the image dimentions used in Minecraft4k
immutable float scale = 2;//2;
immutable int width = 428;//428;//428;
immutable int height = 240;//240;//240;


// return map index of block at given co-ordinates
int mapindex(int x, int y, int z)
{
    return (z & mapmask) << (mappow << 1) | (y & mapmask) << mappow | (x & mapmask);
}

int mapindex(float x, float y, float z)
{
    return mapindex(cast(int)x, cast(int)y, cast(int)z);
}

import std.c.stdlib;

float frand()
{
    
    return cast(float)rand() / RAND_MAX;
    //return uniform(0f, 1f);
}

// return pixel of given colour
uint rgba(uint r, uint g, uint b)
{
    return 0xFF000000 | b << 16 | g << 8 | r;
}


// return frame buffer index of pixel at given co-ordinates; (0, 0) is top left
int framebufindex(int x, int y)
{
    
    return width * y  + x;
}

// create the world map
uint[] generate_map()
{
    uint[] map = new uint[mapdim * mapdim * mapdim];
    
    for (int x = 0; x < mapdim; x++) {
        for (int y = 0; y < mapdim; y++) {
            for (int z = 0; z < mapdim; z++) {
                float yd = (cast(float)y - 32.5f) * 0.4f;
                float zd = (cast(float)z - 32.5f) * 0.4f;
                if (frand() > sqrt(sqrt(yd * yd + zd * zd)) - 0.8 || frand() < 0.8)
                    map[mapindex(x,y,z)] = 0; // there won't be a block here
                else
                    map[mapindex(x,y,z)] = 0x00FFFFFF & cast(uint)(frand() * 0xffffff); // block colour
            }
        }
    }
    
    return map;
}

// render the next frame into the given 'frame_buf'
void render_blocks(void * private_renderer_data, uint * frame_buf)
{
    // the given 'private_renderer_data' is our world map
    uint * map = cast(uint *)private_renderer_data;
    float dx = cast(float)(Clock.currSystemTick.length % (TickDuration.ticksPerSec * 10)) / (TickDuration.ticksPerSec * 10);
    float ox = 32.5f + dx * mapdim;
    float oy = 32.5;
    float oz = 32.5;
    
    for (int x = 0; x < width; ++x) {
        const float rotzd = cast(float)(x - width / 2) / height;
        for (int y = 0; y < height; ++y) {
            const float rotyd = cast(float)(y - height / 2) / height;
            const float rotxd = 1;
            
            uint col = 0;
            uint br = 255;
            float ddist = 0;
            float closest = 32;
            for (int d = 0; d < 3; d++) {
                const float dimLength = d == 0 ? rotxd : d == 1 ? rotyd : rotzd;
                float ll = 1 / fabs(dimLength);
                float xd = rotxd * ll;
                float yd = rotyd * ll;
                float zd = rotzd * ll;
                
                float initial;
                switch (d) {
                    case 0: initial = ox - cast(int)ox; break;
                    case 1: initial = oy - cast(int)oy; break;
                    default: initial = oz - cast(int)oz; break;
                }
                if (dimLength > 0)
                    initial = 1 - initial;
                
                float dist = ll * initial;
                float xp = ox + xd * initial;
                float yp = oy + yd * initial;
                float zp = oz + zd * initial;
                
                if (dimLength < 0)
                    --(d == 0 ? xp : d == 1 ? yp : zp);
                
                while (dist < closest) {
                    uint tex = map[mapindex(xp, yp, zp)];
                    if (tex > 0) {
                        col = tex;
                        ddist = 255 - cast(int)(dist / 32 * 255);
                        br = 255 * (255 - ((d + 2) % 3) * 50) / 255;
                        closest = dist;
                    }
                    xp += xd;
                    yp += yd;
                    zp += zd;
                    dist += ll;
                }
            }
            
            immutable uint r = cast(uint)(((col >> 16) & 0xff) * br * ddist / (255 * 255));
            immutable uint g = cast(uint)(((col >> 8) & 0xff) * br * ddist / (255 * 255));
            immutable uint b = cast(uint)(((col) & 0xff) * br * ddist / (255 * 255));
            
            frame_buf[framebufindex(x, y)] = rgba(r, g, b);
        }
    }
}

void main()
{
    scope gem graphics = new gem(width, height, scale);    
    uint[] map = generate_map();
    graphics.run(&render_blocks, map.ptr);
}

mk4kb.d

import std.stdio;
import derelict.sdl2.sdl;
import derelict.opengl3.gl;
import std.random;
import std.math;
import std.datetime;
import std.functional;
import core.memory;

static import std.compiler;

import gem;

// the block world map is stored in a cube of side mapdim; each map entry
// determines the colour of the corresponding block
immutable int mappow = 6;
immutable int mapdim = 1 << mappow;
immutable int mapmask = mapdim - 1;

// these are the image dimentions used in Minecraft4k
immutable float scale = 2;//2;
immutable int width = 428;//428;//428;
immutable int height = 240;//240;//240;


// return map index of block at given co-ordinates
int mapindex(int x, int y, int z)
{
    return (z & mapmask) << (mappow << 1) | (y & mapmask) << mappow | (x & mapmask);
}

int mapindex(float x, float y, float z)
{
    return mapindex(cast(int)x, cast(int)y, cast(int)z);
}

import std.c.stdlib;

float frand()
{
    
    return cast(float)rand() / RAND_MAX;
    //return uniform(0f, 1f);
}

// return pixel of given colour
uint rgba(uint r, uint g, uint b)
{
    return 0xFF000000 | b << 16 | g << 8 | r;
}


// return frame buffer index of pixel at given co-ordinates; (0, 0) is top left
int framebufindex(int x, int y)
{
    
    return width * y  + x;
}

alias uint uint32_t;

// algorithmicly generate bricks and wood and water and ground and ...
uint[] generate_textures()
{
    uint[] texmap = new uint[16 * 16 * 3 * 16];
    
    for (int i = 1; i < 16; ++i) {
        float br = 255 - rand() % 96;
        for (int y = 0; y < 16 * 3; ++y) {
            for (int x = 0; x < 16; ++x) {
                uint32_t color = 0x966C4A; // ground
                
                if (i == 4) // stone
                    color = 0x7F7F7F;
                
                if (i != 4 || rand() % 3 == 0)
                    br = 255 - rand() % 96;
                
                if ((i == 1 && y < (((x * x * 3 + x * 81) >> 2) & 3) + 18))
                    color = 0x6AAA40; // grass
                else if ((i == 1 && y < (((x * x * 3 + x * 81) >> 2) & 3) + 19))
                    br = br * 2 / 3;
                
                if (i == 7) { // wood
                    color = 0x675231;
                    if (x > 0 && x < 15 && ((y > 0 && y < 15) || (y > 32 && y < 47))) {
                        color = 0xBC9862;
                        float xd = cast(float)(x - 7);
                        float yd = cast(float)((y & 15) - 7);
                        if (xd < 0)
                            xd = 1 - xd;
                        if (yd < 0)
                            yd = 1 - yd;
                        if (yd > xd)
                            xd = yd;
                        
                        br = 196 - rand() % 32 + cast(int)xd % 3 * 32;
                    }
                    else if (rand() % 2 == 0)
                        br = br * (150 - (x & 1) * 100) / 100;
                }
                
                if (i == 5) { // brick
                    color = 0xB53A15;
                    if ((x + (y >> 2) * 4) % 8 == 0 || y % 4 == 0)
                        color = 0xBCAFA5;
                }
                
                if (i == 9) // water
                    color = 0x4040ff;
                
                uint32_t brr = cast(uint)br;
                if (y >= 32)
                    brr /= 2;
                
                if (i == 8) { // leaves
                    color = 0x50D937;
                    if (rand() % 2 == 0) {
                        color = 0;
                        brr = 255;
                    }
                }
                
                uint32_t col = (((color >> 16) & 0xff) * brr / 255) << 16
                             | (((color >> 8) & 0xff) * brr / 255) << 8
                             | (((color) & 0xff) * brr / 255);
                texmap[x + y * 16 + i * 256 * 3] = col;
            }
        }
    }
    
    return texmap;
}


// create the world map
uint[] generate_map()
{
    uint[] map = new uint[mapdim * mapdim * mapdim];
    
    for (int x = 0; x < mapdim; x++) {
        for (int y = 0; y < mapdim; y++) {
            for (int z = 0; z < mapdim; z++) {
                float yd = (y - 32.5f) * 0.4f;
                float zd = (z - 32.5f) * 0.4f;
                if (frand() > sqrt(sqrt(yd * yd + zd * zd)) - 0.8 || frand() < 0.6)
                    map[mapindex(x,y,z)] = 0; // there won't be a block here
                else
                    map[mapindex(x,y,z)] = rand() % 16; // assign a block type (or none)
            }
        }
    }
    
    return map;
}


struct render_info { // bundle of data used in render_minecraft()
    uint * map;
    uint * texmap;
    
    this(uint * aMap, uint * aTexmap)
    {
        map = aMap; texmap = aTexmap;
    }
};


// render the next frame into the given 'frame_buf'
void render_minecraft(void * private_renderer_data, uint32_t * frame_buf)
{
    render_info * info = cast(render_info *)private_renderer_data;
    const float pi = 3.14159265f;
    
    float dx = cast(float)(Clock.currSystemTick.length % (TickDuration.ticksPerSec * 10)) / (TickDuration.ticksPerSec * 10);
    float xRot = sin(dx * pi * 2) * 0.4f + pi / 2;
    float yRot = cos(dx * pi * 2) * 0.4f;
    float yCos = cos(yRot);
    float ySin = sin(yRot);
    float xCos = cos(xRot);
    float xSin = sin(xRot);
    
    float ox = 32.5f + dx * 64;
    float oy = 32.5f;
    float oz = 32.5f;
    
    for (int x = 0; x < width; ++x) {
        float ___xd = cast(float)(x - width / 2) / height;
        for (int y = 0; y < height; ++y) {
            float __yd = cast(float)(y - height / 2) / height;
            float __zd = 1;
            
            float ___zd = __zd * yCos + __yd * ySin;
            float _yd = __yd * yCos - __zd * ySin;
            
            float _xd = ___xd * xCos + ___zd * xSin;
            float _zd = ___zd * xCos - ___xd * xSin;
            
            uint32_t col = 0;
            uint32_t br = 255;
            float ddist = 0;
            float closest = 32;
            
            for (int d = 0; d < 3; ++d) {
                float dimLength = _xd;
                if (d == 1)
                    dimLength = _yd;
                if (d == 2)
                    dimLength = _zd;
                
                float ll = 1 / (dimLength < 0 ? -dimLength : dimLength);
                float xd = (_xd) * ll;
                float yd = (_yd) * ll;
                float zd = (_zd) * ll;
                
                float initial = ox - cast(int)ox;
                if (d == 1)
                    initial = oy - cast(int)oy;
                if (d == 2)
                    initial = oz - cast(int)oz;
                if (dimLength > 0)
                    initial = 1 - initial;
                
                float dist = ll * initial;
                
                float xp = ox + xd * initial;
                float yp = oy + yd * initial;
                float zp = oz + zd * initial;
                
                if (dimLength < 0) {
                    if (d == 0)
                        xp--;
                    if (d == 1)
                        yp--;
                    if (d == 2)
                        zp--;
                }
                
                while (dist < closest) {
                    uint tex = info.map[mapindex(xp, yp, zp)];
                    
                    if (tex > 0) {
                        uint u = cast(uint32_t)((xp + zp) * 16) & 15;
                        uint v = (cast(uint32_t)(yp * 16) & 15) + 16;
                        if (d == 1) {
                            u = cast(uint32_t)(xp * 16) & 15;
                            v = (cast(uint32_t)(zp * 16) & 15);
                            if (yd < 0)
                                v += 32;
                        }
                        
                        uint32_t cc = info.texmap[u + v * 16 + tex * 256 * 3];
                        if (cc > 0) {
                            col = cc;
                            ddist = 255 - cast(int)(dist / 32 * 255);
                            br = 255 * (255 - ((d + 2) % 3) * 50) / 255;
                            closest = dist;
                        }
                    }
                    xp += xd;
                    yp += yd;
                    zp += zd;
                    dist += ll;
                }
            }
            
            const uint32_t r = cast(uint32_t)(((col >> 16) & 0xff) * br * ddist / (255 * 255));
            const uint32_t g = cast(uint32_t)(((col >> 8) & 0xff) * br * ddist / (255 * 255));
            const uint32_t b = cast(uint32_t)(((col) & 0xff) * br * ddist / (255 * 255));
            
            frame_buf[framebufindex(x, y)] = rgba(r, g, b);
        }
    }
}


void main()
{
    writefln("Compiled with: %s", std.compiler.name);
    uint32_t[] map = generate_map();
    uint32_t[] texmap = generate_textures();
    
    scope gem = new gem(width, height, scale);
    scope auto info = new render_info(map.ptr, texmap.ptr);
    gem.run(&render_minecraft, info);
}

It's not all about speed. But lets have a look. There are some numbers below calculated from frame rates. It's not very scientific. Furthermore the frame rates in C++ and D are very sensitive to the type of floating math you use. For example. Replace the four cast(int) <float-number> with a Math.trunc or floor, and the program will be half as quick!. This effect applies to both C++ and D. The reason for this is due to floating point conformance rules, which cast(int) doesn't have to obey.

The program prints out the frame rate in a console window. Here are some relative numbers against C++ in percentage terms. The reference implementation from Anthony (windows version) and the C++ version on Linux that I compiled from Anthony's source are shown as 100%:

#### Some comments on this:-
  1. Firstly, the DMD compiler was the 32 bit version. In the 32 bit build it does not make use of SIMD instructions.
  2. Secondly, The GDC ( based on DMD 2.063). With the -O3 option it exhibited some minor rendering abnormalities, which I have not investigated. This may be due to the aggressive optimisation -O3 Update: Stone the crows, it appears that G++ also exhibits exactly the same problem with -O3. Why that is I don't know.
  3. Thirdly, I do not have a 64 bit operating system, which probably means that I am classed as poor, it also means I was unable to try DMD 64 bit as presumably it would make some difference.

Speed isn't everything, though the DMD compiler does produce somewhat sluggish code in it's 32 bit version. I have not yet been able to try a 64 bit version with SIMD instructions. GDC performs well on par with the C++ built application. This, really, is all as expected.With GDC and the -O3 flags it's 36% faster than the C++ version (GCC)