Jelajahi Sumber

Some kind of texture handle concept

Karl Zylinski 5 bulan lalu
induk
melakukan
c994c4c77b
5 mengubah file dengan 300 tambahan dan 31 penghapusan
  1. 5 4
      README.md
  2. 18 0
      examples/snake/snake.odin
  3. 231 0
      handle_map/handle_map.odin
  4. 1 1
      karl2d.odin
  5. 45 26
      karl2d_windows.odin

+ 5 - 4
README.md

@@ -16,17 +16,18 @@ Might not be included:
 
 Here follows my near-future TODO list
 
-* Do proper checks of vertex count and dispatch rendering when full
-	* What happens when list is full? We can't just empty the vertex list due to being used by input assembler etc.
-* Make a texture for drawing a rectangle and remove the hack in `shader.hlsl`
-* Should we sort by depth? Maybe we should use Vec3 because some 2D games rely on it?
 * Texture things are a bit of a hack
 	* Make the sampler state configurable
 	* Group set_tex, camera etc into a section of things that cause a render batch dispatch when changed.
 	* Do we need the SRV in the texture?
+* Do proper checks of vertex count and dispatch rendering when full
+	* What happens when list is full? We can't just empty the vertex list due to being used by input assembler etc.
+* Should we sort by depth? Maybe we should use Vec3 because some 2D games rely on it?
+
 
 ## DONE
 
+* Make a texture for drawing a rectangle and remove the hack in `shader.hlsl`
 * Load textures and somehow bind to shader -- split draw calls on texture switch -- needs a start of a batch system.
 * Make 0, 0 be at top left (should vertex data be flipped, or is it a transformation thingy?)
 * Construct vertex buffer from k2.draw_blabla calls. Do we need index buffer? 🤷‍

+ 18 - 0
examples/snake/snake.odin

@@ -7,6 +7,7 @@ import "core:time"
 import "core:math/rand"
 import "base:intrinsics"
 import "core:log"
+import "core:mem"
 
 WINDOW_SIZE :: 1000
 GRID_WIDTH :: 20
