This commit is contained in:
John McCardle 2026-01-25 21:04:01 -05:00
commit 486087b9cb
20 changed files with 2438 additions and 114 deletions

View file

@ -12,6 +12,10 @@
#include <cmath>
#include <Python.h>
// Static member definitions for shader intermediate texture (#106)
std::unique_ptr<sf::RenderTexture> GameEngine::shaderIntermediate;
bool GameEngine::shaderIntermediateInitialized = false;
// #219 - FrameLock implementation for thread-safe UI updates
void FrameLock::acquire() {
@ -718,6 +722,37 @@ sf::Vector2f GameEngine::windowToGameCoords(const sf::Vector2f& windowPos) const
return render_target->mapPixelToCoords(sf::Vector2i(windowPos), gameView);
}
// #106 - Shader intermediate texture: shared texture for shader rendering
void GameEngine::initShaderIntermediate(unsigned int width, unsigned int height) {
if (!sf::Shader::isAvailable()) {
std::cerr << "GameEngine: Shaders not available, skipping intermediate texture init" << std::endl;
return;
}
if (!shaderIntermediate) {
shaderIntermediate = std::make_unique<sf::RenderTexture>();
}
if (!shaderIntermediate->create(width, height)) {
std::cerr << "GameEngine: Failed to create shader intermediate texture ("
<< width << "x" << height << ")" << std::endl;
shaderIntermediate.reset();
shaderIntermediateInitialized = false;
return;
}
shaderIntermediate->setSmooth(false); // Pixel-perfect rendering
shaderIntermediateInitialized = true;
}
sf::RenderTexture& GameEngine::getShaderIntermediate() {
if (!shaderIntermediateInitialized) {
// Initialize with default resolution if not already done
initShaderIntermediate(1024, 768);
}
return *shaderIntermediate;
}
// #153 - Headless simulation control: step() advances simulation time
float GameEngine::step(float dt) {
// In windowed mode, step() is a no-op

View file

@ -185,6 +185,10 @@ private:
sf::View gameView; // View for the game content
ViewportMode viewportMode = ViewportMode::Fit;
// Shader intermediate texture (#106) - shared texture for shader rendering
static std::unique_ptr<sf::RenderTexture> shaderIntermediate;
static bool shaderIntermediateInitialized;
// Profiling overlay
bool showProfilerOverlay = false; // F3 key toggles this
int overlayUpdateCounter = 0; // Only update overlay every N frames
@ -257,6 +261,11 @@ public:
std::string getViewportModeString() const;
sf::Vector2f windowToGameCoords(const sf::Vector2f& windowPos) const;
// Shader system (#106) - shared intermediate texture for shader rendering
static sf::RenderTexture& getShaderIntermediate();
static void initShaderIntermediate(unsigned int width, unsigned int height);
static bool isShaderIntermediateReady() { return shaderIntermediateInitialized; }
// #153 - Headless simulation control
float step(float dt = -1.0f); // Advance simulation; dt<0 means advance to next event
int getSimulationTime() const { return simulation_time; }

View file

@ -27,6 +27,9 @@
#include "PyNoiseSource.h" // Procedural generation noise (#207-208)
#include "PyLock.h" // Thread synchronization (#219)
#include "PyVector.h" // For bresenham Vector support (#215)
#include "PyShader.h" // Shader support (#106)
#include "PyUniformBinding.h" // Shader uniform bindings (#106)
#include "PyUniformCollection.h" // Shader uniform collection (#106)
#include "McRogueFaceVersion.h"
#include "GameEngine.h"
#include "ImGuiConsole.h"
@ -452,6 +455,11 @@ PyObject* PyInit_mcrfpy()
&mcrfpydef::PyBSPType,
&mcrfpydef::PyNoiseSourceType,
/*shaders (#106)*/
&mcrfpydef::PyShaderType,
&mcrfpydef::PyPropertyBindingType,
&mcrfpydef::PyCallableBindingType,
nullptr};
// Types that are used internally but NOT exported to module namespace (#189)
@ -473,6 +481,9 @@ PyObject* PyInit_mcrfpy()
&mcrfpydef::PyBSPAdjacencyType, // #210: BSP.adjacency wrapper
&mcrfpydef::PyBSPAdjacentTilesType, // #210: BSPNode.adjacent_tiles wrapper
/*shader uniform collection - returned by drawable.uniforms but not directly instantiable (#106)*/
&mcrfpydef::PyUniformCollectionType,
nullptr};
// Set up PyWindowType methods and getsetters before PyType_Ready
@ -497,6 +508,17 @@ PyObject* PyInit_mcrfpy()
mcrfpydef::PyNoiseSourceType.tp_methods = PyNoiseSource::methods;
mcrfpydef::PyNoiseSourceType.tp_getset = PyNoiseSource::getsetters;
// Set up PyShaderType methods and getsetters (#106)
mcrfpydef::PyShaderType.tp_methods = PyShader::methods;
mcrfpydef::PyShaderType.tp_getset = PyShader::getsetters;
// Set up PyPropertyBindingType and PyCallableBindingType getsetters (#106)
mcrfpydef::PyPropertyBindingType.tp_getset = PyPropertyBindingType::getsetters;
mcrfpydef::PyCallableBindingType.tp_getset = PyCallableBindingType::getsetters;
// Set up PyUniformCollectionType methods (#106)
mcrfpydef::PyUniformCollectionType.tp_methods = ::PyUniformCollectionType::methods;
// Set up weakref support for all types that need it
PyTimerType.tp_weaklistoffset = offsetof(PyTimerObject, weakreflist);
PyUIFrameType.tp_weaklistoffset = offsetof(PyUIFrameObject, weakreflist);

256
src/PyShader.cpp Normal file
View file

@ -0,0 +1,256 @@
#include "PyShader.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
#include "GameEngine.h"
#include "Resources.h"
#include <sstream>
// Static clock for time uniform
static sf::Clock shader_engine_clock;
static sf::Clock shader_frame_clock;
// Python method and getset definitions
PyGetSetDef PyShader::getsetters[] = {
{"dynamic", (getter)PyShader::get_dynamic, (setter)PyShader::set_dynamic,
MCRF_PROPERTY(dynamic,
"Whether this shader uses time-varying effects (bool). "
"Dynamic shaders invalidate parent caches each frame."), NULL},
{"source", (getter)PyShader::get_source, NULL,
MCRF_PROPERTY(source,
"The GLSL fragment shader source code (str, read-only)."), NULL},
{"is_valid", (getter)PyShader::get_is_valid, NULL,
MCRF_PROPERTY(is_valid,
"True if the shader compiled successfully (bool, read-only)."), NULL},
{NULL}
};
PyMethodDef PyShader::methods[] = {
{"set_uniform", (PyCFunction)PyShader::set_uniform, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(Shader, set_uniform,
MCRF_SIG("(name: str, value: float|tuple)", "None"),
MCRF_DESC("Set a custom uniform value on this shader."),
MCRF_ARGS_START
MCRF_ARG("name", "Uniform variable name in the shader")
MCRF_ARG("value", "Float, vec2 (2-tuple), vec3 (3-tuple), or vec4 (4-tuple)")
MCRF_RAISES("ValueError", "If uniform type cannot be determined")
MCRF_NOTE("Engine uniforms (time, resolution, etc.) are set automatically")
)},
{NULL}
};
// Constructor
PyObject* PyShader::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
{
PyShaderObject* self = (PyShaderObject*)type->tp_alloc(type, 0);
if (self) {
self->shader = nullptr;
self->dynamic = false;
self->weakreflist = NULL;
new (&self->fragment_source) std::string();
}
return (PyObject*)self;
}
// Destructor
void PyShader::dealloc(PyShaderObject* self)
{
// Clear weak references
if (self->weakreflist) {
PyObject_ClearWeakRefs((PyObject*)self);
}
// Destroy C++ objects
self->shader.reset();
self->fragment_source.~basic_string();
// Free Python object
Py_TYPE(self)->tp_free((PyObject*)self);
}
// Initializer
int PyShader::init(PyShaderObject* self, PyObject* args, PyObject* kwds)
{
static const char* keywords[] = {"fragment_source", "dynamic", nullptr};
const char* source = nullptr;
int dynamic = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|p", const_cast<char**>(keywords),
&source, &dynamic)) {
return -1;
}
// Check if shaders are available
if (!sf::Shader::isAvailable()) {
PyErr_SetString(PyExc_RuntimeError,
"Shaders are not available on this system (no GPU support or OpenGL too old)");
return -1;
}
// Store source and dynamic flag
self->fragment_source = source;
self->dynamic = (bool)dynamic;
// Create and compile the shader
self->shader = std::make_shared<sf::Shader>();
// Capture sf::err() output during shader compilation
std::streambuf* oldBuf = sf::err().rdbuf();
std::ostringstream errStream;
sf::err().rdbuf(errStream.rdbuf());
bool success = self->shader->loadFromMemory(source, sf::Shader::Fragment);
// Restore sf::err() and check for errors
sf::err().rdbuf(oldBuf);
if (!success) {
std::string error_msg = errStream.str();
if (error_msg.empty()) {
error_msg = "Shader compilation failed (unknown error)";
}
PyErr_Format(PyExc_ValueError, "Shader compilation failed: %s", error_msg.c_str());
self->shader.reset();
return -1;
}
return 0;
}
// Repr
PyObject* PyShader::repr(PyObject* obj)
{
PyShaderObject* self = (PyShaderObject*)obj;
std::ostringstream ss;
ss << "<Shader";
if (self->shader) {
ss << " valid";
} else {
ss << " invalid";
}
if (self->dynamic) {
ss << " dynamic";
}
ss << ">";
return PyUnicode_FromString(ss.str().c_str());
}
// Property: dynamic
PyObject* PyShader::get_dynamic(PyShaderObject* self, void* closure)
{
return PyBool_FromLong(self->dynamic);
}
int PyShader::set_dynamic(PyShaderObject* self, PyObject* value, void* closure)
{
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "dynamic must be a boolean");
return -1;
}
self->dynamic = PyObject_IsTrue(value);
return 0;
}
// Property: source (read-only)
PyObject* PyShader::get_source(PyShaderObject* self, void* closure)
{
return PyUnicode_FromString(self->fragment_source.c_str());
}
// Property: is_valid (read-only)
PyObject* PyShader::get_is_valid(PyShaderObject* self, void* closure)
{
return PyBool_FromLong(self->shader != nullptr);
}
// Method: set_uniform
PyObject* PyShader::set_uniform(PyShaderObject* self, PyObject* args, PyObject* kwds)
{
static const char* keywords[] = {"name", "value", nullptr};
const char* name = nullptr;
PyObject* value = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sO", const_cast<char**>(keywords),
&name, &value)) {
return NULL;
}
if (!self->shader) {
PyErr_SetString(PyExc_RuntimeError, "Shader is not valid");
return NULL;
}
// Determine the type and set uniform
if (PyFloat_Check(value) || PyLong_Check(value)) {
// Single float
float f = (float)PyFloat_AsDouble(value);
if (PyErr_Occurred()) return NULL;
self->shader->setUniform(name, f);
}
else if (PyTuple_Check(value)) {
Py_ssize_t size = PyTuple_Size(value);
if (size == 2) {
// vec2
float x = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 0));
float y = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 1));
if (PyErr_Occurred()) return NULL;
self->shader->setUniform(name, sf::Glsl::Vec2(x, y));
}
else if (size == 3) {
// vec3
float x = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 0));
float y = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 1));
float z = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 2));
if (PyErr_Occurred()) return NULL;
self->shader->setUniform(name, sf::Glsl::Vec3(x, y, z));
}
else if (size == 4) {
// vec4
float x = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 0));
float y = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 1));
float z = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 2));
float w = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 3));
if (PyErr_Occurred()) return NULL;
self->shader->setUniform(name, sf::Glsl::Vec4(x, y, z, w));
}
else {
PyErr_Format(PyExc_ValueError,
"Tuple must have 2, 3, or 4 elements for vec2/vec3/vec4, got %zd", size);
return NULL;
}
}
else {
PyErr_SetString(PyExc_TypeError,
"Uniform value must be a float or tuple of 2-4 floats");
return NULL;
}
Py_RETURN_NONE;
}
// Static helper: apply engine-provided uniforms
void PyShader::applyEngineUniforms(sf::Shader& shader, sf::Vector2f resolution)
{
// Time uniforms
shader.setUniform("time", shader_engine_clock.getElapsedTime().asSeconds());
shader.setUniform("delta_time", shader_frame_clock.restart().asSeconds());
// Resolution
shader.setUniform("resolution", resolution);
// Mouse position - get from GameEngine if available
sf::Vector2f mouse(0.f, 0.f);
if (Resources::game && !Resources::game->isHeadless()) {
sf::Vector2i mousePos = sf::Mouse::getPosition(Resources::game->getWindow());
mouse = sf::Vector2f(static_cast<float>(mousePos.x), static_cast<float>(mousePos.y));
}
shader.setUniform("mouse", mouse);
// CurrentTexture is handled by SFML automatically when drawing
shader.setUniform("texture", sf::Shader::CurrentTexture);
}
// Static helper: check availability
bool PyShader::isAvailable()
{
return sf::Shader::isAvailable();
}

