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

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