chore: checkpoint web Blender migration source, tools and docs
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

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:
mes123456
2026-11-10 11:18:01 +08:00
parent 122142c26c
commit f3e21c6b98
6887 changed files with 257154 additions and 12294 deletions

View File

@@ -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

View File

@@ -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` */

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View 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

View File

@@ -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);

View File

@@ -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());

View File

@@ -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;

View File

@@ -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

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View 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

View File

@@ -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 = &target; }
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 = &target;
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)

View File

@@ -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

View File

@@ -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);

View File

@@ -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()

View File

@@ -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(