94
src/PyShader.h Normal file
View file

@ -0,0 +1,94 @@
#pragma once
#include "Common.h"
#include "Python.h"
// Forward declarations
class UIDrawable;
// Python object structure for Shader
typedef struct PyShaderObjectStruct {
PyObject_HEAD
std::shared_ptr<sf::Shader> shader;
bool dynamic; // Time-varying shader (affects caching)
std::string fragment_source; // Source code for recompilation
PyObject* weakreflist; // Support weak references
} PyShaderObject;
class PyShader
{
public:
// Python type methods
static PyObject* repr(PyObject* self);
static int init(PyShaderObject* self, PyObject* args, PyObject* kwds);
static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds);
static void dealloc(PyShaderObject* self);
// Property getters/setters
static PyObject* get_dynamic(PyShaderObject* self, void* closure);
static int set_dynamic(PyShaderObject* self, PyObject* value, void* closure);
static PyObject* get_source(PyShaderObject* self, void* closure);
static PyObject* get_is_valid(PyShaderObject* self, void* closure);
// Methods
static PyObject* set_uniform(PyShaderObject* self, PyObject* args, PyObject* kwds);
// Static helper: apply engine-provided uniforms (time, resolution, etc.)
static void applyEngineUniforms(sf::Shader& shader, sf::Vector2f resolution);
// Check if shaders are available on this system
static bool isAvailable();
// Arrays for Python type definition
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
};
namespace mcrfpydef {
// Using inline to ensure single definition across translation units (C++17)
inline PyTypeObject PyShaderType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Shader",
.tp_basicsize = sizeof(PyShaderObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PyShader::dealloc,
.tp_repr = PyShader::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR(
"Shader(fragment_source: str, dynamic: bool = False)\n"
"\n"
"A GPU shader program for visual effects.\n"
"\n"
"Args:\n"
" fragment_source: GLSL fragment shader source code\n"
" dynamic: If True, shader uses time-varying effects and will\n"
" invalidate parent caches each frame\n"
"\n"
"Shaders enable GPU-accelerated visual effects like glow, distortion,\n"
"color manipulation, and more. Assign to drawable.shader to apply.\n"
"\n"
"Engine-provided uniforms (automatically available):\n"
" - float time: Seconds since engine start\n"
" - float delta_time: Seconds since last frame\n"
" - vec2 resolution: Texture size in pixels\n"
" - vec2 mouse: Mouse position in window coordinates\n"
"\n"
"Example:\n"
" shader = mcrfpy.Shader('''\n"
" uniform sampler2D texture;\n"
" uniform float time;\n"
" void main() {\n"
" vec2 uv = gl_TexCoord[0].xy;\n"
" vec4 color = texture2D(texture, uv);\n"
" color.rgb *= 0.5 + 0.5 * sin(time);\n"
" gl_FragColor = color;\n"
" }\n"
" ''', dynamic=True)\n"
" frame.shader = shader\n"
),
.tp_weaklistoffset = offsetof(PyShaderObject, weakreflist),
.tp_methods = nullptr, // Set in McRFPy_API.cpp before PyType_Ready
.tp_getset = nullptr, // Set in McRFPy_API.cpp before PyType_Ready
.tp_init = (initproc)PyShader::init,
.tp_new = PyShader::pynew,
};
}

341
src/PyUniformBinding.cpp Normal file
View file

@ -0,0 +1,341 @@
#include "PyUniformBinding.h"
#include "UIDrawable.h"
#include "UIFrame.h"
#include "UICaption.h"
#include "UISprite.h"
#include "UIGrid.h"
#include "UILine.h"
#include "UICircle.h"
#include "UIArc.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
#include <sstream>
#include <cstring>
// ============================================================================
// PropertyBinding Implementation
// ============================================================================
PropertyBinding::PropertyBinding(std::weak_ptr<UIDrawable> target, const std::string& property)
: target(target), property_name(property) {}
std::optional<float> PropertyBinding::evaluate() const {
auto ptr = target.lock();
if (!ptr) return std::nullopt;
float value = 0.0f;
if (ptr->getProperty(property_name, value)) {
return value;
}
return std::nullopt;
}
bool PropertyBinding::isValid() const {
auto ptr = target.lock();
if (!ptr) return false;
return ptr->hasProperty(property_name);
}
// ============================================================================
// CallableBinding Implementation
// ============================================================================
CallableBinding::CallableBinding(PyObject* callable)
: callable(callable) {
if (callable) {
Py_INCREF(callable);
}
}
CallableBinding::~CallableBinding() {
if (callable) {
Py_DECREF(callable);
}
}
CallableBinding::CallableBinding(CallableBinding&& other) noexcept
: callable(other.callable) {
other.callable = nullptr;
}
CallableBinding& CallableBinding::operator=(CallableBinding&& other) noexcept {
if (this != &other) {
if (callable) {
Py_DECREF(callable);
}
callable = other.callable;
other.callable = nullptr;
}
return *this;
}
std::optional<float> CallableBinding::evaluate() const {
if (!callable || !PyCallable_Check(callable)) {
return std::nullopt;
}
PyObject* result = PyObject_CallNoArgs(callable);
if (!result) {
// Python exception occurred - print and clear it
PyErr_Print();
return std::nullopt;
}
float value = 0.0f;
if (PyFloat_Check(result)) {
value = static_cast<float>(PyFloat_AsDouble(result));
} else if (PyLong_Check(result)) {
value = static_cast<float>(PyLong_AsDouble(result));
} else {
// Try to convert to float
PyObject* float_result = PyNumber_Float(result);
if (float_result) {
value = static_cast<float>(PyFloat_AsDouble(float_result));
Py_DECREF(float_result);
} else {
PyErr_Clear();
Py_DECREF(result);
return std::nullopt;
}
}
Py_DECREF(result);
return value;
}
bool CallableBinding::isValid() const {
return callable && PyCallable_Check(callable);
}
// ============================================================================
// PyPropertyBindingType Python Interface
// ============================================================================
PyGetSetDef PyPropertyBindingType::getsetters[] = {
{"target", (getter)PyPropertyBindingType::get_target, NULL,
MCRF_PROPERTY(target, "The drawable this binding reads from (read-only)."), NULL},
{"property", (getter)PyPropertyBindingType::get_property, NULL,
MCRF_PROPERTY(property, "The property name being read (str, read-only)."), NULL},
{"value", (getter)PyPropertyBindingType::get_value, NULL,
MCRF_PROPERTY(value, "Current value of the binding (float, read-only). Returns None if invalid."), NULL},
{"is_valid", (getter)PyPropertyBindingType::is_valid, NULL,
MCRF_PROPERTY(is_valid, "True if the binding target still exists and property is valid (bool, read-only)."), NULL},
{NULL}
};
PyObject* PyPropertyBindingType::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) {
PyPropertyBindingObject* self = (PyPropertyBindingObject*)type->tp_alloc(type, 0);
if (self) {
self->binding = nullptr;
self->weakreflist = NULL;
}
return (PyObject*)self;
}
void PyPropertyBindingType::dealloc(PyPropertyBindingObject* self) {
if (self->weakreflist) {
PyObject_ClearWeakRefs((PyObject*)self);
}
self->binding.reset();
Py_TYPE(self)->tp_free((PyObject*)self);
}
int PyPropertyBindingType::init(PyPropertyBindingObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"target", "property", nullptr};
PyObject* target_obj = nullptr;
const char* property = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "Os", const_cast<char**>(keywords),
&target_obj, &property)) {
return -1;
}
// Get the shared_ptr from the target drawable by checking the type name
// We can't use PyObject_IsInstance with static type objects from other translation units
// So we check the type name string instead
std::shared_ptr<UIDrawable> target_ptr;
const char* type_name = Py_TYPE(target_obj)->tp_name;
if (strcmp(type_name, "mcrfpy.Frame") == 0) {
target_ptr = ((PyUIFrameObject*)target_obj)->data;
} else if (strcmp(type_name, "mcrfpy.Caption") == 0) {
target_ptr = ((PyUICaptionObject*)target_obj)->data;
} else if (strcmp(type_name, "mcrfpy.Sprite") == 0) {
target_ptr = ((PyUISpriteObject*)target_obj)->data;
} else if (strcmp(type_name, "mcrfpy.Grid") == 0) {
target_ptr = ((PyUIGridObject*)target_obj)->data;
} else if (strcmp(type_name, "mcrfpy.Line") == 0) {
target_ptr = ((PyUILineObject*)target_obj)->data;
} else if (strcmp(type_name, "mcrfpy.Circle") == 0) {
target_ptr = ((PyUICircleObject*)target_obj)->data;
} else if (strcmp(type_name, "mcrfpy.Arc") == 0) {
target_ptr = ((PyUIArcObject*)target_obj)->data;
}
if (!target_ptr) {
PyErr_SetString(PyExc_TypeError,
"PropertyBinding requires a UIDrawable (Frame, Sprite, Caption, Grid, Line, Circle, or Arc)");
return -1;
}
// Validate that the property exists
if (!target_ptr->hasProperty(property)) {
PyErr_Format(PyExc_ValueError,
"Property '%s' is not a valid animatable property on this drawable", property);
return -1;
}
self->binding = std::make_shared<PropertyBinding>(target_ptr, property);
return 0;
}
PyObject* PyPropertyBindingType::repr(PyObject* obj) {
PyPropertyBindingObject* self = (PyPropertyBindingObject*)obj;
std::ostringstream ss;
ss << "<PropertyBinding";
if (self->binding) {
ss << " property='" << self->binding->getPropertyName() << "'";
if (self->binding->isValid()) {
auto val = self->binding->evaluate();
if (val) {
ss << " value=" << *val;
}
} else {
ss << " (invalid)";
}
}
ss << ">";
return PyUnicode_FromString(ss.str().c_str());
}
PyObject* PyPropertyBindingType::get_target(PyPropertyBindingObject* self, void* closure) {
if (!self->binding) {
Py_RETURN_NONE;
}
auto ptr = self->binding->getTarget().lock();
if (!ptr) {
Py_RETURN_NONE;
}
// TODO: Return the actual Python object for the drawable
Py_RETURN_NONE;
}
PyObject* PyPropertyBindingType::get_property(PyPropertyBindingObject* self, void* closure) {
if (!self->binding) {
Py_RETURN_NONE;
}
return PyUnicode_FromString(self->binding->getPropertyName().c_str());
}
PyObject* PyPropertyBindingType::get_value(PyPropertyBindingObject* self, void* closure) {
if (!self->binding) {
Py_RETURN_NONE;
}
auto val = self->binding->evaluate();
if (!val) {
Py_RETURN_NONE;
}
return PyFloat_FromDouble(*val);
}
PyObject* PyPropertyBindingType::is_valid(PyPropertyBindingObject* self, void* closure) {
if (!self->binding) {
Py_RETURN_FALSE;
}
return PyBool_FromLong(self->binding->isValid());
}
// ============================================================================
// PyCallableBindingType Python Interface
// ============================================================================
PyGetSetDef PyCallableBindingType::getsetters[] = {
{"callable", (getter)PyCallableBindingType::get_callable, NULL,
MCRF_PROPERTY(callable, "The Python callable (read-only)."), NULL},
{"value", (getter)PyCallableBindingType::get_value, NULL,
MCRF_PROPERTY(value, "Current value from calling the callable (float, read-only). Returns None on error."), NULL},
{"is_valid", (getter)PyCallableBindingType::is_valid, NULL,
MCRF_PROPERTY(is_valid, "True if the callable is still valid (bool, read-only)."), NULL},
{NULL}
};
PyObject* PyCallableBindingType::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) {
PyCallableBindingObject* self = (PyCallableBindingObject*)type->tp_alloc(type, 0);
if (self) {
self->binding = nullptr;
self->weakreflist = NULL;
}
return (PyObject*)self;
}
void PyCallableBindingType::dealloc(PyCallableBindingObject* self) {
if (self->weakreflist) {
PyObject_ClearWeakRefs((PyObject*)self);
}
self->binding.reset();
Py_TYPE(self)->tp_free((PyObject*)self);
}
int PyCallableBindingType::init(PyCallableBindingObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"callable", nullptr};
PyObject* callable = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", const_cast<char**>(keywords),
&callable)) {
return -1;
}
if (!PyCallable_Check(callable)) {
PyErr_SetString(PyExc_TypeError, "Argument must be callable");
return -1;
}
self->binding = std::make_shared<CallableBinding>(callable);
return 0;
}
PyObject* PyCallableBindingType::repr(PyObject* obj) {
PyCallableBindingObject* self = (PyCallableBindingObject*)obj;
std::ostringstream ss;
ss << "<CallableBinding";
if (self->binding && self->binding->isValid()) {
auto val = self->binding->evaluate();
if (val) {
ss << " value=" << *val;
}
} else {
ss << " (invalid)";
}
ss << ">";
return PyUnicode_FromString(ss.str().c_str());
}
PyObject* PyCallableBindingType::get_callable(PyCallableBindingObject* self, void* closure) {
if (!self->binding) {
Py_RETURN_NONE;
}
PyObject* callable = self->binding->getCallable();
if (!callable) {
Py_RETURN_NONE;
}
Py_INCREF(callable);
return callable;
}
PyObject* PyCallableBindingType::get_value(PyCallableBindingObject* self, void* closure) {
if (!self->binding) {
Py_RETURN_NONE;
}
auto val = self->binding->evaluate();
if (!val) {
Py_RETURN_NONE;
}
return PyFloat_FromDouble(*val);
}
PyObject* PyCallableBindingType::is_valid(PyCallableBindingObject* self, void* closure) {
if (!self->binding) {
Py_RETURN_FALSE;
}
return PyBool_FromLong(self->binding->isValid());
}

