The Bit Anvil

Minecraft 4k ported to the D programming language

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%:

  • Windows
  • - C++ 100% [ Visual studio 2010] With whatever flags Antony used. - DMD (Digital Mars 32 bit) With -O -release -inline -noboundscheck 53% [ Version 2.063 ]
  • Linux
  • - GCC C++ With -O2 100% [Version 4.7.2] - GCC C++ With -O3 106% [Version 4.7.2] - DMD (Digital Mars 32 bit) 55%  [ Version 2.063 ] - GDC 4.6.3 (GNU D) With -O2 109%  [DMD 2.063] - GDC 4.6.3 (GNU D) With -O3 136%  [DMD 2.063]

Some comments on this:-

Firstly, the DMD compiler was the 32 bit version. In the 32 bit build it does not make use of SIMD instructions.
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. 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)

Disms...

The code was a direct port, by that I mean virtually no D specific features have been used - and there isn't a whole lot that can be done with this particular example except perhaps the potentially using parallel foreach which would be an interesting experiment. Here is an example from the D docs:

~~~~ {style="background: #f0f0f0; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"} / Iterate over logs using work units of size 100. foreach(i, ref elem; taskPool.parallel(logs, 100)) { elem = log(i + 1.0); } ~~~~

It should be possible to calculate pixels in some similar manner, given that each pixel is generated independently from the others. (http://dlang.org/phobos/std_parallelism.html)

People have pointed out to me that the immutable keyword should be used where possible. I won't go into it here, but immutable means that the object in question won't ever change. It's different from the const keyword as the latter provides a read only view of an object, and in fact it may be changed elsewhere by other code, possibly in another thread. const is similar to c++ const, however, both immutable and const are transitive.This means that any access of a const object or it's sub components there of (i.e. if it's a struct) will also be const unlike the way const works in C and C++. (http://dlang.org/const-faq.html#transitive-const)

Comments