chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -111,6 +111,8 @@ void BPY_DECREF_RNA_INVALIDATE(void *pyob_ptr);
|
||||
* \param C: The context to set. In rare cases C can be null.
|
||||
*/
|
||||
void BPY_context_set(bContext *C);
|
||||
/** Return the context currently published through the real bpy.context slot. */
|
||||
bContext *BPY_context_get();
|
||||
/**
|
||||
* Use for updating while a python script runs - in case of file load.
|
||||
* \param C: The context to activate as `bpy.context`. In rare cases C can be null.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -67,6 +67,31 @@ struct BPy_IDGroup_Iter {
|
||||
int len_init;
|
||||
/** Iterate in the reverse direction. */
|
||||
bool reversed;
|
||||
#ifdef WITH_WEB
|
||||
bool invalidated;
|
||||
#endif
|
||||
};
|
||||
|
||||
/** Track actual native allocations without retaining Python objects or running finalizers. */
|
||||
void BPy_IDProperty_Track(PyObject *object, IDProperty **property, IDProperty **parent = nullptr);
|
||||
void BPy_IDProperty_Untrack(PyObject *object);
|
||||
bool BPy_IDProperty_IsValid(const IDProperty *property);
|
||||
|
||||
/** Observe a native target across Python conversion, without retaining freed native memory.
|
||||
* Web guards also compare numeric array bytes before commit, including writes through buffers
|
||||
* exported before or during the conversion, and reject latched host cancellation/callback failure.
|
||||
* Native writable buffer semantics are preserved. */
|
||||
struct BPy_IDPropertyMutation {
|
||||
const IDProperty *property;
|
||||
bool changed = false;
|
||||
|
||||
/** Observe descendants when the conversion may replace the entire target tree. Container
|
||||
* membership guards keep observing only the container itself. */
|
||||
explicit BPy_IDPropertyMutation(const IDProperty *property, bool recursive = false);
|
||||
~BPy_IDPropertyMutation();
|
||||
BPy_IDPropertyMutation(const BPy_IDPropertyMutation &) = delete;
|
||||
BPy_IDPropertyMutation &operator=(const BPy_IDPropertyMutation &) = delete;
|
||||
bool check() const;
|
||||
};
|
||||
|
||||
/** Use to implement `IDPropertyGroup.keys/values/items` */
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
#include <Python.h>
|
||||
#include <climits>
|
||||
|
||||
#include "python_compat.hh" /* IWYU pragma: keep. */
|
||||
|
||||
@@ -18,6 +19,7 @@
|
||||
#include "idprop_py_ui_api.hh"
|
||||
|
||||
#include "BKE_idprop.hh"
|
||||
#include "idprop_py_api.hh"
|
||||
|
||||
#include "DNA_ID.h"
|
||||
|
||||
@@ -63,8 +65,8 @@ static bool idprop_ui_data_update_base(IDPropertyUIData *ui_data,
|
||||
static bool py_long_as_int(PyObject *py_long, int *r_int)
|
||||
{
|
||||
if (PyLong_CheckExact(py_long)) {
|
||||
*r_int = int(PyLong_AS_LONG(py_long));
|
||||
return true;
|
||||
*r_int = PyC_Long_AsI32(py_long);
|
||||
return !PyErr_Occurred();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -88,10 +90,16 @@ static bool try_parse_enum_item(PyObject *py_item, const int index, IDPropertyUI
|
||||
Py_ssize_t name_len;
|
||||
Py_ssize_t description_len;
|
||||
const char *identifier = PyUnicode_AsUTF8AndSize(PyTuple_GET_ITEM(py_item, 0), &identifier_len);
|
||||
if (!identifier) {
|
||||
return false;
|
||||
}
|
||||
const char *name = PyUnicode_AsUTF8AndSize(PyTuple_GET_ITEM(py_item, 1), &name_len);
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
const char *description = PyUnicode_AsUTF8AndSize(PyTuple_GET_ITEM(py_item, 2),
|
||||
&description_len);
|
||||
if (!identifier || !name || !description) {
|
||||
if (!description) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -106,10 +114,17 @@ static bool try_parse_enum_item(PyObject *py_item, const int index, IDPropertyUI
|
||||
}
|
||||
else if (item_size == 5) {
|
||||
/* Must have icon value or name. */
|
||||
if (!py_long_as_int(PyTuple_GET_ITEM(py_item, 3), &item.icon) &&
|
||||
!(icon_name = PyUnicode_AsUTF8(PyTuple_GET_ITEM(py_item, 3))))
|
||||
{
|
||||
return false;
|
||||
PyObject *icon = PyTuple_GET_ITEM(py_item, 3);
|
||||
if (PyLong_CheckExact(icon)) {
|
||||
if (!py_long_as_int(icon, &item.icon)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
icon_name = PyUnicode_AsUTF8(icon);
|
||||
if (!icon_name) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!py_long_as_int(PyTuple_GET_ITEM(py_item, 4), &item.value)) {
|
||||
return false;
|
||||
@@ -133,6 +148,10 @@ static IDPropertyUIDataEnumItem *idprop_enum_items_from_py(PyObject *seq_fast, i
|
||||
PyObject **seq_fast_items = PySequence_Fast_ITEMS(seq_fast);
|
||||
int i;
|
||||
|
||||
if (seq_len > INT_MAX) {
|
||||
PyErr_SetString(PyExc_OverflowError, "Too many IDProperty enum items");
|
||||
return nullptr;
|
||||
}
|
||||
items = MEM_new_array<IDPropertyUIDataEnumItem>(seq_len, __func__);
|
||||
r_items_num = seq_len;
|
||||
|
||||
@@ -143,14 +162,21 @@ static IDPropertyUIDataEnumItem *idprop_enum_items_from_py(PyObject *seq_fast, i
|
||||
items[i] = item;
|
||||
}
|
||||
else if (py_item == Py_None) {
|
||||
items[i].identifier = nullptr;
|
||||
items[i] = {};
|
||||
}
|
||||
else {
|
||||
for (int j = 0; j < i; j++) {
|
||||
MEM_SAFE_DELETE(items[j].identifier);
|
||||
MEM_SAFE_DELETE(items[j].name);
|
||||
MEM_SAFE_DELETE(items[j].description);
|
||||
}
|
||||
MEM_delete(items);
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"expected a tuple containing "
|
||||
"(identifier, name, description) and optionally an "
|
||||
"icon name and unique number");
|
||||
if (!PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"expected a tuple containing "
|
||||
"(identifier, name, description) and optionally an "
|
||||
"icon name and unique number");
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
@@ -172,23 +198,33 @@ static bool idprop_ui_data_update_int_default(IDProperty *idprop,
|
||||
return false;
|
||||
}
|
||||
|
||||
Py_ssize_t len = PySequence_Size(default_value);
|
||||
PyObject *values = PySequence_Tuple(default_value);
|
||||
if (!values) {
|
||||
return false;
|
||||
}
|
||||
const Py_ssize_t len = PyTuple_GET_SIZE(values);
|
||||
if (len > INT_MAX) {
|
||||
Py_DECREF(values);
|
||||
PyErr_SetString(PyExc_OverflowError, "IDProperty default array is too large");
|
||||
return false;
|
||||
}
|
||||
int *new_default_array = MEM_new_array_uninitialized<int>(size_t(len), __func__);
|
||||
if (PyC_AsArray(
|
||||
new_default_array, sizeof(int), default_value, len, &PyLong_Type, "ui_data_update") ==
|
||||
new_default_array, sizeof(int), values, len, &PyLong_Type, "ui_data_update") ==
|
||||
-1)
|
||||
{
|
||||
Py_DECREF(values);
|
||||
MEM_delete(new_default_array);
|
||||
return false;
|
||||
}
|
||||
|
||||
ui_data->default_array_len = len;
|
||||
Py_DECREF(values);
|
||||
ui_data->default_array_len = int(len);
|
||||
ui_data->default_array = new_default_array;
|
||||
}
|
||||
else {
|
||||
const int value = PyC_Long_AsI32(default_value);
|
||||
if ((value == -1) && PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_ValueError, "Cannot convert \"default\" argument to integer");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -304,8 +340,10 @@ static bool idprop_ui_data_update_int(IDProperty *idprop, PyObject *args, PyObje
|
||||
|
||||
if (!ELEM(items, nullptr, Py_None)) {
|
||||
PyObject *items_fast;
|
||||
if (!(items_fast = PySequence_Fast(items, "expected a sequence of tuples for the enum items")))
|
||||
if (!(items_fast = PySequence_Tuple(items)))
|
||||
{
|
||||
IDP_ui_data_free_unique_contents(
|
||||
&ui_data.base, IDP_ui_data_type(idprop), &ui_data_orig->base);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -317,7 +355,9 @@ static bool idprop_ui_data_update_int(IDProperty *idprop, PyObject *args, PyObje
|
||||
const bool has_error = (idprop_items == nullptr) ||
|
||||
!IDP_EnumItemsValidate(
|
||||
idprop_items, idprop_items_num, [](const char *msg) {
|
||||
PyErr_SetString(PyExc_ValueError, msg);
|
||||
if (!PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_ValueError, msg);
|
||||
}
|
||||
});
|
||||
Py_DECREF(items_fast);
|
||||
if (idprop_items != nullptr) {
|
||||
@@ -358,26 +398,36 @@ static bool idprop_ui_data_update_bool_default(IDProperty *idprop,
|
||||
return false;
|
||||
}
|
||||
|
||||
Py_ssize_t len = PySequence_Size(default_value);
|
||||
PyObject *values = PySequence_Tuple(default_value);
|
||||
if (!values) {
|
||||
return false;
|
||||
}
|
||||
const Py_ssize_t len = PyTuple_GET_SIZE(values);
|
||||
if (len > INT_MAX) {
|
||||
Py_DECREF(values);
|
||||
PyErr_SetString(PyExc_OverflowError, "IDProperty default array is too large");
|
||||
return false;
|
||||
}
|
||||
int8_t *new_default_array = MEM_new_array_uninitialized<int8_t>(size_t(len), __func__);
|
||||
if (PyC_AsArray(new_default_array,
|
||||
sizeof(int8_t),
|
||||
default_value,
|
||||
values,
|
||||
len,
|
||||
&PyBool_Type,
|
||||
"ui_data_update") == -1)
|
||||
{
|
||||
Py_DECREF(values);
|
||||
MEM_delete(new_default_array);
|
||||
return false;
|
||||
}
|
||||
|
||||
ui_data->default_array_len = len;
|
||||
Py_DECREF(values);
|
||||
ui_data->default_array_len = int(len);
|
||||
ui_data->default_array = new_default_array;
|
||||
}
|
||||
else {
|
||||
const int value = PyC_Long_AsBool(default_value);
|
||||
if ((value == -1) && PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_ValueError, "Cannot convert \"default\" argument to integer");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -448,26 +498,36 @@ static bool idprop_ui_data_update_float_default(IDProperty *idprop,
|
||||
return false;
|
||||
}
|
||||
|
||||
Py_ssize_t len = PySequence_Size(default_value);
|
||||
PyObject *values = PySequence_Tuple(default_value);
|
||||
if (!values) {
|
||||
return false;
|
||||
}
|
||||
const Py_ssize_t len = PyTuple_GET_SIZE(values);
|
||||
if (len > INT_MAX) {
|
||||
Py_DECREF(values);
|
||||
PyErr_SetString(PyExc_OverflowError, "IDProperty default array is too large");
|
||||
return false;
|
||||
}
|
||||
double *new_default_array = MEM_new_array_uninitialized<double>(size_t(len), __func__);
|
||||
if (PyC_AsArray(new_default_array,
|
||||
sizeof(double),
|
||||
default_value,
|
||||
values,
|
||||
len,
|
||||
&PyFloat_Type,
|
||||
"ui_data_update") == -1)
|
||||
{
|
||||
Py_DECREF(values);
|
||||
MEM_delete(new_default_array);
|
||||
return false;
|
||||
}
|
||||
|
||||
ui_data->default_array_len = len;
|
||||
Py_DECREF(values);
|
||||
ui_data->default_array_len = int(len);
|
||||
ui_data->default_array = new_default_array;
|
||||
}
|
||||
else {
|
||||
const double value = PyFloat_AsDouble(default_value);
|
||||
if ((value == -1.0) && PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_ValueError, "Cannot convert \"default\" argument to double");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -665,6 +725,8 @@ static bool idprop_ui_data_update_id(IDProperty *idprop, PyObject *args, PyObjec
|
||||
if (pyrna_enum_value_from_id(
|
||||
rna_enum_id_type_items, id_type, &id_type_tmp, "IDPropertyUIManager.update") == -1)
|
||||
{
|
||||
IDP_ui_data_free_unique_contents(
|
||||
&ui_data.base, IDP_ui_data_type(idprop), &ui_data_orig->base);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -700,46 +762,59 @@ static PyObject *BPy_IDPropertyUIManager_update(BPy_IDPropertyUIManager *self,
|
||||
PyObject *args,
|
||||
PyObject *kwargs)
|
||||
{
|
||||
IDProperty *property = self->property;
|
||||
BLI_assert(IDP_ui_data_supported(property));
|
||||
|
||||
switch (IDP_ui_data_type(property)) {
|
||||
case IDP_UI_DATA_TYPE_INT:
|
||||
IDP_ui_data_ensure(property);
|
||||
if (!idprop_ui_data_update_int(property, args, kwargs)) {
|
||||
return nullptr;
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
case IDP_UI_DATA_TYPE_BOOLEAN:
|
||||
IDP_ui_data_ensure(property);
|
||||
if (!idprop_ui_data_update_bool(property, args, kwargs)) {
|
||||
return nullptr;
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
case IDP_UI_DATA_TYPE_FLOAT:
|
||||
IDP_ui_data_ensure(property);
|
||||
if (!idprop_ui_data_update_float(property, args, kwargs)) {
|
||||
return nullptr;
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
case IDP_UI_DATA_TYPE_STRING:
|
||||
IDP_ui_data_ensure(property);
|
||||
if (!idprop_ui_data_update_string(property, args, kwargs)) {
|
||||
return nullptr;
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
case IDP_UI_DATA_TYPE_ID:
|
||||
IDP_ui_data_ensure(property);
|
||||
if (!idprop_ui_data_update_id(property, args, kwargs)) {
|
||||
return nullptr;
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
case IDP_UI_DATA_TYPE_UNSUPPORTED:
|
||||
PyErr_Format(PyExc_TypeError, "IDProperty \"%s\" does not support RNA data", property->name);
|
||||
return nullptr;
|
||||
if (!BPy_IDProperty_IsValid(self->property)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BLI_assert_unreachable();
|
||||
IDProperty *property = self->property;
|
||||
BPy_IDPropertyMutation target(property);
|
||||
/* Argument conversion runs arbitrary Python. All helpers operate on private UI data,
|
||||
* including their shallow intermediate copies; the live property is never borrowed. */
|
||||
IDProperty staged = *property;
|
||||
staged.ui_data = property->ui_data ? IDP_ui_data_copy(property) : nullptr;
|
||||
if (!IDP_ui_data_supported(&staged)) {
|
||||
PyErr_Format(PyExc_TypeError, "IDProperty \"%s\" does not support RNA data", staged.name);
|
||||
return nullptr;
|
||||
}
|
||||
IDP_ui_data_ensure(&staged);
|
||||
PyObject *keywords = kwargs ? PyDict_Copy(kwargs) : nullptr;
|
||||
if (kwargs && !keywords) {
|
||||
IDP_ui_data_free(&staged);
|
||||
return nullptr;
|
||||
}
|
||||
bool ok = false;
|
||||
switch (IDP_ui_data_type(&staged)) {
|
||||
case IDP_UI_DATA_TYPE_INT:
|
||||
ok = idprop_ui_data_update_int(&staged, args, keywords);
|
||||
break;
|
||||
case IDP_UI_DATA_TYPE_BOOLEAN:
|
||||
ok = idprop_ui_data_update_bool(&staged, args, keywords);
|
||||
break;
|
||||
case IDP_UI_DATA_TYPE_FLOAT:
|
||||
ok = idprop_ui_data_update_float(&staged, args, keywords);
|
||||
break;
|
||||
case IDP_UI_DATA_TYPE_STRING:
|
||||
ok = idprop_ui_data_update_string(&staged, args, keywords);
|
||||
break;
|
||||
case IDP_UI_DATA_TYPE_ID:
|
||||
ok = idprop_ui_data_update_id(&staged, args, keywords);
|
||||
break;
|
||||
case IDP_UI_DATA_TYPE_UNSUPPORTED:
|
||||
BLI_assert_unreachable();
|
||||
break;
|
||||
}
|
||||
/* Release temporary Python owners before the last native identity check. */
|
||||
Py_XDECREF(keywords);
|
||||
if (!ok || !target.check()) {
|
||||
IDP_ui_data_free(&staged);
|
||||
return nullptr;
|
||||
}
|
||||
if (property->ui_data) {
|
||||
IDP_ui_data_free(property);
|
||||
}
|
||||
property->ui_data = staged.ui_data;
|
||||
#ifdef WITH_WEB
|
||||
IDPropertyLifetime::changed(property);
|
||||
#endif
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
@@ -893,6 +968,9 @@ PyDoc_STRVAR(
|
||||
" returned dictionary and their types will depend on the property's type.\n");
|
||||
static PyObject *BPy_IDIDPropertyUIManager_as_dict(BPy_IDPropertyUIManager *self)
|
||||
{
|
||||
if (!BPy_IDProperty_IsValid(self->property)) {
|
||||
return nullptr;
|
||||
}
|
||||
IDProperty *property = self->property;
|
||||
BLI_assert(IDP_ui_data_supported(property));
|
||||
|
||||
@@ -955,6 +1033,9 @@ PyDoc_STRVAR(
|
||||
" Remove the RNA UI data from this IDProperty.\n");
|
||||
static PyObject *BPy_IDPropertyUIManager_clear(BPy_IDPropertyUIManager *self)
|
||||
{
|
||||
if (!BPy_IDProperty_IsValid(self->property)) {
|
||||
return nullptr;
|
||||
}
|
||||
IDProperty *property = self->property;
|
||||
BLI_assert(IDP_ui_data_supported(property));
|
||||
|
||||
@@ -988,6 +1069,9 @@ PyDoc_STRVAR(
|
||||
" :raises TypeError: If the types of the two properties don't match.\n");
|
||||
static PyObject *BPy_IDPropertyUIManager_update_from(BPy_IDPropertyUIManager *self, PyObject *args)
|
||||
{
|
||||
if (!BPy_IDProperty_IsValid(self->property)) {
|
||||
return nullptr;
|
||||
}
|
||||
IDProperty *property = self->property;
|
||||
BLI_assert(IDP_ui_data_supported(property));
|
||||
|
||||
@@ -997,6 +1081,12 @@ static PyObject *BPy_IDPropertyUIManager_update_from(BPy_IDPropertyUIManager *se
|
||||
}
|
||||
|
||||
IDProperty *src_prop = ui_manager_src->property;
|
||||
if (!BPy_IDProperty_IsValid(src_prop)) {
|
||||
return nullptr;
|
||||
}
|
||||
if (src_prop == property) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
if ((property->type != src_prop->type) || property->subtype != src_prop->subtype) {
|
||||
PyErr_SetString(PyExc_TypeError, "Properties type does not match.");
|
||||
return nullptr;
|
||||
@@ -1009,6 +1099,9 @@ static PyObject *BPy_IDPropertyUIManager_update_from(BPy_IDPropertyUIManager *se
|
||||
if (ui_manager_src->property && ui_manager_src->property->ui_data) {
|
||||
property->ui_data = IDP_ui_data_copy(src_prop);
|
||||
}
|
||||
#ifdef WITH_WEB
|
||||
IDPropertyLifetime::changed(property);
|
||||
#endif
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
@@ -1059,13 +1152,22 @@ static PyMethodDef BPy_IDPropertyUIManager_methods[] = {
|
||||
|
||||
static PyObject *BPy_IDPropertyUIManager_repr(BPy_IDPropertyUIManager *self)
|
||||
{
|
||||
if (!BPy_IDProperty_IsValid(self->property)) {
|
||||
return nullptr;
|
||||
}
|
||||
return PyUnicode_FromFormat(
|
||||
"<bpy id prop ui manager: name=\"%s\", address=%p>", self->property->name, self->property);
|
||||
}
|
||||
|
||||
static Py_hash_t BPy_IDPropertyUIManager_hash(BPy_IDPropertyUIManager *self)
|
||||
{
|
||||
return Py_HashPointer(self->property);
|
||||
return Py_HashPointer(self);
|
||||
}
|
||||
|
||||
static void BPy_IDPropertyUIManager_dealloc(PyObject *self)
|
||||
{
|
||||
BPy_IDProperty_Untrack(self);
|
||||
PyObject_Del(self);
|
||||
}
|
||||
|
||||
PyTypeObject BPy_IDPropertyUIManager_Type = {
|
||||
@@ -1074,7 +1176,7 @@ PyTypeObject BPy_IDPropertyUIManager_Type = {
|
||||
/*tp_name*/ "IDPropertyUIManager",
|
||||
/*tp_basicsize*/ sizeof(BPy_IDPropertyUIManager),
|
||||
/*tp_itemsize*/ 0,
|
||||
/*tp_dealloc*/ nullptr,
|
||||
/*tp_dealloc*/ BPy_IDPropertyUIManager_dealloc,
|
||||
/*tp_vectorcall_offset*/ 0,
|
||||
/*tp_getattr*/ nullptr,
|
||||
/*tp_setattr*/ nullptr,
|
||||
|
||||
@@ -154,10 +154,11 @@ int pyrna_enum_bitfield_from_set(const EnumPropertyItem *items,
|
||||
if (PySet_GET_SIZE(value) > 0) {
|
||||
/* Set looping. */
|
||||
PyObject *it = PyObject_GetIter(value);
|
||||
if (!it) {
|
||||
return -1;
|
||||
}
|
||||
while ((key = PyIter_Next(it))) {
|
||||
/* Borrow from the set. */
|
||||
Py_DECREF(key);
|
||||
|
||||
/* A set subclass may yield values that the set itself does not own. */
|
||||
const char *param = PyUnicode_AsUTF8(key);
|
||||
if (param == nullptr) {
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
@@ -173,9 +174,12 @@ int pyrna_enum_bitfield_from_set(const EnumPropertyItem *items,
|
||||
}
|
||||
|
||||
flag |= ret;
|
||||
Py_CLEAR(key);
|
||||
}
|
||||
const bool failed = key != nullptr || PyErr_Occurred();
|
||||
Py_XDECREF(key);
|
||||
Py_DECREF(it);
|
||||
if (key) {
|
||||
if (failed || PyErr_Occurred()) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ set(SRC
|
||||
gpu_py_buffer.cc
|
||||
gpu_py_capabilities.cc
|
||||
gpu_py_compute.cc
|
||||
gpu_py_compute_web.cc
|
||||
gpu_py_element.cc
|
||||
gpu_py_framebuffer.cc
|
||||
gpu_py_matrix.cc
|
||||
|
||||
@@ -116,6 +116,23 @@ static PyObject *pygpu_compute_dispatch(PyObject * /*self*/, PyObject *args, PyO
|
||||
#endif
|
||||
|
||||
static PyMethodDef pygpu_compute__tp_methods[] = {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
{"dispatch_glsl",
|
||||
reinterpret_cast<PyCFunction>(bpygpu_compute_dispatch_glsl),
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
"dispatch_glsl(source, buffers, workgroups, entry_point='main', binding_types=None, "
|
||||
"dispatches=None, timeout_ms=30000) -> tuple[bytes, ...]. "
|
||||
"Compile GLSL compute to WGSL with native Naga WASM, execute on WebGPU, "
|
||||
"and return completed storage contents. Use explicit set=0 binding indices."},
|
||||
{"dispatch_wgsl",
|
||||
reinterpret_cast<PyCFunction>(bpygpu_compute_dispatch_wgsl),
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
"dispatch_wgsl(source, buffers, workgroups, entry_point='main', binding_types=None, "
|
||||
"dispatches=None, timeout_ms=30000) -> tuple[bytes, ...]. "
|
||||
"Execute real WebGPU buffer compute and return completed buffer contents. "
|
||||
"Bindings use group 0 and their sequence index. Inputs are bytes-like or output byte sizes. "
|
||||
"Requires an isolated browser engine worker; never falls back to CPU kernel execution."},
|
||||
#endif
|
||||
{"dispatch",
|
||||
reinterpret_cast<PyCFunction>(pygpu_compute_dispatch),
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
|
||||
@@ -13,5 +13,9 @@
|
||||
namespace blender {
|
||||
|
||||
[[nodiscard]] PyObject *bpygpu_compute_init();
|
||||
#ifdef __EMSCRIPTEN__
|
||||
PyObject *bpygpu_compute_dispatch_wgsl(PyObject *self, PyObject *args, PyObject *kwargs);
|
||||
PyObject *bpygpu_compute_dispatch_glsl(PyObject *self, PyObject *args, PyObject *kwargs);
|
||||
#endif
|
||||
|
||||
} // namespace blender
|
||||
|
||||
160
blender-5.2.0/source/blender/python/gpu/gpu_py_compute_web.cc
Normal file
160
blender-5.2.0/source/blender/python/gpu/gpu_py_compute_web.cc
Normal file
@@ -0,0 +1,160 @@
|
||||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#define PY_SSIZE_T_CLEAN
|
||||
#include <Python.h>
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
# include <emscripten.h>
|
||||
# include <cstdint>
|
||||
# include <memory>
|
||||
# include <vector>
|
||||
# include <exception>
|
||||
|
||||
/* This is a browser API transport, not a CPU implementation of the kernel.
|
||||
* The owning engine worker blocks while a separate worker submits actual
|
||||
* WebGPU compute commands and maps their completed storage buffers. */
|
||||
EM_JS(int, bpy_webgpu_compute, (const char *metadata, const uint32_t *buffers, int count,
|
||||
int timeout, char *error, int error_capacity), {
|
||||
try {
|
||||
if (typeof Module['webGPUComputeSync'] !== 'function') {
|
||||
throw new Error('This WebEngine host has no native WebGPU compute transport');
|
||||
}
|
||||
Module['webGPUComputeSync'](metadata, buffers, count, timeout);
|
||||
return 0;
|
||||
} catch (reason) {
|
||||
stringToUTF8(String(reason && reason.message || reason), error, error_capacity);
|
||||
return -1;
|
||||
}
|
||||
});
|
||||
|
||||
namespace blender {
|
||||
namespace {
|
||||
struct PyRelease { void operator()(PyObject *value) const { Py_XDECREF(value); } };
|
||||
using PyOwned = std::unique_ptr<PyObject, PyRelease>;
|
||||
constexpr Py_ssize_t max_buffer_bytes = 64 * 1024 * 1024;
|
||||
constexpr Py_ssize_t max_total_bytes = 256 * 1024 * 1024;
|
||||
}
|
||||
|
||||
static PyObject *compute_dispatch_source(PyObject *args, PyObject *kwargs, const char *language)
|
||||
{
|
||||
const char *source;
|
||||
Py_ssize_t source_length;
|
||||
PyObject *buffers_arg, *groups, *kinds_arg = Py_None, *steps = Py_None;
|
||||
const char *entry_point = "main";
|
||||
int timeout = 30000;
|
||||
static const char *keywords[] = {"source", "buffers", "workgroups", "entry_point",
|
||||
"binding_types", "dispatches", "timeout_ms", nullptr};
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s#OO|sOOi:dispatch_wgsl",
|
||||
const_cast<char **>(keywords), &source, &source_length,
|
||||
&buffers_arg, &groups, &entry_point, &kinds_arg, &steps, &timeout))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
if (source_length <= 0 || source_length > 1024 * 1024 || timeout < 100 || timeout > 120000) {
|
||||
PyErr_SetString(PyExc_ValueError, "Compute source or timeout exceeds the native request limit");
|
||||
return nullptr;
|
||||
}
|
||||
try {
|
||||
PyOwned buffers(PySequence_Fast(buffers_arg, "buffers must be a sequence of buffer objects or byte sizes"));
|
||||
if (!buffers) { return nullptr; }
|
||||
const Py_ssize_t count = PySequence_Fast_GET_SIZE(buffers.get());
|
||||
if (count < 1 || count > 16) {
|
||||
PyErr_SetString(PyExc_ValueError, "Compute requires between 1 and 16 buffers");
|
||||
return nullptr;
|
||||
}
|
||||
PyOwned kinds;
|
||||
if (kinds_arg == Py_None) {
|
||||
kinds.reset(PyList_New(count));
|
||||
if (!kinds) { return nullptr; }
|
||||
for (Py_ssize_t i = 0; i < count; i++) {
|
||||
PyObject *kind = PyUnicode_FromString("storage");
|
||||
if (!kind) { return nullptr; }
|
||||
PyList_SET_ITEM(kinds.get(), i, kind);
|
||||
}
|
||||
}
|
||||
else {
|
||||
kinds.reset(PySequence_List(kinds_arg));
|
||||
if (!kinds) { return nullptr; }
|
||||
if (PyList_GET_SIZE(kinds.get()) != count) {
|
||||
PyErr_SetString(PyExc_ValueError, "binding_types must match buffers");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
PyOwned metadata(Py_BuildValue("{s:s,s:s#,s:s,s:O,s:O,s:O}", "language", language, "source", source, source_length,
|
||||
"entryPoint", entry_point, "groups", groups,
|
||||
"kinds", kinds.get(), "steps", steps));
|
||||
if (!metadata) { return nullptr; }
|
||||
PyOwned json_module(PyImport_ImportModule("json"));
|
||||
if (!json_module) { return nullptr; }
|
||||
PyOwned encoded(PyObject_CallMethod(json_module.get(), "dumps", "O", metadata.get()));
|
||||
if (!encoded) { return nullptr; }
|
||||
const char *json = PyUnicode_AsUTF8(encoded.get());
|
||||
if (!json) { return nullptr; }
|
||||
std::vector<PyOwned> copies;
|
||||
std::vector<uint32_t> descriptors;
|
||||
Py_ssize_t total = 0;
|
||||
for (Py_ssize_t i = 0; i < count; i++) {
|
||||
PyObject *item = PySequence_Fast_GET_ITEM(buffers.get(), i);
|
||||
PyOwned bytes;
|
||||
if (PyLong_Check(item) && !PyBool_Check(item)) {
|
||||
const Py_ssize_t size = PyLong_AsSsize_t(item);
|
||||
if (size == -1 && PyErr_Occurred()) { return nullptr; }
|
||||
if (size < 4 || size % 4 || size > max_buffer_bytes || size > max_total_bytes - total) {
|
||||
PyErr_SetString(PyExc_ValueError, "Compute buffer size must be aligned and within the native budget");
|
||||
return nullptr;
|
||||
}
|
||||
// Python bytearray(size) defines real zero-initialized output storage.
|
||||
bytes.reset(PyObject_CallFunction(reinterpret_cast<PyObject *>(&PyByteArray_Type), "n", size));
|
||||
}
|
||||
else {
|
||||
Py_buffer view;
|
||||
if (PyObject_GetBuffer(item, &view, PyBUF_CONTIG_RO) < 0) { return nullptr; }
|
||||
if (view.len < 4 || view.len % 4 || view.len > max_buffer_bytes || view.len > max_total_bytes - total) {
|
||||
PyBuffer_Release(&view);
|
||||
PyErr_SetString(PyExc_ValueError, "Compute input must be contiguous, aligned and within the native budget");
|
||||
return nullptr;
|
||||
}
|
||||
bytes.reset(PyByteArray_FromStringAndSize(static_cast<const char *>(view.buf), view.len));
|
||||
PyBuffer_Release(&view);
|
||||
}
|
||||
if (!bytes) { return nullptr; }
|
||||
const Py_ssize_t size = PyByteArray_GET_SIZE(bytes.get());
|
||||
total += size;
|
||||
descriptors.push_back(uint32_t(reinterpret_cast<uintptr_t>(PyByteArray_AS_STRING(bytes.get()))));
|
||||
descriptors.push_back(uint32_t(size));
|
||||
copies.push_back(std::move(bytes));
|
||||
}
|
||||
char error[4096] = {};
|
||||
if (bpy_webgpu_compute(json, descriptors.data(), int(count), timeout, error, sizeof(error)) != 0) {
|
||||
PyErr_SetString(PyExc_RuntimeError, error);
|
||||
return nullptr;
|
||||
}
|
||||
PyOwned results(PyTuple_New(count));
|
||||
if (!results) { return nullptr; }
|
||||
for (Py_ssize_t i = 0; i < count; i++) {
|
||||
PyObject *value = PyBytes_FromStringAndSize(PyByteArray_AS_STRING(copies[i].get()),
|
||||
PyByteArray_GET_SIZE(copies[i].get()));
|
||||
if (!value) { return nullptr; }
|
||||
PyTuple_SET_ITEM(results.get(), i, value);
|
||||
}
|
||||
return results.release();
|
||||
}
|
||||
catch (const std::bad_alloc &) { return PyErr_NoMemory(); }
|
||||
catch (const std::exception &error) {
|
||||
PyErr_SetString(PyExc_RuntimeError, error.what());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
PyObject *bpygpu_compute_dispatch_wgsl(PyObject *, PyObject *args, PyObject *kwargs)
|
||||
{
|
||||
return compute_dispatch_source(args, kwargs, "wgsl");
|
||||
}
|
||||
|
||||
PyObject *bpygpu_compute_dispatch_glsl(PyObject *, PyObject *args, PyObject *kwargs)
|
||||
{
|
||||
return compute_dispatch_source(args, kwargs, "glsl");
|
||||
}
|
||||
} // namespace blender
|
||||
#endif
|
||||
@@ -685,10 +685,13 @@ static PyObject *pygpu_texture_read(BPyGPUTexture *self)
|
||||
}
|
||||
|
||||
void *buf = GPU_texture_read(self->tex, best_data_format, 0);
|
||||
const Py_ssize_t component_len = GPU_texture_component_len(tex_format);
|
||||
const Py_ssize_t component_len = best_data_format == GPU_DATA_10_11_11_REV ?
|
||||
1 : GPU_texture_component_len(tex_format);
|
||||
Py_ssize_t shape[4];
|
||||
int shape_len;
|
||||
if (GPU_texture_dimensions(self->tex) == 3) {
|
||||
if (GPU_texture_dimensions(self->tex) == 3 ||
|
||||
(GPU_texture_dimensions(self->tex) == 2 && GPU_texture_is_array(self->tex)))
|
||||
{
|
||||
shape[0] = GPU_texture_depth(self->tex);
|
||||
shape[1] = GPU_texture_height(self->tex);
|
||||
shape[2] = GPU_texture_width(self->tex);
|
||||
|
||||
@@ -751,7 +751,9 @@ static PyObject *bpy_import_test(const char *modname)
|
||||
Py_DECREF(mod);
|
||||
}
|
||||
else {
|
||||
#ifndef __EMSCRIPTEN__
|
||||
PyErr_Print();
|
||||
#endif
|
||||
}
|
||||
|
||||
return mod;
|
||||
@@ -812,11 +814,21 @@ void BPy_init_modules(bContext *C)
|
||||
BLI_assert(PyDict_GET_SIZE(bpy_types_dict_dummy) == 0);
|
||||
Py_DECREF(bpy_types_dict_dummy);
|
||||
|
||||
if (!bpy_types_module_py) {
|
||||
BPY_rna_types_dict_set(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
PyObject *bpy_types_module_py_dict = PyModule_GetDict(bpy_types_module_py);
|
||||
BPY_rna_types_dict_set(bpy_types_module_py_dict);
|
||||
}
|
||||
PyModule_AddObject(mod, "data", BPY_rna_module());
|
||||
BPY_rna_types_finalize_external_types(bpy_types);
|
||||
if (PyErr_Occurred()) {
|
||||
/* Preserve the original metaclass/allocation exception for the embedded
|
||||
* interpreter caller instead of continuing with partially built types. */
|
||||
return;
|
||||
}
|
||||
|
||||
PyModule_AddObject(mod, "props", BPY_rna_props());
|
||||
PyModule_AddObject(mod, "ops", BPY_operator_module());
|
||||
|
||||
@@ -18,6 +18,10 @@ struct bContext;
|
||||
|
||||
/** Creates the `bpy` module and adds it to `sys.modules` for importing. */
|
||||
void BPy_init_modules(bContext *C);
|
||||
#ifdef WITH_WEB
|
||||
int BPy_register_embedded_modules();
|
||||
void BPy_init_embedded_modules(bContext *C);
|
||||
#endif
|
||||
|
||||
extern PyObject *bpy_package_py;
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@
|
||||
|
||||
#include "bpy_app_handlers.hh"
|
||||
#include "bpy_rna.hh"
|
||||
#include "bpy_rna_callback.hh"
|
||||
#ifdef WITH_WEB
|
||||
# include <array>
|
||||
# include <unordered_map>
|
||||
#endif
|
||||
|
||||
#include "BPY_extern.hh"
|
||||
|
||||
@@ -303,6 +308,324 @@ static PyTypeObject BPyPersistent_Type = {
|
||||
|
||||
static PyObject *py_cb_array[BKE_CB_EVT_TOT] = {nullptr};
|
||||
|
||||
#ifdef WITH_WEB
|
||||
using WebHandlerLists = std::array<PyObject *, BKE_CB_EVT_TOT>;
|
||||
static WebHandlerLists web_global_handlers{};
|
||||
/* Only explicit native startup publication updates this template. In particular,
|
||||
* a cached mutable startup list written by a retiring Main is not a producer for
|
||||
* the next Main. Tuples retain the real callable objects, not Python substitutes. */
|
||||
static WebHandlerLists web_startup_handlers{};
|
||||
struct WebHandlerBank {
|
||||
WebHandlerLists lists{};
|
||||
WebHandlerLists startup{};
|
||||
};
|
||||
static std::unordered_map<Main *, WebHandlerBank> web_main_handlers;
|
||||
/* Borrowed: bpy.app owns this struct sequence for the interpreter lifetime. */
|
||||
static PyObject *web_handler_struct = nullptr;
|
||||
static Main *web_handler_owner = nullptr;
|
||||
static WebHandlerLists web_extension_before{};
|
||||
static WebHandlerLists web_extension_template_before{};
|
||||
static bool web_extension_transaction = false;
|
||||
static int web_handler_bank_sync(WebHandlerBank &bank);
|
||||
|
||||
static void web_handler_refs_release(WebHandlerLists &refs)
|
||||
{
|
||||
/* Detach every slot before a DECREF can run Python. */
|
||||
const WebHandlerLists previous = refs;
|
||||
refs = {};
|
||||
for (PyObject *value : previous) {
|
||||
Py_XDECREF(value);
|
||||
}
|
||||
}
|
||||
|
||||
int BPY_app_handlers_web_startup_begin()
|
||||
{
|
||||
if (!web_handler_struct || web_handler_owner || web_extension_transaction) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "PYTHON_HANDLER_STARTUP_CONTEXT_INVALID");
|
||||
return -1;
|
||||
}
|
||||
for (int i = 0; i < BKE_CB_EVT_TOT; i++) {
|
||||
web_extension_before[i] = PyList_AsTuple(web_global_handlers[i]);
|
||||
web_extension_template_before[i] = Py_XNewRef(web_startup_handlers[i]);
|
||||
if (!web_extension_before[i]) {
|
||||
web_handler_refs_release(web_extension_before);
|
||||
web_handler_refs_release(web_extension_template_before);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
web_extension_transaction = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int BPY_app_handlers_web_startup_end(bool commit)
|
||||
{
|
||||
if (!web_extension_transaction) {
|
||||
return 0;
|
||||
}
|
||||
bool ok = true;
|
||||
/* Publication is provisional until the desktop's context and handler bank
|
||||
* have both activated. Restore the template as well as cached list identity. */
|
||||
if (!commit) {
|
||||
std::swap(web_startup_handlers, web_extension_template_before);
|
||||
for (int i = 0; i < BKE_CB_EVT_TOT; i++) {
|
||||
if (PyList_SetSlice(web_global_handlers[i], 0, PY_SSIZE_T_MAX,
|
||||
web_extension_before[i]) < 0) {
|
||||
ok = false;
|
||||
PyErr_PrintEx(0);
|
||||
}
|
||||
}
|
||||
for (auto &[owner, bank] : web_main_handlers) {
|
||||
if (web_handler_bank_sync(bank) < 0) {
|
||||
ok = false;
|
||||
PyErr_PrintEx(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
web_extension_transaction = false;
|
||||
web_handler_refs_release(web_extension_before);
|
||||
web_handler_refs_release(web_extension_template_before);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
int BPY_app_handlers_web_startup_publish()
|
||||
{
|
||||
if (!web_handler_struct || web_handler_owner || BPY_callback_web_resetting()) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "PYTHON_HANDLER_STARTUP_CONTEXT_INVALID");
|
||||
return -1;
|
||||
}
|
||||
WebHandlerLists next{};
|
||||
for (int i = 0; i < BKE_CB_EVT_TOT; i++) {
|
||||
next[i] = PyList_AsTuple(web_global_handlers[i]);
|
||||
if (!next[i]) {
|
||||
web_handler_refs_release(next);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
WebHandlerLists previous = web_startup_handlers;
|
||||
web_startup_handlers = next;
|
||||
web_handler_refs_release(previous);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool web_handler_tuple_contains(PyObject *tuple, PyObject *item)
|
||||
{
|
||||
for (Py_ssize_t i = 0; tuple && i < PyTuple_GET_SIZE(tuple); i++) {
|
||||
if (PyTuple_GET_ITEM(tuple, i) == item) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Console can create a Main bank before the first desktop loads extensions.
|
||||
* Apply only the startup delta: keep cached list identity, session handlers and
|
||||
* intentional session removals of older startup handlers. Never call equality
|
||||
* or Python iteration hooks to determine a native registration's identity. */
|
||||
static int web_handler_bank_sync(WebHandlerBank &bank)
|
||||
{
|
||||
WebHandlerLists previous{}, next{}, baseline{};
|
||||
bool ok = true;
|
||||
for (int event = 0; ok && event < BKE_CB_EVT_TOT; event++) {
|
||||
baseline[event] = Py_XNewRef(web_startup_handlers[event]);
|
||||
if (bank.startup[event] == baseline[event]) {
|
||||
continue;
|
||||
}
|
||||
previous[event] = PyList_AsTuple(bank.lists[event]);
|
||||
next[event] = PyList_New(0);
|
||||
ok = previous[event] && next[event];
|
||||
if (!ok) {
|
||||
break;
|
||||
}
|
||||
for (Py_ssize_t i = 0; ok && i < PyTuple_GET_SIZE(previous[event]); i++) {
|
||||
PyObject *item = PyTuple_GET_ITEM(previous[event], i);
|
||||
if (!web_handler_tuple_contains(bank.startup[event], item) ||
|
||||
web_handler_tuple_contains(baseline[event], item)) {
|
||||
ok = PyList_Append(next[event], item) == 0;
|
||||
}
|
||||
}
|
||||
for (Py_ssize_t i = 0; ok && baseline[event] &&
|
||||
i < PyTuple_GET_SIZE(baseline[event]); i++) {
|
||||
PyObject *item = PyTuple_GET_ITEM(baseline[event], i);
|
||||
if (!web_handler_tuple_contains(bank.startup[event], item) &&
|
||||
!web_handler_tuple_contains(previous[event], item)) {
|
||||
ok = PyList_Append(next[event], item) == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
/* Every removed callable is pinned until all list writes are complete. */
|
||||
for (int event = 0; event < BKE_CB_EVT_TOT; event++) {
|
||||
if (next[event] && PyList_SetSlice(bank.lists[event], 0, PY_SSIZE_T_MAX,
|
||||
next[event]) < 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
PyObject *error = PyErr_GetRaisedException();
|
||||
if (ok) {
|
||||
std::swap(bank.startup, baseline);
|
||||
}
|
||||
else {
|
||||
/* A later event can fail after earlier slices were already written. Keep
|
||||
* the old baseline and restore every list before returning activation failure. */
|
||||
for (int event = 0; event < BKE_CB_EVT_TOT; event++) {
|
||||
if (previous[event] &&
|
||||
PyList_SetSlice(bank.lists[event], 0, PY_SSIZE_T_MAX, previous[event]) < 0) {
|
||||
PyErr_PrintEx(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
web_handler_refs_release(next);
|
||||
web_handler_refs_release(previous);
|
||||
web_handler_refs_release(baseline);
|
||||
PyErr_SetRaisedException(error);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Pin every original callable before changing any list. A DECREF can execute
|
||||
* arbitrary __del__/weakref callbacks which clear another event's list, append
|
||||
* handlers, or remove their own entry. No borrowed item/index survives that
|
||||
* boundary. List identity is retained for ordinary file-load reset. */
|
||||
static int web_handler_lists_reset(const WebHandlerLists &lists, const bool do_all)
|
||||
{
|
||||
WebHandlerLists snapshots{}, retained{};
|
||||
PyObject *perm_id = do_all ? nullptr : PyUnicode_FromString(PERMINENT_CB_ID);
|
||||
bool ok = do_all || perm_id != nullptr;
|
||||
for (int pos = 0; ok && pos < BKE_CB_EVT_TOT; pos++) {
|
||||
snapshots[pos] = PyList_AsTuple(lists[pos]);
|
||||
retained[pos] = PyList_New(0);
|
||||
ok = snapshots[pos] && retained[pos];
|
||||
if (!ok || do_all) {
|
||||
continue;
|
||||
}
|
||||
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(snapshots[pos]); i++) {
|
||||
PyObject *item = PyTuple_GET_ITEM(snapshots[pos], i);
|
||||
PyObject *function = PyMethod_Check(item) ? PyMethod_GET_FUNCTION(item) : item;
|
||||
PyObject **dict = PyFunction_Check(function) ? _PyObject_GetDictPtr(function) : nullptr;
|
||||
if (dict && *dict && PyDict_GetItem(*dict, perm_id)) {
|
||||
if (PyList_Append(retained[pos], item) < 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
for (int pos = 0; pos < BKE_CB_EVT_TOT; pos++) {
|
||||
if (PyList_SetSlice(lists[pos], 0, PY_SSIZE_T_MAX, retained[pos]) < 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
PyObject *error = PyErr_GetRaisedException();
|
||||
Py_XDECREF(perm_id);
|
||||
// All list writes finish before the first finalizer can run. Registrations
|
||||
// made by a file-load finalizer are new registrations, not entries to revisit.
|
||||
for (PyObject *list : retained) {
|
||||
Py_XDECREF(list);
|
||||
}
|
||||
for (PyObject *snapshot : snapshots) {
|
||||
Py_XDECREF(snapshot);
|
||||
}
|
||||
PyErr_SetRaisedException(error);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
int BPY_app_handlers_web_activate(Main *owner)
|
||||
{
|
||||
if (!web_handler_struct || owner == web_handler_owner) {
|
||||
return 0;
|
||||
}
|
||||
if (owner && !web_main_handlers.contains(owner)) {
|
||||
WebHandlerBank bank;
|
||||
for (int i = 0; i < BKE_CB_EVT_TOT; i++) {
|
||||
/* Startup handlers are original Python callables, shared intentionally.
|
||||
* Each Main gets independent mutable lists (including slice/list operations). */
|
||||
bank.startup[i] = Py_XNewRef(web_startup_handlers[i]);
|
||||
bank.lists[i] = bank.startup[i] ? PySequence_List(bank.startup[i]) : PyList_New(0);
|
||||
if (!bank.lists[i]) {
|
||||
web_handler_refs_release(bank.lists);
|
||||
web_handler_refs_release(bank.startup);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
try {
|
||||
web_main_handlers.emplace(owner, bank);
|
||||
}
|
||||
catch (...) {
|
||||
web_handler_refs_release(bank.lists);
|
||||
web_handler_refs_release(bank.startup);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
const WebHandlerLists &lists = owner ? web_main_handlers.at(owner).lists : web_global_handlers;
|
||||
WebHandlerLists previous{};
|
||||
Main *previous_owner = web_handler_owner;
|
||||
web_handler_owner = owner;
|
||||
for (int i = 0; i < BKE_CB_EVT_TOT; i++) {
|
||||
previous[i] = py_cb_array[i];
|
||||
py_cb_array[i] = lists[i];
|
||||
PyStructSequence_SET_ITEM(web_handler_struct, i, Py_NewRef(lists[i]));
|
||||
}
|
||||
/* Keep the previous published lists alive until synchronization succeeds.
|
||||
* Allocation failure must not leave bpy.app.handlers bound to a failed Main. */
|
||||
const int result = owner ? web_handler_bank_sync(web_main_handlers.at(owner)) : 0;
|
||||
if (result < 0) {
|
||||
web_handler_owner = previous_owner;
|
||||
for (int i = 0; i < BKE_CB_EVT_TOT; i++) {
|
||||
py_cb_array[i] = previous[i];
|
||||
PyStructSequence_SET_ITEM(web_handler_struct, i, previous[i]);
|
||||
Py_DECREF(lists[i]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
web_handler_refs_release(previous);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int BPY_app_handlers_web_owner_replace(Main *previous, Main *replacement)
|
||||
{
|
||||
if (previous == replacement) {
|
||||
return 0; /* Allocator reused the retired Main address. */
|
||||
}
|
||||
if (web_main_handlers.contains(replacement)) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "PYTHON_HANDLERS_REPLACEMENT_ALREADY_OWNED");
|
||||
return -1;
|
||||
}
|
||||
auto bank = web_main_handlers.extract(previous);
|
||||
if (!bank.empty()) {
|
||||
bank.key() = replacement;
|
||||
web_main_handlers.insert(std::move(bank));
|
||||
}
|
||||
if (web_handler_owner == previous) {
|
||||
web_handler_owner = replacement;
|
||||
}
|
||||
/* py_cb_array and cached Python lists keep the exact same references. */
|
||||
return 0;
|
||||
}
|
||||
|
||||
void BPY_app_handlers_web_clear(Main *owner)
|
||||
{
|
||||
auto it = web_main_handlers.find(owner);
|
||||
if (it == web_main_handlers.end()) {
|
||||
return;
|
||||
}
|
||||
WebHandlerBank bank = it->second;
|
||||
web_main_handlers.erase(it);
|
||||
/* Detach ownership before finalizers run. The active struct still owns its
|
||||
* lists until scope exit, but this bank can never be dispatched again. */
|
||||
if (web_handler_lists_reset(bank.lists, true) < 0) {
|
||||
pyrna_callback_error("bpy.app.handlers", "Main_reset");
|
||||
PyErr_PrintEx(0);
|
||||
}
|
||||
web_handler_refs_release(bank.startup);
|
||||
web_handler_refs_release(bank.lists);
|
||||
}
|
||||
#endif
|
||||
|
||||
static PyObject *make_app_cb_info()
|
||||
{
|
||||
PyObject *app_cb_info;
|
||||
@@ -318,6 +641,9 @@ static PyObject *make_app_cb_info()
|
||||
Py_FatalError("invalid callback slots 1");
|
||||
}
|
||||
PyStructSequence_SET_ITEM(app_cb_info, pos, (py_cb_array[pos] = PyList_New(0)));
|
||||
#ifdef WITH_WEB
|
||||
web_global_handlers[pos] = Py_XNewRef(py_cb_array[pos]);
|
||||
#endif
|
||||
}
|
||||
if (app_cb_info_fields[pos + APP_CB_OTHER_FIELDS].name != nullptr) {
|
||||
Py_FatalError("invalid callback slots 2");
|
||||
@@ -326,6 +652,10 @@ static PyObject *make_app_cb_info()
|
||||
/* custom function */
|
||||
PyStructSequence_SET_ITEM(app_cb_info, pos++, (PyObject *)&BPyPersistent_Type);
|
||||
|
||||
#ifdef WITH_WEB
|
||||
web_handler_struct = app_cb_info;
|
||||
#endif
|
||||
|
||||
return app_cb_info;
|
||||
}
|
||||
|
||||
@@ -372,6 +702,22 @@ PyObject *BPY_app_handlers_struct()
|
||||
void BPY_app_handlers_reset(const bool do_all)
|
||||
{
|
||||
PyGILState_STATE gilstate = PyGILState_Ensure();
|
||||
#ifdef WITH_WEB
|
||||
if (!do_all) {
|
||||
BPY_app_timers_web_file_load();
|
||||
}
|
||||
WebHandlerLists lists{};
|
||||
for (int pos = 0; pos < BKE_CB_EVT_TOT; pos++) {
|
||||
lists[pos] = Py_NewRef(py_cb_array[pos]);
|
||||
}
|
||||
if (web_handler_lists_reset(lists, do_all) < 0) {
|
||||
pyrna_callback_error("bpy.app.handlers", "file_load_reset");
|
||||
PyErr_PrintEx(0);
|
||||
}
|
||||
for (PyObject *list : lists) {
|
||||
Py_DECREF(list);
|
||||
}
|
||||
#else
|
||||
int pos = 0;
|
||||
|
||||
if (do_all) {
|
||||
@@ -415,6 +761,7 @@ void BPY_app_handlers_reset(const bool do_all)
|
||||
|
||||
Py_DECREF(perm_id_str);
|
||||
}
|
||||
#endif
|
||||
|
||||
PyGILState_Release(gilstate);
|
||||
}
|
||||
@@ -432,68 +779,98 @@ static PyObject *choose_arguments(PyObject *func, PyObject *args_all, PyObject *
|
||||
}
|
||||
|
||||
/* the actual callback - not necessarily called from py */
|
||||
void bpy_app_generic_callback(Main * /*main*/,
|
||||
void bpy_app_generic_callback(Main *main,
|
||||
PointerRNA **pointers,
|
||||
const int pointers_num,
|
||||
void *arg)
|
||||
{
|
||||
PyObject *cb_list = py_cb_array[POINTER_AS_INT(arg)];
|
||||
if (PyList_GET_SIZE(cb_list) > 0) {
|
||||
const PyGILState_STATE gilstate = PyGILState_Ensure();
|
||||
|
||||
const int num_arguments = 2;
|
||||
PyObject *args_all = PyTuple_New(num_arguments); /* save python creating each call */
|
||||
PyObject *args_single = PyTuple_New(1);
|
||||
PyObject *func;
|
||||
PyObject *ret;
|
||||
Py_ssize_t pos;
|
||||
|
||||
/* setup arguments */
|
||||
for (int i = 0; i < pointers_num; ++i) {
|
||||
PyTuple_SET_ITEM(
|
||||
args_all, i, pyrna_struct_CreatePyObject_with_primitive_support(pointers[i]));
|
||||
}
|
||||
for (int i = pointers_num; i < num_arguments; ++i) {
|
||||
PyTuple_SET_ITEM(args_all, i, Py_NewRef(Py_None));
|
||||
}
|
||||
|
||||
if (pointers_num == 0) {
|
||||
PyTuple_SET_ITEM(args_single, 0, Py_NewRef(Py_None));
|
||||
}
|
||||
else {
|
||||
PyTuple_SET_ITEM(
|
||||
args_single, 0, pyrna_struct_CreatePyObject_with_primitive_support(pointers[0]));
|
||||
}
|
||||
|
||||
/* Iterate the list and run the callbacks
|
||||
* NOTE: don't store the list size since the scripts may remove themselves. */
|
||||
for (pos = 0; pos < PyList_GET_SIZE(cb_list); pos++) {
|
||||
func = PyList_GET_ITEM(cb_list, pos);
|
||||
PyObject *args = choose_arguments(func, args_all, args_single);
|
||||
ret = PyObject_Call(func, args, nullptr);
|
||||
if (ret == nullptr) {
|
||||
/* Don't set last system variables because they might cause some
|
||||
* dangling pointers to external render engines (when exception
|
||||
* happens during rendering) which will break logic of render pipeline
|
||||
* which expects to be the only user of render engine when rendering
|
||||
* is finished. */
|
||||
|
||||
/* Note the handler called, the exception itself typically has the function name. */
|
||||
PySys_WriteStderr("Error in bpy.app.handlers.%s[%d]:\n",
|
||||
app_cb_info_fields[POINTER_AS_INT(arg)].name,
|
||||
int(pos));
|
||||
PyErr_PrintEx(0);
|
||||
}
|
||||
else {
|
||||
Py_DECREF(ret);
|
||||
}
|
||||
}
|
||||
|
||||
Py_DECREF(args_all);
|
||||
Py_DECREF(args_single);
|
||||
|
||||
const PyGILState_STATE gilstate = PyGILState_Ensure();
|
||||
#ifdef WITH_WEB
|
||||
if (BPyWebRegistrationOwnerScope::synchronize() < 0) {
|
||||
pyrna_callback_error("bpy.app.handlers", "Main_owner_transfer");
|
||||
PyErr_PrintEx(0);
|
||||
PyGILState_Release(gilstate);
|
||||
return;
|
||||
}
|
||||
if (BPY_callback_web_resetting() ||
|
||||
(main && web_handler_owner && main != web_handler_owner)) {
|
||||
PyGILState_Release(gilstate);
|
||||
return;
|
||||
}
|
||||
#else
|
||||
UNUSED_VARS(main);
|
||||
#endif
|
||||
PyObject *cb_list = Py_NewRef(py_cb_array[POINTER_AS_INT(arg)]);
|
||||
const char *event = app_cb_info_fields[POINTER_AS_INT(arg)].name;
|
||||
/* Own a finite set for this dispatch. Self-removal must not skip the next
|
||||
* handler; append belongs to the next event, and removed handlers stay skipped.
|
||||
* The tuple also retains callable instances through unregister and reporting. */
|
||||
PyObject *callbacks = PyList_AsTuple(cb_list);
|
||||
PyObject *args_all = nullptr;
|
||||
PyObject *args_single = nullptr;
|
||||
if (callbacks && PyTuple_GET_SIZE(callbacks) > 0 && pyrna_callback_check() == 0) {
|
||||
const int num_arguments = 2;
|
||||
BLI_assert(pointers_num <= num_arguments);
|
||||
args_all = PyTuple_New(num_arguments);
|
||||
args_single = PyTuple_New(1);
|
||||
bool args_ok = args_all && args_single;
|
||||
if (args_ok) {
|
||||
for (int i = 0; i < num_arguments; i++) {
|
||||
PyObject *value = i < pointers_num ?
|
||||
pyrna_struct_CreatePyObject_with_primitive_support(pointers[i]) :
|
||||
Py_NewRef(Py_None);
|
||||
PyTuple_SET_ITEM(args_all, i, value);
|
||||
if (!value) {
|
||||
args_ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (args_ok) {
|
||||
PyTuple_SET_ITEM(args_single, 0, Py_NewRef(PyTuple_GET_ITEM(args_all, 0)));
|
||||
}
|
||||
}
|
||||
if (args_ok) {
|
||||
for (Py_ssize_t pos = 0; pos < PyTuple_GET_SIZE(callbacks); pos++) {
|
||||
if (pyrna_callback_check() == -1) {
|
||||
break;
|
||||
}
|
||||
PyObject *func = PyTuple_GET_ITEM(callbacks, pos);
|
||||
bool registered = false;
|
||||
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(cb_list); i++) {
|
||||
/* No Python equality methods while determining registration lifetime. */
|
||||
if (PyList_GET_ITEM(cb_list, i) == func) {
|
||||
registered = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!registered) {
|
||||
continue;
|
||||
}
|
||||
PyObject *args = choose_arguments(func, args_all, args_single);
|
||||
PyObject *ret = pyrna_callback_call(func, args, "bpy.app.handlers", event);
|
||||
if (!ret) {
|
||||
pyrna_callback_error("bpy.app.handlers", event);
|
||||
PySys_WriteStderr("Error in bpy.app.handlers.%s[%d]:\n", event, int(pos));
|
||||
/* Do not retain render-engine RNA through sys.last_* exception state. */
|
||||
PyErr_PrintEx(0);
|
||||
}
|
||||
Py_XDECREF(ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (PyErr_Occurred()) {
|
||||
pyrna_callback_error("bpy.app.handlers", event);
|
||||
PyErr_PrintEx(0);
|
||||
}
|
||||
Py_XDECREF(args_all);
|
||||
Py_XDECREF(args_single);
|
||||
Py_XDECREF(callbacks);
|
||||
Py_DECREF(cb_list);
|
||||
if (pyrna_callback_check() == -1) {
|
||||
pyrna_callback_error("bpy.app.handlers", event);
|
||||
PyErr_PrintEx(0);
|
||||
}
|
||||
PyGILState_Release(gilstate);
|
||||
}
|
||||
|
||||
} // namespace blender
|
||||
|
||||
@@ -50,10 +50,15 @@ static PyObject *make_oiio_info()
|
||||
#define SetObjItem(obj) PyStructSequence_SET_ITEM(oiio_info, pos++, obj)
|
||||
|
||||
curversion = OIIO_getVersionHex();
|
||||
SetObjItem(PyBool_FromLong(1));
|
||||
SetObjItem(PyBool_FromLong(curversion > 0));
|
||||
SetObjItem(PyC_Tuple_Pack_I32({curversion / 10000, (curversion / 100) % 100, curversion % 100}));
|
||||
SetObjItem(PyUnicode_FromFormat(
|
||||
"%2d, %2d, %2d", curversion / 10000, (curversion / 100) % 100, curversion % 100));
|
||||
if (curversion > 0) {
|
||||
SetObjItem(PyUnicode_FromFormat(
|
||||
"%2d, %2d, %2d", curversion / 10000, (curversion / 100) % 100, curversion % 100));
|
||||
}
|
||||
else {
|
||||
SetObjItem(PyUnicode_FromString("Unknown"));
|
||||
}
|
||||
|
||||
if (UNLIKELY(PyErr_Occurred())) {
|
||||
Py_DECREF(oiio_info);
|
||||
|
||||
@@ -13,15 +13,43 @@
|
||||
#include <algorithm>
|
||||
|
||||
#include "bpy_app_timers.hh"
|
||||
#include "bpy_rna.hh"
|
||||
#include "bpy_rna_callback.hh"
|
||||
#ifdef WITH_WEB
|
||||
# include <vector>
|
||||
#endif
|
||||
|
||||
#include "../generic/py_capi_utils.hh"
|
||||
#include "../generic/python_compat.hh" /* IWYU pragma: keep. */
|
||||
|
||||
namespace blender {
|
||||
|
||||
#ifdef WITH_WEB
|
||||
struct WebPythonTimer {
|
||||
PyObject *function;
|
||||
Main *owner;
|
||||
bool persistent;
|
||||
bool startup_delta = false;
|
||||
};
|
||||
static std::vector<WebPythonTimer *> web_timers;
|
||||
static int web_timer_teardown_depth = 0;
|
||||
|
||||
static WebPythonTimer *web_timer_find(PyObject *function)
|
||||
{
|
||||
for (WebPythonTimer *timer : web_timers) {
|
||||
if (timer->function == function && timer->owner == BPY_callback_web_owner_get() &&
|
||||
BLI_timer_is_registered(uintptr_t(timer))) {
|
||||
return timer;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
static double handle_returned_value(PyObject *function, PyObject *ret)
|
||||
{
|
||||
if (ret == nullptr) {
|
||||
pyrna_callback_error("bpy.app.timers", "execute");
|
||||
PyErr_PrintEx(0);
|
||||
return -1;
|
||||
}
|
||||
@@ -32,7 +60,12 @@ static double handle_returned_value(PyObject *function, PyObject *ret)
|
||||
|
||||
double value = PyFloat_AsDouble(ret);
|
||||
if (value == -1.0f && PyErr_Occurred()) {
|
||||
pyrna_callback_error("bpy.app.timers", "return_value");
|
||||
#ifdef WITH_WEB
|
||||
PyErr_PrintEx(0);
|
||||
#else
|
||||
PyErr_Clear();
|
||||
#endif
|
||||
printf("Error: 'bpy.app.timers' callback ");
|
||||
PyObject_Print(function, stdout, Py_PRINT_RAW);
|
||||
printf(" did not return None or float.\n");
|
||||
@@ -47,11 +80,52 @@ static double handle_returned_value(PyObject *function, PyObject *ret)
|
||||
static double py_timer_execute(uintptr_t /*uuid*/, void *user_data)
|
||||
{
|
||||
PyGILState_STATE gilstate = PyGILState_Ensure();
|
||||
#ifdef WITH_WEB
|
||||
if (BPyWebRegistrationOwnerScope::synchronize() < 0) {
|
||||
pyrna_callback_error("bpy.app.timers", "Main_owner_transfer");
|
||||
PyErr_PrintEx(0);
|
||||
PyGILState_Release(gilstate);
|
||||
return 0.0;
|
||||
}
|
||||
auto *timer = static_cast<WebPythonTimer *>(user_data);
|
||||
if (!timer->function) {
|
||||
PyGILState_Release(gilstate);
|
||||
return -1.0;
|
||||
}
|
||||
if (BPY_callback_web_resetting() ||
|
||||
(timer->owner && timer->owner != BPY_callback_web_owner_get())) {
|
||||
/* Leave a due timer due until its own Main is dispatched. */
|
||||
PyGILState_Release(gilstate);
|
||||
return 0.0;
|
||||
}
|
||||
#endif
|
||||
|
||||
PyObject *function = static_cast<PyObject *>(user_data);
|
||||
/* An unstarted timer survives cancellation of an earlier callback this frame. */
|
||||
if (pyrna_callback_check() == -1) {
|
||||
pyrna_callback_error("bpy.app.timers", "before_execute");
|
||||
PyErr_PrintEx(0);
|
||||
PyGILState_Release(gilstate);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
PyObject *py_ret = PyObject_CallObject(function, nullptr);
|
||||
const double ret = handle_returned_value(function, py_ret);
|
||||
/* unregister() immediately releases the timer's reference, including when
|
||||
* called from this function or its returned object's __float__ method. */
|
||||
PyObject *function = Py_NewRef(
|
||||
#ifdef WITH_WEB
|
||||
timer->function
|
||||
#else
|
||||
static_cast<PyObject *>(user_data)
|
||||
#endif
|
||||
);
|
||||
PyObject *py_ret = pyrna_callback_call(function, nullptr, "bpy.app.timers", "execute");
|
||||
double ret = handle_returned_value(function, py_ret);
|
||||
Py_XDECREF(py_ret);
|
||||
Py_DECREF(function);
|
||||
if (pyrna_callback_check() == -1) {
|
||||
pyrna_callback_error("bpy.app.timers", "return_conversion_or_release");
|
||||
PyErr_PrintEx(0);
|
||||
ret = -1.0;
|
||||
}
|
||||
|
||||
PyGILState_Release(gilstate);
|
||||
|
||||
@@ -62,12 +136,75 @@ static void py_timer_free(uintptr_t /*uuid*/, void *user_data)
|
||||
{
|
||||
PyGILState_STATE gilstate = PyGILState_Ensure();
|
||||
|
||||
PyObject *function = static_cast<PyObject *>(user_data);
|
||||
Py_DECREF(function);
|
||||
#ifdef WITH_WEB
|
||||
auto *timer = static_cast<WebPythonTimer *>(user_data);
|
||||
std::erase(web_timers, timer);
|
||||
PyObject *function = timer->function;
|
||||
delete timer;
|
||||
Py_XDECREF(function);
|
||||
#else
|
||||
Py_DECREF(static_cast<PyObject *>(user_data));
|
||||
#endif
|
||||
|
||||
PyGILState_Release(gilstate);
|
||||
}
|
||||
|
||||
#ifdef WITH_WEB
|
||||
static void web_timers_clear(Main *owner, bool include_persistent, bool startup_only = false)
|
||||
{
|
||||
web_timer_teardown_depth++;
|
||||
for (;;) {
|
||||
auto it = std::find_if(web_timers.begin(), web_timers.end(),
|
||||
[owner, include_persistent, startup_only](WebPythonTimer *timer) {
|
||||
return timer->owner == owner &&
|
||||
(!startup_only || timer->startup_delta) &&
|
||||
(include_persistent || !timer->persistent);
|
||||
});
|
||||
if (it == web_timers.end()) {
|
||||
break;
|
||||
}
|
||||
WebPythonTimer *timer = *it;
|
||||
web_timers.erase(it);
|
||||
/* A file load may already have tagged the native timer for deferred removal. */
|
||||
PyObject *function = timer->function;
|
||||
timer->function = nullptr;
|
||||
timer->owner = nullptr;
|
||||
BLI_timer_unregister(uintptr_t(timer));
|
||||
Py_XDECREF(function);
|
||||
}
|
||||
web_timer_teardown_depth--;
|
||||
}
|
||||
|
||||
void BPY_app_timers_web_clear(Main *owner)
|
||||
{
|
||||
web_timers_clear(owner, true);
|
||||
}
|
||||
|
||||
void BPY_app_timers_web_owner_replace(Main *previous, Main *replacement)
|
||||
{
|
||||
/* File-load cleanup already removes non-persistent timers. Move only records
|
||||
* still alive; never resurrect a canceled callback or alter its deadline. */
|
||||
for (WebPythonTimer *timer : web_timers) {
|
||||
if (timer->owner == previous) {
|
||||
timer->owner = replacement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BPY_app_timers_web_startup_rollback()
|
||||
{
|
||||
web_timers_clear(nullptr, true, true);
|
||||
}
|
||||
|
||||
void BPY_app_timers_web_file_load()
|
||||
{
|
||||
web_timers_clear(BPY_callback_web_owner_get(), false);
|
||||
if (BPY_callback_web_owner_get()) {
|
||||
web_timers_clear(nullptr, false);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
bpy_app_timers_register_doc,
|
||||
@@ -87,6 +224,15 @@ PyDoc_STRVAR(
|
||||
" :type persistent: bool\n");
|
||||
static PyObject *bpy_app_timers_register(PyObject * /*self*/, PyObject *args, PyObject *kw)
|
||||
{
|
||||
#ifdef WITH_WEB
|
||||
if (BPyWebRegistrationOwnerScope::synchronize() < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
if (BPY_callback_web_resetting() || web_timer_teardown_depth != 0) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Cannot register timers during Main/timer teardown");
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
PyObject *function;
|
||||
double first_interval = 0;
|
||||
int persistent = false;
|
||||
@@ -113,8 +259,18 @@ static PyObject *bpy_app_timers_register(PyObject * /*self*/, PyObject *args, Py
|
||||
}
|
||||
|
||||
Py_INCREF(function);
|
||||
#ifdef WITH_WEB
|
||||
auto *timer = new WebPythonTimer{function, BPY_callback_web_owner_get(), bool(persistent)};
|
||||
timer->startup_delta = BPY_web_startup_transaction_active();
|
||||
web_timers.push_back(timer);
|
||||
/* BLI's file-load sweep is process-global. BPY_app_handlers_reset performs the
|
||||
* Python timer sweep for the active Main only, preserving other open sessions. */
|
||||
BLI_timer_register(
|
||||
uintptr_t(timer), py_timer_execute, timer, py_timer_free, first_interval, true);
|
||||
#else
|
||||
BLI_timer_register(
|
||||
intptr_t(function), py_timer_execute, function, py_timer_free, first_interval, persistent);
|
||||
#endif
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
@@ -129,7 +285,19 @@ PyDoc_STRVAR(
|
||||
" :type function: Callable[[], float | None]\n");
|
||||
static PyObject *bpy_app_timers_unregister(PyObject * /*self*/, PyObject *function)
|
||||
{
|
||||
#ifdef WITH_WEB
|
||||
if (BPyWebRegistrationOwnerScope::synchronize() < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
WebPythonTimer *timer = web_timer_find(function);
|
||||
if (timer && BPY_web_startup_transaction_active() && !timer->startup_delta) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "PYTHON_STARTUP_CANNOT_REMOVE_EXISTING_TIMER");
|
||||
return nullptr;
|
||||
}
|
||||
if (!timer || !BLI_timer_unregister(uintptr_t(timer))) {
|
||||
#else
|
||||
if (!BLI_timer_unregister(intptr_t(function))) {
|
||||
#endif
|
||||
PyErr_SetString(PyExc_ValueError, "function is not registered");
|
||||
return nullptr;
|
||||
}
|
||||
@@ -149,7 +317,14 @@ PyDoc_STRVAR(
|
||||
" :rtype: bool\n");
|
||||
static PyObject *bpy_app_timers_is_registered(PyObject * /*self*/, PyObject *function)
|
||||
{
|
||||
#ifdef WITH_WEB
|
||||
if (BPyWebRegistrationOwnerScope::synchronize() < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
const bool ret = web_timer_find(function) != nullptr;
|
||||
#else
|
||||
const bool ret = BLI_timer_is_registered(intptr_t(function));
|
||||
#endif
|
||||
return PyBool_FromLong(ret);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "bpy_path.hh"
|
||||
#include "bpy_props.hh"
|
||||
#include "bpy_rna.hh"
|
||||
#include "bpy_rna_access.hh"
|
||||
|
||||
#include "bpy_app_translations.hh"
|
||||
|
||||
@@ -291,12 +292,45 @@ void BPY_modules_update()
|
||||
|
||||
bContext *BPY_context_get()
|
||||
{
|
||||
/* The embedded WebEngine keeps CPython alive while a Main is replaced. A
|
||||
* Python finalizer can therefore observe the bpy module after its context
|
||||
* slot has been cleared. Match the desktop null-context boundary instead
|
||||
* of dereferencing a retired module while RNA wrappers are being released. */
|
||||
if (bpy_context_module == nullptr || !bpy_context_module->ptr.has_value()) {
|
||||
return nullptr;
|
||||
}
|
||||
return static_cast<bContext *>(bpy_context_module->ptr->data);
|
||||
}
|
||||
|
||||
#ifdef WITH_WEB
|
||||
BPyRNAMainAccessScope::BPyRNAMainAccessScope()
|
||||
: RNAMainAccessScope(G_MAIN,
|
||||
bpy_context_module && bpy_context_module->ptr ? BPY_context_get() :
|
||||
nullptr)
|
||||
{
|
||||
}
|
||||
#endif
|
||||
|
||||
void BPY_context_set(bContext *C)
|
||||
{
|
||||
if (bpy_context_module == nullptr || !bpy_context_module->ptr.has_value()) {
|
||||
return;
|
||||
}
|
||||
#ifdef WITH_WEB
|
||||
/* The WebEngine binds the real bpy.context directly while dispatching a
|
||||
* native operator. A nested temp-override or a canvas rebind can replace
|
||||
* that pointer without passing through bpy_context_set_ex(), so retire the
|
||||
* previous context before publishing the new one. Otherwise an in-flight
|
||||
* PointerRNA conversion can continue reading its old context store after
|
||||
* the module has switched to another Main. */
|
||||
bContext *previous = static_cast<bContext *>(bpy_context_module->ptr->data);
|
||||
if (previous != nullptr && previous != C) {
|
||||
RNAMainAccessScope::context_binding_changed(previous);
|
||||
}
|
||||
pyrna_context_rebind(C);
|
||||
#else
|
||||
bpy_context_module->ptr->data = static_cast<void *>(C);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef WITH_FLUID
|
||||
@@ -351,6 +385,32 @@ static _inittab bpy_internal_modules[] = {
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
|
||||
#ifdef WITH_WEB
|
||||
int BPy_register_embedded_modules()
|
||||
{
|
||||
return PyImport_ExtendInittab(bpy_internal_modules);
|
||||
}
|
||||
|
||||
void BPy_init_embedded_modules(bContext *C)
|
||||
{
|
||||
/* Match BPY_python_start's order while keeping the host's PyConfig and GIL. */
|
||||
bpy_intern_string_init();
|
||||
BPY_rna_init();
|
||||
BPy_init_modules(C);
|
||||
if (PyErr_Occurred()) {
|
||||
return;
|
||||
}
|
||||
pyrna_alloc_types();
|
||||
if (PyErr_Occurred()) {
|
||||
return;
|
||||
}
|
||||
/* Keep the original Blender exit hook attached to the embedded interpreter.
|
||||
* This is required for bpy.app handlers and WM-owned Python references when
|
||||
* CPython receives SystemExit or the host tears down its Main. */
|
||||
BPY_atexit_register();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef WITH_PYTHON_MODULE
|
||||
/**
|
||||
* Convenience function for #BPY_python_start.
|
||||
|
||||
@@ -18,17 +18,139 @@
|
||||
|
||||
#include "BKE_context.hh"
|
||||
|
||||
#ifdef WITH_WEB
|
||||
# include "BLI_vector.hh"
|
||||
#endif
|
||||
|
||||
#include "WM_message.hh"
|
||||
|
||||
#include "RNA_access.hh"
|
||||
|
||||
#include "bpy_capi_utils.hh"
|
||||
#include "bpy_rna.hh"
|
||||
#include "bpy_rna_callback.hh"
|
||||
|
||||
#include "bpy_msgbus.hh" /* own include */
|
||||
|
||||
namespace blender {
|
||||
|
||||
#ifdef WITH_WEB
|
||||
struct WebMsgbusSubscription {
|
||||
wmMsgBus *bus;
|
||||
/* Borrowed from the native subscription until its free_data callback. */
|
||||
PyObject *owner;
|
||||
PyObject *user_data;
|
||||
bool startup_delta;
|
||||
};
|
||||
static Vector<WebMsgbusSubscription> web_msgbus_subscriptions;
|
||||
static bool web_msgbus_resetting = false;
|
||||
static int web_msgbus_free_depth = 0;
|
||||
|
||||
void BPY_msgbus_web_reset_set(const bool resetting)
|
||||
{
|
||||
web_msgbus_resetting = resetting;
|
||||
}
|
||||
|
||||
void BPY_msgbus_web_clear(wmMsgBus *mbus)
|
||||
{
|
||||
BLI_assert(web_msgbus_resetting);
|
||||
while (true) {
|
||||
PyObject *owner = nullptr;
|
||||
for (const WebMsgbusSubscription &entry : web_msgbus_subscriptions) {
|
||||
if (entry.bus == mbus) {
|
||||
owner = Py_NewRef(entry.owner);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!owner) {
|
||||
return;
|
||||
}
|
||||
/* free_data releases both owner and payload inside the native list walk.
|
||||
* Retain every matching payload until that walk returns: its finalizer can
|
||||
* replace Main (destroying this bus), even though msgbus mutation itself
|
||||
* is disabled by the reset guard. */
|
||||
Vector<PyObject *> payloads;
|
||||
for (const WebMsgbusSubscription &entry : web_msgbus_subscriptions) {
|
||||
if (entry.bus == mbus && entry.owner == owner) {
|
||||
payloads.append(Py_NewRef(entry.user_data));
|
||||
}
|
||||
}
|
||||
WM_msgbus_clear_by_owner(mbus, owner);
|
||||
PyObject *first_error = PyErr_GetRaisedException();
|
||||
for (PyObject *payload : payloads) {
|
||||
Py_DECREF(payload);
|
||||
if (PyErr_Occurred()) {
|
||||
if (!first_error) {
|
||||
first_error = PyErr_GetRaisedException();
|
||||
}
|
||||
else {
|
||||
PyErr_Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
Py_DECREF(owner);
|
||||
if (first_error) {
|
||||
PyErr_SetRaisedException(first_error);
|
||||
}
|
||||
if (PyErr_Occurred()) {
|
||||
pyrna_callback_error("bpy.msgbus", "reset_release");
|
||||
PyErr_PrintEx(0);
|
||||
}
|
||||
/* Re-select from the live registry; mbus is only an identity after release. */
|
||||
}
|
||||
}
|
||||
|
||||
void BPY_msgbus_web_startup_rollback()
|
||||
{
|
||||
while (true) {
|
||||
wmMsgBus *bus = nullptr;
|
||||
PyObject *owner = nullptr;
|
||||
for (const WebMsgbusSubscription &entry : web_msgbus_subscriptions) {
|
||||
if (entry.startup_delta) {
|
||||
bus = entry.bus;
|
||||
owner = Py_NewRef(entry.owner);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!owner) {
|
||||
return;
|
||||
}
|
||||
WM_msgbus_clear_by_owner(bus, owner);
|
||||
Py_DECREF(owner);
|
||||
}
|
||||
}
|
||||
|
||||
static bool web_msgbus_startup_owner_check(wmMsgBus *bus, PyObject *owner)
|
||||
{
|
||||
if (BPY_web_startup_transaction_active()) {
|
||||
for (const WebMsgbusSubscription &entry : web_msgbus_subscriptions) {
|
||||
if (entry.bus == bus && entry.owner == owner && !entry.startup_delta) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "PYTHON_STARTUP_MSGBUS_OWNER_ALREADY_IN_USE");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
static wmMsgBus *bpy_msgbus_context_bus(bContext *C)
|
||||
{
|
||||
#ifdef WITH_WEB
|
||||
if (web_msgbus_resetting || web_msgbus_free_depth != 0) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "PYTHON_MSGBUS_RELEASING");
|
||||
return nullptr;
|
||||
}
|
||||
wmMsgBus *mbus = C ? CTX_wm_message_bus(C) : nullptr;
|
||||
if (!mbus) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "PYTHON_MSGBUS_CONTEXT_UNAVAILABLE");
|
||||
}
|
||||
return mbus;
|
||||
#else
|
||||
return CTX_wm_message_bus(C);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Internal Utils
|
||||
* \{ */
|
||||
@@ -148,25 +270,43 @@ static void bpy_msgbus_notify(bContext *C,
|
||||
pyrna_write_set(true);
|
||||
}
|
||||
|
||||
PyObject *user_data = static_cast<PyObject *>(msg_val->user_data);
|
||||
/* clear_by_owner() may release the subscription from inside notify. The WM
|
||||
* dispatcher owns no Python references, so retain both payload and owner. */
|
||||
PyObject *owner = Py_NewRef(static_cast<PyObject *>(msg_val->owner));
|
||||
PyObject *user_data = Py_NewRef(static_cast<PyObject *>(msg_val->user_data));
|
||||
BLI_assert(PyTuple_GET_SIZE(user_data) == BPY_MSGBUS_USER_DATA_LEN);
|
||||
|
||||
PyObject *callback_args = PyTuple_GET_ITEM(user_data, 0);
|
||||
PyObject *callback_notify = PyTuple_GET_ITEM(user_data, 1);
|
||||
|
||||
PyObject *ret = PyObject_CallObject(callback_notify, callback_args);
|
||||
bool deferred = false;
|
||||
#ifdef WITH_WEB
|
||||
deferred = pyrna_callback_check() == -1;
|
||||
msg_val->defer_notify = deferred;
|
||||
#endif
|
||||
PyObject *ret = deferred ? nullptr :
|
||||
pyrna_callback_call(
|
||||
callback_notify, callback_args, "bpy.msgbus", "notify");
|
||||
|
||||
if (ret == nullptr) {
|
||||
pyrna_callback_error("bpy.msgbus", "notify");
|
||||
PyC_Err_PrintWithFunc(callback_notify);
|
||||
}
|
||||
else {
|
||||
if (ret != Py_None) {
|
||||
PyErr_SetString(PyExc_ValueError, "the return value must be None");
|
||||
pyrna_callback_error("bpy.msgbus", "notify_return");
|
||||
PyC_Err_PrintWithFunc(callback_notify);
|
||||
}
|
||||
Py_DECREF(ret);
|
||||
}
|
||||
|
||||
Py_DECREF(user_data);
|
||||
Py_DECREF(owner);
|
||||
if (pyrna_callback_check() == -1) {
|
||||
pyrna_callback_error("bpy.msgbus", "notify_release");
|
||||
PyErr_PrintEx(0);
|
||||
}
|
||||
if (!is_write_ok) {
|
||||
pyrna_write_set(false);
|
||||
}
|
||||
@@ -178,8 +318,21 @@ static void bpy_msgbus_subscribe_value_free_data(wmMsgSubscribeKey * /*msg_key*/
|
||||
wmMsgSubscribeValue *msg_val)
|
||||
{
|
||||
const PyGILState_STATE gilstate = PyGILState_Ensure();
|
||||
#ifdef WITH_WEB
|
||||
/* clear_by_owner and bus destruction iterate native lists. Destructors must
|
||||
* not recursively change those lists while a subscription is being freed. */
|
||||
web_msgbus_free_depth++;
|
||||
for (int64_t i = web_msgbus_subscriptions.size(); i-- > 0;) {
|
||||
if (web_msgbus_subscriptions[i].user_data == msg_val->user_data) {
|
||||
web_msgbus_subscriptions.remove(i);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Py_DECREF(msg_val->owner);
|
||||
Py_DECREF(msg_val->user_data);
|
||||
#ifdef WITH_WEB
|
||||
web_msgbus_free_depth--;
|
||||
#endif
|
||||
PyGILState_Release(gilstate);
|
||||
}
|
||||
|
||||
@@ -276,7 +429,10 @@ static PyObject *bpy_msgbus_subscribe_rna(PyObject * /*self*/, PyObject *args, P
|
||||
|
||||
/* NOTE: we may want to have a way to pass this in. */
|
||||
bContext *C = BPY_context_get();
|
||||
wmMsgBus *mbus = CTX_wm_message_bus(C);
|
||||
wmMsgBus *mbus = bpy_msgbus_context_bus(C);
|
||||
if (!mbus) {
|
||||
return nullptr;
|
||||
}
|
||||
wmMsgParams_RNA msg_key_params = {{}};
|
||||
|
||||
wmMsgSubscribeValue msg_val_params = {nullptr};
|
||||
@@ -299,6 +455,13 @@ static PyObject *bpy_msgbus_subscribe_rna(PyObject * /*self*/, PyObject *args, P
|
||||
}
|
||||
|
||||
/* owner can be anything. */
|
||||
#ifdef WITH_WEB
|
||||
/* Rollback clears by owner, so never mix a pre-existing owner's subscriptions
|
||||
* with this pass, including duplicate native subscription keys. */
|
||||
if (!web_msgbus_startup_owner_check(mbus, py_owner)) {
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
{
|
||||
msg_val_params.owner = py_owner;
|
||||
Py_INCREF(py_owner);
|
||||
@@ -306,6 +469,10 @@ static PyObject *bpy_msgbus_subscribe_rna(PyObject * /*self*/, PyObject *args, P
|
||||
|
||||
{
|
||||
PyObject *user_data = PyTuple_New(2);
|
||||
if (!user_data) {
|
||||
Py_DECREF(py_owner);
|
||||
return nullptr;
|
||||
}
|
||||
PyTuple_SET_ITEMS(user_data, Py_NewRef(callback_args), Py_NewRef(callback_notify));
|
||||
msg_val_params.user_data = user_data;
|
||||
}
|
||||
@@ -314,6 +481,11 @@ static PyObject *bpy_msgbus_subscribe_rna(PyObject * /*self*/, PyObject *args, P
|
||||
msg_val_params.free_data = bpy_msgbus_subscribe_value_free_data;
|
||||
|
||||
WM_msg_subscribe_rna_params(mbus, &msg_key_params, &msg_val_params, __func__);
|
||||
#ifdef WITH_WEB
|
||||
web_msgbus_subscriptions.append(
|
||||
{mbus, py_owner, static_cast<PyObject *>(msg_val_params.user_data),
|
||||
BPY_web_startup_transaction_active()});
|
||||
#endif
|
||||
|
||||
if (false) { /* For debugging. */
|
||||
WM_msg_dump(mbus, __func__);
|
||||
@@ -357,7 +529,10 @@ static PyObject *bpy_msgbus_publish_rna(PyObject * /*self*/, PyObject *args, PyO
|
||||
|
||||
/* NOTE: we may want to have a way to pass this in. */
|
||||
bContext *C = BPY_context_get();
|
||||
wmMsgBus *mbus = CTX_wm_message_bus(C);
|
||||
wmMsgBus *mbus = bpy_msgbus_context_bus(C);
|
||||
if (!mbus) {
|
||||
return nullptr;
|
||||
}
|
||||
wmMsgParams_RNA msg_key_params = {{}};
|
||||
|
||||
if (py_msgbus_rna_key_from_py(py_sub, &msg_key_params, error_prefix) == -1) {
|
||||
@@ -381,7 +556,15 @@ PyDoc_STRVAR(
|
||||
static PyObject *bpy_msgbus_clear_by_owner(PyObject * /*self*/, PyObject *py_owner)
|
||||
{
|
||||
bContext *C = BPY_context_get();
|
||||
wmMsgBus *mbus = CTX_wm_message_bus(C);
|
||||
wmMsgBus *mbus = bpy_msgbus_context_bus(C);
|
||||
if (!mbus) {
|
||||
return nullptr;
|
||||
}
|
||||
#ifdef WITH_WEB
|
||||
if (!web_msgbus_startup_owner_check(mbus, py_owner)) {
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
WM_msgbus_clear_by_owner(mbus, py_owner);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
@@ -14,4 +14,10 @@ namespace blender {
|
||||
|
||||
[[nodiscard]] PyObject *BPY_msgbus_module();
|
||||
|
||||
#ifdef WITH_WEB
|
||||
struct wmMsgBus;
|
||||
void BPY_msgbus_web_reset_set(bool resetting);
|
||||
void BPY_msgbus_web_clear(wmMsgBus *mbus);
|
||||
#endif
|
||||
|
||||
} // namespace blender
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
*/
|
||||
|
||||
#include <Python.h>
|
||||
#include <exception>
|
||||
#include <optional>
|
||||
|
||||
#include "DNA_windowmanager_types.h"
|
||||
@@ -26,6 +27,7 @@
|
||||
#include "bpy_capi_utils.hh"
|
||||
#include "bpy_operator_function.hh"
|
||||
#include "bpy_rna.hh" /* for setting argument properties & type method `get_rna_type`. */
|
||||
#include "bpy_rna_access.hh"
|
||||
|
||||
#include "RNA_access.hh"
|
||||
#include "RNA_enum_types.hh"
|
||||
@@ -77,6 +79,45 @@ static const char *opname_as_py(const char *opname, char opname_buf[OP_MAX_TYPEN
|
||||
/** \name Private Utility Functions
|
||||
* \{ */
|
||||
|
||||
/* Poll and keyword conversion can call Python, including unregister/register of
|
||||
* the very operator being prepared. A name lookup alone cannot detect an
|
||||
* allocator reusing the old operator/type address. Observe the original RNA
|
||||
* lifetime until dispatch, without keeping a removed registration callable. */
|
||||
class BPyOperatorAccessScope {
|
||||
#ifdef WITH_WEB
|
||||
BPyRNAMainAccessScope main_;
|
||||
#endif
|
||||
BPyRNATypeAccessScope type_;
|
||||
const wmOperatorType *operator_;
|
||||
char idname_[OP_MAX_TYPENAME];
|
||||
|
||||
public:
|
||||
explicit BPyOperatorAccessScope(wmOperatorType *ot) : type_(ot->srna), operator_(ot)
|
||||
{
|
||||
STRNCPY(idname_, ot->idname);
|
||||
}
|
||||
|
||||
int check() const
|
||||
{
|
||||
#ifdef WITH_WEB
|
||||
if (main_.check() == -1) {
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
if (type_.check() == -1) {
|
||||
return -1;
|
||||
}
|
||||
if (WM_operatortype_find(idname_, true) != operator_) {
|
||||
if (!PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_ReferenceError,
|
||||
"Operator was removed during poll or property conversion");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update view layer dependencies.
|
||||
* If there is no active view layer update all view layers.
|
||||
@@ -196,8 +237,13 @@ static PyObject *bpy_op_fn_poll_impl(const char *opname, const char *context_str
|
||||
context = wm::OpCallContext(context_int);
|
||||
}
|
||||
|
||||
BPyOperatorAccessScope access(ot);
|
||||
/* main purpose of this function */
|
||||
PyObject *ret = WM_operator_poll_context(C, ot, context) ? Py_True : Py_False;
|
||||
if (access.check() == -1) {
|
||||
BPY_modules_update();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return Py_NewRef(ret);
|
||||
}
|
||||
@@ -285,7 +331,13 @@ static std::optional<wmOperatorStatus> bpy_op_fn_call_impl(const char *opname,
|
||||
context = wm::OpCallContext(context_int);
|
||||
}
|
||||
|
||||
if (WM_operator_poll_context(C, ot, context) == false) {
|
||||
BPyOperatorAccessScope access(ot);
|
||||
const bool poll_ok = WM_operator_poll_context(C, ot, context);
|
||||
if (access.check() == -1) {
|
||||
BPY_modules_update();
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!poll_ok) {
|
||||
bool msg_free = false;
|
||||
const char *msg = CTX_wm_operator_poll_msg_get(C, &msg_free);
|
||||
PyErr_Format(PyExc_RuntimeError,
|
||||
@@ -306,6 +358,12 @@ static std::optional<wmOperatorStatus> bpy_op_fn_call_impl(const char *opname,
|
||||
error_val = pyrna_pydict_to_props(
|
||||
&ptr, kw, false, "Converting py args to operator properties:");
|
||||
}
|
||||
/* Conversion can succeed in the new Main or on the last property even
|
||||
* though its callback retired our original dispatch target. Free only the
|
||||
* owned IDProperty storage below; never execute the saved operator then. */
|
||||
if (access.check() == -1) {
|
||||
error_val = -1;
|
||||
}
|
||||
|
||||
if (error_val == 0) {
|
||||
ReportList *reports;
|
||||
@@ -324,7 +382,24 @@ static std::optional<wmOperatorStatus> bpy_op_fn_call_impl(const char *opname,
|
||||
PyThreadState *ts = PyEval_SaveThread();
|
||||
#endif
|
||||
|
||||
retval = WM_operator_call_py(C, ot, context, &ptr, reports, is_undo);
|
||||
#ifdef __EMSCRIPTEN__
|
||||
try {
|
||||
#endif
|
||||
retval = WM_operator_call_py(C, ot, context, &ptr, reports, is_undo);
|
||||
#ifdef __EMSCRIPTEN__
|
||||
}
|
||||
catch (const std::exception &error) {
|
||||
/* Native GPU/editor failures must return through CPython's normal
|
||||
* report path. Unwinding here would skip GIL reacquisition and leave
|
||||
* the caller-owned properties/reports behind. Keep callback edits. */
|
||||
BKE_report(reports, RPT_ERROR, error.what());
|
||||
retval = OPERATOR_CANCELLED;
|
||||
}
|
||||
catch (...) {
|
||||
BKE_report(reports, RPT_ERROR, "Native operator callback failed");
|
||||
retval = OPERATOR_CANCELLED;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef BPY_RELEASE_GIL
|
||||
/* regain GIL */
|
||||
@@ -371,16 +446,18 @@ static std::optional<wmOperatorStatus> bpy_op_fn_call_impl(const char *opname,
|
||||
#endif
|
||||
}
|
||||
|
||||
if (error_val == -1) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/* When calling `bpy.ops.wm.read_factory_settings()` `bpy.data's` main pointer
|
||||
* is freed by clear_globals(), further access will crash blender.
|
||||
* Setting context is not needed in this case, only calling because this
|
||||
* function corrects bpy.data (internal Main pointer) */
|
||||
BPY_modules_update();
|
||||
|
||||
/* An operator may replace Main and still report an error. Refresh bpy.data
|
||||
* on that path too, before handing the exception back to a Python callback. */
|
||||
if (error_val == -1) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
@@ -410,6 +487,10 @@ static PyObject *bpy_op_fn_call(BPyOpFunction *self, PyObject *args, PyObject *k
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#ifdef WITH_WEB
|
||||
BPyRNAMainAccessScope main_access;
|
||||
#endif
|
||||
|
||||
/* Pre-call view-layer update.
|
||||
*
|
||||
* Run to account for any RNA values the user changes.
|
||||
@@ -417,6 +498,14 @@ static PyObject *bpy_op_fn_call(BPyOpFunction *self, PyObject *args, PyObject *k
|
||||
* operators are supposed to operate on. There might be some
|
||||
* corner cases when operator need a full scene update though. */
|
||||
bpy_op_view_layer_update(C);
|
||||
#ifdef WITH_WEB
|
||||
/* Dependency handlers run Python. Do not dispatch the pending menu action
|
||||
* into a replacement Main after its pre-call update changed ownership. */
|
||||
if (main_access.check() == -1) {
|
||||
BPY_modules_update();
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
std::optional<wmOperatorStatus> result = bpy_op_fn_call_impl(
|
||||
self->idname, kwargs, context_str, is_undo);
|
||||
@@ -426,7 +515,12 @@ static PyObject *bpy_op_fn_call(BPyOpFunction *self, PyObject *args, PyObject *k
|
||||
}
|
||||
|
||||
/* Post-call: if operator finished and window manager unchanged, update view-layer again. */
|
||||
if ((*result & OPERATOR_FINISHED) && CTX_wm_manager(C) == wm) {
|
||||
if ((*result & OPERATOR_FINISHED) &&
|
||||
#ifdef WITH_WEB
|
||||
main_access.valid() &&
|
||||
#endif
|
||||
CTX_wm_manager(C) == wm)
|
||||
{
|
||||
bpy_op_view_layer_update(C);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -304,6 +304,45 @@ void pyrna_struct_validity_exception_only(const BPy_StructRNA *pysrna);
|
||||
extern PyMethodDef meth_bpy_register_class;
|
||||
extern PyMethodDef meth_bpy_unregister_class;
|
||||
|
||||
/** Native Python callback boundary. Caller owns GIL, arguments and callable through conversion.
|
||||
* Checks do not throw C++ exceptions; errors stay in Python until the native caller reports them.
|
||||
* The Web host additionally latches failures until WM/context cleanup has completed. */
|
||||
[[nodiscard]] int pyrna_callback_check();
|
||||
void pyrna_callback_error(const char *owner, const char *callback);
|
||||
[[nodiscard]] PyObject *pyrna_callback_call(PyObject *callable,
|
||||
PyObject *args,
|
||||
const char *owner,
|
||||
const char *callback);
|
||||
|
||||
#ifdef WITH_WEB
|
||||
struct Main;
|
||||
struct bContext;
|
||||
/** Scoped host boundary; callbacks run with the GIL and must not throw C++ exceptions. */
|
||||
struct BPyWebCallbackHooks {
|
||||
int (*check)() = nullptr;
|
||||
void (*error)(const char *class_identifier, const char *function_identifier) = nullptr;
|
||||
};
|
||||
BPyWebCallbackHooks pyrna_web_callback_hooks_set(BPyWebCallbackHooks hooks);
|
||||
void pyrna_context_rebind(bContext *C);
|
||||
void pyrna_web_types_owner_set(Main *owner, bool resetting);
|
||||
/** Metadata-only query; keep a live failed-reset owner reachable for explicit cleanup. */
|
||||
[[nodiscard]] bool pyrna_web_types_owner_has_registrations(Main *owner);
|
||||
/** Bracket the native keyconfig journal. Rollback ends after WM has restored allocations;
|
||||
* commit ends before WM frees retired allocations. No Python callbacks are invoked. */
|
||||
void pyrna_web_keyconfig_startup_begin();
|
||||
void pyrna_web_keyconfig_startup_end(bool commit);
|
||||
/** Unregister the active Main's classes in reverse registration order. */
|
||||
[[nodiscard]] int pyrna_web_types_unregister(bContext *C);
|
||||
/** Session ownership for properties added to persistent built-in RNA types. */
|
||||
[[nodiscard]] bool pyrna_web_property_write_check(StructRNA *srna,
|
||||
const char *identifier,
|
||||
bool creating);
|
||||
[[nodiscard]] bool pyrna_web_property_reference_check(StructRNA *srna);
|
||||
void pyrna_web_property_track(StructRNA *srna, PropertyRNA *prop);
|
||||
void pyrna_web_property_forget(StructRNA *srna, const char *identifier);
|
||||
[[nodiscard]] int pyrna_web_property_py_data_clear(StructRNA *srna, const char *identifier);
|
||||
#endif
|
||||
|
||||
/* bpy.utils._bl_owner_(get/set) */
|
||||
extern PyMethodDef meth_bpy_owner_id_set;
|
||||
extern PyMethodDef meth_bpy_owner_id_get;
|
||||
|
||||
148
blender-5.2.0/source/blender/python/intern/bpy_rna_access.hh
Normal file
148
blender-5.2.0/source/blender/python/intern/bpy_rna_access.hh
Normal file
@@ -0,0 +1,148 @@
|
||||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Python.h>
|
||||
#include <memory>
|
||||
|
||||
#include "RNA_access.hh"
|
||||
#include "RNA_access_guard.hh"
|
||||
#include "RNA_types.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
#ifdef WITH_WEB
|
||||
class BPyRNAMainAccessScope : public RNAMainAccessScope {
|
||||
public:
|
||||
/* Capture the native module context without resolving Python context overrides. */
|
||||
BPyRNAMainAccessScope();
|
||||
|
||||
int check() const
|
||||
{
|
||||
if (valid()) {
|
||||
return 0;
|
||||
}
|
||||
if (!PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_ReferenceError,
|
||||
"Main changed during Python property conversion or callback");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* Keep the complete native inheritance chain observed while Python allocates
|
||||
* a class or runs a metaclass hook. A Python reference does not own StructRNA. */
|
||||
class BPyRNATypeAccessScope {
|
||||
#ifdef WITH_WEB
|
||||
BPyRNAMainAccessScope main_;
|
||||
#endif
|
||||
RNAPropertyAccessScope access_;
|
||||
std::unique_ptr<BPyRNATypeAccessScope> base_;
|
||||
bool retiring_;
|
||||
|
||||
public:
|
||||
explicit BPyRNATypeAccessScope(StructRNA *type)
|
||||
: access_(nullptr, nullptr, false, type), retiring_(RNATypeTeardownScope::contains(type))
|
||||
{
|
||||
if (retiring_) {
|
||||
return;
|
||||
}
|
||||
StructRNA *base = type ? RNA_struct_base(type) : nullptr;
|
||||
if (base && base != type) {
|
||||
base_ = std::make_unique<BPyRNATypeAccessScope>(base);
|
||||
}
|
||||
}
|
||||
|
||||
bool target_valid() const
|
||||
{
|
||||
return !retiring_ && access_.valid();
|
||||
}
|
||||
|
||||
int check() const
|
||||
{
|
||||
if (!target_valid()) {
|
||||
if (!PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_ReferenceError,
|
||||
"RNA type was removed during Python class construction");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return base_ ? base_->check() : 0;
|
||||
}
|
||||
};
|
||||
|
||||
class BPyRNAPropertyAccessScope : public RNAPropertyAccessScope {
|
||||
#ifdef WITH_WEB
|
||||
BPyRNAMainAccessScope main_;
|
||||
#endif
|
||||
|
||||
public:
|
||||
BPyRNAPropertyAccessScope(const PointerRNA *ptr, PropertyRNA *prop)
|
||||
: RNAPropertyAccessScope(ptr->owner_id, prop, false, ptr->type)
|
||||
{
|
||||
observe_pointer(*ptr);
|
||||
}
|
||||
|
||||
int check() const
|
||||
{
|
||||
#ifdef WITH_WEB
|
||||
if (main_.check() == -1) {
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
if (!callback_valid()) {
|
||||
if (!PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_RuntimeError,
|
||||
"RNA property callback failed; assignment was not completed");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (valid()) {
|
||||
return 0;
|
||||
}
|
||||
/* Neither the wrapper nor the property metadata may survive the callback. */
|
||||
if (!PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_ReferenceError,
|
||||
"RNA access target was removed during a Python callback");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
class BPyRNAFunctionAccessScope {
|
||||
BPyRNAPropertyAccessScope owner_;
|
||||
BPyRNATypeAccessScope type_;
|
||||
RNAFunctionAccessScope function_;
|
||||
|
||||
public:
|
||||
BPyRNAFunctionAccessScope(const PointerRNA *ptr, FunctionRNA *func, ParameterList *parameters)
|
||||
: owner_(ptr, nullptr), type_(ptr->type), function_(func)
|
||||
{
|
||||
ParameterIterator iter;
|
||||
RNA_parameter_list_begin(parameters, &iter);
|
||||
for (; iter.valid; RNA_parameter_list_next(&iter)) {
|
||||
function_.add_parameter(iter.parm);
|
||||
}
|
||||
RNA_parameter_list_end(&iter);
|
||||
}
|
||||
|
||||
int check() const
|
||||
{
|
||||
if (owner_.check() == -1 || type_.check() == -1) {
|
||||
return -1;
|
||||
}
|
||||
if (!function_.valid()) {
|
||||
if (!PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_ReferenceError,
|
||||
"RNA function or parameter was removed during a Python callback");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "RNA_types.hh"
|
||||
|
||||
#include "bpy_rna.hh"
|
||||
#include "bpy_rna_access.hh"
|
||||
|
||||
#include "MEM_guardedalloc.h"
|
||||
|
||||
@@ -234,11 +235,18 @@ static int validate_array_length(PyObject *rvalue,
|
||||
int *r_totitem,
|
||||
const char *error_prefix)
|
||||
{
|
||||
BPyRNAPropertyAccessScope access(ptr, prop);
|
||||
int dimsize[MAX_ARRAY_DIMENSION];
|
||||
int tot, totdim, len;
|
||||
|
||||
totdim = RNA_property_array_dimension(ptr, prop, dimsize);
|
||||
if (access.check() == -1) {
|
||||
return -1;
|
||||
}
|
||||
tot = count_items(rvalue, totdim - lvalue_dim);
|
||||
if (access.check() == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (tot == -1) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
@@ -250,6 +258,9 @@ static int validate_array_length(PyObject *rvalue,
|
||||
}
|
||||
if ((RNA_property_flag(prop) & PROP_DYNAMIC) && lvalue_dim == 0) {
|
||||
const int tot_expected = RNA_property_array_length(ptr, prop);
|
||||
if (access.check() == -1) {
|
||||
return -1;
|
||||
}
|
||||
if (tot_expected != tot) {
|
||||
*r_totitem = tot;
|
||||
if (!prop_is_param_dyn_alloc) {
|
||||
@@ -271,6 +282,9 @@ static int validate_array_length(PyObject *rvalue,
|
||||
/* length is a constraint */
|
||||
if (!lvalue_dim) {
|
||||
len = RNA_property_array_length(ptr, prop);
|
||||
if (access.check() == -1) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
/* array item assignment */
|
||||
else {
|
||||
@@ -323,8 +337,12 @@ static int validate_array(PyObject *rvalue,
|
||||
int *r_totitem,
|
||||
const char *error_prefix)
|
||||
{
|
||||
BPyRNAPropertyAccessScope access(ptr, prop);
|
||||
int dimsize[MAX_ARRAY_DIMENSION];
|
||||
const int totdim = RNA_property_array_dimension(ptr, prop, dimsize);
|
||||
if (access.check() == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* validate type first because length validation may modify property array length */
|
||||
|
||||
@@ -333,7 +351,8 @@ static int validate_array(PyObject *rvalue,
|
||||
if (MatrixObject_Check(rvalue)) {
|
||||
MatrixObject *pymat = reinterpret_cast<MatrixObject *>(rvalue);
|
||||
|
||||
if (BaseMath_ReadCallback(pymat) == -1) {
|
||||
const int read_result = BaseMath_ReadCallback(pymat);
|
||||
if (access.check() == -1 || read_result == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -376,14 +395,15 @@ static int validate_array(PyObject *rvalue,
|
||||
|
||||
{
|
||||
const int prop_flag = RNA_property_flag(prop);
|
||||
if (validate_array_type(rvalue,
|
||||
const int type_result = validate_array_type(rvalue,
|
||||
lvalue_dim,
|
||||
totdim,
|
||||
dimsize,
|
||||
(prop_flag & PROP_DYNAMIC) != 0,
|
||||
check_item_type,
|
||||
item_type_str,
|
||||
error_prefix) == -1)
|
||||
error_prefix);
|
||||
if (access.check() == -1 || type_result == -1)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
@@ -402,6 +422,7 @@ static char *copy_value_single(PyObject *item,
|
||||
const ItemConvert_FuncArg *convert_item,
|
||||
RNA_SetIndexFunc rna_set_index)
|
||||
{
|
||||
BPyRNAPropertyAccessScope access(ptr, prop);
|
||||
if (!data) {
|
||||
union {
|
||||
float fl;
|
||||
@@ -410,11 +431,20 @@ static char *copy_value_single(PyObject *item,
|
||||
char *value = static_cast<char *>(static_cast<void *>(&value_buf));
|
||||
|
||||
convert_item->func(&convert_item->arg, item, value);
|
||||
if (access.check() == -1 || PyErr_Occurred()) {
|
||||
return nullptr;
|
||||
}
|
||||
rna_set_index(ptr, prop, *index, value);
|
||||
if (access.check() == -1) {
|
||||
return nullptr;
|
||||
}
|
||||
(*index) += 1;
|
||||
}
|
||||
else {
|
||||
convert_item->func(&convert_item->arg, item, data);
|
||||
if (access.check() == -1 || PyErr_Occurred()) {
|
||||
return nullptr;
|
||||
}
|
||||
data += item_size;
|
||||
}
|
||||
|
||||
@@ -435,8 +465,10 @@ static char *copy_values_fast(PyObject *seq,
|
||||
const ItemConvert_FuncArg *convert_item,
|
||||
RNA_SetIndexFunc rna_set_index)
|
||||
{
|
||||
BPyRNAPropertyAccessScope access(ptr, prop);
|
||||
PyObject *seq_fast = PySequence_Fast(seq, "");
|
||||
if (seq_fast == nullptr) {
|
||||
if (access.check() == -1 || seq_fast == nullptr) {
|
||||
Py_XDECREF(seq_fast);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -455,17 +487,26 @@ static char *copy_values_fast(PyObject *seq,
|
||||
index,
|
||||
convert_item,
|
||||
rna_set_index);
|
||||
if (access.check() == -1 || PyErr_Occurred()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (Py_ssize_t i = 0; i < seq_size; i++) {
|
||||
data = copy_value_single(
|
||||
seq_items[i], ptr, prop, data, item_size, index, convert_item, rna_set_index);
|
||||
if (access.check() == -1 || PyErr_Occurred()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Py_DECREF(seq_fast);
|
||||
|
||||
if (access.check() == -1 || PyErr_Occurred()) {
|
||||
return nullptr;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -479,6 +520,7 @@ static char *copy_values(PyObject *seq,
|
||||
const ItemConvert_FuncArg *convert_item,
|
||||
RNA_SetIndexFunc rna_set_index)
|
||||
{
|
||||
BPyRNAPropertyAccessScope access(ptr, prop);
|
||||
#ifdef USE_MATHUTILS
|
||||
if (dim == 0) {
|
||||
if (MatrixObject_Check(seq)) {
|
||||
@@ -498,6 +540,9 @@ static char *copy_values(PyObject *seq,
|
||||
#endif /* USE_MATHUTILS */
|
||||
|
||||
const int totdim = RNA_property_array_dimension(ptr, prop, nullptr);
|
||||
if (access.check() == -1) {
|
||||
return nullptr;
|
||||
}
|
||||
return copy_values_fast(
|
||||
seq, ptr, prop, dim, totdim, data, item_size, index, convert_item, rna_set_index);
|
||||
}
|
||||
@@ -513,6 +558,7 @@ static int py_to_array(PyObject *seq,
|
||||
RNA_SetArrayFunc rna_set_array,
|
||||
const char *error_prefix)
|
||||
{
|
||||
BPyRNAPropertyAccessScope access(ptr, prop);
|
||||
// int totdim, dim_size[MAX_ARRAY_DIMENSION];
|
||||
int totitem;
|
||||
char *data = nullptr;
|
||||
@@ -523,7 +569,7 @@ static int py_to_array(PyObject *seq,
|
||||
/* Use #ParameterDynAlloc which defines its own array length. */
|
||||
const bool prop_is_param_dyn_alloc = param_data && (flag & PROP_DYNAMIC);
|
||||
|
||||
if (validate_array(seq,
|
||||
const int validation_result = validate_array(seq,
|
||||
ptr,
|
||||
prop,
|
||||
prop_is_param_dyn_alloc,
|
||||
@@ -531,7 +577,8 @@ static int py_to_array(PyObject *seq,
|
||||
check_item_type,
|
||||
item_type_str,
|
||||
&totitem,
|
||||
error_prefix) == -1)
|
||||
error_prefix);
|
||||
if (access.check() == -1 || validation_result == -1)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
@@ -553,11 +600,16 @@ static int py_to_array(PyObject *seq,
|
||||
}
|
||||
else {
|
||||
data = static_cast<char *>(PyMem_MALLOC(item_size * totitem));
|
||||
if (!data) {
|
||||
PyErr_NoMemory();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* will only fail in very rare cases since we already validated the
|
||||
* python data, the check here is mainly for completeness. */
|
||||
if (copy_values(seq, ptr, prop, 0, data, item_size, nullptr, convert_item, nullptr) != nullptr)
|
||||
char *end = copy_values(seq, ptr, prop, 0, data, item_size, nullptr, convert_item, nullptr);
|
||||
if (access.check() == 0 && !PyErr_Occurred() && end != nullptr)
|
||||
{
|
||||
if (param_data == nullptr) {
|
||||
/* nullptr can only pass through in case RNA property array-length is 0 (impossible?) */
|
||||
@@ -570,15 +622,17 @@ static int py_to_array(PyObject *seq,
|
||||
PyMem_FREE(data);
|
||||
}
|
||||
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
if (!PyErr_Occurred()) {
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
"%s internal error parsing sequence of type '%s' after successful validation",
|
||||
error_prefix,
|
||||
Py_TYPE(seq)->tp_name);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
return access.check();
|
||||
}
|
||||
|
||||
static int py_to_array_index(PyObject *py,
|
||||
@@ -593,10 +647,14 @@ static int py_to_array_index(PyObject *py,
|
||||
RNA_SetIndexFunc rna_set_index,
|
||||
const char *error_prefix)
|
||||
{
|
||||
BPyRNAPropertyAccessScope access(ptr, prop);
|
||||
int totdim, dimsize[MAX_ARRAY_DIMENSION];
|
||||
int totitem, i;
|
||||
|
||||
totdim = RNA_property_array_dimension(ptr, prop, dimsize);
|
||||
if (access.check() == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* convert index */
|
||||
|
||||
@@ -631,7 +689,7 @@ static int py_to_array_index(PyObject *py,
|
||||
}
|
||||
else {
|
||||
const bool prop_is_param_dyn_alloc = false;
|
||||
if (validate_array(py,
|
||||
const int validation_result = validate_array(py,
|
||||
ptr,
|
||||
prop,
|
||||
prop_is_param_dyn_alloc,
|
||||
@@ -639,7 +697,8 @@ static int py_to_array_index(PyObject *py,
|
||||
check_item_type,
|
||||
item_type_str,
|
||||
&totitem,
|
||||
error_prefix) == -1)
|
||||
error_prefix);
|
||||
if (access.check() == -1 || validation_result == -1)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
@@ -648,7 +707,7 @@ static int py_to_array_index(PyObject *py,
|
||||
copy_values(py, ptr, prop, lvalue_dim, nullptr, 0, &index, convert_item, rna_set_index);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
return (access.check() == -1 || PyErr_Occurred()) ? -1 : 0;
|
||||
}
|
||||
|
||||
static void py_to_float(const ItemConvertArgData *arg, PyObject *py, char *data)
|
||||
@@ -732,11 +791,17 @@ static void convert_item_init_bool(PointerRNA * /*ptr*/,
|
||||
int pyrna_py_to_array(
|
||||
PointerRNA *ptr, PropertyRNA *prop, char *param_data, PyObject *py, const char *error_prefix)
|
||||
{
|
||||
PointerRNA target = *ptr;
|
||||
if (ptr->data != nullptr) { ptr = ⌖ }
|
||||
BPyRNAPropertyAccessScope access(ptr, prop);
|
||||
int ret;
|
||||
switch (RNA_property_type(prop)) {
|
||||
case PROP_FLOAT: {
|
||||
ItemConvert_FuncArg convert_item;
|
||||
convert_item_init_float(ptr, prop, &convert_item);
|
||||
if (access.check() == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = py_to_array(py,
|
||||
ptr,
|
||||
@@ -753,6 +818,9 @@ int pyrna_py_to_array(
|
||||
case PROP_INT: {
|
||||
ItemConvert_FuncArg convert_item;
|
||||
convert_item_init_int(ptr, prop, &convert_item);
|
||||
if (access.check() == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = py_to_array(py,
|
||||
ptr,
|
||||
@@ -789,7 +857,7 @@ int pyrna_py_to_array(
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
return access.check() == -1 ? -1 : ret;
|
||||
}
|
||||
|
||||
int pyrna_py_to_array_index(PointerRNA *ptr,
|
||||
@@ -800,11 +868,17 @@ int pyrna_py_to_array_index(PointerRNA *ptr,
|
||||
PyObject *py,
|
||||
const char *error_prefix)
|
||||
{
|
||||
PointerRNA target = *ptr;
|
||||
ptr = ⌖
|
||||
BPyRNAPropertyAccessScope access(ptr, prop);
|
||||
int ret;
|
||||
switch (RNA_property_type(prop)) {
|
||||
case PROP_FLOAT: {
|
||||
ItemConvert_FuncArg convert_item;
|
||||
convert_item_init_float(ptr, prop, &convert_item);
|
||||
if (access.check() == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = py_to_array_index(py,
|
||||
ptr,
|
||||
@@ -822,6 +896,9 @@ int pyrna_py_to_array_index(PointerRNA *ptr,
|
||||
case PROP_INT: {
|
||||
ItemConvert_FuncArg convert_item;
|
||||
convert_item_init_int(ptr, prop, &convert_item);
|
||||
if (access.check() == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = py_to_array_index(py,
|
||||
ptr,
|
||||
@@ -860,7 +937,7 @@ int pyrna_py_to_array_index(PointerRNA *ptr,
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
return access.check() == -1 ? -1 : ret;
|
||||
}
|
||||
|
||||
PyObject *pyrna_array_index(PointerRNA *ptr, PropertyRNA *prop, int index)
|
||||
|
||||
@@ -10,6 +10,16 @@
|
||||
*/
|
||||
|
||||
#include <Python.h>
|
||||
#ifdef WITH_WEB
|
||||
# include <algorithm>
|
||||
# include <vector>
|
||||
# include "BKE_context.hh"
|
||||
# include "BKE_main.hh"
|
||||
# include "BLI_listbase.h"
|
||||
# include "DNA_windowmanager_types.h"
|
||||
#endif
|
||||
|
||||
#include "MEM_guardedalloc.h"
|
||||
|
||||
#include "../generic/py_capi_rna.hh"
|
||||
#include "../generic/python_utildefines.hh"
|
||||
@@ -21,6 +31,8 @@
|
||||
#include "RNA_prototypes.hh"
|
||||
|
||||
#include "BKE_screen.hh"
|
||||
#include "BKE_context.hh"
|
||||
#include "BKE_global.hh"
|
||||
|
||||
#include "WM_api.hh"
|
||||
|
||||
@@ -38,6 +50,111 @@ namespace blender {
|
||||
static const char *rna_capsule_id = "RNA_HANDLE";
|
||||
static const char *rna_capsule_id_invalid = "RNA_HANDLE_REMOVED";
|
||||
|
||||
/* The native registration owns args. The capsule is a borrowed back-pointer, cleared by its
|
||||
* destructor. Native teardown invalidates a surviving capsule before releasing Python objects. */
|
||||
struct BPyDrawCallback {
|
||||
PyObject *args;
|
||||
PyObject *capsule = nullptr;
|
||||
StructRNA *owner;
|
||||
ARegionType *region_type = nullptr;
|
||||
#ifdef WITH_WEB
|
||||
Main *main = nullptr;
|
||||
void *handle = nullptr;
|
||||
bool startup_delta = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifdef WITH_WEB
|
||||
static Main *web_callback_owner = nullptr;
|
||||
static bool web_callback_resetting = false;
|
||||
static std::vector<BPyDrawCallback *> web_draw_callbacks;
|
||||
static bool web_startup_transaction = false;
|
||||
|
||||
void BPY_web_startup_transaction_set(bool active) { web_startup_transaction = active; }
|
||||
bool BPY_web_startup_transaction_active() { return web_startup_transaction; }
|
||||
|
||||
void BPY_callback_web_owner_set(Main *owner, bool resetting)
|
||||
{
|
||||
web_callback_owner = owner;
|
||||
web_callback_resetting = resetting;
|
||||
}
|
||||
Main *BPY_callback_web_owner_get() { return web_callback_owner; }
|
||||
bool BPY_callback_web_resetting() { return web_callback_resetting; }
|
||||
|
||||
BPyWebRegistrationOwnerScope::BPyWebRegistrationOwnerScope(bContext *context)
|
||||
: owner_(CTX_data_main(context)), context_(context), lifetime_(owner_, context),
|
||||
previous_(active_)
|
||||
{
|
||||
active_ = this;
|
||||
}
|
||||
|
||||
BPyWebRegistrationOwnerScope::~BPyWebRegistrationOwnerScope()
|
||||
{
|
||||
active_ = previous_;
|
||||
}
|
||||
|
||||
int BPyWebRegistrationOwnerScope::synchronize()
|
||||
{
|
||||
auto *scope = active_;
|
||||
if (!scope || !scope->owner_ || web_callback_resetting ||
|
||||
scope->lifetime_.main_alive())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (!scope->lifetime_.context_alive() || BPY_context_get() != scope->context_ ||
|
||||
!G_MAIN || CTX_data_main(scope->context_) != G_MAIN ||
|
||||
web_callback_owner != scope->owner_)
|
||||
{
|
||||
PyErr_SetString(PyExc_RuntimeError, "PYTHON_REGISTRATION_REPLACEMENT_CONTEXT_INVALID");
|
||||
return -1;
|
||||
}
|
||||
Main *previous = scope->owner_;
|
||||
Main *replacement = G_MAIN;
|
||||
if (BPY_web_registration_owner_replace(previous, replacement) < 0) {
|
||||
return -1;
|
||||
}
|
||||
scope->owner_ = replacement;
|
||||
scope->lifetime_.follow_decoded_main_lifetime(replacement);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int BPY_web_registration_owner_replace(Main *previous, Main *replacement)
|
||||
{
|
||||
/* Rekey without releasing Python objects or running finalizers. Reject an
|
||||
* existing destination bank before changing any journal or callback owner. */
|
||||
if (BPY_app_handlers_web_owner_replace(previous, replacement) < 0) {
|
||||
return -1;
|
||||
}
|
||||
pyrna_web_types_owner_replace(previous, replacement);
|
||||
for (BPyDrawCallback *callback : web_draw_callbacks) {
|
||||
if (callback->main == previous) {
|
||||
callback->main = replacement;
|
||||
}
|
||||
}
|
||||
BPY_app_timers_web_owner_replace(previous, replacement);
|
||||
if (web_callback_owner == previous) {
|
||||
web_callback_owner = replacement;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool web_draw_callback_allowed(const bContext *C, void *customdata)
|
||||
{
|
||||
const auto *data = static_cast<BPyDrawCallback *>(customdata);
|
||||
return !web_callback_resetting && (!data->main || data->main == CTX_data_main(C));
|
||||
}
|
||||
#endif
|
||||
|
||||
static void cb_customdata_free(void *customdata);
|
||||
|
||||
static int draw_callback_teardown_depth = 0;
|
||||
|
||||
class BPyDrawCallbackTeardownScope {
|
||||
public:
|
||||
BPyDrawCallbackTeardownScope() { draw_callback_teardown_depth++; }
|
||||
~BPyDrawCallbackTeardownScope() { draw_callback_teardown_depth--; }
|
||||
};
|
||||
|
||||
static const EnumPropertyItem region_draw_mode_items[] = {
|
||||
{REGION_DRAW_POST_PIXEL, "POST_PIXEL", 0, "Post Pixel", ""},
|
||||
{REGION_DRAW_POST_VIEW, "POST_VIEW", 0, "Post View", ""},
|
||||
@@ -48,14 +165,21 @@ static const EnumPropertyItem region_draw_mode_items[] = {
|
||||
|
||||
static void cb_region_draw(const bContext *C, ARegion * /*region*/, void *customdata)
|
||||
{
|
||||
#ifdef WITH_WEB
|
||||
if (!web_draw_callback_allowed(C, customdata)) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
PyGILState_STATE gilstate;
|
||||
bpy_context_set(const_cast<bContext *>(C), &gilstate);
|
||||
|
||||
PyObject *cb_func, *cb_args, *result;
|
||||
|
||||
cb_func = PyTuple_GET_ITEM((PyObject *)customdata, 1);
|
||||
cb_args = PyTuple_GET_ITEM((PyObject *)customdata, 2);
|
||||
result = PyObject_CallObject(cb_func, cb_args);
|
||||
/* Removal (including removal of another handler) may free customdata during the call. */
|
||||
PyObject *registration = Py_NewRef(static_cast<BPyDrawCallback *>(customdata)->args);
|
||||
cb_func = PyTuple_GET_ITEM(registration, 1);
|
||||
cb_args = PyTuple_GET_ITEM(registration, 2);
|
||||
result = pyrna_callback_call(cb_func, cb_args, "Space", "draw_handler");
|
||||
|
||||
if (result) {
|
||||
Py_DECREF(result);
|
||||
@@ -63,6 +187,7 @@ static void cb_region_draw(const bContext *C, ARegion * /*region*/, void *custom
|
||||
else {
|
||||
PyErr_Print();
|
||||
}
|
||||
Py_DECREF(registration);
|
||||
|
||||
bpy_context_clear(const_cast<bContext *>(C), &gilstate);
|
||||
}
|
||||
@@ -71,6 +196,9 @@ static void cb_region_draw(const bContext *C, ARegion * /*region*/, void *custom
|
||||
static PyObject *PyC_Tuple_CopySized(PyObject *src, int len_dst)
|
||||
{
|
||||
PyObject *dst = PyTuple_New(len_dst);
|
||||
if (!dst) {
|
||||
return nullptr;
|
||||
}
|
||||
const int len_src = PyTuple_GET_SIZE(src);
|
||||
BLI_assert(len_src <= len_dst);
|
||||
for (int i = 0; i < len_src; i++) {
|
||||
@@ -86,31 +214,46 @@ static void cb_wm_cursor_draw(bContext *C,
|
||||
const float2 & /*tilt*/,
|
||||
void *customdata)
|
||||
{
|
||||
#ifdef WITH_WEB
|
||||
if (!web_draw_callback_allowed(C, customdata)) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
PyGILState_STATE gilstate;
|
||||
bpy_context_set(C, &gilstate);
|
||||
|
||||
PyObject *cb_func, *cb_args, *result;
|
||||
cb_func = PyTuple_GET_ITEM((PyObject *)customdata, 1);
|
||||
cb_args = PyTuple_GET_ITEM((PyObject *)customdata, 2);
|
||||
PyObject *registration = Py_NewRef(static_cast<BPyDrawCallback *>(customdata)->args);
|
||||
cb_func = PyTuple_GET_ITEM(registration, 1);
|
||||
cb_args = PyTuple_GET_ITEM(registration, 2);
|
||||
|
||||
const int cb_args_len = PyTuple_GET_SIZE(cb_args);
|
||||
|
||||
PyObject *cb_args_xy = PyTuple_New(2);
|
||||
PyTuple_SET_ITEMS(cb_args_xy, PyLong_FromLong(xy.x), PyLong_FromLong(xy.y));
|
||||
|
||||
PyObject *cb_args_with_xy = PyC_Tuple_CopySized(cb_args, cb_args_len + 1);
|
||||
PyTuple_SET_ITEM(cb_args_with_xy, cb_args_len, cb_args_xy);
|
||||
|
||||
result = PyObject_CallObject(cb_func, cb_args_with_xy);
|
||||
|
||||
Py_DECREF(cb_args_with_xy);
|
||||
result = nullptr;
|
||||
if (pyrna_callback_check() == 0) {
|
||||
PyObject *cb_args_xy = Py_BuildValue("(ii)", xy.x, xy.y);
|
||||
if (cb_args_xy) {
|
||||
PyObject *cb_args_with_xy = PyC_Tuple_CopySized(cb_args, cb_args_len + 1);
|
||||
if (cb_args_with_xy) {
|
||||
PyTuple_SET_ITEM(cb_args_with_xy, cb_args_len, cb_args_xy);
|
||||
result = pyrna_callback_call(
|
||||
cb_func, cb_args_with_xy, "WindowManager", "draw_cursor");
|
||||
Py_DECREF(cb_args_with_xy);
|
||||
}
|
||||
else {
|
||||
Py_DECREF(cb_args_xy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result) {
|
||||
Py_DECREF(result);
|
||||
}
|
||||
else {
|
||||
pyrna_callback_error("WindowManager", "draw_cursor");
|
||||
PyErr_Print();
|
||||
}
|
||||
Py_DECREF(registration);
|
||||
|
||||
bpy_context_clear(C, &gilstate);
|
||||
}
|
||||
@@ -251,16 +394,32 @@ static eSpace_Type rna_Space_refine_reverse(StructRNA *srna)
|
||||
|
||||
static void cb_rna_capsule_destructor(PyObject *capsule)
|
||||
{
|
||||
PyObject *args = static_cast<PyObject *>(PyCapsule_GetContext(capsule));
|
||||
Py_DECREF(args);
|
||||
auto *data = static_cast<BPyDrawCallback *>(PyCapsule_GetContext(capsule));
|
||||
data->capsule = nullptr;
|
||||
}
|
||||
|
||||
PyObject *pyrna_callback_classmethod_add(PyObject * /*self*/, PyObject *args)
|
||||
{
|
||||
#ifdef WITH_WEB
|
||||
if (BPyWebRegistrationOwnerScope::synchronize() < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
if (web_callback_resetting) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Cannot register draw callbacks during Main reset");
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
/* Releasing args can invoke Python finalizers. Do not let them repopulate a native list
|
||||
* whose owning screen type or WM is being destroyed. */
|
||||
if (draw_callback_teardown_depth != 0) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Cannot register draw callbacks during native teardown");
|
||||
return nullptr;
|
||||
}
|
||||
void *handle;
|
||||
PyObject *cls;
|
||||
PyObject *cb_func, *cb_args;
|
||||
StructRNA *srna;
|
||||
BPyDrawCallback *data = nullptr;
|
||||
|
||||
if (PyTuple_GET_SIZE(args) < 2) {
|
||||
PyErr_SetString(PyExc_ValueError, "handler_add(handler): expected at least 2 args");
|
||||
@@ -303,11 +462,14 @@ PyObject *pyrna_callback_classmethod_add(PyObject * /*self*/, PyObject *args)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
data = MEM_new<BPyDrawCallback>(__func__);
|
||||
data->args = Py_NewRef(args);
|
||||
data->owner = srna;
|
||||
handle = WM_paint_cursor_activate(params.space_type_enum.value,
|
||||
params.region_type_enum.value,
|
||||
nullptr,
|
||||
cb_wm_cursor_draw,
|
||||
static_cast<void *>(args));
|
||||
data);
|
||||
}
|
||||
else if (RNA_struct_is_a(srna, RNA_Space)) {
|
||||
struct {
|
||||
@@ -344,35 +506,52 @@ PyObject *pyrna_callback_classmethod_add(PyObject * /*self*/, PyObject *args)
|
||||
PyExc_TypeError, "region type %R not in space", params.region_type_enum.value_orig);
|
||||
return nullptr;
|
||||
}
|
||||
handle = ED_region_draw_cb_activate(
|
||||
art, cb_region_draw, static_cast<void *>(args), params.event_enum.value);
|
||||
data = MEM_new<BPyDrawCallback>(__func__);
|
||||
data->args = Py_NewRef(args);
|
||||
data->owner = srna;
|
||||
data->region_type = art;
|
||||
handle = ED_region_draw_cb_activate(art, cb_region_draw, data, params.event_enum.value);
|
||||
}
|
||||
else {
|
||||
PyErr_SetString(PyExc_TypeError, "callback_add(): type does not support callbacks");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* Keep the 'args' reference as long as the callback exists.
|
||||
* This reference is decremented in #BPY_callback_screen_free and #BPY_callback_wm_free. */
|
||||
Py_INCREF(args);
|
||||
|
||||
#ifdef WITH_WEB
|
||||
data->main = web_callback_owner;
|
||||
data->handle = handle;
|
||||
data->startup_delta = web_startup_transaction;
|
||||
web_draw_callbacks.push_back(data);
|
||||
#endif
|
||||
PyObject *ret = PyCapsule_New(handle, rna_capsule_id, nullptr);
|
||||
|
||||
/* Store 'args' in context as well for simple access. */
|
||||
if (!ret) {
|
||||
if (data->region_type) {
|
||||
ED_region_draw_cb_exit(data->region_type, handle);
|
||||
}
|
||||
else {
|
||||
WM_paint_cursor_end(static_cast<wmPaintCursor *>(handle));
|
||||
}
|
||||
cb_customdata_free(data);
|
||||
return nullptr;
|
||||
}
|
||||
data->capsule = ret;
|
||||
PyCapsule_SetContext(ret, data);
|
||||
PyCapsule_SetDestructor(ret, cb_rna_capsule_destructor);
|
||||
PyCapsule_SetContext(ret, args);
|
||||
Py_INCREF(args);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
PyObject *pyrna_callback_classmethod_remove(PyObject * /*self*/, PyObject *args)
|
||||
{
|
||||
#ifdef WITH_WEB
|
||||
if (BPyWebRegistrationOwnerScope::synchronize() < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
PyObject *cls;
|
||||
PyObject *py_handle;
|
||||
void *handle;
|
||||
StructRNA *srna;
|
||||
bool capsule_clear = false;
|
||||
bool handle_removed = false;
|
||||
|
||||
if (PyTuple_GET_SIZE(args) < 2) {
|
||||
@@ -391,6 +570,21 @@ PyObject *pyrna_callback_classmethod_remove(PyObject * /*self*/, PyObject *args)
|
||||
"callback_remove(handler): null handler given, invalid or already removed");
|
||||
return nullptr;
|
||||
}
|
||||
auto *data = static_cast<BPyDrawCallback *>(PyCapsule_GetContext(py_handle));
|
||||
#ifdef WITH_WEB
|
||||
if (web_startup_transaction && !data->startup_delta) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "PYTHON_STARTUP_CANNOT_REMOVE_EXISTING_CALLBACK");
|
||||
return nullptr;
|
||||
}
|
||||
if (data->main != web_callback_owner && (data->main || web_callback_resetting)) {
|
||||
PyErr_SetString(PyExc_ValueError, "callback_remove(): handler belongs to another Main");
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
if (data->owner != srna) {
|
||||
PyErr_SetString(PyExc_ValueError, "callback_remove(): handler belongs to another type");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (srna == RNA_WindowManager) {
|
||||
if (!PyArg_ParseTuple(
|
||||
@@ -399,7 +593,6 @@ PyObject *pyrna_callback_classmethod_remove(PyObject * /*self*/, PyObject *args)
|
||||
return nullptr;
|
||||
}
|
||||
handle_removed = WM_paint_cursor_end(static_cast<wmPaintCursor *>(handle));
|
||||
capsule_clear = true;
|
||||
}
|
||||
else if (RNA_struct_is_a(srna, RNA_Space)) {
|
||||
const char *error_prefix = "Space.draw_handler_remove";
|
||||
@@ -437,32 +630,23 @@ PyObject *pyrna_callback_classmethod_remove(PyObject * /*self*/, PyObject *args)
|
||||
params.region_type_enum.value_orig);
|
||||
return nullptr;
|
||||
}
|
||||
if (data->region_type != art) {
|
||||
PyErr_SetString(PyExc_ValueError, "draw_handler_remove(): handler belongs to another region");
|
||||
return nullptr;
|
||||
}
|
||||
handle_removed = ED_region_draw_cb_exit(art, handle);
|
||||
capsule_clear = true;
|
||||
}
|
||||
else {
|
||||
PyErr_SetString(PyExc_TypeError, "callback_remove(): type does not support callbacks");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* When `handle_removed == false`: Blender has already freed the data
|
||||
* (freeing screen data when loading a new file for example).
|
||||
* This will have already decremented the user, so don't decrement twice. */
|
||||
if (handle_removed == true) {
|
||||
/* The handle has been removed, so decrement its custom-data. */
|
||||
PyObject *handle_args = static_cast<PyObject *>(PyCapsule_GetContext(py_handle));
|
||||
Py_DECREF(handle_args);
|
||||
}
|
||||
|
||||
/* don't allow reuse */
|
||||
if (capsule_clear) {
|
||||
PyCapsule_Destructor destructor_fn = PyCapsule_GetDestructor(py_handle);
|
||||
if (destructor_fn) {
|
||||
destructor_fn(py_handle);
|
||||
PyCapsule_SetDestructor(py_handle, nullptr);
|
||||
}
|
||||
PyCapsule_SetName(py_handle, rna_capsule_id_invalid);
|
||||
if (!handle_removed) {
|
||||
/* The cursor may temporarily belong to an inactive WM/list. Keep its owner alive. */
|
||||
PyErr_SetString(PyExc_ValueError, "callback_remove(): handler is not in the active draw list");
|
||||
return nullptr;
|
||||
}
|
||||
cb_customdata_free(data);
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
@@ -475,24 +659,86 @@ static void cb_customdata_free(void *customdata)
|
||||
{
|
||||
PyGILState_STATE gilstate = PyGILState_Ensure();
|
||||
|
||||
PyObject *tuple = static_cast<PyObject *>(customdata);
|
||||
Py_DECREF(tuple);
|
||||
auto *data = static_cast<BPyDrawCallback *>(customdata);
|
||||
#ifdef WITH_WEB
|
||||
std::erase(web_draw_callbacks, data);
|
||||
#endif
|
||||
if (data->capsule) {
|
||||
PyCapsule_SetDestructor(data->capsule, nullptr);
|
||||
PyCapsule_SetName(data->capsule, rna_capsule_id_invalid);
|
||||
PyCapsule_SetContext(data->capsule, nullptr);
|
||||
data->capsule = nullptr;
|
||||
}
|
||||
Py_DECREF(data->args);
|
||||
MEM_delete(data);
|
||||
|
||||
PyGILState_Release(gilstate);
|
||||
}
|
||||
|
||||
void BPY_callback_screen_free(ARegionType *art)
|
||||
{
|
||||
BPyDrawCallbackTeardownScope teardown;
|
||||
ED_region_draw_cb_remove_by_type(
|
||||
art, reinterpret_cast<void *>(cb_region_draw), cb_customdata_free);
|
||||
}
|
||||
|
||||
void BPY_callback_wm_free(wmWindowManager *wm)
|
||||
{
|
||||
BPyDrawCallbackTeardownScope teardown;
|
||||
WM_paint_cursor_remove_by_type(
|
||||
wm, reinterpret_cast<void *>(cb_wm_cursor_draw), cb_customdata_free);
|
||||
}
|
||||
|
||||
#ifdef WITH_WEB
|
||||
void BPY_callback_web_startup_rollback()
|
||||
{
|
||||
BPyDrawCallbackTeardownScope teardown;
|
||||
for (;;) {
|
||||
auto it = std::find_if(web_draw_callbacks.begin(), web_draw_callbacks.end(),
|
||||
[](BPyDrawCallback *data) { return data->startup_delta; });
|
||||
if (it == web_draw_callbacks.end()) {
|
||||
return;
|
||||
}
|
||||
BPyDrawCallback *data = *it;
|
||||
if (data->region_type) {
|
||||
ED_region_draw_cb_exit(data->region_type, data->handle);
|
||||
}
|
||||
else {
|
||||
WM_paint_cursor_end(static_cast<wmPaintCursor *>(data->handle));
|
||||
}
|
||||
cb_customdata_free(data);
|
||||
}
|
||||
}
|
||||
|
||||
void BPY_callback_web_clear(bContext *C)
|
||||
{
|
||||
BPY_callback_web_clear(CTX_data_main(C));
|
||||
}
|
||||
|
||||
void BPY_callback_web_clear(Main *owner)
|
||||
{
|
||||
BPyDrawCallbackTeardownScope teardown;
|
||||
/* Drain tracked registrations, not a WM list across Python finalizers. Native
|
||||
* Main destruction removes its cursors from this registry before returning. */
|
||||
for (;;) {
|
||||
auto it = std::find_if(web_draw_callbacks.begin(), web_draw_callbacks.end(),
|
||||
[owner](BPyDrawCallback *data) { return data->main == owner; });
|
||||
if (it == web_draw_callbacks.end()) {
|
||||
break;
|
||||
}
|
||||
BPyDrawCallback *data = *it;
|
||||
if (data->region_type) {
|
||||
ED_region_draw_cb_exit(data->region_type, data->handle);
|
||||
}
|
||||
else {
|
||||
WM_paint_cursor_end(static_cast<wmPaintCursor *>(data->handle));
|
||||
}
|
||||
/* Erase before DECREF: finalizers may remove other registrations. */
|
||||
cb_customdata_free(data);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/** \} */
|
||||
|
||||
} // namespace blender
|
||||
|
||||
@@ -9,11 +9,85 @@
|
||||
*/
|
||||
|
||||
#include <Python.h>
|
||||
#ifdef WITH_WEB
|
||||
# include "RNA_access_guard.hh"
|
||||
#endif
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct BPy_StructRNA;
|
||||
|
||||
#ifdef WITH_WEB
|
||||
struct Main;
|
||||
struct bContext;
|
||||
/* Install before reset releases any Python reference. The journal owner stays
|
||||
* fixed while native cleanup can follow Main replacement in the live context. */
|
||||
class BPyWebRNAUnregisterScope {
|
||||
Main *owner_;
|
||||
Main *main_;
|
||||
bContext *context_;
|
||||
RNAMainAccessScope lifetime_;
|
||||
BPyWebRNAUnregisterScope *previous_;
|
||||
inline static BPyWebRNAUnregisterScope *active_ = nullptr;
|
||||
|
||||
public:
|
||||
BPyWebRNAUnregisterScope(Main *owner, bContext *context);
|
||||
~BPyWebRNAUnregisterScope();
|
||||
BPyWebRNAUnregisterScope(const BPyWebRNAUnregisterScope &) = delete;
|
||||
BPyWebRNAUnregisterScope &operator=(const BPyWebRNAUnregisterScope &) = delete;
|
||||
static bool owns(Main *owner);
|
||||
static Main *main_for_owner(Main *owner, bContext *context);
|
||||
static bool context_alive_for_owner(Main *owner);
|
||||
/* Audited metadata/UI retirement may outlive the reset context. A surviving
|
||||
* Main must still have its UI references cleared by the native callback. */
|
||||
static Main *metadata_main_for_owner(Main *owner);
|
||||
};
|
||||
/* Observe the session, not a temporary bpy.context override. Only a destroyed
|
||||
* Main in this same live context can transfer persistent registrations. */
|
||||
class BPyWebRegistrationOwnerScope {
|
||||
Main *owner_;
|
||||
bContext *context_;
|
||||
RNAMainAccessScope lifetime_;
|
||||
BPyWebRegistrationOwnerScope *previous_;
|
||||
inline static BPyWebRegistrationOwnerScope *active_ = nullptr;
|
||||
|
||||
public:
|
||||
explicit BPyWebRegistrationOwnerScope(bContext *context);
|
||||
~BPyWebRegistrationOwnerScope();
|
||||
BPyWebRegistrationOwnerScope(const BPyWebRegistrationOwnerScope &) = delete;
|
||||
BPyWebRegistrationOwnerScope &operator=(const BPyWebRegistrationOwnerScope &) = delete;
|
||||
static int synchronize();
|
||||
};
|
||||
void pyrna_web_types_owner_replace(Main *previous, Main *replacement);
|
||||
/* Caller must establish same-session replacement authority. Transfers all banks
|
||||
* without releasing references; a destination conflict leaves them unchanged. */
|
||||
int BPY_web_registration_owner_replace(Main *previous, Main *replacement);
|
||||
int BPY_app_handlers_web_owner_replace(Main *previous, Main *replacement);
|
||||
void BPY_app_timers_web_owner_replace(Main *previous, Main *replacement);
|
||||
/* Null owner denotes interpreter startup registrations, not a replaceable session. */
|
||||
void BPY_callback_web_owner_set(Main *owner, bool resetting);
|
||||
Main *BPY_callback_web_owner_get();
|
||||
bool BPY_callback_web_resetting();
|
||||
void BPY_callback_web_clear(bContext *C);
|
||||
void BPY_callback_web_clear(Main *owner);
|
||||
void BPY_app_timers_web_clear(Main *owner);
|
||||
void BPY_app_timers_web_file_load();
|
||||
/* Switch the actual bpy.app.handlers lists; cached lists retain their original owner. */
|
||||
int BPY_app_handlers_web_activate(Main *owner);
|
||||
/* Publish only after the original startup/extension registration pass succeeds. */
|
||||
int BPY_app_handlers_web_startup_publish();
|
||||
void BPY_app_handlers_web_clear(Main *owner);
|
||||
/* First desktop extension pass: journal only its native registration delta. */
|
||||
void BPY_web_startup_transaction_set(bool active);
|
||||
bool BPY_web_startup_transaction_active();
|
||||
void BPY_callback_web_startup_rollback();
|
||||
void BPY_app_timers_web_startup_rollback();
|
||||
void BPY_msgbus_web_startup_rollback();
|
||||
int pyrna_web_startup_rollback();
|
||||
int BPY_app_handlers_web_startup_begin();
|
||||
int BPY_app_handlers_web_startup_end(bool commit);
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
[[nodiscard]] PyObject *pyrna_callback_add(BPy_StructRNA *self, PyObject *args);
|
||||
[[nodiscard]] PyObject *pyrna_callback_remove(BPy_StructRNA *self, PyObject *args);
|
||||
|
||||
@@ -50,3 +50,6 @@ set(LIB
|
||||
|
||||
|
||||
blender_add_lib(bf_python_mathutils "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
|
||||
if(WITH_WEB)
|
||||
target_compile_definitions(bf_python_mathutils PRIVATE MATH_WEB_CORE_ONLY)
|
||||
endif()
|
||||
|
||||
@@ -863,6 +863,7 @@ PyMODINIT_FUNC PyInit_mathutils()
|
||||
PyModule_AddType(mod, &color_Type);
|
||||
|
||||
/* submodule */
|
||||
#ifndef MATH_WEB_CORE_ONLY
|
||||
PyModule_AddObject(mod, "geometry", (submodule = PyInit_mathutils_geometry()));
|
||||
/* XXX, python doesn't do imports with this usefully yet
|
||||
* 'from mathutils.geometry import PolyFill'
|
||||
@@ -889,6 +890,7 @@ PyMODINIT_FUNC PyInit_mathutils()
|
||||
PyC_Module_AddToSysModules(sys_modules, submodule);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
mathutils_matrix_row_cb_index = Mathutils_RegisterCallback(&mathutils_matrix_row_cb);
|
||||
mathutils_matrix_col_cb_index = Mathutils_RegisterCallback(&mathutils_matrix_col_cb);
|
||||
mathutils_matrix_translation_cb_index = Mathutils_RegisterCallback(
|
||||
|
||||
Reference in New Issue
Block a user