201
src/PyUniformBinding.h Normal file
View file

@ -0,0 +1,201 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include <variant>
#include <map>
#include <memory>
#include <optional>
// Forward declarations
class UIDrawable;
/**
* @brief Variant type for uniform values
*
* Supports float, vec2, vec3, and vec4 uniform types.
*/
using UniformValue = std::variant<
float,
sf::Glsl::Vec2,
sf::Glsl::Vec3,
sf::Glsl::Vec4
>;
/**
* @brief Base class for uniform bindings
*
* Bindings provide dynamic uniform values that are evaluated each frame.
*/
class UniformBinding {
public:
virtual ~UniformBinding() = default;
/**
* @brief Evaluate the binding and return its current value
* @return The current uniform value, or std::nullopt if binding is invalid
*/
virtual std::optional<float> evaluate() const = 0;
/**
* @brief Check if the binding is still valid
*/
virtual bool isValid() const = 0;
};
/**
* @brief Binding that reads a property from a UIDrawable
*
* Uses a weak_ptr to prevent dangling references if the target is destroyed.
*/
class PropertyBinding : public UniformBinding {
public:
PropertyBinding(std::weak_ptr<UIDrawable> target, const std::string& property);
std::optional<float> evaluate() const override;
bool isValid() const override;
// Accessors for Python
std::weak_ptr<UIDrawable> getTarget() const { return target; }
const std::string& getPropertyName() const { return property_name; }
private:
std::weak_ptr<UIDrawable> target;
std::string property_name;
};
/**
* @brief Binding that calls a Python callable to get the value
*
* The callable should return a float value.
*/
class CallableBinding : public UniformBinding {
public:
explicit CallableBinding(PyObject* callable);
~CallableBinding();
// Non-copyable due to PyObject reference management
CallableBinding(const CallableBinding&) = delete;
CallableBinding& operator=(const CallableBinding&) = delete;
// Move semantics
CallableBinding(CallableBinding&& other) noexcept;
CallableBinding& operator=(CallableBinding&& other) noexcept;
std::optional<float> evaluate() const override;
bool isValid() const override;
// Accessor for Python
PyObject* getCallable() const { return callable; }
private:
PyObject* callable; // Owned reference
};
// Python object structures for bindings
typedef struct {
PyObject_HEAD
std::shared_ptr<PropertyBinding> binding;
PyObject* weakreflist;
} PyPropertyBindingObject;
typedef struct {
PyObject_HEAD
std::shared_ptr<CallableBinding> binding;
PyObject* weakreflist;
} PyCallableBindingObject;
// Python type class for PropertyBinding
class PyPropertyBindingType {
public:
static PyObject* repr(PyObject* self);
static int init(PyPropertyBindingObject* self, PyObject* args, PyObject* kwds);
static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds);
static void dealloc(PyPropertyBindingObject* self);
static PyObject* get_target(PyPropertyBindingObject* self, void* closure);
static PyObject* get_property(PyPropertyBindingObject* self, void* closure);
static PyObject* get_value(PyPropertyBindingObject* self, void* closure);
static PyObject* is_valid(PyPropertyBindingObject* self, void* closure);
static PyGetSetDef getsetters[];
};
// Python type class for CallableBinding
class PyCallableBindingType {
public:
static PyObject* repr(PyObject* self);
static int init(PyCallableBindingObject* self, PyObject* args, PyObject* kwds);
static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds);
static void dealloc(PyCallableBindingObject* self);
static PyObject* get_callable(PyCallableBindingObject* self, void* closure);
static PyObject* get_value(PyCallableBindingObject* self, void* closure);
static PyObject* is_valid(PyCallableBindingObject* self, void* closure);
static PyGetSetDef getsetters[];
};
namespace mcrfpydef {
// Using inline to ensure single definition across translation units (C++17)
inline PyTypeObject PyPropertyBindingType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.PropertyBinding",
.tp_basicsize = sizeof(PyPropertyBindingObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)::PyPropertyBindingType::dealloc,
.tp_repr = ::PyPropertyBindingType::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR(
"PropertyBinding(target: UIDrawable, property: str)\n"
"\n"
"A binding that reads a property value from a UI drawable.\n"
"\n"
"Args:\n"
" target: The drawable to read the property from\n"
" property: Name of the property to read (e.g., 'x', 'opacity')\n"
"\n"
"Use this to create dynamic shader uniforms that follow a drawable's\n"
"properties. The binding automatically handles cases where the target\n"
"is destroyed.\n"
"\n"
"Example:\n"
" other_frame = mcrfpy.Frame(pos=(100, 100))\n"
" frame.uniforms['offset_x'] = mcrfpy.PropertyBinding(other_frame, 'x')\n"
),
.tp_weaklistoffset = offsetof(PyPropertyBindingObject, weakreflist),
.tp_getset = nullptr, // Set in McRFPy_API.cpp before PyType_Ready
.tp_init = (initproc)::PyPropertyBindingType::init,
.tp_new = ::PyPropertyBindingType::pynew,
};
inline PyTypeObject PyCallableBindingType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.CallableBinding",
.tp_basicsize = sizeof(PyCallableBindingObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)::PyCallableBindingType::dealloc,
.tp_repr = ::PyCallableBindingType::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR(
"CallableBinding(callable: Callable[[], float])\n"
"\n"
"A binding that calls a Python function to get its value.\n"
"\n"
"Args:\n"
" callable: A function that takes no arguments and returns a float\n"
"\n"
"The callable is invoked every frame when the shader is rendered.\n"
"Keep the callable lightweight to avoid performance issues.\n"
"\n"
"Example:\n"
" player_health = 100\n"
" frame.uniforms['health_pct'] = mcrfpy.CallableBinding(\n"
" lambda: player_health / 100.0\n"
" )\n"
),
.tp_weaklistoffset = offsetof(PyCallableBindingObject, weakreflist),
.tp_getset = nullptr, // Set in McRFPy_API.cpp before PyType_Ready
.tp_init = (initproc)::PyCallableBindingType::init,
.tp_new = ::PyCallableBindingType::pynew,
};
}

405
src/PyUniformCollection.cpp Normal file
View file

