chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -67,6 +67,31 @@ struct BPy_IDGroup_Iter {
|
||||
int len_init;
|
||||
/** Iterate in the reverse direction. */
|
||||
bool reversed;
|
||||
#ifdef WITH_WEB
|
||||
bool invalidated;
|
||||
#endif
|
||||
};
|
||||
|
||||
/** Track actual native allocations without retaining Python objects or running finalizers. */
|
||||
void BPy_IDProperty_Track(PyObject *object, IDProperty **property, IDProperty **parent = nullptr);
|
||||
void BPy_IDProperty_Untrack(PyObject *object);
|
||||
bool BPy_IDProperty_IsValid(const IDProperty *property);
|
||||
|
||||
/** Observe a native target across Python conversion, without retaining freed native memory.
|
||||
* Web guards also compare numeric array bytes before commit, including writes through buffers
|
||||
* exported before or during the conversion, and reject latched host cancellation/callback failure.
|
||||
* Native writable buffer semantics are preserved. */
|
||||
struct BPy_IDPropertyMutation {
|
||||
const IDProperty *property;
|
||||
bool changed = false;
|
||||
|
||||
/** Observe descendants when the conversion may replace the entire target tree. Container
|
||||
* membership guards keep observing only the container itself. */
|
||||
explicit BPy_IDPropertyMutation(const IDProperty *property, bool recursive = false);
|
||||
~BPy_IDPropertyMutation();
|
||||
BPy_IDPropertyMutation(const BPy_IDPropertyMutation &) = delete;
|
||||
BPy_IDPropertyMutation &operator=(const BPy_IDPropertyMutation &) = delete;
|
||||
bool check() const;
|
||||
};
|
||||
|
||||
/** Use to implement `IDPropertyGroup.keys/values/items` */
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
#include <Python.h>
|
||||
#include <climits>
|
||||
|
||||
#include "python_compat.hh" /* IWYU pragma: keep. */
|
||||
|
||||
@@ -18,6 +19,7 @@
|
||||
#include "idprop_py_ui_api.hh"
|
||||
|
||||
#include "BKE_idprop.hh"
|
||||
#include "idprop_py_api.hh"
|
||||
|
||||
#include "DNA_ID.h"
|
||||
|
||||
@@ -63,8 +65,8 @@ static bool idprop_ui_data_update_base(IDPropertyUIData *ui_data,
|
||||
static bool py_long_as_int(PyObject *py_long, int *r_int)
|
||||
{
|
||||
if (PyLong_CheckExact(py_long)) {
|
||||
*r_int = int(PyLong_AS_LONG(py_long));
|
||||
return true;
|
||||
*r_int = PyC_Long_AsI32(py_long);
|
||||
return !PyErr_Occurred();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -88,10 +90,16 @@ static bool try_parse_enum_item(PyObject *py_item, const int index, IDPropertyUI
|
||||
Py_ssize_t name_len;
|
||||
Py_ssize_t description_len;
|
||||
const char *identifier = PyUnicode_AsUTF8AndSize(PyTuple_GET_ITEM(py_item, 0), &identifier_len);
|
||||
if (!identifier) {
|
||||
return false;
|
||||
}
|
||||
const char *name = PyUnicode_AsUTF8AndSize(PyTuple_GET_ITEM(py_item, 1), &name_len);
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
const char *description = PyUnicode_AsUTF8AndSize(PyTuple_GET_ITEM(py_item, 2),
|
||||
&description_len);
|
||||
if (!identifier || !name || !description) {
|
||||
if (!description) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -106,10 +114,17 @@ static bool try_parse_enum_item(PyObject *py_item, const int index, IDPropertyUI
|
||||
}
|
||||
else if (item_size == 5) {
|
||||
/* Must have icon value or name. */
|
||||
if (!py_long_as_int(PyTuple_GET_ITEM(py_item, 3), &item.icon) &&
|
||||
!(icon_name = PyUnicode_AsUTF8(PyTuple_GET_ITEM(py_item, 3))))
|
||||
{
|
||||
return false;
|
||||
PyObject *icon = PyTuple_GET_ITEM(py_item, 3);
|
||||
if (PyLong_CheckExact(icon)) {
|
||||
if (!py_long_as_int(icon, &item.icon)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
icon_name = PyUnicode_AsUTF8(icon);
|
||||
if (!icon_name) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!py_long_as_int(PyTuple_GET_ITEM(py_item, 4), &item.value)) {
|
||||
return false;
|
||||
@@ -133,6 +148,10 @@ static IDPropertyUIDataEnumItem *idprop_enum_items_from_py(PyObject *seq_fast, i
|
||||
PyObject **seq_fast_items = PySequence_Fast_ITEMS(seq_fast);
|
||||
int i;
|
||||
|
||||
if (seq_len > INT_MAX) {
|
||||
PyErr_SetString(PyExc_OverflowError, "Too many IDProperty enum items");
|
||||
return nullptr;
|
||||
}
|
||||
items = MEM_new_array<IDPropertyUIDataEnumItem>(seq_len, __func__);
|
||||
r_items_num = seq_len;
|
||||
|
||||
@@ -143,14 +162,21 @@ static IDPropertyUIDataEnumItem *idprop_enum_items_from_py(PyObject *seq_fast, i
|
||||
items[i] = item;
|
||||
}
|
||||
else if (py_item == Py_None) {
|
||||
items[i].identifier = nullptr;
|
||||
items[i] = {};
|
||||
}
|
||||
else {
|
||||
for (int j = 0; j < i; j++) {
|
||||
MEM_SAFE_DELETE(items[j].identifier);
|
||||
MEM_SAFE_DELETE(items[j].name);
|
||||
MEM_SAFE_DELETE(items[j].description);
|
||||
}
|
||||
MEM_delete(items);
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"expected a tuple containing "
|
||||
"(identifier, name, description) and optionally an "
|
||||
"icon name and unique number");
|
||||
if (!PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"expected a tuple containing "
|
||||
"(identifier, name, description) and optionally an "
|
||||
"icon name and unique number");
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
@@ -172,23 +198,33 @@ static bool idprop_ui_data_update_int_default(IDProperty *idprop,
|
||||
return false;
|
||||
}
|
||||
|
||||
Py_ssize_t len = PySequence_Size(default_value);
|
||||
PyObject *values = PySequence_Tuple(default_value);
|
||||
if (!values) {
|
||||
return false;
|
||||
}
|
||||
const Py_ssize_t len = PyTuple_GET_SIZE(values);
|
||||
if (len > INT_MAX) {
|
||||
Py_DECREF(values);
|
||||
PyErr_SetString(PyExc_OverflowError, "IDProperty default array is too large");
|
||||
return false;
|
||||
}
|
||||
int *new_default_array = MEM_new_array_uninitialized<int>(size_t(len), __func__);
|
||||
if (PyC_AsArray(
|
||||
new_default_array, sizeof(int), default_value, len, &PyLong_Type, "ui_data_update") ==
|
||||
new_default_array, sizeof(int), values, len, &PyLong_Type, "ui_data_update") ==
|
||||
-1)
|
||||
{
|
||||
Py_DECREF(values);
|
||||
MEM_delete(new_default_array);
|
||||
return false;
|
||||
}
|
||||
|
||||
ui_data->default_array_len = len;
|
||||
Py_DECREF(values);
|
||||
ui_data->default_array_len = int(len);
|
||||
ui_data->default_array = new_default_array;
|
||||
}
|
||||
else {
|
||||
const int value = PyC_Long_AsI32(default_value);
|
||||
if ((value == -1) && PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_ValueError, "Cannot convert \"default\" argument to integer");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -304,8 +340,10 @@ static bool idprop_ui_data_update_int(IDProperty *idprop, PyObject *args, PyObje
|
||||
|
||||
if (!ELEM(items, nullptr, Py_None)) {
|
||||
PyObject *items_fast;
|
||||
if (!(items_fast = PySequence_Fast(items, "expected a sequence of tuples for the enum items")))
|
||||
if (!(items_fast = PySequence_Tuple(items)))
|
||||
{
|
||||
IDP_ui_data_free_unique_contents(
|
||||
&ui_data.base, IDP_ui_data_type(idprop), &ui_data_orig->base);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -317,7 +355,9 @@ static bool idprop_ui_data_update_int(IDProperty *idprop, PyObject *args, PyObje
|
||||
const bool has_error = (idprop_items == nullptr) ||
|
||||
!IDP_EnumItemsValidate(
|
||||
idprop_items, idprop_items_num, [](const char *msg) {
|
||||
PyErr_SetString(PyExc_ValueError, msg);
|
||||
if (!PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_ValueError, msg);
|
||||
}
|
||||
});
|
||||
Py_DECREF(items_fast);
|
||||
if (idprop_items != nullptr) {
|
||||
@@ -358,26 +398,36 @@ static bool idprop_ui_data_update_bool_default(IDProperty *idprop,
|
||||
return false;
|
||||
}
|
||||
|
||||
Py_ssize_t len = PySequence_Size(default_value);
|
||||
PyObject *values = PySequence_Tuple(default_value);
|
||||
if (!values) {
|
||||
return false;
|
||||
}
|
||||
const Py_ssize_t len = PyTuple_GET_SIZE(values);
|
||||
if (len > INT_MAX) {
|
||||
Py_DECREF(values);
|
||||
PyErr_SetString(PyExc_OverflowError, "IDProperty default array is too large");
|
||||
return false;
|
||||
}
|
||||
int8_t *new_default_array = MEM_new_array_uninitialized<int8_t>(size_t(len), __func__);
|
||||
if (PyC_AsArray(new_default_array,
|
||||
sizeof(int8_t),
|
||||
default_value,
|
||||
values,
|
||||
len,
|
||||
&PyBool_Type,
|
||||
"ui_data_update") == -1)
|
||||
{
|
||||
Py_DECREF(values);
|
||||
MEM_delete(new_default_array);
|
||||
return false;
|
||||
}
|
||||
|
||||
ui_data->default_array_len = len;
|
||||
Py_DECREF(values);
|
||||
ui_data->default_array_len = int(len);
|
||||
ui_data->default_array = new_default_array;
|
||||
}
|
||||
else {
|
||||
const int value = PyC_Long_AsBool(default_value);
|
||||
if ((value == -1) && PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_ValueError, "Cannot convert \"default\" argument to integer");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -448,26 +498,36 @@ static bool idprop_ui_data_update_float_default(IDProperty *idprop,
|
||||
return false;
|
||||
}
|
||||
|
||||
Py_ssize_t len = PySequence_Size(default_value);
|
||||
PyObject *values = PySequence_Tuple(default_value);
|
||||
if (!values) {
|
||||
return false;
|
||||
}
|
||||
const Py_ssize_t len = PyTuple_GET_SIZE(values);
|
||||
if (len > INT_MAX) {
|
||||
Py_DECREF(values);
|
||||
PyErr_SetString(PyExc_OverflowError, "IDProperty default array is too large");
|
||||
return false;
|
||||
}
|
||||
double *new_default_array = MEM_new_array_uninitialized<double>(size_t(len), __func__);
|
||||
if (PyC_AsArray(new_default_array,
|
||||
sizeof(double),
|
||||
default_value,
|
||||
values,
|
||||
len,
|
||||
&PyFloat_Type,
|
||||
"ui_data_update") == -1)
|
||||
{
|
||||
Py_DECREF(values);
|
||||
MEM_delete(new_default_array);
|
||||
return false;
|
||||
}
|
||||
|
||||
ui_data->default_array_len = len;
|
||||
Py_DECREF(values);
|
||||
ui_data->default_array_len = int(len);
|
||||
ui_data->default_array = new_default_array;
|
||||
}
|
||||
else {
|
||||
const double value = PyFloat_AsDouble(default_value);
|
||||
if ((value == -1.0) && PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_ValueError, "Cannot convert \"default\" argument to double");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -665,6 +725,8 @@ static bool idprop_ui_data_update_id(IDProperty *idprop, PyObject *args, PyObjec
|
||||
if (pyrna_enum_value_from_id(
|
||||
rna_enum_id_type_items, id_type, &id_type_tmp, "IDPropertyUIManager.update") == -1)
|
||||
{
|
||||
IDP_ui_data_free_unique_contents(
|
||||
&ui_data.base, IDP_ui_data_type(idprop), &ui_data_orig->base);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -700,46 +762,59 @@ static PyObject *BPy_IDPropertyUIManager_update(BPy_IDPropertyUIManager *self,
|
||||
PyObject *args,
|
||||
PyObject *kwargs)
|
||||
{
|
||||
IDProperty *property = self->property;
|
||||
BLI_assert(IDP_ui_data_supported(property));
|
||||
|
||||
switch (IDP_ui_data_type(property)) {
|
||||
case IDP_UI_DATA_TYPE_INT:
|
||||
IDP_ui_data_ensure(property);
|
||||
if (!idprop_ui_data_update_int(property, args, kwargs)) {
|
||||
return nullptr;
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
case IDP_UI_DATA_TYPE_BOOLEAN:
|
||||
IDP_ui_data_ensure(property);
|
||||
if (!idprop_ui_data_update_bool(property, args, kwargs)) {
|
||||
return nullptr;
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
case IDP_UI_DATA_TYPE_FLOAT:
|
||||
IDP_ui_data_ensure(property);
|
||||
if (!idprop_ui_data_update_float(property, args, kwargs)) {
|
||||
return nullptr;
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
case IDP_UI_DATA_TYPE_STRING:
|
||||
IDP_ui_data_ensure(property);
|
||||
if (!idprop_ui_data_update_string(property, args, kwargs)) {
|
||||
return nullptr;
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
case IDP_UI_DATA_TYPE_ID:
|
||||
IDP_ui_data_ensure(property);
|
||||
if (!idprop_ui_data_update_id(property, args, kwargs)) {
|
||||
return nullptr;
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
case IDP_UI_DATA_TYPE_UNSUPPORTED:
|
||||
PyErr_Format(PyExc_TypeError, "IDProperty \"%s\" does not support RNA data", property->name);
|
||||
return nullptr;
|
||||
if (!BPy_IDProperty_IsValid(self->property)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BLI_assert_unreachable();
|
||||
IDProperty *property = self->property;
|
||||
BPy_IDPropertyMutation target(property);
|
||||
/* Argument conversion runs arbitrary Python. All helpers operate on private UI data,
|
||||
* including their shallow intermediate copies; the live property is never borrowed. */
|
||||
IDProperty staged = *property;
|
||||
staged.ui_data = property->ui_data ? IDP_ui_data_copy(property) : nullptr;
|
||||
if (!IDP_ui_data_supported(&staged)) {
|
||||
PyErr_Format(PyExc_TypeError, "IDProperty \"%s\" does not support RNA data", staged.name);
|
||||
return nullptr;
|
||||
}
|
||||
IDP_ui_data_ensure(&staged);
|
||||
PyObject *keywords = kwargs ? PyDict_Copy(kwargs) : nullptr;
|
||||
if (kwargs && !keywords) {
|
||||
IDP_ui_data_free(&staged);
|
||||
return nullptr;
|
||||
}
|
||||
bool ok = false;
|
||||
switch (IDP_ui_data_type(&staged)) {
|
||||
case IDP_UI_DATA_TYPE_INT:
|
||||
ok = idprop_ui_data_update_int(&staged, args, keywords);
|
||||
break;
|
||||
case IDP_UI_DATA_TYPE_BOOLEAN:
|
||||
ok = idprop_ui_data_update_bool(&staged, args, keywords);
|
||||
break;
|
||||
case IDP_UI_DATA_TYPE_FLOAT:
|
||||
ok = idprop_ui_data_update_float(&staged, args, keywords);
|
||||
break;
|
||||
case IDP_UI_DATA_TYPE_STRING:
|
||||
ok = idprop_ui_data_update_string(&staged, args, keywords);
|
||||
break;
|
||||
case IDP_UI_DATA_TYPE_ID:
|
||||
ok = idprop_ui_data_update_id(&staged, args, keywords);
|
||||
break;
|
||||
case IDP_UI_DATA_TYPE_UNSUPPORTED:
|
||||
BLI_assert_unreachable();
|
||||
break;
|
||||
}
|
||||
/* Release temporary Python owners before the last native identity check. */
|
||||
Py_XDECREF(keywords);
|
||||
if (!ok || !target.check()) {
|
||||
IDP_ui_data_free(&staged);
|
||||
return nullptr;
|
||||
}
|
||||
if (property->ui_data) {
|
||||
IDP_ui_data_free(property);
|
||||
}
|
||||
property->ui_data = staged.ui_data;
|
||||
#ifdef WITH_WEB
|
||||
IDPropertyLifetime::changed(property);
|
||||
#endif
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
@@ -893,6 +968,9 @@ PyDoc_STRVAR(
|
||||
" returned dictionary and their types will depend on the property's type.\n");
|
||||
static PyObject *BPy_IDIDPropertyUIManager_as_dict(BPy_IDPropertyUIManager *self)
|
||||
{
|
||||
if (!BPy_IDProperty_IsValid(self->property)) {
|
||||
return nullptr;
|
||||
}
|
||||
IDProperty *property = self->property;
|
||||
BLI_assert(IDP_ui_data_supported(property));
|
||||
|
||||
@@ -955,6 +1033,9 @@ PyDoc_STRVAR(
|
||||
" Remove the RNA UI data from this IDProperty.\n");
|
||||
static PyObject *BPy_IDPropertyUIManager_clear(BPy_IDPropertyUIManager *self)
|
||||
{
|
||||
if (!BPy_IDProperty_IsValid(self->property)) {
|
||||
return nullptr;
|
||||
}
|
||||
IDProperty *property = self->property;
|
||||
BLI_assert(IDP_ui_data_supported(property));
|
||||
|
||||
@@ -988,6 +1069,9 @@ PyDoc_STRVAR(
|
||||
" :raises TypeError: If the types of the two properties don't match.\n");
|
||||
static PyObject *BPy_IDPropertyUIManager_update_from(BPy_IDPropertyUIManager *self, PyObject *args)
|
||||
{
|
||||
if (!BPy_IDProperty_IsValid(self->property)) {
|
||||
return nullptr;
|
||||
}
|
||||
IDProperty *property = self->property;
|
||||
BLI_assert(IDP_ui_data_supported(property));
|
||||
|
||||
@@ -997,6 +1081,12 @@ static PyObject *BPy_IDPropertyUIManager_update_from(BPy_IDPropertyUIManager *se
|
||||
}
|
||||
|
||||
IDProperty *src_prop = ui_manager_src->property;
|
||||
if (!BPy_IDProperty_IsValid(src_prop)) {
|
||||
return nullptr;
|
||||
}
|
||||
if (src_prop == property) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
if ((property->type != src_prop->type) || property->subtype != src_prop->subtype) {
|
||||
PyErr_SetString(PyExc_TypeError, "Properties type does not match.");
|
||||
return nullptr;
|
||||
@@ -1009,6 +1099,9 @@ static PyObject *BPy_IDPropertyUIManager_update_from(BPy_IDPropertyUIManager *se
|
||||
if (ui_manager_src->property && ui_manager_src->property->ui_data) {
|
||||
property->ui_data = IDP_ui_data_copy(src_prop);
|
||||
}
|
||||
#ifdef WITH_WEB
|
||||
IDPropertyLifetime::changed(property);
|
||||
#endif
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
@@ -1059,13 +1152,22 @@ static PyMethodDef BPy_IDPropertyUIManager_methods[] = {
|
||||
|
||||
static PyObject *BPy_IDPropertyUIManager_repr(BPy_IDPropertyUIManager *self)
|
||||
{
|
||||
if (!BPy_IDProperty_IsValid(self->property)) {
|
||||
return nullptr;
|
||||
}
|
||||
return PyUnicode_FromFormat(
|
||||
"<bpy id prop ui manager: name=\"%s\", address=%p>", self->property->name, self->property);
|
||||
}
|
||||
|
||||
static Py_hash_t BPy_IDPropertyUIManager_hash(BPy_IDPropertyUIManager *self)
|
||||
{
|
||||
return Py_HashPointer(self->property);
|
||||
return Py_HashPointer(self);
|
||||
}
|
||||
|
||||
static void BPy_IDPropertyUIManager_dealloc(PyObject *self)
|
||||
{
|
||||
BPy_IDProperty_Untrack(self);
|
||||
PyObject_Del(self);
|
||||
}
|
||||
|
||||
PyTypeObject BPy_IDPropertyUIManager_Type = {
|
||||
@@ -1074,7 +1176,7 @@ PyTypeObject BPy_IDPropertyUIManager_Type = {
|
||||
/*tp_name*/ "IDPropertyUIManager",
|
||||
/*tp_basicsize*/ sizeof(BPy_IDPropertyUIManager),
|
||||
/*tp_itemsize*/ 0,
|
||||
/*tp_dealloc*/ nullptr,
|
||||
/*tp_dealloc*/ BPy_IDPropertyUIManager_dealloc,
|
||||
/*tp_vectorcall_offset*/ 0,
|
||||
/*tp_getattr*/ nullptr,
|
||||
/*tp_setattr*/ nullptr,
|
||||
|
||||
@@ -154,10 +154,11 @@ int pyrna_enum_bitfield_from_set(const EnumPropertyItem *items,
|
||||
if (PySet_GET_SIZE(value) > 0) {
|
||||
/* Set looping. */
|
||||
PyObject *it = PyObject_GetIter(value);
|
||||
if (!it) {
|
||||
return -1;
|
||||
}
|
||||
while ((key = PyIter_Next(it))) {
|
||||
/* Borrow from the set. */
|
||||
Py_DECREF(key);
|
||||
|
||||
/* A set subclass may yield values that the set itself does not own. */
|
||||
const char *param = PyUnicode_AsUTF8(key);
|
||||
if (param == nullptr) {
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
@@ -173,9 +174,12 @@ int pyrna_enum_bitfield_from_set(const EnumPropertyItem *items,
|
||||
}
|
||||
|
||||
flag |= ret;
|
||||
Py_CLEAR(key);
|
||||
}
|
||||
const bool failed = key != nullptr || PyErr_Occurred();
|
||||
Py_XDECREF(key);
|
||||
Py_DECREF(it);
|
||||
if (key) {
|
||||
if (failed || PyErr_Occurred()) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user