Compare commits
4 Commits
12a1e6eca9
...
1229f5c017
| Author | SHA1 | Date | |
|---|---|---|---|
| 1229f5c017 | |||
| 34faf8b61e | |||
| c97db37b31 | |||
| 05fe3c2ee8 |
@@ -0,0 +1,329 @@
|
|||||||
|
# Glossary
|
||||||
|
|
||||||
|
## AssetRegistry
|
||||||
|
|
||||||
|
An AssetRegistry owns GPU-backed assets on the caller's behalf and hands out
|
||||||
|
lightweight Handles instead of pointers or ownership.
|
||||||
|
|
||||||
|
Internally it's a collection of per-type pools, each responsible for one
|
||||||
|
asset type:
|
||||||
|
|
||||||
|
```
|
||||||
|
AssetRegistry
|
||||||
|
├── MeshPool → MeshHandle
|
||||||
|
├── TexturePool → TextureHandle (planned)
|
||||||
|
└── MaterialPool → MaterialHandle (planned)
|
||||||
|
```
|
||||||
|
|
||||||
|
A Handle is a small (index, generation) pair. The index locates the slot in
|
||||||
|
the pool; the generation is bumped when that slot is released, so a Handle
|
||||||
|
kept around after release() is detected as stale instead of resolving to
|
||||||
|
someone else's asset (or freed memory).
|
||||||
|
|
||||||
|
Typical lifecycle:
|
||||||
|
|
||||||
|
```
|
||||||
|
load<Mesh>(path) / loadMesh(...) → MeshHandle
|
||||||
|
│
|
||||||
|
get<Mesh>(handle) → const Mesh&
|
||||||
|
│
|
||||||
|
release<Mesh>(handle)
|
||||||
|
```
|
||||||
|
|
||||||
|
Think: "Where do I get or release a GPU asset by handle, without owning its
|
||||||
|
lifetime directly?"
|
||||||
|
|
||||||
|
## Buffer
|
||||||
|
|
||||||
|
A Buffer is a linear region of GPU memory used to store arbitrary data.
|
||||||
|
|
||||||
|
Unlike an Image, which is designed for multidimensional image data, a buffer is essentially:
|
||||||
|
|
||||||
|
```
|
||||||
|
[ byte byte byte byte byte byte ... ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Buffers commonly contain:
|
||||||
|
|
||||||
|
Vertex data
|
||||||
|
Index data
|
||||||
|
Uniform data
|
||||||
|
Storage data
|
||||||
|
Indirect draw commands
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
Vertex Buffer
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ Vertex │ Vertex │ Vertex │ ... │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
In Vulkan, VkBuffer / vk::Buffer is a fundamental resource.
|
||||||
|
|
||||||
|
Think: "A linear chunk of GPU-accessible data."
|
||||||
|
|
||||||
|
## Camera
|
||||||
|
|
||||||
|
A Camera defines how the scene is viewed and projected onto the screen.
|
||||||
|
|
||||||
|
It generally contains:
|
||||||
|
|
||||||
|
Position
|
||||||
|
Orientation
|
||||||
|
Projection parameters
|
||||||
|
View matrix
|
||||||
|
Projection matrix
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
Camera
|
||||||
|
├── Position
|
||||||
|
├── Rotation
|
||||||
|
├── Projection
|
||||||
|
│ ├── Perspective
|
||||||
|
│ └── Orthographic
|
||||||
|
├── View Matrix
|
||||||
|
└── Projection Matrix
|
||||||
|
```
|
||||||
|
|
||||||
|
Think: "From where and how am I looking at the scene?"
|
||||||
|
|
||||||
|
## Canvas
|
||||||
|
|
||||||
|
A Canvas is a coordinate/rendering space onto which things are drawn.
|
||||||
|
|
||||||
|
For UI, for example:
|
||||||
|
|
||||||
|
```
|
||||||
|
Canvas
|
||||||
|
┌──────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ │
|
||||||
|
│ │ Panel │ │
|
||||||
|
│ └──────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Button │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
A canvas establishes things such as:
|
||||||
|
|
||||||
|
Dimensions
|
||||||
|
Coordinate system
|
||||||
|
Origin
|
||||||
|
Scaling
|
||||||
|
Potentially clipping
|
||||||
|
|
||||||
|
In a UI system, a Canvas is essentially:
|
||||||
|
|
||||||
|
The space in which UI elements are laid out and rendered.
|
||||||
|
|
||||||
|
## Frame
|
||||||
|
|
||||||
|
Frame is context-dependent, but in a renderer it usually means one iteration of rendering the scene to produce one displayed image.
|
||||||
|
|
||||||
|
## Layer
|
||||||
|
|
||||||
|
A Layer is a logical ordering/grouping of content.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
Layer 0: Background
|
||||||
|
Layer 1: World
|
||||||
|
Layer 2: Characters
|
||||||
|
Layer 3: Effects
|
||||||
|
Layer 4: UI
|
||||||
|
|
||||||
|
Higher layers can be rendered over lower layers.
|
||||||
|
|
||||||
|
In a 2D engine, layers might determine draw order:
|
||||||
|
|
||||||
|
```
|
||||||
|
Background
|
||||||
|
↓
|
||||||
|
Tiles
|
||||||
|
↓
|
||||||
|
Player
|
||||||
|
↓
|
||||||
|
Particles
|
||||||
|
↓
|
||||||
|
UI
|
||||||
|
```
|
||||||
|
|
||||||
|
However, Layer is very engine-specific. In a 3D renderer it could instead represent a rendering category, visibility mask, or render-pass grouping.
|
||||||
|
|
||||||
|
## Material
|
||||||
|
|
||||||
|
A Material describes how a surface should be rendered.
|
||||||
|
|
||||||
|
It usually combines:
|
||||||
|
|
||||||
|
Shader/pipeline configuration
|
||||||
|
Textures
|
||||||
|
Colors
|
||||||
|
Numerical parameters
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
Material
|
||||||
|
├── Shader
|
||||||
|
├── Albedo Texture
|
||||||
|
├── Normal Texture
|
||||||
|
├── Roughness
|
||||||
|
├── Metallic
|
||||||
|
└── Base Color
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```
|
||||||
|
Mesh + Material + Transform
|
||||||
|
↓
|
||||||
|
Renderer
|
||||||
|
↓
|
||||||
|
GPU
|
||||||
|
```
|
||||||
|
|
||||||
|
A useful conceptual distinction is:
|
||||||
|
|
||||||
|
Mesh = what the object is shaped like.
|
||||||
|
Material = how that shape should look.
|
||||||
|
|
||||||
|
## Mesh
|
||||||
|
|
||||||
|
A Mesh is the geometric data used to describe the shape of an object.
|
||||||
|
|
||||||
|
Typically contains:
|
||||||
|
|
||||||
|
Vertex positions
|
||||||
|
Normals
|
||||||
|
Texture coordinates (UVs)
|
||||||
|
Tangents
|
||||||
|
Indices
|
||||||
|
|
||||||
|
For example, a cube mesh contains the vertices and triangles necessary to describe a cube.
|
||||||
|
|
||||||
|
```
|
||||||
|
Mesh
|
||||||
|
├── Vertices
|
||||||
|
├── Indices
|
||||||
|
└── Vertex attributes
|
||||||
|
├── Position
|
||||||
|
├── Normal
|
||||||
|
├── UV
|
||||||
|
└── Tangent
|
||||||
|
```
|
||||||
|
|
||||||
|
Think: "What shape is this object?"
|
||||||
|
|
||||||
|
## Panel
|
||||||
|
|
||||||
|
A Panel is a rectangular UI region/container.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────┐
|
||||||
|
│ Toolbar │
|
||||||
|
├───────────┬──────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ Scene │ Viewport │
|
||||||
|
│ │ │
|
||||||
|
├───────────┼──────────────────┤
|
||||||
|
│ Inspector │ Console │
|
||||||
|
└───────────┴──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Each of those regions could be a panel.
|
||||||
|
|
||||||
|
A panel generally has:
|
||||||
|
|
||||||
|
Position
|
||||||
|
Size
|
||||||
|
Child elements
|
||||||
|
UI contents
|
||||||
|
Potentially its own rendering/context
|
||||||
|
|
||||||
|
This is primarily a UI concept, rather than a GPU concept.
|
||||||
|
|
||||||
|
## PanelRegistry
|
||||||
|
|
||||||
|
A PanelRegistry owns Panels on the caller's behalf and hands out lightweight
|
||||||
|
PanelHandles instead of pointers or ownership.
|
||||||
|
|
||||||
|
Internally it's a generation-checked slot pool, the same shape AssetRegistry's
|
||||||
|
per-type pools use:
|
||||||
|
|
||||||
|
```
|
||||||
|
PanelRegistry
|
||||||
|
└── Slot { Panel, generation } → PanelHandle
|
||||||
|
```
|
||||||
|
|
||||||
|
Typical lifecycle:
|
||||||
|
|
||||||
|
```
|
||||||
|
add(panel) / emplace<T>(...) → PanelHandle
|
||||||
|
│
|
||||||
|
get(handle) / get<T>(handle) → Panel& / T&
|
||||||
|
│
|
||||||
|
remove(handle)
|
||||||
|
```
|
||||||
|
|
||||||
|
Each frame, update(frameInfo) calls onUpdate on every visible Panel it holds.
|
||||||
|
|
||||||
|
Think: "Where do I register or look up a UI panel by handle, without owning
|
||||||
|
its lifetime directly?"
|
||||||
|
|
||||||
|
## Sprite
|
||||||
|
|
||||||
|
A Sprite is a 2D renderable representation of an image, usually a rectangular region of a texture.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
Texture Atlas
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Player │ Enemy │
|
||||||
|
│ │ │
|
||||||
|
├────────┼────────────┤
|
||||||
|
│ Tree │ Rock │
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
A sprite might reference only the Player region of that texture.
|
||||||
|
|
||||||
|
A useful engine abstraction might be:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Sprite {
|
||||||
|
Texture* texture;
|
||||||
|
Rect uv;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Think: "Which 2D picture/region am I drawing?"
|
||||||
|
|
||||||
|
## Texture
|
||||||
|
|
||||||
|
A Texture is image data intended to be used by a shader.
|
||||||
|
|
||||||
|
It can represent:
|
||||||
|
|
||||||
|
Color/albedo
|
||||||
|
Normal maps
|
||||||
|
Roughness
|
||||||
|
Metallic values
|
||||||
|
Masks
|
||||||
|
Height maps
|
||||||
|
etc.
|
||||||
|
|
||||||
|
A texture doesn't necessarily have to be displayed directly.
|
||||||
|
|
||||||
|
Texture → Shader → Material/Object
|
||||||
|
|
||||||
|
Think: "What image/data does the shader sample?"
|
||||||
@@ -3,10 +3,13 @@
|
|||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <sophia/asset_registry.hpp>
|
||||||
#include <sophia/camera_2d.hpp>
|
#include <sophia/camera_2d.hpp>
|
||||||
#include <sophia/canvas.hpp>
|
#include <sophia/canvas.hpp>
|
||||||
#include <sophia/frame_info.hpp>
|
#include <sophia/frame_info.hpp>
|
||||||
#include <sophia/types.hpp>
|
#include <sophia/types.hpp>
|
||||||
|
#include <sophia/ui/debug_ui.hpp>
|
||||||
|
#include <sophia/ui/panel_registry.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -27,7 +30,7 @@ class Application {
|
|||||||
Application(const Application&) = delete;
|
Application(const Application&) = delete;
|
||||||
Application& operator=(const Application&) = delete;
|
Application& operator=(const Application&) = delete;
|
||||||
|
|
||||||
Application(const Config& config);
|
explicit Application(const Config& config);
|
||||||
virtual ~Application();
|
virtual ~Application();
|
||||||
|
|
||||||
void run();
|
void run();
|
||||||
@@ -40,15 +43,15 @@ class Application {
|
|||||||
virtual void onRender() {}
|
virtual void onRender() {}
|
||||||
virtual void onDestroy() {}
|
virtual void onDestroy() {}
|
||||||
|
|
||||||
|
AssetRegistry& assets() { return this->assetRegistry; }
|
||||||
|
PanelRegistry& panels() { return this->panelRegistry; }
|
||||||
|
|
||||||
|
DebugUI& debugUI() { return this->_debugUI; }
|
||||||
|
|
||||||
Canvas canvas;
|
Canvas canvas;
|
||||||
Camera2D camera;
|
Camera2D camera;
|
||||||
std::vector<std::string> args;
|
std::vector<std::string> args;
|
||||||
|
|
||||||
// Creates a mesh that persists across frames, for callers that want to
|
|
||||||
// draw the same geometry many times with different transforms (e.g.
|
|
||||||
// instancing) instead of resubmitting vertices every frame via Canvas.
|
|
||||||
MeshHandle createMesh(std::vector<CanvasVertex> vertices,
|
|
||||||
std::vector<u32> indices);
|
|
||||||
void drawMesh(MeshHandle handle, const glm::mat4& transform);
|
void drawMesh(MeshHandle handle, const glm::mat4& transform);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -59,6 +62,11 @@ class Application {
|
|||||||
|
|
||||||
class Impl;
|
class Impl;
|
||||||
std::unique_ptr<Impl> impl;
|
std::unique_ptr<Impl> impl;
|
||||||
|
|
||||||
|
AssetRegistry assetRegistry;
|
||||||
|
PanelRegistry panelRegistry;
|
||||||
|
|
||||||
|
DebugUI _debugUI;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <sophia/handles.hpp>
|
||||||
|
#include <sophia/ui/panel_registry.hpp>
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
class DebugUI {
|
||||||
|
public:
|
||||||
|
DebugUI(const DebugUI&) = delete;
|
||||||
|
DebugUI& operator=(const DebugUI&) = delete;
|
||||||
|
|
||||||
|
explicit DebugUI(PanelRegistry& registry);
|
||||||
|
~DebugUI() = default;
|
||||||
|
|
||||||
|
void show();
|
||||||
|
void hide();
|
||||||
|
|
||||||
|
void showPanelRegistry(bool show = true);
|
||||||
|
void showFrameStats(bool show = true);
|
||||||
|
|
||||||
|
private:
|
||||||
|
PanelRegistry& registry;
|
||||||
|
|
||||||
|
PanelHandle panelRegistryHandle;
|
||||||
|
PanelHandle frameStatsHandle;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <sophia/frame_info.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
class Panel {
|
||||||
|
public:
|
||||||
|
Panel(const Panel&) = delete;
|
||||||
|
Panel& operator=(const Panel&) = delete;
|
||||||
|
|
||||||
|
Panel(Panel&&) noexcept = default;
|
||||||
|
Panel& operator=(Panel&&) noexcept = default;
|
||||||
|
|
||||||
|
explicit Panel(std::string name = {}) : name{std::move(name)} {}
|
||||||
|
virtual ~Panel() = default;
|
||||||
|
|
||||||
|
virtual void onUpdate(const FrameInfo& frameInfo) = 0;
|
||||||
|
|
||||||
|
const std::string& getName() const { return name; }
|
||||||
|
|
||||||
|
bool isVisible() const { return this->visible; }
|
||||||
|
void setVisible(bool visible) { this->visible = visible; }
|
||||||
|
void toggleVisible() { this->visible = !this->visible; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool visible = true;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string name;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <sophia/frame_info.hpp>
|
||||||
|
#include <sophia/handles.hpp>
|
||||||
|
#include <sophia/ui/panel.hpp>
|
||||||
|
#include <sophia/util/assert.hpp>
|
||||||
|
#include <string_view>
|
||||||
|
#include <typeinfo>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
class PanelRegistry {
|
||||||
|
public:
|
||||||
|
PanelRegistry(const PanelRegistry&) = delete;
|
||||||
|
PanelRegistry& operator=(const PanelRegistry&) = delete;
|
||||||
|
|
||||||
|
PanelRegistry() = default;
|
||||||
|
~PanelRegistry() = default;
|
||||||
|
|
||||||
|
PanelHandle add(std::unique_ptr<Panel> panel);
|
||||||
|
|
||||||
|
template <typename T, typename... Args>
|
||||||
|
PanelHandle emplace(Args&&... args) {
|
||||||
|
return add(std::make_unique<T>(std::forward<Args>(args)...));
|
||||||
|
}
|
||||||
|
|
||||||
|
const Panel& get(PanelHandle handle) const;
|
||||||
|
Panel& get(PanelHandle handle) {
|
||||||
|
return const_cast<Panel&>(std::as_const(*this).get(handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
const T& get(PanelHandle handle) const {
|
||||||
|
const Panel& panel = get(handle);
|
||||||
|
SOPHIA_ASSERT(dynamic_cast<const T*>(&panel) != nullptr,
|
||||||
|
"PanelHandle does not refer to a", typeid(T).name());
|
||||||
|
return static_cast<const T&>(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
T& get(PanelHandle handle) {
|
||||||
|
return const_cast<T&>(std::as_const(*this).template get<T>(handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove(PanelHandle handle);
|
||||||
|
bool isValid(PanelHandle handle) const;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
const T* find() const {
|
||||||
|
for (const Slot& slot : slots) {
|
||||||
|
if (slot.panel) {
|
||||||
|
if (const T* panel = dynamic_cast<const T*>(slot.panel.get())) {
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
T* find() {
|
||||||
|
return const_cast<T*>(std::as_const(*this).template find<T>());
|
||||||
|
}
|
||||||
|
|
||||||
|
const Panel* find(std::string_view name) const {
|
||||||
|
for (const Slot& slot : slots) {
|
||||||
|
if (slot.panel && slot.panel->getName() == name) {
|
||||||
|
return slot.panel.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Panel* find(std::string_view name) {
|
||||||
|
return const_cast<Panel*>(std::as_const(*this).find(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 size() const;
|
||||||
|
u32 visibleCount() const;
|
||||||
|
|
||||||
|
std::vector<std::string_view> names() const {
|
||||||
|
std::vector<std::string_view> result;
|
||||||
|
result.reserve(slots.size());
|
||||||
|
for (const Slot& slot : slots) {
|
||||||
|
if (slot.panel) {
|
||||||
|
result.push_back(slot.panel->getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(const FrameInfo& frameInfo);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Slot {
|
||||||
|
std::unique_ptr<Panel> panel;
|
||||||
|
u32 generation = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Slot> slots;
|
||||||
|
std::vector<u32> freeList;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -4,6 +4,10 @@
|
|||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr u32 GENERATION_MASK = (1u << 12) - 1;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
MeshPool::MeshPool(Device& device) : device{device} {}
|
MeshPool::MeshPool(Device& device) : device{device} {}
|
||||||
|
|
||||||
MeshPool::~MeshPool() = default;
|
MeshPool::~MeshPool() = default;
|
||||||
@@ -12,30 +16,43 @@ MeshHandle MeshPool::create(const Mesh::Builder& builder) {
|
|||||||
auto mesh = std::make_unique<Mesh>(device, builder);
|
auto mesh = std::make_unique<Mesh>(device, builder);
|
||||||
|
|
||||||
if (!freeList.empty()) {
|
if (!freeList.empty()) {
|
||||||
MeshHandle handle = freeList.back();
|
u32 index = freeList.back();
|
||||||
freeList.pop_back();
|
freeList.pop_back();
|
||||||
meshes[handle] = std::move(mesh);
|
slots[index].mesh = std::move(mesh);
|
||||||
return handle;
|
return MeshHandle{index, slots[index].generation};
|
||||||
}
|
}
|
||||||
|
|
||||||
meshes.push_back(std::move(mesh));
|
slots.push_back(Slot{std::move(mesh), 0});
|
||||||
return static_cast<MeshHandle>(meshes.size() - 1);
|
return MeshHandle{static_cast<u32>(slots.size() - 1), 0};
|
||||||
}
|
}
|
||||||
|
|
||||||
const Mesh& MeshPool::get(MeshHandle handle) const {
|
const Mesh& MeshPool::get(MeshHandle handle) const {
|
||||||
SOPHIA_ASSERT(handle < meshes.size(), "invalid MeshHandle:", handle);
|
SOPHIA_ASSERT(handle.isValid(), "invalid MeshHandle");
|
||||||
SOPHIA_ASSERT(meshes[handle] != nullptr,
|
SOPHIA_ASSERT(handle.index < slots.size(),
|
||||||
"MeshHandle refers to a released mesh:", handle);
|
"MeshHandle index out of range:", handle.index);
|
||||||
return *meshes[handle];
|
|
||||||
|
const Slot& slot = slots[handle.index];
|
||||||
|
SOPHIA_ASSERT(slot.generation == handle.generation,
|
||||||
|
"stale MeshHandle (generation mismatch):", handle.index);
|
||||||
|
SOPHIA_ASSERT(slot.mesh != nullptr,
|
||||||
|
"MeshHandle refers to a released mesh:", handle.index);
|
||||||
|
return *slot.mesh;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MeshPool::release(MeshHandle handle) {
|
void MeshPool::release(MeshHandle handle) {
|
||||||
SOPHIA_ASSERT(handle < meshes.size(), "invalid MeshHandle:", handle);
|
SOPHIA_ASSERT(handle.isValid(), "invalid MeshHandle");
|
||||||
SOPHIA_ASSERT(meshes[handle] != nullptr,
|
SOPHIA_ASSERT(handle.index < slots.size(),
|
||||||
"MeshHandle already released:", handle);
|
"MeshHandle index out of range:", handle.index);
|
||||||
|
|
||||||
meshes[handle].reset();
|
Slot& slot = slots[handle.index];
|
||||||
freeList.push_back(handle);
|
SOPHIA_ASSERT(slot.generation == handle.generation,
|
||||||
|
"stale MeshHandle (generation mismatch):", handle.index);
|
||||||
|
SOPHIA_ASSERT(slot.mesh != nullptr,
|
||||||
|
"MeshHandle already released:", handle.index);
|
||||||
|
|
||||||
|
slot.mesh.reset();
|
||||||
|
slot.generation = (slot.generation + 1) & GENERATION_MASK;
|
||||||
|
freeList.push_back(handle.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -21,10 +21,15 @@ class MeshPool {
|
|||||||
void release(MeshHandle handle);
|
void release(MeshHandle handle);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct Slot {
|
||||||
|
std::unique_ptr<Mesh> mesh;
|
||||||
|
u32 generation = 0;
|
||||||
|
};
|
||||||
|
|
||||||
Device& device;
|
Device& device;
|
||||||
|
|
||||||
std::vector<std::unique_ptr<Mesh>> meshes;
|
std::vector<Slot> slots;
|
||||||
std::vector<MeshHandle> freeList;
|
std::vector<u32> freeList;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#include "imgui_renderer.hpp"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <imgui_impl_glfw.h>
|
||||||
|
#include <imgui_impl_vulkan.h>
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
ImGuiRenderer::ImGuiRenderer(Window& window, Device& device,
|
||||||
|
vk::Format colorAttachmentFormat,
|
||||||
|
u32 minImageCount) {
|
||||||
|
// 1. Construct ImGui context
|
||||||
|
IMGUI_CHECKVERSION();
|
||||||
|
ImGui::CreateContext();
|
||||||
|
|
||||||
|
ImGui_ImplGlfw_InitForVulkan(window.getGLFWwindow(), true);
|
||||||
|
|
||||||
|
ImGui_ImplVulkan_InitInfo initInfo = {};
|
||||||
|
device.populateImGuiInitInfo(initInfo);
|
||||||
|
initInfo.DescriptorPoolSize =
|
||||||
|
IMGUI_IMPL_VULKAN_MINIMUM_SAMPLED_IMAGE_POOL_SIZE + 5;
|
||||||
|
initInfo.MinImageCount = minImageCount;
|
||||||
|
initInfo.ImageCount = minImageCount;
|
||||||
|
|
||||||
|
VkFormat rawColorAttachmentFormat =
|
||||||
|
static_cast<VkFormat>(colorAttachmentFormat);
|
||||||
|
initInfo.UseDynamicRendering = true;
|
||||||
|
initInfo.PipelineInfoMain.PipelineRenderingCreateInfo.sType =
|
||||||
|
VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR;
|
||||||
|
initInfo.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount =
|
||||||
|
1;
|
||||||
|
initInfo.PipelineInfoMain.PipelineRenderingCreateInfo
|
||||||
|
.pColorAttachmentFormats = &rawColorAttachmentFormat;
|
||||||
|
|
||||||
|
ImGui_ImplVulkan_Init(&initInfo);
|
||||||
|
|
||||||
|
// 2. Enable dark theme
|
||||||
|
ImGui::StyleColorsDark();
|
||||||
|
|
||||||
|
// 3. Enable docking / keyboard
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||||
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||||
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When viewports are enabled we tweak WindowRounding/WindowBg so
|
||||||
|
* platform windows can look identical to regular ones.
|
||||||
|
*/
|
||||||
|
ImGuiStyle& style = ImGui::GetStyle();
|
||||||
|
if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
||||||
|
style.WindowRounding = 0.0f;
|
||||||
|
style.Colors[ImGuiCol_WindowBg].w = 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiRenderer::~ImGuiRenderer() {
|
||||||
|
ImGui_ImplVulkan_Shutdown();
|
||||||
|
ImGui_ImplGlfw_Shutdown();
|
||||||
|
ImGui::DestroyContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGuiRenderer::beginFrame() {
|
||||||
|
ImGui_ImplVulkan_NewFrame();
|
||||||
|
ImGui_ImplGlfw_NewFrame();
|
||||||
|
ImGui::NewFrame();
|
||||||
|
|
||||||
|
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(),
|
||||||
|
ImGuiDockNodeFlags_PassthruCentralNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGuiRenderer::endFrame(vk::CommandBuffer commandBuffer) {
|
||||||
|
ImGui::Render();
|
||||||
|
ImDrawData* mainDrawData = ImGui::GetDrawData();
|
||||||
|
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
||||||
|
ImGui::UpdatePlatformWindows();
|
||||||
|
ImGui::RenderPlatformWindowsDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui_ImplVulkan_RenderDrawData(mainDrawData, commandBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <sophia/types.hpp>
|
||||||
|
|
||||||
|
#include "device.hpp"
|
||||||
|
#include "window.hpp"
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
class ImGuiRenderer {
|
||||||
|
public:
|
||||||
|
ImGuiRenderer(const ImGuiRenderer&) = delete;
|
||||||
|
ImGuiRenderer& operator=(const ImGuiRenderer&) = delete;
|
||||||
|
|
||||||
|
ImGuiRenderer(Window& window, Device& device,
|
||||||
|
vk::Format colorAttachmentFormat, u32 minImageCount);
|
||||||
|
~ImGuiRenderer();
|
||||||
|
|
||||||
|
void beginFrame();
|
||||||
|
void endFrame(vk::CommandBuffer commandBuffer);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -45,6 +45,9 @@ class RenderContext {
|
|||||||
vk::Format getImageFormat() const {
|
vk::Format getImageFormat() const {
|
||||||
return this->swapchain->getImageFormat();
|
return this->swapchain->getImageFormat();
|
||||||
}
|
}
|
||||||
|
u32 getImageCount() const {
|
||||||
|
return static_cast<u32>(this->swapchain->imageCount());
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void recreateSwapchain();
|
void recreateSwapchain();
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#include <sophia/ui/debug_ui.hpp>
|
||||||
|
|
||||||
|
#include "panels/frame_stats_panel.hpp"
|
||||||
|
#include "panels/panel_registry_panel.hpp"
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
DebugUI::DebugUI(PanelRegistry& registry) : registry{registry} {
|
||||||
|
// #ifndef NDEBUG
|
||||||
|
this->panelRegistryHandle = registry.emplace<PanelRegistryPanel>(registry);
|
||||||
|
this->frameStatsHandle = registry.emplace<FrameStatsPanel>();
|
||||||
|
// #endif // !NDEBUG
|
||||||
|
}
|
||||||
|
|
||||||
|
void DebugUI::show() {
|
||||||
|
showPanelRegistry(true);
|
||||||
|
showFrameStats(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DebugUI::hide() {
|
||||||
|
showPanelRegistry(false);
|
||||||
|
showFrameStats(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DebugUI::showPanelRegistry(bool show) {
|
||||||
|
if (registry.isValid(panelRegistryHandle)) {
|
||||||
|
registry.get(panelRegistryHandle).setVisible(show);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DebugUI::showFrameStats(bool show) {
|
||||||
|
if (registry.isValid(frameStatsHandle)) {
|
||||||
|
registry.get(frameStatsHandle).setVisible(show);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#include <sophia/ui/panel_registry.hpp>
|
||||||
|
#include <sophia/util/assert.hpp>
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr u32 GENERATION_MASK = (1u << 12) - 1;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
PanelHandle PanelRegistry::add(std::unique_ptr<Panel> panel) {
|
||||||
|
if (!freeList.empty()) {
|
||||||
|
u32 index = freeList.back();
|
||||||
|
freeList.pop_back();
|
||||||
|
slots[index].panel = std::move(panel);
|
||||||
|
return PanelHandle{index, slots[index].generation};
|
||||||
|
}
|
||||||
|
|
||||||
|
slots.push_back(Slot{std::move(panel), 0});
|
||||||
|
return PanelHandle{static_cast<u32>(slots.size() - 1), 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
const Panel& PanelRegistry::get(PanelHandle handle) const {
|
||||||
|
SOPHIA_ASSERT(handle.isValid(), "invalid PanelHandle");
|
||||||
|
SOPHIA_ASSERT(handle.index < slots.size(),
|
||||||
|
"PanelHandle index out of range:", handle.index);
|
||||||
|
|
||||||
|
const Slot& slot = slots[handle.index];
|
||||||
|
SOPHIA_ASSERT(slot.generation == handle.generation,
|
||||||
|
"stale PanelHandle (generation mismatch):", handle.index);
|
||||||
|
SOPHIA_ASSERT(slot.panel != nullptr,
|
||||||
|
"PanelHandle refers to a removed panel:", handle.index);
|
||||||
|
return *slot.panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PanelRegistry::remove(PanelHandle handle) {
|
||||||
|
SOPHIA_ASSERT(handle.isValid(), "invalid PanelHandle");
|
||||||
|
SOPHIA_ASSERT(handle.index < slots.size(),
|
||||||
|
"PanelHandle index out of range:", handle.index);
|
||||||
|
|
||||||
|
Slot& slot = slots[handle.index];
|
||||||
|
SOPHIA_ASSERT(slot.generation == handle.generation,
|
||||||
|
"stale PanelHandle (generation mismatch):", handle.index);
|
||||||
|
SOPHIA_ASSERT(slot.panel != nullptr,
|
||||||
|
"PanelHandle already removed:", handle.index);
|
||||||
|
|
||||||
|
slot.panel.reset();
|
||||||
|
slot.generation = (slot.generation + 1) & GENERATION_MASK;
|
||||||
|
freeList.push_back(handle.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PanelRegistry::isValid(PanelHandle handle) const {
|
||||||
|
if (!handle.isValid() || handle.index >= slots.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Slot& slot = slots[handle.index];
|
||||||
|
return slot.generation == handle.generation && slot.panel != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 PanelRegistry::size() const {
|
||||||
|
u32 count = 0;
|
||||||
|
for (const Slot& slot : slots) {
|
||||||
|
if (slot.panel) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 PanelRegistry::visibleCount() const {
|
||||||
|
u32 count = 0;
|
||||||
|
for (const Slot& slot : slots) {
|
||||||
|
if (slot.panel && slot.panel->isVisible()) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PanelRegistry::update(const FrameInfo& frameInfo) {
|
||||||
|
for (auto& slot : slots) {
|
||||||
|
if (slot.panel && slot.panel->isVisible()) {
|
||||||
|
slot.panel->onUpdate(frameInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#include "ecs_inspector_panel.hpp"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include <sophia/components.hpp>
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
void EcsInspectorPanel::onUpdate(const FrameInfo&) {
|
||||||
|
ImGui::Begin("ECS Inspector");
|
||||||
|
|
||||||
|
entityScratch.clear();
|
||||||
|
for (entt::entity entity : registry.storage<entt::entity>()) {
|
||||||
|
entityScratch.push_back(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::BeginChild("EntityList", ImVec2(0, 200), true);
|
||||||
|
ImGuiListClipper clipper;
|
||||||
|
clipper.Begin(static_cast<int>(entityScratch.size()));
|
||||||
|
while (clipper.Step()) {
|
||||||
|
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) {
|
||||||
|
entt::entity entity = entityScratch[static_cast<size_t>(i)];
|
||||||
|
const TagComponent* tag = registry.try_get<TagComponent>(entity);
|
||||||
|
std::string label = tag ? tag->tag : "Entity";
|
||||||
|
|
||||||
|
ImGui::PushID(static_cast<int>(entt::to_integral(entity)));
|
||||||
|
if (ImGui::Selectable(label.c_str(), selected == entity)) {
|
||||||
|
selected = entity;
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
if (registry.valid(selected)) {
|
||||||
|
if (auto* transform = registry.try_get<TransformComponent>(selected)) {
|
||||||
|
ImGui::DragFloat3("Position", &transform->position.x, 0.1f);
|
||||||
|
ImGui::DragFloat3("Scale", &transform->scale.x, 0.1f);
|
||||||
|
ImGui::Text("Rotation: %.2f, %.2f, %.2f, %.2f", transform->rotation.w,
|
||||||
|
transform->rotation.x, transform->rotation.y,
|
||||||
|
transform->rotation.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto* mesh = registry.try_get<MeshComponent>(selected)) {
|
||||||
|
ImGui::Text("Mesh: index=%u generation=%u", mesh->handle.index,
|
||||||
|
mesh->handle.generation);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ImGui::TextDisabled("No entity selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <entt/entt.hpp>
|
||||||
|
#include <sophia/frame_info.hpp>
|
||||||
|
#include <sophia/ui/panel.hpp>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
class EcsInspectorPanel : public Panel {
|
||||||
|
public:
|
||||||
|
explicit EcsInspectorPanel(entt::registry& registry)
|
||||||
|
: Panel{"ECS Inspector"}, registry{registry} {}
|
||||||
|
|
||||||
|
void onUpdate(const FrameInfo& frameInfo) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
entt::registry& registry;
|
||||||
|
entt::entity selected = entt::null;
|
||||||
|
std::vector<entt::entity> entityScratch;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include <sophia/frame_info.hpp>
|
||||||
|
#include <sophia/ui/panel.hpp>
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
class FrameStatsPanel : public Panel {
|
||||||
|
public:
|
||||||
|
FrameStatsPanel() : Panel{"Frame Stats"} {}
|
||||||
|
|
||||||
|
void onUpdate(const FrameInfo& frameInfo) override {
|
||||||
|
frameCount++;
|
||||||
|
accumulatedTime += frameInfo.deltaTime;
|
||||||
|
|
||||||
|
if (frameInfo.elapsedTime >= nextRefreshTime) {
|
||||||
|
displayedFps = static_cast<double>(frameCount) / accumulatedTime;
|
||||||
|
displayedFrameTimeMs =
|
||||||
|
(accumulatedTime / static_cast<double>(frameCount)) * 1000.0;
|
||||||
|
|
||||||
|
frameCount = 0;
|
||||||
|
accumulatedTime = 0.0;
|
||||||
|
nextRefreshTime = frameInfo.elapsedTime + REFRESH_INTERVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Begin("Frame Stats");
|
||||||
|
ImGui::Text("FPS: %.1f", displayedFps);
|
||||||
|
ImGui::Text("Frame time: %.3f ms", displayedFrameTimeMs);
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr double REFRESH_INTERVAL = 1.0;
|
||||||
|
|
||||||
|
double accumulatedTime = 0.0;
|
||||||
|
u32 frameCount = 0;
|
||||||
|
double nextRefreshTime = 0.0;
|
||||||
|
double displayedFps = 0.0;
|
||||||
|
double displayedFrameTimeMs = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#include "panel_registry_panel.hpp"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
void PanelRegistryPanel::onUpdate(const FrameInfo&) {
|
||||||
|
ImGui::Begin("PanelRegistry");
|
||||||
|
ImGui::Text("%u panels (%u visible)", registry.size(),
|
||||||
|
registry.visibleCount());
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
for (std::string_view name : registry.names()) {
|
||||||
|
Panel* panel = registry.find(name);
|
||||||
|
if (!panel || panel == this) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool visible = panel->isVisible();
|
||||||
|
if (ImGui::Checkbox(name.data(), &visible)) {
|
||||||
|
panel->setVisible(visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <sophia/frame_info.hpp>
|
||||||
|
#include <sophia/ui/panel.hpp>
|
||||||
|
#include <sophia/ui/panel_registry.hpp>
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
class PanelRegistryPanel : public Panel {
|
||||||
|
public:
|
||||||
|
explicit PanelRegistryPanel(PanelRegistry& registry)
|
||||||
|
: Panel{"PanelRegistry"}, registry{registry} {}
|
||||||
|
|
||||||
|
void onUpdate(const FrameInfo& frameInfo) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
PanelRegistry& registry;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
+5
-1
@@ -5,7 +5,11 @@ class Testbed : public sophia::Application {
|
|||||||
public:
|
public:
|
||||||
Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {}
|
Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {}
|
||||||
|
|
||||||
void onCreate() override { std::cout << "Testbed created" << std::endl; }
|
void onCreate() override {
|
||||||
|
std::cout << "Testbed created" << std::endl;
|
||||||
|
|
||||||
|
debugUI().show();
|
||||||
|
}
|
||||||
|
|
||||||
void onUpdate(const sophia::FrameInfo& frameInfo) override {}
|
void onUpdate(const sophia::FrameInfo& frameInfo) override {}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user