@ -0,0 +1,405 @@
#include "PyUniformCollection.h"
#include "UIDrawable.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
#include <sstream>
// ============================================================================
// UniformCollection Implementation
// ============================================================================
void UniformCollection::setFloat(const std::string& name, float value) {
entries[name] = UniformValue(value);
}
void UniformCollection::setVec2(const std::string& name, float x, float y) {
entries[name] = UniformValue(sf::Glsl::Vec2(x, y));
}
void UniformCollection::setVec3(const std::string& name, float x, float y, float z) {
entries[name] = UniformValue(sf::Glsl::Vec3(x, y, z));
}
void UniformCollection::setVec4(const std::string& name, float x, float y, float z, float w) {
entries[name] = UniformValue(sf::Glsl::Vec4(x, y, z, w));
}
void UniformCollection::setPropertyBinding(const std::string& name, std::shared_ptr<PropertyBinding> binding) {
entries[name] = binding;
}
void UniformCollection::setCallableBinding(const std::string& name, std::shared_ptr<CallableBinding> binding) {
entries[name] = binding;
}
void UniformCollection::remove(const std::string& name) {
entries.erase(name);
}
bool UniformCollection::contains(const std::string& name) const {
return entries.find(name) != entries.end();
}
std::vector<std::string> UniformCollection::getNames() const {
std::vector<std::string> names;
names.reserve(entries.size());
for (const auto& [name, _] : entries) {
names.push_back(name);
}
return names;
}
void UniformCollection::applyTo(sf::Shader& shader) const {
for (const auto& [name, entry] : entries) {
std::visit([&shader, &name](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, UniformValue>) {
// Static value
std::visit([&shader, &name](auto&& val) {
using V = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<V, float>) {
shader.setUniform(name, val);
} else if constexpr (std::is_same_v<V, sf::Glsl::Vec2>) {
shader.setUniform(name, val);
} else if constexpr (std::is_same_v<V, sf::Glsl::Vec3>) {
shader.setUniform(name, val);
} else if constexpr (std::is_same_v<V, sf::Glsl::Vec4>) {
shader.setUniform(name, val);
}
}, arg);
}
else if constexpr (std::is_same_v<T, std::shared_ptr<PropertyBinding>>) {
// Property binding - evaluate and apply
if (arg && arg->isValid()) {
auto val = arg->evaluate();
if (val) {
shader.setUniform(name, *val);
}
}
}
else if constexpr (std::is_same_v<T, std::shared_ptr<CallableBinding>>) {
// Callable binding - evaluate and apply
if (arg && arg->isValid()) {
auto val = arg->evaluate();
if (val) {
shader.setUniform(name, *val);
}
}
}
}, entry);
}
}
bool UniformCollection::hasDynamicBindings() const {
for (const auto& [_, entry] : entries) {
if (std::holds_alternative<std::shared_ptr<CallableBinding>>(entry)) {
return true;
}
}
return false;
}
const UniformEntry* UniformCollection::getEntry(const std::string& name) const {
auto it = entries.find(name);
if (it == entries.end()) return nullptr;
return &it->second;
}
// ============================================================================
// PyUniformCollectionType Python Interface
// ============================================================================
PyMethodDef PyUniformCollectionType::methods[] = {
{"keys", (PyCFunction)PyUniformCollectionType::keys, METH_NOARGS,
"Return list of uniform names"},
{"values", (PyCFunction)PyUniformCollectionType::values, METH_NOARGS,
"Return list of uniform values"},
{"items", (PyCFunction)PyUniformCollectionType::items, METH_NOARGS,
"Return list of (name, value) tuples"},
{"clear", (PyCFunction)PyUniformCollectionType::clear, METH_NOARGS,
"Remove all uniforms"},
{NULL}
};
PyMappingMethods PyUniformCollectionType::mapping_methods = {
.mp_length = PyUniformCollectionType::mp_length,
.mp_subscript = PyUniformCollectionType::mp_subscript,
.mp_ass_subscript = PyUniformCollectionType::mp_ass_subscript,
};
PySequenceMethods PyUniformCollectionType::sequence_methods = {
.sq_length = nullptr,
.sq_concat = nullptr,
.sq_repeat = nullptr,
.sq_item = nullptr,
.was_sq_slice = nullptr,
.sq_ass_item = nullptr,
.was_sq_ass_slice = nullptr,
.sq_contains = PyUniformCollectionType::sq_contains,
};
void PyUniformCollectionType::dealloc(PyUniformCollectionObject* self) {
if (self->weakreflist) {
PyObject_ClearWeakRefs((PyObject*)self);
}
// Don't delete collection - it's owned by UIDrawable
Py_TYPE(self)->tp_free((PyObject*)self);
}
PyObject* PyUniformCollectionType::repr(PyObject* obj) {
PyUniformCollectionObject* self = (PyUniformCollectionObject*)obj;
std::ostringstream ss;
ss << "<UniformCollection";
if (self->collection) {
auto names = self->collection->getNames();
ss << " [";
for (size_t i = 0; i < names.size(); ++i) {
if (i > 0) ss << ", ";
ss << "'" << names[i] << "'";
}
ss << "]";
}
ss << ">";
return PyUnicode_FromString(ss.str().c_str());
}
Py_ssize_t PyUniformCollectionType::mp_length(PyObject* obj) {
PyUniformCollectionObject* self = (PyUniformCollectionObject*)obj;
if (!self->collection) return 0;
return static_cast<Py_ssize_t>(self->collection->getNames().size());
}
PyObject* PyUniformCollectionType::mp_subscript(PyObject* obj, PyObject* key) {
PyUniformCollectionObject* self = (PyUniformCollectionObject*)obj;
if (!self->collection) {
PyErr_SetString(PyExc_RuntimeError, "UniformCollection is not valid");
return NULL;
}
if (!PyUnicode_Check(key)) {
PyErr_SetString(PyExc_TypeError, "Uniform name must be a string");
return NULL;
}
const char* name = PyUnicode_AsUTF8(key);
const UniformEntry* entry = self->collection->getEntry(name);
if (!entry) {
PyErr_Format(PyExc_KeyError, "'%s'", name);
return NULL;
}
// Convert entry to Python object
return std::visit([](auto&& arg) -> PyObject* {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, UniformValue>) {
return std::visit([](auto&& val) -> PyObject* {
using V = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<V, float>) {
return PyFloat_FromDouble(val);
} else if constexpr (std::is_same_v<V, sf::Glsl::Vec2>) {
return Py_BuildValue("(ff)", val.x, val.y);
} else if constexpr (std::is_same_v<V, sf::Glsl::Vec3>) {
return Py_BuildValue("(fff)", val.x, val.y, val.z);
} else if constexpr (std::is_same_v<V, sf::Glsl::Vec4>) {
return Py_BuildValue("(ffff)", val.x, val.y, val.z, val.w);
}
Py_RETURN_NONE;
}, arg);
}
else if constexpr (std::is_same_v<T, std::shared_ptr<PropertyBinding>>) {
// Return the current value for now
// TODO: Return the actual PropertyBinding object
if (arg && arg->isValid()) {
auto val = arg->evaluate();
if (val) {
return PyFloat_FromDouble(*val);
}
}
Py_RETURN_NONE;
}
else if constexpr (std::is_same_v<T, std::shared_ptr<CallableBinding>>) {
// Return the current value for now
// TODO: Return the actual CallableBinding object
if (arg && arg->isValid()) {
auto val = arg->evaluate();
if (val) {
return PyFloat_FromDouble(*val);
}
}
Py_RETURN_NONE;
}
Py_RETURN_NONE;
}, *entry);
}
int PyUniformCollectionType::mp_ass_subscript(PyObject* obj, PyObject* key, PyObject* value) {
PyUniformCollectionObject* self = (PyUniformCollectionObject*)obj;
if (!self->collection) {
PyErr_SetString(PyExc_RuntimeError, "UniformCollection is not valid");
return -1;
}
if (!PyUnicode_Check(key)) {
PyErr_SetString(PyExc_TypeError, "Uniform name must be a string");
return -1;
}
const char* name = PyUnicode_AsUTF8(key);
// Delete operation
if (value == NULL) {
self->collection->remove(name);
return 0;
}
// Check for binding types first
// PropertyBinding
if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyPropertyBindingType)) {
PyPropertyBindingObject* binding = (PyPropertyBindingObject*)value;
if (binding->binding) {
self->collection->setPropertyBinding(name, binding->binding);
return 0;
}
PyErr_SetString(PyExc_ValueError, "PropertyBinding is not valid");
return -1;
}
// CallableBinding
if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyCallableBindingType)) {
PyCallableBindingObject* binding = (PyCallableBindingObject*)value;
if (binding->binding) {
self->collection->setCallableBinding(name, binding->binding);
return 0;
}
PyErr_SetString(PyExc_ValueError, "CallableBinding is not valid");
return -1;
}
// Float value
if (PyFloat_Check(value) || PyLong_Check(value)) {
float f = static_cast<float>(PyFloat_AsDouble(value));
if (PyErr_Occurred()) return -1;
self->collection->setFloat(name, f);
return 0;
}
// Tuple for vec2/vec3/vec4
if (PyTuple_Check(value)) {
Py_ssize_t size = PyTuple_Size(value);
if (size == 2) {
float x = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 0)));
float y = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 1)));
if (PyErr_Occurred()) return -1;
self->collection->setVec2(name, x, y);
return 0;
}
else if (size == 3) {
float x = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 0)));
float y = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 1)));
float z = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 2)));
if (PyErr_Occurred()) return -1;
self->collection->setVec3(name, x, y, z);
return 0;
}
else if (size == 4) {
float x = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 0)));
float y = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 1)));
float z = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 2)));
float w = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 3)));
if (PyErr_Occurred()) return -1;
self->collection->setVec4(name, x, y, z, w);
return 0;
}
else {
PyErr_Format(PyExc_ValueError,
"Tuple must have 2, 3, or 4 elements for vec2/vec3/vec4, got %zd", size);
return -1;
}
}
PyErr_SetString(PyExc_TypeError,
"Uniform value must be a float, tuple (vec2/vec3/vec4), PropertyBinding, or CallableBinding");
return -1;
}
int PyUniformCollectionType::sq_contains(PyObject* obj, PyObject* key) {
PyUniformCollectionObject* self = (PyUniformCollectionObject*)obj;
if (!self->collection) return 0;
if (!PyUnicode_Check(key)) return 0;
const char* name = PyUnicode_AsUTF8(key);
return self->collection->contains(name) ? 1 : 0;
}
PyObject* PyUniformCollectionType::keys(PyUniformCollectionObject* self, PyObject* Py_UNUSED(ignored)) {
if (!self->collection) {
return PyList_New(0);
}
auto names = self->collection->getNames();
PyObject* list = PyList_New(names.size());
for (size_t i = 0; i < names.size(); ++i) {
PyList_SetItem(list, i, PyUnicode_FromString(names[i].c_str()));
}
return list;
}
PyObject* PyUniformCollectionType::values(PyUniformCollectionObject* self, PyObject* Py_UNUSED(ignored)) {
if (!self->collection) {
return PyList_New(0);
}
auto names = self->collection->getNames();
PyObject* list = PyList_New(names.size());
for (size_t i = 0; i < names.size(); ++i) {
PyObject* key = PyUnicode_FromString(names[i].c_str());
PyObject* val = mp_subscript((PyObject*)self, key);
Py_DECREF(key);
if (!val) {
Py_DECREF(list);
return NULL;
}
PyList_SetItem(list, i, val);
}
return list;
}
PyObject* PyUniformCollectionType::items(PyUniformCollectionObject* self, PyObject* Py_UNUSED(ignored)) {
if (!self->collection) {
return PyList_New(0);
}
auto names = self->collection->getNames();
PyObject* list = PyList_New(names.size());
for (size_t i = 0; i < names.size(); ++i) {
PyObject* key = PyUnicode_FromString(names[i].c_str());
PyObject* val = mp_subscript((PyObject*)self, key);
if (!val) {
Py_DECREF(key);
Py_DECREF(list);
return NULL;
}
PyObject* tuple = PyTuple_Pack(2, key, val);
Py_DECREF(key);
Py_DECREF(val);
PyList_SetItem(list, i, tuple);
}
return list;
}
PyObject* PyUniformCollectionType::clear(PyUniformCollectionObject* self, PyObject* Py_UNUSED(ignored)) {
if (self->collection) {
auto names = self->collection->getNames();
for (const auto& name : names) {
self->collection->remove(name);
}
}
Py_RETURN_NONE;
}

