#include "PyShader.h" #include "McRFPy_API.h" #include "McRFPy_Doc.h" #include "GameEngine.h" #include "Resources.h" #include // 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(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(); // 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) { 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(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(mousePos.x), static_cast(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(); }