From 562cb9e890e653f9112fc5a7962a498c4c5f523c Mon Sep 17 00:00:00 2001 From: Kevin Trogant Date: Mon, 9 Jan 2023 12:52:26 +0100 Subject: [PATCH] import old blogposts & update projects --- config.toml | 3 +- content/about.md | 1 + content/posts/data_oriented_fsm.md | 81 ++++++++++++++++++++ content/posts/first_post.md | 6 -- content/posts/tmux_cheatsheet.md | 60 +++++++++++++++ content/posts/vulkan_mem_mgr.md | 119 +++++++++++++++++++++++++++++ content/projects.md | 7 ++ 7 files changed, 270 insertions(+), 7 deletions(-) create mode 100644 content/posts/data_oriented_fsm.md delete mode 100644 content/posts/first_post.md create mode 100644 content/posts/tmux_cheatsheet.md create mode 100644 content/posts/vulkan_mem_mgr.md diff --git a/config.toml b/config.toml index bf3e182..ae59fae 100644 --- a/config.toml +++ b/config.toml @@ -14,7 +14,8 @@ enableRobotsTXT = true {name = "twitter", url = "https://twitter.com/GreenFoxLight"}, {name = "linkedin", url = "https://www.linkedin.com/in/kevin-trogant-232124224/"} ] - description = "Programming | 3D Graphics | Sailing | Tabletop RPGs" + description = "Game Development | 3D Graphics" +# | Sailing | Tabletop RPGs" [menu] [[menu.main]] diff --git a/content/about.md b/content/about.md index 97365dd..f63cb5b 100644 --- a/content/about.md +++ b/content/about.md @@ -2,6 +2,7 @@ title: "About" date: 2023-01-04T10:09:32+01:00 draft: false +type: "page" --- I study computer science at Humboldt-Universität in Berlin, Germany. diff --git a/content/posts/data_oriented_fsm.md b/content/posts/data_oriented_fsm.md new file mode 100644 index 0000000..7846230 --- /dev/null +++ b/content/posts/data_oriented_fsm.md @@ -0,0 +1,81 @@ +--- +title: "Data Oriented Finite State Machines" +date: 2022-03-29T16:01:00+02:00 +draft: false +type: "post" +--- + + +Finite state machines (FSMs) are probably one of the most widespread techniques used for game AI. + +For my own engine i thought about ways to implement FSMs with performance in mind. The fundamental assumption guiding the design was: “There will be many AI entities using the same state-machine.”[1](#fn:1) + +The design itself is pretty simple and i’m sure that i’m not the first person to come up with this. The structure for a FSM is shown here: + + #define XE_AI_FSM_MAX_STATES 16 + #define XE_AI_FSM_STATE_FUNC(name) uint32_t name(xeAIEntity entity, uint32_t previousState) + typedef XE_AI_FSM_STATE_FUNC(xeAIFSMStateFunc); + + typedef struct + { + uint32_t maxEntities; + uint32_t stateCount; + uint32_t entityCounts[XE_AI_FSM_MAX_STATES]; + xeAIFSMStateEntity* stateEntities[XE_AI_FSM_MAX_STATES]; + xeAIFSMStateEntity* nextStateEntities[XE_AI_FSM_MAX_STATES]; + xeAIFSMStateFunc* stateFuncs[XE_AI_FSM_MAX_STATES]; + } xeAIFSM; + +I arbitrarily limited the number of states to 16 because this saves me from some memory management, but it would be trivial to remove this limit. For every state (which is identified by an unsigned integer between `0` and `stateCount`), the FSM saves the AI entities in that state (`stateEntities`). The type `xeAIFSMStateEntity` is shown below. It contains an reference to the entity data and the previous state of the entity, which can be used by the state function to determine if the entity just entered the state (for example, if a monster just entered the “chase player” state, we could play a special sound effect). + + + typedef struct + { + xeAIEntity entity; + uint32_t previousState; + } xeAIFSMStateEntity; + +Every frame, all state functions need to be executed. For that, we iterate over all states and call the function for that state (`stateFuncs[stateIndex]`) for every entity in `stateEntities[stateIndex]`. This can be done in parallel, for example via a job system (which is what i do in my engine). The state function returns the new state of the entity and the run function of the FSM will place the append the entity to `nextStateEntities[newState]`. After all states were executed, `stateEntities` and `nextStateEntities` are swapped. + +Code for this function is shown below; i removed all parallelization code for improved clarity. + + void xe_ai_fsm_run(xeAIFSM* fsm) + { + uint32_t nextEntityCounts[XE_AI_FSM_MAX_STATES]; + XE_ZERO_ARRAY(nextEntityCounts, XE_AI_FSM_MAX_STATES); + + for (uint32_t state = 0; state < XE_AI_FSM_MAX_STATES; ++state) { + xeAIFSMStateFunc* stateFunc = fsm->stateFuncs[state]; + assert(stateFunc); + uint32_t entityCount = fsm->entityCounts[state]; + xeAIFSMStateEntity* entities = fsm->stateEntities[state]; + for (uint32_t entityIdx = 0; entityIdx < entityCount; ++entityIdx) { + uint32_t nextState = stateFunc(entities[entityIdx].entity, entities[entityIdx].previousState); + entities[entityIdx].previousState = state; + if (nextState >= fsm->stateCount) { + xe_log_error("FSM %x entity (%u;%u) tried to move to invalid state %u", + (uintptr_t)fsm, + state, entityIdx, + nextState); + nextState = state; + assert(!"Tried to move an entity to an invalid state"); + } + /* Move to (potentially) new state */ + fsm->nextStateEntities[nextState][nextEntityCounts[nextState]] = entities[entityIdx]; + ++nextEntityCounts[nextState]; + } + } + + /* Swap pointers and update counts */ + for (uint32_t state = 0; state < XE_AI_FSM_MAX_STATES; ++state) { + xeAIFSMStateEntity* tmp = fsm->stateEntities[state]; + fsm->stateEntities[state] = fsm->nextStateEntities[state]; + fsm->nextStateEntities[state] = tmp; + fsm->entityCounts[state] = nextEntityCounts[state]; + } + } + +* * * + +1. “Where there is one, there are many.”, [as Mike action would say](https://youtu.be/rX0ItVEVjHc?t=871). [↩](#fnref:1) + \ No newline at end of file diff --git a/content/posts/first_post.md b/content/posts/first_post.md deleted file mode 100644 index 93219ae..0000000 --- a/content/posts/first_post.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: "First_post" -date: 2023-01-04T09:39:51+01:00 -draft: true ---- - diff --git a/content/posts/tmux_cheatsheet.md b/content/posts/tmux_cheatsheet.md new file mode 100644 index 0000000..9cc42a6 --- /dev/null +++ b/content/posts/tmux_cheatsheet.md @@ -0,0 +1,60 @@ +--- +title: "TMUX Cheat Sheet" +date: 2022-04-08T13:20:00+02:00 +draft: false +type: "post" +--- + +Default prefix “key” is `Ctrl + b`. + +Sessions +-------- + +Creating a new named session: + + tmux new-session -s + + +Listing all sessions: + + tmux list-session + + +Attach to a running session: + + tmux attach -t + + +To detach from a session, press: ` + d` + +Renaming a session: + + tmux rename-session -t + + +Kill a session: + + tmux kill-session -t + + +Windows +------- + +To create a new window, within a session press: ` + c`. + +To list all windows, press: ` + w`. The list also allows us to select a window using `UP, DOWN` and `ENTER`. + +To rename a window, press: ` + ,`. + +To terminate a window, press: ` + &`. + +Copy-Mode +--------- + +To enter copy mode, press: ` + [`. + +Begin selection with: ` + Space`. + +Hit `ENTER` to copy the selected text to tmux’s clipboard. + +Paste with ` + ]`. \ No newline at end of file diff --git a/content/posts/vulkan_mem_mgr.md b/content/posts/vulkan_mem_mgr.md new file mode 100644 index 0000000..5b54c3a --- /dev/null +++ b/content/posts/vulkan_mem_mgr.md @@ -0,0 +1,119 @@ +--- +title: "A Simple Vulkan Memory Manager" +date: 2022-03-10T13:54:40+01:00 +draft: false +type: "post" +--- + + +As part of my journey towards understanding the vulkan api, i tried to implement a simple memory management scheme from scratch. The core idea of the system is to divide the available memory into pools and then to sub-allocate from the appropriate pool. + +A pool has the following structure: + + struct xeVulkanMemoryPool + { + VkDeviceMemory deviceMemory; + VkDeviceSize poolSize; + VkDeviceSize blockSize; + + xeQueue blockQueue; + }; + +The device memory is allocated via `vkAllocateMemory` and divided into blocks of equal size. The queue maintains the offsets of free blocks. + +Allocating a block is then simply: + + static xeResult xe_allocate_pool_memory(xeVulkanMemoryPool* pool, xeVulkanMemoryAllocation* allocation) + { + VkDeviceSize offset; + xeResult result = XE_SUCCESS; + if (xe_pop_queue(&pool->blockQueue, &offset) == XE_QUEUE_EMPTY) + result = XE_OUT_OF_MEMORY; + else { + allocation->deviceMemory = pool->deviceMemory; + allocation->offset = offset; + } + return result; + } + +The used queue is lockless, so this function is thread-safe. + +Freeing a block is done by pushing the offset into the queue. + +The more interesting part comes from managing the pools. This is the responsiblity of the “memory manager”. Its structure is shown below: + + struct xeVulkanMemoryManager + { + VkDevice device; + VkPhysicalDeviceMemoryProperties memoryProperties; + + uint32_t maxMemoryPoolCount; + + /* we track the amount of memory we allocated */ + uint32_t deviceLocalBudget; + uint32_t deviceLocalHostVisibleBudget; + uint32_t usedDeviceLocalBudget; + uint32_t usedDeviceLocalHostVisibleBudget; + + VkDeviceSize maxAllocationSize; + + /* Hash from blockSize + memory type + required properties to the index + * of the first pool that satisfies these requirements. + * If the pool is full check memoryPoolLinks[index] for another pool. + * If no suitable pool with available memory is found, create a new pool. + */ + xeHash poolHash; + xeVulkanMemoryPool* memoryPools; + uint32_t* memoryPoolLinks; + }; + +The algorithm for allocating a block of device memory works as follows (pseudo-c) + + xeResult xe_allocate_vulkan_memory(xeVulkanMemoryManager* memoryManager, + uint32_t memoryTypeBits, + VkMemoryPropertyFlags requiredProperties, + VkDeviceSize size, + VkDeviceSize alignment, + xeVulkanMemoryAllocation* allocation) + { + /* Determine block size from size+alignment. + * and round to nearest multiple of 64 kb. + * This keeps the number of different pools smaller. */ + size = XE_MAX(size, alignment); + size = round_to_multiple(size, XE_KB(64)); + + /* Construct the lookup key. + * The lower 32 bits are made up of the memory type and the block size, + * the upper 32 bits contain the required memory properties */ + uint64_t log2BlockSize = log2(size); + uint32_t memoryTypeIndex = find_vulkan_memory_type_index(memoryManager, memoryTypeBits, requiredProperties); + uint64_t key = memoryTypeIndex | (log2BlockSize << VK_MAX_MEMORY_TYPES) | (uint64_t)requiredProperties << 32); + + uint32_t poolIndex = xe_hash_lookup(memoryManager->poolHash, key, NO_SUITABLE_POOL); + if (poolIndex == NO_SUITABLE_POOL) { + /* Create a new pool with the required parameters, insert into the hash table and allocate from that pool */ + } + else { + /* Try to allocate from that pool */ + if (xe_allocate_pool_memory(&memoryManager->memoryPools[poolIndex], allocation) == XE_SUCCESS) + return XE_SUCCESS; + + /* Try the next suitable pool */ + uint32_t lastValidPoolIndex = poolIndex; + while (1) { + if (poolIndex == NO_SUITABLE_POOL) + break; + poolIndex = memoryManager->memoryPoolLinks[poolIndex]; + if (xe_allocate_pool_memory(&memoryManager->memoryPools[poolIndex], allocation) == XE_SUCCESS) + return XE_SUCCESS; + lastValidPoolIndex = poolIndex; + } + + /* Create a new pool, update memoryPoolLinks[lastValidPoolIndex] and allocate from the new pool */ + } + + /* Allocation failed */ + return XE_OUT_OF_MEMORY; + } + +This system is probably not as sophisticated as the one used by [VMA](https://gpuopen.com/vulkan-memory-allocator/) but it is easy to understand, which in my opinion is the most valuable property of a learning project. diff --git a/content/projects.md b/content/projects.md index b471cd4..6edd9eb 100644 --- a/content/projects.md +++ b/content/projects.md @@ -2,6 +2,7 @@ title: "Projects" date: 2023-01-04T10:18:30+01:00 draft: false +type: "page" --- You can find the source code for some of these projects at [gitea](https://libneat.hopto.org/git). @@ -27,3 +28,9 @@ The project is currently on hold, but the idea might be re-used at some point. My bachelor thesis delt with implementing an GPU algorithm for time series comparison. A paper describing the implementation was published at the [PARS-Workshop 2019](https://fg-pars.gi.de/veranstaltung/pars-workshop-2019). +The paper itself can be found [here at page 63](https://dl.gi.de/bitstream/handle/20.500.12116/33861/PARS-Mitteilungen_2019.pdf). + +# Krimi Dinner Engine + +A simple base layer for a murder mystery dinner game. +The game itself is not by me, but i contributed the basic technology for rendering and android & windows compatibility.