157
src/PyUniformCollection.h Normal file
View file

@ -0,0 +1,157 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include "PyUniformBinding.h"
#include <map>
#include <variant>
// Forward declarations
class UIDrawable;
/**
* @brief Entry in UniformCollection - can be static value or binding
*/
using UniformEntry = std::variant<
UniformValue, // Static value (float, vec2, vec3, vec4)
std::shared_ptr<PropertyBinding>, // Property binding
std::shared_ptr<CallableBinding> // Callable binding
>;
/**
* @brief Collection of shader uniforms for a UIDrawable
*
* Stores both static uniform values and dynamic bindings. When applying
* uniforms to a shader, static values are used directly while bindings
* are evaluated each frame.
*/
class UniformCollection {
public:
UniformCollection() = default;
/**
* @brief Set a static uniform value
*/
void setFloat(const std::string& name, float value);
void setVec2(const std::string& name, float x, float y);
void setVec3(const std::string& name, float x, float y, float z);
void setVec4(const std::string& name, float x, float y, float z, float w);
/**
* @brief Set a property binding
*/
void setPropertyBinding(const std::string& name, std::shared_ptr<PropertyBinding> binding);
/**
* @brief Set a callable binding
*/
void setCallableBinding(const std::string& name, std::shared_ptr<CallableBinding> binding);
/**
* @brief Remove a uniform
*/
void remove(const std::string& name);
/**
* @brief Check if a uniform exists
*/
bool contains(const std::string& name) const;
/**
* @brief Get all uniform names
*/
std::vector<std::string> getNames() const;
/**
* @brief Apply all uniforms to a shader
*/
void applyTo(sf::Shader& shader) const;
/**
* @brief Check if any binding is dynamic (callable)
*/
bool hasDynamicBindings() const;
/**
* @brief Get the entry for a uniform (for Python access)
*/
const UniformEntry* getEntry(const std::string& name) const;
private:
std::map<std::string, UniformEntry> entries;
};
// Python object structure for UniformCollection
typedef struct {
PyObject_HEAD
UniformCollection* collection; // Owned by UIDrawable, not by this object
std::weak_ptr<UIDrawable> owner; // For checking validity
PyObject* weakreflist;
} PyUniformCollectionObject;
/**
* @brief Python type for UniformCollection
*
* Supports dict-like access: collection["name"] = value
*/
class PyUniformCollectionType {
public:
static PyObject* repr(PyObject* self);
static void dealloc(PyUniformCollectionObject* self);
// Mapping protocol (dict-like access)
static Py_ssize_t mp_length(PyObject* self);
static PyObject* mp_subscript(PyObject* self, PyObject* key);
static int mp_ass_subscript(PyObject* self, PyObject* key, PyObject* value);
// Sequence protocol for 'in' operator
static int sq_contains(PyObject* self, PyObject* key);
// Methods
static PyObject* keys(PyUniformCollectionObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* values(PyUniformCollectionObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* items(PyUniformCollectionObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* clear(PyUniformCollectionObject* self, PyObject* Py_UNUSED(ignored));
static PyMethodDef methods[];
static PyMappingMethods mapping_methods;
static PySequenceMethods sequence_methods;
};
namespace mcrfpydef {
// Using inline to ensure single definition across translation units (C++17)
inline PyTypeObject PyUniformCollectionType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.UniformCollection",
.tp_basicsize = sizeof(PyUniformCollectionObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)::PyUniformCollectionType::dealloc,
.tp_repr = ::PyUniformCollectionType::repr,
.tp_as_sequence = &::PyUniformCollectionType::sequence_methods,
.tp_as_mapping = &::PyUniformCollectionType::mapping_methods,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR(
"UniformCollection - dict-like container for shader uniforms.\n"
"\n"
"This object is accessed via drawable.uniforms and supports:\n"
"- Getting: value = uniforms['name']\n"
"- Setting: uniforms['name'] = value\n"
"- Deleting: del uniforms['name']\n"
"- Checking: 'name' in uniforms\n"
"- Iterating: for name in uniforms.keys()\n"
"\n"
"Values can be:\n"
"- float: Single value uniform\n"
"- tuple: vec2 (2-tuple), vec3 (3-tuple), or vec4 (4-tuple)\n"
"- PropertyBinding: Dynamic value from another drawable's property\n"
"- CallableBinding: Dynamic value from a Python function\n"
"\n"
"Example:\n"
" frame.uniforms['intensity'] = 0.5\n"
" frame.uniforms['color'] = (1.0, 0.5, 0.0, 1.0)\n"
" frame.uniforms['offset'] = mcrfpy.PropertyBinding(other, 'x')\n"
" del frame.uniforms['intensity']\n"
),
.tp_weaklistoffset = offsetof(PyUniformCollectionObject, weakreflist),
.tp_methods = nullptr, // Set in McRFPy_API.cpp
};
}

View file

@ -282,4 +282,19 @@ static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure)
"Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER)." \
), (void*)type_enum}
// #106: Shader support - GPU-accelerated visual effects
#define UIDRAWABLE_SHADER_GETSETTERS(type_enum) \
{"shader", (getter)UIDrawable::get_shader, (setter)UIDrawable::set_shader, \
MCRF_PROPERTY(shader, \
"Shader for GPU visual effects (Shader or None). " \
"When set, the drawable is rendered through the shader program. " \
"Set to None to disable shader effects." \
), (void*)type_enum}, \
{"uniforms", (getter)UIDrawable::get_uniforms, NULL, \
MCRF_PROPERTY(uniforms, \
"Collection of shader uniforms (read-only access to collection). " \
"Set uniforms via dict-like syntax: drawable.uniforms['name'] = value. " \
"Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding." \
), (void*)type_enum}
// UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete

View file

@ -5,6 +5,8 @@
#include "PyFont.h"
#include "PythonObjectCache.h"
#include "PyAlignment.h"
#include "PyShader.h" // #106: Shader support
#include "PyUniformCollection.h" // #106: Uniform collection support
// UIDrawable methods now in UIBase.h
#include <algorithm>
@ -40,10 +42,43 @@ void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target)
color.a = static_cast<sf::Uint8>(255 * opacity);
text.setFillColor(color);
// #106: Shader rendering path
if (shader && shader->shader) {
// Get the text bounds for rendering
auto bounds = text.getGlobalBounds();
sf::Vector2f screen_pos = offset + position;
// Get or create intermediate texture
auto& intermediate = GameEngine::getShaderIntermediate();
intermediate.clear(sf::Color::Transparent);
// Render text at origin in intermediate texture
sf::Text temp_text = text;
temp_text.setPosition(0, 0); // Render at origin of intermediate texture
intermediate.draw(temp_text);
intermediate.display();
// Create result sprite from intermediate texture
sf::Sprite result_sprite(intermediate.getTexture());
result_sprite.setPosition(screen_pos);
// Apply engine uniforms
sf::Vector2f resolution(bounds.width, bounds.height);
PyShader::applyEngineUniforms(*shader->shader, resolution);
// Apply user uniforms
if (uniforms) {
uniforms->applyTo(*shader->shader);
}
// Draw with shader
target.draw(result_sprite, shader->shader.get());
} else {
// Standard rendering path (no shader)
text.move(offset);
//Resources::game->getWindow().draw(text);
target.draw(text);
text.move(-offset);
}
// Restore original alpha
color.a = 255;
@ -314,6 +349,7 @@ PyGetSetDef UICaption::getsetters[] = {
UIDRAWABLE_GETSETTERS,
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICAPTION),
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UICAPTION),
UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UICAPTION),
{NULL}
};
@ -595,6 +631,10 @@ bool UICaption::setProperty(const std::string& name, float value) {
markDirty(); // #144 - Z-order change affects parent
return true;
}
// #106: Check for shader uniform properties
if (setShaderProperty(name, value)) {
return true;
}
return false;
}
@ -674,6 +714,10 @@ bool UICaption::getProperty(const std::string& name, float& value) const {
value = static_cast<float>(z_index);
return true;
}
// #106: Check for shader uniform properties
if (getShaderProperty(name, value)) {
return true;
}
return false;
}
@ -715,6 +759,10 @@ bool UICaption::hasProperty(const std::string& name) const {
if (name == "text") {
return true;
}
// #106: Check for shader uniform properties
if (hasShaderProperty(name)) {
return true;
}
return false;
}

View file

