1104 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			D
		
	
	
	
			
		
		
	
	
			1104 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			D
		
	
	
	
import std.stdio;
 | 
						|
import derelict.sdl2.sdl;
 | 
						|
import derelict.imgui.imgui;
 | 
						|
import glad.gl.all;
 | 
						|
 | 
						|
import window;
 | 
						|
import imgui;
 | 
						|
 | 
						|
bool checkShaderError(GLuint shader, GLuint flag, bool is_program, in char[] shader_path) nothrow {
 | 
						|
 | 
						|
	GLint result;
 | 
						|
 | 
						|
	(is_program) ? glGetProgramiv(shader, flag, &result)
 | 
						|
		: glGetShaderiv(shader, flag, &result);
 | 
						|
 | 
						|
	if (result == GL_FALSE) {
 | 
						|
 | 
						|
		GLchar[256] log; //FIXME this is potentially fatal
 | 
						|
		(is_program) ? glGetProgramInfoLog(shader, log.sizeof, null, log.ptr)
 | 
						|
			: glGetShaderInfoLog(shader, log.sizeof, null, log.ptr);
 | 
						|
 | 
						|
		printf("[OpenGL] Error in %s: %s\n", shader_path.ptr, log.ptr);
 | 
						|
		return false;
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	return true;
 | 
						|
 | 
						|
} // checkShaderError
 | 
						|
 | 
						|
struct PixelBuffer {
 | 
						|
 | 
						|
	struct Shader {
 | 
						|
 | 
						|
		private {
 | 
						|
			GLuint shader_prog;
 | 
						|
		}
 | 
						|
 | 
						|
		@disable this(this);
 | 
						|
 | 
						|
		void compile(const (GLchar*)* vs_source, const (GLchar*)* fs_source) {
 | 
						|
			
 | 
						|
			import core.stdc.stdlib : exit;
 | 
						|
 | 
						|
			GLuint new_vs_shader = glCreateShader(GL_VERTEX_SHADER);
 | 
						|
			glShaderSource(new_vs_shader, 1, vs_source, null);
 | 
						|
			glCompileShader(new_vs_shader);
 | 
						|
			if (!checkShaderError(new_vs_shader, GL_COMPILE_STATUS, false, "vertex_shader")) {
 | 
						|
				exit(-1);
 | 
						|
			}
 | 
						|
 | 
						|
			GLuint new_fs_shader = glCreateShader(GL_FRAGMENT_SHADER);
 | 
						|
			glShaderSource(new_fs_shader, 1, fs_source, null);
 | 
						|
			glCompileShader(new_fs_shader);
 | 
						|
			if (!checkShaderError(new_fs_shader, GL_COMPILE_STATUS, false, "fragment_shader")) {
 | 
						|
				exit(-1);
 | 
						|
			}
 | 
						|
			
 | 
						|
			GLuint new_shader = glCreateProgram();
 | 
						|
			glAttachShader(new_shader, new_vs_shader);
 | 
						|
			glAttachShader(new_shader, new_fs_shader);
 | 
						|
 | 
						|
			// glBindAttribLocation(new_shader, 0, "screen_size");
 | 
						|
 | 
						|
			glLinkProgram(new_shader);
 | 
						|
			if (!checkShaderError(new_shader, GL_LINK_STATUS, true, "shader_program")) {
 | 
						|
				exit(-1);
 | 
						|
			}
 | 
						|
 | 
						|
			glValidateProgram(new_shader);
 | 
						|
			if (!checkShaderError(new_shader, GL_VALIDATE_STATUS, true, "shader_program")) {
 | 
						|
				exit(-1);
 | 
						|
			}
 | 
						|
 | 
						|
			shader_prog = new_shader;
 | 
						|
			
 | 
						|
		} // compile
 | 
						|
 | 
						|
		void setUniforms(int w, int h) {
 | 
						|
			auto attr_loc = glGetUniformLocation(shader_prog, "screen_size");
 | 
						|
			glUniform2f(attr_loc, cast(float)w, cast(float)h);
 | 
						|
		} // setUniforms
 | 
						|
 | 
						|
		void bind() {
 | 
						|
			glUseProgram(shader_prog);
 | 
						|
		} // bind
 | 
						|
 | 
						|
		void unbind() {
 | 
						|
			glUseProgram(0);
 | 
						|
		} // unbind
 | 
						|
 | 
						|
	} // Shader
 | 
						|
 | 
						|
	struct Texture {
 | 
						|
 | 
						|
		import derelict.sdl2.sdl;
 | 
						|
		import derelict.sdl2.image;
 | 
						|
 | 
						|
		private {
 | 
						|
 | 
						|
			GLuint texture_; //OpenGL handle for texture
 | 
						|
			GLenum input_format_, output_format_, data_type_;
 | 
						|
			int width_, height_;
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
		@property @nogc nothrow {
 | 
						|
 | 
						|
			int width() const { return width_; }
 | 
						|
			int height() const { return height_; }
 | 
						|
			GLuint handle() { return texture_; }
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
		@disable this(this);
 | 
						|
 | 
						|
		nothrow @nogc
 | 
						|
		this(int width, int height, GLenum input_format, GLenum output_format, GLenum unpack_alignment) {
 | 
						|
 | 
						|
			width_ = width;
 | 
						|
			height_ = height;
 | 
						|
			input_format_ = input_format;
 | 
						|
			output_format_ = output_format;
 | 
						|
			data_type_ = GL_UNSIGNED_BYTE;
 | 
						|
 | 
						|
			glGenTextures(1, &texture_);
 | 
						|
			glBindTexture(GL_TEXTURE_2D, texture_);
 | 
						|
 | 
						|
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
 | 
						|
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 | 
						|
 | 
						|
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
 | 
						|
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
 | 
						|
 | 
						|
			glPixelStorei(GL_UNPACK_ALIGNMENT, unpack_alignment);
 | 
						|
			glTexImage2D(GL_TEXTURE_2D, 0, input_format_, width_, height_, 0, output_format_, GL_UNSIGNED_BYTE, cast(void*)0);
 | 
						|
 | 
						|
			glBindTexture(GL_TEXTURE_2D, 0);
 | 
						|
 | 
						|
		} //this
 | 
						|
 | 
						|
		nothrow @nogc
 | 
						|
		void create(void* pixels, int width, int height, GLenum input_format = GL_RGB, GLenum output_format = GL_RGB, GLenum data_type = GL_UNSIGNED_BYTE) {
 | 
						|
 | 
						|
			width_ = width;
 | 
						|
			height_ = height;
 | 
						|
			input_format_ = input_format;
 | 
						|
			output_format_ = output_format;
 | 
						|
			data_type_ = data_type;
 | 
						|
 | 
						|
			//generate single texture, put handle in texture
 | 
						|
			glGenTextures(1, &texture_);
 | 
						|
 | 
						|
			//normal 2d texture, bind to our texture handle
 | 
						|
			glBindTexture(GL_TEXTURE_2D, texture_);
 | 
						|
 | 
						|
			//set texture parameters in currently bound texture, controls texture wrapping (or GL_CLAMP?)
 | 
						|
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
 | 
						|
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 | 
						|
 | 
						|
			//linearly interpolate between pixels, MIN if texture is too small for drawing area, MAG if drawing area is smaller than texture
 | 
						|
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
 | 
						|
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 | 
						|
 | 
						|
			//texture type, level, format to store as, width, height, border, format loaded in
 | 
						|
			glTexImage2D(GL_TEXTURE_2D, 0, input_format_, width_, height_, 0, output_format_, data_type_, pixels);
 | 
						|
 | 
						|
			//UNBIND
 | 
						|
			glBindTexture(GL_TEXTURE_2D, 0);
 | 
						|
 | 
						|
		} // this
 | 
						|
 | 
						|
		~this() nothrow @nogc {
 | 
						|
 | 
						|
			if (texture_ != 0) {
 | 
						|
				glDeleteTextures(1, &texture_);
 | 
						|
			}
 | 
						|
 | 
						|
		} // ~this
 | 
						|
 | 
						|
		/**
 | 
						|
		* Binds the texture handle, takes an argument for which texture unit to use.
 | 
						|
		*/
 | 
						|
		nothrow @nogc
 | 
						|
		void bind(int unit) {
 | 
						|
 | 
						|
			assert(unit >= 0 && unit <= 31);
 | 
						|
			glActiveTexture(GL_TEXTURE0 + unit); //since this is sequential, this works
 | 
						|
			glBindTexture(GL_TEXTURE_2D, texture_);
 | 
						|
 | 
						|
		} // bind
 | 
						|
 | 
						|
		nothrow @nogc
 | 
						|
		void unbind() {
 | 
						|
 | 
						|
			glBindTexture(GL_TEXTURE_2D, 0);
 | 
						|
 | 
						|
		} // unbind
 | 
						|
 | 
						|
		/**
 | 
						|
		* Updates the texture in place given the new texture buffer.
 | 
						|
		* Takes an optional offset to update only a part of the texture.
 | 
						|
		**/
 | 
						|
		nothrow @nogc
 | 
						|
		void update(void[] pixels, size_t offset = 0) {
 | 
						|
 | 
						|
			bind(0);
 | 
						|
			glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width_, height_, input_format_, data_type_, pixels.ptr);
 | 
						|
			unbind();
 | 
						|
 | 
						|
		} // update
 | 
						|
 | 
						|
	} // Texture
 | 
						|
 | 
						|
	/**
 | 
						|
	* Generic VertexArray structure, used to upload data of any given vertex type to the GPU.
 | 
						|
	*/
 | 
						|
	struct VertexArray {
 | 
						|
 | 
						|
		private {
 | 
						|
 | 
						|
			GLuint vao_;
 | 
						|
			GLuint vbo_;
 | 
						|
			GLenum type_; // type of vertex data, GL_TRIANGLES etc
 | 
						|
			uint num_vertices_;
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
		@disable this(this);
 | 
						|
 | 
						|
		nothrow @nogc
 | 
						|
		void create(in float[2][] vertices, GLenum draw_type = GL_STATIC_DRAW, GLenum primitive = GL_TRIANGLES)  {
 | 
						|
 | 
						|
			this.num_vertices_ = cast(uint)vertices.length;
 | 
						|
			this.type_ = primitive;
 | 
						|
 | 
						|
			glGenVertexArrays(1, &vao_);
 | 
						|
			glBindVertexArray(vao_);
 | 
						|
 | 
						|
			glGenBuffers(1, &vbo_);
 | 
						|
			glBindBuffer(GL_ARRAY_BUFFER, vbo_);
 | 
						|
			glBufferData(GL_ARRAY_BUFFER, vertices.length * vertices[0].sizeof, vertices.ptr, draw_type);
 | 
						|
 | 
						|
			// pos
 | 
						|
			glEnableVertexAttribArray(0);
 | 
						|
			glVertexAttribPointer(0,
 | 
						|
				2,
 | 
						|
				GL_FLOAT,
 | 
						|
				GL_FALSE,
 | 
						|
				float.sizeof * 2,
 | 
						|
				null
 | 
						|
			);
 | 
						|
 | 
						|
			glBindVertexArray(0);
 | 
						|
 | 
						|
		} // this
 | 
						|
 | 
						|
		~this() nothrow @nogc {
 | 
						|
			if (vao_ != 0) {
 | 
						|
				glDeleteVertexArrays(1, &vao_);
 | 
						|
			}
 | 
						|
		} // ~this
 | 
						|
 | 
						|
		void bind() nothrow @nogc {
 | 
						|
			glBindVertexArray(vao_);
 | 
						|
		} // bind
 | 
						|
 | 
						|
		void draw() nothrow @nogc {
 | 
						|
			glDrawArrays(type_, 0, num_vertices_);
 | 
						|
		} // draw
 | 
						|
 | 
						|
		void unbind() nothrow @nogc {
 | 
						|
			glBindVertexArray(0);
 | 
						|
		} // unbind
 | 
						|
 | 
						|
	} // VertexArray
 | 
						|
 | 
						|
	const char* vs_shader = q{
 | 
						|
		#version 330 core
 | 
						|
 | 
						|
		uniform vec2 screen_size;
 | 
						|
 | 
						|
		layout(location = 0) in vec2 pos;
 | 
						|
 | 
						|
		out vec2 frag_uv;
 | 
						|
 | 
						|
		void main() {
 | 
						|
			gl_Position = vec4(pos, 0.0, 1.0);
 | 
						|
			frag_uv = clamp(pos, vec2(0.0, 0.0), vec2(1.0, 1.0));
 | 
						|
		}
 | 
						|
 | 
						|
	};
 | 
						|
 | 
						|
	const char* fs_shader = q{
 | 
						|
		#version 330 core
 | 
						|
 | 
						|
		uniform sampler2D tex;
 | 
						|
 | 
						|
		in vec2 frag_uv;
 | 
						|
 | 
						|
		out vec4 out_col;
 | 
						|
 | 
						|
		void main() {
 | 
						|
			float v = texture(tex, frag_uv.st).r * 255;
 | 
						|
			out_col = vec4(v, v, v, 1.0);
 | 
						|
		}
 | 
						|
 | 
						|
	};
 | 
						|
 | 
						|
	Shader shader;
 | 
						|
	VertexArray vao;
 | 
						|
	Texture tex;
 | 
						|
 | 
						|
	void create(void* pixels, int w, int h) {
 | 
						|
 | 
						|
		float[2][6] rect = [
 | 
						|
			[-1.0f, -1.0f], // top left
 | 
						|
			[1.0f, -1.0f], // top right
 | 
						|
			[1.0f, 1.0f], // bottom right
 | 
						|
 | 
						|
			[-1.0f, -1.0f], // top left
 | 
						|
			[-1.0f, 1.0f], // bottom left
 | 
						|
			[1.0f, 1.0f], // bottom right
 | 
						|
		];
 | 
						|
 | 
						|
		vao.create(rect[]);
 | 
						|
		tex.create(pixels, w, h, GL_RED, GL_RED);
 | 
						|
		shader.compile(&vs_shader, &fs_shader);
 | 
						|
 | 
						|
	} // create
 | 
						|
 | 
						|
	void draw(int screen_w, int screen_h) {
 | 
						|
 | 
						|
		vao.bind();
 | 
						|
		shader.bind();
 | 
						|
		shader.setUniforms(screen_w, screen_h);
 | 
						|
		tex.bind(0);
 | 
						|
		vao.draw();
 | 
						|
		
 | 
						|
	} // draw
 | 
						|
 | 
						|
	nothrow @nogc
 | 
						|
	void update(void[] pixels, size_t offset = 0) {
 | 
						|
		tex.update(pixels, offset);
 | 
						|
	} // update
 | 
						|
 | 
						|
} // PixelBuffer
 | 
						|
 | 
						|
struct Chip8Status {
 | 
						|
 | 
						|
	//emu ptr
 | 
						|
	Emulator* run_;
 | 
						|
	Chip8* emu_;
 | 
						|
 | 
						|
	// mem editor
 | 
						|
	// MemoryEditor mem_editor_;
 | 
						|
 | 
						|
	// state
 | 
						|
	bool status_menu_ = true;
 | 
						|
	int stack_cur_ = -1;
 | 
						|
 | 
						|
	void initialize(Emulator* run, Chip8* emu) {
 | 
						|
		this.run_ = run;
 | 
						|
		this.emu_ = emu;
 | 
						|
	} // initialize
 | 
						|
 | 
						|
	alias Callback = float delegate(int idx, const char** out_text);
 | 
						|
 | 
						|
    static extern(C) bool doCallback(void* ptr, int idx, const (char**) out_text) {
 | 
						|
 | 
						|
        auto callback = *(cast(Callback*) ptr);
 | 
						|
        callback(idx, out_text);
 | 
						|
		return true;
 | 
						|
 | 
						|
    } // doCallback;
 | 
						|
 | 
						|
	void getStackFrame(int idx, const (char**) out_text) {
 | 
						|
 | 
						|
		static char[32] frame_text;
 | 
						|
 | 
						|
		import core.stdc.stdio : sprintf;
 | 
						|
		sprintf(frame_text.ptr, "0x%04X", emu_.stack[idx]);
 | 
						|
 | 
						|
		auto p = frame_text.ptr;
 | 
						|
		auto op = cast(char**)out_text;
 | 
						|
		*op = p;
 | 
						|
 | 
						|
	} // getStackFrame
 | 
						|
 | 
						|
	void resetShortcut() {
 | 
						|
 | 
						|
		emu_.reset();
 | 
						|
		
 | 
						|
	} // resetShortcut
 | 
						|
 | 
						|
	void loadShortcut() {
 | 
						|
 | 
						|
		import std.file : read;
 | 
						|
 | 
						|
		auto buf = read("programs/sqrt.ch8");
 | 
						|
		emu_.load(0x200, buf); // do ze load yes, will copy all the data in
 | 
						|
 | 
						|
	} // loadShortcut
 | 
						|
 | 
						|
	void saveShortcut() {
 | 
						|
 | 
						|
	} // saveShortcut
 | 
						|
 | 
						|
	void debugShortcut() {
 | 
						|
		status_menu_ = !status_menu_;
 | 
						|
	} // debugShortcut
 | 
						|
 | 
						|
	void redrawShortcut() {
 | 
						|
		emu_.draw_flag = true;
 | 
						|
	} // redrawShortcut
 | 
						|
 | 
						|
	void toggleRunShortcut() {
 | 
						|
		emu_.run_flag = !emu_.run_flag;		
 | 
						|
	} // toggleRunShortcut
 | 
						|
 | 
						|
	void stepShortcut() {
 | 
						|
		emu_.step();
 | 
						|
	} // stepShortcut
 | 
						|
 | 
						|
	void quitShortcut() {
 | 
						|
		run_.quit();
 | 
						|
	} // quitShortcut
 | 
						|
 | 
						|
	void draw() {
 | 
						|
 | 
						|
		if (!status_menu_) return;
 | 
						|
 | 
						|
		if (igBeginMainMenuBar()) {
 | 
						|
 | 
						|
			if (igBeginMenu("Menu")) {
 | 
						|
 | 
						|
				if (igMenuItem("Reset", "CTRL+R")) {
 | 
						|
					resetShortcut();
 | 
						|
				}
 | 
						|
 | 
						|
				if (igMenuItem("Load", "CTRL+L")) {
 | 
						|
					loadShortcut();
 | 
						|
				}
 | 
						|
 | 
						|
				if (igMenuItem("Debug", "CTRL+D")) {
 | 
						|
					debugShortcut();
 | 
						|
				}
 | 
						|
 | 
						|
				if (igMenuItem("Quit", "CTRL+Q")) {
 | 
						|
					quitShortcut();
 | 
						|
				}
 | 
						|
 | 
						|
				igEndMenu();
 | 
						|
			}
 | 
						|
 | 
						|
			igEndMainMenuBar();
 | 
						|
		}
 | 
						|
 | 
						|
		{
 | 
						|
 | 
						|
			import std.range : chunks;
 | 
						|
			
 | 
						|
			igBegin("Emulator Status");
 | 
						|
 | 
						|
			igBeginChild("General");
 | 
						|
			igText("OpCode: 0x%04X", emu_.cpu.opcode);
 | 
						|
			igText("PC:");
 | 
						|
			igSameLine();
 | 
						|
			igDragInt("##pc", cast(int*)&emu_.cpu.pc, 0.5f, 0, emu_.ram.length);
 | 
						|
 | 
						|
			igText("Registers (v0 - vF)");
 | 
						|
			igColumns(4, null, false);
 | 
						|
 | 
						|
			auto n = 0;
 | 
						|
			foreach (ref chunk; emu_.cpu.v[].chunks(4)) {
 | 
						|
				igText("v%0X 0x%02X ", n, chunk[0]);
 | 
						|
				igText("v%0X 0x%02X ", n+1, chunk[1]);
 | 
						|
				igText("v%0X 0x%02X ", n+2, chunk[2]);
 | 
						|
				igText("v%0X 0x%02X ", n+3, chunk[3]);
 | 
						|
				igNextColumn();
 | 
						|
				n += 4;
 | 
						|
			}
 | 
						|
 | 
						|
			igColumns(1, null, false);
 | 
						|
 | 
						|
			igText("Index Register: 0x%04X", emu_.cpu.i);
 | 
						|
			igText("Delay Timer: 0x%04X", emu_.cpu.delay_timer);
 | 
						|
			igText("Sound Timer: 0x%04X", emu_.cpu.sound_timer);
 | 
						|
 | 
						|
			if(igButton("Step")) {
 | 
						|
				emu_.step();
 | 
						|
			}
 | 
						|
 | 
						|
			igSameLine();
 | 
						|
 | 
						|
			if (igButton((emu_.run_flag) ? "Stop" : "Run")) {
 | 
						|
				emu_.run_flag = !emu_.run_flag;
 | 
						|
			}
 | 
						|
 | 
						|
			igText("Stack");
 | 
						|
			auto d = &getStackFrame;
 | 
						|
			igPushItemWidth(-1);
 | 
						|
			igListBox2("", &stack_cur_, &doCallback, cast(void*)&d, emu_.stack.length, 16);
 | 
						|
			igPopItemWidth();
 | 
						|
 | 
						|
			igEndChild();
 | 
						|
			igEnd();
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
		// mem_editor_.draw("Emulator Memory", emu_.ram[]);
 | 
						|
 | 
						|
	} // draw
 | 
						|
 | 
						|
	void handleEvent(ref SDL_Event ev) {
 | 
						|
		switch (ev.type) with (SDL_EventType) {
 | 
						|
			case SDL_KEYDOWN:
 | 
						|
				if ((ev.key.keysym.mod & KMOD_CTRL) != 0) {
 | 
						|
					switch (ev.key.keysym.scancode) with (SDL_EventType) {
 | 
						|
						case SDL_SCANCODE_R: resetShortcut(); break;
 | 
						|
						case SDL_SCANCODE_L: loadShortcut(); break;
 | 
						|
						case SDL_SCANCODE_D: debugShortcut(); break;
 | 
						|
						case SDL_SCANCODE_G: redrawShortcut(); break;
 | 
						|
						case SDL_SCANCODE_T: toggleRunShortcut(); break;
 | 
						|
						case SDL_SCANCODE_S: stepShortcut(); break;
 | 
						|
						case SDL_SCANCODE_Q: quitShortcut(); break;
 | 
						|
						default: break;
 | 
						|
					}
 | 
						|
				} else {
 | 
						|
					if (ev.key.keysym.scancode == SDL_SCANCODE_ESCAPE) {
 | 
						|
						quitShortcut();
 | 
						|
					}
 | 
						|
				}
 | 
						|
				break;
 | 
						|
			default:
 | 
						|
				break;
 | 
						|
		}
 | 
						|
	} // handleEvent
 | 
						|
 | 
						|
} // Chip8Status
 | 
						|
 | 
						|
pure
 | 
						|
@property {
 | 
						|
	ubyte x(ref ushort oc) {
 | 
						|
		return (oc & 0x0F00) >> 8;
 | 
						|
	}
 | 
						|
 | 
						|
	ubyte y(ref ushort oc) {
 | 
						|
		return (oc & 0x00F0) >> 4;
 | 
						|
	}
 | 
						|
 | 
						|
	ubyte n(ref ushort oc) {
 | 
						|
		return oc & 0x000F;
 | 
						|
	}
 | 
						|
 | 
						|
	ubyte nn(ref ushort oc) {
 | 
						|
		return (oc & 0x00FF);
 | 
						|
	}
 | 
						|
 | 
						|
	ushort nnn(ref ushort oc) {
 | 
						|
		return (oc & 0x0FFF);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
struct Chip8 {
 | 
						|
 | 
						|
	alias OpCode = ushort;
 | 
						|
	alias ProgramCounter = ushort;
 | 
						|
	alias Memory = ubyte[4096];
 | 
						|
	alias Stack = ushort[16];
 | 
						|
 | 
						|
	alias Register = ubyte;
 | 
						|
	alias Registers = Register[16];
 | 
						|
	alias IndexRegister = ushort;
 | 
						|
 | 
						|
	alias KeyPad = ubyte[16];
 | 
						|
 | 
						|
	struct CPU {
 | 
						|
 | 
						|
		OpCode opcode = 0; //current opcode
 | 
						|
		ProgramCounter pc = 0x200;
 | 
						|
 | 
						|
		Registers v; //16 general purpose registers
 | 
						|
		IndexRegister i;
 | 
						|
 | 
						|
		Register delay_timer;
 | 
						|
		Register sound_timer;
 | 
						|
 | 
						|
	} // CPU
 | 
						|
 | 
						|
	CPU cpu;
 | 
						|
	Memory ram;
 | 
						|
 | 
						|
	Stack stack;
 | 
						|
	ubyte sp;
 | 
						|
 | 
						|
	KeyPad kp;
 | 
						|
 | 
						|
	ubyte[64*32] screen_buf;
 | 
						|
	ubyte[3][64*32] screen_data;
 | 
						|
 | 
						|
	bool run_flag;
 | 
						|
	bool draw_flag;
 | 
						|
 | 
						|
	void load(size_t offset, in void[] data) {
 | 
						|
 | 
						|
		assert(offset + data.length < ram.length);
 | 
						|
 | 
						|
		ram[offset .. offset + data.length] = cast(ubyte[])data[];
 | 
						|
 | 
						|
	} // load
 | 
						|
 | 
						|
	void reset() {
 | 
						|
 | 
						|
		cpu = cpu.init;
 | 
						|
		stack = stack.init;
 | 
						|
		ram[0x200 .. $ - 1] = 0;
 | 
						|
		cpu.pc = 0x200;
 | 
						|
		sp = sp.init;
 | 
						|
 | 
						|
		run_flag = run_flag.init;
 | 
						|
		draw_flag = draw_flag.init;
 | 
						|
 | 
						|
		screen_buf = screen_buf.init;
 | 
						|
		screen_data = screen_data.init;
 | 
						|
 | 
						|
	} // reset
 | 
						|
 | 
						|
	void step() {
 | 
						|
 | 
						|
		cpu.opcode = ram[cpu.pc] << 8 | ram[cpu.pc + 1];
 | 
						|
		auto pc_target = cast(ProgramCounter)(cpu.pc + 2);
 | 
						|
 | 
						|
		writefln("opcode: 0x%X", cpu.opcode);
 | 
						|
 | 
						|
		switch (cpu.opcode & 0xF000) with (cpu) {
 | 
						|
 | 
						|
			case 0x0000:
 | 
						|
 | 
						|
				switch (cpu.opcode & 0x0FFF) {
 | 
						|
 | 
						|
					case 0x00E0: // 0x00E0 Clears the screen.
 | 
						|
						screen_buf[0..$] = 0;
 | 
						|
						draw_flag = true;
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x00EE: // 0x00EE Returns from a subroutine.
 | 
						|
						pc_target = stack[--sp];
 | 
						|
						break;
 | 
						|
 | 
						|
					default: // 0x0NNN Calls RCA 1802 program at address NNN. Not necessary for most ROMs.
 | 
						|
						//assert(0, "0x0NNN RCA 1802 program opcode not implemented!");
 | 
						|
						break;
 | 
						|
 | 
						|
				}
 | 
						|
 | 
						|
				break;
 | 
						|
 | 
						|
			case 0x1000: // 0x1NNN Jumps to address NNN.
 | 
						|
				pc_target = cpu.opcode.nnn;
 | 
						|
				break;
 | 
						|
 | 
						|
			case 0x2000: // 0x2NNN Calls subroutine at NNN.
 | 
						|
 | 
						|
				stack[sp++] = cpu.pc;
 | 
						|
				pc_target = cpu.opcode.nnn;
 | 
						|
				break;
 | 
						|
 | 
						|
			case 0x3000: // 0x3XNN Skips the next instruction if VX equals NN.
 | 
						|
 | 
						|
				if (cpu.v[cpu.opcode.x] == (cpu.opcode.nn)) {
 | 
						|
					pc_target += 2;
 | 
						|
				}
 | 
						|
 | 
						|
				break;
 | 
						|
 | 
						|
			case 0x4000: // 0x4XNN Skips the next instruction if VX doesn't equal NN.
 | 
						|
 | 
						|
				if (cpu.v[cpu.opcode.x] != (cpu.opcode.nn)) {
 | 
						|
					pc_target += 2;
 | 
						|
				}
 | 
						|
 | 
						|
				break;
 | 
						|
 | 
						|
			case 0x5000: // 0x5XYO Skips the next instruction if VX equals VY.
 | 
						|
 | 
						|
				if (cpu.v[cpu.opcode.x] == cpu.v[cpu.opcode.y]) {
 | 
						|
					pc_target += 2;
 | 
						|
				}
 | 
						|
 | 
						|
				break;
 | 
						|
 | 
						|
			case 0x6000: // 0x6XNN Sets VX to NN.
 | 
						|
				writefln("%d, %d", cpu.opcode.x, cpu.opcode & 0x00FF);
 | 
						|
				cpu.v[cpu.opcode.x] = cpu.opcode.nn;
 | 
						|
				break;
 | 
						|
 | 
						|
			case 0x7000: // 0x7XNN Adds NN to VX.
 | 
						|
				cpu.v[cpu.opcode.x] += cpu.opcode.nn;
 | 
						|
				break;
 | 
						|
 | 
						|
			case 0x8000:
 | 
						|
 | 
						|
				auto x = cpu.opcode.x;
 | 
						|
				auto y = cpu.opcode.y;
 | 
						|
 | 
						|
				switch (cpu.opcode.n) {
 | 
						|
 | 
						|
					case 0x0000: // 0x8XY0 Sets VX to the value of VY.
 | 
						|
						cpu.v[x] = cpu.v[y];
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x0001: // 0x8XY1 Sets VX to VX or VY.
 | 
						|
						cpu.v[x] = cpu.v[x] | cpu.v[y];
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x0002: // 0x8XY2	Sets VX to VX and VY.
 | 
						|
						cpu.v[x] = cpu.v[x] & cpu.v[y];
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x0003: // 0x8XY3 Sets VX to VX xor VY.
 | 
						|
						cpu.v[x] = cpu.v[x] ^ cpu.v[y];
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x0004: // 0x8XY4 Adds VY to VX. VF is set to 1 when there's a carry, and to 0 when there isn't.
 | 
						|
						auto vx = cpu.v[x];
 | 
						|
						auto vy = cpu.v[y];
 | 
						|
						if (cast(ushort)vx + cast(ushort)vy > 255) {
 | 
						|
							cpu.v[0xF] = 1;
 | 
						|
						} else { 
 | 
						|
							cpu.v[0xF] = 0;
 | 
						|
						}
 | 
						|
						cpu.v[x] += cpu.v[y];
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x0005: // 0x8XY5 VY is subtracted from VX. VF is set to 0 when there's a borrow, and 1 when there isn't.
 | 
						|
						auto vx = cpu.v[x];
 | 
						|
						auto vy = cpu.v[y];
 | 
						|
						if (vx > vy) {
 | 
						|
							cpu.v[0xF] = 1;
 | 
						|
						} else {
 | 
						|
							cpu.v[0xF] = 0;
 | 
						|
						}
 | 
						|
						cpu.v[x] -= cpu.v[y];
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x0006: // 0x8XY6 Shifts VX right by one. VF is set to the value of the least significant bit of VX before the shift.
 | 
						|
 | 
						|
						auto vx = cpu.v[x];
 | 
						|
						cpu.v[0xF] = (vx & 0b10000000) >> 7;
 | 
						|
						cpu.v[x] >>= 1;
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x0007: // 0x8XY7 Sets VX to VY minus VX. VF is set to 0 when there's a borrow, and 1 when there isn't.
 | 
						|
						auto vx = cpu.v[x];
 | 
						|
						auto vy = cpu.v[y];
 | 
						|
						if (vy > vx) {
 | 
						|
							cpu.v[0xF] = 1;
 | 
						|
						} else {
 | 
						|
							cpu.v[0xF] = 0;
 | 
						|
						}
 | 
						|
						cpu.v[x] = cast(Register)(vy - vx); // TODO borrow flag
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x000E: // 0x8XYE Shifts VX left by one. VF is set to the value of the most significant bit of VX before the shift.
 | 
						|
 | 
						|
						auto vx = cpu.v[x];
 | 
						|
						cpu.v[0xF] = (vx & 0b10000000) >> 7;
 | 
						|
						cpu.v[x] <<= 1;
 | 
						|
						break;
 | 
						|
 | 
						|
					default: // unhandled for some reason
 | 
						|
						writefln("unknown opcode: 0x%x", cpu.opcode);
 | 
						|
						break;
 | 
						|
 | 
						|
				}
 | 
						|
 | 
						|
				break;
 | 
						|
 | 
						|
			case 0x9000: // 0x9XYO Skips the next instruction if VX doesn't equal VY.
 | 
						|
 | 
						|
				if (cpu.v[cpu.opcode.x] != cpu.v[cpu.opcode.y]) {
 | 
						|
					pc_target += 2; // do skip yes
 | 
						|
				}
 | 
						|
 | 
						|
				break;
 | 
						|
 | 
						|
			case 0xA000: // 0xANNN  Sets I to the address NNN.
 | 
						|
				cpu.i = cpu.opcode.nnn;
 | 
						|
				break;
 | 
						|
 | 
						|
			case 0xB000: // 0xBNNN Jumps to the address NNN plus V0.
 | 
						|
				pc_target = cast(ProgramCounter)(cpu.opcode.nnn + cpu.v[0x0]);
 | 
						|
				break;
 | 
						|
 | 
						|
			case 0xC000: // 0xCXNN Sets VX to the result of a bitwise and operation on a random number and NN.
 | 
						|
 | 
						|
				import std.random : uniform;
 | 
						|
				auto x = cpu.opcode.x;
 | 
						|
				cpu.v[x] = uniform(Register.min, Register.max) & (cpu.opcode & 0x00FF);
 | 
						|
				break;
 | 
						|
 | 
						|
			// 0xDXYN
 | 
						|
			// Sprites stored in memory at location in index register (I), 8bits wide.
 | 
						|
			// Wraps around the screen. If when drawn, clears a pixel, register VF is set to 1 otherwise it is zero.
 | 
						|
			// All drawing is XOR drawing (i.e. it toggles the screen pixels).
 | 
						|
			// Sprites are drawn starting at position VX, VY. N is the number of 8bit rows that need to be drawn.
 | 
						|
			// If N is greater than 1, second line continues at position VX, VY+1, and so on.
 | 
						|
			case 0xD000:
 | 
						|
 | 
						|
				auto spr_addr = cpu.i;
 | 
						|
				auto x = cpu.opcode.x;
 | 
						|
				auto y = cpu.opcode.y;
 | 
						|
				auto n = cpu.opcode.n;
 | 
						|
 | 
						|
				foreach(int row; 0 .. n) {
 | 
						|
 | 
						|
					ushort pixel = ram[spr_addr + row];
 | 
						|
 | 
						|
					foreach (int col; 0 .. 8) {
 | 
						|
						if ((pixel & 0x80) > 0) {
 | 
						|
							auto x_off = (x + col) % 64;
 | 
						|
							auto y_off = (y + row) % 32;
 | 
						|
							auto offset = x_off + (y_off * 64);
 | 
						|
							if (screen_buf[offset] == 1) {
 | 
						|
								cpu.v[0xF] = 1;
 | 
						|
							} else {
 | 
						|
								cpu.v[0xF] = 0;
 | 
						|
							}
 | 
						|
							screen_buf[offset] ^= 1;
 | 
						|
							writefln("write to offset: %d", offset);
 | 
						|
						}
 | 
						|
						pixel <<= 1;
 | 
						|
					}
 | 
						|
 | 
						|
				}
 | 
						|
 | 
						|
				draw_flag = true;
 | 
						|
 | 
						|
				break;
 | 
						|
 | 
						|
			case 0xE000:
 | 
						|
 | 
						|
				auto x = cpu.opcode.x;
 | 
						|
				auto key = cpu.v[x];
 | 
						|
 | 
						|
				switch (cpu.opcode & 0x000F) {
 | 
						|
 | 
						|
					case 0x000E: // 0xEX9E Skips the next instruction if the key stored in VX is pressed.
 | 
						|
						writefln("0xEXA1: skip instruction if VX not pressed: %x", key);
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x0001: // 0xEXA1 Skips the next instruction if the key stored in VX isn't pressed.
 | 
						|
						writefln("0xEXA1: skip instruction if VX not pressed: %x", key);
 | 
						|
						break;
 | 
						|
 | 
						|
					default: //unhandled for some reason
 | 
						|
						writefln("unknown opcode: 0x%x", cpu.opcode);
 | 
						|
						break;
 | 
						|
 | 
						|
				}
 | 
						|
 | 
						|
				break;
 | 
						|
 | 
						|
			case 0xF000:
 | 
						|
 | 
						|
				switch (cpu.opcode & 0x00FF) {
 | 
						|
 | 
						|
					case 0x0007: // 0xFX07 Sets VX to the value of the delay timer.
 | 
						|
						cpu.v[cpu.opcode.x] = cpu.delay_timer;
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x000A: // 0xFX0A A key press is awaited, and then stored in VX.
 | 
						|
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x0015: // 0xFX15 Sets the delay timer to VX.
 | 
						|
						cpu.delay_timer = cpu.v[cpu.opcode.x];
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x0018: // 0xFX18 Sets the sound timer to VX.
 | 
						|
						cpu.sound_timer = cpu.v[cpu.opcode.x];
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x001E: // 0xFX1E Adds VX to I.
 | 
						|
						cpu.i += cpu.v[cpu.opcode.x];
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x0029: // 0xFX29 Sets I to the location of the sprite for the character in VX.
 | 
						|
 | 
						|
						auto vx = cpu.v[cpu.opcode.x];
 | 
						|
						ushort char_addr = 0x200 + (vx * 40); // base of char sprites + value of vx * bits per character
 | 
						|
						cpu.i = char_addr;
 | 
						|
						break;
 | 
						|
 | 
						|
					// 0xFX33 Stores the Binary-coded decimal representation of VX,
 | 
						|
					//  with the most significant of three digits at the address in I,
 | 
						|
					//  the middle digit at I plus 1, and the least significant digit at I plus 2.
 | 
						|
					case 0x0033:
 | 
						|
 | 
						|
						auto vx = cpu.v[cpu.opcode.x];
 | 
						|
						ram[cpu.i] = vx / 100;
 | 
						|
						ram[cpu.i + 1] = (vx / 10) % 10;
 | 
						|
						ram[cpu.i + 2] = (vx % 100) % 10;
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x0055: // 0xFX55 Stores V0 to VX in memory starting at address I.
 | 
						|
 | 
						|
						auto addr = cpu.i;
 | 
						|
						foreach (reg; cpu.v) {
 | 
						|
							ram[addr++] = reg;
 | 
						|
						}
 | 
						|
 | 
						|
						break;
 | 
						|
 | 
						|
					case 0x0065: // 0xFX65 Fills V0 to VX with values from memory starting at address I.
 | 
						|
 | 
						|
						auto addr = cpu.i;
 | 
						|
						foreach (ref reg; cpu.v) {
 | 
						|
							reg = ram[addr++];
 | 
						|
						}
 | 
						|
 | 
						|
						break;
 | 
						|
 | 
						|
					default: // unhandled for some reason
 | 
						|
						writefln("unknown opcode: 0x%x", cpu.opcode);
 | 
						|
						break;
 | 
						|
 | 
						|
				}
 | 
						|
 | 
						|
				break;
 | 
						|
 | 
						|
			default:
 | 
						|
				writefln("unknown opcode: 0x%x", cpu.opcode);
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
		/* now update pc and timer registers. */
 | 
						|
 | 
						|
		cpu.pc = pc_target;
 | 
						|
 | 
						|
		if (cpu.delay_timer != 0u) {
 | 
						|
			--cpu.delay_timer;
 | 
						|
		}
 | 
						|
 | 
						|
		if (cpu.sound_timer != 0u) {
 | 
						|
 | 
						|
			if (cpu.sound_timer == 1u) {
 | 
						|
				writefln("beep!");
 | 
						|
			}
 | 
						|
 | 
						|
			--cpu.sound_timer;
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
	} // step
 | 
						|
 | 
						|
	void handleEvent(ref SDL_Event ev) {
 | 
						|
 | 
						|
	} // handleEvent
 | 
						|
 | 
						|
	void tick() {
 | 
						|
 | 
						|
		if (run_flag) {
 | 
						|
			step();
 | 
						|
		}
 | 
						|
 | 
						|
	} // tick
 | 
						|
 | 
						|
} // Emulator
 | 
						|
 | 
						|
void loadLibs() {
 | 
						|
 | 
						|
	DerelictSDL2.load();
 | 
						|
	DerelictImgui.load();
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
void initLibs() {
 | 
						|
 | 
						|
	SDL_Init(SDL_INIT_VIDEO);
 | 
						|
	SDL_GL_LoadLibrary(null);
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
void setupImgui() {
 | 
						|
	
 | 
						|
}
 | 
						|
 | 
						|
void main() {
 | 
						|
 | 
						|
	loadLibs();
 | 
						|
	initLibs();
 | 
						|
 | 
						|
	Emulator emu;
 | 
						|
	emu.create();
 | 
						|
	emu.run();
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
struct Emulator {
 | 
						|
 | 
						|
	bool running;
 | 
						|
 | 
						|
	// debug
 | 
						|
	Chip8Status status;
 | 
						|
 | 
						|
	Window window;
 | 
						|
	Imgui imgui;
 | 
						|
	Chip8 emu;
 | 
						|
 | 
						|
	// drawing
 | 
						|
	PixelBuffer buf;
 | 
						|
 | 
						|
	void create() {
 | 
						|
 | 
						|
		// create window
 | 
						|
		window.createWindow(960, 768);
 | 
						|
 | 
						|
		// setup imgui
 | 
						|
		imgui.initialize();
 | 
						|
		imgui.createDeviceObjects();
 | 
						|
 | 
						|
		// setup debug ui
 | 
						|
		status.initialize(&this, &emu);
 | 
						|
 | 
						|
		// debug data
 | 
						|
		emu.screen_buf[0] = 1;
 | 
						|
		emu.screen_buf[64 - 1] = 1;
 | 
						|
		emu.screen_buf[64*32 - 64] = 1;
 | 
						|
		emu.screen_buf[64*32 - 1] = 1;
 | 
						|
 | 
						|
		// set up pixel buffer to poke at
 | 
						|
		buf.create(emu.screen_buf.ptr, 64, 32);
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	void handleEvents() {
 | 
						|
 | 
						|
		SDL_Event event;
 | 
						|
		while (SDL_PollEvent(&event)) {
 | 
						|
 | 
						|
			imgui.handleEvent(event);
 | 
						|
			status.handleEvent(event);
 | 
						|
			emu.handleEvent(event);
 | 
						|
 | 
						|
			switch (event.type) with (SDL_EventType) {
 | 
						|
				case SDL_QUIT: {
 | 
						|
					running = false;
 | 
						|
					break;
 | 
						|
				}
 | 
						|
				default: {
 | 
						|
					break;
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
	} // handleEvents
 | 
						|
 | 
						|
	void run() {
 | 
						|
 | 
						|
		running = true;
 | 
						|
 | 
						|
		while (running) {
 | 
						|
			handleEvents();
 | 
						|
			tick();
 | 
						|
			draw();
 | 
						|
		}
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	void tick() {
 | 
						|
 | 
						|
		emu.tick();
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	void draw() {
 | 
						|
 | 
						|
		window.renderClear(0x428bca);
 | 
						|
 | 
						|
		int w, h;
 | 
						|
		window.windowSize(w, h);
 | 
						|
		if (emu.draw_flag) {
 | 
						|
			buf.update(emu.screen_buf);
 | 
						|
			emu.draw_flag = false;
 | 
						|
		}
 | 
						|
		buf.draw(w, h);
 | 
						|
 | 
						|
		imgui.newFrame(window);
 | 
						|
 | 
						|
		status.draw();
 | 
						|
 | 
						|
		imgui.endFrame();
 | 
						|
		window.renderPresent();
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	void quit() {
 | 
						|
		running = false;
 | 
						|
	}
 | 
						|
 | 
						|
} |