diff --git a/Makefile b/Makefile index 7df854a0..7de14cba 100644 --- a/Makefile +++ b/Makefile @@ -1,171 +1,172 @@ -# Defaults -DEFAULTS_DIR := ./makefiles/defaults -_mkdir := $(shell mkdir -p $(DEFAULTS_DIR)) - -ifeq ($(origin ARCH),undefined) - ARCH := $(shell cat "$(DEFAULTS_DIR)/arch" 2>/dev/null) -endif -ifeq ($(strip $(ARCH)),) - ifeq ($(OS),Windows_NT) - ARCH := win64 - else - UNAME_S := $(shell uname -s) - ifeq ($(UNAME_S),Linux) - ARCH := linux - else - $(warning Unknown host '$(UNAME_S)', defaulting ARCH to linux) - ARCH := linux - endif - endif - _write_arch := $(shell echo -n "$(ARCH)" > "$(DEFAULTS_DIR)/arch") -endif - -ifeq ($(origin COMPILER),undefined) - COMPILER := $(shell cat "$(DEFAULTS_DIR)/cc" 2>/dev/null) -endif -ifeq ($(strip $(COMPILER)),) - ifneq ($(shell command -v gcc 2>/dev/null),) - COMPILER := gcc - else ifneq ($(shell command -v clang 2>/dev/null),) - COMPILER := clang - else - $(warning No gcc or clang found, defaulting COMPILER to gcc) - COMPILER := gcc - endif - _write_cc := $(shell echo -n "$(COMPILER)" > "$(DEFAULTS_DIR)/cc") -endif - -# to-do: deduce via existence of Vulkan/OpenGL headers -ifeq ($(origin RENDERER),undefined) - RENDERER := $(shell cat "$(DEFAULTS_DIR)/renderer" 2>/dev/null) -endif -ifeq ($(strip $(RENDERER)),) - RENDERER := vulkan - _write_rend := $(shell echo -n "$(RENDERER)" > "$(DEFAULTS_DIR)/renderer") -endif - -TARGET_NAME = program -TARGET_EXTENSION = .exe -DLIB_EXTENSION = .dll -SLIB_EXTENSION = .a -PREFIX = $(ARCH).$(COMPILER).$(RENDERER) - -# Basic Paths -CXX := $(CDIR)$(CXX) -BIN_DIR += ./bin - -ENGINE_SRC_DIR += ./engine/src -ENGINE_INC_DIR += ./engine/inc -ENGINE_LIB_DIR += ./engine/lib -DEP_SRC_DIR += ./dep/src -EXT_SRC_DIR += ./ext -CLIENT_SRC_DIR += ./client - -# Base Flags -OPTIMIZATIONS = -O3 -fstrict-aliasing -DUF_NO_EXCEPTIONS -WARNINGS = -Wall -Wno-unknown-pragmas -Wno-unused-function -Wno-unused-variable -Wno-switch -Wno-reorder -Wno-sign-compare -Wno-unused-but-set-variable -Wno-ignored-attributes -Wno-narrowing -Wno-misleading-indentation -FLAGS += -std=c++2b $(OPTIMIZATIONS) $(WARNINGS) -fdiagnostics-color=always - -# Base Library Definitions -LIB_NAME += uf -EXT_LIB_NAME += ext -PREFIX_PATH = $(ARCH)/$(COMPILER)/$(RENDERER) -INC_DIR += $(ENGINE_INC_DIR) -LIB_DIR += $(ENGINE_LIB_DIR) - -INCS += -I$(ENGINE_INC_DIR) -I./dep/include/ -LIBS += -L$(ENGINE_LIB_DIR) -L$(LIB_DIR)/$(PREFIX_PATH)/ -L$(LIB_DIR)/$(ARCH)/$(COMPILER)/ -L$(LIB_DIR)/$(ARCH)/ -LINKS += $(UF_LIBS) $(EXT_LIBS) $(DEPS) - -# DLL -SRCS_DLL := $(shell find $(ENGINE_SRC_DIR) -name "*.cpp") $(shell find $(DEP_SRC_DIR) -name "*.cpp") -SRCS_DLL_C := $(shell find $(ENGINE_SRC_DIR) -name "*.c") $(shell find $(DEP_SRC_DIR) -name "*.c") -OBJS_DLL += $(patsubst %.cpp,%.$(PREFIX).o,$(SRCS_DLL)) $(patsubst %.c,%.$(PREFIX).o,$(SRCS_DLL_C)) -BASE_DLL += lib$(LIB_NAME) - -IM_DLL += $(ENGINE_LIB_DIR)/$(PREFIX_PATH)/$(BASE_DLL)$(DLIB_EXTENSION) -EX_DLL += $(BIN_DIR)/exe/lib/$(PREFIX_PATH)/$(BASE_DLL)$(DLIB_EXTENSION) - -EXT_IM_DLL += $(ENGINE_LIB_DIR)/$(PREFIX_PATH)/$(BASE_EXT_DLL)$(DLIB_EXTENSION) -EXT_EX_DLL += $(BIN_DIR)/exe/lib/$(PREFIX_PATH)/$(BASE_EXT_DLL)$(DLIB_EXTENSION) - -SRCS_EXT_DLL := $(shell find $(EXT_SRC_DIR) -name "*.cpp") -OBJS_EXT_DLL += $(patsubst %.cpp,%.$(PREFIX).o,$(SRCS_EXT_DLL)) -BASE_EXT_DLL += lib$(EXT_LIB_NAME) - -EXT_DEPS += -l$(LIB_NAME) $(DEPS) -EXT_INC_DIR += $(INC_DIR) -EXT_INCS += $(INCS) -EXT_LIBS += $(LIBS) -EXT_LINKS += $(UF_LIBS) $(EXT_LIBS) $(EXT_DEPS) - -# Executable -SRCS := $(shell find $(CLIENT_SRC_DIR) -name "*.cpp") -OBJS += $(patsubst %.cpp,%.$(PREFIX).o,$(SRCS)) -TARGET += $(BIN_DIR)/exe/$(TARGET_NAME).$(PREFIX)$(TARGET_EXTENSION) - -# Shaders -SRCS_SHADERS := $(shell find bin/data/shaders/ -name "*.glsl") -TARGET_SHADERS += $(patsubst %.glsl,%.spv,$(SRCS_SHADERS)) - -.DEFAULT_GOAL := $(PREFIX) - -.PHONY: $(PREFIX) clean run run-debug clean-shaders backup -.FORCE: - -include makefiles/platforms/$(ARCH).$(COMPILER).mk - -ifneq (,$(findstring win64,$(ARCH))) - include makefiles/platforms/win64.mk -else ifneq (,$(findstring linux,$(ARCH))) - include makefiles/platforms/linux.mk -else ifneq (,$(findstring dreamcast,$(ARCH))) - include makefiles/platforms/dreamcast.mk -endif - -include makefiles/dependencies.mk - -# Build Rules -$(PREFIX): $(EX_DLL) $(EXT_EX_DLL) $(TARGET) $(TARGET_SHADERS) - -%.$(PREFIX).o: %.cpp - $(CXX) $(FLAGS) $(INCS) -c $< -o $@ - -%.$(PREFIX).o: %.c - $(CC) $(FLAGS) $(INCS) -c $< -o $@ - -ifneq ($(ARCH),dreamcast) -$(TARGET): $(OBJS) - $(CXX) $(FLAGS) $(OBJS) $(LIBS) $(INCS) $(LINKS) -l$(LIB_NAME) -l$(EXT_LIB_NAME) -o $(TARGET) -endif - -%.spv: %.glsl - $(GLSLC) --target-env=vulkan1.2 -o $@ $< - @-$(SPV_LINTER) $@ - @-$(SPV_OPTIMIZER) --preserve-bindings --preserve-spec-constants -O $@ -o $@ - -shaders: $(TARGET_SHADERS) - -clean: - @-rm $(EX_DLL) - @-rm $(EXT_EX_DLL) - @-rm $(TARGET) - - @-rm -f $(OBJS_DLL) - @-rm -f $(OBJS_EXT_DLL) - @-rm -f $(OBJS) - -clean-shaders: - @-rm -f $(TARGET_SHADERS) - -run: - @echo -n $(ARCH) > "./bin/exe/default/arch" - @echo -n $(COMPILER) > "./bin/exe/default/cc" - @echo -n $(RENDERER) > "./bin/exe/default/renderer" - ./program.sh - -run-debug: - @echo -n $(ARCH) > "./bin/exe/default/arch" - @echo -n $(COMPILER) > "./bin/exe/default/cc" - @echo -n $(RENDERER) > "./bin/exe/default/renderer" - ./debug.sh +# Defaults +DEFAULTS_DIR := ./makefiles/defaults +_mkdir := $(shell mkdir -p $(DEFAULTS_DIR)) + +ifeq ($(origin ARCH),undefined) + ARCH := $(shell cat "$(DEFAULTS_DIR)/arch" 2>/dev/null) +endif +ifeq ($(strip $(ARCH)),) + ifeq ($(OS),Windows_NT) + ARCH := win64 + else + UNAME_S := $(shell uname -s) + ifeq ($(UNAME_S),Linux) + ARCH := linux + else + $(warning Unknown host '$(UNAME_S)', defaulting ARCH to linux) + ARCH := linux + endif + endif + _write_arch := $(shell echo -n "$(ARCH)" > "$(DEFAULTS_DIR)/arch") +endif + +ifeq ($(origin COMPILER),undefined) + COMPILER := $(shell cat "$(DEFAULTS_DIR)/cc" 2>/dev/null) +endif +ifeq ($(strip $(COMPILER)),) + ifneq ($(shell command -v gcc 2>/dev/null),) + COMPILER := gcc + else ifneq ($(shell command -v clang 2>/dev/null),) + COMPILER := clang + else + $(warning No gcc or clang found, defaulting COMPILER to gcc) + COMPILER := gcc + endif + _write_cc := $(shell echo -n "$(COMPILER)" > "$(DEFAULTS_DIR)/cc") +endif + +# to-do: deduce via existence of Vulkan/OpenGL headers +ifeq ($(origin RENDERER),undefined) + RENDERER := $(shell cat "$(DEFAULTS_DIR)/renderer" 2>/dev/null) +endif +ifeq ($(strip $(RENDERER)),) + RENDERER := vulkan + _write_rend := $(shell echo -n "$(RENDERER)" > "$(DEFAULTS_DIR)/renderer") +endif + +TARGET_NAME = program +TARGET_EXTENSION = .exe +DLIB_EXTENSION = .dll +SLIB_EXTENSION = .a +PREFIX = $(ARCH).$(COMPILER).$(RENDERER) + +# Basic Paths +CXX := $(CDIR)$(CXX) +BIN_DIR += ./bin + +ENGINE_SRC_DIR += ./engine/src +ENGINE_INC_DIR += ./engine/inc +ENGINE_LIB_DIR += ./engine/lib +DEP_SRC_DIR += ./dep/src +EXT_SRC_DIR += ./ext +CLIENT_SRC_DIR += ./client + +# Base Flags +OPTIMIZATIONS = -O3 -fstrict-aliasing -DUF_NO_EXCEPTIONS +WARNINGS = -Wall -Wno-unknown-pragmas -Wno-unused-function -Wno-unused-variable -Wno-switch -Wno-reorder -Wno-sign-compare -Wno-unused-but-set-variable -Wno-ignored-attributes -Wno-narrowing -Wno-misleading-indentation +FLAGS += -std=c++2b $(OPTIMIZATIONS) $(WARNINGS) -fdiagnostics-color=always + +# Base Library Definitions +LIB_NAME += uf +EXT_LIB_NAME += ext +PREFIX_PATH = $(ARCH)/$(COMPILER)/$(RENDERER) +INC_DIR += $(ENGINE_INC_DIR) +LIB_DIR += $(ENGINE_LIB_DIR) + +INCS += -I$(ENGINE_INC_DIR) -I./dep/include/ +LIBS += -L$(ENGINE_LIB_DIR) -L$(LIB_DIR)/$(PREFIX_PATH)/ -L$(LIB_DIR)/$(ARCH)/$(COMPILER)/ -L$(LIB_DIR)/$(ARCH)/ +LINKS += $(UF_LIBS) $(EXT_LIBS) $(DEPS) + +# DLL +SRCS_DLL := $(shell find $(ENGINE_SRC_DIR) -name "*.cpp") $(shell find $(DEP_SRC_DIR) -name "*.cpp") +SRCS_DLL_C := $(shell find $(ENGINE_SRC_DIR) -name "*.c") $(shell find $(DEP_SRC_DIR) -name "*.c") +OBJS_DLL += $(patsubst %.cpp,%.$(PREFIX).o,$(SRCS_DLL)) $(patsubst %.c,%.$(PREFIX).o,$(SRCS_DLL_C)) +BASE_DLL += lib$(LIB_NAME) + +IM_DLL += $(ENGINE_LIB_DIR)/$(PREFIX_PATH)/$(BASE_DLL)$(DLIB_EXTENSION) +EX_DLL += $(BIN_DIR)/exe/lib/$(PREFIX_PATH)/$(BASE_DLL)$(DLIB_EXTENSION) + +EXT_IM_DLL += $(ENGINE_LIB_DIR)/$(PREFIX_PATH)/$(BASE_EXT_DLL)$(DLIB_EXTENSION) +EXT_EX_DLL += $(BIN_DIR)/exe/lib/$(PREFIX_PATH)/$(BASE_EXT_DLL)$(DLIB_EXTENSION) + +SRCS_EXT_DLL := $(shell find $(EXT_SRC_DIR) -name "*.cpp") +OBJS_EXT_DLL += $(patsubst %.cpp,%.$(PREFIX).o,$(SRCS_EXT_DLL)) +BASE_EXT_DLL += lib$(EXT_LIB_NAME) + +EXT_DEPS += -l$(LIB_NAME) $(DEPS) +EXT_INC_DIR += $(INC_DIR) +EXT_INCS += $(INCS) +EXT_LIBS += $(LIBS) +EXT_LINKS += $(UF_LIBS) $(EXT_LIBS) $(EXT_DEPS) + +# Executable +SRCS := $(shell find $(CLIENT_SRC_DIR) -name "*.cpp") +OBJS += $(patsubst %.cpp,%.$(PREFIX).o,$(SRCS)) +TARGET += $(BIN_DIR)/exe/$(TARGET_NAME).$(PREFIX)$(TARGET_EXTENSION) + +# Shaders +SRCS_SHADERS := $(shell find bin/data/shaders/ -name "*.glsl") +TARGET_SHADERS += $(patsubst %.glsl,%.spv,$(SRCS_SHADERS)) + +.DEFAULT_GOAL := $(PREFIX) + +.PHONY: $(PREFIX) clean run run-debug clean-shaders backup +.FORCE: + +include makefiles/platforms/$(ARCH).$(COMPILER).mk + +ifneq (,$(findstring win64,$(ARCH))) + include makefiles/platforms/win64.mk +else ifneq (,$(findstring linux,$(ARCH))) + include makefiles/platforms/linux.mk +else ifneq (,$(findstring dreamcast,$(ARCH))) + include makefiles/platforms/dreamcast.mk +endif + +include makefiles/dependencies.mk + +# Build Rules +$(PREFIX): $(EX_DLL) $(EXT_EX_DLL) $(TARGET) $(TARGET_SHADERS) + +%.$(PREFIX).o: %.cpp + $(CXX) $(FLAGS) $(INCS) -c $< -o $@ + +%.$(PREFIX).o: %.c + $(CC) $(FLAGS) $(INCS) -c $< -o $@ + +ifneq ($(ARCH),dreamcast) +$(TARGET): $(OBJS) + $(CXX) $(FLAGS) $(OBJS) $(LIBS) $(INCS) $(LINKS) -l$(LIB_NAME) -l$(EXT_LIB_NAME) -o $(TARGET) +endif + +%.spv: %.glsl + $(GLSLC) --target-env=vulkan1.2 -o $@ $< + @-$(SPV_LINTER) $@ + @-$(SPV_OPTIMIZER) --preserve-bindings --preserve-spec-constants -O $@ -o $@ + +shaders: $(TARGET_SHADERS) + +clean: + @-rm $(EX_DLL) + @-rm $(EXT_EX_DLL) + @-rm $(TARGET) + + @-rm -f $(OBJS_DLL) + @-rm -f $(OBJS_EXT_DLL) + @-rm -f $(OBJS) + +clean-shaders: + @-rm -f $(TARGET_SHADERS) + +run: + @echo -n $(ARCH) > "./bin/exe/default/arch" + @echo -n $(COMPILER) > "./bin/exe/default/cc" + @echo -n $(RENDERER) > "./bin/exe/default/renderer" + ./program.sh + +run-debug: + @echo -n $(ARCH) > "./bin/exe/default/arch" + @echo -n $(COMPILER) > "./bin/exe/default/cc" + @echo -n $(RENDERER) > "./bin/exe/default/renderer" + ./debug.sh + diff --git a/bin/data/entities/scripts/dark/osm.lua b/bin/data/entities/scripts/dark/osm.lua index 764ae2a6..24d42ccb 100644 --- a/bin/data/entities/scripts/dark/osm.lua +++ b/bin/data/entities/scripts/dark/osm.lua @@ -1,89 +1,509 @@ -_G.OSM = _G.OSM or {} - -_G.OSM["TweqLockedButton"] = { - onMessage = function(entity, payload, dMeta) end, - - onFrob = function(entity, payload, dMeta) - local eName = entity:name() or entity:uid() - -- todo: deduce lock state - local isLocked = true - - print(entity, "is locked?", isLocked) - - if isLocked then - -- print(string.format("%s is locked! Emitting 'cardfail'.", eName)) - _G.DarkUtils.playSound(entity, "", "cardfail", { spatial = true, maxDistance = 15.0 }) - -- entity:callHook("ui:FlashMessage", { text = "Access Required: {}" }) - else - -- unlock logic - local cTags = (dMeta["class_tags"] or "") .. ", Event StateChange" - _G.DarkUtils.playSound(entity, cTags, "", { spatial = true, maxDistance = 15.0 }) - - entity:callHook("link:Broadcast.%UID%", { - message = "TurnOn", flavors = { "ControlDevice" }, caller = payload.user, callerDarkID = dMeta["id"] - }) - end - end -} - - -_G.OSM["RelayTrap"] = function(entity, payload, dMeta) - local msg = payload.message - entity:callHook("link:Broadcast.%UID%", { - message = msg, - flavors = { "ControlDevice", "SwitchLink" }, - caller = entity:uid(), - callerDarkID = payload.callerDarkID - }) -end - -_G.OSM["TrapQBFilter"] = _G.OSM["RelayTrap"] -_G.OSM["TrapQBNegFilter"] = _G.OSM["RelayTrap"] -_G.OSM["ElevatorButton"] = _G.OSM["RelayTrap"] - -_G.OSM["RequireAllTrap"] = function(entity, payload, dMeta) - local msg = payload.message - local callerDarkID = payload.callerDarkID - - _G.RAT_States = _G.RAT_States or {} - local uid = entity:uid() - _G.RAT_States[uid] = _G.RAT_States[uid] or { inputs = {}, wasOn = false } - local state = _G.RAT_States[uid] - - if callerDarkID then - state.inputs[callerDarkID] = (msg == "TurnOn") - end - - local allOn = true - local incoming = dMeta["incoming_connections"] or {} - for i = 1, #incoming do - local conn = incoming[i] - if conn.flavor == "ControlDevice" or conn.flavor == "SwitchLink" then - if not state.inputs[conn.source_id] then - allOn = false - break - end - end - end - - if allOn and not state.wasOn then - state.wasOn = true - entity:callHook("link:Broadcast.%UID%", { message = "TurnOn", flavors = { "ControlDevice", "SwitchLink" }, callerDarkID = dMeta["id"], caller = uid }) - elseif not allOn and state.wasOn then - state.wasOn = false - entity:callHook("link:Broadcast.%UID%", { message = "TurnOff", flavors = { "ControlDevice", "SwitchLink" }, callerDarkID = dMeta["id"], caller = uid }) - end -end - -_G.OSM["BaseElevator"] = function(entity, payload, dMeta) - local msg = payload.message - local eName = entity:name() or entity:uid() - - -- print(string.format("%s (Elevator) received command: %s", eName, msg)) - - if msg == "TurnOn" or msg == "TurnOff" then - -- print(string.format("--> Commanding Lift '%s' to move!", eName)) - end -end - -_G.OSM["Elevator"] = _G.OSM["BaseElevator"] \ No newline at end of file +_G.OSM = _G.OSM or {} +_G.DarkUtils = _G.DarkUtils or {} +_G.DarkQB = _G.DarkQB or {} + +_G.OSM_States = _G.OSM_States or {} +local function getState( entity ) + local uid = entity:uid() + _G.OSM_States[uid] = _G.OSM_States[uid] or {} + return _G.OSM_States[uid] +end + +-- play a frob/activate sound for an object, falling back through tag queries +local function playActivateSound( entity, dMeta ) + local tags = dMeta["class_tags"] or "" + if tags ~= "" then tags = tags .. ", " end + local played = _G.DarkUtils.playSound( entity, tags .. "Event Activate", nil, { spatial = true, maxDistance = 15.0 } ) + if not played then played = _G.DarkUtils.playSound( entity, tags .. "Event StateChange", nil, { spatial = true, maxDistance = 15.0 } ) end + if not played then _G.DarkUtils.playSound( entity, dMeta["class_tags"] or "", nil, { spatial = true, maxDistance = 15.0 } ) end +end + +local function broadcast( entity, dMeta, message, caller, flavors ) + entity:callHook( "link:Broadcast.%UID%", { + message = message, + flavors = flavors or { "ControlDevice", "SwitchLink" }, + caller = caller or entity:uid(), + callerDarkID = dMeta["id"] + }) +end + +-- resolve a dark object id to an entity +local function getTarget( darkID ) + local targetUID = _G.DarkTargets and _G.DarkTargets[darkID] + if not targetUID then return nil end + local targetEnt = entities.get(targetUID) + if targetEnt and targetEnt:uid() then return targetEnt end + return nil +end + +-- find the first connection matching a flavor substring, returns (connection, target entity) +local function findConnection( dMeta, flavorMatch ) + local conns = dMeta["connections"] or {} + for i = 1, #conns do + local conn = conns[i] + if string.find( conn.flavor or "", flavorMatch, 1, true ) then + return conn, getTarget( conn.target_id ) + end + end + return nil, nil +end + +-- per-object state cleanup +local ent = ent +ent:addHook( "entity:Destroy.%UID%", function() + if _G.OSM_States[ent:uid()] then _G.OSM_States[ent:uid()] = nil end +end) + +-- relays / logic +_G.OSM["RelayTrap"] = function(entity, payload, dMeta) + local msg = payload.message + broadcast( entity, dMeta, msg, entity:uid(), { "ControlDevice", "SwitchLink" } ) +end + +-- forwards any message it receives (including Toggle) +_G.OSM["TrapRouter"] = _G.OSM["RelayTrap"] + +-- relays the first message it receives, then goes inert +_G.OSM["OnceRouter"] = function(entity, payload, dMeta) + local st = getState( entity ) + if st.onceFired then return end + st.onceFired = true + broadcast( entity, dMeta, payload.message, entity:uid(), { "ControlDevice", "SwitchLink" } ) +end + +-- relays a message after a delay (seconds; override with dark metadata "delay") +_G.OSM["TrapDelay"] = function(entity, payload, dMeta) + local msg = payload.message + if not msg then return end + local delay = tonumber( dMeta["delay"] ) or 2.0 + entity:queueHook( "link:Broadcast.%UID%", { + message = msg, + flavors = { "ControlDevice", "SwitchLink" }, + caller = entity:uid(), + callerDarkID = dMeta["id"] + }, delay ) +end + +-- quest bit (QB) helpers. Bits are keyed by the dark id of the object that +-- set them; filters gate on the bits of their incoming sources. +local function qbSet( key, value ) + if value then _G.DarkQB[key] = true else _G.DarkQB[key] = nil end +end + +local function qbSatisfied( dMeta, negative ) + local incoming = dMeta["incoming_connections"] or {} + if #incoming == 0 then return true end -- fail open if unwired + for i = 1, #incoming do + local conn = incoming[i] + local key = tostring( dMeta["qb"] or conn.source_id ) + if _G.DarkQB[key] then return not negative end + end + return negative +end + +-- sets a quest bit when triggered, then relays the message +_G.OSM["TrapQBSet"] = function(entity, payload, dMeta) + local msg = payload.message + local key = tostring( dMeta["id"] ) + if msg == "TurnOn" then qbSet( key, true ) + elseif msg == "TurnOff" then qbSet( key, false ) end + broadcast( entity, dMeta, msg, entity:uid(), { "ControlDevice", "SwitchLink" } ) +end + +-- sets a quest bit when frobb'd +_G.OSM["FrobQB"] = { + onFrob = function(entity, payload, dMeta) + qbSet( tostring( dMeta["id"] ), true ) + playActivateSound( entity, dMeta ) + broadcast( entity, dMeta, "TurnOn", payload.user ) + end +} + +-- only relays while its quest bit is set +_G.OSM["TrapQBFilter"] = function(entity, payload, dMeta) + if not qbSatisfied( dMeta, false ) then return end + broadcast( entity, dMeta, payload.message, entity:uid(), { "ControlDevice", "SwitchLink" } ) +end + +-- only relays while its quest bit is unset +_G.OSM["TrapQBNegFilter"] = function(entity, payload, dMeta) + if not qbSatisfied( dMeta, true ) then return end + broadcast( entity, dMeta, payload.message, entity:uid(), { "ControlDevice", "SwitchLink" } ) +end + +-- relays only once all of its control inputs are on +_G.OSM["RequireAllTrap"] = function(entity, payload, dMeta) + local msg = payload.message + local callerDarkID = payload.callerDarkID + + _G.RAT_States = _G.RAT_States or {} + local uid = entity:uid() + _G.RAT_States[uid] = _G.RAT_States[uid] or { inputs = {}, wasOn = false } + local state = _G.RAT_States[uid] + + if callerDarkID then + state.inputs[callerDarkID] = (msg == "TurnOn") + end + + local allOn = true + local incoming = dMeta["incoming_connections"] or {} + for i = 1, #incoming do + local conn = incoming[i] + if conn.flavor == "ControlDevice" or conn.flavor == "SwitchLink" then + if not state.inputs[conn.source_id] then + allOn = false + break + end + end + end + + if allOn and not state.wasOn then + state.wasOn = true + entity:callHook("link:Broadcast.%UID%", { message = "TurnOn", flavors = { "ControlDevice", "SwitchLink" }, callerDarkID = dMeta["id"], caller = uid }) + elseif not allOn and state.wasOn then + state.wasOn = false + entity:callHook("link:Broadcast.%UID%", { message = "TurnOff", flavors = { "ControlDevice", "SwitchLink" }, callerDarkID = dMeta["id"], caller = uid }) + end +end + +-- buttons / tweqs +_G.OSM["TweqLockedButton"] = { + onMessage = function(entity, payload, dMeta) end, + + onFrob = function(entity, payload, dMeta) + local eName = entity:name() or entity:uid() + -- todo: deduce lock state + local isLocked = true + + print(entity, "is locked?", isLocked) + + if isLocked then + -- print(string.format("%s is locked! Emitting 'cardfail'.", eName)) + _G.DarkUtils.playSound(entity, "", "cardfail", { spatial = true, maxDistance = 15.0 }) + -- entity:callHook("ui:FlashMessage", { text = "Access Required: {}" }) + else + -- unlock logic + local cTags = (dMeta["class_tags"] or "") .. ", Event StateChange" + _G.DarkUtils.playSound(entity, cTags, "", { spatial = true, maxDistance = 15.0 }) + + entity:callHook("link:Broadcast.%UID%", { + message = "TurnOn", flavors = { "ControlDevice" }, caller = payload.user, callerDarkID = dMeta["id"] + }) + end + end +} + +-- basic button: frob turns it on (one-shot) +_G.OSM["BaseButton"] = { + onFrob = function(entity, payload, dMeta) + playActivateSound( entity, dMeta ) + broadcast( entity, dMeta, "TurnOn", payload.user ) + end +} + +-- toggle button: alternates TurnOn / TurnOff on each frob +_G.OSM["TwoStateButton"] = { + onFrob = function(entity, payload, dMeta) + local st = getState( entity ) + st.isOn = not st.isOn + playActivateSound( entity, dMeta ) + broadcast( entity, dMeta, st.isOn and "TurnOn" or "TurnOff", payload.user ) + end +} + +-- depressable tweq: frob latches it on +_G.OSM["TweqDepressable"] = { + onFrob = function(entity, payload, dMeta) + playActivateSound( entity, dMeta ) + broadcast( entity, dMeta, "TurnOn", payload.user ) + end +} + +-- toggle tweq: frob or message flips its state and relays it +_G.OSM["TrapTweq"] = { + onMessage = function(entity, payload, dMeta) + local st = getState( entity ) + st.isOn = (payload.message == "TurnOn") + broadcast( entity, dMeta, payload.message, payload.caller or entity:uid(), { "ControlDevice", "SwitchLink" } ) + end, + + onFrob = function(entity, payload, dMeta) + local st = getState( entity ) + st.isOn = not st.isOn + playActivateSound( entity, dMeta ) + broadcast( entity, dMeta, st.isOn and "TurnOn" or "TurnOff", payload.user ) + end +} + +-- doors +-- standard door: frob toggles it open/closed (door.lua performs the motion) +_G.OSM["StdDoor"] = { + onMessage = function(entity, payload, dMeta) + local st = getState( entity ) + if payload.message == "TurnOn" then st.isOpen = true + elseif payload.message == "TurnOff" then st.isOpen = false end + end, + + onFrob = function(entity, payload, dMeta) + local st = getState( entity ) + if st.isOpen == nil then + st.isOpen = (tonumber( (dMeta["door"] or {})["status"] ) == 1) + end + st.isOpen = not st.isOpen + playActivateSound( entity, dMeta ) + broadcast( entity, dMeta, st.isOpen and "TurnOn" or "TurnOff", payload.user ) + end +} + +-- lights +-- find the light child node created by the level loader ("{name}_light") +local function getLightComponent( entity ) + local lightEnt = entity:findByName( (entity:name() or "") .. "_light" ) + if not lightEnt then return nil end + return lightEnt:getComponent("LightBehavior::Metadata") +end + +local function setLightPower( entity, on ) + local st = getState( entity ) + local lightComp = getLightComponent( entity ) + if not lightComp then return end + if st.lightPower == nil then st.lightPower = lightComp.power end + lightComp.power = on and st.lightPower or 0.0 +end + +-- switchable light: TurnOn/TurnOff (or frob) toggles its power +_G.OSM["BaseLight"] = { + onMessage = function(entity, payload, dMeta) + if payload.message == "TurnOn" then setLightPower( entity, true ) + elseif payload.message == "TurnOff" then setLightPower( entity, false ) end + end, + + onFrob = function(entity, payload, dMeta) + local st = getState( entity ) + st.isOn = not st.isOn + playActivateSound( entity, dMeta ) + setLightPower( entity, st.isOn ) + broadcast( entity, dMeta, st.isOn and "TurnOn" or "TurnOff", payload.user ) + end +} + +-- light that hums while it is on +_G.OSM["LightSoundOn"] = { + onMessage = function(entity, payload, dMeta) + if payload.message == "TurnOn" then + setLightPower( entity, true ) + local st = getState( entity ) + if not st.isSounding then + local soundMeta = dMeta["sound"] or {} + local explicitSchema = soundMeta["schema"] or "" + local played = _G.DarkUtils.playSound( entity, "", explicitSchema, { spatial = true, loop = true, unique = true } ) + if not played then + played = _G.DarkUtils.playSound( entity, (dMeta["class_tags"] or "") .. ", Event Activate", nil, { spatial = true, loop = true, unique = true } ) + end + st.isSounding = played ~= nil + end + elseif payload.message == "TurnOff" then + setLightPower( entity, false ) + local st = getState( entity ) + if st.isSounding then + st.isSounding = nil + entity:callHook( "sound:Stop.%UID%", {} ) + end + end + end, + + onFrob = function(entity, payload, dMeta) + local st = getState( entity ) + st.isOn = not st.isOn + playActivateSound( entity, dMeta ) + setLightPower( entity, st.isOn ) + broadcast( entity, dMeta, st.isOn and "TurnOn" or "TurnOff", payload.user ) + end +} + +-- gravity zones +local function gravityTick( self, factor ) + local body = self:getComponent("PhysicsBody") + if not body or not body:initialized() then return end + + local st = getState( self ) + local affected = st.gravAffected or {} + st.gravAffected = affected + local current = {} + + local events = body:getCollisionEvents() + for i = 1, #events do + local event = events[i] + local otherBody + if event.a:getObject():uid() == self:uid() then + otherBody = event.b + elseif event.b:getObject():uid() == self:uid() then + otherBody = event.a + end + + if otherBody then + local otherEnt = otherBody:getObject() + local uid = otherEnt:uid() + if uid ~= self:uid() and otherBody:getMass() > 0.0 then + current[uid] = true + if not affected[uid] then + otherBody:setGravity( Vector3f( 0, -9.81 * factor, 0 ) ) + affected[uid] = true + end + end + end + end + + for uid, _ in pairs( affected ) do + if not current[uid] then + local otherEnt = entities.get(uid) + if otherEnt and otherEnt:uid() then + local otherBody = otherEnt:getComponent("PhysicsBody") + if otherBody and otherBody:initialized() then + otherBody:enableGravity( true ) + end + end + affected[uid] = nil + end + end +end + +-- destruction / teleport / message traps +local function destroyTick( self ) + local body = self:getComponent("PhysicsBody") + if not body or not body:initialized() then return end + + local events = body:getCollisionEvents() + for i = 1, #events do + local event = events[i] + local otherBody + if event.a:getObject():uid() == self:uid() then + otherBody = event.b + elseif event.b:getObject():uid() == self:uid() then + otherBody = event.a + end + + if otherBody then + local otherEnt = otherBody:getObject() + if otherEnt:name() ~= "Player" and otherBody:getMass() > 0.0 then + entities.destroy( otherEnt ) + end + end + end +end + +-- destroys dynamic objects that touch it +_G.OSM["TriggerDestroy"] = function(entity, payload, dMeta) end +_G.OSM["TrapDestroyer"] = _G.OSM["TriggerDestroy"] +_G.OSM["TrapDestroy"] = _G.OSM["TriggerDestroy"] + +-- on trigger, destroys every object whose name matches its target connection +local function destroyAllMatching( entity, dMeta ) + local conn, _ = findConnection( dMeta, "" ) + if not conn or not conn.target_node then return end + local pattern = conn.target_node + + for i, victim in ipairs( entities.all() ) do + if victim and victim:uid() and victim:uid() ~= entity:uid() and victim:name() == pattern then + entities.destroy( victim ) + end + end +end + +_G.OSM["TrapTerminator"] = { + onMessage = function(entity, payload, dMeta) + if payload.message == "TurnOn" or payload.message == "Toggle" then + destroyAllMatching( entity, dMeta ) + end + end, + onFrob = function(entity, payload, dMeta) + playActivateSound( entity, dMeta ) + destroyAllMatching( entity, dMeta ) + end +} + +_G.OSM["DestroyAllByName"] = _G.OSM["TrapTerminator"] + +-- teleports the player to its teleport target on trigger or frob +local function teleportPlayer( entity, dMeta ) + local conn, targetEnt = findConnection( dMeta, "Tele" ) + if not targetEnt then return end + + local player = entities.controller() + if not player or not player:uid() then return end + + local playerTransform = player:getComponent("Transform") + local targetTransform = targetEnt:getComponent("Transform") + playerTransform.position = targetTransform.position + playerTransform.orientation = targetTransform.orientation +end + +_G.OSM["TrapTeleport"] = { + onMessage = function(entity, payload, dMeta) + if payload.message == "TurnOn" or payload.message == "Toggle" then + teleportPlayer( entity, dMeta ) + end + end, + onFrob = function(entity, payload, dMeta) + playActivateSound( entity, dMeta ) + teleportPlayer( entity, dMeta ) + end +} + +-- flashes a text message to the player on trigger or frob +_G.OSM["TrapMessage"] = { + onMessage = function(entity, payload, dMeta) + local text = dMeta["message"] or (entity:name() or "") + print( "[OSM] TrapMessage: " .. tostring(text) ) + entity:callHook( "ui:FlashMessage", { text = text } ) + end, + onFrob = function(entity, payload, dMeta) + local text = dMeta["message"] or (entity:name() or "") + print( "[OSM] TrapMessage: " .. tostring(text) ) + entity:callHook( "ui:FlashMessage", { text = text } ) + end +} + +-- elevators +_G.OSM["ElevatorButton"] = _G.OSM["RelayTrap"] +_G.OSM["RerouteElevatorButton"] = _G.OSM["RelayTrap"] + +_G.OSM["BaseElevator"] = function(entity, payload, dMeta) + local msg = payload.message + local eName = entity:name() or entity:uid() + + -- print(string.format("%s (Elevator) received command: %s", eName, msg)) + + if msg == "TurnOn" or msg == "TurnOff" then + -- print(string.format("--> Commanding Lift '%s' to move!", eName)) + end +end + +_G.OSM["Elevator"] = _G.OSM["BaseElevator"] +_G.OSM["OldStyleBaseElevator"] = _G.OSM["BaseElevator"] + +-- case variants found in level data (to-do: case insensitive mappings?) +_G.OSM["basebutton"] = _G.OSM["BaseButton"] +_G.OSM["trapdelay"] = _G.OSM["TrapDelay"] +_G.OSM["triggerdestroy"] = _G.OSM["TriggerDestroy"] +_G.OSM["Trapterminator"] = _G.OSM["TrapTerminator"] +_G.OSM["LIghtSoundOn"] = _G.OSM["LightSoundOn"] +_G.OSM["tweqtrap"] = _G.OSM["TrapTweq"] +_G.OSM["tweqbutton"] = _G.OSM["BaseButton"] +_G.OSM["Tweqable"] = _G.OSM["TrapTweq"] + +-- per-object tick binding (only for scripts that need one) +local metadata = ent:getComponent("Metadata") +local darkMeta = metadata["dark"] or {} +local myScripts = darkMeta["scripts"] or {} + +for i = 1, #myScripts do + local script = myScripts[i] + if script == "TrapGravity" or script == "ZeroGravRoom" then + local factor = tonumber( darkMeta["gravity"] ) + if factor == nil then factor = 0.0 end + ent:bind( "tick", function(self) gravityTick( self, factor ) end ) + elseif script == "TriggerDestroy" or script == "triggerdestroy" or script == "TrapDestroyer" or script == "TrapDestroy" then + ent:bind( "tick", function(self) destroyTick( self ) end ) + end +end diff --git a/bin/data/entities/scripts/valve/breakable.lua b/bin/data/entities/scripts/valve/breakable.lua new file mode 100644 index 00000000..7dd31328 --- /dev/null +++ b/bin/data/entities/scripts/valve/breakable.lua @@ -0,0 +1,42 @@ +local ent = ent +local metadata = ent:getComponent("Metadata") +local metadataValve = metadata["valve"] or {} +local physicsBody = ent:getComponent("PhysicsBody") + +local health = tonumber(metadataValve["health"]) or 100.0 +local flags = tonumber(metadataValve["spawnflags"]) or 0 +local unbreakable = (math.floor(flags / 1) % 2) ~= 0 +local damageScale = tonumber(metadataValve["damagescale"]) or 1.0 + +local alive = true + +local function destroy() + if not alive then return end + alive = false + ent:queueHook("io:FireOutput.%UID%", { output = "OnBreak" }, 0) + entities.destroy(ent) +end + +ent:bind( "tick", function(self) + if not alive or unbreakable then return end + if not physicsBody:initialized() then return end + + local collisionEvents = physicsBody:getCollisionEvents() + for i, event in ipairs(collisionEvents) do + if event.impulse > 1.0 then + health = health - (event.impulse * damageScale) + if health <= 0 then + destroy() + break + end + end + end +end ) + +ent:addHook("io:Input.%UID%", function( payload ) + local input = payload.input + + if input == "Kill" or input == "Break" then + destroy() + end +end) diff --git a/bin/data/entities/scripts/valve/button.lua b/bin/data/entities/scripts/valve/button.lua new file mode 100644 index 00000000..3becec44 --- /dev/null +++ b/bin/data/entities/scripts/valve/button.lua @@ -0,0 +1,124 @@ +local ent = ent +local metadata = ent:getComponent("Metadata") +local metadataValve = metadata["valve"] or {} + +local timer = Timer.new() +if not timer:running() then + timer:start() +end + +local SOURCE_TO_METERS = 0.07 + +local transform = ent:getComponent("Transform") +local physicsBody = ent:getComponent("PhysicsBody") + +local lip = (tonumber(metadataValve["lip"]) or 8.0) * SOURCE_TO_METERS +local wait = tonumber(metadataValve["wait"]) or 1.0 +local flags = tonumber(metadataValve["spawnflags"]) or 0 + +-- movement direction: movedir vector, else angle keyvalue, else local up +local moveDir = Vector3f(0, 1, 0) +local movedir = metadataValve["movedir"] +if type(movedir) == "table" then + moveDir = Vector3f(movedir[1], movedir[2], movedir[3]):normalize() +elseif type(movedir) == "string" and movedir ~= "" then + local x, y, z = movedir:match("^%s*(-?%d+%.?%d*)%s+(-?%d+%.?%d*)%s+(-?%d+%.?%d*)%s*$") + if x then + moveDir = Vector3f(tonumber(x), tonumber(y), tonumber(z)):normalize() + end +else + local angle = tonumber(metadataValve["angle"]) + if angle then + if angle == -1 then + moveDir = Vector3f(0, 1, 0) + elseif angle == -2 then + moveDir = Vector3f(0, -1, 0) + else + local yaw = math.rad(angle) + moveDir = Vector3f(math.cos(yaw), 0.0, -math.sin(yaw)) + end + end +end + +local pressTime = 0.5 +if wait > 0 then + pressTime = math.max(0.25, math.min(wait, 2.0)) +end +local speed = lip / pressTime + +-- 0 = idle, 1 = pressing out, 2 = held, 3 = retracting +local state = 0 +local currentDistance = 0 + +local function press() + if state ~= 0 then return end + state = 1 + ent:queueHook("io:FireOutput.%UID%", { output = "OnPressed" }, 0) + ent:queueHook("io:FireOutput.%UID%", { output = "OnUsed" }, 0) +end + +local function release() + if state == 2 then + state = 3 + end +end + +ent:bind( "tick", function(self) + if state == 1 then + local remaining = lip - currentDistance + local move = math.min(time.delta() * speed, remaining) + currentDistance = currentDistance + move + transform.position = transform.position + moveDir * move + + if currentDistance >= lip then + state = 2 + timer:reset() + end + elseif state == 3 then + local move = math.min(time.delta() * speed, currentDistance) + currentDistance = currentDistance - move + transform.position = transform.position - moveDir * move + + if currentDistance <= 0 then + state = 0 + ent:queueHook("io:FireOutput.%UID%", { output = "OnUnpressed" }, 0) + end + elseif state == 2 and wait > 0 then + if timer:elapsed() >= wait then + state = 3 + end + end + + -- press when touched (floor buttons) + if state == 0 and physicsBody:initialized() then + local collisionEvents = physicsBody:getCollisionEvents() + for i, event in ipairs(collisionEvents) do + local other = nil + if event.a:getObject():uid() == ent:uid() then + other = event.b + elseif event.b:getObject():uid() == ent:uid() then + other = event.a + end + + if other then + press() + break + end + end + end +end ) + +ent:addHook( "entity:Use.%UID%", function( payload ) + if payload.user == ent:uid() then return end + press() +end ) + +ent:addHook("io:Input.%UID%", function( payload ) + local input = payload.input + + if input == "Press" or input == "Down" then + press() + elseif input == "Release" or input == "Up" or input == "Reset" then + release() + end +end) diff --git a/bin/data/entities/scripts/valve/case.lua b/bin/data/entities/scripts/valve/case.lua new file mode 100644 index 00000000..348abade --- /dev/null +++ b/bin/data/entities/scripts/valve/case.lua @@ -0,0 +1,25 @@ +local ent = ent +local metadata = ent:getComponent("Metadata") +local metadataValve = metadata["valve"] or {} + +local cases = {} +for i = 1, 20 do + local value = metadataValve["Case" .. i] + if value ~= nil and tostring(value) ~= "" then + cases[i] = tostring(value) + end +end + +ent:addHook("io:Input.%UID%", function( payload ) + local param = payload.parameter + local p = (param == nil) and "" or tostring(param) + + for i = 1, 20 do + if cases[i] then + if cases[i] == p or tonumber(cases[i]) == tonumber(p) then + ent:callHook("io:FireOutput.%UID%", { output = "Case" .. i }) + break + end + end + end +end) diff --git a/bin/data/entities/scripts/valve/counter.lua b/bin/data/entities/scripts/valve/counter.lua new file mode 100644 index 00000000..c2537a68 --- /dev/null +++ b/bin/data/entities/scripts/valve/counter.lua @@ -0,0 +1,37 @@ +local ent = ent +local metadata = ent:getComponent("Metadata") +local metadataValve = metadata["valve"] or {} + +local startCount = tonumber(metadataValve["StartCount"]) or 0 +local tripCount = tonumber(metadataValve["TripCount"]) or 10 +local flags = tonumber(metadataValve["spawnflags"]) or 0 + +local enabled = (math.floor(flags / 1) % 2) == 0 +local count = startCount + +ent:addHook("io:Input.%UID%", function( payload ) + if not enabled then return end + + local input = payload.input + + if input == "Increment" then + count = count + 1 + if count >= tripCount then + ent:callHook("io:FireOutput.%UID%", { output = "OnHigh" }) + count = startCount + end + elseif input == "Decrement" then + count = count - 1 + if count < startCount then + ent:callHook("io:FireOutput.%UID%", { output = "OnLow" }) + count = startCount + end + elseif input == "Reset" then + count = startCount + elseif input == "Set" then + local value = tonumber(payload.parameter) + if value then + count = math.floor(value) + end + end +end) diff --git a/bin/data/entities/scripts/valve/hurt.lua b/bin/data/entities/scripts/valve/hurt.lua new file mode 100644 index 00000000..5f47e066 --- /dev/null +++ b/bin/data/entities/scripts/valve/hurt.lua @@ -0,0 +1,56 @@ +local ent = ent +local metadata = ent:getComponent("Metadata") +local metadataValve = metadata["valve"] or {} +local physicsBody = ent:getComponent("PhysicsBody") + +local damage = tonumber(metadataValve["damage"]) or 1.0 +local delay = tonumber(metadataValve["delay"]) or 0.5 + +-- note: the engine has no health/damage system yet, so this trigger only +-- reports contact via I/O outputs (OnEntityTouch / OnTrigger / OnEndTouch) + +local touching = {} + +local function entityKey( body ) + local object = body:getObject() + local meta = object:getComponent("Metadata") + local valve = meta["valve"] or {} + if valve["targetname"] then + return tostring(valve["targetname"]) + end + return tostring(object:uid()) +end + +ent:bind( "tick", function(self) + if not physicsBody:initialized() then return end + + local currentCollisions = {} + local collisionEvents = physicsBody:getCollisionEvents() + + for i, event in ipairs(collisionEvents) do + local other = nil + if event.a:getObject():uid() == ent:uid() then + other = event.b + elseif event.b:getObject():uid() == ent:uid() then + other = event.a + end + + if other then + local key = entityKey(other) + currentCollisions[key] = true + + if not touching[key] then + touching[key] = true + ent:queueHook("io:FireOutput.%UID%", { output = "OnEntityTouch", parameter = key }, 0) + ent:queueHook("io:FireOutput.%UID%", { output = "OnTrigger" }, 0) + end + end + end + + for key, _ in pairs(touching) do + if not currentCollisions[key] then + touching[key] = nil + ent:queueHook("io:FireOutput.%UID%", { output = "OnEndTouch", parameter = key }, 0) + end + end +end ) diff --git a/bin/data/entities/scripts/valve/ladder.lua b/bin/data/entities/scripts/valve/ladder.lua new file mode 100644 index 00000000..3670756c --- /dev/null +++ b/bin/data/entities/scripts/valve/ladder.lua @@ -0,0 +1,34 @@ +local ent = ent +local metadata = ent:getComponent("Metadata") +local metadataValve = metadata["valve"] or {} +local physicsBody = ent:getComponent("PhysicsBody") + +local controller = entities.controller() +local controllerBody = controller:getComponent("PhysicsBody") + +local climbSpeed = 2.0 + +ent:bind( "tick", function(self) + if not physicsBody:initialized() then return end + if not controllerBody:initialized() then return end + + local box = physicsBody:bounds() + local pos = controllerBody:getTransform().position + + local inside = + pos.x >= box.min.x - 0.5 and pos.x <= box.max.x + 0.5 and + pos.z >= box.min.z - 0.5 and pos.z <= box.max.z + 0.5 and + pos.y >= box.min.y - 0.5 and pos.y <= box.max.y + 1.0 + + if not inside then return end + + local velocity = controllerBody:getVelocity() + + if window.keyPressed("Space") or window.keyPressed("W") then + velocity.y = climbSpeed + elseif window.keyPressed("S") then + velocity.y = -climbSpeed + end + + controllerBody:setVelocity(velocity) +end ) diff --git a/bin/data/entities/scripts/valve/relay.lua b/bin/data/entities/scripts/valve/relay.lua new file mode 100644 index 00000000..fc903370 --- /dev/null +++ b/bin/data/entities/scripts/valve/relay.lua @@ -0,0 +1,43 @@ +local ent = ent +local metadata = ent:getComponent("Metadata") +local metadataValve = metadata["valve"] or {} + +local timer = Timer.new() +if not timer:running() then + timer:start() +end + +local delay = tonumber(metadataValve["delay"]) or 0.0 +local enabled = true + +local pendingOutputs = {} + +ent:addHook("io:Input.%UID%", function( payload ) + local input = payload.input + + if input == "Enable" then + enabled = true + elseif input == "Disable" then + enabled = false + elseif input == "Toggle" then + enabled = not enabled + else + if not enabled then return end + + table.insert(pendingOutputs, { + fireTime = timer:elapsed() + math.max(0.0, delay), + input = input, + parameter = payload.parameter + }) + end +end) + +ent:bind( "tick", function(self) + for i = #pendingOutputs, 1, -1 do + local job = pendingOutputs[i] + if timer:elapsed() >= job.fireTime then + ent:queueHook("io:FireOutput.%UID%", { output = job.input, parameter = job.parameter }, 0) + table.remove(pendingOutputs, i) + end + end +end ) diff --git a/bin/data/entities/scripts/valve/timer.lua b/bin/data/entities/scripts/valve/timer.lua new file mode 100644 index 00000000..c0cf416d --- /dev/null +++ b/bin/data/entities/scripts/valve/timer.lua @@ -0,0 +1,55 @@ +local ent = ent +local metadata = ent:getComponent("Metadata") +local metadataValve = metadata["valve"] or {} + +local timer = Timer.new() +if not timer:running() then + timer:start() +end + +local refireTime = tonumber(metadataValve["RefireTime"]) or 10.0 +local randomRange = tonumber(metadataValve["random"]) or 0.0 +local flags = tonumber(metadataValve["spawnflags"]) or 0 + +local startDisabled = (tonumber(metadataValve["StartDisabled"]) or 0) ~= 0 +if not startDisabled and (math.floor(flags / 1) % 2) ~= 0 then + startDisabled = true +end + +local enabled = not startDisabled +local nextFire = refireTime + +local function interval() + local value = refireTime + if randomRange > 0 then + value = value + (math.random() * 2.0 - 1.0) * randomRange + end + return math.max(0.01, value) +end + +ent:bind( "tick", function(self) + if not enabled then return end + + if timer:elapsed() >= nextFire then + ent:queueHook("io:FireOutput.%UID%", { output = "OnTimer" }, 0) + nextFire = timer:elapsed() + interval() + end +end ) + +ent:addHook("io:Input.%UID%", function( payload ) + local input = payload.input + + if input == "Enable" then + enabled = true + nextFire = timer:elapsed() + refireTime + elseif input == "Disable" then + enabled = false + elseif input == "Toggle" then + enabled = not enabled + if enabled then + nextFire = timer:elapsed() + refireTime + end + elseif input == "Reset" then + nextFire = timer:elapsed() + refireTime + end +end) diff --git a/bin/data/entities/scripts/valve/train.lua b/bin/data/entities/scripts/valve/train.lua new file mode 100644 index 00000000..9885457c --- /dev/null +++ b/bin/data/entities/scripts/valve/train.lua @@ -0,0 +1,95 @@ +local ent = ent +local metadata = ent:getComponent("Metadata") +local metadataValve = metadata["valve"] or {} + +local SOURCE_TO_METERS = 0.07 + +local transform = ent:getComponent("Transform") +local physicsBody = ent:getComponent("PhysicsBody") + +local speed = (tonumber(metadataValve["speed"]) or 200.0) * SOURCE_TO_METERS +local targetName = metadataValve["target"] + +-- collect waypoints from path_corner entities chained via their target keyvalue +local waypoints = {} +local hasLoop = false +if type(targetName) == "string" and targetName ~= "" then + local cornerByName = {} + for i, e in ipairs(entities.all()) do + if e:name() == "path_corner" then + local meta = e:getComponent("Metadata") + local valve = meta["valve"] or {} + local tname = valve["targetname"] + if tname then + cornerByName[tname] = e + end + end + end + + local current = cornerByName[targetName] + local firstUid = nil + local visited = {} + while current and not visited[current:uid()] do + visited[current:uid()] = true + if firstUid == nil then firstUid = current:uid() end + table.insert(waypoints, current:getComponent("Transform").position) + + local meta = current:getComponent("Metadata") + local valve = meta["valve"] or {} + current = cornerByName[valve["target"]] + end + hasLoop = (current ~= nil and current:uid() == firstUid) +end + +-- 0 = idle, 1 = moving +local state = (#waypoints >= 2) and 1 or 0 +local waypointIndex = 1 +local lastPosition = transform.position + +ent:bind( "tick", function(self) + if state ~= 1 then return end + + local target = waypoints[waypointIndex] + local toTarget = target - transform.position + local dist = math.sqrt(toTarget.x * toTarget.x + toTarget.y * toTarget.y + toTarget.z * toTarget.z) + local step = time.delta() * speed + + if dist <= step then + transform.position = target + waypointIndex = waypointIndex + 1 + if waypointIndex > #waypoints then + if hasLoop then + waypointIndex = 1 + else + state = 0 + end + end + else + local dir = toTarget / dist + transform.position = transform.position + dir * step + end + + -- carry passengers riding on the train + local delta = transform.position - lastPosition + lastPosition = transform.position + + if physicsBody:initialized() then + local box = physicsBody:bounds() + for i, e in ipairs(entities.all()) do + if e:uid() ~= ent:uid() then + local body = e:getComponent("PhysicsBody") + if body:initialized() then + local pos = body:getTransform().position + local riding = + pos.x >= box.min.x - 0.6 and pos.x <= box.max.x + 0.6 and + pos.z >= box.min.z - 0.6 and pos.z <= box.max.z + 0.6 and + pos.y >= box.min.y - 0.5 and pos.y <= box.max.y + 2.5 + if riding then + local t = e:getComponent("Transform") + t.position = t.position + delta + end + end + end + end + end +end ) diff --git a/engine/inc/uf/ext/vulkan/device.h b/engine/inc/uf/ext/vulkan/device.h index 3a69a450..104a650e 100644 --- a/engine/inc/uf/ext/vulkan/device.h +++ b/engine/inc/uf/ext/vulkan/device.h @@ -125,6 +125,14 @@ namespace ext { uf::stl::unordered_map checkpoints; + // queue API calls (submit/waitIdle/present/checkpoint reads) must not overlap on the same VkQueue, even from different threads + struct QueueLocks { + std::mutex mutex; + uf::stl::unordered_map> locks; + uf::stl::unordered_map> poolLocks; + }; + std::unique_ptr queueLocks; + uf::Window* window; struct QueueFamilyIndices { @@ -176,6 +184,8 @@ namespace ext { VkCommandPool getCommandPool( QueueEnum ); VkQueue getQueue( QueueEnum, uf::thread::id_t ); VkCommandPool getCommandPool( QueueEnum, uf::thread::id_t ); + std::unique_lock lockQueue( VkQueue queue ); + std::unique_lock lockPool( VkCommandPool pool ); // RAII void initialize(); diff --git a/engine/src/engine/ext/voxelizer/behavior.cpp b/engine/src/engine/ext/voxelizer/behavior.cpp index 37625e27..994660b5 100644 --- a/engine/src/engine/ext/voxelizer/behavior.cpp +++ b/engine/src/engine/ext/voxelizer/behavior.cpp @@ -484,6 +484,7 @@ void ext::VoxelizerSceneBehavior::destroy( uf::Object& self ){ texture.device = &uf::renderer::device; texture.view = view; } + metadata.views.clear(); } void ext::VoxelizerSceneBehavior::Metadata::serialize( uf::Object& self, uf::Serializer& serializer ) { serializer["vxgi"]["size"] = /*this->*/voxelSize.x; diff --git a/engine/src/engine/graph/graph.cpp b/engine/src/engine/graph/graph.cpp index 02e1f773..b93fcc41 100644 --- a/engine/src/engine/graph/graph.cpp +++ b/engine/src/engine/graph/graph.cpp @@ -1422,6 +1422,52 @@ void uf::graph::process( pod::Graph& graph, int32_t index, uf::Object& parent ) node.metadata["holdable"] = (mass <= 35.0f) && !motionDisabled && !preventPickup; } + // bind button + else if ( node.name == "func_button" ) { + loadJson["assets"].emplace_back("ent://scripts/valve/button.lua"); + // signal to assign a physics body + if ( ext::json::isNull( node.metadata["physics"] ) ) { + node.metadata["physics"]["type"] = "bounding box"; + } + } + // bind train / moving platform + else if ( node.name == "func_train" || node.name == "func_track" ) { + loadJson["assets"].emplace_back("ent://scripts/valve/train.lua"); + // signal to assign a physics body + if ( ext::json::isNull( node.metadata["physics"] ) ) { + node.metadata["physics"]["type"] = "bounding box"; + } + } + // bind breakable + else if ( node.name == "func_breakable" || node.name.starts_with("breakable_") ) { + loadJson["assets"].emplace_back("ent://scripts/valve/breakable.lua"); + // signal to assign a physics body + if ( ext::json::isNull( node.metadata["physics"] ) ) { + node.metadata["physics"]["type"] = "mesh"; + node.metadata["physics"]["category"] = "static"; + } + } + // bind ladder + else if ( node.name == "func_ladder" ) { + loadJson["assets"].emplace_back("ent://scripts/valve/ladder.lua"); + // signal to assign a physics body + if ( ext::json::isNull( node.metadata["physics"] ) ) { + node.metadata["physics"]["type"] = "bounding box"; + node.metadata["physics"]["category"] = "trigger"; + } + } + // bind logic entities (pure I/O, no physics body) + else if ( node.name.starts_with("logic_") ) { + if ( node.name == "logic_timer" ) { + loadJson["assets"].emplace_back("ent://scripts/valve/timer.lua"); + } else if ( node.name == "logic_relay" ) { + loadJson["assets"].emplace_back("ent://scripts/valve/relay.lua"); + } else if ( node.name == "logic_counter" ) { + loadJson["assets"].emplace_back("ent://scripts/valve/counter.lua"); + } else if ( node.name == "logic_case" ) { + loadJson["assets"].emplace_back("ent://scripts/valve/case.lua"); + } + } // assume all other funcs are to have a physics body else if ( node.name.starts_with("func_") ) { if ( ext::json::isNull( node.metadata["physics"] ) ) { @@ -1433,7 +1479,11 @@ void uf::graph::process( pod::Graph& graph, int32_t index, uf::Object& parent ) // check if trigger if ( node.name.starts_with("trigger_") ) { - loadJson["assets"].emplace_back("ent://scripts/valve/trigger.lua"); + if ( node.name == "trigger_hurt" ) { + loadJson["assets"].emplace_back("ent://scripts/valve/hurt.lua"); + } else { + loadJson["assets"].emplace_back("ent://scripts/valve/trigger.lua"); + } // signal to assign a physics body if ( ext::json::isNull( node.metadata["physics"] ) ) { node.metadata["physics"]["type"] = "bounding box"; diff --git a/engine/src/ext/vulkan/buffer.cpp b/engine/src/ext/vulkan/buffer.cpp index 0654ac8d..bf15817d 100644 --- a/engine/src/ext/vulkan/buffer.cpp +++ b/engine/src/ext/vulkan/buffer.cpp @@ -39,7 +39,7 @@ void ext::vulkan::Buffer::aliasBuffer( const ext::vulkan::Buffer& buffer ) { this->memory = buffer.memory; this->descriptor = buffer.descriptor; this->alignment = buffer.alignment; - this->stride = buffer.alignment; + this->stride = buffer.stride; this->address = buffer.address; this->mapped = buffer.mapped; this->usage = buffer.usage; diff --git a/engine/src/ext/vulkan/device.cpp b/engine/src/ext/vulkan/device.cpp index fad735af..4f008a42 100644 --- a/engine/src/ext/vulkan/device.cpp +++ b/engine/src/ext/vulkan/device.cpp @@ -617,10 +617,14 @@ VkCommandBuffer ext::vulkan::Device::createCommandBuffer( VkCommandBufferLevel l if ( !pool.empty() ) { commandBuffer = pool.top(); pool.pop(); + // pooled command buffers are returned in the recorded state, they must be reset before re-recording + vkResetCommandBuffer( commandBuffer, 0 ); } } if ( commandBuffer == VK_NULL_HANDLE ) { - VkCommandBufferAllocateInfo cmdBufAllocateInfo = ext::vulkan::initializers::commandBufferAllocateInfo( getCommandPool(queue), level, 1 ); + auto pool = getCommandPool( queue ); + auto lock = this->lockPool( pool ); + VkCommandBufferAllocateInfo cmdBufAllocateInfo = ext::vulkan::initializers::commandBufferAllocateInfo( pool, level, 1 ); VK_CHECK_RESULT( vkAllocateCommandBuffers( logicalDevice, &cmdBufAllocateInfo, &commandBuffer ) ); } @@ -647,25 +651,28 @@ void ext::vulkan::Device::flushCommandBuffer( VkCommandBuffer commandBuffer, Que submitInfo.pCommandBuffers = &commandBuffer; auto queue = getQueue( queueType ); - VK_CHECK_RESULT(vkQueueSubmit( queue, 1, &submitInfo, fence)); + { + auto lock = this->lockQueue( queue ); + VK_CHECK_RESULT(vkQueueSubmit( queue, 1, &submitInfo, fence)); - if ( immediate ) { - VkResult res = vkWaitForFences( this->logicalDevice, 1, &fence, VK_TRUE, VK_DEFAULT_FENCE_TIMEOUT ); - VK_CHECK_QUEUE_CHECKPOINT( queue, res ); + if ( immediate ) { + VkResult res = vkWaitForFences( this->logicalDevice, 1, &fence, VK_TRUE, VK_DEFAULT_FENCE_TIMEOUT ); + VK_CHECK_QUEUE_CHECKPOINT( queue, res ); - uf::checkpoint::deallocate(checkpoints[commandBuffer]); - checkpoints[commandBuffer] = NULL; - checkpoints.erase(commandBuffer); + uf::checkpoint::deallocate(checkpoints[commandBuffer]); + checkpoints[commandBuffer] = NULL; + checkpoints.erase(commandBuffer); - this->destroyFence( fence ); + this->destroyFence( fence ); - this->reusable.commandBuffers[queueType][uf::thread::current_id()].emplace( commandBuffer ); - } else { - ext::vulkan::mutex.lock(); - auto& transient = this->transient.commandBuffers[queueType][uf::thread::current_id()]; - transient.commandBuffers.emplace_back(commandBuffer); - transient.fences.emplace_back(fence); - ext::vulkan::mutex.unlock(); + this->reusable.commandBuffers[queueType][uf::thread::current_id()].emplace( commandBuffer ); + } else { + ext::vulkan::mutex.lock(); + auto& transient = this->transient.commandBuffers[queueType][uf::thread::current_id()]; + transient.commandBuffers.emplace_back(commandBuffer); + transient.fences.emplace_back(fence); + ext::vulkan::mutex.unlock(); + } } } pod::Checkpoint* ext::vulkan::Device::markCommandBuffer( VkCommandBuffer commandBuffer, pod::Checkpoint::Type type, const uf::stl::string& name, const uf::stl::string& info ) { @@ -703,25 +710,28 @@ void ext::vulkan::Device::flushCommandBuffer( ext::vulkan::CommandBuffer& comman submitInfo.pCommandBuffers = &commandBuffer.handle; auto queue = getQueue( commandBuffer.queueType, commandBuffer.threadId ); - VK_CHECK_RESULT(vkQueueSubmit( queue, 1, &submitInfo, fence)); + { + auto lock = this->lockQueue( queue ); + VK_CHECK_RESULT(vkQueueSubmit( queue, 1, &submitInfo, fence)); - if ( commandBuffer.immediate ) { - VkResult res = vkWaitForFences( this->logicalDevice, 1, &fence, VK_TRUE, VK_DEFAULT_FENCE_TIMEOUT ); - VK_CHECK_QUEUE_CHECKPOINT( queue, res ); + if ( commandBuffer.immediate ) { + VkResult res = vkWaitForFences( this->logicalDevice, 1, &fence, VK_TRUE, VK_DEFAULT_FENCE_TIMEOUT ); + VK_CHECK_QUEUE_CHECKPOINT( queue, res ); - uf::checkpoint::deallocate(checkpoints[commandBuffer.handle]); - checkpoints[commandBuffer.handle] = NULL; - checkpoints.erase(commandBuffer.handle); + uf::checkpoint::deallocate(checkpoints[commandBuffer.handle]); + checkpoints[commandBuffer.handle] = NULL; + checkpoints.erase(commandBuffer.handle); - this->destroyFence( fence ); + this->destroyFence( fence ); - this->reusable.commandBuffers[commandBuffer.queueType][commandBuffer.threadId].emplace( commandBuffer.handle ); - } else { - ext::vulkan::mutex.lock(); - auto& transient = this->transient.commandBuffers[commandBuffer.queueType][commandBuffer.threadId]; - transient.commandBuffers.emplace_back(commandBuffer.handle); - transient.fences.emplace_back(fence); - ext::vulkan::mutex.unlock(); + this->reusable.commandBuffers[commandBuffer.queueType][commandBuffer.threadId].emplace( commandBuffer.handle ); + } else { + ext::vulkan::mutex.lock(); + auto& transient = this->transient.commandBuffers[commandBuffer.queueType][commandBuffer.threadId]; + transient.commandBuffers.emplace_back(commandBuffer.handle); + transient.fences.emplace_back(fence); + ext::vulkan::mutex.unlock(); + } } } @@ -889,10 +899,28 @@ VkQueue ext::vulkan::Device::getQueue( ext::vulkan::QueueEnum queueEnum, uf::thr } return queue; } +std::unique_lock ext::vulkan::Device::lockQueue( VkQueue queue ) { + UF_ASSERT( this->queueLocks ); + std::lock_guard guard( this->queueLocks->mutex ); + auto& locks = this->queueLocks->locks; + auto it = locks.find( queue ); + if ( it == locks.end() ) it = locks.emplace( queue, std::make_unique() ).first; + return std::unique_lock( *it->second ); +} +std::unique_lock ext::vulkan::Device::lockPool( VkCommandPool pool ) { + UF_ASSERT( this->queueLocks ); + std::lock_guard guard( this->queueLocks->mutex ); + auto& locks = this->queueLocks->poolLocks; + auto it = locks.find( pool ); + if ( it == locks.end() ) it = locks.emplace( pool, std::make_unique() ).first; + return std::unique_lock( *it->second ); +} void ext::vulkan::Device::initialize() { auto& device = *this; + this->queueLocks = std::make_unique(); + uf::stl::vector instanceLayers = { // "VK_LAYER_KHRONOS_synchronization2", }; @@ -1172,6 +1200,41 @@ void ext::vulkan::Device::initialize() { // Else we use the same queue queueFamilyIndices.transfer = queueFamilyIndices.graphics; } + // Present queue (must be resolved before tallying, queues are created per-family) + { + uint32_t graphicsQueueNodeIndex = UINT32_MAX; + uint32_t presentQueueNodeIndex = UINT32_MAX; + uint32_t computeQueueNodeIndex = UINT32_MAX; + uint32_t transferQueueNodeIndex = UINT32_MAX; + + int i = 0; + for (const auto& queueFamily : queueFamilyProperties) { + if ( queueFamily.queueCount > 0 && queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT ) { + graphicsQueueNodeIndex = i; + } + + if ( queueFamily.queueCount > 0 && queueFamily.queueFlags & VK_QUEUE_COMPUTE_BIT ) { + computeQueueNodeIndex = i; + } + + if ( queueFamily.queueCount > 0 && queueFamily.queueFlags & VK_QUEUE_TRANSFER_BIT ) { + transferQueueNodeIndex = i; + } + + VkBool32 presentSupport = false; + vkGetPhysicalDeviceSurfaceSupportKHR( this->physicalDevice, i, surface, &presentSupport ); + if ( queueFamily.queueCount > 0 && presentSupport ) { + presentQueueNodeIndex = i; + } + + if ( graphicsQueueNodeIndex != UINT32_MAX && presentQueueNodeIndex != UINT32_MAX && computeQueueNodeIndex != UINT32_MAX ) break; + + i++; + } + + if ( presentQueueNodeIndex == UINT32_MAX ) presentQueueNodeIndex = queueFamilyIndices.graphics; + queueFamilyIndices.present = presentQueueNodeIndex; + } // Dedicated acquire queue { queueFamilyIndices.acquire = queueFamilyIndices.present; @@ -1203,7 +1266,12 @@ void ext::vulkan::Device::initialize() { { std::map familyIndexCounters; auto assignQueueIndex = [&](uint32_t family) -> uint32_t { - return familyIndexCounters[family]++; + // roles may share a queue when the family has fewer queues than requested + uint32_t available = MAX( 1, std::min( requestedQueuesPerFamily[family], queueFamilyProperties[family].queueCount ) ); + auto& counter = familyIndexCounters[family]; + uint32_t index = counter % available; + counter++; + return index; }; device.queueIndices.graphics = assignQueueIndex( device.queueFamilyIndices.graphics ); @@ -1227,8 +1295,7 @@ void ext::vulkan::Device::initialize() { deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; deviceCreateInfo.queueCreateInfoCount = static_cast(queueCreateInfos.size());; deviceCreateInfo.pQueueCreateInfos = queueCreateInfos.data(); - // deviceCreateInfo.pEnabledFeatures = &enabledFeatures; - deviceCreateInfo.pEnabledFeatures = nullptr; + deviceCreateInfo.pEnabledFeatures = ext::vulkan::settings::requested::featureChain["physicalDevice2"].as(false) ? nullptr : &enabledFeatures; VkDeviceGroupDeviceCreateInfo groupDeviceCreateInfo = {}; groupDeviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_GROUP_DEVICE_CREATE_INFO; @@ -1409,43 +1476,12 @@ void ext::vulkan::Device::initialize() { getCommandPool( QueueEnum::TRANSFER ); // Set queue { - uint32_t graphicsQueueNodeIndex = UINT32_MAX; - uint32_t presentQueueNodeIndex = UINT32_MAX; - uint32_t computeQueueNodeIndex = UINT32_MAX; - uint32_t transferQueueNodeIndex = UINT32_MAX; - - int i = 0; - for (const auto& queueFamily : queueFamilyProperties) { - if ( queueFamily.queueCount > 0 && queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT ) { - graphicsQueueNodeIndex = i; - } - - if ( queueFamily.queueCount > 0 && queueFamily.queueFlags & VK_QUEUE_COMPUTE_BIT ) { - computeQueueNodeIndex = i; - } - - if ( queueFamily.queueCount > 0 && queueFamily.queueFlags & VK_QUEUE_TRANSFER_BIT ) { - transferQueueNodeIndex = i; - } - - VkBool32 presentSupport = false; - vkGetPhysicalDeviceSurfaceSupportKHR( this->physicalDevice, i, surface, &presentSupport ); - if ( queueFamily.queueCount > 0 && presentSupport ) { - presentQueueNodeIndex = i; - } - - if ( graphicsQueueNodeIndex != UINT32_MAX && presentQueueNodeIndex != UINT32_MAX && computeQueueNodeIndex != UINT32_MAX ) break; - - i++; - } - VK_VALIDATION_MESSAGE("Graphics queue: family={}, index={}", device.queueFamilyIndices.graphics, device.queueIndices.graphics ); VK_VALIDATION_MESSAGE("Compute queue: family={}, index={}", device.queueFamilyIndices.compute, device.queueIndices.compute ); VK_VALIDATION_MESSAGE("Transfer queue: family={}, index={}", device.queueFamilyIndices.transfer, device.queueIndices.transfer ); VK_VALIDATION_MESSAGE("Present queue: family={}, index={}", device.queueFamilyIndices.present, device.queueIndices.present ); VK_VALIDATION_MESSAGE("Acquire queue: family={}, index={}", device.queueFamilyIndices.acquire, device.queueIndices.acquire ); - device.queueFamilyIndices.present = presentQueueNodeIndex; getQueue( QueueEnum::GRAPHICS ); getQueue( QueueEnum::PRESENT ); getQueue( QueueEnum::COMPUTE ); @@ -1613,7 +1649,7 @@ void ext::vulkan::Device::destroy() { for ( auto& pair_1 : this->transient.commandBuffers ) { for ( auto& pair : pair_1.second ) { for ( auto& commandBuffer : pair.second.commandBuffers ) { - vkFreeCommandBuffers(logicalDevice, getCommandPool( pair_1.first ), 1, &commandBuffer); + vkFreeCommandBuffers(logicalDevice, getCommandPool( pair_1.first, pair.first ), 1, &commandBuffer); VK_UNREGISTER_HANDLE( commandBuffer ); commandBuffer = VK_NULL_HANDLE; } @@ -1635,8 +1671,8 @@ void ext::vulkan::Device::destroy() { VK_UNREGISTER_HANDLE( fence ); } for ( auto& [queueType, threadMap] : this->reusable.commandBuffers) { - VkCommandPool commandPool = getCommandPool(queueType); for ( auto& [threadId, pool] : threadMap) { + VkCommandPool commandPool = getCommandPool(queueType, threadId); if ( pool.empty() ) continue; uf::stl::vector buffersToFree; buffersToFree.reserve(pool.size()); @@ -1670,6 +1706,18 @@ void ext::vulkan::Device::destroy() { for ( auto& pair : Pipeline::pipelines ) pair.second.destroy(); Pipeline::pipelines.clear(); + // pipelines may have deferred their SBT buffers into transient.buffers above + for ( auto& buffer : this->transient.buffers ) { + buffer.destroy(false); + } + this->transient.buffers.clear(); + + // deferred texture destroys (e.g. the empty textures) that outlived the last flush + for ( auto& texture : this->transient.textures ) { + texture.destroy(false); + } + this->transient.textures.clear(); + descriptorAllocator.destroy(); for ( auto& pair : this->commandPool.graphics.container() ) { @@ -1687,6 +1735,20 @@ void ext::vulkan::Device::destroy() { VK_UNREGISTER_HANDLE( pair.second ); pair.second = VK_NULL_HANDLE; } + // the VMA allocator must die while the device is still alive, and only once every allocation it owns has been freed + if ( allocator ) { + VmaTotalStatistics statistics = {}; + vmaCalculateStatistics( allocator, &statistics ); + uint32_t allocationCount = 0; + for ( auto& detail : statistics.memoryType ) allocationCount += detail.statistics.allocationCount; + if ( allocationCount > 0 ) { + UF_MSG_DEBUG("VMA allocator still owns {} allocations at shutdown", allocationCount ); + } + vmaDestroyAllocator( allocator ); + VK_UNREGISTER_HANDLE( allocator ); + allocator = nullptr; + } + if ( this->logicalDevice ) { vkDestroyDevice( this->logicalDevice, nullptr ); VK_UNREGISTER_HANDLE( this->logicalDevice ); @@ -1706,9 +1768,6 @@ void ext::vulkan::Device::destroy() { VK_UNREGISTER_HANDLE( this->instance ); this->instance = nullptr; } - -// vmaDestroyAllocator( allocator ); - VK_UNREGISTER_HANDLE( allocator ); } void ext::vulkan::DescriptorAllocator::initialize(VkDevice inDevice) { diff --git a/engine/src/ext/vulkan/graphic.cpp b/engine/src/ext/vulkan/graphic.cpp index 5fce949d..92b73aa6 100644 --- a/engine/src/ext/vulkan/graphic.cpp +++ b/engine/src/ext/vulkan/graphic.cpp @@ -487,16 +487,16 @@ void ext::vulkan::Pipeline::destroy() { descriptorPool = VK_NULL_HANDLE; } */ - if ( pipelineLayout != VK_NULL_HANDLE ) { - vkDestroyPipelineLayout( *device, pipelineLayout, nullptr ); - VK_UNREGISTER_HANDLE( pipelineLayout ); - pipelineLayout = VK_NULL_HANDLE; - } if ( pipeline != VK_NULL_HANDLE ) { vkDestroyPipeline( *device, pipeline, nullptr ); VK_UNREGISTER_HANDLE( pipeline ); pipeline = VK_NULL_HANDLE; } + if ( pipelineLayout != VK_NULL_HANDLE ) { + vkDestroyPipelineLayout( *device, pipelineLayout, nullptr ); + VK_UNREGISTER_HANDLE( pipelineLayout ); + pipelineLayout = VK_NULL_HANDLE; + } for ( auto descriptorSetLayout : descriptorSetLayouts ) { if ( descriptorSetLayout != VK_NULL_HANDLE ) { vkDestroyDescriptorSetLayout( *device, descriptorSetLayout, nullptr ); @@ -505,6 +505,8 @@ void ext::vulkan::Pipeline::destroy() { } descriptorSetLayouts.clear(); + ext::vulkan::Buffers::destroy(); + // if ( settings::experimental::dedicatedThread ) ext::vulkan::states::rebuild = true; /* if ( ext::vulkan::hasRenderMode(descriptor.renderMode, true) ) { @@ -1004,6 +1006,8 @@ void ext::vulkan::DescriptorSets::update( const Graphic& graphic, const GraphicD } else */ { vkUpdateDescriptorSets( *device, writeDescriptorSets.size(), writeDescriptorSets.data(), 0, NULL ); } + // the updated sets may be bound into standing command buffers; re-record them so they pick up the new contents + renderMode.rerecord = true; this->metadata.built = true; return; @@ -1714,6 +1718,10 @@ void ext::vulkan::Graphic::generateTopAccelerationStructure( const uf::stl::vect } else UF_EXCEPTION("Buffers not found: {}", "tlasInstance"); auto& buffer = this->buffers.at(instanceIndex); + if ( instancesVK.size() * sizeof( VkAccelerationStructureInstanceKHR ) > buffer.allocationInfo.size ) { + buffer.destroy(); + buffer.initialize( NULL, instancesVK.size() * sizeof( VkAccelerationStructureInstanceKHR ), VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR ); + } void* map = buffer.map(); uf::stl::memcpy(map, instancesVK.data(), instancesVK.size() * sizeof(VkAccelerationStructureInstanceKHR)); buffer.unmap(); @@ -1942,7 +1950,7 @@ void ext::vulkan::Graphic::initializeDescriptorSet() { initializeDescriptorSet( this->descriptor ); } ext::vulkan::DescriptorSets& ext::vulkan::Graphic::initializeDescriptorSet( const GraphicDescriptor& descriptor ) { - auto& pipeline = getPipeline(); + auto& pipeline = getPipeline( descriptor ); auto& descriptorSet = descriptorSets[descriptor]; // ensure pipeline exists (because we're passing this as const) diff --git a/engine/src/ext/vulkan/rendermode.cpp b/engine/src/ext/vulkan/rendermode.cpp index 4071d9d5..79a69f5b 100644 --- a/engine/src/ext/vulkan/rendermode.cpp +++ b/engine/src/ext/vulkan/rendermode.cpp @@ -267,13 +267,17 @@ ext::vulkan::RenderMode::commands_container_t& ext::vulkan::RenderMode::getComma if ( !exists ) { commands.resize( swapchain.buffers ); - VkCommandBufferAllocateInfo cmdBufAllocateInfo = ext::vulkan::initializers::commandBufferAllocateInfo( - device->getCommandPool(this->queueEnum, id), - VK_COMMAND_BUFFER_LEVEL_PRIMARY, - static_cast(commands.size()) - ); + auto pool = device->getCommandPool(this->queueEnum, id); + { + auto lock = device->lockPool( pool ); + VkCommandBufferAllocateInfo cmdBufAllocateInfo = ext::vulkan::initializers::commandBufferAllocateInfo( + pool, + VK_COMMAND_BUFFER_LEVEL_PRIMARY, + static_cast(commands.size()) + ); - VK_CHECK_RESULT(vkAllocateCommandBuffers(*device, &cmdBufAllocateInfo, commands.data())); + VK_CHECK_RESULT(vkAllocateCommandBuffers(*device, &cmdBufAllocateInfo, commands.data())); + } } return commands; } @@ -295,7 +299,10 @@ void ext::vulkan::RenderMode::cleanupAllCommands() { if ( commandBuffers.empty() ) continue; VkQueue queue = device->getQueue( queueEnum, threadID ); - vkQueueWaitIdle( queue ); + { + auto lock = device->lockQueue( queue ); + vkQueueWaitIdle( queue ); + } /* VkResult res = vkWaitForFences( *device, fences.size(), fences.data(), VK_TRUE, VK_DEFAULT_FENCE_TIMEOUT ); VK_CHECK_QUEUE_CHECKPOINT( queue, res ); @@ -307,7 +314,11 @@ void ext::vulkan::RenderMode::cleanupAllCommands() { device->checkpoints.erase(commandBuffer); } - vkFreeCommandBuffers( *device, device->getCommandPool(queueEnum, threadID), static_cast(commandBuffers.size()), commandBuffers.data()); + { + auto pool = device->getCommandPool(queueEnum, threadID); + auto lock = device->lockPool( pool ); + vkFreeCommandBuffers( *device, pool, static_cast(commandBuffers.size()), commandBuffers.data()); + } commandBuffers.clear(); } container.clear(); @@ -320,7 +331,10 @@ void ext::vulkan::RenderMode::cleanupCommands( uf::thread::id_t id ) { VkQueue queue = device->getQueue( queueEnum, threadID ); - vkQueueWaitIdle( queue ); + { + auto lock = device->lockQueue( queue ); + vkQueueWaitIdle( queue ); + } /* VkResult res = vkWaitForFences( *device, fences.size(), fences.data(), VK_TRUE, VK_DEFAULT_FENCE_TIMEOUT ); VK_CHECK_QUEUE_CHECKPOINT( queue, res ); @@ -332,7 +346,11 @@ void ext::vulkan::RenderMode::cleanupCommands( uf::thread::id_t id ) { device->checkpoints.erase(commandBuffer); } - vkFreeCommandBuffers( *device, device->getCommandPool(queueEnum, threadID), static_cast(commandBuffers.size()), commandBuffers.data()); + { + auto pool = device->getCommandPool(queueEnum, threadID); + auto lock = device->lockPool( pool ); + vkFreeCommandBuffers( *device, pool, static_cast(commandBuffers.size()), commandBuffers.data()); + } commandBuffers.clear(); } this->commands.cleanup( id ); @@ -481,7 +499,9 @@ void ext::vulkan::RenderMode::destroy() { for ( auto& pair : this->commands.container() ) { if ( !pair.second.empty() ) { - vkFreeCommandBuffers( *device, device->getCommandPool(this->queueEnum, pair.first), static_cast(pair.second.size()), pair.second.data()); + auto pool = device->getCommandPool(this->queueEnum, pair.first); + auto lock = device->lockPool( pool ); + vkFreeCommandBuffers( *device, pool, static_cast(pair.second.size()), pair.second.data()); } pair.second.clear(); } @@ -506,9 +526,12 @@ void ext::vulkan::RenderMode::synchronize( uint64_t timeout ) { lockMutex(); VkQueue queue = device->getQueue( queueEnum, this->mostRecentCommandPoolId ); - VkResult res = vkWaitForFences( *device, fences.size(), fences.data(), VK_TRUE, timeout ); -// VkResult res = vkWaitForFences(*device, 1, &fences[states::currentBuffer], VK_TRUE, timeout); - VK_CHECK_QUEUE_CHECKPOINT( queue, res ); + { + auto lock = device->lockQueue( queue ); + VkResult res = vkWaitForFences( *device, fences.size(), fences.data(), VK_TRUE, timeout ); + // VkResult res = vkWaitForFences(*device, 1, &fences[states::currentBuffer], VK_TRUE, timeout); + VK_CHECK_QUEUE_CHECKPOINT( queue, res ); + } unlockMutex(); } diff --git a/engine/src/ext/vulkan/rendermodes/base.cpp b/engine/src/ext/vulkan/rendermodes/base.cpp index 1e44e919..4fd7a14b 100644 --- a/engine/src/ext/vulkan/rendermodes/base.cpp +++ b/engine/src/ext/vulkan/rendermodes/base.cpp @@ -111,7 +111,8 @@ VkSubmitInfo ext::vulkan::BaseRenderMode::queue() { submitInfo.pWaitDstStageMask = waitStageMask; submitInfo.pWaitSemaphores = &swapchain.presentCompleteSemaphores[states::currentBuffer]; submitInfo.waitSemaphoreCount = 1; - submitInfo.pSignalSemaphores = &renderCompleteSemaphores[states::currentBuffer]; + // the present-wait semaphore is paired with the acquired image, so it is only reused once that image is re-acquired + submitInfo.pSignalSemaphores = &renderCompleteSemaphores[states::imageIndex]; submitInfo.signalSemaphoreCount = 1; submitInfo.pCommandBuffers = &commands[states::currentBuffer]; submitInfo.commandBufferCount = 1; @@ -129,11 +130,16 @@ void ext::vulkan::BaseRenderMode::render() { { VkSubmitInfo submitInfo = this->queue(); VkQueue queue = device->getQueue( QueueEnum::GRAPHICS ); + auto lock = device->lockQueue( queue ); VkResult res = vkQueueSubmit( queue, 1, &submitInfo, fences[states::currentBuffer]); VK_CHECK_QUEUE_CHECKPOINT( queue, res ); } - VK_CHECK_RESULT(swapchain.queuePresent(device->getQueue( QueueEnum::PRESENT ), states::imageIndex, renderCompleteSemaphores[states::currentBuffer])); + { + VkQueue queue = device->getQueue( QueueEnum::PRESENT ); + auto lock = device->lockQueue( queue ); + VK_CHECK_RESULT(swapchain.queuePresent( queue, states::imageIndex, renderCompleteSemaphores[states::imageIndex])); + } states::currentBuffer = (states::currentBuffer + 1) % ext::vulkan::swapchain.buffers; this->executed = true; diff --git a/engine/src/ext/vulkan/rendermodes/deferred.cpp b/engine/src/ext/vulkan/rendermodes/deferred.cpp index ee2100ca..27eec208 100644 --- a/engine/src/ext/vulkan/rendermodes/deferred.cpp +++ b/engine/src/ext/vulkan/rendermodes/deferred.cpp @@ -668,10 +668,17 @@ void ext::vulkan::DeferredRenderMode::render() { VK_COMMAND_BUFFER_CALLBACK( EXECUTE_BEGIN, VkCommandBuffer{}, 0, {} ); + // wait on the slot's previous deferred submit so its fence can be re-signaled + VK_CHECK_RESULT(vkWaitForFences( *device, 1, &fences[states::currentBuffer], VK_TRUE, VK_DEFAULT_FENCE_TIMEOUT )); + VK_CHECK_RESULT(vkResetFences( *device, 1, &fences[states::currentBuffer] )); + VkSubmitInfo submitInfo = this->queue(); - VkQueue queue = device->getQueue( QueueEnum::GRAPHICS ); - VkResult res = vkQueueSubmit( queue, 1, &submitInfo, VK_NULL_HANDLE/*fences[states::currentBuffer]*/); - VK_CHECK_QUEUE_CHECKPOINT( queue, res ); + { + VkQueue queue = device->getQueue( QueueEnum::GRAPHICS ); + auto lock = device->lockQueue( queue ); + VkResult res = vkQueueSubmit( queue, 1, &submitInfo, fences[states::currentBuffer]); + VK_CHECK_QUEUE_CHECKPOINT( queue, res ); + } VK_COMMAND_BUFFER_CALLBACK( EXECUTE_END, VkCommandBuffer{}, 0, {} ); @@ -686,18 +693,21 @@ void ext::vulkan::DeferredRenderMode::destroy() { ::postprocesses::dof.atomicCounter.destroy(false); for ( auto& view : ::postprocesses::bloom.views ) { + vkDestroyImageView(device->logicalDevice, view, nullptr); VK_UNREGISTER_HANDLE(view); } ::postprocesses::bloom.views.clear(); for ( auto& view : ::postprocesses::dof.views ) { + vkDestroyImageView(device->logicalDevice, view, nullptr); VK_UNREGISTER_HANDLE(view); } ::postprocesses::dof.views.clear(); for ( auto& view : ::postprocesses::depthPyramid.views ) { + vkDestroyImageView(device->logicalDevice, view, nullptr); VK_UNREGISTER_HANDLE(view); } diff --git a/engine/src/ext/vulkan/rendermodes/rendertarget.cpp b/engine/src/ext/vulkan/rendermodes/rendertarget.cpp index 88142098..7804e83a 100644 --- a/engine/src/ext/vulkan/rendermodes/rendertarget.cpp +++ b/engine/src/ext/vulkan/rendermodes/rendertarget.cpp @@ -333,8 +333,11 @@ void ext::vulkan::RenderTargetRenderMode::render() { VkSubmitInfo submitInfo = this->queue(); VkQueue queue = device->getQueue( QueueEnum::GRAPHICS ); - VkResult res = vkQueueSubmit( queue, 1, &submitInfo, /*VK_NULL_HANDLE*/fences[states::currentBuffer]); - VK_CHECK_QUEUE_CHECKPOINT( queue, res ); + { + auto lock = device->lockQueue( queue ); + VkResult res = vkQueueSubmit( queue, 1, &submitInfo, /*VK_NULL_HANDLE*/fences[states::currentBuffer]); + VK_CHECK_QUEUE_CHECKPOINT( queue, res ); + } VK_COMMAND_BUFFER_CALLBACK( EXECUTE_END, VkCommandBuffer{}, 0, {} ); this->executed = true; diff --git a/engine/src/ext/vulkan/rendermodes/vr.cpp b/engine/src/ext/vulkan/rendermodes/vr.cpp index b5bd1e7c..0e64e5cd 100644 --- a/engine/src/ext/vulkan/rendermodes/vr.cpp +++ b/engine/src/ext/vulkan/rendermodes/vr.cpp @@ -251,8 +251,11 @@ void ext::vulkan::VrRenderMode::render() { VkSubmitInfo submitInfo = this->queue(); VkQueue queue = device->getQueue( QueueEnum::GRAPHICS ); - VkResult res = vkQueueSubmit( queue, 1, &submitInfo, /*VK_NULL_HANDLE*/fences[states::currentBuffer]); - VK_CHECK_QUEUE_CHECKPOINT( queue, res ); + { + auto lock = device->lockQueue( queue ); + VkResult res = vkQueueSubmit( queue, 1, &submitInfo, /*VK_NULL_HANDLE*/fences[states::currentBuffer]); + VK_CHECK_QUEUE_CHECKPOINT( queue, res ); + } VK_COMMAND_BUFFER_CALLBACK( EXECUTE_END, VkCommandBuffer{}, 0, {} ); #if UF_USE_OPENVR ext::openvr::submit(); diff --git a/engine/src/ext/vulkan/rendertarget.cpp b/engine/src/ext/vulkan/rendertarget.cpp index 16d3ade9..c26f981d 100644 --- a/engine/src/ext/vulkan/rendertarget.cpp +++ b/engine/src/ext/vulkan/rendertarget.cpp @@ -35,6 +35,17 @@ size_t ext::vulkan::RenderTarget::attach( const Attachment::Descriptor& descript VK_UNREGISTER_HANDLE( view ); } attachment->views.clear(); + // view and framebufferView are distinct handles not covered by views[] + if ( attachment->view ) { + vkDestroyImageView( *device, attachment->view, nullptr ); + VK_UNREGISTER_HANDLE( attachment->view ); + attachment->view = VK_NULL_HANDLE; + } + if ( attachment->framebufferView ) { + vkDestroyImageView( *device, attachment->framebufferView, nullptr ); + VK_UNREGISTER_HANDLE( attachment->framebufferView ); + attachment->framebufferView = VK_NULL_HANDLE; + } if ( attachment->image && attachment->descriptor.layout != VK_IMAGE_LAYOUT_PRESENT_SRC_KHR ) { vmaDestroyImage( allocator, attachment->image, attachment->allocation ); attachment->image = VK_NULL_HANDLE; @@ -482,7 +493,17 @@ void ext::vulkan::RenderTarget::destroy() { framebuffers.clear(); for ( auto& attachment : attachments ) { - if ( attachment.descriptor.aliased ) continue; + if ( attachment.descriptor.aliased ) { + if ( attachment.descriptor.layout != VK_IMAGE_LAYOUT_PRESENT_SRC_KHR ) continue; + for ( auto& view : attachment.views ) { + if ( view != VK_NULL_HANDLE ) { + vkDestroyImageView( *device, view, nullptr ); + VK_UNREGISTER_HANDLE( view ); + } + } + attachment.views.clear(); + continue; + } if ( attachment.framebufferView ) { vkDestroyImageView(*device, attachment.framebufferView, nullptr); VK_UNREGISTER_HANDLE( attachment.framebufferView ); diff --git a/engine/src/ext/vulkan/texture.cpp b/engine/src/ext/vulkan/texture.cpp index 7200ce12..d43d0b5e 100644 --- a/engine/src/ext/vulkan/texture.cpp +++ b/engine/src/ext/vulkan/texture.cpp @@ -696,6 +696,7 @@ void ext::vulkan::Texture::asRenderTarget( Device& device, uint32_t width, uint3 viewCreateInfo.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; viewCreateInfo.image = image; VK_CHECK_RESULT(vkCreateImageView(device, &viewCreateInfo, nullptr, &view)); + VK_REGISTER_HANDLE( view ); // Initialize a descriptor for later use this->updateDescriptors(); diff --git a/engine/src/ext/vulkan/vulkan.cpp b/engine/src/ext/vulkan/vulkan.cpp index 9268639c..2df2b720 100644 --- a/engine/src/ext/vulkan/vulkan.cpp +++ b/engine/src/ext/vulkan/vulkan.cpp @@ -540,10 +540,14 @@ void ext::vulkan::tick() { auto tasks = uf::thread::schedule( settings::invariant::multithreadedRecording ); for ( auto& renderMode : renderModes ) { if ( !renderMode || (renderMode->executed && !renderMode->execute) ) continue; if ( ext::vulkan::states::rebuild || renderMode->rebuild ) tasks.queue([renderMode]{ + renderMode->synchronize(); + renderMode->cleanupAllCommands(); renderMode->bindPipelines(); renderMode->createCommandBuffers(); }); else if ( renderMode->rerecord ) tasks.queue([renderMode]{ + renderMode->synchronize(); + renderMode->cleanupAllCommands(); renderMode->createCommandBuffers(); }); } @@ -641,9 +645,17 @@ void ext::vulkan::render() { VK_CHECK_RESULT(vkResetFences(device, fences.size(), fences.data())); } if ( !submitsCompute.empty() ) - VK_CHECK_RESULT(vkQueueSubmit(device.getQueue( QueueEnum::COMPUTE ), submitsCompute.size(), submitsCompute.data(), ::auxFences.compute[states::currentBuffer])); + { + VkQueue queue = device.getQueue( QueueEnum::COMPUTE ); + auto lock = device.lockQueue( queue ); + VK_CHECK_RESULT(vkQueueSubmit(queue, submitsCompute.size(), submitsCompute.data(), ::auxFences.compute[states::currentBuffer])); + } if ( !submitsGraphics.empty() ) - VK_CHECK_RESULT(vkQueueSubmit(device.getQueue( QueueEnum::GRAPHICS ), submitsGraphics.size(), submitsGraphics.data(), ::auxFences.graphics[states::currentBuffer])); + { + VkQueue queue = device.getQueue( QueueEnum::GRAPHICS ); + auto lock = device.lockQueue( queue ); + VK_CHECK_RESULT(vkQueueSubmit(queue, submitsGraphics.size(), submitsGraphics.data(), ::auxFences.graphics[states::currentBuffer])); + } } // submit swapchain (record + submit + present) last @@ -696,6 +708,28 @@ void ext::vulkan::render() { // ext::vulkan::mutex.unlock(); + // wait on any in-flight transient commands so their staging resources can be destroyed, and return their command buffers to the reusable pool + for ( auto& [ queueType, commandBuffers ] : transient.commandBuffers ) { + for ( auto& [ threadId, tuple ] : commandBuffers ) { + constexpr size_t TOTAL = 64; + for ( size_t i = 0; i < tuple.fences.size(); i += TOTAL ) { + size_t count = std::min( tuple.fences.size() - i, TOTAL ); + VK_CHECK_RESULT( vkWaitForFences( device, count, &tuple.fences[i], VK_TRUE, UINT64_MAX ) ); + } + for ( auto fence : tuple.fences ) device.destroyFence( fence ); + tuple.fences.clear(); + auto& pool = device.reusable.commandBuffers[queueType][threadId]; + for ( auto commandBuffer : tuple.commandBuffers ) { + if ( auto it = device.checkpoints.find(commandBuffer); it != device.checkpoints.end() ) { + uf::checkpoint::deallocate( it->second ); + device.checkpoints.erase( it ); + } + pool.emplace( commandBuffer ); + } + tuple.commandBuffers.clear(); + } + } + // cleanup in-flight buffers for ( auto& buffer : transient.buffers ) buffer.destroy(false); transient.buffers.clear(); @@ -726,8 +760,9 @@ void ext::vulkan::destroy( bool soft ) { for ( auto& renderMode : renderModes ) { if ( !renderMode || !renderMode->device ) continue; renderMode->destroy(); - delete renderMode; - renderMode = NULL; + if ( std::find( ext::vulkan::renderModes.begin(), ext::vulkan::renderModes.end(), renderMode ) != ext::vulkan::renderModes.end() ) { + delete renderMode; + } } ext::vulkan::renderModes.clear();