@ -13,6 +13,8 @@
#include "PyAnimation.h"
#include "PyEasing.h"
#include "PySceneObject.h" // #183: For scene parent lookup
#include "PyShader.h" // #106: Shader support
#include "PyUniformCollection.h" // #106: Uniform collection support
// Helper function to extract UIDrawable* from any Python UI object
// Returns nullptr and sets Python error on failure
@ -947,6 +949,195 @@ void UIDrawable::markDirty() {
markContentDirty();
}
// #106 - Shader support
void UIDrawable::markShaderDynamic() {
shader_dynamic = true;
// Propagate to parent to invalidate caches
auto p = parent.lock();
if (p) {
p->markShaderDynamic();
}
}
// #106: Shader uniform property helpers for animation support
bool UIDrawable::setShaderProperty(const std::string& name, float value) {
// Check if name starts with "shader."
if (name.compare(0, 7, "shader.") != 0) {
return false;
}
// Extract the uniform name after "shader."
std::string uniform_name = name.substr(7);
if (uniform_name.empty()) {
return false;
}
// Initialize uniforms collection if needed
if (!uniforms) {
uniforms = std::make_unique<UniformCollection>();
}
// Set the uniform value
uniforms->setFloat(uniform_name, value);
markDirty();
return true;
}
bool UIDrawable::getShaderProperty(const std::string& name, float& value) const {
// Check if name starts with "shader."
if (name.compare(0, 7, "shader.") != 0) {
return false;
}
// Extract the uniform name after "shader."
std::string uniform_name = name.substr(7);
if (uniform_name.empty() || !uniforms) {
return false;
}
// Try to get the value from uniforms
const auto* entry = uniforms->getEntry(uniform_name);
if (!entry) {
return false;
}
// UniformEntry is variant<UniformValue, shared_ptr<PropertyBinding>, shared_ptr<CallableBinding>>
// UniformValue is variant<float, vec2, vec3, vec4>
// So we need to check for UniformValue first, then extract the float from it
// Try to extract static UniformValue from the entry
if (const auto* uval = std::get_if<UniformValue>(entry)) {
// Now try to extract float from UniformValue
if (const float* fval = std::get_if<float>(uval)) {
value = *fval;
return true;
}
// Could be vec2/vec3/vec4 - not a float, return false
return false;
}
// For bindings, evaluate and return
if (const auto* prop_binding = std::get_if<std::shared_ptr<PropertyBinding>>(entry)) {
auto opt_val = (*prop_binding)->evaluate();
if (opt_val) {
value = *opt_val;
return true;
}
} else if (const auto* call_binding = std::get_if<std::shared_ptr<CallableBinding>>(entry)) {
auto opt_val = (*call_binding)->evaluate();
if (opt_val) {
value = *opt_val;
return true;
}
}
return false;
}
bool UIDrawable::hasShaderProperty(const std::string& name) const {
// Check if name starts with "shader."
if (name.compare(0, 7, "shader.") != 0) {
return false;
}
// Shader uniforms are always valid property names (they'll be created on set)
return true;
}
// Python API for shader property
PyObject* UIDrawable::get_shader(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
if (!drawable->shader) {
Py_RETURN_NONE;
}
// Return the shader object (increment reference)
Py_INCREF(drawable->shader.get());
return (PyObject*)drawable->shader.get();
}
int UIDrawable::set_shader(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
if (value == Py_None) {
// Clear shader
drawable->shader.reset();
drawable->shader_dynamic = false;
drawable->markDirty();
return 0;
}
// Check if it's a Shader object
if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyShaderType)) {
PyErr_SetString(PyExc_TypeError, "shader must be a Shader object or None");
return -1;
}
PyShaderObject* shader_obj = (PyShaderObject*)value;
if (!shader_obj->shader) {
PyErr_SetString(PyExc_ValueError, "Shader is not valid (compilation failed?)");
return -1;
}
// Store the shader
drawable->shader = std::shared_ptr<PyShaderObject>(shader_obj, [](PyShaderObject* p) {
// Custom deleter that doesn't delete the Python object
// The Python reference counting handles that
});
Py_INCREF(shader_obj); // Keep the Python object alive
// Create uniforms collection if needed
if (!drawable->uniforms) {
drawable->uniforms = std::make_unique<UniformCollection>();
}
// Set dynamic flag if shader is dynamic
if (shader_obj->dynamic) {
drawable->markShaderDynamic();
}
// Enable RenderTexture for shader rendering (if not already enabled)
auto bounds = drawable->get_bounds();
if (bounds.width > 0 && bounds.height > 0) {
drawable->enableRenderTexture(
static_cast<unsigned int>(bounds.width),
static_cast<unsigned int>(bounds.height)
);
}
drawable->markDirty();
return 0;
}
PyObject* UIDrawable::get_uniforms(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
// Create uniforms collection if needed
if (!drawable->uniforms) {
drawable->uniforms = std::make_unique<UniformCollection>();
}
// Create and return a Python wrapper for the collection
PyUniformCollectionObject* collection = (PyUniformCollectionObject*)
mcrfpydef::PyUniformCollectionType.tp_alloc(&mcrfpydef::PyUniformCollectionType, 0);
if (!collection) return NULL;
collection->collection = drawable->uniforms.get();
collection->weakreflist = NULL;
// Note: owner weak_ptr could be set here if we had access to shared_ptr
return (PyObject*)collection;
}
// Python API - get parent drawable
PyObject* UIDrawable::get_parent(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));

View file

@ -16,6 +16,12 @@
#include "Resources.h"
#include "UIBase.h"
// Forward declarations for shader support (#106)
class UniformCollection;
// PyShaderObject is a typedef, forward declare as a struct with explicit typedef
typedef struct PyShaderObjectStruct PyShaderObject;
class UIFrame; class UICaption; class UISprite; class UIEntity; class UIGrid;
enum PyObjectsEnum : int
@ -205,6 +211,12 @@ public:
// Check if a property name is valid for animation on this drawable type
virtual bool hasProperty(const std::string& name) const { return false; }
// #106: Shader uniform property helpers for animation support
// These methods handle "shader.uniform_name" property paths
bool setShaderProperty(const std::string& name, float value);
bool getShaderProperty(const std::string& name, float& value) const;
bool hasShaderProperty(const std::string& name) const;
// Note: animate_helper is now a free function (UIDrawable_animate_impl) declared in UIBase.h
// to avoid incomplete type issues with template instantiation.
@ -253,6 +265,20 @@ protected:
public:
void disableRenderTexture();
// Shader support (#106)
std::shared_ptr<PyShaderObject> shader;
std::unique_ptr<UniformCollection> uniforms;
bool shader_dynamic = false; // True if shader uses time-varying effects
// Mark this drawable as having dynamic shader effects
// Propagates up to parent to invalidate caches
void markShaderDynamic();
// Python API for shader properties
static PyObject* get_shader(PyObject* self, void* closure);
static int set_shader(PyObject* self, PyObject* value, void* closure);
static PyObject* get_uniforms(PyObject* self, void* closure);
protected:
public:

View file

@ -12,6 +12,8 @@
#include "PyAnimation.h"
#include "PyEasing.h"
#include "PyPositionHelper.h"
#include "PyShader.h" // #106: Shader support
#include "PyUniformCollection.h" // #106: Uniform collection support
// UIDrawable methods now in UIBase.h
#include "UIEntityPyMethods.h"
@ -1035,6 +1037,14 @@ PyGetSetDef UIEntity::getsetters[] = {
{"visible", (getter)UIEntity_get_visible, (setter)UIEntity_set_visible, "Visibility flag", NULL},
{"opacity", (getter)UIEntity_get_opacity, (setter)UIEntity_set_opacity, "Opacity (0.0 = transparent, 1.0 = opaque)", NULL},
{"name", (getter)UIEntity_get_name, (setter)UIEntity_set_name, "Name for finding elements", NULL},
{"shader", (getter)UIEntity_get_shader, (setter)UIEntity_set_shader,
"Shader for GPU visual effects (Shader or None). "
"When set, the entity is rendered through the shader program. "
"Set to None to disable shader effects.", NULL},
{"uniforms", (getter)UIEntity_get_uniforms, NULL,
"Collection of shader uniforms (read-only access to collection). "
"Set uniforms via dict-like syntax: entity.uniforms['name'] = value. "
"Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding.", NULL},
{NULL} /* Sentinel */
};
@ -1073,6 +1083,10 @@ bool UIEntity::setProperty(const std::string& name, float value) {
if (grid) grid->markDirty(); // #144 - Content change
return true;
}
// #106: Shader uniform properties - delegate to sprite
if (sprite.setShaderProperty(name, value)) {
return true;
}
return false;
}
@ -1098,6 +1112,10 @@ bool UIEntity::getProperty(const std::string& name, float& value) const {
value = sprite.getScale().x; // Assuming uniform scale
return true;
}
// #106: Shader uniform properties - delegate to sprite
if (sprite.getShaderProperty(name, value)) {
return true;
}
return false;
}
@ -1110,6 +1128,10 @@ bool UIEntity::hasProperty(const std::string& name) const {
if (name == "sprite_index" || name == "sprite_number") {
return true;
}
// #106: Shader uniform properties - delegate to sprite
if (sprite.hasShaderProperty(name)) {
return true;
}
return false;
}

View file

@ -1,6 +1,8 @@
#pragma once
#include "UIEntity.h"
#include "UIBase.h"
#include "PyShader.h" // #106: Shader support
#include "PyUniformCollection.h" // #106: Uniform collection support
// UIEntity-specific property implementations
// These delegate to the wrapped sprite member
@ -73,3 +75,72 @@ static int UIEntity_set_name(PyUIEntityObject* self, PyObject* value, void* clos
self->data->sprite.name = name_str;
return 0;
}
// #106: Shader property - delegate to sprite
static PyObject* UIEntity_get_shader(PyUIEntityObject* self, void* closure)
{
auto& shader_ptr = self->data->sprite.shader;
if (!shader_ptr) {
Py_RETURN_NONE;
}
// Return the PyShaderObject (which is also a PyObject)
Py_INCREF((PyObject*)shader_ptr.get());
return (PyObject*)shader_ptr.get();
}
static int UIEntity_set_shader(PyUIEntityObject* self, PyObject* value, void* closure)
{
if (value == Py_None || value == NULL) {
self->data->sprite.shader.reset();
self->data->sprite.shader_dynamic = false;
return 0;
}
// Check if value is a Shader object
if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyShaderType)) {
PyErr_SetString(PyExc_TypeError, "shader must be a Shader object or None");
return -1;
}
PyShaderObject* shader_obj = (PyShaderObject*)value;
// Store a shared_ptr to the PyShaderObject
// We need to increment the refcount since we're storing a reference
Py_INCREF(value);
self->data->sprite.shader = std::shared_ptr<PyShaderObject>(shader_obj, [](PyShaderObject* p) {
Py_DECREF((PyObject*)p);
});
// Initialize uniforms collection if needed
if (!self->data->sprite.uniforms) {
self->data->sprite.uniforms = std::make_unique<UniformCollection>();
}
// Propagate dynamic flag
if (shader_obj->dynamic) {
self->data->sprite.markShaderDynamic();
}
return 0;
}
// #106: Uniforms property - delegate to sprite's uniforms collection
static PyObject* UIEntity_get_uniforms(PyUIEntityObject* self, void* closure)
{
// Initialize uniforms collection if needed
if (!self->data->sprite.uniforms) {
self->data->sprite.uniforms = std::make_unique<UniformCollection>();
}
// Create a Python wrapper for the uniforms collection
PyUniformCollectionObject* uniforms_obj = (PyUniformCollectionObject*)mcrfpydef::PyUniformCollectionType.tp_alloc(&mcrfpydef::PyUniformCollectionType, 0);
if (!uniforms_obj) {
return NULL;
}
// The collection is owned by the sprite, we just provide a view
uniforms_obj->collection = self->data->sprite.uniforms.get();
uniforms_obj->weakreflist = NULL;
return (PyObject*)uniforms_obj;
}

View file

