From d36fd6d2ee791b70fed4f0a99b6dabfa55ff62b7 Mon Sep 17 00:00:00 2001 From: wcjbr Date: Sun, 16 Aug 2026 13:32:51 +0800 Subject: [PATCH] aa --- .clang-format | 6 + .gitignore | 4 + .gitmodules | 3 - CMakeLists.txt | 25 + CMakePresets.json | 17 + compile_commands.json | 1 + config.ceg | 26 + examples/square/config.ceg | 30 + examples/square/main.cpp | 49 ++ include/actor.hpp | 112 +++ include/behavior.hpp | 65 ++ include/config.hpp | 54 ++ include/engine.hpp | 9 + include/game.hpp | 21 + include/mesh.hpp | 51 ++ include/openceg.hpp | 9 + include/render.hpp | 139 ++++ src/config.cpp | 323 +++++++++ src/engine.cpp | 117 ++++ src/engine_main.cpp | 5 + src/game.cpp | 17 + src/glfw_win32_stub.cpp | 415 +++++++++++ src/opencge.cpp | 1 + src/render.cpp | 1340 ++++++++++++++++++++++++++++++++++++ third_party/ConsoleLib | 1 - 25 files changed, 2836 insertions(+), 4 deletions(-) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 120000 compile_commands.json create mode 100644 config.ceg create mode 100644 examples/square/config.ceg create mode 100644 examples/square/main.cpp create mode 100644 include/actor.hpp create mode 100644 include/behavior.hpp create mode 100644 include/config.hpp create mode 100644 include/engine.hpp create mode 100644 include/game.hpp create mode 100644 include/mesh.hpp create mode 100644 include/openceg.hpp create mode 100644 include/render.hpp create mode 100644 src/config.cpp create mode 100644 src/engine.cpp create mode 100644 src/engine_main.cpp create mode 100644 src/game.cpp create mode 100644 src/glfw_win32_stub.cpp create mode 100644 src/opencge.cpp create mode 100644 src/render.cpp delete mode 160000 third_party/ConsoleLib diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..9026068 --- /dev/null +++ b/.clang-format @@ -0,0 +1,6 @@ +Language: Cpp +BasedOnStyle: Google +IndentWidth: 4 +ColumnLimit: 100 +AllowShortIfStatementsOnASingleLine: false +AlignTrailingComments: true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b97babe --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +out +.vscode +.cache +.idea \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index f7d569c..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "third_party/ConsoleLib"] - path = third_party/ConsoleLib - url = https://github.com/ZeroOSProject/ConsoleLib.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ee733ba --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.10.0) +project(opencge VERSION 0.1.0 LANGUAGES C CXX) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(opencge STATIC + src/opencge.cpp + src/game.cpp + src/render.cpp + src/config.cpp + src/engine.cpp +) +target_include_directories(opencge PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_compile_features(opencge PUBLIC cxx_std_17) + +find_package(glfw3 REQUIRED) +find_package(Vulkan REQUIRED) +find_package(PkgConfig REQUIRED) +pkg_check_modules(JSONCPP REQUIRED IMPORTED_TARGET jsoncpp) +target_link_libraries(opencge PUBLIC glfw Vulkan::Vulkan PkgConfig::JSONCPP) + +add_executable(square_example examples/square/main.cpp) +target_link_libraries(square_example PRIVATE opencge) + +add_executable(opencge_engine src/engine_main.cpp) +target_link_libraries(opencge_engine PRIVATE opencge) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..a44d178 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,17 @@ +{ + "version": 8, + "configurePresets": [ + { + "name": "Clang", + "displayName": "使用工具链文件配置预设", + "description": "设置 Ninja 生成器、版本和安装目录", + "generator": "Ninja", + "binaryDir": "${sourceDir}/out/build/${presetName}", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_TOOLCHAIN_FILE": "", + "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}" + } + } + ] +} \ No newline at end of file diff --git a/compile_commands.json b/compile_commands.json new file mode 120000 index 0000000..3d1c45e --- /dev/null +++ b/compile_commands.json @@ -0,0 +1 @@ +out/build/Clang/compile_commands.json \ No newline at end of file diff --git a/config.ceg b/config.ceg new file mode 100644 index 0000000..140ee50 --- /dev/null +++ b/config.ceg @@ -0,0 +1,26 @@ +{ + "window": { + "width": 800, + "height": 600, + "title": "OpenCGE - config.ceg" + }, + + "input": [ + { + "action": "quit", + "keys": ["Escape"] + } + ], + + "actors": [ + { + "name": "square", + "position": { "x": 0, "y": 0 }, + "facing": { "x": 1, "y": 0 }, + "mesh": { + "shape": "quad", + "color": { "r": 0.5, "g": 0.5, "b": 0.5, "a": 1.0 } + } + } + ] +} diff --git a/examples/square/config.ceg b/examples/square/config.ceg new file mode 100644 index 0000000..859a666 --- /dev/null +++ b/examples/square/config.ceg @@ -0,0 +1,30 @@ +{ + "window": { + "width": 800, + "height": 600, + "title": "OpenCGE - Square Example" + }, + + "input": [ + { "action": "quit", "keys": ["Escape"] }, + { "action": "reset", "mouse_buttons": ["Left"] }, + { "action": "move_left", "keys": ["Left"] }, + { "action": "move_right", "keys": ["Right"] }, + { "action": "move_up", "keys": ["Up"] }, + { "action": "move_down", "keys": ["Down"] } + ], + + "actors": [ + { + "name": "square", + "init": "square_init", + "tick": "square_tick", + "position": { "x": 0, "y": 0 }, + "facing": { "x": 1, "y": 0 }, + "mesh": { + "shape": "quad", + "color": { "r": 0.2, "g": 0.8, "b": 0.9, "a": 1.0 } + } + } + ] +} diff --git a/examples/square/main.cpp b/examples/square/main.cpp new file mode 100644 index 0000000..9f17f18 --- /dev/null +++ b/examples/square/main.cpp @@ -0,0 +1,49 @@ +#include + +#include + +namespace { + +void register_square_behaviors() { + auto& behaviors = openceg::BehaviorRegistry::getInstance(); + + behaviors.register_init("square_init", [](openceg::Actor& actor) { + actor.set_position({0.0F, 0.0F}); + actor.set_facing({1.0F, 0.0F}); + if (actor.mesh() != nullptr) { + actor.mesh()->set_color({0.2F, 0.8F, 0.9F, 1.0F}); + } + }); + + behaviors.register_tick( + "square_tick", + [](openceg::Actor& actor, float dt, const openceg::render::InputMap& input) { + const float t = actor.age(); + actor.set_facing({ + std::cos(t * 1.5F), + std::sin(t * 1.5F), + }); + + if (actor.mesh() != nullptr) { + actor.mesh()->set_color({ + 0.5F + 0.5F * std::sin(t * 2.0F), + 0.5F + 0.5F * std::sin(t * 2.0F + 2.094F), + 0.5F + 0.5F * std::sin(t * 2.0F + 4.189F), + 1.0F, + }); + } + + auto position = actor.position(); + const float move_speed = 0.7F; + position[0] += input.get_axis("move_left", "move_right") * move_speed * dt; + position[1] += input.get_axis("move_up", "move_down") * move_speed * dt; + actor.set_position(position); + }); +} + +} // namespace + +int main(int argc, char** argv) { + register_square_behaviors(); + return openceg::run_engine(argc, argv, "examples/square/config.ceg"); +} diff --git a/include/actor.hpp b/include/actor.hpp new file mode 100644 index 0000000..a8f8467 --- /dev/null +++ b/include/actor.hpp @@ -0,0 +1,112 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace openceg::render { +class InputMap; +} + +namespace openceg { + +class Actor { + private: + size_t id; + std::string nickName_; + mutable std::mutex transform_mutex_; + std::array position_{0.0F, 0.0F}; + std::array facing_{1.0F, 0.0F}; + std::shared_ptr mesh_; + + float age_ = 0.0F; + bool initialized_ = false; + std::function init_handler_; + std::function tick_handler_; + + public: + Actor() noexcept = default; + virtual ~Actor() = default; + + Actor(const Actor&) = delete; + Actor& operator=(const Actor&) = delete; + Actor(Actor&&) noexcept = delete; + Actor& operator=(Actor&&) noexcept = delete; + + Actor(std::string nickName, Game& game) { + id = game.new_id(); + nickName_ = std::move(nickName); + } + + Actor(std::string nickName, Game& game, std::shared_ptr mesh) + : Actor(std::move(nickName), game) { + mesh_ = std::move(mesh); + } + + static Actor& getInstance() { + static Actor instance; + return instance; + } + + const std::string& name() const { return nickName_; } + + float age() const { return age_; } + + void set_init_handler(std::function handler) { + init_handler_ = std::move(handler); + } + + void set_tick_handler( + std::function handler) { + tick_handler_ = std::move(handler); + } + + // 类似 Godot 的 _ready()。 + void initialize() { + if (initialized_) { + return; + } + initialized_ = true; + if (init_handler_) { + init_handler_(*this); + } + } + + // 类似 Godot 的 _process(delta)。 + void process(float dt, const render::InputMap& input) { + age_ += dt; + if (tick_handler_) { + tick_handler_(*this, dt, input); + } + } + + std::array position() const { + std::lock_guard lock(transform_mutex_); + return position_; + } + + void set_position(std::array position) { + std::lock_guard lock(transform_mutex_); + position_ = position; + } + + std::array facing() const { + std::lock_guard lock(transform_mutex_); + return facing_; + } + + void set_facing(std::array facing) { + std::lock_guard lock(transform_mutex_); + facing_ = facing; + } + + const std::shared_ptr& mesh() const { return mesh_; } + void set_mesh(std::shared_ptr mesh) { mesh_ = std::move(mesh); } +}; + +} // namespace openceg diff --git a/include/behavior.hpp b/include/behavior.hpp new file mode 100644 index 0000000..faa1650 --- /dev/null +++ b/include/behavior.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace openceg::render { +class InputMap; +} + +namespace openceg { + +using ActorInitHandler = std::function; +using ActorTickHandler = std::function; + +// 全局行为函数注册表。config.ceg 中的 init/tick 名字在这里解析为 C++ 处理函数。 +class BehaviorRegistry { + public: + static BehaviorRegistry& getInstance() { + static BehaviorRegistry instance; + return instance; + } + + BehaviorRegistry(const BehaviorRegistry&) = delete; + BehaviorRegistry& operator=(const BehaviorRegistry&) = delete; + + void register_init(const std::string& name, ActorInitHandler handler) { + std::lock_guard lock(mutex_); + init_handlers_[name] = std::move(handler); + } + + void register_tick(const std::string& name, ActorTickHandler handler) { + std::lock_guard lock(mutex_); + tick_handlers_[name] = std::move(handler); + } + + ActorInitHandler init(const std::string& name) const { + std::lock_guard lock(mutex_); + const auto it = init_handlers_.find(name); + if (it == init_handlers_.end()) { + return {}; + } + return it->second; + } + + ActorTickHandler tick(const std::string& name) const { + std::lock_guard lock(mutex_); + const auto it = tick_handlers_.find(name); + if (it == tick_handlers_.end()) { + return {}; + } + return it->second; + } + + private: + BehaviorRegistry() = default; + mutable std::mutex mutex_; + std::unordered_map init_handlers_; + std::unordered_map tick_handlers_; +}; + +} // namespace openceg diff --git a/include/config.hpp b/include/config.hpp new file mode 100644 index 0000000..ab91692 --- /dev/null +++ b/include/config.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include + +namespace openceg { + +struct WindowConfig { + int width = 800; + int height = 600; + std::string title = "OpenCGE"; +}; + +struct MeshConfig { + std::string shape = "quad"; + std::array color{0.5F, 0.5F, 0.5F, 1.0F}; +}; + +struct ActorConfig { + std::string name; + std::string init_function; + std::string tick_function; + std::array position{0.0F, 0.0F}; + std::array facing{1.0F, 0.0F}; + MeshConfig mesh; +}; + +struct InputBindingConfig { + std::string action; + std::vector keys; + std::vector mouse_buttons; +}; + +struct GameConfig { + WindowConfig window; + std::vector input_bindings; + std::vector actors; +}; + +// 从 JSON 加载游戏场景配置(类似 Godot 的场景描述)。 +class ConfigLoader { + public: + bool load(const std::string& path); + const GameConfig& config() const; + const std::string& last_error() const; + + private: + GameConfig config_; + std::string last_error_; +}; + +} // namespace openceg diff --git a/include/engine.hpp b/include/engine.hpp new file mode 100644 index 0000000..aa1f234 --- /dev/null +++ b/include/engine.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace openceg { + +// 通用引擎入口:读取 config.ceg,实例化场景并驱动 Actor 的 init/tick。 +// 具体游戏行为通过 BehaviorRegistry 注册,因此引擎本身不内置任何游戏逻辑。 +int run_engine(int argc, char** argv, const char* default_config_path = "config.ceg"); + +} // namespace openceg diff --git a/include/game.hpp b/include/game.hpp new file mode 100644 index 0000000..b72fa2a --- /dev/null +++ b/include/game.hpp @@ -0,0 +1,21 @@ +#pragma once +#include + +namespace openceg { +class Game { + private: + size_t id_cnt; + + public: + Game(); + ~Game(); + Game(const Game&) = delete; + Game& operator=(const Game&) = delete; + Game(Game&&) noexcept = delete; + Game& operator=(Game&&) noexcept = delete; + + static Game& getInstance(); + void launch(); + size_t new_id(); +}; +} // namespace openceg diff --git a/include/mesh.hpp b/include/mesh.hpp new file mode 100644 index 0000000..8137723 --- /dev/null +++ b/include/mesh.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace openceg { + +// 渲染组件:描述 Actor 的几何形状、颜色和纹理占位。 +class Mesh { + public: + struct Vertex { + std::array position; + std::array color; + }; + + std::vector vertices; + std::vector indices; + std::string texture; // 纹理路径占位,暂未采样。 + + std::array color() const { + std::lock_guard lock(*color_mutex_); + return color_; + } + + void set_color(std::array color) { + std::lock_guard lock(*color_mutex_); + color_ = color; + } + + static Mesh quad() { + Mesh mesh; + mesh.vertices = { + {{-1.0F, -1.0F}, {1.0F, 1.0F, 1.0F, 1.0F}}, + {{1.0F, -1.0F}, {1.0F, 1.0F, 1.0F, 1.0F}}, + {{1.0F, 1.0F}, {1.0F, 1.0F, 1.0F, 1.0F}}, + {{-1.0F, 1.0F}, {1.0F, 1.0F, 1.0F, 1.0F}}, + }; + mesh.indices = {0, 1, 2, 2, 3, 0}; + return mesh; + } + + private: + std::unique_ptr color_mutex_ = std::make_unique(); + std::array color_{1.0F, 1.0F, 1.0F, 1.0F}; +}; + +} // namespace openceg diff --git a/include/openceg.hpp b/include/openceg.hpp new file mode 100644 index 0000000..9fffa89 --- /dev/null +++ b/include/openceg.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include diff --git a/include/render.hpp b/include/render.hpp new file mode 100644 index 0000000..c865890 --- /dev/null +++ b/include/render.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace openceg::render { + +struct InputEvent { + enum class Type { + Key, + MouseButton, + CursorMove, + Scroll, + }; + + enum class Action { + Release = 0, + Press = 1, + Repeat = 2, + }; + + Type type = Type::Key; + Action action = Action::Press; + int key = -1; + int scancode = -1; + int mods = 0; + int button = -1; + double x = 0.0; + double y = 0.0; +}; + +class Renderer { + public: + Renderer(); + ~Renderer(); + + Renderer(const Renderer&) = delete; + Renderer& operator=(const Renderer&) = delete; + Renderer(Renderer&&) = delete; + Renderer& operator=(Renderer&&) = delete; + + void init(int width, int height, const char* title); + void draw(const Actor* const* actors, size_t actor_count); + bool should_close() const; + void poll_events() const; + + using InputCallback = std::function; + void set_input_callback(InputCallback callback); + + bool is_key_down(int key) const; + bool is_mouse_button_down(int button) const; + std::array cursor_position() const; + + void destroy(); + + private: + struct Impl; + Impl* impl_; +}; + +// 类似 Godot InputMap 的按键动作绑定。 +class InputMap { + public: + void add_action(const std::string& action); + void bind_key(const std::string& action, int key); + void unbind_key(const std::string& action, int key); + void bind_mouse_button(const std::string& action, int button); + void unbind_mouse_button(const std::string& action, int button); + + void handle_event(const InputEvent& event); + void end_frame(); + + bool is_action_pressed(const std::string& action) const; + bool is_action_just_pressed(const std::string& action) const; + bool is_action_just_released(const std::string& action) const; + float get_action_strength(const std::string& action) const; + float get_axis(const std::string& negative_action, + const std::string& positive_action) const; + + private: + struct ActionState { + std::unordered_set keys; + std::unordered_set mouse_buttons; + std::unordered_map held_keys; + std::unordered_map held_buttons; + size_t held_key_count = 0; + size_t held_button_count = 0; + bool pressed = false; + bool just_pressed = false; + bool just_released = false; + }; + + mutable std::mutex mutex_; + std::unordered_map actions_; +}; + +// 渲染线程包装:主线程更新 Actor,渲染线程持续读取并提交绘制。 +class RenderLoop { + public: + RenderLoop() = default; + ~RenderLoop(); + + RenderLoop(const RenderLoop&) = delete; + RenderLoop& operator=(const RenderLoop&) = delete; + RenderLoop(RenderLoop&&) = delete; + RenderLoop& operator=(RenderLoop&&) = delete; + + void start(int width, int height, const char* title); + void stop(); + + void set_actors(std::vector actors); + void poll_events() const; + bool should_close() const; + + InputMap& input(); + const InputMap& input() const; + + private: + void run(); + + Renderer renderer_; + InputMap input_map_; + std::thread thread_; + std::atomic running_{false}; + mutable std::mutex actors_mutex_; + std::vector actors_; +}; + +} // namespace openceg::render diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..179de26 --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,323 @@ +#include + +#include + +#include +#include +#include +#include +#include + +namespace openceg { +namespace { + +std::string lowercase(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return value; +} + +int parse_key_name(const std::string& name) { + const std::string key = lowercase(name); + if (key == "space") return 32; + if (key == "apostrophe" || key == "'") return 39; + if (key == "comma" || key == ",") return 44; + if (key == "minus" || key == "-") return 45; + if (key == "period" || key == ".") return 46; + if (key == "slash" || key == "/") return 47; + if (key.size() == 1 && key[0] >= '0' && key[0] <= '9') return key[0]; + if (key.size() == 1 && key[0] >= 'a' && key[0] <= 'z') return key[0] - 'a' + 'A'; + if (key == "semicolon" || key == ";") return 59; + if (key == "equal" || key == "=") return 61; + if (key == "leftbracket" || key == "[") return 91; + if (key == "backslash" || key == "\\") return 92; + if (key == "rightbracket" || key == "]") return 93; + if (key == "graveaccent" || key == "`") return 96; + if (key == "escape" || key == "esc") return 256; + if (key == "enter" || key == "return") return 257; + if (key == "tab") return 258; + if (key == "backspace") return 259; + if (key == "insert") return 260; + if (key == "delete" || key == "del") return 261; + if (key == "right") return 262; + if (key == "left") return 263; + if (key == "down") return 264; + if (key == "up") return 265; + if (key == "pageup" || key == "pgup") return 266; + if (key == "pagedown" || key == "pgdown" || key == "pgdn") return 267; + if (key == "home") return 268; + if (key == "end") return 269; + if (key == "capslock" || key == "caps") return 280; + if (key == "scrolllock") return 281; + if (key == "numlock") return 282; + if (key == "printscreen" || key == "prtscr") return 283; + if (key == "pause") return 284; + if (key.size() > 1 && key[0] == 'f') { + try { + int number = std::stoi(key.substr(1)); + if (number >= 1 && number <= 25) return 290 + number - 1; + } catch (...) { + } + } + if (key == "kp0" || key == "keypad0") return 320; + if (key == "kp1" || key == "keypad1") return 321; + if (key == "kp2" || key == "keypad2") return 322; + if (key == "kp3" || key == "keypad3") return 323; + if (key == "kp4" || key == "keypad4") return 324; + if (key == "kp5" || key == "keypad5") return 325; + if (key == "kp6" || key == "keypad6") return 326; + if (key == "kp7" || key == "keypad7") return 327; + if (key == "kp8" || key == "keypad8") return 328; + if (key == "kp9" || key == "keypad9") return 329; + if (key == "leftshift" || key == "lshift") return 340; + if (key == "rightshift" || key == "rshift") return 344; + if (key == "leftcontrol" || key == "leftctrl" || key == "lctrl") return 341; + if (key == "rightcontrol" || key == "rightctrl" || key == "rctrl") return 345; + if (key == "leftalt" || key == "lalt") return 342; + if (key == "rightalt" || key == "ralt") return 346; + if (key == "leftsuper" || key == "lsuper") return 343; + if (key == "rightsuper" || key == "rsuper") return 347; + + try { + return std::stoi(name); + } catch (...) { + return -1; + } +} + +int parse_mouse_button(const std::string& name) { + const std::string button = lowercase(name); + if (button == "left") return 0; + if (button == "right") return 1; + if (button == "middle") return 2; + try { + return std::stoi(name); + } catch (...) { + return -1; + } +} + +int json_int(const Json::Value& value, int fallback) { + if (value.isNumeric()) { + return value.asInt(); + } + if (value.isString()) { + try { + return std::stoi(value.asString()); + } catch (...) { + } + } + return fallback; +} + +float json_float(const Json::Value& value, float fallback) { + if (value.isNumeric()) { + return value.asFloat(); + } + if (value.isString()) { + try { + return std::stof(value.asString()); + } catch (...) { + } + } + return fallback; +} + +std::string json_string(const Json::Value& value, const std::string& fallback) { + if (value.isString()) { + return value.asString(); + } + if (value.isNumeric()) { + return std::to_string(value.asInt()); + } + return fallback; +} + +void add_binding_code(const Json::Value& value, int type, std::vector& output) { + if (value.isArray()) { + for (Json::ArrayIndex i = 0; i < value.size(); ++i) { + add_binding_code(value[i], type, output); + } + return; + } + + int code = -1; + if (value.isNumeric()) { + code = value.asInt(); + } else if (value.isString()) { + code = type == 0 ? parse_key_name(value.asString()) + : parse_mouse_button(value.asString()); + } + if (code >= 0) { + output.push_back(code); + } +} + +void parse_window(const Json::Value& root, WindowConfig& window) { + if (!root.isMember("window") || !root["window"].isObject()) { + return; + } + const Json::Value& value = root["window"]; + if (value.isMember("width")) { + window.width = json_int(value["width"], window.width); + } + if (value.isMember("height")) { + window.height = json_int(value["height"], window.height); + } + if (value.isMember("title")) { + window.title = json_string(value["title"], window.title); + } +} + +void parse_input(const Json::Value& root, std::vector& bindings) { + if (!root.isMember("input") || !root["input"].isArray()) { + return; + } + + for (const Json::Value& item : root["input"]) { + if (!item.isObject()) { + continue; + } + + InputBindingConfig binding; + binding.action = item.isMember("action") + ? json_string(item["action"], "") + : json_string(item["name"], ""); + + if (item.isMember("key")) { + add_binding_code(item["key"], 0, binding.keys); + } + if (item.isMember("keys")) { + add_binding_code(item["keys"], 0, binding.keys); + } + if (item.isMember("mouse")) { + add_binding_code(item["mouse"], 1, binding.mouse_buttons); + } + if (item.isMember("mouse_buttons")) { + add_binding_code(item["mouse_buttons"], 1, binding.mouse_buttons); + } + + if (!binding.action.empty()) { + bindings.push_back(std::move(binding)); + } + } +} + +void parse_color(const Json::Value& mesh, MeshConfig& mesh_config) { + if (!mesh.isObject() || !mesh.isMember("color")) { + return; + } + + const Json::Value& color = mesh["color"]; + if (color.isObject()) { + if (color.isMember("r")) mesh_config.color[0] = json_float(color["r"], mesh_config.color[0]); + if (color.isMember("g")) mesh_config.color[1] = json_float(color["g"], mesh_config.color[1]); + if (color.isMember("b")) mesh_config.color[2] = json_float(color["b"], mesh_config.color[2]); + if (color.isMember("a")) mesh_config.color[3] = json_float(color["a"], mesh_config.color[3]); + } else if (color.isArray()) { + for (Json::ArrayIndex i = 0; i < color.size() && i < 4; ++i) { + mesh_config.color[i] = json_float(color[i], mesh_config.color[i]); + } + } +} + +ActorConfig parse_actor(const Json::Value& value) { + ActorConfig actor; + + if (value.isMember("name")) { + actor.name = json_string(value["name"], ""); + } + if (value.isMember("init")) { + actor.init_function = json_string(value["init"], ""); + } + if (value.isMember("tick")) { + actor.tick_function = json_string(value["tick"], ""); + } + + if (value.isMember("position") && value["position"].isObject()) { + const Json::Value& position = value["position"]; + actor.position[0] = json_float(position["x"], actor.position[0]); + actor.position[1] = json_float(position["y"], actor.position[1]); + } + + if (value.isMember("facing") && value["facing"].isObject()) { + const Json::Value& facing = value["facing"]; + actor.facing[0] = json_float(facing["x"], actor.facing[0]); + actor.facing[1] = json_float(facing["y"], actor.facing[1]); + } + + if (value.isMember("mesh") && value["mesh"].isObject()) { + const Json::Value& mesh = value["mesh"]; + if (mesh.isMember("shape")) { + actor.mesh.shape = json_string(mesh["shape"], actor.mesh.shape); + } + parse_color(mesh, actor.mesh); + } + + return actor; +} + +void parse_actors(const Json::Value& root, std::vector& actors) { + const Json::Value* list = nullptr; + + if (root.isMember("actors") && root["actors"].isArray()) { + list = &root["actors"]; + } else if (root.isMember("scene")) { + const Json::Value& scene = root["scene"]; + if (scene.isArray()) { + list = &scene; + } else if (scene.isObject() && scene.isMember("actors") && scene["actors"].isArray()) { + list = &scene["actors"]; + } + } + + if (list == nullptr) { + return; + } + + for (const Json::Value& item : *list) { + if (!item.isObject()) { + continue; + } + ActorConfig actor = parse_actor(item); + if (!actor.name.empty()) { + actors.push_back(std::move(actor)); + } + } +} + +} // namespace + +bool ConfigLoader::load(const std::string& path) { + Json::Value root; + Json::CharReaderBuilder builder; + std::ifstream file(path); + std::string errors; + + if (!Json::parseFromStream(builder, file, &root, &errors)) { + last_error_ = "无法解析 JSON 配置文件: " + path + " - " + errors; + return false; + } + + if (!root.isObject()) { + last_error_ = "JSON 根节点必须是对象: " + path; + return false; + } + + config_ = GameConfig{}; + parse_window(root, config_.window); + parse_input(root, config_.input_bindings); + parse_actors(root, config_.actors); + return true; +} + +const GameConfig& ConfigLoader::config() const { + return config_; +} + +const std::string& ConfigLoader::last_error() const { + return last_error_; +} + +} // namespace openceg diff --git a/src/engine.cpp b/src/engine.cpp new file mode 100644 index 0000000..a4bbfec --- /dev/null +++ b/src/engine.cpp @@ -0,0 +1,117 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace openceg { + +int run_engine(int argc, char** argv, const char* default_config_path) { + std::string config_path = default_config_path != nullptr ? default_config_path : "config.ceg"; + if (argc > 1) { + config_path = argv[1]; + } + + ConfigLoader loader; + if (!loader.load(config_path)) { + std::fprintf(stderr, "%s\n", loader.last_error().c_str()); + return 1; + } + + const GameConfig& config = loader.config(); + Game& game = Game::getInstance(); + BehaviorRegistry& behaviors = BehaviorRegistry::getInstance(); + + std::vector> actor_storage; + std::vector actors; + + for (const ActorConfig& actor_config : config.actors) { + auto mesh = std::make_shared(); + if (actor_config.mesh.shape == "quad") { + *mesh = Mesh::quad(); + } else { + // 当前渲染器只提供 quad;未知形状先按 quad 创建,便于后续扩展。 + *mesh = Mesh::quad(); + } + mesh->set_color(actor_config.mesh.color); + + auto actor = std::make_shared(actor_config.name, game, mesh); + actor->set_position(actor_config.position); + actor->set_facing(actor_config.facing); + + if (!actor_config.init_function.empty()) { + auto handler = behaviors.init(actor_config.init_function); + if (handler) { + actor->set_init_handler(std::move(handler)); + } else { + std::fprintf(stderr, "未找到 init 处理函数: %s\n", + actor_config.init_function.c_str()); + } + } + + if (!actor_config.tick_function.empty()) { + auto handler = behaviors.tick(actor_config.tick_function); + if (handler) { + actor->set_tick_handler(std::move(handler)); + } else { + std::fprintf(stderr, "未找到 tick 处理函数: %s\n", + actor_config.tick_function.c_str()); + } + } + + actors.push_back(actor.get()); + actor_storage.push_back(std::move(actor)); + } + + for (auto& actor : actor_storage) { + actor->initialize(); + } + + render::RenderLoop render_loop; + for (const InputBindingConfig& binding : config.input_bindings) { + render_loop.input().add_action(binding.action); + for (int key : binding.keys) { + render_loop.input().bind_key(binding.action, key); + } + for (int button : binding.mouse_buttons) { + render_loop.input().bind_mouse_button(binding.action, button); + } + } + + // 引擎级默认退出动作,不包含具体游戏逻辑。 + render_loop.input().add_action("quit"); + render_loop.input().bind_key("quit", 256); + + render_loop.set_actors(std::move(actors)); + render_loop.start(config.window.width, config.window.height, + config.window.title.c_str()); + + auto last = std::chrono::steady_clock::now(); + while (!render_loop.should_close()) { + render_loop.poll_events(); + if (render_loop.input().is_action_just_pressed("quit")) { + break; + } + + const auto now = std::chrono::steady_clock::now(); + const float dt = std::chrono::duration(now - last).count(); + last = now; + + for (auto& actor : actor_storage) { + actor->process(dt, render_loop.input()); + } + + render_loop.input().end_frame(); + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + + render_loop.stop(); + return 0; +} + +} // namespace openceg diff --git a/src/engine_main.cpp b/src/engine_main.cpp new file mode 100644 index 0000000..a0a5f2c --- /dev/null +++ b/src/engine_main.cpp @@ -0,0 +1,5 @@ +#include + +int main(int argc, char** argv) { + return openceg::run_engine(argc, argv); +} diff --git a/src/game.cpp b/src/game.cpp new file mode 100644 index 0000000..3e9414b --- /dev/null +++ b/src/game.cpp @@ -0,0 +1,17 @@ +#include + +namespace openceg { + +Game::Game() { id_cnt = 0; } +Game::~Game() = default; + +Game& Game::getInstance() { + static Game instance; + return instance; +} + +void Game::launch() {} + +size_t Game::new_id() { return id_cnt++; } + +} diff --git a/src/glfw_win32_stub.cpp b/src/glfw_win32_stub.cpp new file mode 100644 index 0000000..73720a5 --- /dev/null +++ b/src/glfw_win32_stub.cpp @@ -0,0 +1,415 @@ +// Minimal GLFW-compatible Win32 backend for cross-compiling the example. +// It implements only the subset used by src/render.cpp. +#include +#include + +#include +#include +#include + +#define GLFW_INCLUDE_VULKAN +#define VK_USE_PLATFORM_WIN32_KHR +#include +#include +#include + +struct GLFWwindow { + HWND handle = nullptr; + void* user_pointer = nullptr; + bool should_close = false; + int width = 0; + int height = 0; + + GLFWkeyfun key_callback = nullptr; + GLFWmousebuttonfun mouse_button_callback = nullptr; + GLFWcursorposfun cursor_pos_callback = nullptr; + GLFWscrollfun scroll_callback = nullptr; + + std::array key_state{}; + std::array mouse_button_state{}; + double cursor_x = 0.0; + double cursor_y = 0.0; +}; + +namespace { + +constexpr wchar_t kWindowClassName[] = L"OpenCGE_GLFWStub"; +HINSTANCE g_instance = nullptr; +bool g_initialized = false; +bool g_resizable = true; + +int map_key(WPARAM vk) { + if (vk >= 'A' && vk <= 'Z') { + return GLFW_KEY_A + static_cast(vk - 'A'); + } + if (vk >= '0' && vk <= '9') { + return GLFW_KEY_0 + static_cast(vk - '0'); + } + switch (vk) { + case VK_ESCAPE: + return GLFW_KEY_ESCAPE; + case VK_LEFT: + return GLFW_KEY_LEFT; + case VK_RIGHT: + return GLFW_KEY_RIGHT; + case VK_UP: + return GLFW_KEY_UP; + case VK_DOWN: + return GLFW_KEY_DOWN; + case VK_SPACE: + return GLFW_KEY_SPACE; + case VK_RETURN: + return GLFW_KEY_ENTER; + case VK_TAB: + return GLFW_KEY_TAB; + case VK_BACK: + return GLFW_KEY_BACKSPACE; + case VK_DELETE: + return GLFW_KEY_DELETE; + case VK_LSHIFT: + return GLFW_KEY_LEFT_SHIFT; + case VK_RSHIFT: + return GLFW_KEY_RIGHT_SHIFT; + case VK_LCONTROL: + return GLFW_KEY_LEFT_CONTROL; + case VK_RCONTROL: + return GLFW_KEY_RIGHT_CONTROL; + case VK_LMENU: + return GLFW_KEY_LEFT_ALT; + case VK_RMENU: + return GLFW_KEY_RIGHT_ALT; + default: + break; + } + return GLFW_KEY_UNKNOWN; +} + +int current_mods() { + int mods = 0; + if (GetKeyState(VK_SHIFT) & 0x8000) { + mods |= GLFW_MOD_SHIFT; + } + if (GetKeyState(VK_CONTROL) & 0x8000) { + mods |= GLFW_MOD_CONTROL; + } + if (GetKeyState(VK_MENU) & 0x8000) { + mods |= GLFW_MOD_ALT; + } + if ((GetKeyState(VK_LWIN) & 0x8000) || (GetKeyState(VK_RWIN) & 0x8000)) { + mods |= GLFW_MOD_SUPER; + } + return mods; +} + +void handle_key(GLFWwindow* window, UINT message, WPARAM wparam, LPARAM lparam) { + if (window == nullptr || window->key_callback == nullptr) { + return; + } + + const int key = map_key(wparam); + const int scancode = static_cast((lparam >> 16) & 0x1ff); + const int action = message == WM_KEYDOWN || message == WM_SYSKEYDOWN + ? (((lparam & (1U << 30)) != 0) ? GLFW_REPEAT : GLFW_PRESS) + : GLFW_RELEASE; + + if (key >= 0 && key < static_cast(window->key_state.size())) { + window->key_state[key] = action == GLFW_RELEASE ? GLFW_RELEASE : GLFW_PRESS; + } + + window->key_callback(window, key, scancode, action, current_mods()); +} + +int map_mouse_button(UINT message, WPARAM wparam) { + switch (message) { + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + return GLFW_MOUSE_BUTTON_LEFT; + case WM_RBUTTONDOWN: + case WM_RBUTTONUP: + return GLFW_MOUSE_BUTTON_RIGHT; + case WM_MBUTTONDOWN: + case WM_MBUTTONUP: + return GLFW_MOUSE_BUTTON_MIDDLE; + case WM_XBUTTONDOWN: + case WM_XBUTTONUP: + return GET_XBUTTON_WPARAM(wparam) == XBUTTON1 ? GLFW_MOUSE_BUTTON_4 + : GLFW_MOUSE_BUTTON_5; + default: + return -1; + } +} + +void handle_mouse_button(GLFWwindow* window, UINT message, WPARAM wparam) { + if (window == nullptr || window->mouse_button_callback == nullptr) { + return; + } + + const int button = map_mouse_button(message, wparam); + if (button < 0) { + return; + } + + const bool is_down = message == WM_LBUTTONDOWN || message == WM_RBUTTONDOWN || + message == WM_MBUTTONDOWN || message == WM_XBUTTONDOWN; + window->mouse_button_state[button] = is_down ? GLFW_PRESS : GLFW_RELEASE; + window->mouse_button_callback(window, button, is_down ? GLFW_PRESS : GLFW_RELEASE, + current_mods()); +} + +LRESULT CALLBACK window_proc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + auto* window = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + + switch (message) { + case WM_CLOSE: + if (window != nullptr) { + window->should_close = true; + } + return 0; + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + case WM_KEYUP: + case WM_SYSKEYUP: + handle_key(window, message, wparam, lparam); + return 0; + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + case WM_RBUTTONDOWN: + case WM_RBUTTONUP: + case WM_MBUTTONDOWN: + case WM_MBUTTONUP: + case WM_XBUTTONDOWN: + case WM_XBUTTONUP: + handle_mouse_button(window, message, wparam); + return 0; + case WM_MOUSEMOVE: + if (window != nullptr) { + window->cursor_x = static_cast(GET_X_LPARAM(lparam)); + window->cursor_y = static_cast(GET_Y_LPARAM(lparam)); + if (window->cursor_pos_callback != nullptr) { + window->cursor_pos_callback(window, window->cursor_x, window->cursor_y); + } + } + return 0; + case WM_MOUSEWHEEL: + if (window != nullptr && window->scroll_callback != nullptr) { + const double delta = static_cast(GET_WHEEL_DELTA_WPARAM(wparam)) / + static_cast(WHEEL_DELTA); + window->scroll_callback(window, 0.0, delta); + } + return 0; + default: + break; + } + + return DefWindowProcW(hwnd, message, wparam, lparam); +} + +} // namespace + +extern "C" { + +int glfwInit(void) { + if (g_initialized) { + return GLFW_TRUE; + } + + g_instance = GetModuleHandleW(nullptr); + + WNDCLASSEXW wc{}; + wc.cbSize = sizeof(wc); + wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; + wc.lpfnWndProc = window_proc; + wc.hInstance = g_instance; + wc.hCursor = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512)); + wc.lpszClassName = kWindowClassName; + if (RegisterClassExW(&wc) == 0) { + return GLFW_FALSE; + } + + g_initialized = true; + return GLFW_TRUE; +} + +void glfwTerminate(void) { + g_initialized = false; +} + +void glfwWindowHint(int hint, int value) { + if (hint == GLFW_RESIZABLE) { + g_resizable = value != GLFW_FALSE; + } +} + +GLFWwindow* glfwCreateWindow(int width, int height, const char* title, GLFWmonitor*, GLFWwindow*) { + if (!g_initialized) { + return nullptr; + } + + const int wide_length = MultiByteToWideChar(CP_UTF8, 0, title, -1, nullptr, 0); + std::vector wide_title(wide_length); + MultiByteToWideChar(CP_UTF8, 0, title, -1, wide_title.data(), wide_length); + + DWORD style = WS_OVERLAPPEDWINDOW; + if (!g_resizable) { + style &= ~(WS_THICKFRAME | WS_MAXIMIZEBOX); + } + + RECT rect{0, 0, width, height}; + AdjustWindowRectEx(&rect, style, FALSE, 0); + + HWND hwnd = CreateWindowExW( + 0, kWindowClassName, wide_title.data(), style, CW_USEDEFAULT, CW_USEDEFAULT, + rect.right - rect.left, rect.bottom - rect.top, nullptr, nullptr, g_instance, nullptr); + if (hwnd == nullptr) { + return nullptr; + } + + auto* window = new GLFWwindow(); + window->handle = hwnd; + window->width = width; + window->height = height; + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(window)); + + ShowWindow(hwnd, SW_SHOW); + UpdateWindow(hwnd); + return window; +} + +void glfwDestroyWindow(GLFWwindow* window) { + if (window == nullptr) { + return; + } + if (window->handle != nullptr) { + DestroyWindow(window->handle); + } + delete window; +} + +void glfwSetWindowUserPointer(GLFWwindow* window, void* pointer) { + if (window != nullptr) { + window->user_pointer = pointer; + } +} + +void* glfwGetWindowUserPointer(GLFWwindow* window) { + return window != nullptr ? window->user_pointer : nullptr; +} + +GLFWkeyfun glfwSetKeyCallback(GLFWwindow* window, GLFWkeyfun callback) { + if (window == nullptr) { + return nullptr; + } + GLFWkeyfun previous = window->key_callback; + window->key_callback = callback; + return previous; +} + +GLFWmousebuttonfun glfwSetMouseButtonCallback(GLFWwindow* window, GLFWmousebuttonfun callback) { + if (window == nullptr) { + return nullptr; + } + GLFWmousebuttonfun previous = window->mouse_button_callback; + window->mouse_button_callback = callback; + return previous; +} + +GLFWcursorposfun glfwSetCursorPosCallback(GLFWwindow* window, GLFWcursorposfun callback) { + if (window == nullptr) { + return nullptr; + } + GLFWcursorposfun previous = window->cursor_pos_callback; + window->cursor_pos_callback = callback; + return previous; +} + +GLFWscrollfun glfwSetScrollCallback(GLFWwindow* window, GLFWscrollfun callback) { + if (window == nullptr) { + return nullptr; + } + GLFWscrollfun previous = window->scroll_callback; + window->scroll_callback = callback; + return previous; +} + +void glfwPollEvents(void) { + MSG msg{}; + while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } +} + +int glfwWindowShouldClose(GLFWwindow* window) { + return window != nullptr && window->should_close ? GLFW_TRUE : GLFW_FALSE; +} + +int glfwGetKey(GLFWwindow* window, int key) { + if (window == nullptr || key < 0 || key >= static_cast(window->key_state.size())) { + return GLFW_RELEASE; + } + return window->key_state[key] != 0 ? GLFW_PRESS : GLFW_RELEASE; +} + +int glfwGetMouseButton(GLFWwindow* window, int button) { + if (window == nullptr || button < 0 || + button >= static_cast(window->mouse_button_state.size())) { + return GLFW_RELEASE; + } + return window->mouse_button_state[button] != 0 ? GLFW_PRESS : GLFW_RELEASE; +} + +void glfwGetCursorPos(GLFWwindow* window, double* xpos, double* ypos) { + if (xpos != nullptr) { + *xpos = window != nullptr ? window->cursor_x : 0.0; + } + if (ypos != nullptr) { + *ypos = window != nullptr ? window->cursor_y : 0.0; + } +} + +void glfwGetFramebufferSize(GLFWwindow* window, int* width, int* height) { + if (window == nullptr || window->handle == nullptr) { + return; + } + RECT client{}; + GetClientRect(window->handle, &client); + if (width != nullptr) { + *width = client.right - client.left; + } + if (height != nullptr) { + *height = client.bottom - client.top; + } +} + +const char** glfwGetRequiredInstanceExtensions(uint32_t* count) { + static const char* extensions[] = { + VK_KHR_SURFACE_EXTENSION_NAME, + VK_KHR_WIN32_SURFACE_EXTENSION_NAME, + }; + if (count != nullptr) { + *count = static_cast(sizeof(extensions) / sizeof(extensions[0])); + } + return extensions; +} + +VkResult glfwCreateWindowSurface(VkInstance instance, GLFWwindow* window, + const VkAllocationCallbacks* allocator, + VkSurfaceKHR* surface) { + if (instance == VK_NULL_HANDLE || window == nullptr || surface == nullptr) { + return VK_ERROR_INITIALIZATION_FAILED; + } + + auto create_surface = reinterpret_cast( + vkGetInstanceProcAddr(instance, "vkCreateWin32SurfaceKHR")); + if (create_surface == nullptr) { + return VK_ERROR_EXTENSION_NOT_PRESENT; + } + + VkWin32SurfaceCreateInfoKHR info{}; + info.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; + info.hinstance = g_instance; + info.hwnd = window->handle; + return create_surface(instance, &info, allocator, surface); +} + +} // extern "C" diff --git a/src/opencge.cpp b/src/opencge.cpp new file mode 100644 index 0000000..df2e194 --- /dev/null +++ b/src/opencge.cpp @@ -0,0 +1 @@ +#include diff --git a/src/render.cpp b/src/render.cpp new file mode 100644 index 0000000..bdae664 --- /dev/null +++ b/src/render.cpp @@ -0,0 +1,1340 @@ +#include + +#define GLFW_INCLUDE_VULKAN +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace openceg::render { +namespace { + + +constexpr std::array kQuadVertShader = { + 0x07230203,0x00010000,0x000d000b,0x0000002e,0x00000000,0x00020011,0x00000001,0x0006000b,0x00000001,0x4c534c47,0x6474732e,0x3035342e, + 0x00000000,0x0003000e,0x00000000,0x00000001,0x0009000f,0x00000000,0x00000004,0x6e69616d,0x00000000,0x0000000d,0x00000019,0x00000023, + 0x00000025,0x00030003,0x00000002,0x000001c2,0x000a0004,0x475f4c47,0x4c474f4f,0x70635f45,0x74735f70,0x5f656c79,0x656e696c,0x7269645f, + 0x69746365,0x00006576,0x00080004,0x475f4c47,0x4c474f4f,0x6e695f45,0x64756c63,0x69645f65,0x74636572,0x00657669,0x00040005,0x00000004, + 0x6e69616d,0x00000000,0x00060005,0x0000000b,0x505f6c67,0x65567265,0x78657472,0x00000000,0x00060006,0x0000000b,0x00000000,0x505f6c67, + 0x7469736f,0x006e6f69,0x00070006,0x0000000b,0x00000001,0x505f6c67,0x746e696f,0x657a6953,0x00000000,0x00070006,0x0000000b,0x00000002, + 0x435f6c67,0x4470696c,0x61747369,0x0065636e,0x00070006,0x0000000b,0x00000003,0x435f6c67,0x446c6c75,0x61747369,0x0065636e,0x00030005, + 0x0000000d,0x00000000,0x00030005,0x00000011,0x004f4255,0x00050006,0x00000011,0x00000000,0x65646f6d,0x0000006c,0x00030005,0x00000013, + 0x006f6275,0x00040005,0x00000019,0x736f5061,0x00000000,0x00040005,0x00000023,0x6c6f4376,0x0000726f,0x00040005,0x00000025,0x6c6f4361, + 0x0000726f,0x00030005,0x00000027,0x00004350,0x00050006,0x00000027,0x00000000,0x6f6c6f63,0x00000072,0x00030005,0x00000029,0x00006370, + 0x00030047,0x0000000b,0x00000002,0x00050048,0x0000000b,0x00000000,0x0000000b,0x00000000,0x00050048,0x0000000b,0x00000001,0x0000000b, + 0x00000001,0x00050048,0x0000000b,0x00000002,0x0000000b,0x00000003,0x00050048,0x0000000b,0x00000003,0x0000000b,0x00000004,0x00030047, + 0x00000011,0x00000002,0x00040048,0x00000011,0x00000000,0x00000005,0x00050048,0x00000011,0x00000000,0x00000007,0x00000010,0x00050048, + 0x00000011,0x00000000,0x00000023,0x00000000,0x00040047,0x00000013,0x00000021,0x00000000,0x00040047,0x00000013,0x00000022,0x00000000, + 0x00040047,0x00000019,0x0000001e,0x00000000,0x00040047,0x00000023,0x0000001e,0x00000000,0x00040047,0x00000025,0x0000001e,0x00000001, + 0x00030047,0x00000027,0x00000002,0x00050048,0x00000027,0x00000000,0x00000023,0x00000000,0x00020013,0x00000002,0x00030021,0x00000003, + 0x00000002,0x00030016,0x00000006,0x00000020,0x00040017,0x00000007,0x00000006,0x00000004,0x00040015,0x00000008,0x00000020,0x00000000, + 0x0004002b,0x00000008,0x00000009,0x00000001,0x0004001c,0x0000000a,0x00000006,0x00000009,0x0006001e,0x0000000b,0x00000007,0x00000006, + 0x0000000a,0x0000000a,0x00040020,0x0000000c,0x00000003,0x0000000b,0x0004003b,0x0000000c,0x0000000d,0x00000003,0x00040015,0x0000000e, + 0x00000020,0x00000001,0x0004002b,0x0000000e,0x0000000f,0x00000000,0x00040018,0x00000010,0x00000007,0x00000004,0x0003001e,0x00000011, + 0x00000010,0x00040020,0x00000012,0x00000002,0x00000011,0x0004003b,0x00000012,0x00000013,0x00000002,0x00040020,0x00000014,0x00000002, + 0x00000010,0x00040017,0x00000017,0x00000006,0x00000002,0x00040020,0x00000018,0x00000001,0x00000017,0x0004003b,0x00000018,0x00000019, + 0x00000001,0x0004002b,0x00000006,0x0000001b,0x00000000,0x0004002b,0x00000006,0x0000001c,0x3f800000,0x00040020,0x00000021,0x00000003, + 0x00000007,0x0004003b,0x00000021,0x00000023,0x00000003,0x00040020,0x00000024,0x00000001,0x00000007,0x0004003b,0x00000024,0x00000025, + 0x00000001,0x0003001e,0x00000027,0x00000007,0x00040020,0x00000028,0x00000009,0x00000027,0x0004003b,0x00000028,0x00000029,0x00000009, + 0x00040020,0x0000002a,0x00000009,0x00000007,0x00050036,0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,0x00050041, + 0x00000014,0x00000015,0x00000013,0x0000000f,0x0004003d,0x00000010,0x00000016,0x00000015,0x0004003d,0x00000017,0x0000001a,0x00000019, + 0x00050051,0x00000006,0x0000001d,0x0000001a,0x00000000,0x00050051,0x00000006,0x0000001e,0x0000001a,0x00000001,0x00070050,0x00000007, + 0x0000001f,0x0000001d,0x0000001e,0x0000001b,0x0000001c,0x00050091,0x00000007,0x00000020,0x00000016,0x0000001f,0x00050041,0x00000021, + 0x00000022,0x0000000d,0x0000000f,0x0003003e,0x00000022,0x00000020,0x0004003d,0x00000007,0x00000026,0x00000025,0x00050041,0x0000002a, + 0x0000002b,0x00000029,0x0000000f,0x0004003d,0x00000007,0x0000002c,0x0000002b,0x00050085,0x00000007,0x0000002d,0x00000026,0x0000002c, + 0x0003003e,0x00000023,0x0000002d,0x000100fd,0x00010038, +}; + +constexpr std::array kQuadFragShader = { + 0x07230203,0x00010000,0x000d000b,0x0000000d,0x00000000,0x00020011,0x00000001,0x0006000b,0x00000001,0x4c534c47,0x6474732e,0x3035342e, + 0x00000000,0x0003000e,0x00000000,0x00000001,0x0007000f,0x00000004,0x00000004,0x6e69616d,0x00000000,0x00000009,0x0000000b,0x00030010, + 0x00000004,0x00000007,0x00030003,0x00000002,0x000001c2,0x000a0004,0x475f4c47,0x4c474f4f,0x70635f45,0x74735f70,0x5f656c79,0x656e696c, + 0x7269645f,0x69746365,0x00006576,0x00080004,0x475f4c47,0x4c474f4f,0x6e695f45,0x64756c63,0x69645f65,0x74636572,0x00657669,0x00040005, + 0x00000004,0x6e69616d,0x00000000,0x00050005,0x00000009,0x4374756f,0x726f6c6f,0x00000000,0x00040005,0x0000000b,0x6c6f4376,0x0000726f, + 0x00040047,0x00000009,0x0000001e,0x00000000,0x00040047,0x0000000b,0x0000001e,0x00000000,0x00020013,0x00000002,0x00030021,0x00000003, + 0x00000002,0x00030016,0x00000006,0x00000020,0x00040017,0x00000007,0x00000006,0x00000004,0x00040020,0x00000008,0x00000003,0x00000007, + 0x0004003b,0x00000008,0x00000009,0x00000003,0x00040020,0x0000000a,0x00000001,0x00000007,0x0004003b,0x0000000a,0x0000000b,0x00000001, + 0x00050036,0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,0x0004003d,0x00000007,0x0000000c,0x0000000b,0x0003003e, + 0x00000009,0x0000000c,0x000100fd,0x00010038, +}; + +struct FrameData { + VkFence in_flight = VK_NULL_HANDLE; + VkSemaphore image_available = VK_NULL_HANDLE; + VkSemaphore render_finished = VK_NULL_HANDLE; + VkCommandBuffer command_buffer = VK_NULL_HANDLE; +}; + +void check(VkResult result, const char* what) { + if (result != VK_SUCCESS) { + throw std::runtime_error(what); + } +} + +#ifdef VK_EXT_debug_utils +VKAPI_ATTR VkBool32 VKAPI_CALL debug_callback( + VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMessageTypeFlagsEXT type, + const VkDebugUtilsMessengerCallbackDataEXT* callback_data, void*) { + if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) { + std::fprintf(stderr, "VK ERROR: %s\n", callback_data->pMessage); + } + return VK_FALSE; +} +#endif + +uint32_t find_memory_type(VkPhysicalDevice physical_device, uint32_t type_filter, + VkMemoryPropertyFlags properties) { + VkPhysicalDeviceMemoryProperties memory_properties{}; + vkGetPhysicalDeviceMemoryProperties(physical_device, &memory_properties); + for (uint32_t i = 0; i < memory_properties.memoryTypeCount; ++i) { + if ((type_filter & (1u << i)) && + (memory_properties.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } + } + throw std::runtime_error("failed to find suitable Vulkan memory type"); +} + +void create_buffer(VkPhysicalDevice physical_device, VkDevice device, VkDeviceSize size, + VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer, + VkDeviceMemory& memory) { + VkBufferCreateInfo buffer_info{}; + buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + buffer_info.size = size; + buffer_info.usage = usage; + buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + check(vkCreateBuffer(device, &buffer_info, nullptr, &buffer), "vkCreateBuffer"); + + VkMemoryRequirements requirements{}; + vkGetBufferMemoryRequirements(device, buffer, &requirements); + + VkMemoryAllocateInfo alloc_info{}; + alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + alloc_info.allocationSize = requirements.size; + alloc_info.memoryTypeIndex = + find_memory_type(physical_device, requirements.memoryTypeBits, properties); + check(vkAllocateMemory(device, &alloc_info, nullptr, &memory), "vkAllocateMemory"); + check(vkBindBufferMemory(device, buffer, memory, 0), "vkBindBufferMemory"); +} + +VkShaderModule create_shader_module(VkDevice device, const uint32_t* code, size_t size) { + VkShaderModuleCreateInfo create_info{}; + create_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + create_info.codeSize = size; + create_info.pCode = code; + VkShaderModule module = VK_NULL_HANDLE; + check(vkCreateShaderModule(device, &create_info, nullptr, &module), "vkCreateShaderModule"); + return module; +} + +} // namespace + +struct Renderer::Impl { + GLFWwindow* window = nullptr; + VkInstance instance = VK_NULL_HANDLE; + VkDebugUtilsMessengerEXT debug_messenger = VK_NULL_HANDLE; + VkSurfaceKHR surface = VK_NULL_HANDLE; + VkPhysicalDevice physical_device = VK_NULL_HANDLE; + VkDevice device = VK_NULL_HANDLE; + VkQueue graphics_queue = VK_NULL_HANDLE; + VkQueue present_queue = VK_NULL_HANDLE; + uint32_t graphics_family = 0; + uint32_t present_family = 0; + + VkSwapchainKHR swapchain = VK_NULL_HANDLE; + VkFormat swapchain_format = VK_FORMAT_UNDEFINED; + VkExtent2D swapchain_extent{}; + std::vector swapchain_images; + std::vector swapchain_image_views; + std::vector framebuffers; + + VkRenderPass render_pass = VK_NULL_HANDLE; + VkDescriptorSetLayout descriptor_set_layout = VK_NULL_HANDLE; + VkDescriptorPool descriptor_pool = VK_NULL_HANDLE; + VkDescriptorSet descriptor_set = VK_NULL_HANDLE; + VkPipelineLayout pipeline_layout = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; + + VkCommandPool command_pool = VK_NULL_HANDLE; + std::vector frames; + std::vector images_in_flight; + + VkBuffer vertex_buffer = VK_NULL_HANDLE; + VkDeviceMemory vertex_buffer_memory = VK_NULL_HANDLE; + VkBuffer index_buffer = VK_NULL_HANDLE; + VkDeviceMemory index_buffer_memory = VK_NULL_HANDLE; + uint32_t cached_index_count = 0; + uint32_t cached_vertex_count = 0; + const Mesh* cached_mesh = nullptr; + bool mesh_uploaded = false; + VkBuffer uniform_buffer = VK_NULL_HANDLE; + VkDeviceMemory uniform_buffer_memory = VK_NULL_HANDLE; + VkDeviceSize uniform_buffer_size = sizeof(float) * 16; + void* uniform_mapped = nullptr; + + size_t current_frame = 0; + bool initialized = false; + Renderer::InputCallback input_callback; + + void init(int width, int height, const char* title); + void draw(const Actor* const* actors, size_t actor_count); + void upload_mesh(const Mesh& mesh); + void destroy(); +}; + +Renderer::Renderer() : impl_(new Impl()) {} +Renderer::~Renderer() { + destroy(); + delete impl_; +} + +void Renderer::init(int width, int height, const char* title) { + impl_->init(width, height, title); +} + +void Renderer::draw(const Actor* const* actors, size_t actor_count) { + impl_->draw(actors, actor_count); +} + +bool Renderer::should_close() const { + return impl_->window != nullptr && glfwWindowShouldClose(impl_->window); +} + +void Renderer::poll_events() const { + glfwPollEvents(); +} + +void Renderer::set_input_callback(InputCallback callback) { + impl_->input_callback = std::move(callback); +} + +bool Renderer::is_key_down(int key) const { + return impl_->window != nullptr && glfwGetKey(impl_->window, key) == GLFW_PRESS; +} + +bool Renderer::is_mouse_button_down(int button) const { + return impl_->window != nullptr && glfwGetMouseButton(impl_->window, button) == GLFW_PRESS; +} + +std::array Renderer::cursor_position() const { + if (impl_->window == nullptr) { + return {0.0, 0.0}; + } + double x = 0.0; + double y = 0.0; + glfwGetCursorPos(impl_->window, &x, &y); + return {x, y}; +} + +void Renderer::destroy() { + impl_->destroy(); +} + +void InputMap::add_action(const std::string& action) { + std::lock_guard lock(mutex_); + actions_.try_emplace(action); +} + +void InputMap::bind_key(const std::string& action, int key) { + std::lock_guard lock(mutex_); + ActionState& state = actions_[action]; + state.keys.insert(key); + state.held_keys.try_emplace(key, false); +} + +void InputMap::unbind_key(const std::string& action, int key) { + std::lock_guard lock(mutex_); + auto action_it = actions_.find(action); + if (action_it == actions_.end()) { + return; + } + ActionState& state = action_it->second; + state.keys.erase(key); + auto held_it = state.held_keys.find(key); + if (held_it != state.held_keys.end() && held_it->second) { + held_it->second = false; + if (state.held_key_count > 0) { + --state.held_key_count; + } + if (state.held_key_count + state.held_button_count == 0) { + state.pressed = false; + state.just_released = true; + } + } + state.held_keys.erase(key); +} + +void InputMap::bind_mouse_button(const std::string& action, int button) { + std::lock_guard lock(mutex_); + ActionState& state = actions_[action]; + state.mouse_buttons.insert(button); + state.held_buttons.try_emplace(button, false); +} + +void InputMap::unbind_mouse_button(const std::string& action, int button) { + std::lock_guard lock(mutex_); + auto action_it = actions_.find(action); + if (action_it == actions_.end()) { + return; + } + ActionState& state = action_it->second; + state.mouse_buttons.erase(button); + auto held_it = state.held_buttons.find(button); + if (held_it != state.held_buttons.end() && held_it->second) { + held_it->second = false; + if (state.held_button_count > 0) { + --state.held_button_count; + } + if (state.held_key_count + state.held_button_count == 0) { + state.pressed = false; + state.just_released = true; + } + } + state.held_buttons.erase(button); +} + +void InputMap::handle_event(const InputEvent& event) { + std::lock_guard lock(mutex_); + + for (auto& [action, state] : actions_) { + std::unordered_set* bindings = nullptr; + std::unordered_map* held_map = nullptr; + size_t* held_count = nullptr; + + if (event.type == InputEvent::Type::Key) { + bindings = &state.keys; + held_map = &state.held_keys; + held_count = &state.held_key_count; + } else if (event.type == InputEvent::Type::MouseButton) { + bindings = &state.mouse_buttons; + held_map = &state.held_buttons; + held_count = &state.held_button_count; + } else { + continue; + } + + const int input_id = event.type == InputEvent::Type::Key ? event.key : event.button; + if (bindings->find(input_id) == bindings->end()) { + continue; + } + + if (event.action == InputEvent::Action::Press) { + bool& held = (*held_map)[input_id]; + if (!held) { + held = true; + ++(*held_count); + if (state.held_key_count + state.held_button_count == 1) { + state.pressed = true; + state.just_pressed = true; + } + } + } else if (event.action == InputEvent::Action::Release) { + auto held_it = held_map->find(input_id); + if (held_it == held_map->end() || !held_it->second) { + continue; + } + held_it->second = false; + if (*held_count > 0) { + --(*held_count); + } + if (state.held_key_count + state.held_button_count == 0) { + state.pressed = false; + state.just_released = true; + } + } + } +} + +void InputMap::end_frame() { + std::lock_guard lock(mutex_); + for (auto& [action, state] : actions_) { + state.just_pressed = false; + state.just_released = false; + } +} + +bool InputMap::is_action_pressed(const std::string& action) const { + std::lock_guard lock(mutex_); + auto it = actions_.find(action); + return it != actions_.end() && it->second.pressed; +} + +bool InputMap::is_action_just_pressed(const std::string& action) const { + std::lock_guard lock(mutex_); + auto it = actions_.find(action); + return it != actions_.end() && it->second.just_pressed; +} + +bool InputMap::is_action_just_released(const std::string& action) const { + std::lock_guard lock(mutex_); + auto it = actions_.find(action); + return it != actions_.end() && it->second.just_released; +} + +float InputMap::get_action_strength(const std::string& action) const { + std::lock_guard lock(mutex_); + auto it = actions_.find(action); + return it != actions_.end() && it->second.pressed ? 1.0F : 0.0F; +} + +float InputMap::get_axis(const std::string& negative_action, + const std::string& positive_action) const { + return get_action_strength(positive_action) - get_action_strength(negative_action); +} + +RenderLoop::~RenderLoop() { + stop(); +} + +void RenderLoop::start(int width, int height, const char* title) { + if (running_) { + return; + } + + renderer_.init(width, height, title); + renderer_.set_input_callback([this](const InputEvent& event) { + input_map_.handle_event(event); + }); + + running_ = true; + thread_ = std::thread(&RenderLoop::run, this); +} + +void RenderLoop::stop() { + if (!running_) { + return; + } + running_ = false; + if (thread_.joinable()) { + thread_.join(); + } + renderer_.destroy(); +} + +void RenderLoop::set_actors(std::vector actors) { + std::lock_guard lock(actors_mutex_); + actors_ = std::move(actors); +} + +void RenderLoop::poll_events() const { + renderer_.poll_events(); +} + +bool RenderLoop::should_close() const { + return renderer_.should_close(); +} + +InputMap& RenderLoop::input() { + return input_map_; +} + +const InputMap& RenderLoop::input() const { + return input_map_; +} + +void RenderLoop::run() { + while (running_) { + std::vector actors; + { + std::lock_guard lock(actors_mutex_); + actors = actors_; + } + + if (!actors.empty()) { + renderer_.draw(actors.data(), actors.size()); + } else { + std::this_thread::yield(); + } + } +} + +void Renderer::Impl::init(int width, int height, const char* title) { + if (initialized) { + return; + } + + if (!glfwInit()) { + throw std::runtime_error("failed to initialize GLFW"); + } + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); + window = glfwCreateWindow(width, height, title, nullptr, nullptr); + if (window == nullptr) { + throw std::runtime_error("failed to create GLFW window"); + } + + glfwSetWindowUserPointer(window, this); + glfwSetKeyCallback(window, [](GLFWwindow* target, int key, int scancode, int action, + int mods) { + auto* impl = static_cast(glfwGetWindowUserPointer(target)); + if (impl == nullptr || !impl->input_callback) { + return; + } + InputEvent event; + event.type = InputEvent::Type::Key; + event.action = static_cast(action); + event.key = key; + event.scancode = scancode; + event.mods = mods; + impl->input_callback(event); + }); + glfwSetMouseButtonCallback(window, [](GLFWwindow* target, int button, int action, + int mods) { + auto* impl = static_cast(glfwGetWindowUserPointer(target)); + if (impl == nullptr || !impl->input_callback) { + return; + } + InputEvent event; + event.type = InputEvent::Type::MouseButton; + event.action = static_cast(action); + event.button = button; + event.mods = mods; + impl->input_callback(event); + }); + glfwSetCursorPosCallback(window, [](GLFWwindow* target, double x, double y) { + auto* impl = static_cast(glfwGetWindowUserPointer(target)); + if (impl == nullptr || !impl->input_callback) { + return; + } + InputEvent event; + event.type = InputEvent::Type::CursorMove; + event.x = x; + event.y = y; + impl->input_callback(event); + }); + glfwSetScrollCallback(window, [](GLFWwindow* target, double x, double y) { + auto* impl = static_cast(glfwGetWindowUserPointer(target)); + if (impl == nullptr || !impl->input_callback) { + return; + } + InputEvent event; + event.type = InputEvent::Type::Scroll; + event.x = x; + event.y = y; + impl->input_callback(event); + }); + + // 让窗口管理器完成首次表面配置,避免交换链尺寸在首帧后才变化。 + glfwPollEvents(); + + uint32_t glfw_extension_count = 0; + const char** glfw_extensions = glfwGetRequiredInstanceExtensions(&glfw_extension_count); + std::vector instance_extensions(glfw_extensions, + glfw_extensions + glfw_extension_count); + instance_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + + const char* validation_layer = "VK_LAYER_KHRONOS_validation"; + bool use_validation = false; + uint32_t layer_count = 0; + vkEnumerateInstanceLayerProperties(&layer_count, nullptr); + std::vector available_layers(layer_count); + vkEnumerateInstanceLayerProperties(&layer_count, available_layers.data()); + for (const auto& layer : available_layers) { + if (std::strcmp(layer.layerName, validation_layer) == 0) { + use_validation = true; + break; + } + } + + VkApplicationInfo app_info{}; + app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + app_info.pApplicationName = title; + app_info.applicationVersion = VK_MAKE_VERSION(1, 0, 0); + app_info.pEngineName = "OpenCGE"; + app_info.engineVersion = VK_MAKE_VERSION(1, 0, 0); + app_info.apiVersion = VK_API_VERSION_1_0; + + VkInstanceCreateInfo instance_info{}; + instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + instance_info.pApplicationInfo = &app_info; + instance_info.enabledExtensionCount = static_cast(instance_extensions.size()); + instance_info.ppEnabledExtensionNames = instance_extensions.data(); + if (use_validation) { + instance_info.enabledLayerCount = 1; + instance_info.ppEnabledLayerNames = &validation_layer; + } + check(vkCreateInstance(&instance_info, nullptr, &instance), "vkCreateInstance"); + + if (use_validation) { + auto create_messenger = reinterpret_cast( + vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT")); + if (create_messenger != nullptr) { + VkDebugUtilsMessengerCreateInfoEXT messenger_info{}; + messenger_info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; + messenger_info.messageSeverity = + VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + messenger_info.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; + messenger_info.pfnUserCallback = debug_callback; + create_messenger(instance, &messenger_info, nullptr, &debug_messenger); + } + } + + check(glfwCreateWindowSurface(instance, window, nullptr, &surface), "glfwCreateWindowSurface"); + + uint32_t physical_device_count = 0; + check(vkEnumeratePhysicalDevices(instance, &physical_device_count, nullptr), + "vkEnumeratePhysicalDevices"); + if (physical_device_count == 0) { + throw std::runtime_error("no Vulkan physical device"); + } + std::vector physical_devices(physical_device_count); + check(vkEnumeratePhysicalDevices(instance, &physical_device_count, physical_devices.data()), + "vkEnumeratePhysicalDevices"); + + int best_score = -1; + for (VkPhysicalDevice candidate : physical_devices) { + uint32_t family_count = 0; + vkGetPhysicalDeviceQueueFamilyProperties(candidate, &family_count, nullptr); + std::vector families(family_count); + vkGetPhysicalDeviceQueueFamilyProperties(candidate, &family_count, families.data()); + + int graphics_index = -1; + int present_index = -1; + bool swapchain_supported = false; + + uint32_t extension_count = 0; + vkEnumerateDeviceExtensionProperties(candidate, nullptr, &extension_count, nullptr); + std::vector extensions(extension_count); + vkEnumerateDeviceExtensionProperties(candidate, nullptr, &extension_count, + extensions.data()); + for (const auto& extension : extensions) { + if (std::strcmp(extension.extensionName, VK_KHR_SWAPCHAIN_EXTENSION_NAME) == 0) { + swapchain_supported = true; + break; + } + } + + for (uint32_t i = 0; i < family_count; ++i) { + if (families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { + graphics_index = static_cast(i); + } + VkBool32 supports_present = VK_FALSE; + vkGetPhysicalDeviceSurfaceSupportKHR(candidate, i, surface, &supports_present); + if (supports_present) { + present_index = static_cast(i); + } + if (graphics_index >= 0 && present_index >= 0) { + break; + } + } + + VkPhysicalDeviceProperties properties{}; + vkGetPhysicalDeviceProperties(candidate, &properties); + + int score = 0; + if (graphics_index >= 0 && present_index >= 0 && swapchain_supported) { + score += 1000; + } + if (properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { + score += 100; + } + if (score > best_score) { + best_score = score; + physical_device = candidate; + graphics_family = static_cast(graphics_index); + present_family = static_cast(present_index); + } + } + + if (best_score < 1000) { + throw std::runtime_error("no suitable Vulkan device (graphics + present + swapchain)"); + } + + std::vector queue_infos; + std::array unique_families = {graphics_family, present_family}; + float queue_priority = 1.0F; + for (uint32_t family : unique_families) { + VkDeviceQueueCreateInfo queue_info{}; + queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queue_info.queueFamilyIndex = family; + queue_info.queueCount = 1; + queue_info.pQueuePriorities = &queue_priority; + queue_infos.push_back(queue_info); + } + if (queue_infos.size() == 2 && graphics_family == present_family) { + queue_infos.resize(1); + } + + VkDeviceCreateInfo device_info{}; + device_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + device_info.queueCreateInfoCount = static_cast(queue_infos.size()); + device_info.pQueueCreateInfos = queue_infos.data(); + const char* device_extension = VK_KHR_SWAPCHAIN_EXTENSION_NAME; + device_info.enabledExtensionCount = 1; + device_info.ppEnabledExtensionNames = &device_extension; + check(vkCreateDevice(physical_device, &device_info, nullptr, &device), "vkCreateDevice"); + vkGetDeviceQueue(device, graphics_family, 0, &graphics_queue); + vkGetDeviceQueue(device, present_family, 0, &present_queue); + + VkSurfaceCapabilitiesKHR capabilities{}; + check(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_device, surface, &capabilities), + "vkGetPhysicalDeviceSurfaceCapabilitiesKHR"); + + uint32_t format_count = 0; + check(vkGetPhysicalDeviceSurfaceFormatsKHR(physical_device, surface, &format_count, nullptr), + "vkGetPhysicalDeviceSurfaceFormatsKHR"); + std::vector formats(format_count); + check(vkGetPhysicalDeviceSurfaceFormatsKHR(physical_device, surface, &format_count, + formats.data()), + "vkGetPhysicalDeviceSurfaceFormatsKHR"); + VkSurfaceFormatKHR surface_format = formats[0]; + for (const auto& format : formats) { + if (format.format == VK_FORMAT_B8G8R8A8_SRGB && + format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { + surface_format = format; + break; + } + } + + uint32_t present_mode_count = 0; + check(vkGetPhysicalDeviceSurfacePresentModesKHR(physical_device, surface, &present_mode_count, + nullptr), + "vkGetPhysicalDeviceSurfacePresentModesKHR"); + std::vector present_modes(present_mode_count); + check(vkGetPhysicalDeviceSurfacePresentModesKHR(physical_device, surface, &present_mode_count, + present_modes.data()), + "vkGetPhysicalDeviceSurfacePresentModesKHR"); + VkPresentModeKHR present_mode = VK_PRESENT_MODE_FIFO_KHR; + for (VkPresentModeKHR mode : present_modes) { + if (mode == VK_PRESENT_MODE_MAILBOX_KHR) { + present_mode = mode; + break; + } + } + + if (capabilities.currentExtent.width != UINT32_MAX) { + swapchain_extent = capabilities.currentExtent; + } else { + int fb_width = 0; + int fb_height = 0; + glfwGetFramebufferSize(window, &fb_width, &fb_height); + swapchain_extent.width = + std::clamp(static_cast(fb_width), capabilities.minImageExtent.width, + capabilities.maxImageExtent.width); + swapchain_extent.height = + std::clamp(static_cast(fb_height), capabilities.minImageExtent.height, + capabilities.maxImageExtent.height); + } + + uint32_t image_count = capabilities.minImageCount + 1; + if (capabilities.maxImageCount > 0 && image_count > capabilities.maxImageCount) { + image_count = capabilities.maxImageCount; + } + + VkSwapchainCreateInfoKHR swapchain_info{}; + swapchain_info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; + swapchain_info.surface = surface; + swapchain_info.minImageCount = image_count; + swapchain_info.imageFormat = surface_format.format; + swapchain_info.imageColorSpace = surface_format.colorSpace; + swapchain_info.imageExtent = swapchain_extent; + swapchain_info.imageArrayLayers = 1; + swapchain_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + swapchain_info.imageSharingMode = + graphics_family == present_family ? VK_SHARING_MODE_EXCLUSIVE : VK_SHARING_MODE_CONCURRENT; + swapchain_info.queueFamilyIndexCount = graphics_family == present_family ? 0 : 2; + std::array families = {graphics_family, present_family}; + swapchain_info.pQueueFamilyIndices = families.data(); + swapchain_info.preTransform = capabilities.currentTransform; + swapchain_info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + swapchain_info.presentMode = present_mode; + swapchain_info.clipped = VK_TRUE; + swapchain_info.oldSwapchain = VK_NULL_HANDLE; + + swapchain_format = surface_format.format; + check(vkCreateSwapchainKHR(device, &swapchain_info, nullptr, &swapchain), + "vkCreateSwapchainKHR"); + + uint32_t actual_image_count = 0; + check(vkGetSwapchainImagesKHR(device, swapchain, &actual_image_count, nullptr), + "vkGetSwapchainImagesKHR"); + swapchain_images.resize(actual_image_count); + check(vkGetSwapchainImagesKHR(device, swapchain, &actual_image_count, swapchain_images.data()), + "vkGetSwapchainImagesKHR"); + swapchain_image_views.resize(actual_image_count); + for (uint32_t i = 0; i < actual_image_count; ++i) { + VkImageViewCreateInfo view_info{}; + view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + view_info.image = swapchain_images[i]; + view_info.viewType = VK_IMAGE_VIEW_TYPE_2D; + view_info.format = swapchain_format; + view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + view_info.subresourceRange.baseMipLevel = 0; + view_info.subresourceRange.levelCount = 1; + view_info.subresourceRange.baseArrayLayer = 0; + view_info.subresourceRange.layerCount = 1; + check(vkCreateImageView(device, &view_info, nullptr, &swapchain_image_views[i]), + "vkCreateImageView"); + } + + VkAttachmentDescription color_attachment{}; + color_attachment.format = swapchain_format; + color_attachment.samples = VK_SAMPLE_COUNT_1_BIT; + color_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + color_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + color_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + color_attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + color_attachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + + VkAttachmentReference color_reference{}; + color_reference.attachment = 0; + color_reference.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &color_reference; + + VkRenderPassCreateInfo render_pass_info{}; + render_pass_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + render_pass_info.attachmentCount = 1; + render_pass_info.pAttachments = &color_attachment; + render_pass_info.subpassCount = 1; + render_pass_info.pSubpasses = &subpass; + check(vkCreateRenderPass(device, &render_pass_info, nullptr, &render_pass), + "vkCreateRenderPass"); + + framebuffers.resize(actual_image_count); + for (uint32_t i = 0; i < actual_image_count; ++i) { + VkFramebufferCreateInfo framebuffer_info{}; + framebuffer_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + framebuffer_info.renderPass = render_pass; + framebuffer_info.attachmentCount = 1; + framebuffer_info.pAttachments = &swapchain_image_views[i]; + framebuffer_info.width = swapchain_extent.width; + framebuffer_info.height = swapchain_extent.height; + framebuffer_info.layers = 1; + check(vkCreateFramebuffer(device, &framebuffer_info, nullptr, &framebuffers[i]), + "vkCreateFramebuffer"); + } + + VkCommandPoolCreateInfo command_pool_info{}; + command_pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + command_pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + command_pool_info.queueFamilyIndex = graphics_family; + check(vkCreateCommandPool(device, &command_pool_info, nullptr, &command_pool), + "vkCreateCommandPool"); + + const uint32_t frame_count = actual_image_count; + + VkSemaphoreCreateInfo semaphore_info{}; + semaphore_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + + frames.resize(frame_count); + images_in_flight.resize(actual_image_count, VK_NULL_HANDLE); + for (FrameData& frame : frames) { + check(vkCreateSemaphore(device, &semaphore_info, nullptr, &frame.image_available), + "vkCreateSemaphore"); + check(vkCreateSemaphore(device, &semaphore_info, nullptr, &frame.render_finished), + "vkCreateSemaphore"); + + VkFenceCreateInfo fence_info{}; + fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT; + check(vkCreateFence(device, &fence_info, nullptr, &frame.in_flight), "vkCreateFence"); + + VkCommandBufferAllocateInfo allocate_info{}; + allocate_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocate_info.commandPool = command_pool; + allocate_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocate_info.commandBufferCount = 1; + check(vkAllocateCommandBuffers(device, &allocate_info, &frame.command_buffer), + "vkAllocateCommandBuffers"); + } + + VkDescriptorSetLayoutBinding layout_binding{}; + layout_binding.binding = 0; + layout_binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + layout_binding.descriptorCount = 1; + layout_binding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + + VkDescriptorSetLayoutCreateInfo descriptor_layout_info{}; + descriptor_layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + descriptor_layout_info.bindingCount = 1; + descriptor_layout_info.pBindings = &layout_binding; + check(vkCreateDescriptorSetLayout(device, &descriptor_layout_info, nullptr, + &descriptor_set_layout), + "vkCreateDescriptorSetLayout"); + + VkPushConstantRange push_constant_range{}; + push_constant_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + push_constant_range.offset = 0; + push_constant_range.size = sizeof(float) * 4; + + VkPipelineLayoutCreateInfo pipeline_layout_info{}; + pipeline_layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + pipeline_layout_info.setLayoutCount = 1; + pipeline_layout_info.pSetLayouts = &descriptor_set_layout; + pipeline_layout_info.pushConstantRangeCount = 1; + pipeline_layout_info.pPushConstantRanges = &push_constant_range; + check(vkCreatePipelineLayout(device, &pipeline_layout_info, nullptr, &pipeline_layout), + "vkCreatePipelineLayout"); + + VkDescriptorPoolSize pool_size{}; + pool_size.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + pool_size.descriptorCount = 1; + + VkDescriptorPoolCreateInfo pool_info{}; + pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + pool_info.maxSets = 1; + pool_info.poolSizeCount = 1; + pool_info.pPoolSizes = &pool_size; + check(vkCreateDescriptorPool(device, &pool_info, nullptr, &descriptor_pool), + "vkCreateDescriptorPool"); + + VkDescriptorSetAllocateInfo set_allocate_info{}; + set_allocate_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + set_allocate_info.descriptorPool = descriptor_pool; + set_allocate_info.descriptorSetCount = 1; + set_allocate_info.pSetLayouts = &descriptor_set_layout; + check(vkAllocateDescriptorSets(device, &set_allocate_info, &descriptor_set), + "vkAllocateDescriptorSets"); + + create_buffer(physical_device, device, uniform_buffer_size, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + uniform_buffer, uniform_buffer_memory); + check(vkMapMemory(device, uniform_buffer_memory, 0, uniform_buffer_size, 0, &uniform_mapped), + "vkMapMemory"); + + VkDescriptorBufferInfo buffer_info{}; + buffer_info.buffer = uniform_buffer; + buffer_info.offset = 0; + buffer_info.range = uniform_buffer_size; + + VkWriteDescriptorSet descriptor_write{}; + descriptor_write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptor_write.dstSet = descriptor_set; + descriptor_write.dstBinding = 0; + descriptor_write.dstArrayElement = 0; + descriptor_write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + descriptor_write.descriptorCount = 1; + descriptor_write.pBufferInfo = &buffer_info; + vkUpdateDescriptorSets(device, 1, &descriptor_write, 0, nullptr); + + VkShaderModule vertex_module = create_shader_module( + device, kQuadVertShader.data(), kQuadVertShader.size() * sizeof(kQuadVertShader[0])); + VkShaderModule fragment_module = create_shader_module( + device, kQuadFragShader.data(), kQuadFragShader.size() * sizeof(kQuadFragShader[0])); + + VkPipelineShaderStageCreateInfo vertex_stage{}; + vertex_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + vertex_stage.stage = VK_SHADER_STAGE_VERTEX_BIT; + vertex_stage.module = vertex_module; + vertex_stage.pName = "main"; + + VkPipelineShaderStageCreateInfo fragment_stage{}; + fragment_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + fragment_stage.stage = VK_SHADER_STAGE_FRAGMENT_BIT; + fragment_stage.module = fragment_module; + fragment_stage.pName = "main"; + + std::array stages = {vertex_stage, fragment_stage}; + + VkVertexInputBindingDescription vertex_binding{}; + vertex_binding.binding = 0; + vertex_binding.stride = sizeof(Mesh::Vertex); + vertex_binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::array vertex_attributes{}; + vertex_attributes[0].binding = 0; + vertex_attributes[0].location = 0; + vertex_attributes[0].format = VK_FORMAT_R32G32_SFLOAT; + vertex_attributes[0].offset = offsetof(Mesh::Vertex, position); + vertex_attributes[1].binding = 0; + vertex_attributes[1].location = 1; + vertex_attributes[1].format = VK_FORMAT_R32G32B32A32_SFLOAT; + vertex_attributes[1].offset = offsetof(Mesh::Vertex, color); + + VkPipelineVertexInputStateCreateInfo vertex_input{}; + vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + vertex_input.vertexBindingDescriptionCount = 1; + vertex_input.pVertexBindingDescriptions = &vertex_binding; + vertex_input.vertexAttributeDescriptionCount = + static_cast(vertex_attributes.size()); + vertex_input.pVertexAttributeDescriptions = vertex_attributes.data(); + + VkPipelineInputAssemblyStateCreateInfo input_assembly{}; + input_assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + input_assembly.primitiveRestartEnable = VK_FALSE; + + VkViewport viewport{}; + viewport.x = 0.0F; + viewport.y = 0.0F; + viewport.width = static_cast(swapchain_extent.width); + viewport.height = static_cast(swapchain_extent.height); + viewport.minDepth = 0.0F; + viewport.maxDepth = 1.0F; + + VkRect2D scissor{}; + scissor.offset = {0, 0}; + scissor.extent = swapchain_extent; + + VkPipelineViewportStateCreateInfo viewport_state{}; + viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + viewport_state.viewportCount = 1; + viewport_state.pViewports = &viewport; + viewport_state.scissorCount = 1; + viewport_state.pScissors = &scissor; + + VkPipelineRasterizationStateCreateInfo rasterizer{}; + rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rasterizer.depthClampEnable = VK_FALSE; + rasterizer.rasterizerDiscardEnable = VK_FALSE; + rasterizer.polygonMode = VK_POLYGON_MODE_FILL; + rasterizer.cullMode = VK_CULL_MODE_NONE; + rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + rasterizer.depthBiasEnable = VK_FALSE; + rasterizer.lineWidth = 1.0F; + + VkPipelineMultisampleStateCreateInfo multisampling{}; + multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + multisampling.sampleShadingEnable = VK_FALSE; + + VkPipelineColorBlendAttachmentState color_blend_attachment{}; + color_blend_attachment.blendEnable = VK_FALSE; + color_blend_attachment.colorWriteMask = + VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | + VK_COLOR_COMPONENT_A_BIT; + + VkPipelineColorBlendStateCreateInfo color_blending{}; + color_blending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + color_blending.logicOpEnable = VK_FALSE; + color_blending.attachmentCount = 1; + color_blending.pAttachments = &color_blend_attachment; + + VkGraphicsPipelineCreateInfo pipeline_info{}; + pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + pipeline_info.stageCount = static_cast(stages.size()); + pipeline_info.pStages = stages.data(); + pipeline_info.pVertexInputState = &vertex_input; + pipeline_info.pInputAssemblyState = &input_assembly; + pipeline_info.pViewportState = &viewport_state; + pipeline_info.pRasterizationState = &rasterizer; + pipeline_info.pMultisampleState = &multisampling; + pipeline_info.pDepthStencilState = nullptr; + pipeline_info.pColorBlendState = &color_blending; + pipeline_info.pDynamicState = nullptr; + pipeline_info.layout = pipeline_layout; + pipeline_info.renderPass = render_pass; + pipeline_info.subpass = 0; + check(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &pipeline), + "vkCreateGraphicsPipelines"); + + vkDestroyShaderModule(device, vertex_module, nullptr); + vkDestroyShaderModule(device, fragment_module, nullptr); + + initialized = true; +} + +void Renderer::Impl::upload_mesh(const Mesh& mesh) { + if (cached_mesh == &mesh && mesh_uploaded) { + return; + } + + if (vertex_buffer != VK_NULL_HANDLE) { + vkDestroyBuffer(device, vertex_buffer, nullptr); + vertex_buffer = VK_NULL_HANDLE; + } + if (vertex_buffer_memory != VK_NULL_HANDLE) { + vkFreeMemory(device, vertex_buffer_memory, nullptr); + vertex_buffer_memory = VK_NULL_HANDLE; + } + if (index_buffer != VK_NULL_HANDLE) { + vkDestroyBuffer(device, index_buffer, nullptr); + index_buffer = VK_NULL_HANDLE; + } + if (index_buffer_memory != VK_NULL_HANDLE) { + vkFreeMemory(device, index_buffer_memory, nullptr); + index_buffer_memory = VK_NULL_HANDLE; + } + + cached_vertex_count = static_cast(mesh.vertices.size()); + cached_index_count = static_cast(mesh.indices.size()); + cached_mesh = &mesh; + mesh_uploaded = false; + + if (cached_vertex_count == 0 || cached_index_count == 0) { + return; + } + + const VkDeviceSize vertex_size = sizeof(Mesh::Vertex) * cached_vertex_count; + create_buffer(physical_device, device, vertex_size, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + vertex_buffer, vertex_buffer_memory); + void* vertex_mapped = nullptr; + check(vkMapMemory(device, vertex_buffer_memory, 0, vertex_size, 0, &vertex_mapped), + "vkMapMemory"); + std::memcpy(vertex_mapped, mesh.vertices.data(), vertex_size); + vkUnmapMemory(device, vertex_buffer_memory); + + const VkDeviceSize index_size = sizeof(uint32_t) * cached_index_count; + create_buffer(physical_device, device, index_size, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + index_buffer, index_buffer_memory); + void* index_mapped = nullptr; + check(vkMapMemory(device, index_buffer_memory, 0, index_size, 0, &index_mapped), + "vkMapMemory"); + std::memcpy(index_mapped, mesh.indices.data(), index_size); + vkUnmapMemory(device, index_buffer_memory); + + mesh_uploaded = true; +} + +void Renderer::Impl::draw(const Actor* const* actors, size_t actor_count) { + if (!initialized || actor_count == 0) { + return; + } + + FrameData& frame = frames[current_frame % frames.size()]; + check(vkWaitForFences(device, 1, &frame.in_flight, VK_TRUE, UINT64_MAX), "vkWaitForFences"); + + uint32_t image_index = 0; + VkResult acquire_result = vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, + frame.image_available, + VK_NULL_HANDLE, &image_index); + if (acquire_result == VK_ERROR_OUT_OF_DATE_KHR) { + return; + } + if (acquire_result != VK_SUCCESS && acquire_result != VK_SUBOPTIMAL_KHR) { + check(acquire_result, "vkAcquireNextImageKHR"); + } + + if (images_in_flight[image_index] != VK_NULL_HANDLE) { + check(vkWaitForFences(device, 1, &images_in_flight[image_index], VK_TRUE, UINT64_MAX), + "vkWaitForFences(image)"); + } + images_in_flight[image_index] = frame.in_flight; + check(vkResetFences(device, 1, &frame.in_flight), "vkResetFences"); + + // 示例渲染器一次上传一个 Mesh;最后一个 Actor 的网格用于绑定。 + for (size_t i = 0; i < actor_count; ++i) { + const Actor& actor = *actors[i]; + const auto& mesh = actor.mesh(); + if (mesh != nullptr) { + upload_mesh(*mesh); + } + } + + check(vkResetCommandBuffer(frame.command_buffer, 0), "vkResetCommandBuffer"); + + VkCommandBufferBeginInfo begin_info{}; + begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + check(vkBeginCommandBuffer(frame.command_buffer, &begin_info), "vkBeginCommandBuffer"); + + VkClearValue clear_value{}; + clear_value.color = {{0.03F, 0.03F, 0.06F, 1.0F}}; + + VkRenderPassBeginInfo render_pass_begin{}; + render_pass_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + render_pass_begin.renderPass = render_pass; + render_pass_begin.framebuffer = framebuffers[image_index]; + render_pass_begin.renderArea.offset = {0, 0}; + render_pass_begin.renderArea.extent = swapchain_extent; + render_pass_begin.clearValueCount = 1; + render_pass_begin.pClearValues = &clear_value; + + vkCmdBeginRenderPass(frame.command_buffer, &render_pass_begin, VK_SUBPASS_CONTENTS_INLINE); + vkCmdBindPipeline(frame.command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + vkCmdBindDescriptorSets(frame.command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, + 0, 1, &descriptor_set, 0, nullptr); + + if (vertex_buffer == VK_NULL_HANDLE || index_buffer == VK_NULL_HANDLE || + cached_index_count == 0 || cached_vertex_count == 0) { + vkCmdEndRenderPass(frame.command_buffer); + check(vkEndCommandBuffer(frame.command_buffer), "vkEndCommandBuffer"); + } else { + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(frame.command_buffer, 0, 1, &vertex_buffer, &offset); + vkCmdBindIndexBuffer(frame.command_buffer, index_buffer, 0, VK_INDEX_TYPE_UINT32); + + for (size_t i = 0; i < actor_count; ++i) { + const Actor& actor = *actors[i]; + if (actor.mesh() == nullptr) { + continue; + } + + const auto position = actor.position(); + const auto facing = actor.facing(); + const float scale = 0.35F; + float facing_x = facing[0]; + float facing_y = facing[1]; + const float facing_length = std::sqrt(facing_x * facing_x + facing_y * facing_y); + if (facing_length > 0.0001F) { + facing_x /= facing_length; + facing_y /= facing_length; + } else { + facing_x = 1.0F; + facing_y = 0.0F; + } + + // 朝向向量作为局部 X 轴,旋转 + 平移,把单位四边形放到 Actor 的屏幕坐标。 + std::array model = {{ + scale * facing_x, scale * facing_y, 0.0F, 0.0F, + -scale * facing_y, scale * facing_x, 0.0F, 0.0F, + 0.0F, 0.0F, 1.0F, 0.0F, + position[0], position[1], 0.0F, 1.0F, + }}; + std::memcpy(uniform_mapped, model.data(), uniform_buffer_size); + + const std::array color = actor.mesh()->color(); + vkCmdPushConstants(frame.command_buffer, pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT, 0, + sizeof(float) * 4, color.data()); + vkCmdDrawIndexed(frame.command_buffer, cached_index_count, 1, 0, 0, 0); + } + + vkCmdEndRenderPass(frame.command_buffer); + check(vkEndCommandBuffer(frame.command_buffer), "vkEndCommandBuffer"); + } + + VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + VkSubmitInfo submit_info{}; + submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submit_info.waitSemaphoreCount = 1; + submit_info.pWaitSemaphores = &frame.image_available; + submit_info.pWaitDstStageMask = &wait_stage; + submit_info.commandBufferCount = 1; + submit_info.pCommandBuffers = &frame.command_buffer; + FrameData& image_frame = frames[image_index]; + submit_info.signalSemaphoreCount = 1; + submit_info.pSignalSemaphores = &image_frame.render_finished; + check(vkQueueSubmit(graphics_queue, 1, &submit_info, frame.in_flight), "vkQueueSubmit"); + + VkPresentInfoKHR present_info{}; + present_info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + present_info.waitSemaphoreCount = 1; + present_info.pWaitSemaphores = &image_frame.render_finished; + present_info.swapchainCount = 1; + present_info.pSwapchains = &swapchain; + present_info.pImageIndices = &image_index; + VkResult present_result = vkQueuePresentKHR(present_queue, &present_info); + if (present_result != VK_SUCCESS && present_result != VK_SUBOPTIMAL_KHR && + present_result != VK_ERROR_OUT_OF_DATE_KHR) { + check(present_result, "vkQueuePresentKHR"); + } + (void)present_result; + + current_frame++; +} + +void Renderer::Impl::destroy() { + if (!initialized) { + return; + } + if (device != VK_NULL_HANDLE) { + vkDeviceWaitIdle(device); + } + + if (uniform_mapped != nullptr && uniform_buffer_memory != VK_NULL_HANDLE) { + vkUnmapMemory(device, uniform_buffer_memory); + uniform_mapped = nullptr; + } + + for (FrameData& frame : frames) { + if (frame.in_flight != VK_NULL_HANDLE) { + vkDestroyFence(device, frame.in_flight, nullptr); + } + if (frame.image_available != VK_NULL_HANDLE) { + vkDestroySemaphore(device, frame.image_available, nullptr); + } + if (frame.render_finished != VK_NULL_HANDLE) { + vkDestroySemaphore(device, frame.render_finished, nullptr); + } + if (frame.command_buffer != VK_NULL_HANDLE) { + vkFreeCommandBuffers(device, command_pool, 1, &frame.command_buffer); + } + } + + if (uniform_buffer != VK_NULL_HANDLE) { + vkDestroyBuffer(device, uniform_buffer, nullptr); + } + if (uniform_buffer_memory != VK_NULL_HANDLE) { + vkFreeMemory(device, uniform_buffer_memory, nullptr); + } + if (vertex_buffer != VK_NULL_HANDLE) { + vkDestroyBuffer(device, vertex_buffer, nullptr); + } + if (vertex_buffer_memory != VK_NULL_HANDLE) { + vkFreeMemory(device, vertex_buffer_memory, nullptr); + } + if (index_buffer != VK_NULL_HANDLE) { + vkDestroyBuffer(device, index_buffer, nullptr); + } + if (index_buffer_memory != VK_NULL_HANDLE) { + vkFreeMemory(device, index_buffer_memory, nullptr); + } + + if (descriptor_pool != VK_NULL_HANDLE) { + vkDestroyDescriptorPool(device, descriptor_pool, nullptr); + } + if (descriptor_set_layout != VK_NULL_HANDLE) { + vkDestroyDescriptorSetLayout(device, descriptor_set_layout, nullptr); + } + if (pipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(device, pipeline, nullptr); + } + if (pipeline_layout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, pipeline_layout, nullptr); + } + if (command_pool != VK_NULL_HANDLE) { + vkDestroyCommandPool(device, command_pool, nullptr); + } + for (VkFramebuffer framebuffer : framebuffers) { + if (framebuffer != VK_NULL_HANDLE) { + vkDestroyFramebuffer(device, framebuffer, nullptr); + } + } + if (render_pass != VK_NULL_HANDLE) { + vkDestroyRenderPass(device, render_pass, nullptr); + } + for (VkImageView view : swapchain_image_views) { + if (view != VK_NULL_HANDLE) { + vkDestroyImageView(device, view, nullptr); + } + } + if (swapchain != VK_NULL_HANDLE) { + vkDestroySwapchainKHR(device, swapchain, nullptr); + } + if (device != VK_NULL_HANDLE) { + vkDestroyDevice(device, nullptr); + } + if (surface != VK_NULL_HANDLE && instance != VK_NULL_HANDLE) { + vkDestroySurfaceKHR(instance, surface, nullptr); + } + if (debug_messenger != VK_NULL_HANDLE && instance != VK_NULL_HANDLE) { + auto destroy_messenger = reinterpret_cast( + vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT")); + if (destroy_messenger != nullptr) { + destroy_messenger(instance, debug_messenger, nullptr); + } + debug_messenger = VK_NULL_HANDLE; + } + if (instance != VK_NULL_HANDLE) { + vkDestroyInstance(instance, nullptr); + } + if (window != nullptr) { + glfwDestroyWindow(window); + glfwTerminate(); + } + + swapchain_images.clear(); + swapchain_image_views.clear(); + framebuffers.clear(); + frames.clear(); + initialized = false; +} + +} // namespace openceg::render diff --git a/third_party/ConsoleLib b/third_party/ConsoleLib deleted file mode 160000 index 5f51a52..0000000 --- a/third_party/ConsoleLib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5f51a52ca6b41ba3865b42b272eeacc1287c762b