@@ -60,6 +61,23 @@ restart :: proc() {
 
 main :: proc() {
 	context.logger = log.create_console_logger()
+
+
+	when ODIN_DEBUG {
+		track: mem.Tracking_Allocator
+		mem.tracking_allocator_init(&track, context.allocator)
+		context.allocator = mem.tracking_allocator(&track)
+
+		defer {
+			if len(track.allocation_map) > 0 {
+				for _, entry in track.allocation_map {
+					fmt.eprintf("%v leaked: %v bytes\n", entry.location, entry.size)
+				}
+			}
+			mem.tracking_allocator_destroy(&track)
+		}
+	}
+
 	k2.init(WINDOW_SIZE, WINDOW_SIZE, "Snake")
 	prev_time := time.now()
 

+ 231 - 0
handle_map/handle_map.odin

@@ -0,0 +1,231 @@
+/* Handle-based map using fixed arrays. By Karl Zylinski (karl@zylinski.se)
+
+The Handle_Map maps a handle to an item. A handle consists of an index and a
+generation. The item can be any type. Such a handle can be stored as a permanent
+reference, where you'd usually store a pointer. The benefit of handles is that
+you know if some other system has destroyed the object at that index, since the
+generation will then differ.
+
+This implementation uses fixed arrays and therefore
+involves no dynamic memory allocations.
+
+Example (assumes this package is imported under the alias `hm`):
+
+	Entity_Handle :: hm.Handle
+
+	Entity :: struct {
+		// All items must contain a handle
+		handle: Entity_Handle,
+		pos: [2]f32,
+	}
+
+	// Note: We use `1024`, if you use a bigger number within a proc you may
+	// blow the stack. In those cases: Store the array inside a global variable
+	// or a dynamically allocated struct.
+	entities: hm.Handle_Map(Entity, Entity_Handle, 1024)
+
+	h1 := hm.add(&entities, Entity { pos = { 5, 7 } })
+	h2 := hm.add(&entities, Entity { pos = { 10, 5 } })
+
+	// Resolve handle -> pointer
+	if h2e := hm.get(&entities, h2); h2e != nil {
+		h2e.pos.y = 123
+	}
+
+	// Will remove this entity, leaving an unused slot
+	hm.remove(&entities, h1)
+
+	// Will reuse the slot h1 used
+	h3 := hm.add(&entities, Entity { pos = { 1, 2 } })
+
+	// Iterate. You can also use `for e in hm.items {}` and skip any item where
+	// `e.handle.idx == 0`. The iterator does that automatically. There's also
+	// `skip` procedure in this package that check `e.handle.idx == 0` for you.
+	ent_iter := hm.make_iter(&entities)
+	for e, h in hm.iter(&ent_iter) {
+		e.pos += { 5, 1 }
+	}
+*/
+package handle_map_fixed
+
+import "base:intrinsics"
+
+// Returned from the `add` proc. Store these as permanent references to items in
+// the handle map. You can resolve the handle to a pointer using the `get` proc.
+Handle :: struct {
+	// index into `items` array of the `Handle_Map` struct.
+	idx: u32,
+
+	// When using the `get` proc, this will be matched to the `gen` on the item
+	// in the handle map. The handle is only valid if they match. If they don't
+	// match, then it means that the slot in the handle map has been reused.
+	gen: u32,
+}
+
+Handle_Map :: struct($T: typeid, $HT: typeid, $N: int) {
+	// Each item must have a field `handle` of type `HT`.
+	//
+	// There's always a "dummy element" at index 0. This way, a Handle with
+	// `idx == 0` means "no Handle". This means that you actually have `N - 1`
+	// items available.
+	items: [N]T,
+
+	// How much of `items` that is in use.
+	num_items: u32,
+
+	// The index of the first unused element in `items`. At this index in
+	// `unused_items` you'll find the next-next unused index. Used by `add` and
+	// originally set by `remove`.
+	next_unused: u32,
+
+	// An element in this array that is non-zero means the thing at that index
+	// in `items` is unused. The non-zero number is the index of the next unused
+	// item. This forms a linked series of indices. The series starts with
+	// `next_unused`.
+	unused_items: [N]u32,
+
+	// Only used for making it possible to quickly calculate the number of valid
+	// elements.
+	num_unused: u32,
+}
+
+// Clears the handle map using `mem_zero`. It doesn't do `m^ = {}` because that
+// may blow the stack for a handle map with very big `N`
+clear :: proc(m: ^Handle_Map($T, $HT, $N)) {
+	intrinsics.mem_zero(m, size_of(m^))
+}
+
+// Add a value of type `T` to the handle map. Returns a handle you can use as a
+// permanent reference.
+//
+// Will reuse the item at `next_unused` if that value is non-zero.
+//
+// Second return value is `false` if the handle-based map is full.
+add :: proc(m: ^Handle_Map($T, $HT, $N), v: T) -> (HT, bool) #optional_ok {
+	v := v
+
+	if m.next_unused != 0 {
+		idx := m.next_unused
+		item := &m.items[idx]
+		m.next_unused = m.unused_items[idx]
+		m.unused_items[idx] = 0
+		gen := item.handle.gen
+		item^ = v
+		item.handle.idx = u32(idx)
+		item.handle.gen = gen + 1
+		m.num_unused -= 1
+		return item.handle, true
+	}
+
+	// We always have a "dummy item" at index zero. This is because handle.idx
+	// being zero means "no item", so we can't use that slot for anything.
+	if m.num_items == 0 {
+		m.items[0] = {}
+		m.num_items += 1
+	}
+
+	if m.num_items == len(m.items) {
+		return {}, false
+	}
+
+	item := &m.items[m.num_items]
+	item^ = v
+	item.handle.idx = u32(m.num_items)
+	item.handle.gen = 1
+	m.num_items += 1
+	return item.handle, true
+}
+
+// Resolve a handle to a pointer of type `^T`. The pointer is stable since the
+// handle map uses a fixed array. But you should _not_ store the pointer
+// permanently. The item may get reused if any part of your program destroys and
+// reuses that slot. Only store handles permanently and temporarily resolve them
+// into pointers as needed.
+get :: proc(m: ^Handle_Map($T, $HT, $N), h: HT) -> ^T {
+	if h.idx <= 0 || h.idx >= m.num_items {
+		return nil
+	}
+
+	if item := &m.items[h.idx]; item.handle == h {
+		return item
+	}
+
+	return nil
+}
+
+// Remove an item from the handle map. You choose which item by passing a handle
+// to this proc. The item is not really destroyed, rather its index is just
+// set on `m.next_unused`. Also, the item's `handle.idx` is set to zero, this
+// is used by the `iter` proc in order to skip that item when iterating.
+remove :: proc(m: ^Handle_Map($T, $HT, $N), h: HT) {
+	if h.idx <= 0 || h.idx >= m.num_items {
+		return
+	}
+
+	if item := &m.items[h.idx]; item.handle == h {
+		m.unused_items[h.idx] = m.next_unused
+		m.next_unused = h.idx
+		m.num_unused += 1
+		item.handle.idx = 0
+	}
+}
+
+// Tells you if a handle maps to a valid item. This is done by checking if the
+// handle on the item is the same as the passed handle.
+valid :: proc(m: Handle_Map($T, $HT, $N), h: HT) -> bool {
+	return h.idx > 0 && h.idx < m.num_items && m.items[h.idx].handle == h
+}
+
+// Tells you how many valid items there are in the handle map.
+num_used :: proc(m: Handle_Map($T, $HT, $N)) -> int {
+	return int(m.num_items - m.num_unused)
+}
+
+// The maximum number of items the handle map can contain.
+cap :: proc(m: Handle_Map($T, $HT, $N)) -> int {
+	return N
+}
+
+// For iterating a handle map. Create using `make_iter`.
+Handle_Map_Iterator :: struct($T: typeid, $HT: typeid, $N: int) {
+	m: ^Handle_Map(T, HT, N),
+	index: u32,
+}
+
+// Create an iterator. Use with `iter` to do the actual iteration.
+make_iter :: proc(m: ^Handle_Map($T, $HT, $N)) -> Handle_Map_Iterator(T, HT, N) {
+	return { m = m, index = 1 }
+}
+
+// Iterate over the handle map. Skips unused slots, meaning that it skips slots
+// with handle.idx == 0.
+//
+// Usage:
+//     my_iter := hm.make_iter(&my_handle_map)
+//     for e in hm.iter(&my_iter) {}
+// 
+// Instead of using an iterator you can also loop over `items` and check if
+// `item.handle.idx == 0` and in that case skip that item.
+iter :: proc(it: ^Handle_Map_Iterator($T, $HT, $N)) -> (val: ^T, h: HT, cond: bool) {
+	for _ in it.index..<it.m.num_items {
+		item := &it.m.items[it.index]
+		it.index += 1
+
+		if item.handle.idx != 0 {
+			return item, item.handle, true
+		}
+	}
+
+	return nil, {}, false
+}
+
+// If you don't want to use iterator, you can instead do:
+// for &item in my_map.items {
+//     if hm.skip(item) {
+//         continue
+//     }
+//     // do stuff
+// }
+skip :: proc(e: $T) -> bool {
+	return e.handle.idx == 0
+}

+ 1 - 1
karl2d.odin

@@ -97,7 +97,7 @@ Rect :: struct {
 }
 
 Texture :: struct {
-	id: _Texture_Type,
+	id: Texture_Handle,
 	width: int,
 	height: int,
 }

+ 45 - 26
karl2d_windows.odin

@@ -13,6 +13,7 @@ import "core:slice"
 import "core:mem"
 import "core:math"
 import "core:image"
+import hm "handle_map"
 
 import "core:image/bmp"
 import "core:image/png"
@@ -25,6 +26,7 @@ _ :: tga
 _init :: proc(width: int, height: int, title: string,
               allocator := context.allocator, loc := #caller_location) -> ^State {
 	s = new(State, allocator, loc)
+	s.allocator = allocator
 	s.custom_context = context
 	CLASS_NAME :: "karl2d"
 	instance := win32.HINSTANCE(win32.GetModuleHandleW(nil))
@@ -178,7 +180,7 @@ _init :: proc(width: int, height: int, title: string,
 		CPUAccessFlags = {.WRITE},
 	}
 	ch(s.device->CreateBuffer(&vertex_buffer_desc, nil, &s.vertex_buffer_gpu))
-	s.vertex_buffer_cpu = make([]Vertex, VERTEX_BUFFER_MAX)
+	s.vertex_buffer_cpu = make([]Vertex, VERTEX_BUFFER_MAX, allocator, loc)
 
 	blend_desc := d3d11.BLEND_DESC {
 		RenderTarget = {
@@ -229,6 +231,9 @@ s: ^State
 
 VERTEX_BUFFER_MAX :: 10000
 
+Texture_Handle :: distinct hm.Handle
+TEXTURE_NONE :: Texture_Handle {}
+
 State :: struct {
 	swapchain: ^dxgi.ISwapChain1,
 	framebuffer_view: ^d3d11.IRenderTargetView,
@@ -249,7 +254,9 @@ State :: struct {
 	// these need to be generalized
 	sampler_state: ^d3d11.ISamplerState,
 
-	set_tex: Texture,
+	textures: hm.Handle_Map(_Texture, Texture_Handle, 1024*10),
+
+	set_tex: Texture_Handle,
 
 	info_queue: ^d3d11.IInfoQueue,
 	vertex_buffer_gpu: ^d3d11.IBuffer,
@@ -260,6 +267,7 @@ State :: struct {
 
 	run: bool,
 	custom_context: runtime.Context,
+	allocator: runtime.Allocator,
 
 	camera: Maybe(Camera),
 	width: int,
@@ -347,6 +355,7 @@ _shutdown :: proc() {
 	s.input_layout->Release()
 	s.swapchain->Release()
 	s.blend_state->Release()
+	delete(s.vertex_buffer_cpu, s.allocator)
 
 	when ODIN_DEBUG {
 		debug: ^d3d11.IDebug
@@ -361,6 +370,10 @@ _shutdown :: proc() {
 
 	s.device->Release()
 	s.info_queue->Release()
+
+	a := s.allocator
+	free(s, a)
+	s = nil
 }
 
 _set_internal_state :: proc(new_state: ^State) {
@@ -384,13 +397,12 @@ _clear :: proc(color: Color) {
 	s.device_context->ClearDepthStencilView(s.depth_buffer_view, {.DEPTH}, 1, 0)
 }
 
-_Texture_Type :: struct {
+_Texture :: struct {
+	handle: Texture_Handle,
 	tex: ^d3d11.ITexture2D,
 	view: ^d3d11.IShaderResourceView,
 }
 
-
-
 _load_texture_from_file :: proc(filename: string) -> Texture {
 	img, img_err := image.load_from_file(filename, allocator = context.temp_allocator)
 
@@ -425,19 +437,26 @@ _load_texture_from_memory :: proc(data: []u8, width: int, height: int) -> Textur
 
 	texture_view: ^d3d11.IShaderResourceView
 	s.device->CreateShaderResourceView(texture, nil, &texture_view)
+
+	tex := _Texture {
+		tex = texture,
+		view = texture_view,
+	}
+
+	handle := hm.add(&s.textures, tex)
+
 	return {
-		id = {
-			tex = texture,
-			view = texture_view,
-		},
+		id = handle,
 		width = width,
 		height = height,
 	}
 }
 
 _destroy_texture :: proc(tex: Texture) {
-	tex.id.tex->Release()
-	tex.id.view->Release()
+	if t := hm.get(&s.textures, tex.id); t != nil {
+		t.tex->Release()
+		t.view->Release()	
+	}
 }
 
 _draw_texture :: proc(tex: Texture, pos: Vec2, tint := WHITE) {
@@ -479,12 +498,12 @@ batch_vertex :: proc(v: Vec2, uv: Vec2, color: Color) {
 }
 
 _draw_texture_ex :: proc(tex: Texture, src: Rect, dst: Rect, origin: Vec2, rot: f32, tint := WHITE) {
-	if tex.width == 0 || tex.height == 0 || tex.id.tex == nil {
+	if tex.width == 0 || tex.height == 0 {
 		return
 	}
 
-	if s.set_tex.id.tex != nil && s.set_tex.id.tex != tex.id.tex {
-		maybe_draw_current_batch()
+	if s.set_tex != TEXTURE_NONE && s.set_tex != tex.id {
+		_draw_current_batch()
 	}
 
 	r := dst
@@ -492,7 +511,7 @@ _draw_texture_ex :: proc(tex: Texture, src: Rect, dst: Rect, origin: Vec2, rot:
 	r.x -= origin.x
 	r.y -= origin.y
 
-	s.set_tex = tex
+	s.set_tex = tex.id
 	tl, tr, bl, br: Vec2
 
 	// Rotation adapted from Raylib's "DrawTexturePro"
@@ -545,11 +564,11 @@ _draw_texture_ex :: proc(tex: Texture, src: Rect, dst: Rect, origin: Vec2, rot:
 }
 
 _draw_rectangle :: proc(r: Rect, c: Color) {
-	if s.set_tex.id.tex != nil && s.set_tex.id.tex != s.shape_drawing_texture.id.tex {
-		maybe_draw_current_batch()
+	if s.set_tex != TEXTURE_NONE && s.set_tex != s.shape_drawing_texture.id {
+		_draw_current_batch()
 	}
 
-	s.set_tex = s.shape_drawing_texture
+	s.set_tex = s.shape_drawing_texture.id
 
 	batch_vertex({r.x, r.y}, {0, 0}, c)
 	batch_vertex({r.x + r.w, r.y}, {1, 0}, c)
@@ -681,7 +700,7 @@ _set_camera :: proc(camera: Maybe(Camera)) {
 	}
 
 	s.camera = camera
-	maybe_draw_current_batch()
+	_draw_current_batch()
 
 	if c, c_ok := camera.?; c_ok {
 		origin_trans :=linalg.matrix4_translate(vec3_from_vec2(-c.origin))
@@ -719,15 +738,11 @@ _process_events :: proc() {
 	}
 }
 
-maybe_draw_current_batch :: proc() {
+_draw_current_batch :: proc() {
 	if s.vertex_buffer_cpu_count == 0 {
 		return
 	}
 
-	_draw_current_batch()
-}
-
-_draw_current_batch :: proc() {
 	viewport := d3d11.VIEWPORT{
 		0, 0,
 		f32(s.width), f32(s.height),
@@ -768,7 +783,11 @@ _draw_current_batch :: proc() {
 	dc->RSSetState(s.rasterizer_state)
 
 	dc->PSSetShader(s.pixel_shader, nil, 0)
-	dc->PSSetShaderResources(0, 1, &s.set_tex.id.view)
+
+	if t := hm.get(&s.textures, s.set_tex); t != nil {
+		dc->PSSetShaderResources(0, 1, &t.view)	
+	}
+	
 	dc->PSSetSamplers(0, 1, &s.sampler_state)
 
 	dc->OMSetRenderTargets(1, &s.framebuffer_view, s.depth_buffer_view)
@@ -789,7 +808,7 @@ make_default_projection :: proc(w, h: int) -> matrix[4,4]f32 {
 }
 
 _present :: proc() {
-	maybe_draw_current_batch()
+	_draw_current_batch()
 	ch(s.swapchain->Present(1, {}))
 	s.vertex_buffer_offset = 0
 	s.vertex_buffer_cpu_count = 0