@ -8,6 +8,8 @@
#include "McRFPy_API.h"
#include "PythonObjectCache.h"
#include "PyAlignment.h"
#include "PyShader.h" // #106: Shader support
#include "PyUniformCollection.h" // #106: Uniform collection
#include <iostream> // #106: for shader error output
// UIDrawable methods now in UIBase.h
@ -110,11 +112,11 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
// TODO: Apply opacity when SFML supports it on shapes
// #144: Use RenderTexture for clipping OR texture caching OR shaders
// #144: Use RenderTexture for clipping OR texture caching OR shaders (#106)
// clip_children: requires texture for clipping effect (only when has children)
// cache_subtree: uses texture for performance (always, even without children)
// shader_enabled: requires texture for shader post-processing
bool use_texture = (clip_children && !children->empty()) || cache_subtree || shader_enabled;
// shader: requires texture for shader post-processing
bool use_texture = (clip_children && !children->empty()) || cache_subtree || (shader && shader->shader);
if (use_texture) {
// Enable RenderTexture if not already enabled
@ -170,13 +172,19 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
// Use `position` instead of box.getPosition() - box was set to (0,0) for texture rendering
render_sprite.setPosition(offset + position);
// #106 POC: Apply shader if enabled
if (shader_enabled && shader) {
// Update time uniform for animated effects
static sf::Clock shader_clock;
shader->setUniform("time", shader_clock.getElapsedTime().asSeconds());
shader->setUniform("texture", sf::Shader::CurrentTexture);
target.draw(render_sprite, shader.get());
// #106: Apply shader if set
if (shader && shader->shader) {
// Apply engine uniforms (time, resolution, mouse, texture)
sf::Vector2f resolution(render_sprite.getLocalBounds().width,
render_sprite.getLocalBounds().height);
PyShader::applyEngineUniforms(*shader->shader, resolution);
// Apply user-defined uniforms
if (uniforms) {
uniforms->applyTo(*shader->shader);
}
target.draw(render_sprite, shader->shader.get());
} else {
target.draw(render_sprite);
}
@ -462,87 +470,6 @@ int UIFrame::set_cache_subtree(PyUIFrameObject* self, PyObject* value, void* clo
}
// #106 - Shader POC: shader_enabled property
PyObject* UIFrame::get_shader_enabled(PyUIFrameObject* self, void* closure)
{
return PyBool_FromLong(self->data->shader_enabled);
}
int UIFrame::set_shader_enabled(PyUIFrameObject* self, PyObject* value, void* closure)
{
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "shader_enabled must be a boolean");
return -1;
}
bool new_shader = PyObject_IsTrue(value);
if (new_shader != self->data->shader_enabled) {
self->data->shader_enabled = new_shader;
if (new_shader) {
// Initialize the test shader if not already done
if (!self->data->shader) {
self->data->initializeTestShader();
}
// Shader requires RenderTexture - enable it
auto size = self->data->box.getSize();
if (size.x > 0 && size.y > 0) {
self->data->enableRenderTexture(static_cast<unsigned int>(size.x),
static_cast<unsigned int>(size.y));
}
}
// Note: we don't disable RenderTexture when shader disabled -
// clip_children or cache_subtree may still need it
self->data->markDirty();
}
return 0;
}
// #106 - Initialize test shader (hardcoded glow/brightness effect)
void UIFrame::initializeTestShader()
{
// Check if shaders are available
if (!sf::Shader::isAvailable()) {
std::cerr << "Shaders are not available on this system!" << std::endl;
return;
}
shader = std::make_unique<sf::Shader>();
// Simple color inversion + wave distortion shader for POC
// This makes it obvious the shader is working
const std::string fragmentShader = R"(
uniform sampler2D texture;
uniform float time;
void main() {
vec2 uv = gl_TexCoord[0].xy;
// Subtle wave distortion based on time
uv.x += sin(uv.y * 10.0 + time * 2.0) * 0.01;
uv.y += cos(uv.x * 10.0 + time * 2.0) * 0.01;
vec4 color = texture2D(texture, uv);
// Glow effect: boost brightness and add slight color shift
float glow = 0.2 + 0.1 * sin(time * 3.0);
color.rgb = color.rgb * (1.0 + glow);
// Slight hue shift for visual interest
color.r += 0.1 * sin(time);
color.b += 0.1 * cos(time);
gl_FragColor = color;
}
)";
if (!shader->loadFromMemory(fragmentShader, sf::Shader::Fragment)) {
std::cerr << "Failed to load test shader!" << std::endl;
shader.reset();
}
}
// Define the PyObjectType alias for the macros
typedef PyUIFrameObject PyObjectType;
@ -581,10 +508,10 @@ PyGetSetDef UIFrame::getsetters[] = {
{"grid_size", (getter)UIDrawable::get_grid_size, (setter)UIDrawable::set_grid_size, "Size in grid tile coordinates (only when parent is Grid)", (void*)PyObjectsEnum::UIFRAME},
{"clip_children", (getter)UIFrame::get_clip_children, (setter)UIFrame::set_clip_children, "Whether to clip children to frame bounds", NULL},
{"cache_subtree", (getter)UIFrame::get_cache_subtree, (setter)UIFrame::set_cache_subtree, "#144: Cache subtree rendering to texture for performance", NULL},
{"shader_enabled", (getter)UIFrame::get_shader_enabled, (setter)UIFrame::set_shader_enabled, "#106 POC: Enable test shader effect", NULL},
UIDRAWABLE_GETSETTERS,
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIFRAME),
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIFRAME),
UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UIFRAME),
{NULL}
};
@ -930,6 +857,10 @@ bool UIFrame::setProperty(const std::string& name, float value) {
markDirty();
return true;
}
// #106: Check for shader uniform properties
if (setShaderProperty(name, value)) {
return true;
}
return false;
}
@ -1006,6 +937,10 @@ bool UIFrame::getProperty(const std::string& name, float& value) const {
value = box.getOutlineColor().a;
return true;
}
// #106: Check for shader uniform properties
if (getShaderProperty(name, value)) {
return true;
}
return false;
}
@ -1049,5 +984,9 @@ bool UIFrame::hasProperty(const std::string& name) const {
if (name == "position" || name == "size") {
return true;
}
// #106: Check for shader uniform properties
if (hasShaderProperty(name)) {
return true;
}
return false;
}

View file

@ -33,10 +33,6 @@ public:
bool clip_children = false; // Whether to clip children to frame bounds
bool cache_subtree = false; // #144: Whether to cache subtree rendering to texture
// Shader POC (#106)
std::unique_ptr<sf::Shader> shader;
bool shader_enabled = false;
void initializeTestShader(); // Load hardcoded test shader
void render(sf::Vector2f, sf::RenderTarget&) override final;
void move(sf::Vector2f);
PyObjectsEnum derived_type() override final;
@ -60,8 +56,6 @@ public:
static int set_clip_children(PyUIFrameObject* self, PyObject* value, void* closure);
static PyObject* get_cache_subtree(PyUIFrameObject* self, void* closure);
static int set_cache_subtree(PyUIFrameObject* self, PyObject* value, void* closure);
static PyObject* get_shader_enabled(PyUIFrameObject* self, void* closure);
static int set_shader_enabled(PyUIFrameObject* self, PyObject* value, void* closure);
static PyGetSetDef getsetters[];
static PyObject* repr(PyUIFrameObject* self);
static int init(PyUIFrameObject* self, PyObject* args, PyObject* kwds);

View file

@ -11,6 +11,8 @@
#include "PyPositionHelper.h" // For standardized position argument parsing
#include "PyVector.h" // #179, #181 - For Vector return types
#include "PyHeightMap.h" // #199 - HeightMap application methods
#include "PyShader.h" // #106: Shader support
#include "PyUniformCollection.h" // #106: Uniform collection support
#include <algorithm>
#include <cmath> // #142 - for std::floor, std::isnan
#include <cstring> // #150 - for strcmp
@ -351,9 +353,21 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
// render to window
renderTexture.display();
//Resources::game->getWindow().draw(output);
target.draw(output);
// #106: Apply shader if set
if (shader && shader->shader) {
sf::Vector2f resolution(box.getSize().x, box.getSize().y);
PyShader::applyEngineUniforms(*shader->shader, resolution);
// Apply user uniforms
if (uniforms) {
uniforms->applyTo(*shader->shader);
}
target.draw(output, shader->shader.get());
} else {
target.draw(output);
}
}
UIGridPoint& UIGrid::at(int x, int y)
@ -2232,6 +2246,7 @@ PyGetSetDef UIGrid::getsetters[] = {
"Callback when a grid cell is clicked. Called with (cell_pos: Vector).", NULL},
{"hovered_cell", (getter)UIGrid::get_hovered_cell, NULL,
"Currently hovered cell as (x, y) tuple, or None if not hovering.", NULL},
UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UIGRID),
{NULL} /* Sentinel */
};
@ -2517,6 +2532,10 @@ bool UIGrid::setProperty(const std::string& name, float value) {
markDirty(); // #144 - Content change
return true;
}
// #106: Shader uniform properties
if (setShaderProperty(name, value)) {
return true;
}
return false;
}
@ -2592,6 +2611,10 @@ bool UIGrid::getProperty(const std::string& name, float& value) const {
value = static_cast<float>(fill_color.a);
return true;
}
// #106: Shader uniform properties
if (getShaderProperty(name, value)) {
return true;
}
return false;
}
@ -2625,5 +2648,9 @@ bool UIGrid::hasProperty(const std::string& name) const {
if (name == "position" || name == "size" || name == "center") {
return true;
}
// #106: Shader uniform properties
if (hasShaderProperty(name)) {
return true;
}
return false;
}

View file

@ -4,6 +4,8 @@
#include "PythonObjectCache.h"
#include "UIFrame.h" // #144: For snapshot= parameter
#include "PyAlignment.h"
#include "PyShader.h" // #106: Shader support
#include "PyUniformCollection.h" // #106: Uniform collection support
// UIDrawable methods now in UIBase.h
UIDrawable* UISprite::click_at(sf::Vector2f point)
@ -87,9 +89,43 @@ void UISprite::render(sf::Vector2f offset, sf::RenderTarget& target)
color.a = static_cast<sf::Uint8>(255 * opacity);
sprite.setColor(color);
// #106: Shader rendering path
if (shader && shader->shader) {
// Get the sprite bounds for rendering
auto bounds = sprite.getGlobalBounds();
sf::Vector2f screen_pos = offset + position;
// Get or create intermediate texture
auto& intermediate = GameEngine::getShaderIntermediate();
intermediate.clear(sf::Color::Transparent);
// Render sprite at origin in intermediate texture
sf::Sprite temp_sprite = sprite;
temp_sprite.setPosition(0, 0); // Render at origin of intermediate texture
intermediate.draw(temp_sprite);
intermediate.display();
// Create result sprite from intermediate texture
sf::Sprite result_sprite(intermediate.getTexture());
result_sprite.setPosition(screen_pos);
// Apply engine uniforms
sf::Vector2f resolution(bounds.width, bounds.height);
PyShader::applyEngineUniforms(*shader->shader, resolution);
// Apply user uniforms
if (uniforms) {
uniforms->applyTo(*shader->shader);
}
// Draw with shader
target.draw(result_sprite, shader->shader.get());
} else {
// Standard rendering path (no shader)
sprite.move(offset);
target.draw(sprite);
sprite.move(-offset);
}
// Restore original alpha
color.a = 255;
@ -359,6 +395,7 @@ PyGetSetDef UISprite::getsetters[] = {
UIDRAWABLE_GETSETTERS,
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UISPRITE),
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UISPRITE),
UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UISPRITE),
{NULL}
};
@ -591,6 +628,10 @@ bool UISprite::setProperty(const std::string& name, float value) {
markDirty(); // #144 - Z-order change affects parent
return true;
}
// #106: Check for shader uniform properties
if (setShaderProperty(name, value)) {
return true;
}
return false;
}
@ -633,6 +674,10 @@ bool UISprite::getProperty(const std::string& name, float& value) const {
value = static_cast<float>(z_index);
return true;
}
// #106: Check for shader uniform properties
if (getShaderProperty(name, value)) {
return true;
}
return false;
}
@ -659,5 +704,9 @@ bool UISprite::hasProperty(const std::string& name) const {
if (name == "sprite_index" || name == "sprite_number") {
return true;
}
// #106: Check for shader uniform properties
if (hasShaderProperty(name)) {
return true;
}
return false;
}

422
tests/unit/shader_test.py Normal file
View file

@ -0,0 +1,422 @@
#!/usr/bin/env python3
"""Unit tests for the Shader system (Issue #106)
Tests cover:
- Shader creation and compilation
- Static uniforms (float, vec2, vec3, vec4)
- PropertyBinding for dynamic uniform values
- CallableBinding for computed uniform values
- Shader assignment to various drawable types
- Dynamic flag propagation
"""
import mcrfpy
import sys
def test_shader_creation():
"""Test basic shader creation"""
print("Testing shader creation...")
# Valid shader
shader = mcrfpy.Shader('''
uniform sampler2D texture;
void main() {
gl_FragColor = texture2D(texture, gl_TexCoord[0].xy);
}
''')
assert shader is not None, "Shader should be created"
assert shader.is_valid, "Shader should be valid"
assert shader.dynamic == False, "Shader should not be dynamic by default"
# Dynamic shader
dynamic_shader = mcrfpy.Shader('''
uniform sampler2D texture;
uniform float time;
void main() {
gl_FragColor = texture2D(texture, gl_TexCoord[0].xy);
}
''', dynamic=True)
assert dynamic_shader.dynamic == True, "Shader should be dynamic when specified"
print(" PASS: Basic shader creation works")
def test_shader_source():
"""Test that shader source is stored correctly"""
print("Testing shader source storage...")
source = '''uniform sampler2D texture;
void main() {
gl_FragColor = texture2D(texture, gl_TexCoord[0].xy);
}'''
shader = mcrfpy.Shader(source)
assert source in shader.source, "Shader source should be stored"
print(" PASS: Shader source is stored")
def test_static_uniforms():
"""Test static uniform values"""
print("Testing static uniforms...")
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
# Test float uniform
frame.uniforms['intensity'] = 0.5
assert abs(frame.uniforms['intensity'] - 0.5) < 0.001, "Float uniform should match"
# Test vec2 uniform
frame.uniforms['offset'] = (10.0, 20.0)
val = frame.uniforms['offset']
assert len(val) == 2, "Vec2 should have 2 components"
assert abs(val[0] - 10.0) < 0.001, "Vec2.x should match"
assert abs(val[1] - 20.0) < 0.001, "Vec2.y should match"
# Test vec3 uniform
frame.uniforms['color_rgb'] = (1.0, 0.5, 0.0)
val = frame.uniforms['color_rgb']
assert len(val) == 3, "Vec3 should have 3 components"
# Test vec4 uniform
frame.uniforms['color'] = (1.0, 0.5, 0.0, 1.0)
val = frame.uniforms['color']
assert len(val) == 4, "Vec4 should have 4 components"
print(" PASS: Static uniforms work")
def test_uniform_keys():
"""Test uniform collection keys/values/items"""
print("Testing uniform collection methods...")
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
frame.uniforms['a'] = 1.0
frame.uniforms['b'] = 2.0
frame.uniforms['c'] = 3.0
keys = frame.uniforms.keys()
assert 'a' in keys, "Key 'a' should be present"
assert 'b' in keys, "Key 'b' should be present"
assert 'c' in keys, "Key 'c' should be present"
assert len(keys) == 3, "Should have 3 keys"
# Test 'in' operator
assert 'a' in frame.uniforms, "'in' operator should work"
assert 'nonexistent' not in frame.uniforms, "'not in' should work"
# Test deletion
del frame.uniforms['b']
assert 'b' not in frame.uniforms, "Deleted key should be gone"
assert len(frame.uniforms.keys()) == 2, "Should have 2 keys after deletion"
print(" PASS: Uniform collection methods work")
def test_property_binding():
"""Test PropertyBinding for dynamic uniform values"""
print("Testing PropertyBinding...")
# Create source and target frames
source_frame = mcrfpy.Frame(pos=(100, 200), size=(50, 50))
target_frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
# Create binding to source frame's x position
binding = mcrfpy.PropertyBinding(source_frame, 'x')
assert binding is not None, "PropertyBinding should be created"
assert binding.property == 'x', "Property name should be stored"
assert abs(binding.value - 100.0) < 0.001, "Initial value should be 100"
assert binding.is_valid == True, "Binding should be valid"
# Assign binding to uniform
target_frame.uniforms['source_x'] = binding
# Check that value tracks changes
source_frame.x = 300
assert abs(binding.value - 300.0) < 0.001, "Binding should track changes"
print(" PASS: PropertyBinding works")
def test_callable_binding():
"""Test CallableBinding for computed uniform values"""
print("Testing CallableBinding...")
counter = [0] # Use list for closure
def compute_value():
counter[0] += 1
return counter[0] * 0.1
binding = mcrfpy.CallableBinding(compute_value)
assert binding is not None, "CallableBinding should be created"
assert binding.is_valid == True, "Binding should be valid"
# Each access should call the function
v1 = binding.value
v2 = binding.value
v3 = binding.value
assert abs(v1 - 0.1) < 0.001, "First call should return 0.1"
assert abs(v2 - 0.2) < 0.001, "Second call should return 0.2"
assert abs(v3 - 0.3) < 0.001, "Third call should return 0.3"
# Assign to uniform
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
frame.uniforms['computed'] = binding
print(" PASS: CallableBinding works")
def test_shader_on_frame():
"""Test shader assignment to Frame"""
print("Testing shader on Frame...")
shader = mcrfpy.Shader('''
uniform sampler2D texture;
uniform float intensity;
void main() {
vec4 color = texture2D(texture, gl_TexCoord[0].xy);
color.rgb *= intensity;
gl_FragColor = color;
}
''')
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
assert frame.shader is None, "Shader should be None initially"
frame.shader = shader
assert frame.shader is not None, "Shader should be assigned"
frame.uniforms['intensity'] = 0.8
assert abs(frame.uniforms['intensity'] - 0.8) < 0.001, "Uniform should be set"
# Test shader removal
frame.shader = None
assert frame.shader is None, "Shader should be removable"
print(" PASS: Shader on Frame works")
def test_shader_on_sprite():
"""Test shader assignment to Sprite"""
print("Testing shader on Sprite...")
shader = mcrfpy.Shader('''
uniform sampler2D texture;
void main() {
gl_FragColor = texture2D(texture, gl_TexCoord[0].xy);
}
''')
sprite = mcrfpy.Sprite(pos=(0, 0))
assert sprite.shader is None, "Shader should be None initially"
sprite.shader = shader
assert sprite.shader is not None, "Shader should be assigned"
sprite.uniforms['test'] = 1.0
assert abs(sprite.uniforms['test'] - 1.0) < 0.001, "Uniform should be set"
print(" PASS: Shader on Sprite works")
def test_shader_on_caption():
"""Test shader assignment to Caption"""
print("Testing shader on Caption...")
shader = mcrfpy.Shader('''
uniform sampler2D texture;
void main() {
gl_FragColor = texture2D(texture, gl_TexCoord[0].xy);
}
''')
caption = mcrfpy.Caption(text="Test", pos=(0, 0))
assert caption.shader is None, "Shader should be None initially"
caption.shader = shader
assert caption.shader is not None, "Shader should be assigned"
caption.uniforms['test'] = 1.0
assert abs(caption.uniforms['test'] - 1.0) < 0.001, "Uniform should be set"
print(" PASS: Shader on Caption works")
def test_shader_on_grid():
"""Test shader assignment to Grid"""
print("Testing shader on Grid...")
shader = mcrfpy.Shader('''
uniform sampler2D texture;
void main() {
gl_FragColor = texture2D(texture, gl_TexCoord[0].xy);
}
''')
grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(200, 200))
assert grid.shader is None, "Shader should be None initially"
grid.shader = shader
assert grid.shader is not None, "Shader should be assigned"
grid.uniforms['test'] = 1.0
assert abs(grid.uniforms['test'] - 1.0) < 0.001, "Uniform should be set"
print(" PASS: Shader on Grid works")
def test_shader_on_entity():
"""Test shader assignment to Entity"""
print("Testing shader on Entity...")
shader = mcrfpy.Shader('''
uniform sampler2D texture;
void main() {
gl_FragColor = texture2D(texture, gl_TexCoord[0].xy);
}
''')
entity = mcrfpy.Entity()
assert entity.shader is None, "Shader should be None initially"
entity.shader = shader
assert entity.shader is not None, "Shader should be assigned"
entity.uniforms['test'] = 1.0
assert abs(entity.uniforms['test'] - 1.0) < 0.001, "Uniform should be set"
print(" PASS: Shader on Entity works")
def test_shared_shader():
"""Test that multiple drawables can share the same shader"""
print("Testing shared shader...")
shader = mcrfpy.Shader('''
uniform sampler2D texture;
uniform float intensity;
void main() {
vec4 color = texture2D(texture, gl_TexCoord[0].xy);
color.rgb *= intensity;
gl_FragColor = color;
}
''')
frame1 = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
frame2 = mcrfpy.Frame(pos=(100, 0), size=(100, 100))
# Assign same shader to both
frame1.shader = shader
frame2.shader = shader
# But different uniform values
frame1.uniforms['intensity'] = 0.5
frame2.uniforms['intensity'] = 1.0
assert abs(frame1.uniforms['intensity'] - 0.5) < 0.001, "Frame1 intensity should be 0.5"
assert abs(frame2.uniforms['intensity'] - 1.0) < 0.001, "Frame2 intensity should be 1.0"
print(" PASS: Shared shader with different uniforms works")
def test_shader_animation_properties():
"""Test that shader uniforms can be animated via the animation system"""
print("Testing shader animation properties...")
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
# Set initial uniform value
frame.uniforms['intensity'] = 0.0
# Test animate() method with shader.X property syntax
# This uses hasProperty/setProperty internally
try:
frame.animate('shader.intensity', 1.0, 0.5, mcrfpy.Easing.LINEAR)
animation_works = True
except Exception as e:
animation_works = False
print(f" Animation error: {e}")
assert animation_works, "Animating shader uniforms should work"
# Test with different drawable types
sprite = mcrfpy.Sprite(pos=(0, 0))
sprite.uniforms['glow'] = 0.0
try:
sprite.animate('shader.glow', 2.0, 1.0, mcrfpy.Easing.EASE_IN)
sprite_animation_works = True
except Exception as e:
sprite_animation_works = False
print(f" Sprite animation error: {e}")
assert sprite_animation_works, "Animating Sprite shader uniforms should work"
# Test Caption
caption = mcrfpy.Caption(text="Test", pos=(0, 0))
caption.uniforms['alpha'] = 1.0
try:
caption.animate('shader.alpha', 0.0, 0.5, mcrfpy.Easing.EASE_OUT)
caption_animation_works = True
except Exception as e:
caption_animation_works = False
print(f" Caption animation error: {e}")
assert caption_animation_works, "Animating Caption shader uniforms should work"
# Test Grid
grid = mcrfpy.Grid(grid_size=(5, 5), pos=(0, 0), size=(100, 100))
grid.uniforms['zoom_effect'] = 1.0
try:
grid.animate('shader.zoom_effect', 2.0, 1.0, mcrfpy.Easing.LINEAR)
grid_animation_works = True
except Exception as e:
grid_animation_works = False
print(f" Grid animation error: {e}")
assert grid_animation_works, "Animating Grid shader uniforms should work"
print(" PASS: Shader animation properties work")
def run_all_tests():
"""Run all shader tests"""
print("=" * 50)
print("Shader System Unit Tests")
print("=" * 50)
print()
try:
test_shader_creation()
test_shader_source()
test_static_uniforms()
test_uniform_keys()
test_property_binding()
test_callable_binding()
test_shader_on_frame()
test_shader_on_sprite()
test_shader_on_caption()
test_shader_on_grid()
test_shader_on_entity()
test_shared_shader()
test_shader_animation_properties()
print()
print("=" * 50)
print("ALL TESTS PASSED")
print("=" * 50)
sys.exit(0)
except AssertionError as e:
print(f" FAIL: {e}")
sys.exit(1)
except Exception as e:
print(f" ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
run_all_tests()